Skip to main content

raster_style/
codec.rs

1use std::collections::HashSet;
2use std::sync::OnceLock;
3
4use serde::Deserialize;
5use serde_json::{Map, Number, Value};
6
7use crate::error::{Error, Result};
8use crate::json::{canonical_value, parse_strict};
9use crate::schema::default_schemas;
10use crate::style::Style;
11
12#[derive(Debug, Clone, Copy)]
13pub struct Options {
14    pub max_query_bytes: usize,
15    pub max_parameters: usize,
16}
17impl Default for Options {
18    fn default() -> Self {
19        Self {
20            max_query_bytes: 8192,
21            max_parameters: 256,
22        }
23    }
24}
25
26#[derive(Debug, Clone, Default)]
27pub struct Codec {
28    options: Options,
29}
30
31#[derive(Deserialize)]
32struct Binding {
33    key: String,
34    path: String,
35    codec: String,
36}
37#[derive(Deserialize)]
38struct Registry {
39    bindings: Vec<Binding>,
40}
41static BINDINGS: OnceLock<std::result::Result<Vec<Binding>, String>> = OnceLock::new();
42
43fn bindings() -> Result<&'static [Binding]> {
44    BINDINGS
45        .get_or_init(|| {
46            let mut registry: Registry =
47                serde_json::from_str(include_str!("../assets/query-bindings-v3.json"))
48                    .map_err(|error| error.to_string())?;
49            registry
50                .bindings
51                .sort_by(|left, right| left.key.cmp(&right.key));
52            Ok(registry.bindings)
53        })
54        .as_deref()
55        .map_err(|_| Error::new("E_CONFIG", "Invalid embedded binding registry"))
56}
57
58fn escape_component(text: &str) -> String {
59    const HEX: &[u8; 16] = b"0123456789ABCDEF";
60    let mut output = String::with_capacity(text.len());
61    for &byte in text.as_bytes() {
62        if byte.is_ascii_alphanumeric() || b"-._~".contains(&byte) {
63            output.push(byte as char);
64        } else {
65            output.push('%');
66            output.push(HEX[(byte >> 4) as usize] as char);
67            output.push(HEX[(byte & 15) as usize] as char);
68        }
69    }
70    output
71}
72
73fn unescape_component(text: &str) -> Result<String> {
74    let bytes = text.as_bytes();
75    let mut output = Vec::with_capacity(bytes.len());
76    let mut index = 0;
77    while index < bytes.len() {
78        let byte = bytes[index];
79        if !(0x21..=0x7e).contains(&byte) || byte == b'+' || byte == b'#' {
80            return Err(Error::new(
81                "E_QUERY_SYNTAX",
82                "Query components must be URI-encoded ASCII",
83            ));
84        }
85        if byte == b'%' {
86            let hex = bytes
87                .get(index + 1..index + 3)
88                .ok_or_else(|| Error::new("E_QUERY_SYNTAX", "Incomplete percent escape"))?;
89            let high = (hex[0] as char).to_digit(16);
90            let low = (hex[1] as char).to_digit(16);
91            match (high, low) {
92                (Some(high), Some(low)) => output.push((high * 16 + low) as u8),
93                _ => return Err(Error::new("E_QUERY_SYNTAX", "Invalid percent escape")),
94            }
95            index += 3;
96        } else {
97            output.push(byte);
98            index += 1;
99        }
100    }
101    String::from_utf8(output)
102        .map_err(|_| Error::new("E_QUERY_SYNTAX", "Invalid UTF-8 query component"))
103}
104
105fn numeric(text: &str, integer: bool) -> Result<Value> {
106    if text.bytes().any(|byte| byte.is_ascii_whitespace()) {
107        return Err(Error::new("E_QUERY_SYNTAX", "Expected a numeric token"));
108    }
109    let number: f64 = serde_json::from_str(text)
110        .map_err(|_| Error::new("E_QUERY_SYNTAX", "Expected a finite numeric token"))?;
111    if !number.is_finite()
112        || (integer && (number.fract() != 0.0 || number.abs() > 9_007_199_254_740_991.0))
113    {
114        return Err(Error::new(
115            "E_QUERY_SYNTAX",
116            "Expected a finite numeric token",
117        ));
118    }
119    Ok(Value::Number(Number::from_f64(number).ok_or_else(
120        || Error::new("E_QUERY_SYNTAX", "Non-finite number"),
121    )?))
122}
123
124fn encode_value(value: &Value, codec: &str) -> Result<String> {
125    match codec {
126        "formula" | "post_formula" => crate::profile::encode_formula(value),
127        "nodata" => {
128            if value == "nan" {
129                Ok("nan".into())
130            } else {
131                canonical_value(value)
132            }
133        }
134        "string" => Ok(value.as_str().expect("schema-validated string").into()),
135        "color" => Ok(value.as_str().expect("schema-validated color")[1..].into()),
136        "boolean" | "integer" | "number" | "json" => canonical_value(value),
137        "pair" => Ok(format!(
138            "{},{}",
139            canonical_value(&value[0])?,
140            canonical_value(&value[1])?
141        )),
142        _ => Err(Error::new("E_CONFIG", "Unknown binding codec")),
143    }
144}
145
146fn decode_value(text: &str, codec: &str) -> Result<Value> {
147    match codec {
148        "formula" | "post_formula" => crate::profile::decode_formula(text, codec == "post_formula"),
149        "nodata" => {
150            if text == "nan" {
151                Ok(Value::String("nan".into()))
152            } else {
153                numeric(text, false)
154            }
155        }
156        "string" => Ok(Value::String(text.into())),
157        "number" => numeric(text, false),
158        "integer" => numeric(text, true),
159        "boolean" => match text {
160            "true" => Ok(Value::Bool(true)),
161            "false" => Ok(Value::Bool(false)),
162            _ => Err(Error::new("E_QUERY_SYNTAX", "Expected true or false")),
163        },
164        "color" => {
165            if text.len() != 8 || !text.bytes().all(|byte| byte.is_ascii_hexdigit()) {
166                return Err(Error::new("E_QUERY_SYNTAX", "Expected 8-digit RGBA"));
167            }
168            Ok(Value::String(format!("#{}", text.to_ascii_lowercase())))
169        }
170        "json" => parse_strict(text),
171        "pair" => {
172            let parts: Vec<_> = text.split(',').collect();
173            if parts.len() != 2 {
174                return Err(Error::new("E_QUERY_SYNTAX", "Expected a numeric pair"));
175            }
176            Ok(Value::Array(vec![
177                numeric(parts[0], false)?,
178                numeric(parts[1], false)?,
179            ]))
180        }
181        _ => Err(Error::new("E_CONFIG", "Unknown binding codec")),
182    }
183}
184
185fn get_path<'a>(value: &'a Value, path: &str) -> Option<&'a Value> {
186    let mut current = value;
187    for part in path.split('.') {
188        current = current.get(part)?;
189    }
190    Some(current)
191}
192fn set_path(value: &mut Value, path: &str, item: Value) {
193    let parts: Vec<_> = path.split('.').collect();
194    let mut current = value;
195    for &part in &parts[..parts.len() - 1] {
196        current = current
197            .as_object_mut()
198            .expect("registry-controlled path")
199            .entry(part)
200            .or_insert_with(|| Value::Object(Map::new()));
201    }
202    current
203        .as_object_mut()
204        .expect("registry-controlled path")
205        .insert(parts[parts.len() - 1].into(), item);
206}
207
208impl Codec {
209    pub fn new(options: Options) -> Result<Self> {
210        if options.max_query_bytes == 0 || options.max_parameters == 0 {
211            return Err(Error::new("E_CONFIG", "Budgets must be positive"));
212        }
213        default_schemas()?;
214        bindings()?;
215        Ok(Self { options })
216    }
217
218    pub fn encode_query(&self, style: &Style) -> Result<String> {
219        let mut pairs = Vec::new();
220        let core = default_schemas()?;
221        let fields: Map<String, Value> = style
222            .as_value()
223            .as_object()
224            .expect("validated style")
225            .iter()
226            .filter(|(key, _)| !core.is_core_field(key))
227            .map(|(key, value)| (key.clone(), value.clone()))
228            .collect();
229        let fields = if fields.is_empty() {
230            None
231        } else {
232            Some(Value::Object(fields))
233        };
234        for binding in bindings()? {
235            let Some(value) = (if binding.path == "*" {
236                fields.as_ref()
237            } else {
238                get_path(style.as_value(), &binding.path)
239            }) else {
240                continue;
241            };
242            let repeated = binding.codec.starts_with("repeat_");
243            let values = if repeated {
244                value.as_array().expect("schema-validated array").as_slice()
245            } else {
246                std::slice::from_ref(value)
247            };
248            for item in values {
249                let encoded = encode_value(item, binding.codec.trim_start_matches("repeat_"))?;
250                pairs.push(format!(
251                    "{}={}",
252                    escape_component(&binding.key),
253                    escape_component(&encoded)
254                ));
255                if pairs.len() > self.options.max_parameters {
256                    return Err(Error::new("E_LIMIT", "Query parameter budget exceeded"));
257                }
258            }
259        }
260        let query = pairs.join("&");
261        if query.len() > self.options.max_query_bytes {
262            return Err(Error::new(
263                "E_LIMIT",
264                "Inline query is too long; use a style reference",
265            ));
266        }
267        Ok(query)
268    }
269
270    pub fn decode_query(&self, query: &str) -> Result<Style> {
271        let raw = query.strip_prefix('?').unwrap_or(query);
272        if raw.len() > self.options.max_query_bytes {
273            return Err(Error::new("E_LIMIT", "Query byte budget exceeded"));
274        }
275        let parts: Vec<_> = if raw.is_empty() {
276            Vec::new()
277        } else {
278            raw.split('&').collect()
279        };
280        if parts.len() > self.options.max_parameters {
281            return Err(Error::new("E_LIMIT", "Query parameter budget exceeded"));
282        }
283        let registry = bindings()?;
284        let mut document = Value::Object(Map::new());
285        let mut seen = HashSet::new();
286        for part in parts {
287            let (raw_key, raw_value) = part
288                .split_once('=')
289                .filter(|(key, _)| !key.is_empty())
290                .ok_or_else(|| Error::new("E_QUERY_SYNTAX", "Expected key=value"))?;
291            let key = unescape_component(raw_key)?;
292            let value = unescape_component(raw_value)?;
293            let binding = registry
294                .iter()
295                .find(|binding| binding.key == key)
296                .ok_or_else(|| {
297                    Error::new("E_UNKNOWN_PARAMETER", "Unknown Q3 query parameter").at(&key)
298                })?;
299            let repeated = binding.codec.starts_with("repeat_");
300            if !seen.insert(key.clone()) && !repeated {
301                return Err(
302                    Error::new("E_DUPLICATE_PARAMETER", "Duplicate singleton parameter").at(key),
303                );
304            }
305            let decoded = decode_value(&value, binding.codec.trim_start_matches("repeat_"))?;
306            if binding.path == "*" {
307                let Value::Object(fields) = decoded else {
308                    return Err(Error::new(
309                        "E_QUERY_SYNTAX",
310                        "extension_fields must be a JSON object",
311                    ));
312                };
313                for (field, item) in fields {
314                    if default_schemas()?.is_core_field(&field) {
315                        return Err(Error::new(
316                            "E_QUERY_SYNTAX",
317                            "extension_fields cannot contain core fields",
318                        )
319                        .at(field));
320                    }
321                    document
322                        .as_object_mut()
323                        .expect("query object")
324                        .insert(field, item);
325                }
326            } else if repeated {
327                if get_path(&document, &binding.path).is_none() {
328                    set_path(&mut document, &binding.path, Value::Array(Vec::new()));
329                }
330                let pointer = format!("/{}", binding.path.replace('.', "/"));
331                document
332                    .pointer_mut(&pointer)
333                    .and_then(Value::as_array_mut)
334                    .expect("registry-controlled array")
335                    .push(decoded);
336            } else {
337                set_path(&mut document, &binding.path, decoded);
338            }
339        }
340        Style::from_validated_value(document)
341    }
342
343    pub fn json_to_query(&self, json: &str) -> Result<String> {
344        self.encode_query(&Style::from_json(json)?)
345    }
346    pub fn query_to_json(&self, query: &str) -> Result<String> {
347        self.decode_query(query)?.to_json()
348    }
349}
350
351pub fn encode_query(style: &Style) -> Result<String> {
352    Codec::default().encode_query(style)
353}
354pub fn decode_query(query: &str) -> Result<Style> {
355    Codec::default().decode_query(query)
356}
357pub fn json_to_query(json: &str) -> Result<String> {
358    Codec::default().json_to_query(json)
359}
360pub fn query_to_json(query: &str) -> Result<String> {
361    Codec::default().query_to_json(query)
362}