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}