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