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}