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