@@ -25,8 +25,22 @@ use std::fmt::{Debug, Display, Formatter};
2525
2626use percent_encoding:: { utf8_percent_encode, AsciiSet , CONTROLS } ;
2727
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
30+ . add ( b' ' )
31+ . add ( b'"' )
32+ . add ( b'#' )
33+ . add ( b'<' )
34+ . add ( b'>' )
35+ // The following values are not strictly required by RFC 3986 but could help resolving recursion
36+ // where a URL is passed as a value. In these cases, occurrences of equal signs and ampersands
37+ // could break parsing.
38+ // By a similar logic, encoding the percent sign helps to resolve ambiguity.
39+ // The plus sign is also added to the set as to not confuse it with a space.
40+ . add ( b'%' )
41+ . add ( b'&' )
42+ . add ( b'=' )
43+ . add ( b'+' ) ;
3044
3145/// A query string builder for percent encoding key-value pairs.
3246///
@@ -229,8 +243,8 @@ impl Display for QueryString {
229243 write ! (
230244 f,
231245 "{key}={value}" ,
232- key = utf8_percent_encode( & pair. key, FRAGMENT ) ,
233- value = utf8_percent_encode( & pair. value, FRAGMENT )
246+ key = utf8_percent_encode( & pair. key, QUERY ) ,
247+ value = utf8_percent_encode( & pair. value, QUERY )
234248 ) ?;
235249 }
236250 Ok ( ( ) )
@@ -327,4 +341,55 @@ mod tests {
327341 "https://example.com/?q=apple&q=pear&answer=42"
328342 ) ;
329343 }
344+
345+ #[ test]
346+ fn test_characters ( ) {
347+ let tests = vec ! [
348+ ( "space" , " " , "%20" ) ,
349+ ( "double_quote" , "\" " , "%22" ) ,
350+ ( "hash" , "#" , "%23" ) ,
351+ ( "less_than" , "<" , "%3C" ) ,
352+ ( "equals" , "=" , "%3D" ) ,
353+ ( "greater_than" , ">" , "%3E" ) ,
354+ ( "percent" , "%" , "%25" ) ,
355+ ( "ampersand" , "&" , "%26" ) ,
356+ ( "plus" , "+" , "%2B" ) ,
357+ //
358+ ( "dollar" , "$" , "$" ) ,
359+ ( "single_quote" , "'" , "'" ) ,
360+ ( "comma" , "," , "," ) ,
361+ ( "forward_slash" , "/" , "/" ) ,
362+ ( "colon" , ":" , ":" ) ,
363+ ( "semicolon" , ";" , ";" ) ,
364+ ( "question_mark" , "?" , "?" ) ,
365+ ( "at" , "@" , "@" ) ,
366+ ( "left_bracket" , "[" , "[" ) ,
367+ ( "backslash" , "\\ " , "\\ " ) ,
368+ ( "right_bracket" , "]" , "]" ) ,
369+ ( "caret" , "^" , "^" ) ,
370+ ( "underscore" , "_" , "_" ) ,
371+ ( "grave" , "^" , "^" ) ,
372+ ( "left_curly" , "{" , "{" ) ,
373+ ( "pipe" , "|" , "|" ) ,
374+ ( "right_curly" , "}" , "}" ) ,
375+ ] ;
376+
377+ let mut qs = QueryString :: new ( ) ;
378+ for ( key, value, _) in & tests {
379+ qs. push ( key. to_string ( ) , value. to_string ( ) ) ;
380+ }
381+
382+ let mut expected = String :: new ( ) ;
383+ for ( i, ( key, _, value) ) in tests. iter ( ) . enumerate ( ) {
384+ if i > 0 {
385+ expected. push ( '&' ) ;
386+ }
387+ expected. push_str ( & format ! ( "{key}={value}" ) ) ;
388+ }
389+
390+ assert_eq ! (
391+ format!( "https://example.com/{qs}" ) ,
392+ format!( "https://example.com/?{expected}" )
393+ ) ;
394+ }
330395}
0 commit comments