From 3381079039f06935d7dadc8529c154f792911105 Mon Sep 17 00:00:00 2001 From: angrynode Date: Fri, 7 Nov 2025 17:16:54 +0100 Subject: [PATCH 1/2] fix: tracker URLs in magnet are url-encoded --- src/magnet_link.rs | 54 +++++++++++++++++++++++++++++--- src/subcommand/torrent/create.rs | 2 +- src/subcommand/torrent/link.rs | 8 ++--- 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/magnet_link.rs b/src/magnet_link.rs index 9f61cf9..66fcf26 100644 --- a/src/magnet_link.rs +++ b/src/magnet_link.rs @@ -1,10 +1,15 @@ use crate::common::*; +use url::form_urlencoded::byte_serialize as urlencode; #[derive(Clone, Debug, PartialEq)] pub(crate) struct MagnetLink { pub(crate) infohash: Infohash, pub(crate) name: Option, pub(crate) peers: Vec, + /// Trackers contained in magnet `tr` fields. + /// + /// URLs here are url-decoded into human-readable URLs, but are automatically + /// re-encoded in [`MagnetLink::to_url`] and in the [Display] implementation. pub(crate) trackers: Vec, pub(crate) indices: BTreeSet, } @@ -50,6 +55,13 @@ impl MagnetLink { self.indices.insert(index); } + /// Produce a parsed URL from the magnet. + /// + /// We are not naively URL-encoding the data because `xt=urn:btih:INFOHASH` + /// is not url-encoded, despite being a query param. + /// + /// We are not naively string-pushing the data because `tr=TRACKER_URL` + /// has to be properly url-encoded. pub(crate) fn to_url(&self) -> Url { let mut url = Url::parse("magnet:").invariant_unwrap("`magnet:` is valid URL"); @@ -62,7 +74,9 @@ impl MagnetLink { for tracker in &self.trackers { query.push_str("&tr="); - query.push_str(tracker.as_str()); + for part in urlencode(tracker.as_str().as_bytes()) { + query.push_str(part); + } } for peer in &self.peers { @@ -210,7 +224,7 @@ mod tests { link.add_tracker(Url::parse("http://foo.com/announce").unwrap()); assert_eq!( link.to_url().as_str(), - "magnet:?xt=urn:btih:da39a3ee5e6b4b0d3255bfef95601890afd80709&tr=http://foo.com/announce" + "magnet:?xt=urn:btih:da39a3ee5e6b4b0d3255bfef95601890afd80709&tr=http%3A%2F%2Ffoo.com%2Fannounce" ); } @@ -240,8 +254,8 @@ mod tests { concat!( "magnet:?xt=urn:btih:da39a3ee5e6b4b0d3255bfef95601890afd80709", "&dn=foo", - "&tr=http://foo.com/announce", - "&tr=http://bar.net/announce", + "&tr=http%3A%2F%2Ffoo.com%2Fannounce", + "&tr=http%3A%2F%2Fbar.net%2Fannounce", "&x.pe=foo.com:1337", "&x.pe=bar.net:666", ), @@ -263,6 +277,38 @@ mod tests { assert_eq!(link_to, link_from); } + #[test] + fn link_from_str_tracker_round_trip() { + let magnet_str = concat!( + "magnet:?xt=urn:btih:da39a3ee5e6b4b0d3255bfef95601890afd80709", + "&dn=foo", + "&tr=http%3A%2F%2Ffoo.com%2Fannounce", + "&tr=http%3A%2F%2Fbar.net%2Fannounce" + ); + + let link_from = MagnetLink::from_str(magnet_str).unwrap(); + let link_roundtripped = MagnetLink::from_str(&link_from.to_string()).unwrap(); + assert_eq!(link_from, link_roundtripped,); + } + + #[test] + fn link_from_str_tracker_urlencoding() { + let magnet_str = concat!( + "magnet:?xt=urn:btih:da39a3ee5e6b4b0d3255bfef95601890afd80709", + "&dn=foo", + "&tr=http%3A%2F%2Ffoo.com%2Fannounce", + ); + + let link_from = MagnetLink::from_str(magnet_str).unwrap(); + let tracker_url = link_from.trackers.first().unwrap(); + + // Url is properly url-decoded + assert_eq!(tracker_url, &Url::parse("http://foo.com/announce").unwrap(),); + + // When human-printing the URL, it's not reencoded + assert_eq!(tracker_url.as_str(), "http://foo.com/announce",); + } + #[test] fn link_from_str_url_error() { let link = "%imdl.io"; diff --git a/src/subcommand/torrent/create.rs b/src/subcommand/torrent/create.rs index fc4bd8f..97a2a00 100644 --- a/src/subcommand/torrent/create.rs +++ b/src/subcommand/torrent/create.rs @@ -2593,7 +2593,7 @@ Content Size 9 bytes "magnet:\ ?xt=urn:btih:516735f4b80f2b5487eed5f226075bdcde33a54e\ &dn=foo\ - &tr=http://foo.com/announce\n" + &tr=http%3A%2F%2Ffoo.com%2Fannounce\n" ); } diff --git a/src/subcommand/torrent/link.rs b/src/subcommand/torrent/link.rs index 1114fd8..c89331b 100644 --- a/src/subcommand/torrent/link.rs +++ b/src/subcommand/torrent/link.rs @@ -233,7 +233,7 @@ mod tests { assert_eq!( env.out(), format!( - "magnet:?xt=urn:btih:{}&dn=foo&tr=https://foo.com/announce\n", + "magnet:?xt=urn:btih:{}&dn=foo&tr=https%3A%2F%2Ffoo.com%2Fannounce\n", infohash ), ); @@ -266,7 +266,7 @@ mod tests { assert_eq!( env.out(), format!( - "magnet:?xt=urn:btih:{}&dn=foo&tr=https://foo.com/announce&tr=https://bar.com/announce\n", + "magnet:?xt=urn:btih:{}&dn=foo&tr=https%3A%2F%2Ffoo.com%2Fannounce&tr=https%3A%2F%2Fbar.com%2Fannounce\n", infohash ), ); @@ -300,7 +300,7 @@ mod tests { assert_eq!( env.out(), format!( - "magnet:?xt=urn:btih:{}&dn=foo&tr=https://foo.com/announce&x.pe=foo.com:1337\n", + "magnet:?xt=urn:btih:{}&dn=foo&tr=https%3A%2F%2Ffoo.com%2Fannounce&x.pe=foo.com:1337\n", infohash ), ); @@ -336,7 +336,7 @@ mod tests { assert_eq!( env.out(), format!( - "magnet:?xt=urn:btih:{}&dn=foo&tr=https://foo.com/announce&so=2,4,6\n", + "magnet:?xt=urn:btih:{}&dn=foo&tr=https%3A%2F%2Ffoo.com%2Fannounce&so=2,4,6\n", infohash ), ); From e2ecd249b798a6d1c27d5a097fa3381180cdce14 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Fri, 7 Nov 2025 22:55:16 -0800 Subject: [PATCH 2/2] Remove comments --- src/magnet_link.rs | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/src/magnet_link.rs b/src/magnet_link.rs index 66fcf26..7d09798 100644 --- a/src/magnet_link.rs +++ b/src/magnet_link.rs @@ -1,15 +1,10 @@ -use crate::common::*; -use url::form_urlencoded::byte_serialize as urlencode; +use {crate::common::*, url::form_urlencoded::byte_serialize as urlencode}; #[derive(Clone, Debug, PartialEq)] pub(crate) struct MagnetLink { pub(crate) infohash: Infohash, pub(crate) name: Option, pub(crate) peers: Vec, - /// Trackers contained in magnet `tr` fields. - /// - /// URLs here are url-decoded into human-readable URLs, but are automatically - /// re-encoded in [`MagnetLink::to_url`] and in the [Display] implementation. pub(crate) trackers: Vec, pub(crate) indices: BTreeSet, } @@ -55,13 +50,6 @@ impl MagnetLink { self.indices.insert(index); } - /// Produce a parsed URL from the magnet. - /// - /// We are not naively URL-encoding the data because `xt=urn:btih:INFOHASH` - /// is not url-encoded, despite being a query param. - /// - /// We are not naively string-pushing the data because `tr=TRACKER_URL` - /// has to be properly url-encoded. pub(crate) fn to_url(&self) -> Url { let mut url = Url::parse("magnet:").invariant_unwrap("`magnet:` is valid URL"); @@ -302,11 +290,10 @@ mod tests { let link_from = MagnetLink::from_str(magnet_str).unwrap(); let tracker_url = link_from.trackers.first().unwrap(); - // Url is properly url-decoded - assert_eq!(tracker_url, &Url::parse("http://foo.com/announce").unwrap(),); - - // When human-printing the URL, it's not reencoded - assert_eq!(tracker_url.as_str(), "http://foo.com/announce",); + assert_eq!( + tracker_url, + &"http://foo.com/announce".parse::().unwrap(), + ); } #[test]