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}