/rust/registry/src/index.crates.io-1949cf8c6b5b557f/ureq-3.4.2/src/query.rs
Line | Count | Source |
1 | | use std::borrow::Cow; |
2 | | use std::fmt; |
3 | | use std::iter::Enumerate; |
4 | | use std::ops::Deref; |
5 | | use std::str::Chars; |
6 | | |
7 | | use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode}; |
8 | | |
9 | | /// AsciiSet for characters that need to be percent-encoded in URL query parameters. |
10 | | /// |
11 | | /// This set follows URL specification from <https://url.spec.whatwg.org/> |
12 | | pub const ENCODED_IN_QUERY: &AsciiSet = &CONTROLS |
13 | | .add(b' ') |
14 | | .add(b'"') |
15 | | .add(b'#') |
16 | | .add(b'$') |
17 | | .add(b'%') |
18 | | .add(b'&') |
19 | | .add(b'\'') // Single quote should be encoded according to the URL specs |
20 | | .add(b'+') |
21 | | .add(b',') |
22 | | .add(b'/') |
23 | | .add(b':') |
24 | | .add(b';') |
25 | | .add(b'<') |
26 | | .add(b'=') |
27 | | .add(b'>') |
28 | | .add(b'?') |
29 | | .add(b'@') |
30 | | .add(b'[') |
31 | | .add(b'\\') |
32 | | .add(b']') |
33 | | .add(b'^') |
34 | | .add(b'`') |
35 | | .add(b'{') |
36 | | .add(b'|') |
37 | | .add(b'}'); |
38 | | |
39 | | #[derive(Clone)] |
40 | | pub(crate) struct QueryParam<'a> { |
41 | | source: Source<'a>, |
42 | | } |
43 | | |
44 | | #[derive(Clone)] |
45 | | enum Source<'a> { |
46 | | Borrowed(&'a str), |
47 | | Owned(String), |
48 | | } |
49 | | |
50 | | /// Percent-encode a string using the ENCODED_IN_QUERY set. |
51 | 0 | pub fn url_enc(i: &str) -> Cow<str> { |
52 | 0 | utf8_percent_encode(i, ENCODED_IN_QUERY).into() |
53 | 0 | } |
54 | | |
55 | | /// Percent-encode a string using the ENCODED_IN_QUERY set, but replace encoded `%20` with `+`. |
56 | 0 | pub fn form_url_enc(i: &str) -> Cow<str> { |
57 | 0 | let mut iter = utf8_percent_encode(i, ENCODED_IN_QUERY).map(|part| match part { |
58 | 0 | "%20" => "+", |
59 | 0 | _ => part, |
60 | 0 | }); |
61 | | |
62 | | // We try to avoid allocating if we can (returning a Cow). |
63 | 0 | match iter.next() { |
64 | 0 | None => "".into(), |
65 | 0 | Some(first) => match iter.next() { |
66 | | // Case avoids allocation |
67 | 0 | None => first.into(), |
68 | | // Following allocates |
69 | 0 | Some(second) => { |
70 | 0 | let mut string = first.to_owned(); |
71 | 0 | string.push_str(second); |
72 | 0 | string.extend(iter); |
73 | 0 | string.into() |
74 | | } |
75 | | }, |
76 | | } |
77 | 0 | } |
78 | | |
79 | | impl<'a> QueryParam<'a> { |
80 | | /// Create a new key-value pair with both the key and value percent-encoded. |
81 | 0 | pub fn new_key_value(param: &str, value: &str) -> QueryParam<'static> { |
82 | 0 | let s = format!("{}={}", url_enc(param), url_enc(value)); |
83 | 0 | QueryParam { |
84 | 0 | source: Source::Owned(s), |
85 | 0 | } |
86 | 0 | } |
87 | | |
88 | | /// Create a new key-value pair without percent-encoding. |
89 | | /// |
90 | | /// This is used by query_raw() to add parameters that are already encoded |
91 | | /// or that should not be encoded. |
92 | 0 | pub fn new_key_value_raw(param: &str, value: &str) -> QueryParam<'static> { |
93 | 0 | let s = format!("{}={}", param, value); |
94 | 0 | QueryParam { |
95 | 0 | source: Source::Owned(s), |
96 | 0 | } |
97 | 0 | } |
98 | | |
99 | 0 | fn as_str(&self) -> &str { |
100 | 0 | match &self.source { |
101 | 0 | Source::Borrowed(v) => v, |
102 | 0 | Source::Owned(v) => v.as_str(), |
103 | | } |
104 | 0 | } |
105 | | } |
106 | | |
107 | 0 | pub(crate) fn parse_query_params(query_string: &str) -> impl Iterator<Item = QueryParam<'_>> { |
108 | 0 | assert!(query_string.is_ascii()); |
109 | 0 | QueryParamIterator(query_string, query_string.chars().enumerate()) |
110 | 0 | } |
111 | | |
112 | | struct QueryParamIterator<'a>(&'a str, Enumerate<Chars<'a>>); |
113 | | |
114 | | impl<'a> Iterator for QueryParamIterator<'a> { |
115 | | type Item = QueryParam<'a>; |
116 | | |
117 | 0 | fn next(&mut self) -> Option<Self::Item> { |
118 | 0 | let mut first = None; |
119 | 0 | let mut value = None; |
120 | 0 | let mut separator = None; |
121 | | |
122 | 0 | for (n, c) in self.1.by_ref() { |
123 | 0 | if first.is_none() { |
124 | 0 | first = Some(n); |
125 | 0 | } |
126 | 0 | if value.is_none() && c == '=' { |
127 | 0 | value = Some(n + 1); |
128 | 0 | } |
129 | 0 | if c == '&' { |
130 | 0 | separator = Some(n); |
131 | 0 | break; |
132 | 0 | } |
133 | | } |
134 | | |
135 | 0 | if let Some(start) = first { |
136 | 0 | let end = separator.unwrap_or(self.0.len()); |
137 | 0 | let chunk = &self.0[start..end]; |
138 | 0 | return Some(QueryParam { |
139 | 0 | source: Source::Borrowed(chunk), |
140 | 0 | }); |
141 | 0 | } |
142 | | |
143 | 0 | None |
144 | 0 | } |
145 | | } |
146 | | |
147 | | impl<'a> fmt::Debug for QueryParam<'a> { |
148 | 0 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
149 | 0 | f.debug_tuple("QueryParam").field(&self.as_str()).finish() |
150 | 0 | } |
151 | | } |
152 | | |
153 | | impl<'a> fmt::Display for QueryParam<'a> { |
154 | 0 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
155 | 0 | match &self.source { |
156 | 0 | Source::Borrowed(v) => write!(f, "{}", v), |
157 | 0 | Source::Owned(v) => write!(f, "{}", v), |
158 | | } |
159 | 0 | } |
160 | | } |
161 | | |
162 | | impl<'a> Deref for QueryParam<'a> { |
163 | | type Target = str; |
164 | | |
165 | 0 | fn deref(&self) -> &Self::Target { |
166 | 0 | self.as_str() |
167 | 0 | } |
168 | | } |
169 | | |
170 | | impl<'a> PartialEq for QueryParam<'a> { |
171 | 0 | fn eq(&self, other: &Self) -> bool { |
172 | 0 | self.as_str() == other.as_str() |
173 | 0 | } |
174 | | } |
175 | | |
176 | | #[cfg(test)] |
177 | | mod test { |
178 | | use super::*; |
179 | | |
180 | | use crate::http::Uri; |
181 | | |
182 | | #[test] |
183 | | fn query_string_does_not_start_with_question_mark() { |
184 | | let u: Uri = "https://foo.com/qwe?abc=qwe".parse().unwrap(); |
185 | | assert_eq!(u.query(), Some("abc=qwe")); |
186 | | } |
187 | | |
188 | | #[test] |
189 | | fn percent_encoding_is_not_decoded() { |
190 | | let u: Uri = "https://foo.com/qwe?abc=%20123".parse().unwrap(); |
191 | | assert_eq!(u.query(), Some("abc=%20123")); |
192 | | } |
193 | | |
194 | | #[test] |
195 | | fn fragments_are_not_a_thing() { |
196 | | let u: Uri = "https://foo.com/qwe?abc=qwe#yaz".parse().unwrap(); |
197 | | assert_eq!(u.to_string(), "https://foo.com/qwe?abc=qwe"); |
198 | | } |
199 | | |
200 | | fn p(s: &str) -> Vec<String> { |
201 | | parse_query_params(s).map(|q| q.to_string()).collect() |
202 | | } |
203 | | |
204 | | #[test] |
205 | | fn parse_query_string() { |
206 | | assert_eq!(parse_query_params("").next(), None); |
207 | | assert_eq!(p("&"), vec![""]); |
208 | | assert_eq!(p("="), vec!["="]); |
209 | | assert_eq!(p("&="), vec!["", "="]); |
210 | | assert_eq!(p("foo=bar"), vec!["foo=bar"]); |
211 | | assert_eq!(p("foo=bar&"), vec!["foo=bar"]); |
212 | | assert_eq!(p("foo=bar&foo2=bar2"), vec!["foo=bar", "foo2=bar2"]); |
213 | | } |
214 | | |
215 | | #[test] |
216 | | fn do_not_url_encode_some_things() { |
217 | | const NOT_ENCODE: &str = "!()*-._~"; |
218 | | let q = QueryParam::new_key_value("key", NOT_ENCODE); |
219 | | assert_eq!(q.as_str(), format!("key={}", NOT_ENCODE)); |
220 | | } |
221 | | |
222 | | #[test] |
223 | | fn special_encoding_space_for_form() { |
224 | | let value = "value with spaces and 'quotes'"; |
225 | | let form = form_url_enc(value); |
226 | | assert_eq!(form.as_ref(), "value+with+spaces+and+%27quotes%27"); |
227 | | } |
228 | | |
229 | | #[test] |
230 | | fn do_encode_single_quote() { |
231 | | let value = "value'with'quotes"; |
232 | | let q = QueryParam::new_key_value("key", value); |
233 | | assert_eq!(q.as_str(), "key=value%27with%27quotes"); |
234 | | } |
235 | | |
236 | | #[test] |
237 | | fn raw_query_param_no_encoding() { |
238 | | // Use URI-valid characters for the raw param test |
239 | | let value = "value-without-spaces&special='chars'"; |
240 | | let q = QueryParam::new_key_value_raw("key", value); |
241 | | assert_eq!(q.as_str(), format!("key={}", value)); |
242 | | |
243 | | // Verify that symbols like &=+?/ remain unencoded in raw mode |
244 | | // but are encoded in normal mode |
245 | | let special_symbols = "symbols&=+?/'"; |
246 | | let q_raw = QueryParam::new_key_value_raw("raw", special_symbols); |
247 | | let q_encoded = QueryParam::new_key_value("encoded", special_symbols); |
248 | | |
249 | | // Raw should preserve all special chars, encoded should encode them |
250 | | assert_eq!(q_raw.as_str(), "raw=symbols&=+?/'"); |
251 | | assert_ne!(q_raw.as_str(), q_encoded.as_str()); |
252 | | } |
253 | | } |