Skip to main content

hitbox_http/
query.rs

1//! Query string parsing utilities.
2//!
3//! Provides [`parse`] for deserializing URL query strings into key-value maps.
4//! Used internally by query predicates and extractors.
5//!
6//! # Supported Syntax
7//!
8//! - Simple parameters: `key=value`
9//! - Multiple parameters: `key1=value1&key2=value2`
10//! - Array syntax: `color[]=red&color[]=blue`
11//! - Nested parameters: `filter[status]=active` (up to 5 levels deep)
12//!
13//! # Security
14//!
15//! Nesting depth is limited to 5 levels to prevent DoS attacks via deeply
16//! nested queries that could cause stack overflow or memory exhaustion.
17//!
18//! # Examples
19//!
20//! ```
21//! use hitbox_http::query::{parse, Value};
22//!
23//! let params = parse("page=1&limit=10").unwrap();
24//! assert_eq!(params.get("page").unwrap().inner(), vec!["1"]);
25//!
26//! // Array parameters
27//! let params = parse("color[]=red&color[]=blue").unwrap();
28//! assert_eq!(params.get("color").unwrap().inner(), vec!["red", "blue"]);
29//! ```
30
31use serde::Deserialize;
32use std::collections::HashMap;
33
34/// Maximum nesting depth for query string parsing.
35///
36/// Limits structures like `a[b][c][d][e][f]=value` to prevent DoS attacks
37/// via deeply nested queries that could cause stack overflow or memory exhaustion.
38const MAX_QUERY_DEPTH: usize = 5;
39
40/// A query parameter value, either a single string or an array of strings.
41///
42/// # Examples
43///
44/// ```
45/// use hitbox_http::query::{parse, Value};
46///
47/// // Scalar value
48/// let params = parse("name=alice").unwrap();
49/// assert!(matches!(params.get("name"), Some(Value::Scalar(_))));
50///
51/// // Array value (using bracket syntax)
52/// let params = parse("tags[]=rust&tags[]=http").unwrap();
53/// assert!(matches!(params.get("tags"), Some(Value::Array(_))));
54/// ```
55#[derive(Debug, Deserialize, PartialEq, Eq)]
56#[serde(untagged)]
57pub enum Value {
58    /// A single parameter value.
59    Scalar(String),
60    /// Multiple values for the same parameter (array syntax).
61    Array(Vec<String>),
62}
63
64impl Value {
65    /// Returns all values as a vector.
66    ///
67    /// For [`Scalar`](Self::Scalar), returns a single-element vector.
68    /// For [`Array`](Self::Array), returns all elements.
69    pub fn inner(&self) -> Vec<String> {
70        match self {
71            Value::Scalar(value) => vec![value.to_owned()],
72            Value::Array(values) => values.to_owned(),
73        }
74    }
75
76    /// Returns `true` if the value contains the given string.
77    pub fn contains(&self, value: &String) -> bool {
78        self.inner().contains(value)
79    }
80}
81
82/// Parses a query string into a map of key-value pairs.
83///
84/// Returns `None` if:
85/// - The query string is malformed
86/// - Nesting depth exceeds 5 levels
87///
88/// # Examples
89///
90/// ```
91/// use hitbox_http::query::parse;
92///
93/// let params = parse("page=1&sort=name").unwrap();
94/// assert_eq!(params.get("page").unwrap().inner(), vec!["1"]);
95/// assert_eq!(params.get("sort").unwrap().inner(), vec!["name"]);
96///
97/// // Exceeds depth limit
98/// assert!(parse("a[b][c][d][e][f][g]=1").is_none());
99/// ```
100pub fn parse(value: &str) -> Option<HashMap<String, Value>> {
101    serde_qs::Config::new(MAX_QUERY_DEPTH, false)
102        .deserialize_str(value)
103        .ok()
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_parse_valid_one() {
112        let hash_map = parse("key=value").unwrap();
113        let value = hash_map.get("key").unwrap();
114        assert_eq!(value.inner(), vec!["value"]);
115    }
116
117    #[test]
118    fn test_parse_valid_multiple() {
119        let hash_map = parse("key-one=value-one&key-two=value-two&key-three=value-three").unwrap();
120        let value = hash_map.get("key-one").unwrap();
121        assert_eq!(value.inner(), vec!["value-one"]);
122        let value = hash_map.get("key-two").unwrap();
123        assert_eq!(value.inner(), vec!["value-two"]);
124        let value = hash_map.get("key-three").unwrap();
125        assert_eq!(value.inner(), vec!["value-three"]);
126    }
127
128    #[test]
129    fn test_parse_not_valid() {
130        let hash_map = parse("   wrong   ").unwrap();
131        assert_eq!(hash_map.len(), 1);
132    }
133
134    #[test]
135    fn test_parse_exceeds_depth_returns_none() {
136        // Nesting depth exceeds configured limit (5), should return None
137        assert!(parse("a[b][c][d][e][f][g]=1").is_none());
138    }
139
140    #[test]
141    fn test_parse_array_bracket_syntax() {
142        // Note: serde_qs only supports bracket syntax for arrays (color[]=a&color[]=b)
143        // Repeated keys without brackets (color=a&color=b) are not supported
144        let hash_map = parse("color[]=red&color[]=blue&color[]=green").unwrap();
145        let value = hash_map.get("color").unwrap();
146        assert_eq!(value.inner(), vec!["red", "blue", "green"]);
147    }
148}