1- use { crate :: common:: * , url :: form_urlencoded :: byte_serialize as urlencode } ;
1+ use crate :: common:: * ;
22
33#[ derive( Clone , Debug , PartialEq ) ]
44pub ( crate ) struct MagnetLink {
5+ pub ( crate ) indices : BTreeSet < u64 > ,
56 pub ( crate ) infohash : Infohash ,
67 pub ( crate ) name : Option < String > ,
78 pub ( crate ) peers : Vec < HostPort > ,
89 pub ( crate ) trackers : Vec < Url > ,
9- pub ( crate ) indices : BTreeSet < u64 > ,
1010}
1111
1212impl MagnetLink {
@@ -33,7 +33,6 @@ impl MagnetLink {
3333 }
3434 }
3535
36- #[ allow( dead_code) ]
3736 pub ( crate ) fn set_name ( & mut self , name : impl Into < String > ) {
3837 self . name = Some ( name. into ( ) ) ;
3938 }
@@ -55,31 +54,33 @@ impl MagnetLink {
5554
5655 let mut query = format ! ( "xt=urn:btih:{}" , self . infohash) ;
5756
57+ let mut append = |key : & str , value : & str | {
58+ query. push ( '&' ) ;
59+ query. push_str ( key) ;
60+ query. push ( '=' ) ;
61+ query. push_str ( & Self :: percent_encode_query_param ( value) ) ;
62+ } ;
63+
5864 if let Some ( name) = & self . name {
59- query. push_str ( "&dn=" ) ;
60- query. push_str ( name) ;
65+ append ( "dn" , name) ;
6166 }
6267
6368 for tracker in & self . trackers {
64- query. push_str ( "&tr=" ) ;
65- for part in urlencode ( tracker. as_str ( ) . as_bytes ( ) ) {
66- query. push_str ( part) ;
67- }
69+ append ( "tr" , tracker. as_str ( ) ) ;
6870 }
6971
7072 for peer in & self . peers {
71- query. push_str ( "&x.pe=" ) ;
72- query. push_str ( & peer. to_string ( ) ) ;
73+ append ( "x.pe" , & peer. to_string ( ) ) ;
7374 }
7475
7576 if !self . indices . is_empty ( ) {
76- query . push_str ( "&so=" ) ;
77- for ( i , selection_index ) in self . indices . iter ( ) . enumerate ( ) {
78- if i > 0 {
79- query . push ( ',' ) ;
80- }
81- query . push_str ( & selection_index . to_string ( ) ) ;
82- }
77+ let indices = self
78+ . indices
79+ . iter ( )
80+ . map ( ToString :: to_string )
81+ . collect :: < Vec < String > > ( )
82+ . join ( "," ) ;
83+ append ( "so" , & indices ) ;
8384 }
8485
8586 url. set_query ( Some ( & query) ) ;
@@ -146,6 +147,27 @@ impl MagnetLink {
146147
147148 Ok ( link)
148149 }
150+
151+ fn percent_encode_query_param ( s : & str ) -> String {
152+ const ENCODE : & percent_encoding:: AsciiSet = & percent_encoding:: CONTROLS
153+ . add ( b' ' )
154+ . add ( b'"' )
155+ . add ( b'#' )
156+ . add ( b'%' )
157+ . add ( b'&' )
158+ . add ( b'<' )
159+ . add ( b'=' )
160+ . add ( b'>' )
161+ . add ( b'[' )
162+ . add ( b'\\' )
163+ . add ( b']' )
164+ . add ( b'^' )
165+ . add ( b'`' )
166+ . add ( b'{' )
167+ . add ( b'|' )
168+ . add ( b'}' ) ;
169+ percent_encoding:: utf8_percent_encode ( s, ENCODE ) . to_string ( )
170+ }
149171}
150172
151173impl FromStr for MagnetLink {
@@ -212,7 +234,7 @@ mod tests {
212234 link. add_tracker ( Url :: parse ( "http://foo.com/announce" ) . unwrap ( ) ) ;
213235 assert_eq ! (
214236 link. to_url( ) . as_str( ) ,
215- "magnet:?xt=urn:btih:da39a3ee5e6b4b0d3255bfef95601890afd80709&tr=http%3A%2F%2Ffoo .com%2Fannounce "
237+ "magnet:?xt=urn:btih:da39a3ee5e6b4b0d3255bfef95601890afd80709&tr=http://foo .com/announce "
216238 ) ;
217239 }
218240
@@ -242,8 +264,8 @@ mod tests {
242264 concat!(
243265 "magnet:?xt=urn:btih:da39a3ee5e6b4b0d3255bfef95601890afd80709" ,
244266 "&dn=foo" ,
245- "&tr=http%3A%2F%2Ffoo .com%2Fannounce " ,
246- "&tr=http%3A%2F%2Fbar .net%2Fannounce " ,
267+ "&tr=http://foo .com/announce " ,
268+ "&tr=http://bar .net/announce " ,
247269 "&x.pe=foo.com:1337" ,
248270 "&x.pe=bar.net:666" ,
249271 ) ,
@@ -270,8 +292,8 @@ mod tests {
270292 let magnet_str = concat ! (
271293 "magnet:?xt=urn:btih:da39a3ee5e6b4b0d3255bfef95601890afd80709" ,
272294 "&dn=foo" ,
273- "&tr=http%3A%2F%2Ffoo .com%2Fannounce " ,
274- "&tr=http%3A%2F%2Fbar .net%2Fannounce "
295+ "&tr=http://foo .com/announce " ,
296+ "&tr=http://bar .net/announce "
275297 ) ;
276298
277299 let link_from = MagnetLink :: from_str ( magnet_str) . unwrap ( ) ;
@@ -284,7 +306,7 @@ mod tests {
284306 let magnet_str = concat ! (
285307 "magnet:?xt=urn:btih:da39a3ee5e6b4b0d3255bfef95601890afd80709" ,
286308 "&dn=foo" ,
287- "&tr=http%3A%2F%2Ffoo .com%2Fannounce " ,
309+ "&tr=http://foo .com/announce " ,
288310 ) ;
289311
290312 let link_from = MagnetLink :: from_str ( magnet_str) . unwrap ( ) ;
@@ -390,4 +412,74 @@ mod tests {
390412 } if text == link && addr == bad_addr
391413 ) ;
392414 }
415+
416+ #[ test]
417+ fn magnet_link_query_params_are_percent_encoded ( ) {
418+ let mut e = "magnet:?xt=urn:btih:0000000000000000000000000000000000000000"
419+ . parse :: < MagnetLink > ( )
420+ . unwrap ( ) ;
421+ e. set_name ( "foo bar" ) ;
422+ e. add_tracker ( "http://[::]" . parse ( ) . unwrap ( ) ) ;
423+ e. add_peer ( "[::]:0" . parse ( ) . unwrap ( ) ) ;
424+
425+ assert_eq ! (
426+ e. to_url( ) . as_str( ) ,
427+ concat!(
428+ "magnet:" ,
429+ "?xt=urn:btih:0000000000000000000000000000000000000000" ,
430+ "&dn=foo%20bar" ,
431+ "&tr=http://%5B::%5D/" ,
432+ "&x.pe=%5B::%5D:0" ,
433+ ) ,
434+ ) ;
435+ }
436+
437+ #[ test]
438+ fn percent_encode ( ) {
439+ // Build a string containing all safe characters to test against using the
440+ // `query` grammar from the URL RFC:
441+ //
442+ // https://datatracker.ietf.org/doc/html/rfc3986#section-3.4
443+ //
444+ // `&` and `=` are omitted since they are used to delimit query parameter
445+ // keys and values
446+
447+ // query = *( pchar / "/" / "?" )
448+ let mut safe = "/?" . to_string ( ) ;
449+
450+ // pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
451+ safe. push_str ( ":@" ) ;
452+
453+ // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
454+ for c in 'a' ..='z' {
455+ safe. push ( c) ;
456+ }
457+
458+ for c in 'A' ..='Z' {
459+ safe. push ( c) ;
460+ }
461+
462+ for c in '0' ..='9' {
463+ safe. push ( c) ;
464+ }
465+
466+ safe. push_str ( "-._~" ) ;
467+
468+ // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
469+ safe. push_str ( "!$'()*+,;" ) ;
470+
471+ for c in '\u{0}' ..='\u{80}' {
472+ let s = c. to_string ( ) ;
473+ if safe. contains ( c) {
474+ assert_eq ! ( MagnetLink :: percent_encode_query_param( & s) , s) ;
475+ } else {
476+ assert_eq ! (
477+ MagnetLink :: percent_encode_query_param( & s) ,
478+ s. bytes( )
479+ . map( |byte| format!( "%{byte:02X}" ) )
480+ . collect:: <String >( ) ,
481+ ) ;
482+ }
483+ }
484+ }
393485}
0 commit comments