Skip to main content

rama_net/uri/
query.rs

1//! Query component types — owned [`Query`] and borrowed [`QueryRef`].
2//!
3//! Per RFC 3986 §3.4, the query is opaque bytes between `?` and `#`. The
4//! `key=value&…` shape is a convention (HTML forms, most APIs) — not part
5//! of the URI grammar. Use [`QueryRef::pairs`] to iterate name/value pairs
6//! and, with the `std` feature, [`QueryRef::deserialize`] to read straight into a typed value with
7//! `application/x-www-form-urlencoded` semantics.
8
9use core::fmt;
10use core::hash::Hash;
11
12use crate::std::borrow::Cow;
13use crate::std::string::String;
14use crate::std::vec::Vec;
15
16use super::component_input::IntoUriComponent;
17use super::encode::{
18    encoded_pair_component, encoded_query, encoded_query_cmp, encoded_query_eq,
19    extend_encoded_query, hash_encoded_query, write_encoded_query,
20};
21
22use rama_core::bytes::{Bytes, BytesMut};
23
24use percent_encoding::percent_decode;
25
26/// Owned query component. Cheaply mutable in-place via the
27/// [`QueryMut`](super::QueryMut) RAII guard.
28///
29/// `Default` produces an empty query (zero bytes — distinct from
30/// "no query"; the distinction is owned by [`super::Uri::query`] /
31/// [`super::Uri::set_query`]). `Display` writes the explicitly encoded
32/// query view (no leading `?`). `Hash` / `PartialOrd` / `Ord` use that
33/// same encoded view, so raw component text and its pct-encoded spelling
34/// compare consistently.
35#[derive(Debug, Clone, Default)]
36pub struct Query {
37    pub(crate) bytes: BytesMut,
38}
39
40impl Query {
41    /// Percent-encoded query string (no leading `?`).
42    #[must_use]
43    pub fn as_encoded_str(&self) -> Cow<'_, str> {
44        encoded_query(&self.bytes)
45    }
46
47    /// `true` when the query contains no bytes. An empty query is still
48    /// *present* (`?` on the wire) — the present-vs-absent distinction is
49    /// owned by [`super::Uri::query`].
50    #[must_use]
51    #[inline]
52    pub fn is_empty(&self) -> bool {
53        self.bytes.is_empty()
54    }
55
56    /// Percent-decoded query string. `Cow::Borrowed` when no `%XX`
57    /// escapes are present; `Cow::Owned` otherwise. UTF-8 errors fall
58    /// back to U+FFFD (matches curl, browsers).
59    #[must_use]
60    pub fn as_decoded_str(&self) -> Cow<'_, str> {
61        percent_decode(&self.bytes).decode_utf8_lossy()
62    }
63
64    /// Borrowed view. Named `view` (not `as_ref`) so it doesn't shadow
65    /// the std `AsRef` trait — see the type-level docs.
66    #[must_use]
67    #[inline]
68    pub fn view(&self) -> QueryRef<'_> {
69        QueryRef { bytes: &self.bytes }
70    }
71
72    /// Iterator over `name[=value]` pairs in the query string.
73    ///
74    /// Convenience pass-through to [`QueryRef::pairs`] on the borrowed
75    /// view — see that method for the splitting / decoding contract.
76    #[must_use]
77    pub fn pairs(&self) -> QueryPairs<'_> {
78        QueryPairs::new(&self.bytes)
79    }
80
81    /// Deserialize the query into `T`. See [`QueryRef::deserialize`] for
82    /// the encoding and borrowing contract.
83    #[cfg(feature = "std")]
84    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
85    pub fn deserialize<'de, T>(&'de self) -> Result<T, QueryDeserializeError>
86    where
87        T: serde::de::Deserialize<'de>,
88    {
89        self.view().deserialize::<T>()
90    }
91
92    /// The first form-decoded value for the pair named `name`. See
93    /// [`QueryRef::first_value`] for the matching and bare-key rules.
94    #[must_use]
95    pub fn first_value(&self, name: impl IntoUriComponent) -> Option<Cow<'_, str>> {
96        self.view().first_value(name)
97    }
98
99    /// Iterator over the form-decoded values of every pair named `name`.
100    /// See [`QueryRef::values`].
101    #[must_use]
102    pub fn values(&self, name: impl IntoUriComponent) -> QueryValues<'_> {
103        self.view().values(name)
104    }
105
106    /// `true` when any pair's form-decoded name equals `name`. See
107    /// [`QueryRef::contains_name`].
108    #[must_use]
109    pub fn contains_name(&self, name: impl IntoUriComponent) -> bool {
110        self.view().contains_name(name)
111    }
112}
113
114impl PartialEq for Query {
115    #[inline(always)]
116    fn eq(&self, other: &Self) -> bool {
117        self.view() == other.view()
118    }
119}
120
121impl Eq for Query {}
122
123impl PartialOrd for Query {
124    #[inline(always)]
125    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
126        Some(self.cmp(other))
127    }
128}
129
130impl Ord for Query {
131    #[inline(always)]
132    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
133        self.view().cmp(&other.view())
134    }
135}
136
137impl Hash for Query {
138    #[inline(always)]
139    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
140        self.view().hash(state);
141    }
142}
143
144/// Starting-capacity hint per pair when collecting via [`FromIterator`].
145/// Covers a typical short `name=value` plus the `&` separator with
146/// margin; [`BytesMut`] grows further if the iterator turns out to
147/// produce longer content.
148const COLLECT_BYTES_PER_PAIR: usize = 32;
149
150/// Build a [`Query`] from an iterator of pair-byte slices, joining with `&`.
151fn collect_pairs<'a, I>(iter: I) -> Query
152where
153    I: IntoIterator,
154    I::Item: AsRef<[u8]> + 'a,
155{
156    let iter = iter.into_iter();
157    let mut bytes = BytesMut::with_capacity(iter.size_hint().0 * COLLECT_BYTES_PER_PAIR);
158    for pair in iter {
159        if !bytes.is_empty() {
160            bytes.extend_from_slice(b"&");
161        }
162        bytes.extend_from_slice(pair.as_ref());
163    }
164    Query { bytes }
165}
166
167impl FromIterator<QueryPair> for Query {
168    /// Build a [`Query`] by concatenating pre-encoded pair bytes with
169    /// `&` separators. No re-encoding — the pairs' bytes are assumed to
170    /// already be in canonical on-wire form (which they are, when they
171    /// come from [`QueryRef::pairs`], [`QueryMut::pop`](super::QueryMut::pop)
172    /// or [`QueryMut::drain`](super::QueryMut::drain)).
173    fn from_iter<I: IntoIterator<Item = QueryPair>>(iter: I) -> Self {
174        collect_pairs(iter.into_iter().map(|p| p.raw))
175    }
176}
177
178impl<'a> FromIterator<QueryPairRef<'a>> for Query {
179    /// Build a [`Query`] from borrowed pair views by copying their raw
180    /// bytes. See [`FromIterator<QueryPair>`](Query#impl-FromIterator<QueryPair>-for-Query)
181    /// for the no-re-encoding contract.
182    fn from_iter<I: IntoIterator<Item = QueryPairRef<'a>>>(iter: I) -> Self {
183        collect_pairs(iter.into_iter().map(|p| p.raw))
184    }
185}
186
187/// Borrowed view of a URI query component (no leading `?`).
188#[derive(Debug, Clone, Copy)]
189pub struct QueryRef<'a> {
190    pub(crate) bytes: &'a [u8],
191}
192
193impl<'a> QueryRef<'a> {
194    #[must_use]
195    #[inline]
196    pub(crate) const fn new(bytes: &'a [u8]) -> Self {
197        Self { bytes }
198    }
199
200    /// Borrow a query string as a [`QueryRef`] — no allocation.
201    ///
202    /// The input is treated as component text. When rendered through
203    /// [`QueryRef::as_encoded_str`], bytes outside the query grammar are
204    /// percent-encoded while valid existing pct triplets are preserved.
205    #[must_use]
206    #[inline]
207    pub fn from_raw_str(query: &'a str) -> Self {
208        Self::new(query.as_bytes())
209    }
210
211    /// Percent-encoded query string (no leading `?`).
212    #[must_use]
213    pub fn as_encoded_str(self) -> Cow<'a, str> {
214        encoded_query(self.bytes)
215    }
216
217    /// `true` when the query contains no bytes. An empty query is still
218    /// *present* (`?` on the wire) — the present-vs-absent distinction is
219    /// owned by [`super::Uri::query`].
220    #[must_use]
221    #[inline]
222    pub fn is_empty(self) -> bool {
223        self.bytes.is_empty()
224    }
225
226    pub(super) fn write_encoded_to(self, buf: &mut BytesMut) {
227        extend_encoded_query(buf, self.bytes);
228    }
229
230    /// Percent-decoded query string. `Cow::Borrowed` when no `%XX`
231    /// escapes are present; `Cow::Owned` otherwise. UTF-8 errors fall
232    /// back to U+FFFD.
233    #[must_use]
234    pub fn as_decoded_str(&self) -> Cow<'a, str> {
235        percent_decode(self.bytes).decode_utf8_lossy()
236    }
237
238    /// Returns an owned copy. Named `into_owned` (matching
239    /// [`crate::std::borrow::Cow::into_owned`]) so it doesn't shadow the std `ToOwned`
240    /// trait method.
241    #[must_use]
242    pub fn into_owned(self) -> Query {
243        Query {
244            bytes: BytesMut::from(self.bytes),
245        }
246    }
247
248    /// Iterator over `name[=value]` pairs.
249    ///
250    /// Follows WHATWG `URLSearchParams` / form-urlencoded splitting:
251    /// `&` delimits pairs, the first `=` in each pair delimits name from
252    /// value, empty fragments (`&&`, leading/trailing `&`) are dropped.
253    /// Each [`QueryPair`] keeps the bare-vs-empty-value distinction —
254    /// `?foo` → `value = None`, `?foo=` → `value = Some("")`.
255    #[must_use]
256    pub fn pairs(&self) -> QueryPairs<'a> {
257        QueryPairs::new(self.bytes)
258    }
259
260    /// Deserialize the query into `T` with `application/x-www-form-urlencoded`
261    /// semantics: `+` → space, `%XX` → byte, repeated keys collect into a
262    /// `Vec<_>` field.
263    ///
264    /// Bare keys (`?foo`) decode as `foo=""`, matching WHATWG
265    /// `URLSearchParams`. This diverges from [`pairs`](Self::pairs)
266    /// which keeps the bare-vs-empty distinction — use that iterator
267    /// if you need it.
268    ///
269    /// Fields can borrow from the query bytes when the component already has a
270    /// borrowed encoded view: `&'a str` and `Cow<'a, str>` skip the allocation
271    /// when the source value has no `+` / `%XX` to decode. When decoding *is*
272    /// needed, `&'a str` fails (the decoded bytes don't live in the input)
273    /// while `Cow<'a, str>` falls back to `Cow::Owned`. Prefer `Cow<'a, str>`
274    /// or `String` for fields that may contain escapes.
275    #[cfg(feature = "std")]
276    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
277    pub fn deserialize<T>(&self) -> Result<T, QueryDeserializeError>
278    where
279        T: serde::de::Deserialize<'a>,
280    {
281        match self.as_encoded_str() {
282            Cow::Borrowed(encoded) => {
283                serde_html_form::from_str(encoded).map_err(QueryDeserializeError)
284            }
285            Cow::Owned(_) => Err(QueryDeserializeError(
286                <serde_html_form::de::Error as serde::de::Error>::custom(
287                    "query contains component text that needs encoding before deserialization",
288                ),
289            )),
290        }
291    }
292
293    /// The first form-decoded value for the pair named `name`, or `None`
294    /// when no pair matches.
295    ///
296    /// `name` is component text, compared form-decoded on both sides —
297    /// `"a b"`, `"a+b"` and `"a%20b"` all address the same name (the same
298    /// normalization the path matchers apply to their patterns). A bare
299    /// key (`?foo`) yields `Some("")` — the WHATWG convention also used by
300    /// [`deserialize`](Self::deserialize); use [`pairs`](Self::pairs) when
301    /// the bare-vs-empty distinction matters.
302    #[must_use]
303    pub fn first_value(self, name: impl IntoUriComponent) -> Option<Cow<'a, str>> {
304        self.values(name).next()
305    }
306
307    /// Iterator over the form-decoded values of every pair named `name`
308    /// (repeated names are legal: `?tag=a&tag=b`). See
309    /// [`first_value`](Self::first_value) for the matching and bare-key
310    /// rules.
311    #[must_use]
312    #[expect(
313        clippy::needless_pass_by_value,
314        reason = "by-value matches IntoUriComponent's signature on sibling matchers; this impl only borrows the input"
315    )]
316    pub fn values(self, name: impl IntoUriComponent) -> QueryValues<'a> {
317        QueryValues {
318            pairs: self.pairs(),
319            name: form_decode_bytes(&name.as_uri_component_bytes()).into_owned(),
320        }
321    }
322
323    /// `true` when any pair's form-decoded name equals `name` (bare keys
324    /// count). See [`first_value`](Self::first_value) for the matching
325    /// rules.
326    #[must_use]
327    #[expect(
328        clippy::needless_pass_by_value,
329        reason = "by-value matches IntoUriComponent's signature on sibling matchers; this impl only borrows the input"
330    )]
331    pub fn contains_name(self, name: impl IntoUriComponent) -> bool {
332        let name = name.as_uri_component_bytes();
333        let pattern = form_decode_bytes(&name);
334        self.pairs()
335            .any(|p| form_decode_bytes(p.name_bytes()) == pattern)
336    }
337}
338
339impl PartialEq for QueryRef<'_> {
340    #[inline(always)]
341    fn eq(&self, other: &Self) -> bool {
342        encoded_query_eq(self.bytes, other.bytes)
343    }
344}
345
346impl Eq for QueryRef<'_> {}
347
348impl PartialEq<str> for QueryRef<'_> {
349    #[inline(always)]
350    fn eq(&self, other: &str) -> bool {
351        self.eq(&QueryRef::from_raw_str(other))
352    }
353}
354
355impl PartialEq<&str> for QueryRef<'_> {
356    #[inline(always)]
357    fn eq(&self, other: &&str) -> bool {
358        self.eq(&QueryRef::from_raw_str(other))
359    }
360}
361
362impl<'a> PartialEq<QueryRef<'a>> for str {
363    #[inline(always)]
364    fn eq(&self, other: &QueryRef<'a>) -> bool {
365        QueryRef::from_raw_str(self).eq(other)
366    }
367}
368
369impl<'a> PartialEq<QueryRef<'a>> for &str {
370    #[inline(always)]
371    fn eq(&self, other: &QueryRef<'a>) -> bool {
372        QueryRef::from_raw_str(self).eq(other)
373    }
374}
375
376impl PartialOrd for QueryRef<'_> {
377    #[inline(always)]
378    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
379        Some(self.cmp(other))
380    }
381}
382
383impl Ord for QueryRef<'_> {
384    #[inline(always)]
385    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
386        encoded_query_cmp(self.bytes, other.bytes)
387    }
388}
389
390impl Hash for QueryRef<'_> {
391    #[inline(always)]
392    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
393        hash_encoded_query(state, self.bytes);
394    }
395}
396
397/// Returned by [`QueryRef::deserialize`] / [`Query::deserialize`] when the
398/// query string cannot be converted into the target type — type mismatch,
399/// missing required field, malformed encoding, or an escaped value being
400/// fed into a non-owning `&str` field.
401#[derive(Debug)]
402#[cfg(feature = "std")]
403#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
404pub struct QueryDeserializeError(serde_html_form::de::Error);
405
406#[cfg(feature = "std")]
407impl fmt::Display for QueryDeserializeError {
408    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
409        write!(f, "failed to deserialize URI query: {}", self.0)
410    }
411}
412
413#[cfg(feature = "std")]
414impl core::error::Error for QueryDeserializeError {
415    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
416        Some(&self.0)
417    }
418}
419
420/// One owned `name[=value]` pair, cheap to clone. Produced by
421/// [`QueryMut::pop`](super::QueryMut::pop) and
422/// [`QueryMut::drain`](super::QueryMut::drain) — popping a pair off a
423/// query doesn't copy the byte content (the buffer is refcount-shared
424/// with the source).
425#[derive(Debug, Clone, PartialEq, Eq, Hash)]
426pub struct QueryPair {
427    raw: Bytes,
428    /// Byte offset of `=` within `raw`, or `None` for a bare key.
429    ///
430    /// `u32` because a single key=value pair built via the mutation API
431    /// can exceed `MAX_URI_LEN` (the parser-level 16-bit cap applies
432    /// only to parsed inputs, not to caller-built queries). `u16` would
433    /// silently truncate the offset for pairs whose key crosses 65535
434    /// bytes — pinned by the `eq_offset_*` and `huge_pair_*` regression
435    /// tests in `parser::tests::query_pairs`.
436    eq_at: Option<u32>,
437}
438
439impl QueryPair {
440    /// Construct from raw `name[=value]` bytes (no leading `&`).
441    /// Finds the first `=` once at construction; subsequent accessors
442    /// slice without rescanning.
443    #[inline]
444    #[must_use]
445    pub(crate) fn from_raw(raw: Bytes) -> Self {
446        let eq_at = memchr::memchr(b'=', &raw).map(|i| i as u32);
447        Self { raw, eq_at }
448    }
449
450    /// Borrowed view. All inspection methods on `QueryPair` route
451    /// through this — single source of truth for the slicing /
452    /// decoding logic.
453    #[must_use]
454    #[inline]
455    pub fn view(&self) -> QueryPairRef<'_> {
456        QueryPairRef {
457            raw: &self.raw,
458            eq_at: self.eq_at,
459        }
460    }
461
462    /// Percent-encoded name.
463    #[must_use]
464    #[inline]
465    pub fn name_encoded(&self) -> Cow<'_, str> {
466        self.view().name_encoded()
467    }
468
469    /// Name with form-urlencoded decoding: `+` → space, `%XX` → byte.
470    #[must_use]
471    #[inline]
472    pub fn name_decoded(&self) -> Cow<'_, str> {
473        self.view().name_decoded()
474    }
475
476    /// Percent-encoded value, or `None` for a bare key.
477    #[must_use]
478    #[inline]
479    pub fn value_encoded(&self) -> Option<Cow<'_, str>> {
480        self.view().value_encoded()
481    }
482
483    /// Value with form-urlencoded decoding (`+` → space, `%XX` → byte),
484    /// or `None` for a bare key.
485    #[must_use]
486    #[inline]
487    pub fn value_decoded(&self) -> Option<Cow<'_, str>> {
488        self.view().value_decoded()
489    }
490
491    /// `true` if the pair has an `=` separator. `?foo=` → `true`; `?foo` → `false`.
492    #[must_use]
493    #[inline]
494    pub fn has_value(&self) -> bool {
495        self.view().has_value()
496    }
497}
498
499impl core::fmt::Display for QueryPair {
500    #[inline(always)]
501    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
502        core::fmt::Display::fmt(&self.view(), f)
503    }
504}
505
506/// Borrowed `name[=value]` pair view. Yielded by
507/// [`QueryRef::pairs`] / [`Query::pairs`].
508///
509/// Decoded views apply the form-urlencoded convention (`+` → space,
510/// `%XX` → byte) — distinct from [`QueryRef::as_decoded_str`] which only
511/// percent-decodes.
512#[derive(Debug, Clone, Copy, PartialEq, Eq)]
513pub struct QueryPairRef<'a> {
514    raw: &'a [u8],
515    /// Byte offset of `=` within `raw`, or `None` for a bare key.
516    ///
517    /// `u32` because a single key=value pair built via the mutation API
518    /// can exceed `MAX_URI_LEN` (the parser-level 16-bit cap applies
519    /// only to parsed inputs, not to caller-built queries). `u16` would
520    /// silently truncate the offset for pairs whose key crosses 65535
521    /// bytes — pinned by the `eq_offset_*` and `huge_pair_*` regression
522    /// tests in `parser::tests::query_pairs`.
523    eq_at: Option<u32>,
524}
525
526impl<'a> QueryPairRef<'a> {
527    /// Construct from raw `name[=value]` bytes (no leading `&`).
528    #[inline]
529    #[must_use]
530    pub(crate) fn from_raw(raw: &'a [u8]) -> Self {
531        let eq_at = memchr::memchr(b'=', raw).map(|i| i as u32);
532        Self { raw, eq_at }
533    }
534
535    /// Percent-encoded name.
536    #[must_use]
537    pub fn name_encoded(self) -> Cow<'a, str> {
538        encoded_pair_component(self.name_bytes())
539    }
540
541    /// Name with form-urlencoded decoding: `+` → space, `%XX` → byte.
542    /// `Cow::Borrowed` when neither escape is present.
543    #[must_use]
544    pub fn name_decoded(self) -> Cow<'a, str> {
545        form_decode(self.name_bytes())
546    }
547
548    /// Percent-encoded value, or `None` for a bare key.
549    #[must_use]
550    pub fn value_encoded(self) -> Option<Cow<'a, str>> {
551        self.value_bytes().map(encoded_pair_component)
552    }
553
554    /// Value with form-urlencoded decoding (`+` → space, `%XX` → byte),
555    /// or `None` for a bare key.
556    #[must_use]
557    pub fn value_decoded(self) -> Option<Cow<'a, str>> {
558        self.value_bytes().map(form_decode)
559    }
560
561    /// `true` if the pair has an `=` separator. `?foo=` → `true`; `?foo` → `false`.
562    #[must_use]
563    pub fn has_value(self) -> bool {
564        self.eq_at.is_some()
565    }
566
567    /// Allocate an owned [`QueryPair`] copying the raw bytes.
568    ///
569    /// Named `into_owned` (matching the [`crate::std::borrow::Cow::into_owned`] precedent)
570    /// rather than `to_owned` to avoid shadowing the std `ToOwned`
571    /// trait method.
572    #[must_use]
573    pub fn into_owned(self) -> QueryPair {
574        QueryPair {
575            raw: Bytes::copy_from_slice(self.raw),
576            eq_at: self.eq_at,
577        }
578    }
579
580    /// Raw `name[=value]` bytes of the pair (no leading `&`).
581    #[inline(always)]
582    pub(super) fn raw_bytes(self) -> &'a [u8] {
583        self.raw
584    }
585
586    #[inline(always)]
587    pub(super) fn name_bytes(self) -> &'a [u8] {
588        match self.eq_at {
589            Some(i) => &self.raw[..i as usize],
590            None => self.raw,
591        }
592    }
593
594    #[inline(always)]
595    pub(super) fn value_bytes(self) -> Option<&'a [u8]> {
596        self.eq_at.map(|i| &self.raw[i as usize + 1..])
597    }
598}
599
600impl core::fmt::Display for QueryPairRef<'_> {
601    #[inline(always)]
602    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
603        write_encoded_query(f, self.raw)
604    }
605}
606
607/// Iterator over the `name[=value]` pairs of a URI query string. Created by
608/// [`QueryRef::pairs`] / [`Query::pairs`].
609#[derive(Debug, Clone)]
610pub struct QueryPairs<'a> {
611    /// Bytes that haven't been processed yet, excluding any `&` that
612    /// triggered the previous yield.
613    remaining: &'a [u8],
614    /// `true` once all fragments have been consumed.
615    exhausted: bool,
616}
617
618impl<'a> QueryPairs<'a> {
619    #[inline]
620    fn new(bytes: &'a [u8]) -> Self {
621        Self {
622            remaining: bytes,
623            exhausted: bytes.is_empty(),
624        }
625    }
626}
627
628impl<'a> Iterator for QueryPairs<'a> {
629    type Item = QueryPairRef<'a>;
630
631    fn next(&mut self) -> Option<Self::Item> {
632        loop {
633            if self.exhausted {
634                return None;
635            }
636            // Pull off the next `&`-delimited fragment.
637            // `memchr` for SIMD-accelerated boundary search.
638            let fragment = if let Some(i) = memchr::memchr(b'&', self.remaining) {
639                let frag = &self.remaining[..i];
640                self.remaining = &self.remaining[i + 1..];
641                frag
642            } else {
643                let frag = self.remaining;
644                self.remaining = &[];
645                self.exhausted = true;
646                frag
647            };
648
649            if fragment.is_empty() {
650                // Empty fragment (`&&`, leading `&`, trailing `&`) — skip,
651                // matching WHATWG URLSearchParams / serde_html_form behaviour.
652                continue;
653            }
654
655            return Some(QueryPairRef::from_raw(fragment));
656        }
657    }
658}
659
660impl core::iter::FusedIterator for QueryPairs<'_> {}
661
662/// Iterator over the form-decoded values of every pair with a fixed name.
663/// Created by [`QueryRef::values`] / [`Query::values`].
664///
665/// A bare key (`?foo`) yields `""` — see [`QueryRef::first_value`] for the
666/// matching rules.
667#[derive(Debug, Clone)]
668pub struct QueryValues<'a> {
669    pairs: QueryPairs<'a>,
670    /// Form-decoded name pattern, decoded once at construction.
671    name: Vec<u8>,
672}
673
674impl<'a> Iterator for QueryValues<'a> {
675    type Item = Cow<'a, str>;
676
677    fn next(&mut self) -> Option<Self::Item> {
678        loop {
679            let pair = self.pairs.next()?;
680            if *form_decode_bytes(pair.name_bytes()) == *self.name {
681                return Some(pair.value_decoded().unwrap_or(Cow::Borrowed("")));
682            }
683        }
684    }
685}
686
687impl core::iter::FusedIterator for QueryValues<'_> {}
688
689/// Form-urlencoded decode to bytes: `+` → ` `, `%XX` → byte.
690///
691/// Returns `Cow::Borrowed` when the input contains neither `+` nor `%`.
692/// Invalid `%XX` (non-hex or truncated) passes through as a literal `%`.
693///
694/// Name matching compares these decoded BYTES, not a lossy-UTF-8
695/// rendering — lossy decoding collapses every distinct invalid-UTF-8
696/// byte to U+FFFD, which would make unrelated names compare equal
697/// (mirrors `path::segment_eq`'s rationale).
698pub(super) fn form_decode_bytes(input: &[u8]) -> Cow<'_, [u8]> {
699    // Fast path: nothing to decode.
700    let Some(start) = memchr::memchr2(b'+', b'%', input) else {
701        return Cow::Borrowed(input);
702    };
703
704    let mut out = Vec::with_capacity(input.len());
705    out.extend_from_slice(&input[..start]);
706
707    let mut i = start;
708    while i < input.len() {
709        match input[i] {
710            b'+' => {
711                out.push(b' ');
712                i += 1;
713            }
714            b'%' if i + 2 < input.len() => {
715                if let Some(byte) = rama_utils::hex::decode_pair(input[i + 1], input[i + 2]) {
716                    out.push(byte);
717                    i += 3;
718                } else {
719                    // Malformed `%XX` — emit the `%` literally and move on.
720                    out.push(b'%');
721                    i += 1;
722                }
723            }
724            b => {
725                // Catches trailing `%` with < 2 chars remaining, and every
726                // ordinary byte.
727                out.push(b);
728                i += 1;
729            }
730        }
731    }
732
733    Cow::Owned(out)
734}
735
736/// Form-urlencoded decode: `+` → ` `, `%XX` → byte.
737///
738/// String view over [`form_decode_bytes`]. Invalid UTF-8 in the decoded
739/// bytes falls back to U+FFFD.
740fn form_decode(input: &[u8]) -> Cow<'_, str> {
741    match form_decode_bytes(input) {
742        // Safety: parser invariant — query bytes are valid UTF-8, and the
743        // borrowed path means no `%XX` / `+` was decoded.
744        Cow::Borrowed(bytes) => Cow::Borrowed(unsafe { core::str::from_utf8_unchecked(bytes) }),
745        // Happy path: decoded bytes are valid UTF-8 — promote `Vec<u8>` →
746        // `String` without re-allocating. Otherwise fall back to lossy.
747        Cow::Owned(out) => match String::from_utf8(out) {
748            Ok(s) => Cow::Owned(s),
749            Err(e) => Cow::Owned(String::from_utf8_lossy(e.as_bytes()).into_owned()),
750        },
751    }
752}
753
754impl fmt::Display for Query {
755    #[inline(always)]
756    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
757        fmt::Display::fmt(&self.view(), f)
758    }
759}
760
761impl fmt::Display for QueryRef<'_> {
762    /// Renders the encoded query bytes (no leading `?`).
763    #[inline(always)]
764    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
765        write_encoded_query(f, self.bytes)
766    }
767}
768
769impl core::str::FromStr for Query {
770    type Err = core::convert::Infallible;
771
772    /// Encode arbitrary input as a [`Query`] — bytes outside
773    /// `pchar ∪ {'/', '?'}` get percent-encoded. Infallible because
774    /// every input round-trips after encoding; `str::parse` users with
775    /// `?`-ladder code can still use this through the `Result` shape.
776    fn from_str(s: &str) -> Result<Self, Self::Err> {
777        Ok(Self {
778            bytes: super::encode::encode_query(s),
779        })
780    }
781}
782
783#[cfg(test)]
784mod internal_tests {
785    //! Direct tests for the private `form_decode` helper. Behavioural
786    //! coverage via the public `QueryRef::pairs()` API lives in
787    //! `super::super::parser::tests::query_pairs`; these pin the
788    //! function-level invariants that don't surface through the iterator.
789
790    use crate::std::borrow::Cow;
791
792    use super::form_decode;
793
794    // ---- form_decode ------------------------------------------------
795
796    #[test]
797    fn form_decode_empty_borrows() {
798        let out = form_decode(b"");
799        assert!(matches!(out, Cow::Borrowed(_)));
800        assert_eq!(&*out, "");
801    }
802
803    /// Verify the borrowed path is genuinely zero-copy — the returned
804    /// `&str` must point at the same address as the input bytes.
805    #[test]
806    fn form_decode_borrowed_path_is_zero_copy() {
807        let input: &[u8] = b"no-escapes-here";
808        let out = form_decode(input);
809        match out {
810            Cow::Borrowed(s) => {
811                assert_eq!(s.as_ptr(), input.as_ptr(), "borrowed view re-allocated");
812            }
813            Cow::Owned(_) => panic!("expected Cow::Borrowed for input without `+` or `%`"),
814        }
815    }
816
817    /// `%2B` decodes to a literal `+` — the decoder must NOT then
818    /// re-interpret that `+` as a space (no double-decoding).
819    #[test]
820    fn form_decode_pct_2b_is_literal_plus_not_space() {
821        assert_eq!(form_decode(b"%2B"), Cow::Borrowed("+"));
822        assert_eq!(form_decode(b"a%2Bb"), Cow::Borrowed("a+b"));
823    }
824
825    /// `%26` → `&`, `%3D` → `=`. The pair iterator already split on the
826    /// raw bytes, so decoded `&`/`=` are inert here.
827    #[test]
828    fn form_decode_pct_delimiter_bytes() {
829        assert_eq!(form_decode(b"%26"), Cow::Borrowed("&"));
830        assert_eq!(form_decode(b"%3D"), Cow::Borrowed("="));
831        assert_eq!(form_decode(b"a%26b%3Dc"), Cow::Borrowed("a&b=c"));
832    }
833
834    /// `%00` produces a null byte in the resulting `String` — Rust
835    /// strings tolerate interior NUL.
836    #[test]
837    fn form_decode_pct_00_null_byte() {
838        let out = form_decode(b"a%00b");
839        assert_eq!(out.as_bytes(), b"a\x00b");
840    }
841
842    /// 3-byte UTF-8: `%E2%82%AC` → `€` (U+20AC).
843    #[test]
844    fn form_decode_three_byte_utf8() {
845        assert_eq!(form_decode(b"%E2%82%AC"), Cow::Borrowed("€"));
846    }
847
848    /// 4-byte UTF-8: `%F0%9F%98%80` → 😀 (U+1F600).
849    #[test]
850    fn form_decode_four_byte_utf8() {
851        assert_eq!(form_decode(b"%F0%9F%98%80"), Cow::Borrowed("\u{1F600}"));
852    }
853
854    /// Truncated multi-byte UTF-8 — lossy decode emits U+FFFD.
855    #[test]
856    fn form_decode_truncated_utf8_replacement() {
857        // `%E2%82` is the first 2 of 3 bytes for `€` — invalid UTF-8.
858        let out = form_decode(b"%E2%82");
859        assert!(out.contains('\u{FFFD}'), "got {out:?}");
860    }
861
862    /// Mixed `+`, `%XX`, and plain bytes in a single input.
863    #[test]
864    fn form_decode_mixed_input() {
865        assert_eq!(
866            form_decode(b"hello+world%20%21"),
867            Cow::Borrowed("hello world !"),
868        );
869    }
870
871    /// Long-string sanity check: 4 KB of mixed-escape content decodes
872    /// without panicking and produces the expected length.
873    #[test]
874    fn form_decode_long_string() {
875        // Pattern: "a+b%20" repeats; each repeat decodes "a+b%20" (6 bytes)
876        // → "a b " (4 bytes).
877        const N: usize = 1000;
878        let mut input = Vec::with_capacity(6 * N);
879        for _ in 0..N {
880            input.extend_from_slice(b"a+b%20");
881        }
882        let out = form_decode(&input);
883        assert_eq!(out.len(), 4 * N);
884        // Spot-check a couple of windows.
885        assert!(out.starts_with("a b a b "));
886        assert!(out.ends_with("a b a b "));
887    }
888
889    /// Malformed `%XX` sequences (non-hex digits) pass through
890    /// literally — including the `%` itself.
891    #[test]
892    fn form_decode_malformed_pct_literal_passthrough() {
893        assert_eq!(form_decode(b"%ZZ"), Cow::Borrowed("%ZZ"));
894        assert_eq!(form_decode(b"%G0"), Cow::Borrowed("%G0"));
895        assert_eq!(form_decode(b"%-1"), Cow::Borrowed("%-1"));
896    }
897
898    /// Trailing `%` with insufficient remaining bytes — literal `%`.
899    #[test]
900    fn form_decode_trailing_percent_variants() {
901        assert_eq!(form_decode(b"%"), Cow::Borrowed("%"));
902        assert_eq!(form_decode(b"a%"), Cow::Borrowed("a%"));
903        assert_eq!(form_decode(b"a%A"), Cow::Borrowed("a%A"));
904    }
905}