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}