|
23 | 23 |
|
24 | 24 | use std::fmt::{Debug, Display, Formatter}; |
25 | 25 |
|
26 | | -use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; |
| 26 | +use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode}; |
27 | 27 |
|
28 | | -/// https://url.spec.whatwg.org/#fragment-percent-encode-set |
29 | | -const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`'); |
| 28 | +/// https://url.spec.whatwg.org/#query-percent-encode-set |
| 29 | +const QUERY: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'#').add(b'<').add(b'>') |
| 30 | + // The following values are not strictly required by RFC 3986 but could help resolving recursion |
| 31 | + // where a URL is passed as a value. In these cases, occurrences of equal signs and ampersands |
| 32 | + // could break parsing. |
| 33 | + // By a similar logic, encoding the percent sign helps to resolve ambiguity. |
| 34 | + // The plus sign is also added to the set as to not confuse it with a space. |
| 35 | + .add(b'%').add(b'&').add(b'=').add(b'+'); |
30 | 36 |
|
31 | 37 | /// A query string builder for percent encoding key-value pairs. |
32 | 38 | /// |
@@ -229,8 +235,8 @@ impl Display for QueryString { |
229 | 235 | write!( |
230 | 236 | f, |
231 | 237 | "{key}={value}", |
232 | | - key = utf8_percent_encode(&pair.key, FRAGMENT), |
233 | | - value = utf8_percent_encode(&pair.value, FRAGMENT) |
| 238 | + key = utf8_percent_encode(&pair.key, QUERY), |
| 239 | + value = utf8_percent_encode(&pair.value, QUERY) |
234 | 240 | )?; |
235 | 241 | } |
236 | 242 | Ok(()) |
@@ -327,4 +333,54 @@ mod tests { |
327 | 333 | "https://example.com/?q=apple&q=pear&answer=42" |
328 | 334 | ); |
329 | 335 | } |
| 336 | + |
| 337 | + #[test] |
| 338 | + fn test_characters() { |
| 339 | + let tests = vec![ |
| 340 | + ("space", " ", "%20"), |
| 341 | + ("double_quote", "\"", "%22"), |
| 342 | + ("hash", "#", "%23"), |
| 343 | + ("less_than", "<", "%3C"), |
| 344 | + ("equals", "=", "%3D"), |
| 345 | + ("greater_than", ">", "%3E"), |
| 346 | + ("percent", "%", "%25"), |
| 347 | + ("ampersand", "&", "%26"), |
| 348 | + ("plus", "+", "%2B"), |
| 349 | + // |
| 350 | + ("dollar", "$", "$"), |
| 351 | + ("single_quote", "'", "'"), |
| 352 | + ("comma", ",", ","), |
| 353 | + ("forward_slash", "/", "/"), |
| 354 | + ("colon", ":", ":"), |
| 355 | + ("semicolon", ";", ";"), |
| 356 | + ("question_mark", "?", "?"), |
| 357 | + ("at", "@", "@"), |
| 358 | + ("left_bracket", "[", "["), |
| 359 | + ("backslash", "\\", "\\"), |
| 360 | + ("right_bracket", "]", "]"), |
| 361 | + ("caret", "^", "^"), |
| 362 | + ("underscore", "_", "_"), |
| 363 | + ("grave", "^", "^"), |
| 364 | + ("left_curly", "{", "{"), |
| 365 | + ("pipe", "|", "|"), |
| 366 | + ("right_curly", "}", "}"), |
| 367 | + ]; |
| 368 | + |
| 369 | + let mut qs = QueryString::new(); |
| 370 | + for (key, value, _) in &tests { |
| 371 | + qs.push(key.to_string(), value.to_string()); |
| 372 | + } |
| 373 | + |
| 374 | + let mut expected = String::new(); |
| 375 | + for (i, (key, _, value)) in tests.iter().enumerate() { |
| 376 | + if i > 0 { |
| 377 | + expected.push('&'); |
| 378 | + } |
| 379 | + expected.push_str(&format!("{key}={value}")); |
| 380 | + } |
| 381 | + |
| 382 | + assert_eq!( |
| 383 | + format!("https://example.com/{qs}"), |
| 384 | + format!("https://example.com/?{expected}")); |
| 385 | + } |
330 | 386 | } |
0 commit comments