xocomil 0.3.0

A lightweight, zero-allocation HTTP/1.1 request parser and response writer
Documentation
//! Iterator over `application/x-www-form-urlencoded` name/value pairs.
//!
//! Yields raw, **undecoded** byte slices — pass each one through
//! [`pct::decode`](crate::pct::decode) with [`Mode::Form`](crate::pct::Mode::Form)
//! to apply percent-decoding (and `+` → space) into a caller-provided
//! buffer. This keeps the iterator zero-allocation: every item still
//! borrows from the original input.
//!
//! Matches the behavior of WHATWG URL form decoding for splitting:
//! pairs separated by `&`, names and values by the first `=`. A pair
//! with no `=` yields the entire pair as the name with an empty value.
//! Empty pairs (e.g. `&&`) are skipped.
//!
//! # Example
//!
//! ```
//! use xocomil::query;
//!
//! let pairs: Vec<_> = query::parse(b"a=1&b=hello+world&c").collect();
//! assert_eq!(pairs, &[
//!     (&b"a"[..], &b"1"[..]),
//!     (&b"b"[..], &b"hello+world"[..]),
//!     (&b"c"[..], &b""[..]),
//! ]);
//! ```

/// Iterator over `(name, value)` byte slices in a query string.
///
/// Both `name` and `value` are returned **raw** — still percent-encoded,
/// with `+` not yet converted to space. Call
/// [`pct::decode`](crate::pct::decode) with [`Mode::Form`](crate::pct::Mode::Form)
/// to decode either component.
#[derive(Clone, Debug)]
pub struct QueryIter<'a> {
    rest: &'a [u8],
}

impl<'a> Iterator for QueryIter<'a> {
    type Item = (&'a [u8], &'a [u8]);

    fn next(&mut self) -> Option<Self::Item> {
        // Skip leading empty segments (e.g. `&&a=1` -> ("a", "1")).
        loop {
            if self.rest.is_empty() {
                return None;
            }
            if self.rest[0] == b'&' {
                self.rest = &self.rest[1..];
                continue;
            }
            break;
        }

        let (segment, tail) = self.rest.iter().position(|&b| b == b'&').map_or_else(
            || (self.rest, &b""[..]),
            |amp| (&self.rest[..amp], &self.rest[amp + 1..]),
        );
        self.rest = tail;

        let (name, value) = segment.iter().position(|&b| b == b'=').map_or_else(
            || (segment, &b""[..]),
            |eq| (&segment[..eq], &segment[eq + 1..]),
        );
        Some((name, value))
    }
}

/// Parse a query string into an iterator of `(name, value)` pairs.
///
/// Pass the raw query component (the bytes after `?`, without the `?`).
/// See [`QueryIter`] for behavior details.
#[inline]
#[must_use]
pub const fn parse(query: &[u8]) -> QueryIter<'_> {
    QueryIter { rest: query }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn collect(input: &[u8]) -> Vec<(&[u8], &[u8])> {
        parse(input).collect()
    }

    #[test]
    fn empty_input() {
        assert!(collect(b"").is_empty());
    }

    #[test]
    fn single_pair() {
        assert_eq!(collect(b"a=1"), &[(&b"a"[..], &b"1"[..])]);
    }

    #[test]
    fn multiple_pairs() {
        assert_eq!(
            collect(b"a=1&b=2&c=3"),
            &[
                (&b"a"[..], &b"1"[..]),
                (&b"b"[..], &b"2"[..]),
                (&b"c"[..], &b"3"[..]),
            ]
        );
    }

    #[test]
    fn name_without_value() {
        assert_eq!(collect(b"flag"), &[(&b"flag"[..], &b""[..])]);
        assert_eq!(
            collect(b"a=1&flag&b=2"),
            &[
                (&b"a"[..], &b"1"[..]),
                (&b"flag"[..], &b""[..]),
                (&b"b"[..], &b"2"[..]),
            ]
        );
    }

    #[test]
    fn empty_value() {
        assert_eq!(collect(b"a="), &[(&b"a"[..], &b""[..])]);
    }

    #[test]
    fn empty_name() {
        assert_eq!(collect(b"=v"), &[(&b""[..], &b"v"[..])]);
    }

    #[test]
    fn skip_empty_segments() {
        assert_eq!(collect(b"&&a=1"), &[(&b"a"[..], &b"1"[..])]);
        assert_eq!(
            collect(b"a=1&&b=2"),
            &[(&b"a"[..], &b"1"[..]), (&b"b"[..], &b"2"[..])]
        );
        assert!(collect(b"&&&").is_empty());
    }

    #[test]
    fn value_contains_equals() {
        // Only the first `=` splits name from value.
        assert_eq!(collect(b"a=1=2"), &[(&b"a"[..], &b"1=2"[..])]);
    }

    #[test]
    fn raw_undecoded_output() {
        // `+` and `%XX` are left raw — caller decodes per-pair.
        assert_eq!(
            collect(b"q=hello+world&n=foo%20bar"),
            &[
                (&b"q"[..], &b"hello+world"[..]),
                (&b"n"[..], &b"foo%20bar"[..]),
            ]
        );
    }

    #[test]
    fn iterator_is_lazy_and_clone() {
        let iter = parse(b"a=1&b=2");
        let mut a = iter.clone();
        let mut b = iter;
        assert_eq!(a.next(), Some((&b"a"[..], &b"1"[..])));
        assert_eq!(b.next(), Some((&b"a"[..], &b"1"[..])));
    }

    #[test]
    fn integrates_with_pct_form_decode() {
        use crate::pct::{Mode, decode};

        let pairs: Vec<_> = parse(b"q=hello+world").collect();
        let (name, value) = pairs[0];
        let mut nbuf = [0u8; 16];
        let mut vbuf = [0u8; 16];
        assert_eq!(decode(name, Mode::Form, &mut nbuf).unwrap(), b"q");
        assert_eq!(
            decode(value, Mode::Form, &mut vbuf).unwrap(),
            b"hello world"
        );
    }
}