Coverage Report

Created: 2026-09-14 07:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}