1use smallvec::SmallVec;
9use std::borrow::Cow;
10use std::collections::HashMap;
11
12pub type QueryPairs = SmallVec<[(String, String); 8]>;
14
15#[derive(Debug, Clone, Copy)]
22pub struct QueryView<'a> {
23 pairs: &'a [(String, String)],
24}
25
26impl<'a> QueryView<'a> {
27 #[inline]
28 pub(crate) fn new(pairs: &'a [(String, String)]) -> Self {
29 Self { pairs }
30 }
31
32 #[inline]
34 pub fn get(&self, key: &str) -> Option<&'a str> {
35 self.pairs
36 .iter()
37 .find(|(k, _)| k == key)
38 .map(|(_, v)| v.as_str())
39 }
40
41 #[inline]
43 pub fn contains_key(&self, key: &str) -> bool {
44 self.pairs.iter().any(|(k, _)| k == key)
45 }
46
47 #[inline]
49 pub fn get_all(&self, key: &'a str) -> impl Iterator<Item = &'a str> + 'a {
50 self.pairs
51 .iter()
52 .filter(move |(k, _)| k == key)
53 .map(|(_, v)| v.as_str())
54 }
55
56 #[inline]
58 pub fn iter(&self) -> impl Iterator<Item = (&'a str, &'a str)> {
59 self.pairs.iter().map(|(k, v)| (k.as_str(), v.as_str()))
60 }
61
62 #[inline]
64 pub fn len(&self) -> usize {
65 self.pairs.len()
66 }
67
68 #[inline]
70 pub fn is_empty(&self) -> bool {
71 self.pairs.is_empty()
72 }
73
74 pub fn to_hash_map(&self) -> HashMap<String, String> {
80 self.pairs.iter().cloned().collect()
81 }
82}
83
84impl<'a> IntoIterator for QueryView<'a> {
85 type Item = (&'a str, &'a str);
86 type IntoIter = std::iter::Map<
87 std::slice::Iter<'a, (String, String)>,
88 fn(&'a (String, String)) -> (&'a str, &'a str),
89 >;
90
91 fn into_iter(self) -> Self::IntoIter {
92 fn as_strs(pair: &(String, String)) -> (&str, &str) {
93 (pair.0.as_str(), pair.1.as_str())
94 }
95 self.pairs.iter().map(as_strs as fn(_) -> _)
96 }
97}
98
99pub(crate) fn parse(query: &str) -> QueryPairs {
106 let mut out = QueryPairs::new();
107 for pair in query.split('&') {
108 if pair.is_empty() {
109 continue;
110 }
111 let (raw_key, raw_value) = match pair.split_once('=') {
112 Some((k, v)) => (k, v),
113 None => (pair, ""),
114 };
115 if raw_key.is_empty() {
116 continue;
117 }
118 out.push((decode(raw_key).into_owned(), decode(raw_value).into_owned()));
119 }
120 out
121}
122
123fn decode(s: &str) -> Cow<'_, str> {
128 if !s.contains('%') && !s.contains('+') {
129 return Cow::Borrowed(s);
130 }
131
132 let bytes = s.as_bytes();
133 let mut out = Vec::with_capacity(bytes.len());
134 let mut i = 0;
135 while i < bytes.len() {
136 match bytes[i] {
137 b'+' => {
138 out.push(b' ');
139 i += 1;
140 }
141 b'%' => {
142 match bytes
145 .get(i + 1..i + 3)
146 .and_then(|h| std::str::from_utf8(h).ok())
147 .and_then(|h| u8::from_str_radix(h, 16).ok())
148 {
149 Some(byte) => {
150 out.push(byte);
151 i += 3;
152 }
153 None => {
154 out.push(b'%');
156 i += 1;
157 }
158 }
159 }
160 b => {
161 out.push(b);
162 i += 1;
163 }
164 }
165 }
166
167 match String::from_utf8(out) {
168 Ok(decoded) => Cow::Owned(decoded),
169 Err(_) => Cow::Borrowed(s),
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use crate::HttpRequest;
177
178 #[test]
179 fn parses_on_first_access_and_decodes() {
180 let req = HttpRequest::new("GET", "/s?q=hello%20world&page=2");
181 let q = req.query();
182 assert_eq!(q.get("q"), Some("hello world"));
183 assert_eq!(q.get("page"), Some("2"));
184 assert_eq!(q.get("absent"), None);
185 assert_eq!(q.len(), 2);
186 }
187
188 #[test]
189 fn plus_is_a_space_and_percent_escapes_decode() {
190 let req = HttpRequest::new("GET", "/s?a=x+y&b=%2Fpath&c=%E2%9C%93");
191 let q = req.query();
192 assert_eq!(q.get("a"), Some("x y"));
193 assert_eq!(q.get("b"), Some("/path"));
194 assert_eq!(q.get("c"), Some("✓"));
195 }
196
197 #[test]
198 fn repeated_keys_are_all_reachable_and_get_returns_the_first() {
199 let req = HttpRequest::new("GET", "/s?tag=a&tag=b");
200 let q = req.query();
201 assert_eq!(q.get("tag"), Some("a"));
202 assert_eq!(q.get_all("tag").collect::<Vec<_>>(), vec!["a", "b"]);
203 }
204
205 #[test]
206 fn no_query_string_is_an_empty_view_not_a_panic() {
207 let req = HttpRequest::new("GET", "/s");
208 assert!(req.query().is_empty());
209 assert_eq!(req.query_string(), None);
210 assert_eq!(req.query_param("x"), None);
211 }
212
213 #[test]
214 fn malformed_input_degrades_rather_than_failing() {
215 let req = HttpRequest::new("GET", "/s?flag&empty=&=novalue&bad=%zz&trunc=%2");
219 let q = req.query();
220 assert_eq!(q.get("flag"), Some(""));
221 assert_eq!(q.get("empty"), Some(""));
222 assert_eq!(q.get("bad"), Some("%zz"));
225 assert_eq!(q.get("trunc"), Some("%2"));
226 assert_eq!(q.len(), 4);
228 }
229
230 #[test]
231 fn the_view_is_memoized_across_calls() {
232 let req = HttpRequest::new("GET", "/s?a=1");
233 let first = req.query().get("a").map(str::to_owned);
234 let second = req.query().get("a").map(str::to_owned);
235 assert_eq!(first, second);
236 let p1 = req.query().iter().next().map(|(k, _)| k.as_ptr());
238 let p2 = req.query().iter().next().map(|(k, _)| k.as_ptr());
239 assert_eq!(p1, p2);
240 }
241
242 #[test]
243 fn cloning_a_request_does_not_carry_a_stale_cache() {
244 let req = HttpRequest::new("GET", "/s?a=1");
245 assert_eq!(req.query().get("a"), Some("1"));
246 let mut clone = req.clone();
247 clone.path = crate::ByteStr::from("/s?a=2");
248 assert_eq!(clone.query().get("a"), Some("2"));
250 }
251}