Skip to main content

fastapi_http/
query.rs

1//! Query string parsing utilities.
2//!
3//! This module provides zero-copy query string parsing that handles:
4//! - Key-value pair extraction
5//! - Multi-value parameters (same key appearing multiple times)
6//! - Percent-decoding
7//! - Edge cases (empty values, missing values)
8//!
9//! # Example
10//!
11//! ```
12//! use fastapi_http::QueryString;
13//!
14//! let qs = QueryString::parse("a=1&b=2&a=3");
15//!
16//! // Single value access
17//! assert_eq!(qs.get("a"), Some("1"));
18//! assert_eq!(qs.get("b"), Some("2"));
19//!
20//! // Multi-value access
21//! let a_values: Vec<_> = qs.get_all("a").collect();
22//! assert_eq!(a_values, vec!["1", "3"]);
23//! ```
24
25use std::borrow::Cow;
26
27/// Maximum number of query parameters to parse.
28///
29/// This limit prevents algorithmic complexity DoS attacks where an attacker
30/// sends a query string with thousands of parameters. Parameters beyond this
31/// limit are silently ignored.
32pub const MAX_QUERY_PARAMS: usize = 256;
33
34/// A parsed query string with efficient access to parameters.
35///
36/// Query strings are parsed lazily - the input is stored and parsed
37/// on each access. For repeated access patterns, consider using
38/// `to_pairs()` to materialize the results.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct QueryString<'a> {
41    raw: &'a str,
42}
43
44impl<'a> QueryString<'a> {
45    /// Parse a query string (without the leading `?`).
46    ///
47    /// # Example
48    ///
49    /// ```
50    /// use fastapi_http::QueryString;
51    ///
52    /// let qs = QueryString::parse("name=alice&age=30");
53    /// assert_eq!(qs.get("name"), Some("alice"));
54    /// ```
55    #[must_use]
56    pub fn parse(raw: &'a str) -> Self {
57        Self { raw }
58    }
59
60    /// Returns true if the query string is empty.
61    #[must_use]
62    pub fn is_empty(&self) -> bool {
63        self.raw.is_empty()
64    }
65
66    /// Returns the raw query string.
67    #[must_use]
68    pub fn raw(&self) -> &'a str {
69        self.raw
70    }
71
72    /// Get the first value for a key.
73    ///
74    /// Returns `None` if the key doesn't exist.
75    /// Returns the raw (percent-encoded) value. Use `get_decoded` for decoded values.
76    ///
77    /// # Example
78    ///
79    /// ```
80    /// use fastapi_http::QueryString;
81    ///
82    /// let qs = QueryString::parse("name=alice&name=bob");
83    /// assert_eq!(qs.get("name"), Some("alice")); // First value
84    /// assert_eq!(qs.get("missing"), None);
85    /// ```
86    #[must_use]
87    pub fn get(&self, key: &str) -> Option<&'a str> {
88        self.pairs().find(|(k, _)| *k == key).map(|(_, v)| v)
89    }
90
91    /// Get all values for a key.
92    ///
93    /// Returns an iterator over all values for the given key.
94    ///
95    /// # Example
96    ///
97    /// ```
98    /// use fastapi_http::QueryString;
99    ///
100    /// let qs = QueryString::parse("color=red&color=blue&color=green");
101    /// let colors: Vec<_> = qs.get_all("color").collect();
102    /// assert_eq!(colors, vec!["red", "blue", "green"]);
103    /// ```
104    pub fn get_all(&self, key: &str) -> impl Iterator<Item = &'a str> {
105        self.pairs().filter(move |(k, _)| *k == key).map(|(_, v)| v)
106    }
107
108    /// Get the first value for a key, percent-decoded.
109    ///
110    /// Returns a `Cow` that is borrowed if no decoding was needed,
111    /// or owned if percent-decoding was performed.
112    ///
113    /// # Example
114    ///
115    /// ```
116    /// use fastapi_http::QueryString;
117    ///
118    /// let qs = QueryString::parse("msg=hello%20world");
119    /// assert_eq!(qs.get_decoded("msg").as_deref(), Some("hello world"));
120    /// ```
121    #[must_use]
122    pub fn get_decoded(&self, key: &str) -> Option<Cow<'a, str>> {
123        self.get(key).map(percent_decode)
124    }
125
126    /// Check if a key exists in the query string.
127    ///
128    /// # Example
129    ///
130    /// ```
131    /// use fastapi_http::QueryString;
132    ///
133    /// let qs = QueryString::parse("flag&name=alice");
134    /// assert!(qs.contains("flag"));
135    /// assert!(qs.contains("name"));
136    /// assert!(!qs.contains("missing"));
137    /// ```
138    #[must_use]
139    pub fn contains(&self, key: &str) -> bool {
140        self.pairs().any(|(k, _)| k == key)
141    }
142
143    /// Returns an iterator over all key-value pairs.
144    ///
145    /// Keys without values (like `?flag`) have empty string values.
146    /// Values are NOT percent-decoded; use `pairs_decoded` for that.
147    ///
148    /// # Example
149    ///
150    /// ```
151    /// use fastapi_http::QueryString;
152    ///
153    /// let qs = QueryString::parse("a=1&b=2&flag");
154    /// let pairs: Vec<_> = qs.pairs().collect();
155    /// assert_eq!(pairs, vec![("a", "1"), ("b", "2"), ("flag", "")]);
156    /// ```
157    pub fn pairs(&self) -> impl Iterator<Item = (&'a str, &'a str)> {
158        self.raw
159            .split('&')
160            .filter(|s| !s.is_empty())
161            .take(MAX_QUERY_PARAMS) // Limit to prevent DoS
162            .map(|pair| {
163                if let Some(eq_pos) = pair.find('=') {
164                    (&pair[..eq_pos], &pair[eq_pos + 1..])
165                } else {
166                    // Key without value: "flag" -> ("flag", "")
167                    (pair, "")
168                }
169            })
170    }
171
172    /// Returns an iterator over all key-value pairs, with values percent-decoded.
173    ///
174    /// # Example
175    ///
176    /// ```
177    /// use fastapi_http::QueryString;
178    ///
179    /// let qs = QueryString::parse("name=hello%20world&id=123");
180    /// let pairs: Vec<_> = qs.pairs_decoded().collect();
181    /// assert_eq!(pairs[0].0, "name");
182    /// assert_eq!(&*pairs[0].1, "hello world");
183    /// ```
184    pub fn pairs_decoded(&self) -> impl Iterator<Item = (&'a str, Cow<'a, str>)> {
185        self.pairs().map(|(k, v)| (k, percent_decode(v)))
186    }
187
188    /// Collect all pairs into a vector.
189    ///
190    /// Useful when you need to iterate multiple times.
191    #[must_use]
192    pub fn to_pairs(&self) -> Vec<(&'a str, &'a str)> {
193        self.pairs().collect()
194    }
195
196    /// Count the number of parameters.
197    #[must_use]
198    pub fn len(&self) -> usize {
199        self.pairs().count()
200    }
201}
202
203impl Default for QueryString<'_> {
204    fn default() -> Self {
205        Self { raw: "" }
206    }
207}
208
209/// Percent-decode a string.
210///
211/// Returns a `Cow::Borrowed` if no decoding was needed (most common case),
212/// or `Cow::Owned` if percent sequences were decoded.
213///
214/// Handles:
215/// - Standard percent-encoding (%XX)
216/// - UTF-8 multi-byte sequences
217/// - Plus sign as space (common in form data)
218///
219/// Invalid sequences are left as-is for robustness.
220///
221/// # Example
222///
223/// ```
224/// use fastapi_http::percent_decode;
225///
226/// // No decoding needed - returns borrowed
227/// let simple = percent_decode("hello");
228/// assert!(matches!(simple, std::borrow::Cow::Borrowed(_)));
229///
230/// // Decoding needed - returns owned
231/// let encoded = percent_decode("hello%20world");
232/// assert_eq!(&*encoded, "hello world");
233///
234/// // Plus as space
235/// let plus = percent_decode("hello+world");
236/// assert_eq!(&*plus, "hello world");
237/// ```
238pub fn percent_decode(s: &str) -> Cow<'_, str> {
239    // Fast path: no encoding
240    if !s.contains('%') && !s.contains('+') {
241        return Cow::Borrowed(s);
242    }
243
244    let mut result = Vec::with_capacity(s.len());
245    let bytes = s.as_bytes();
246    let mut i = 0;
247
248    while i < bytes.len() {
249        match bytes[i] {
250            b'%' if i + 2 < bytes.len() => {
251                // Try to decode hex pair
252                if let (Some(hi), Some(lo)) = (hex_digit(bytes[i + 1]), hex_digit(bytes[i + 2])) {
253                    result.push(hi << 4 | lo);
254                    i += 3;
255                } else {
256                    // Invalid hex, keep as-is
257                    result.push(b'%');
258                    i += 1;
259                }
260            }
261            b'+' => {
262                // Plus as space (application/x-www-form-urlencoded)
263                result.push(b' ');
264                i += 1;
265            }
266            b => {
267                result.push(b);
268                i += 1;
269            }
270        }
271    }
272
273    // SAFETY: We only decode valid UTF-8 percent sequences,
274    // and non-encoded bytes pass through unchanged.
275    // Invalid UTF-8 will be handled by from_utf8_lossy.
276    Cow::Owned(String::from_utf8_lossy(&result).into_owned())
277}
278
279/// Convert a hex digit to its numeric value.
280fn hex_digit(b: u8) -> Option<u8> {
281    match b {
282        b'0'..=b'9' => Some(b - b'0'),
283        b'a'..=b'f' => Some(b - b'a' + 10),
284        b'A'..=b'F' => Some(b - b'A' + 10),
285        _ => None,
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292
293    #[test]
294    fn empty_query_string() {
295        let qs = QueryString::parse("");
296        assert!(qs.is_empty());
297        assert_eq!(qs.len(), 0);
298        assert_eq!(qs.get("any"), None);
299    }
300
301    #[test]
302    fn single_param() {
303        let qs = QueryString::parse("name=alice");
304        assert!(!qs.is_empty());
305        assert_eq!(qs.len(), 1);
306        assert_eq!(qs.get("name"), Some("alice"));
307        assert_eq!(qs.get("other"), None);
308    }
309
310    #[test]
311    fn multiple_params() {
312        let qs = QueryString::parse("a=1&b=2&c=3");
313        assert_eq!(qs.len(), 3);
314        assert_eq!(qs.get("a"), Some("1"));
315        assert_eq!(qs.get("b"), Some("2"));
316        assert_eq!(qs.get("c"), Some("3"));
317    }
318
319    #[test]
320    fn duplicate_keys() {
321        let qs = QueryString::parse("a=1&b=2&a=3");
322
323        // get() returns first value
324        assert_eq!(qs.get("a"), Some("1"));
325
326        // get_all() returns all values
327        let all_a: Vec<_> = qs.get_all("a").collect();
328        assert_eq!(all_a, vec!["1", "3"]);
329    }
330
331    #[test]
332    fn empty_value() {
333        let qs = QueryString::parse("name=&age=30");
334        assert_eq!(qs.get("name"), Some(""));
335        assert_eq!(qs.get("age"), Some("30"));
336    }
337
338    #[test]
339    fn key_without_value() {
340        let qs = QueryString::parse("flag&name=alice");
341        assert!(qs.contains("flag"));
342        assert_eq!(qs.get("flag"), Some(""));
343        assert_eq!(qs.get("name"), Some("alice"));
344    }
345
346    #[test]
347    fn percent_encoded_value() {
348        let qs = QueryString::parse("msg=hello%20world");
349        assert_eq!(qs.get("msg"), Some("hello%20world")); // raw
350        assert_eq!(qs.get_decoded("msg").as_deref(), Some("hello world")); // decoded
351    }
352
353    #[test]
354    fn plus_as_space() {
355        let qs = QueryString::parse("msg=hello+world");
356        assert_eq!(qs.get("msg"), Some("hello+world")); // raw
357        assert_eq!(qs.get_decoded("msg").as_deref(), Some("hello world")); // decoded
358    }
359
360    #[test]
361    fn utf8_encoded() {
362        // "café" encoded: caf%C3%A9
363        let qs = QueryString::parse("word=caf%C3%A9");
364        assert_eq!(qs.get_decoded("word").as_deref(), Some("café"));
365    }
366
367    #[test]
368    fn special_chars_encoded() {
369        // & encoded as %26, = encoded as %3D
370        let qs = QueryString::parse("data=a%26b%3Dc");
371        assert_eq!(qs.get_decoded("data").as_deref(), Some("a&b=c"));
372    }
373
374    #[test]
375    fn pairs_iterator() {
376        let qs = QueryString::parse("a=1&b=2&c=3");
377        let pairs: Vec<_> = qs.pairs().collect();
378        assert_eq!(pairs, vec![("a", "1"), ("b", "2"), ("c", "3")]);
379    }
380
381    #[test]
382    fn pairs_decoded_iterator() {
383        let qs = QueryString::parse("name=hello%20world&id=123");
384        let pairs: Vec<_> = qs.pairs_decoded().collect();
385        assert_eq!(pairs[0].0, "name");
386        assert_eq!(&*pairs[0].1, "hello world");
387        assert_eq!(pairs[1].0, "id");
388        assert_eq!(&*pairs[1].1, "123");
389    }
390
391    #[test]
392    fn to_pairs() {
393        let qs = QueryString::parse("x=1&y=2");
394        let pairs = qs.to_pairs();
395        assert_eq!(pairs, vec![("x", "1"), ("y", "2")]);
396    }
397
398    #[test]
399    fn contains() {
400        let qs = QueryString::parse("a=1&b=2");
401        assert!(qs.contains("a"));
402        assert!(qs.contains("b"));
403        assert!(!qs.contains("c"));
404    }
405
406    #[test]
407    fn raw_accessor() {
408        let qs = QueryString::parse("a=1&b=2");
409        assert_eq!(qs.raw(), "a=1&b=2");
410    }
411
412    #[test]
413    fn trailing_ampersand() {
414        let qs = QueryString::parse("a=1&b=2&");
415        assert_eq!(qs.len(), 2); // Empty segment is filtered
416        assert_eq!(qs.get("a"), Some("1"));
417        assert_eq!(qs.get("b"), Some("2"));
418    }
419
420    #[test]
421    fn leading_ampersand() {
422        let qs = QueryString::parse("&a=1&b=2");
423        assert_eq!(qs.len(), 2);
424        assert_eq!(qs.get("a"), Some("1"));
425        assert_eq!(qs.get("b"), Some("2"));
426    }
427
428    #[test]
429    fn percent_decode_no_encoding() {
430        let s = "hello";
431        let decoded = percent_decode(s);
432        assert!(matches!(decoded, Cow::Borrowed(_)));
433        assert_eq!(&*decoded, "hello");
434    }
435
436    #[test]
437    fn percent_decode_simple() {
438        assert_eq!(&*percent_decode("hello%20world"), "hello world");
439        assert_eq!(&*percent_decode("%2F"), "/");
440        assert_eq!(&*percent_decode("%3D"), "=");
441    }
442
443    #[test]
444    fn percent_decode_invalid_hex() {
445        // Invalid hex should be kept as-is
446        assert_eq!(&*percent_decode("%ZZ"), "%ZZ");
447        assert_eq!(&*percent_decode("%2"), "%2"); // Incomplete
448    }
449
450    #[test]
451    fn percent_decode_mixed() {
452        assert_eq!(&*percent_decode("a%20b%20c"), "a b c");
453        assert_eq!(&*percent_decode("hello+world%21"), "hello world!");
454    }
455
456    #[test]
457    fn hex_digit_values() {
458        assert_eq!(hex_digit(b'0'), Some(0));
459        assert_eq!(hex_digit(b'9'), Some(9));
460        assert_eq!(hex_digit(b'a'), Some(10));
461        assert_eq!(hex_digit(b'f'), Some(15));
462        assert_eq!(hex_digit(b'A'), Some(10));
463        assert_eq!(hex_digit(b'F'), Some(15));
464        assert_eq!(hex_digit(b'g'), None);
465        assert_eq!(hex_digit(b'Z'), None);
466    }
467
468    #[test]
469    fn default_is_empty() {
470        let qs = QueryString::default();
471        assert!(qs.is_empty());
472        assert_eq!(qs.len(), 0);
473    }
474
475    #[test]
476    fn acceptance_criteria_test() {
477        // Test the exact example from acceptance criteria:
478        // Parses ?a=1&b=2&a=3 into multi-value map correctly
479        let qs = QueryString::parse("a=1&b=2&a=3");
480
481        // First value for 'a'
482        assert_eq!(qs.get("a"), Some("1"));
483
484        // All values for 'a'
485        let all_a: Vec<_> = qs.get_all("a").collect();
486        assert_eq!(all_a, vec!["1", "3"]);
487
488        // Value for 'b'
489        assert_eq!(qs.get("b"), Some("2"));
490
491        // Total count
492        assert_eq!(qs.len(), 3);
493    }
494}