Skip to main content

autumn_web/
query_string.rs

1//! Structured query-string decoding for [`Query<T>`](crate::extract::Query)
2//! (issue #1972).
3//!
4//! See the [extractors guide](https://github.com/autumn-foundation/autumn/blob/trunk/docs/guide/extractors.md)
5//! for the narrative version, with worked examples.
6//!
7//! # Why this exists
8//!
9//! `Query<T>` used to delegate straight to
10//! [`serde_urlencoded`](https://docs.rs/serde_urlencoded), which is **strictly
11//! flat**: it decodes `?q=foo&page=2` and nothing else. A `Vec<String>` field
12//! fed the repeated-key form `?tags=a&tags=b` failed with `invalid type: string
13//! "a", expected a sequence`, and a nested struct field was unrepresentable by
14//! any encoding. That left MCP tool contracts unhonorable — `tools/call`
15//! dispatch renders an array query argument as repeated keys, so a tool whose
16//! `inputSchema` advertised `tags: array` dispatched a request its own handler
17//! rejected — and pushed builders onto comma-separated strings and
18//! JSON-in-a-string fields.
19//!
20//! # Wire format
21//!
22//! The decoder is a **superset** of the flat form: a query string with unique
23//! scalar keys decodes exactly as it did before. On top of that it accepts a
24//! bracketed dialect whose `items[0][sku]` shape matches the repeated-row
25//! encoding [`nested_form`](crate::nested_form) renders — generalized here to
26//! arbitrary objects, sequences and depths. Note this applies to the **query
27//! string only**. Request *bodies* use flat keys: [`Form<T>`](crate::extract::Form)
28//! decodes through `serde_urlencoded`, and
29//! [`ChangesetForm`](crate::form::ChangesetForm) and
30//! [`NestedChangesetForm`](crate::nested_form::NestedChangesetForm) decode
31//! through [`from_urlencoded`](crate::form::from_urlencoded) (repeated keys
32//! fill a `Vec`, brackets stay part of the key).
33//!
34//! ```text
35//! q=foo                       // scalar
36//! page=2                      // scalar, coerced to the field's type
37//! tags=a&tags=b               // repeated key   → sequence ["a", "b"]
38//! tags[]=a&tags[]=b           // append form    → sequence ["a", "b"]
39//! tags[0]=a&tags[2]=c         // indexed form   → sequence ["a", "c"]
40//! filter[status]=open         // nested object  → { status: "open" }
41//! items[0][sku]=A-1           // array of objects
42//! ```
43//!
44//! Percent-encoded brackets (`%5B` / `%5D`) are the same thing: keys are
45//! percent-decoded before parsing, so a client that encodes them round-trips
46//! identically.
47//!
48//! # Deliberate semantics
49//!
50//! * **Scalar coercion matches `serde_urlencoded`.** Values arrive as text and
51//!   are parsed with [`str::parse`] into the field's type, so `page=2` fills a
52//!   `u32` and `flag=true` fills a `bool`. An `Option<T>` field that is
53//!   *present but empty* (`?page=`) still visits `Some`, exactly as
54//!   `serde_urlencoded` does — only an **absent** key is `None`.
55//! * **A duplicated key is an error in a single-valued position.** `?q=a&q=b`
56//!   against a `String` field is rejected — `serde_urlencoded` + serde's derive
57//!   rejected it too (`duplicate field`), and quietly picking one of two values
58//!   is how parameter-pollution bugs are built. A **sequence** field takes every
59//!   occurrence; that is the whole point of the repeated-key form.
60//! * **Errors never echo a value.** A decode failure names the field path and
61//!   the expected type, never the submitted text — the message is returned in
62//!   the 400 body and recorded by error reporters, and a query parameter can
63//!   hold a secret.
64//! * **Shape conflicts poison one key, not the request.**
65//!   `?filter=flat&filter[status]=open` uses one name as both a scalar and a
66//!   container. That key is rejected *if the target claims it*; a target that
67//!   ignores it (ad-tracking junk, a crawler's garbage parameter) still decodes,
68//!   exactly as it did when the key was merely unrecognised.
69//! * **Malformed brackets stay literal.** A key the bracket grammar cannot
70//!   parse (`weird[unclosed`) is used verbatim as a flat key, so a stray
71//!   bracket never turns into a parse failure.
72//! * **Nesting is depth-capped** at [`MAX_DEPTH`] and indices key an ordered
73//!   map rather than a `Vec`, so neither deep nesting nor a huge index
74//!   (`tags[4000000000]=x`) lets a request drive unbounded allocation.
75//!
76//! * **Map iteration is key-ordered.** The tree keys each level with a
77//!   `BTreeMap`, so a `Query<Vec<(String, String)>>` target sees pairs sorted
78//!   by key rather than in submission order (occurrences of one key keep their
79//!   order). Deterministic either way, but not the wire order.
80//!
81//! # Compatibility note
82//!
83//! Brackets are now *structure*, not part of the key text. A target that types
84//! a parameter as a plain value — `Query<HashMap<String, String>>` is the
85//! common one — used to receive `?filter[a]=1` as the literal key
86//! `"filter[a]"`; it now sees a nested object and reports a decode error
87//! naming the fix. Type such a field as a nested struct, or as
88//! `HashMap<String, serde_json::Value>` to accept either shape.
89
90// autumn-panic-gate: request-path module — production code path must be panic-free.
91// See CONTRIBUTING.md "Request-path panic gate". Justify exceptions with
92// #[allow(clippy::<lint>, reason = "…")] at the narrowest scope.
93#![cfg_attr(
94    not(test),
95    deny(
96        clippy::unwrap_used,
97        clippy::expect_used,
98        clippy::panic,
99        clippy::unreachable,
100        clippy::todo,
101        clippy::unimplemented,
102        clippy::indexing_slicing,
103        clippy::string_slice,
104        clippy::arithmetic_side_effects,
105    )
106)]
107
108use std::collections::BTreeMap;
109use std::fmt;
110
111use serde::de::{
112    self, DeserializeOwned, DeserializeSeed, EnumAccess, MapAccess, SeqAccess, VariantAccess,
113    Visitor,
114};
115
116/// Maximum bracket nesting depth accepted in a query key.
117///
118/// `a[b][c]` is depth 3. Anything deeper is rejected as a decode error rather
119/// than allocated, so a hostile query string cannot drive unbounded tree
120/// construction. Real nested query arguments are one or two levels deep.
121pub const MAX_DEPTH: usize = 16;
122
123// ──────────────────────────────────────────────────────────────────
124// Error
125// ──────────────────────────────────────────────────────────────────
126
127/// A query-string decode failure, carrying the field path it occurred at.
128///
129/// Rendered as `filter.limit: invalid digit found in string` so a caller sees
130/// *which* parameter failed, not just that something did.
131#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct QueryError {
133    /// Field path from the root of the query object, outermost first.
134    path: Vec<String>,
135    message: String,
136}
137
138impl QueryError {
139    fn msg(message: impl Into<String>) -> Self {
140        Self {
141            path: Vec::new(),
142            message: message.into(),
143        }
144    }
145
146    /// Prepend one path segment as the error bubbles out of a nested value.
147    fn with_segment(mut self, segment: impl Into<String>) -> Self {
148        self.path.insert(0, segment.into());
149        self
150    }
151}
152
153impl fmt::Display for QueryError {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        if self.path.is_empty() {
156            f.write_str(&self.message)
157        } else {
158            write!(f, "{}: {}", self.path.join("."), self.message)
159        }
160    }
161}
162
163impl std::error::Error for QueryError {}
164
165impl de::Error for QueryError {
166    /// The catch-all serde uses for a `#[serde(deserialize_with)]` helper's own
167    /// message — **discarded**, not bounded.
168    ///
169    /// A helper is free to write `E::custom(format!("invalid token {value}"))`,
170    /// and this module cannot audit what it interpolates. Truncating and
171    /// control-stripping such a message still surfaces the leading 160
172    /// characters of it into the 400 body and every error reporter, which does
173    /// not satisfy the module's guarantee that an error never echoes a
174    /// submitted value — a query parameter can hold a secret. So the content is
175    /// dropped entirely and replaced with a fixed message; the field path is
176    /// attached separately and is derived from the target type, not the
177    /// request. A handler wanting a specific user-facing message should
178    /// validate after extraction, where it controls what the response says.
179    fn custom<T: fmt::Display>(_msg: T) -> Self {
180        Self::msg("a value was rejected by a custom deserializer".to_owned())
181    }
182
183    /// `invalid type: string "SUPERSECRET", expected u32` — serde's default
184    /// renders the **value**. Report the shape only.
185    fn invalid_type(unexp: de::Unexpected<'_>, exp: &dyn de::Expected) -> Self {
186        Self::msg(format!(
187            "invalid type: {}, expected {exp}",
188            unexpected_shape(&unexp)
189        ))
190    }
191
192    /// Same reasoning as `invalid_type` above: the value is the secret-bearing
193    /// half of the message.
194    fn invalid_value(unexp: de::Unexpected<'_>, exp: &dyn de::Expected) -> Self {
195        Self::msg(format!(
196            "invalid value: {}, expected {exp}",
197            unexpected_shape(&unexp)
198        ))
199    }
200
201    /// serde renders the rejected variant name, which is request text. The
202    /// `expected` list is compile-time and safe to keep.
203    fn unknown_variant(variant: &str, expected: &'static [&'static str]) -> Self {
204        let _ = variant;
205        Self::msg(format!(
206            "unknown variant, expected one of {}",
207            quoted_list(expected)
208        ))
209    }
210
211    /// `#[serde(deny_unknown_fields)]` feeds the submitted key here, which is
212    /// both attacker-chosen and unbounded.
213    fn unknown_field(field: &str, expected: &'static [&'static str]) -> Self {
214        let _ = field;
215        Self::msg(format!(
216            "unknown field, expected one of {}",
217            quoted_list(expected)
218        ))
219    }
220
221    // `missing_field` and `duplicate_field` take a `&'static str` that can only
222    // come from the target struct, so their defaults are safe as-is.
223}
224
225/// Name a [`de::Unexpected`]'s shape without reproducing its value.
226const fn unexpected_shape(unexp: &de::Unexpected<'_>) -> &'static str {
227    match unexp {
228        de::Unexpected::Bool(_) => "a boolean",
229        de::Unexpected::Unsigned(_) | de::Unexpected::Signed(_) => "an integer",
230        de::Unexpected::Float(_) => "a float",
231        de::Unexpected::Char(_) => "a character",
232        de::Unexpected::Str(_) => "a string",
233        de::Unexpected::Bytes(_) => "a byte string",
234        de::Unexpected::Unit => "a unit value",
235        de::Unexpected::Option => "an optional value",
236        de::Unexpected::NewtypeStruct => "a newtype struct",
237        de::Unexpected::Seq => "a sequence",
238        de::Unexpected::Map => "an object",
239        de::Unexpected::Enum => "an enum",
240        de::Unexpected::UnitVariant => "a unit variant",
241        de::Unexpected::NewtypeVariant => "a newtype variant",
242        de::Unexpected::TupleVariant => "a tuple variant",
243        de::Unexpected::StructVariant => "a struct variant",
244        // `Other` carries a borrowed description that may be built from request
245        // text, so it gets the same treatment as everything else here.
246        de::Unexpected::Other(_) => "a value",
247    }
248}
249
250/// Render a compile-time `expected` list for an error message.
251fn quoted_list(expected: &'static [&'static str]) -> String {
252    if expected.is_empty() {
253        return "no variants".to_owned();
254    }
255    expected
256        .iter()
257        .map(|name| format!("`{name}`"))
258        .collect::<Vec<_>>()
259        .join(", ")
260}
261
262/// Bound and clean attacker-supplied key text before it reaches an error
263/// message.
264///
265/// A decode failure is returned in the 400 Problem Details body and recorded by
266/// every registered error reporter, so raw request text must never flow into it
267/// unbounded: a long key would bloat the response and an embedded control
268/// character could forge a log line. Values are never included at all — a
269/// mistyped secret (`?token=…` against a typed field) must not be echoed back.
270fn sanitize_key(raw: &str) -> String {
271    const MAX: usize = 48;
272    let mut out: String = raw
273        .chars()
274        .take(MAX)
275        .map(|c| if c.is_control() { '.' } else { c })
276        .collect();
277    if raw.chars().nth(MAX).is_some() {
278        out.push('…');
279    }
280    out
281}
282
283// ──────────────────────────────────────────────────────────────────
284// Key parsing
285// ──────────────────────────────────────────────────────────────────
286
287/// One bracketed step in a query key.
288#[derive(Debug, Clone, PartialEq, Eq)]
289enum Segment<'a> {
290    /// `k[]` — append at the next free position.
291    Append,
292    /// `k[3]` — an explicit position.
293    ///
294    /// Carries both the parsed ordering value and the **raw** spelling: a map
295    /// target must receive the key the caller actually sent (`00` stays `00`,
296    /// and an index past `usize::MAX` is not rewritten to the saturated one),
297    /// while a sequence target still orders by `position`.
298    Index { position: usize, raw: &'a str },
299    /// `k[name]` — a named entry.
300    Key(&'a str),
301}
302
303/// Split `base[seg][seg]…` into its base key and bracketed segments.
304///
305/// Returns `None` when the remainder after the first `[` is not a well-formed
306/// run of bracket groups covering the whole key — the caller then treats the
307/// key as a flat literal, so a stray bracket is never a hard error.
308fn parse_key(key: &str) -> Option<(&str, Vec<Segment<'_>>)> {
309    let open = key.find('[')?;
310    let (base, mut rest) = key.split_at(open);
311    let mut segments = Vec::new();
312    while !rest.is_empty() {
313        // Every remaining group must start with `[` and close with `]`.
314        let (inner, remainder) = rest.strip_prefix('[')?.split_once(']')?;
315        // A nested `[` inside a group means the key is malformed, not nested.
316        if inner.contains('[') {
317            return None;
318        }
319        segments.push(classify_segment(inner));
320        rest = remainder;
321    }
322    Some((base, segments))
323}
324
325/// Classify one bracket group's contents.
326///
327/// An all-digit group is a position. Its value is **saturated** rather than
328/// wrapped or rejected, so an absurd index (`tags[99999999999999999999]`) still
329/// sorts last instead of changing the key's meaning between 32- and 64-bit
330/// targets.
331fn classify_segment(inner: &str) -> Segment<'_> {
332    if inner.is_empty() {
333        return Segment::Append;
334    }
335    if inner.bytes().all(|b| b.is_ascii_digit()) {
336        return Segment::Index {
337            // Saturation only affects ORDERING; `raw` keeps the submitted text,
338            // so nothing about the key itself is platform-dependent.
339            position: inner.parse().unwrap_or(usize::MAX),
340            raw: inner,
341        };
342    }
343    Segment::Key(inner)
344}
345
346// ──────────────────────────────────────────────────────────────────
347// Tree
348// ──────────────────────────────────────────────────────────────────
349
350/// The values submitted under one key, in submission order.
351///
352/// Nearly every key is submitted once, so the first value lives inline instead
353/// of behind a `Vec` allocation. Derefs to `[String]`, so readers see the same
354/// slice either way. An empty `Many` is the empty state.
355#[derive(Debug)]
356enum Values {
357    One(String),
358    Many(Vec<String>),
359}
360
361impl Default for Values {
362    fn default() -> Self {
363        Self::Many(Vec::new())
364    }
365}
366
367impl Values {
368    fn push(&mut self, value: String) {
369        match self {
370            Self::Many(values) if values.is_empty() => *self = Self::One(value),
371            Self::Many(values) => values.push(value),
372            Self::One(_) => {
373                if let Self::One(first) = std::mem::take(self) {
374                    *self = Self::Many(vec![first, value]);
375                }
376            }
377        }
378    }
379}
380
381impl std::ops::Deref for Values {
382    type Target = [String];
383
384    fn deref(&self) -> &[String] {
385        match self {
386            Self::One(value) => std::slice::from_ref(value),
387            Self::Many(values) => values,
388        }
389    }
390}
391
392/// One decoded position in the query tree.
393#[derive(Debug)]
394enum Node {
395    /// Every value submitted under this exact key path, in submission order.
396    ///
397    /// Holding all of them (rather than collapsing to one) is what lets the
398    /// same node serve a scalar field (first wins) and a sequence field (all
399    /// values) without the parser needing to know the target type.
400    Scalar(Values),
401    /// Positional children (`k[]`, `k[3]`), ordered by [`SeqKey`]. A `BTreeMap`
402    /// keeps sparse and out-of-order indices cheap: `tags[4000000000]` costs
403    /// one entry, not four billion.
404    Seq(BTreeMap<SeqKey, Self>),
405    /// Named children (`k[name]`).
406    Map(BTreeMap<String, Self>),
407    /// A key the grammar could not resolve — one name used as two shapes
408    /// (`?filter=flat&filter[status]=open`), or nesting past [`MAX_DEPTH`].
409    ///
410    /// Poisoning the node instead of failing the whole parse keeps decoding a
411    /// **superset** of the old flat form: a target that never asks for this key
412    /// (ad-tracking junk, a crawler's garbage parameter) still decodes, exactly
413    /// as it did when the key was simply unrecognised. Only a target that
414    /// actually claims the key sees the error.
415    Conflict(String),
416}
417
418impl Node {
419    /// Human-readable shape name, for conflict and type-mismatch messages.
420    const fn kind(&self) -> &'static str {
421        match self {
422            Self::Scalar(_) => "a value",
423            Self::Seq(_) => "a sequence",
424            Self::Map(_) => "an object",
425            Self::Conflict(_) => "an unresolvable key",
426        }
427    }
428}
429
430/// A positional child's key: its ordering value plus the spelling that produced
431/// it.
432///
433/// Ordering is by `position` first. A sequence target then sees ascending
434/// indices, no matter how they were written. `tie` breaks ties. Use
435/// [`SeqKey::spelling`] to get the text a **map** target receives, so
436/// `counts[0]` and `counts[00]` stay two distinct keys and neither is
437/// rewritten on the way through.
438#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
439struct SeqKey {
440    position: usize,
441    tie: SeqKeyTie,
442}
443
444/// The tie-break part of a [`SeqKey`], for two keys at the same `position`.
445///
446/// At equal `position`, `Explicit` sorts before `Appended`. An append then
447/// always comes after the explicit index it followed. This holds even when
448/// `position` has saturated at `usize::MAX` and cannot advance. Two appends
449/// cannot collide either: each stores the entry count at the time of its
450/// insert. This count goes up by one on every insert into the same node.
451#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
452enum SeqKeyTie {
453    /// `k[N]`. Keyed by the submitted spelling. `0` and `00` stay distinct.
454    /// Neither is rewritten.
455    Explicit(String),
456    /// `k[]`. Keyed by a per-node insert count, not by `position`.
457    /// Saturation cannot make two appends collide.
458    Appended(usize),
459}
460
461impl SeqKey {
462    /// A key for a position the caller did not spell out: the `k[]` append
463    /// form. Pass the current entry count as `append_seq`. This keeps
464    /// repeated appends distinct. It still works after `position` saturates.
465    const fn synthesized(position: usize, append_seq: usize) -> Self {
466        Self {
467            position,
468            tie: SeqKeyTie::Appended(append_seq),
469        }
470    }
471
472    /// The spelling to give to a map target or an error path: the submitted
473    /// text for an explicit index, or the canonical decimal value for an
474    /// append.
475    fn spelling(&self) -> std::borrow::Cow<'_, str> {
476        match &self.tie {
477            SeqKeyTie::Explicit(raw) => std::borrow::Cow::Borrowed(raw.as_str()),
478            SeqKeyTie::Appended(_) => std::borrow::Cow::Owned(self.position.to_string()),
479        }
480    }
481}
482
483/// Render the key path up to (and including) `upto` segments, for error text.
484///
485/// Built only when a conflict is being reported — the success path never
486/// allocates a path string.
487fn key_path(base: &str, segments: &[Segment<'_>], upto: usize) -> String {
488    let mut out = base.to_owned();
489    for segment in segments.iter().take(upto) {
490        match segment {
491            Segment::Key(name) => {
492                out.push('[');
493                out.push_str(name);
494                out.push(']');
495            }
496            Segment::Index { raw, .. } => {
497                out.push('[');
498                out.push_str(raw);
499                out.push(']');
500            }
501            Segment::Append => out.push_str("[]"),
502        }
503    }
504    out
505}
506
507/// Insert one decoded `key=value` pair into the tree rooted at `root`.
508///
509/// Infallible by design: a key the grammar cannot resolve poisons **that key's
510/// node** ([`Node::Conflict`]) rather than failing the request, so an
511/// unrecognised parameter stays ignorable and only a claimed one errors.
512fn insert(root: &mut BTreeMap<String, Node>, base: &str, segments: &[Segment<'_>], value: String) {
513    // `segments.len()` counts the bracketed steps; the base key is the first
514    // level, so the total depth is `len + 1` — expressed as `>=` to stay clear
515    // of the request-path gate's arithmetic ban.
516    if segments.len() >= MAX_DEPTH {
517        let node = root
518            .entry(base.to_owned())
519            .or_insert_with(|| Node::Scalar(Values::default()));
520        poison(
521            node,
522            format!(
523                "query key `{}` nests deeper than the maximum of {MAX_DEPTH}",
524                sanitize_key(base)
525            ),
526        );
527        return;
528    }
529
530    // Descend to the node this key addresses, creating containers on the way.
531    // The *next* segment decides which container the current entry must be, so
532    // the shape is always known before the entry is materialized.
533    let mut node = root
534        .entry(base.to_owned())
535        .or_insert_with(|| empty_for(segments.first()));
536
537    for (position, segment) in segments.iter().enumerate() {
538        let next = segments.get(position.saturating_add(1));
539        match segment {
540            Segment::Key(name) => {
541                // A container that has so far only seen positions is not
542                // necessarily a sequence: `?filter[0]=zero&filter[name]=value`
543                // is a perfectly good dynamic object, and which it is only
544                // becomes knowable when a named key arrives. Promote rather
545                // than declare a conflict.
546                promote_to_map(node);
547                let Node::Map(entries) = node else {
548                    return poison(node, conflict(base, segments, position, "an object"));
549                };
550                node = entries
551                    .entry((*name).to_owned())
552                    .or_insert_with(|| empty_for(next));
553            }
554            // A position addressed on an already-promoted map keeps its decimal
555            // key, so the two orderings of the same query agree.
556            Segment::Index {
557                position: index,
558                raw,
559            } => match node {
560                Node::Seq(entries) => {
561                    node = entries
562                        .entry(SeqKey {
563                            position: *index,
564                            tie: SeqKeyTie::Explicit((*raw).to_owned()),
565                        })
566                        .or_insert_with(|| empty_for(next));
567                }
568                Node::Map(entries) => {
569                    node = entries
570                        .entry((*raw).to_owned())
571                        .or_insert_with(|| empty_for(next));
572                }
573                other => {
574                    return poison(other, conflict(base, segments, position, "a sequence"));
575                }
576            },
577            // `k[]` appends after the highest position seen so far. This
578            // keeps repeated appends in submission order, even when mixed
579            // with explicit indices. `position` can saturate at
580            // `usize::MAX`. Then `saturating_add(1)` cannot advance it.
581            // `append_seq` still keeps two appends apart.
582            Segment::Append => match node {
583                Node::Seq(entries) => {
584                    let index = entries
585                        .keys()
586                        .next_back()
587                        .map_or(0, |last| last.position.saturating_add(1));
588                    let append_seq = entries.len();
589                    node = entries
590                        .entry(SeqKey::synthesized(index, append_seq))
591                        .or_insert_with(|| empty_for(next));
592                }
593                Node::Map(entries) => {
594                    let index = entries
595                        .keys()
596                        .filter_map(|key| key.parse::<usize>().ok())
597                        .max()
598                        .map_or(0, |last| last.saturating_add(1));
599                    // `index` can collide with an existing key. This can
600                    // happen only after a numeric key has saturated at
601                    // `usize::MAX`. Probe for a free key instead of reusing
602                    // one and silently merging two elements. At most
603                    // `entries.len()` candidates can already be taken, so
604                    // this loop always finds a free one.
605                    let mut key = index.to_string();
606                    let mut suffix = 0usize;
607                    while entries.contains_key(&key) {
608                        key = format!("{index}-{suffix}");
609                        suffix = suffix.saturating_add(1);
610                    }
611                    node = entries.entry(key).or_insert_with(|| empty_for(next));
612                }
613                other => {
614                    return poison(other, conflict(base, segments, position, "a sequence"));
615                }
616            },
617        }
618    }
619
620    match node {
621        Node::Scalar(values) => values.push(value),
622        _ => poison(node, conflict(base, segments, segments.len(), "a value")),
623    }
624}
625
626/// Turn a positional node into a named one, rendering each index as its decimal
627/// key, so a container that mixes `k[0]=` and `k[name]=` stays usable.
628fn promote_to_map(node: &mut Node) {
629    if let Node::Seq(entries) = node {
630        let promoted = std::mem::take(entries)
631            .into_iter()
632            .map(|(key, child)| (key.spelling().into_owned(), child))
633            .collect();
634        *node = Node::Map(promoted);
635    }
636}
637
638/// Mark a node unresolvable, keeping the FIRST diagnosis when a key collides
639/// more than once.
640fn poison(node: &mut Node, message: String) {
641    if !matches!(node, Node::Conflict(_)) {
642        *node = Node::Conflict(message);
643    }
644}
645
646/// The empty container a segment's *successor* requires.
647const fn empty_for(next: Option<&Segment<'_>>) -> Node {
648    match next {
649        None => Node::Scalar(Values::Many(Vec::new())),
650        Some(Segment::Key(_)) => Node::Map(BTreeMap::new()),
651        Some(Segment::Append | Segment::Index { .. }) => Node::Seq(BTreeMap::new()),
652    }
653}
654
655fn conflict(base: &str, segments: &[Segment<'_>], upto: usize, wanted: &str) -> String {
656    format!(
657        "query key `{}` is used as both a container and {wanted}",
658        sanitize_key(&key_path(base, segments, upto))
659    )
660}
661
662/// Build the query tree from an already-percent-decoded pair sequence.
663fn build_tree<'a>(
664    pairs: impl Iterator<Item = (std::borrow::Cow<'a, str>, std::borrow::Cow<'a, str>)>,
665) -> BTreeMap<String, Node> {
666    let mut root = BTreeMap::new();
667    for (key, value) in pairs {
668        match parse_key(&key) {
669            Some((base, segments)) => insert(&mut root, base, &segments, value.into_owned()),
670            // Not a well-formed bracket key: treat it as a flat literal name.
671            None => insert(&mut root, &key, &[], value.into_owned()),
672        }
673    }
674    root
675}
676
677/// Decode a raw (still percent-encoded) query string into `T`.
678///
679/// The string is the part **after** `?`, exactly as
680/// [`Uri::query`](axum::http::Uri::query) returns it. An empty string decodes
681/// into any type whose fields are all optional or defaulted.
682///
683/// # Errors
684///
685/// Returns a [`QueryError`] when the key grammar conflicts (one key used as
686/// both a scalar and a container), when nesting exceeds [`MAX_DEPTH`], or when
687/// a value cannot be coerced into the target field's type.
688pub fn from_query_str<T: DeserializeOwned>(query: &str) -> Result<T, QueryError> {
689    let root = build_tree(url::form_urlencoded::parse(query.as_bytes()));
690    T::deserialize(NodeDeserializer {
691        node: &Node::Map(root),
692    })
693}
694
695// ──────────────────────────────────────────────────────────────────
696// Scalar (leaf) deserializer
697// ──────────────────────────────────────────────────────────────────
698
699/// Deserializer for one textual value, coercing with [`str::parse`] exactly as
700/// `serde_urlencoded`'s part deserializer does.
701struct ValueDeserializer<'a> {
702    value: &'a str,
703}
704
705macro_rules! parse_scalar {
706    ($($method:ident => $visit:ident : $ty:ty),* $(,)?) => {
707        $(
708            fn $method<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, QueryError> {
709                // The offending text is deliberately NOT echoed: the error
710                // reaches the 400 body and every error reporter, and a query
711                // parameter can hold a secret. The field path already names
712                // which parameter failed.
713                let parsed: $ty = self.value.parse().map_err(|_| {
714                    QueryError::msg(concat!("invalid ", stringify!($ty), " value"))
715                })?;
716                visitor.$visit(parsed)
717            }
718        )*
719    };
720}
721
722impl<'de> de::Deserializer<'de> for ValueDeserializer<'_> {
723    type Error = QueryError;
724
725    fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, QueryError> {
726        visitor.visit_str(self.value)
727    }
728
729    parse_scalar! {
730        deserialize_bool => visit_bool: bool,
731        deserialize_i8 => visit_i8: i8,
732        deserialize_i16 => visit_i16: i16,
733        deserialize_i32 => visit_i32: i32,
734        deserialize_i64 => visit_i64: i64,
735        deserialize_i128 => visit_i128: i128,
736        deserialize_u8 => visit_u8: u8,
737        deserialize_u16 => visit_u16: u16,
738        deserialize_u32 => visit_u32: u32,
739        deserialize_u64 => visit_u64: u64,
740        deserialize_u128 => visit_u128: u128,
741        deserialize_f32 => visit_f32: f32,
742        deserialize_f64 => visit_f64: f64,
743        deserialize_char => visit_char: char,
744    }
745
746    fn deserialize_str<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, QueryError> {
747        visitor.visit_str(self.value)
748    }
749
750    fn deserialize_string<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, QueryError> {
751        visitor.visit_str(self.value)
752    }
753
754    fn deserialize_bytes<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, QueryError> {
755        visitor.visit_bytes(self.value.as_bytes())
756    }
757
758    fn deserialize_byte_buf<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, QueryError> {
759        visitor.visit_bytes(self.value.as_bytes())
760    }
761
762    /// A key that is *present* always visits `Some`, matching
763    /// `serde_urlencoded`: only an absent key deserializes as `None`.
764    fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, QueryError> {
765        visitor.visit_some(self)
766    }
767
768    fn deserialize_unit<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, QueryError> {
769        visitor.visit_unit()
770    }
771
772    fn deserialize_unit_struct<V: Visitor<'de>>(
773        self,
774        _name: &'static str,
775        visitor: V,
776    ) -> Result<V::Value, QueryError> {
777        visitor.visit_unit()
778    }
779
780    fn deserialize_newtype_struct<V: Visitor<'de>>(
781        self,
782        _name: &'static str,
783        visitor: V,
784    ) -> Result<V::Value, QueryError> {
785        visitor.visit_newtype_struct(self)
786    }
787
788    fn deserialize_seq<V: Visitor<'de>>(self, _visitor: V) -> Result<V::Value, QueryError> {
789        Err(QueryError::msg("expected a sequence, found a single value"))
790    }
791
792    fn deserialize_tuple<V: Visitor<'de>>(
793        self,
794        _len: usize,
795        visitor: V,
796    ) -> Result<V::Value, QueryError> {
797        self.deserialize_seq(visitor)
798    }
799
800    fn deserialize_tuple_struct<V: Visitor<'de>>(
801        self,
802        _name: &'static str,
803        _len: usize,
804        visitor: V,
805    ) -> Result<V::Value, QueryError> {
806        self.deserialize_seq(visitor)
807    }
808
809    fn deserialize_map<V: Visitor<'de>>(self, _visitor: V) -> Result<V::Value, QueryError> {
810        Err(QueryError::msg("expected an object, found a single value"))
811    }
812
813    fn deserialize_struct<V: Visitor<'de>>(
814        self,
815        _name: &'static str,
816        _fields: &'static [&'static str],
817        visitor: V,
818    ) -> Result<V::Value, QueryError> {
819        self.deserialize_map(visitor)
820    }
821
822    /// A bare value selects a unit variant (`?sort=asc` → `Sort::Asc`).
823    fn deserialize_enum<V: Visitor<'de>>(
824        self,
825        _name: &'static str,
826        _variants: &'static [&'static str],
827        visitor: V,
828    ) -> Result<V::Value, QueryError> {
829        visitor.visit_enum(UnitVariant { value: self.value })
830    }
831
832    fn deserialize_identifier<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, QueryError> {
833        visitor.visit_str(self.value)
834    }
835
836    fn deserialize_ignored_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, QueryError> {
837        visitor.visit_unit()
838    }
839}
840
841/// `EnumAccess` for the `?sort=asc` unit-variant form.
842struct UnitVariant<'a> {
843    value: &'a str,
844}
845
846impl<'de> EnumAccess<'de> for UnitVariant<'_> {
847    type Error = QueryError;
848    type Variant = Self;
849
850    fn variant_seed<V: DeserializeSeed<'de>>(
851        self,
852        seed: V,
853    ) -> Result<(V::Value, Self), QueryError> {
854        let variant = seed.deserialize(ValueDeserializer { value: self.value })?;
855        Ok((variant, self))
856    }
857}
858
859impl<'de> VariantAccess<'de> for UnitVariant<'_> {
860    type Error = QueryError;
861
862    fn unit_variant(self) -> Result<(), QueryError> {
863        Ok(())
864    }
865
866    fn newtype_variant_seed<T: DeserializeSeed<'de>>(
867        self,
868        _seed: T,
869    ) -> Result<T::Value, QueryError> {
870        Err(QueryError::msg(
871            "expected a unit variant; a data-carrying variant needs the bracketed form \
872             `key[variant][field]=…`",
873        ))
874    }
875
876    fn tuple_variant<V: Visitor<'de>>(
877        self,
878        _len: usize,
879        _visitor: V,
880    ) -> Result<V::Value, QueryError> {
881        Err(QueryError::msg("expected a unit variant"))
882    }
883
884    fn struct_variant<V: Visitor<'de>>(
885        self,
886        _fields: &'static [&'static str],
887        _visitor: V,
888    ) -> Result<V::Value, QueryError> {
889        Err(QueryError::msg("expected a unit variant"))
890    }
891}
892
893// ──────────────────────────────────────────────────────────────────
894// Node deserializer
895// ──────────────────────────────────────────────────────────────────
896
897struct NodeDeserializer<'a> {
898    node: &'a Node,
899}
900
901impl<'a> NodeDeserializer<'a> {
902    /// Refuse a node whose key the grammar could not resolve.
903    ///
904    /// Checked by every entry point EXCEPT `deserialize_ignored_any`, so a
905    /// conflicting key a target never claims stays ignorable.
906    fn check(&self) -> Result<(), QueryError> {
907        match self.node {
908            Node::Conflict(message) => Err(QueryError::msg(message.clone())),
909            _ => Ok(()),
910        }
911    }
912
913    /// The leaf view of a scalar node.
914    ///
915    /// A key submitted more than once is an **error** in a single-valued
916    /// position, not a silent first- or last-wins pick: `serde_urlencoded` +
917    /// serde's derive rejected `?sig=a&sig=b` with `duplicate field`, and
918    /// resolving it quietly here would hand a security-relevant handler one of
919    /// two values while a proxy or log pipeline recorded the other. Sequence
920    /// fields still take every occurrence — that is [`Self::deserialize_seq`].
921    fn as_value(&self, wanted: &str) -> Result<ValueDeserializer<'a>, QueryError> {
922        self.check()?;
923        match self.node {
924            Node::Scalar(values) if values.len() > 1 => Err(QueryError::msg(
925                "duplicate query parameter: more than one value was submitted for a \
926                 single-valued field",
927            )),
928            Node::Scalar(values) => Ok(ValueDeserializer {
929                // A scalar node is only ever created by pushing a value, so it
930                // is never empty; the fallback keeps this total regardless.
931                value: values.first().map_or("", String::as_str),
932            }),
933            // The caller sent the bracketed form for a key the target types as
934            // a plain value. Name the fix rather than just the mismatch: this is
935            // the one shape that used to arrive as a literal `key[sub]` string
936            // under `serde_urlencoded`.
937            other => Err(QueryError::msg(format!(
938                "expected {wanted}, found {} — the request used the bracketed form \
939                 (`key[…]=`) for this parameter, so give the field a nested type or \
940                 accept it as `serde_json::Value`",
941                other.kind()
942            ))),
943        }
944    }
945}
946
947/// Forward a scalar-shaped `deserialize_*` call to the leaf deserializer.
948macro_rules! forward_scalar {
949    ($($method:ident),* $(,)?) => {
950        $(
951            fn $method<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, QueryError> {
952                self.as_value("a value")?.$method(visitor)
953            }
954        )*
955    };
956}
957
958impl<'de> de::Deserializer<'de> for NodeDeserializer<'_> {
959    type Error = QueryError;
960
961    /// Self-describing decode, used by untyped targets such as
962    /// `serde_json::Value`: a multi-valued key becomes a sequence, a single
963    /// value stays a string.
964    fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, QueryError> {
965        self.check()?;
966        match self.node {
967            Node::Scalar(values) if values.len() == 1 => {
968                visitor.visit_str(values.first().map_or("", String::as_str))
969            }
970            Node::Scalar(_) | Node::Seq(_) => self.deserialize_seq(visitor),
971            Node::Map(_) | Node::Conflict(_) => self.deserialize_map(visitor),
972        }
973    }
974
975    forward_scalar! {
976        deserialize_bool,
977        deserialize_i8, deserialize_i16, deserialize_i32, deserialize_i64, deserialize_i128,
978        deserialize_u8, deserialize_u16, deserialize_u32, deserialize_u64, deserialize_u128,
979        deserialize_f32, deserialize_f64,
980        deserialize_char, deserialize_str, deserialize_string,
981        deserialize_bytes, deserialize_byte_buf,
982        deserialize_identifier,
983    }
984
985    fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, QueryError> {
986        visitor.visit_some(self)
987    }
988
989    /// A `()`/unit-struct field still has to be a *claimable scalar*: routing
990    /// through `as_value` applies both the poisoned-key check and the shape
991    /// requirement, so `?flag=x&flag[y]=z` cannot decode successfully just
992    /// because the target happens to discard the value.
993    fn deserialize_unit<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, QueryError> {
994        self.as_value("a value")?;
995        visitor.visit_unit()
996    }
997
998    fn deserialize_unit_struct<V: Visitor<'de>>(
999        self,
1000        _name: &'static str,
1001        visitor: V,
1002    ) -> Result<V::Value, QueryError> {
1003        self.as_value("a value")?;
1004        visitor.visit_unit()
1005    }
1006
1007    fn deserialize_newtype_struct<V: Visitor<'de>>(
1008        self,
1009        _name: &'static str,
1010        visitor: V,
1011    ) -> Result<V::Value, QueryError> {
1012        visitor.visit_newtype_struct(self)
1013    }
1014
1015    fn deserialize_seq<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, QueryError> {
1016        self.check()?;
1017        match self.node {
1018            // Repeated keys: every submitted value is one element.
1019            Node::Scalar(values) => visitor.visit_seq(ValueSeq {
1020                values: values.iter(),
1021            }),
1022            // Indexed/append form: ascending index order, gaps compacted.
1023            Node::Seq(entries) => visitor.visit_seq(NodeSeq {
1024                entries: entries.iter(),
1025            }),
1026            // An object addressed as a sequence yields its `(key, value)`
1027            // pairs, so the `Query<Vec<(String, String)>>` idiom keeps working.
1028            Node::Map(entries) => visitor.visit_seq(PairSeq {
1029                pairs: flatten_pairs(entries).into_iter(),
1030            }),
1031            // Rejected by `check` above.
1032            Node::Conflict(message) => Err(QueryError::msg(message.clone())),
1033        }
1034    }
1035
1036    fn deserialize_tuple<V: Visitor<'de>>(
1037        self,
1038        _len: usize,
1039        visitor: V,
1040    ) -> Result<V::Value, QueryError> {
1041        self.deserialize_seq(visitor)
1042    }
1043
1044    fn deserialize_tuple_struct<V: Visitor<'de>>(
1045        self,
1046        _name: &'static str,
1047        _len: usize,
1048        visitor: V,
1049    ) -> Result<V::Value, QueryError> {
1050        self.deserialize_seq(visitor)
1051    }
1052
1053    fn deserialize_map<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, QueryError> {
1054        self.check()?;
1055        match self.node {
1056            Node::Map(entries) => visitor.visit_map(NodeMap {
1057                entries: entries.iter(),
1058                value: None,
1059                key: "",
1060            }),
1061            // An all-digit key (`counts[2024]=5`) parses as a position, because
1062            // the tree is built before the target type is known. Serving a
1063            // positional node as a map — index rendered as its decimal key —
1064            // lets such a field still land in a `HashMap`/struct target instead
1065            // of failing on a shape the caller never chose.
1066            Node::Seq(entries) => visitor.visit_map(IndexMap {
1067                entries: entries.iter(),
1068                value: None,
1069                key: String::new(),
1070            }),
1071            other => Err(QueryError::msg(format!(
1072                "expected an object, found {}",
1073                other.kind()
1074            ))),
1075        }
1076    }
1077
1078    fn deserialize_struct<V: Visitor<'de>>(
1079        self,
1080        _name: &'static str,
1081        _fields: &'static [&'static str],
1082        visitor: V,
1083    ) -> Result<V::Value, QueryError> {
1084        self.deserialize_map(visitor)
1085    }
1086
1087    /// `?sort=asc` selects a unit variant; `?sort[custom][field]=x` selects a
1088    /// data-carrying variant through the single-entry object form.
1089    fn deserialize_enum<V: Visitor<'de>>(
1090        self,
1091        name: &'static str,
1092        variants: &'static [&'static str],
1093        visitor: V,
1094    ) -> Result<V::Value, QueryError> {
1095        self.check()?;
1096        match self.node {
1097            Node::Scalar(_) => self
1098                .as_value("a value")?
1099                .deserialize_enum(name, variants, visitor),
1100            Node::Map(entries) if entries.len() == 1 => match entries.iter().next() {
1101                Some((variant, content)) => visitor.visit_enum(NodeVariant { variant, content }),
1102                // Unreachable given the `len() == 1` guard, but the request-path
1103                // gate forbids proving that with a panic.
1104                None => Err(QueryError::msg("expected an enum variant name")),
1105            },
1106            other => Err(QueryError::msg(format!(
1107                "expected an enum variant name or a single-entry object, found {}",
1108                other.kind()
1109            ))),
1110        }
1111    }
1112
1113    /// Unknown keys are discarded without walking their subtree.
1114    fn deserialize_ignored_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, QueryError> {
1115        visitor.visit_unit()
1116    }
1117}
1118
1119/// One `(key, value)` pair of a map addressed as a sequence.
1120enum PairValue<'a> {
1121    Value(&'a str),
1122    Node(&'a Node),
1123}
1124
1125/// Flatten a map into `(key, value)` pairs, expanding a repeated key into one
1126/// pair per submitted value so the pair view matches the wire.
1127fn flatten_pairs(entries: &BTreeMap<String, Node>) -> Vec<(&str, PairValue<'_>)> {
1128    let mut out = Vec::new();
1129    for (key, node) in entries {
1130        match node {
1131            Node::Scalar(values) => {
1132                out.extend(values.iter().map(|v| (key.as_str(), PairValue::Value(v))));
1133            }
1134            other => out.push((key.as_str(), PairValue::Node(other))),
1135        }
1136    }
1137    out
1138}
1139
1140struct ValueSeq<'a> {
1141    values: std::slice::Iter<'a, String>,
1142}
1143
1144impl<'de> SeqAccess<'de> for ValueSeq<'_> {
1145    type Error = QueryError;
1146
1147    fn next_element_seed<T: DeserializeSeed<'de>>(
1148        &mut self,
1149        seed: T,
1150    ) -> Result<Option<T::Value>, QueryError> {
1151        self.values
1152            .next()
1153            .map(|value| seed.deserialize(ValueDeserializer { value }))
1154            .transpose()
1155    }
1156
1157    fn size_hint(&self) -> Option<usize> {
1158        Some(self.values.len())
1159    }
1160}
1161
1162struct NodeSeq<'a> {
1163    entries: std::collections::btree_map::Iter<'a, SeqKey, Node>,
1164}
1165
1166impl<'de> SeqAccess<'de> for NodeSeq<'_> {
1167    type Error = QueryError;
1168
1169    fn next_element_seed<T: DeserializeSeed<'de>>(
1170        &mut self,
1171        seed: T,
1172    ) -> Result<Option<T::Value>, QueryError> {
1173        let Some((key, node)) = self.entries.next() else {
1174            return Ok(None);
1175        };
1176        seed.deserialize(NodeDeserializer { node })
1177            .map(Some)
1178            // The submitted spelling gets the same bounding and
1179            // control-character stripping every other request-derived segment does.
1180            .map_err(|err| err.with_segment(sanitize_key(&key.spelling())))
1181    }
1182
1183    fn size_hint(&self) -> Option<usize> {
1184        Some(self.entries.len())
1185    }
1186}
1187
1188struct PairSeq<'a> {
1189    pairs: std::vec::IntoIter<(&'a str, PairValue<'a>)>,
1190}
1191
1192impl<'de> SeqAccess<'de> for PairSeq<'_> {
1193    type Error = QueryError;
1194
1195    fn next_element_seed<T: DeserializeSeed<'de>>(
1196        &mut self,
1197        seed: T,
1198    ) -> Result<Option<T::Value>, QueryError> {
1199        let Some((key, value)) = self.pairs.next() else {
1200            return Ok(None);
1201        };
1202        seed.deserialize(PairDeserializer { key, value })
1203            .map(Some)
1204            // Same bounding/stripping `NodeMap` applies: this key is raw,
1205            // percent-decoded request text.
1206            .map_err(|err| err.with_segment(sanitize_key(key)))
1207    }
1208
1209    fn size_hint(&self) -> Option<usize> {
1210        Some(self.pairs.len())
1211    }
1212}
1213
1214/// Deserializes one `(key, value)` pair as a two-element tuple.
1215struct PairDeserializer<'a> {
1216    key: &'a str,
1217    value: PairValue<'a>,
1218}
1219
1220impl<'de> de::Deserializer<'de> for PairDeserializer<'_> {
1221    type Error = QueryError;
1222
1223    fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, QueryError> {
1224        visitor.visit_seq(PairElements {
1225            key: Some(self.key),
1226            value: Some(self.value),
1227        })
1228    }
1229
1230    serde::forward_to_deserialize_any! {
1231        bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
1232        bytes byte_buf option unit unit_struct newtype_struct seq tuple
1233        tuple_struct map struct enum identifier ignored_any
1234    }
1235}
1236
1237struct PairElements<'a> {
1238    key: Option<&'a str>,
1239    value: Option<PairValue<'a>>,
1240}
1241
1242impl<'de> SeqAccess<'de> for PairElements<'_> {
1243    type Error = QueryError;
1244
1245    fn next_element_seed<T: DeserializeSeed<'de>>(
1246        &mut self,
1247        seed: T,
1248    ) -> Result<Option<T::Value>, QueryError> {
1249        if let Some(key) = self.key.take() {
1250            return seed.deserialize(ValueDeserializer { value: key }).map(Some);
1251        }
1252        match self.value.take() {
1253            Some(PairValue::Value(value)) => {
1254                seed.deserialize(ValueDeserializer { value }).map(Some)
1255            }
1256            Some(PairValue::Node(node)) => seed.deserialize(NodeDeserializer { node }).map(Some),
1257            None => Ok(None),
1258        }
1259    }
1260}
1261
1262struct NodeMap<'a> {
1263    entries: std::collections::btree_map::Iter<'a, String, Node>,
1264    value: Option<&'a Node>,
1265    key: &'a str,
1266}
1267
1268impl<'de> MapAccess<'de> for NodeMap<'_> {
1269    type Error = QueryError;
1270
1271    fn next_key_seed<K: DeserializeSeed<'de>>(
1272        &mut self,
1273        seed: K,
1274    ) -> Result<Option<K::Value>, QueryError> {
1275        let Some((key, node)) = self.entries.next() else {
1276            return Ok(None);
1277        };
1278        self.value = Some(node);
1279        self.key = key;
1280        seed.deserialize(ValueDeserializer { value: key }).map(Some)
1281    }
1282
1283    fn next_value_seed<V: DeserializeSeed<'de>>(
1284        &mut self,
1285        seed: V,
1286    ) -> Result<V::Value, QueryError> {
1287        let node = self.value.take().ok_or_else(|| {
1288            QueryError::msg("internal error: query map value requested before its key")
1289        })?;
1290        // Tag the failure with the field it came from, so a caller sees
1291        // `filter.limit: invalid value …` rather than a bare parse error.
1292        seed.deserialize(NodeDeserializer { node })
1293            .map_err(|err| err.with_segment(sanitize_key(self.key)))
1294    }
1295
1296    fn size_hint(&self) -> Option<usize> {
1297        Some(self.entries.len())
1298    }
1299}
1300
1301/// `MapAccess` over a positional node, rendering each index as its decimal key.
1302struct IndexMap<'a> {
1303    entries: std::collections::btree_map::Iter<'a, SeqKey, Node>,
1304    value: Option<&'a Node>,
1305    key: String,
1306}
1307
1308impl<'de> MapAccess<'de> for IndexMap<'_> {
1309    type Error = QueryError;
1310
1311    fn next_key_seed<K: DeserializeSeed<'de>>(
1312        &mut self,
1313        seed: K,
1314    ) -> Result<Option<K::Value>, QueryError> {
1315        let Some((key, node)) = self.entries.next() else {
1316            return Ok(None);
1317        };
1318        self.value = Some(node);
1319        // The submitted spelling, so `counts[00]` reaches a map target as
1320        // `"00"` rather than as a re-rendered `"0"`.
1321        self.key = key.spelling().into_owned();
1322        seed.deserialize(ValueDeserializer { value: &self.key })
1323            .map(Some)
1324    }
1325
1326    fn next_value_seed<V: DeserializeSeed<'de>>(
1327        &mut self,
1328        seed: V,
1329    ) -> Result<V::Value, QueryError> {
1330        let node = self.value.take().ok_or_else(|| {
1331            QueryError::msg("internal error: query map value requested before its key")
1332        })?;
1333        let key = sanitize_key(&self.key);
1334        seed.deserialize(NodeDeserializer { node })
1335            .map_err(|err| err.with_segment(key))
1336    }
1337
1338    fn size_hint(&self) -> Option<usize> {
1339        Some(self.entries.len())
1340    }
1341}
1342
1343/// `EnumAccess` for the single-entry-object variant form.
1344struct NodeVariant<'a> {
1345    variant: &'a str,
1346    content: &'a Node,
1347}
1348
1349impl<'de, 'a> EnumAccess<'de> for NodeVariant<'a> {
1350    type Error = QueryError;
1351    type Variant = NodeContent<'a>;
1352
1353    fn variant_seed<V: DeserializeSeed<'de>>(
1354        self,
1355        seed: V,
1356    ) -> Result<(V::Value, NodeContent<'a>), QueryError> {
1357        let variant = seed.deserialize(ValueDeserializer {
1358            value: self.variant,
1359        })?;
1360        Ok((
1361            variant,
1362            NodeContent {
1363                content: self.content,
1364            },
1365        ))
1366    }
1367}
1368
1369struct NodeContent<'a> {
1370    content: &'a Node,
1371}
1372
1373impl<'de> VariantAccess<'de> for NodeContent<'_> {
1374    type Error = QueryError;
1375
1376    /// A unit variant carries no data, but the node selecting it is still
1377    /// *claimed*: `?mode[asc][unexpected]=v` must not quietly select `Asc` and
1378    /// discard the attached object, and a shape-conflicted node must not pass.
1379    /// Same principle as the unit-field path — validate, then discard.
1380    fn unit_variant(self) -> Result<(), QueryError> {
1381        NodeDeserializer { node: self.content }.as_value("a value")?;
1382        Ok(())
1383    }
1384
1385    fn newtype_variant_seed<T: DeserializeSeed<'de>>(
1386        self,
1387        seed: T,
1388    ) -> Result<T::Value, QueryError> {
1389        seed.deserialize(NodeDeserializer { node: self.content })
1390    }
1391
1392    fn tuple_variant<V: Visitor<'de>>(
1393        self,
1394        _len: usize,
1395        visitor: V,
1396    ) -> Result<V::Value, QueryError> {
1397        de::Deserializer::deserialize_seq(NodeDeserializer { node: self.content }, visitor)
1398    }
1399
1400    fn struct_variant<V: Visitor<'de>>(
1401        self,
1402        _fields: &'static [&'static str],
1403        visitor: V,
1404    ) -> Result<V::Value, QueryError> {
1405        de::Deserializer::deserialize_map(NodeDeserializer { node: self.content }, visitor)
1406    }
1407}
1408
1409#[cfg(test)]
1410mod tests {
1411    use super::*;
1412    use serde::Deserialize;
1413
1414    #[derive(Debug, Deserialize, PartialEq)]
1415    struct Filter {
1416        status: String,
1417        limit: Option<u32>,
1418    }
1419
1420    #[derive(Debug, Deserialize, PartialEq)]
1421    struct Item {
1422        sku: String,
1423        qty: u32,
1424    }
1425
1426    #[derive(Debug, Deserialize, PartialEq, Default)]
1427    struct Args {
1428        q: Option<String>,
1429        page: Option<u32>,
1430        flag: Option<bool>,
1431        tags: Option<Vec<String>>,
1432        filter: Option<Filter>,
1433        items: Option<Vec<Item>>,
1434    }
1435
1436    fn args(query: &str) -> Args {
1437        from_query_str(query).expect("decodes")
1438    }
1439
1440    #[test]
1441    fn flat_scalars_match_the_previous_urlencoded_behaviour() {
1442        let out = args("q=foo&page=2&flag=true");
1443        assert_eq!(out.q.as_deref(), Some("foo"));
1444        assert_eq!(out.page, Some(2));
1445        assert_eq!(out.flag, Some(true));
1446    }
1447
1448    #[test]
1449    fn absent_keys_are_none_and_an_empty_query_decodes() {
1450        assert_eq!(args(""), Args::default());
1451    }
1452
1453    #[test]
1454    fn a_present_but_empty_optional_still_visits_some() {
1455        // Parity with `serde_urlencoded`: presence, not emptiness, decides.
1456        assert_eq!(args("q=").q.as_deref(), Some(""));
1457        assert!(
1458            from_query_str::<Args>("page=").is_err(),
1459            "an empty value for a numeric field is still a coercion failure"
1460        );
1461    }
1462
1463    #[test]
1464    fn repeated_keys_fill_a_sequence_but_a_duplicated_scalar_is_rejected() {
1465        assert_eq!(
1466            args("tags=a&tags=b").tags.unwrap(),
1467            vec!["a".to_owned(), "b".to_owned()]
1468        );
1469        // Fail closed: picking one of two values for a single-valued field is
1470        // how parameter-pollution bugs are built, and `serde_urlencoded` +
1471        // serde's derive rejected it too.
1472        let err = from_query_str::<Args>("q=first&q=second").expect_err("duplicate scalar");
1473        assert!(
1474            err.to_string().contains("duplicate query parameter"),
1475            "{err}"
1476        );
1477    }
1478
1479    #[test]
1480    fn distinct_index_spellings_stay_distinct() {
1481        // Codex P2: `0` and `00` are different keys, so neither may overwrite
1482        // the other. A sequence target orders them by position and keeps both;
1483        // a map target receives each spelling unchanged.
1484        #[derive(Debug, Deserialize)]
1485        struct Counts {
1486            counts: std::collections::HashMap<String, u32>,
1487        }
1488
1489        assert_eq!(
1490            args("tags[0]=a&tags[00]=b").tags.unwrap(),
1491            vec!["a".to_owned(), "b".to_owned()]
1492        );
1493
1494        let out: Counts = from_query_str("counts[00]=5&counts[0]=6").expect("decodes");
1495        assert_eq!(
1496            out.counts.get("00"),
1497            Some(&5),
1498            "spelling preserved: {out:?}"
1499        );
1500        assert_eq!(out.counts.get("0"), Some(&6));
1501
1502        // An index past `usize::MAX` is not rewritten to the saturated value.
1503        let out: Counts = from_query_str("counts[99999999999999999999]=7").expect("decodes");
1504        assert_eq!(out.counts.get("99999999999999999999"), Some(&7));
1505
1506        // The SAME spelling twice is still a genuine duplicate.
1507        assert!(from_query_str::<Args>("tags[0]=a&tags[0]=b").is_err());
1508    }
1509
1510    #[test]
1511    fn a_custom_deserializer_message_is_discarded_not_merely_bounded() {
1512        // Codex P1: a `deserialize_with` helper can interpolate the submitted
1513        // value into its own message. Truncating that still surfaces the first
1514        // 160 characters, so the content is dropped outright.
1515        fn picky<'de, D>(deserializer: D) -> Result<String, D::Error>
1516        where
1517            D: serde::Deserializer<'de>,
1518        {
1519            let raw = String::deserialize(deserializer)?;
1520            Err(serde::de::Error::custom(format!("invalid token {raw}")))
1521        }
1522
1523        #[derive(Debug, Deserialize)]
1524        struct Guarded {
1525            #[serde(deserialize_with = "picky")]
1526            #[allow(
1527                dead_code,
1528                reason = "the helper always errors; the field is never built"
1529            )]
1530            token: String,
1531        }
1532
1533        let err = from_query_str::<Guarded>("token=SUPERSECRET").expect_err("helper rejects");
1534        let rendered = err.to_string();
1535        assert!(!rendered.contains("SUPERSECRET"), "leaked: {rendered}");
1536        assert!(
1537            !rendered.contains("invalid token"),
1538            "retained content: {rendered}"
1539        );
1540        // The field path still identifies *where* the failure was; it comes
1541        // from the target type, not from the request.
1542        assert!(rendered.contains("token"), "{rendered}");
1543
1544        // A secret longer than the old 160-char bound is equally absent.
1545        let long = "S3CRET".repeat(60);
1546        let err = from_query_str::<Guarded>(&format!("token={long}")).expect_err("helper rejects");
1547        assert!(!err.to_string().contains("S3CRET"), "{err}");
1548    }
1549
1550    #[test]
1551    fn a_unit_variant_still_validates_the_node_it_claims() {
1552        // Codex P2: selecting a unit variant must not silently discard an
1553        // attached payload or accept a poisoned node.
1554        #[derive(Debug, Deserialize, PartialEq)]
1555        #[serde(rename_all = "lowercase")]
1556        enum Mode {
1557            Asc,
1558        }
1559        #[derive(Debug, Deserialize)]
1560        struct Sorted {
1561            mode: Mode,
1562        }
1563        assert_eq!(
1564            from_query_str::<Sorted>("mode[asc]=1")
1565                .expect("decodes")
1566                .mode,
1567            Mode::Asc
1568        );
1569        assert!(
1570            from_query_str::<Sorted>("mode[asc][unexpected]=value").is_err(),
1571            "an attached payload must not be silently discarded"
1572        );
1573        assert!(from_query_str::<Sorted>("mode=asc").is_ok());
1574    }
1575
1576    #[test]
1577    fn serde_generated_errors_never_echo_request_text() {
1578        // Codex P1: serde builds its own messages for these cases and the
1579        // defaults embed the submitted text. All three must stay redacted.
1580        #[derive(Debug, Deserialize)]
1581        #[serde(rename_all = "lowercase")]
1582        enum Sort {
1583            Asc,
1584        }
1585        #[derive(Debug, Deserialize)]
1586        struct Sorted {
1587            #[allow(dead_code)]
1588            sort: Sort,
1589        }
1590        #[derive(Debug, Deserialize)]
1591        #[serde(deny_unknown_fields)]
1592        struct Strict {
1593            #[allow(dead_code)]
1594            q: String,
1595        }
1596        // `invalid type` renders the value in serde's default message.
1597        #[derive(Debug, Deserialize)]
1598        struct Nested {
1599            #[allow(dead_code)]
1600            filter: std::collections::HashMap<String, String>,
1601        }
1602
1603        let err = from_query_str::<Sorted>("sort=SUPERSECRET").expect_err("unknown variant");
1604        assert!(!err.to_string().contains("SUPERSECRET"), "{err}");
1605        let err = from_query_str::<Strict>("q=x&SUPERSECRET=1").expect_err("unknown field");
1606        assert!(!err.to_string().contains("SUPERSECRET"), "{err}");
1607        let err =
1608            from_query_str::<Nested>("filter[a]=x&filter=SUPERSECRET").expect_err("shape conflict");
1609        assert!(!err.to_string().contains("SUPERSECRET"), "{err}");
1610    }
1611
1612    #[test]
1613    fn a_unit_field_still_validates_the_node_it_claims() {
1614        // Codex P2: `()` discards its value, but the key is still *claimed* —
1615        // a poisoned shape conflict or a bracketed container under it must not
1616        // decode successfully just because the target throws the value away.
1617        #[derive(Debug, Deserialize)]
1618        struct WithUnit {
1619            #[allow(dead_code)]
1620            flag: (),
1621        }
1622        assert!(
1623            from_query_str::<WithUnit>("flag=x&flag[y]=z").is_err(),
1624            "a claimed conflicting key must fail even for a unit field"
1625        );
1626        assert!(
1627            from_query_str::<WithUnit>("flag[y]=z").is_err(),
1628            "a bracketed container is not a value"
1629        );
1630        assert!(from_query_str::<WithUnit>("flag=x").is_ok());
1631    }
1632
1633    #[test]
1634    fn pair_sequence_errors_sanitize_their_key() {
1635        // Codex P2: this path annotated errors with the raw, percent-decoded
1636        // key while `NodeMap` sanitized — the same control characters and
1637        // unbounded length would have reached the 400 body.
1638        let err = from_query_str::<Vec<(String, u32)>>("\nATTACKER=not-a-number")
1639            .expect_err("coercion failure");
1640        let rendered = err.to_string();
1641        assert!(
1642            !rendered.contains('\n'),
1643            "control chars stripped: {rendered:?}"
1644        );
1645
1646        let long = format!("{}=not-a-number", "k".repeat(500));
1647        let err = from_query_str::<Vec<(String, u32)>>(&long).expect_err("coercion failure");
1648        assert!(
1649            err.to_string().chars().count() < 200,
1650            "key bounded: {}",
1651            err.to_string().len()
1652        );
1653    }
1654
1655    #[test]
1656    fn a_container_may_mix_numeric_and_named_keys() {
1657        // Codex P2: `filter[0]=zero&filter[name]=value` is a valid dynamic
1658        // object; classifying `0` as a position must not poison it.
1659        #[derive(Debug, Deserialize)]
1660        struct Dynamic {
1661            filter: std::collections::HashMap<String, String>,
1662        }
1663        let out: Dynamic = from_query_str("filter[0]=zero&filter[name]=value").expect("decodes");
1664        assert_eq!(out.filter.get("0").map(String::as_str), Some("zero"));
1665        assert_eq!(out.filter.get("name").map(String::as_str), Some("value"));
1666
1667        // The reverse submission order agrees.
1668        let out: Dynamic = from_query_str("filter[name]=value&filter[0]=zero").expect("decodes");
1669        assert_eq!(out.filter.get("0").map(String::as_str), Some("zero"));
1670        assert_eq!(out.filter.get("name").map(String::as_str), Some("value"));
1671
1672        // Promotion does not disturb a purely positional container.
1673        assert_eq!(
1674            args("tags[0]=a&tags[1]=b").tags.unwrap(),
1675            vec!["a".to_owned(), "b".to_owned()]
1676        );
1677    }
1678
1679    #[test]
1680    fn errors_never_echo_the_submitted_value() {
1681        // The message reaches the 400 body and every error reporter, so a
1682        // mistyped secret must not come back out.
1683        let err = from_query_str::<Args>("page=SUPERSECRET").expect_err("coercion failure");
1684        let rendered = err.to_string();
1685        assert!(
1686            !rendered.contains("SUPERSECRET"),
1687            "value leaked: {rendered}"
1688        );
1689        assert!(rendered.contains("page"), "field path kept: {rendered}");
1690    }
1691
1692    #[test]
1693    fn error_text_bounds_and_cleans_attacker_supplied_key_text() {
1694        let long = "k".repeat(500);
1695        let rendered = sanitize_key(&long);
1696        assert!(
1697            rendered.chars().count() <= 49,
1698            "bounded: {}",
1699            rendered.len()
1700        );
1701        assert_eq!(sanitize_key("a\nb"), "a.b");
1702    }
1703
1704    #[test]
1705    fn a_conflicting_key_the_target_ignores_is_still_ignored() {
1706        // Pre-existing behaviour for an unrecognised parameter: it does not
1707        // fail the request. Junk like `?utm=1&utm[src]=x` must stay ignorable.
1708        let out = args("q=ok&utm=1&utm[src]=x");
1709        assert_eq!(out.q.as_deref(), Some("ok"));
1710        // Same for a key that nests past the cap but is never claimed.
1711        let deep = format!("q=ok&junk{}=1", "[x]".repeat(MAX_DEPTH));
1712        assert_eq!(args(&deep).q.as_deref(), Some("ok"));
1713    }
1714
1715    #[test]
1716    fn an_all_digit_object_key_still_lands_in_a_map_target() {
1717        // `counts[2024]=5` parses as a position (the tree is built before the
1718        // target type is known), so a positional node must still serve a map.
1719        let out: std::collections::HashMap<String, std::collections::HashMap<String, u32>> =
1720            from_query_str("counts[2024]=5").expect("decodes");
1721        assert_eq!(out["counts"]["2024"], 5);
1722    }
1723
1724    #[test]
1725    fn append_and_indexed_forms_fill_a_sequence() {
1726        assert_eq!(
1727            args("tags[]=a&tags[]=b").tags.unwrap(),
1728            vec!["a".to_owned(), "b".to_owned()]
1729        );
1730        assert_eq!(
1731            args("tags[1]=b&tags[0]=a").tags.unwrap(),
1732            vec!["a".to_owned(), "b".to_owned()]
1733        );
1734    }
1735
1736    #[test]
1737    fn gapped_indices_compact_in_ascending_order() {
1738        assert_eq!(
1739            args("tags[4]=c&tags[0]=a&tags[2]=b").tags.unwrap(),
1740            vec!["a".to_owned(), "b".to_owned(), "c".to_owned()]
1741        );
1742    }
1743
1744    #[test]
1745    fn an_absurd_index_sorts_last_without_preallocating() {
1746        assert_eq!(
1747            args("tags[99999999999999999999]=last&tags[3]=first")
1748                .tags
1749                .unwrap(),
1750            vec!["first".to_owned(), "last".to_owned()]
1751        );
1752    }
1753
1754    #[test]
1755    fn appends_continue_after_the_highest_explicit_index() {
1756        assert_eq!(
1757            args("tags[]=a&tags[5]=b&tags[]=c").tags.unwrap(),
1758            vec!["a".to_owned(), "b".to_owned(), "c".to_owned()]
1759        );
1760    }
1761
1762    #[test]
1763    fn append_after_a_saturated_explicit_index_sorts_after_it() {
1764        // The explicit index overflows to `usize::MAX`. So `saturating_add(1)`
1765        // cannot move the append past it. The append must still keep
1766        // submission order.
1767        assert_eq!(
1768            args("tags[99999999999999999999]=a&tags[]=b").tags.unwrap(),
1769            vec!["a".to_owned(), "b".to_owned()]
1770        );
1771    }
1772
1773    #[test]
1774    fn append_after_an_index_spelled_exactly_usize_max_does_not_collide() {
1775        // The explicit index is spelled as literally `usize::MAX`. This used
1776        // to match the append key byte for byte. The two entries merged into
1777        // one node instead of staying distinct.
1778        assert_eq!(
1779            args("tags[18446744073709551615]=a&tags[]=b").tags.unwrap(),
1780            vec!["a".to_owned(), "b".to_owned()]
1781        );
1782    }
1783
1784    #[test]
1785    fn append_after_a_saturated_index_does_not_merge_nested_elements() {
1786        // Same collision, one level deeper. Two outer elements must stay
1787        // two. They must not collapse into one.
1788        #[derive(Debug, Deserialize, PartialEq)]
1789        struct Wrapper {
1790            x: String,
1791        }
1792        #[derive(Debug, Deserialize)]
1793        struct Nested {
1794            tags: Vec<Wrapper>,
1795        }
1796
1797        let out: Nested = from_query_str("tags[18446744073709551615][x]=first&tags[][x]=second")
1798            .expect("decodes");
1799        assert_eq!(
1800            out.tags,
1801            vec![
1802                Wrapper {
1803                    x: "first".to_owned()
1804                },
1805                Wrapper {
1806                    x: "second".to_owned()
1807                },
1808            ]
1809        );
1810    }
1811
1812    #[test]
1813    fn an_explicit_index_always_sorts_before_an_append_at_the_same_position() {
1814        // This ordering is load-bearing: it is what lets an append keep
1815        // submission order after `position` saturates and can no longer
1816        // move it forward. Assert it directly, not only through the
1817        // end-to-end tests above.
1818        assert!(SeqKeyTie::Explicit(String::new()) < SeqKeyTie::Appended(0));
1819    }
1820
1821    #[test]
1822    fn append_into_a_promoted_map_after_a_saturated_index_does_not_merge() {
1823        // The same collision as above, on the `Node::Map` append path: a
1824        // `Node::Seq` promoted to a map by a named key, then appended to
1825        // after a saturated index.
1826        #[derive(Debug, Deserialize)]
1827        struct Dynamic {
1828            filter: std::collections::HashMap<String, String>,
1829        }
1830
1831        let out: Dynamic =
1832            from_query_str("filter[18446744073709551615]=zero&filter[name]=value&filter[]=next")
1833                .expect("decodes");
1834        assert_eq!(
1835            out.filter.len(),
1836            3,
1837            "the append must not merge into the saturated key: {out:?}"
1838        );
1839        assert_eq!(
1840            out.filter.get("18446744073709551615").map(String::as_str),
1841            Some("zero")
1842        );
1843        assert_eq!(out.filter.get("name").map(String::as_str), Some("value"));
1844    }
1845
1846    #[test]
1847    fn nested_objects_decode() {
1848        assert_eq!(
1849            args("filter[status]=open&filter[limit]=5").filter.unwrap(),
1850            Filter {
1851                status: "open".to_owned(),
1852                limit: Some(5),
1853            }
1854        );
1855    }
1856
1857    #[test]
1858    fn arrays_of_objects_decode() {
1859        let out = args("items[0][sku]=A-1&items[0][qty]=2&items[1][sku]=B-2&items[1][qty]=3");
1860        assert_eq!(
1861            out.items.unwrap(),
1862            vec![
1863                Item {
1864                    sku: "A-1".to_owned(),
1865                    qty: 2
1866                },
1867                Item {
1868                    sku: "B-2".to_owned(),
1869                    qty: 3
1870                },
1871            ]
1872        );
1873    }
1874
1875    #[test]
1876    fn percent_encoded_brackets_parse_identically() {
1877        assert_eq!(
1878            args("filter%5Bstatus%5D=open").filter.unwrap().status,
1879            "open"
1880        );
1881    }
1882
1883    #[test]
1884    fn plus_is_decoded_as_a_space() {
1885        assert_eq!(args("q=hello+world").q.as_deref(), Some("hello world"));
1886    }
1887
1888    #[test]
1889    fn malformed_bracket_keys_stay_literal() {
1890        assert_eq!(parse_key("weird[unclosed"), None);
1891        assert_eq!(parse_key("a[b][c"), None);
1892        assert_eq!(parse_key("a[[b]]"), None);
1893        assert_eq!(parse_key("plain"), None);
1894        // A literal key with no matching field is simply ignored.
1895        assert_eq!(args("weird[unclosed=1&q=ok").q.as_deref(), Some("ok"));
1896    }
1897
1898    #[test]
1899    fn bracket_keys_parse_into_segments() {
1900        assert_eq!(
1901            parse_key("items[0][sku]"),
1902            Some((
1903                "items",
1904                vec![
1905                    Segment::Index {
1906                        position: 0,
1907                        raw: "0"
1908                    },
1909                    Segment::Key("sku")
1910                ]
1911            ))
1912        );
1913        assert_eq!(parse_key("tags[]"), Some(("tags", vec![Segment::Append])));
1914    }
1915
1916    #[test]
1917    fn shape_conflicts_are_rejected() {
1918        let scalar_then_object = from_query_str::<Args>("filter=flat&filter[status]=open");
1919        assert!(scalar_then_object.is_err());
1920        let object_then_scalar = from_query_str::<Args>("filter[status]=open&filter=flat");
1921        assert!(object_then_scalar.is_err());
1922        let seq_then_object = from_query_str::<Args>("tags[0]=a&tags[name]=b");
1923        assert!(seq_then_object.is_err());
1924    }
1925
1926    #[test]
1927    fn nesting_deeper_than_the_cap_is_rejected_for_a_claimed_key() {
1928        let deep = format!("filter{}=1", "[x]".repeat(MAX_DEPTH));
1929        let err = from_query_str::<Args>(&deep).expect_err("depth-capped");
1930        assert!(err.to_string().contains("deeper"), "{err}");
1931        // One level under the cap parses; `filter` then fails on its own shape,
1932        // not on the depth guard.
1933        let ok = format!("q=x&junk{}=1", "[x]".repeat(MAX_DEPTH - 2));
1934        assert!(from_query_str::<Args>(&ok).is_ok());
1935    }
1936
1937    #[test]
1938    fn errors_name_the_failing_field_path() {
1939        let err = from_query_str::<Args>("filter[status]=open&filter[limit]=nope")
1940            .expect_err("coercion failure");
1941        assert!(err.to_string().starts_with("filter.limit:"), "{err}");
1942    }
1943
1944    #[test]
1945    fn shape_conflicts_surface_only_when_the_target_claims_the_key() {
1946        let err = from_query_str::<Args>("filter=flat&filter[status]=open")
1947            .expect_err("claimed conflicting key");
1948        assert!(err.to_string().contains("used as both"), "{err}");
1949    }
1950
1951    #[test]
1952    fn untyped_targets_decode_through_deserialize_any() {
1953        let out: serde_json::Value =
1954            from_query_str("q=foo&tags=a&tags=b&filter[status]=open").expect("decodes");
1955        assert_eq!(out["q"], "foo");
1956        assert_eq!(out["tags"], serde_json::json!(["a", "b"]));
1957        assert_eq!(out["filter"]["status"], "open");
1958    }
1959
1960    #[test]
1961    fn map_targets_keep_working() {
1962        let out: std::collections::HashMap<String, String> =
1963            from_query_str("a=1&b=2").expect("decodes");
1964        assert_eq!(out.get("a").map(String::as_str), Some("1"));
1965        assert_eq!(out.get("b").map(String::as_str), Some("2"));
1966    }
1967
1968    #[test]
1969    fn pair_sequence_targets_keep_working() {
1970        // The `Query<Vec<(String, String)>>` idiom: one pair per occurrence.
1971        let out: Vec<(String, String)> = from_query_str("a=1&a=2&b=3").expect("decodes");
1972        assert_eq!(
1973            out,
1974            vec![
1975                ("a".to_owned(), "1".to_owned()),
1976                ("a".to_owned(), "2".to_owned()),
1977                ("b".to_owned(), "3".to_owned()),
1978            ]
1979        );
1980    }
1981
1982    #[test]
1983    fn unit_enum_variants_decode_from_a_bare_value() {
1984        #[derive(Debug, Deserialize, PartialEq)]
1985        #[serde(rename_all = "lowercase")]
1986        enum Sort {
1987            Asc,
1988            Desc,
1989        }
1990        #[derive(Debug, Deserialize)]
1991        struct Sorted {
1992            sort: Sort,
1993        }
1994        let out: Sorted = from_query_str("sort=desc").expect("decodes");
1995        assert_eq!(out.sort, Sort::Desc);
1996    }
1997
1998    #[test]
1999    fn unknown_keys_are_ignored_including_nested_ones() {
2000        let out = args("q=ok&unknown[deep][deeper]=1&other=2");
2001        assert_eq!(out.q.as_deref(), Some("ok"));
2002    }
2003
2004    #[test]
2005    fn a_missing_required_field_still_fails() {
2006        assert!(from_query_str::<Filter>("limit=5").is_err());
2007    }
2008
2009    #[test]
2010    fn serde_renames_are_honored_because_serde_resolves_the_key() {
2011        #[derive(Debug, Deserialize)]
2012        #[serde(rename_all = "camelCase")]
2013        struct Renamed {
2014            word_count: u32,
2015        }
2016        let out: Renamed = from_query_str("wordCount=7").expect("decodes");
2017        assert_eq!(out.word_count, 7);
2018    }
2019}