diff --git a/CHANGELOG.md b/CHANGELOG.md index c3195ce..4867903 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 query key/values - Added new `MagnetLinkError` variants to be more precise about what's wrong with a parsed magnet link. +- `MagnetLink::trackers` lists the trackers in the magnet link ## Version 0.3.2 (2025-08-29) diff --git a/Cargo.toml b/Cargo.toml index fcce204..9e1ecfd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ sha1 = "0.10" sha256 = "1.5" rustc-hex = "2.1" serde = { version = "1", features = [ "derive" ] } -fluent-uri = { git = "https://github.com/yescallop/fluent-uri-rs", rev = "5ad3b65" } +fluent-uri = { git = "https://github.com/yescallop/fluent-uri-rs", rev = "5ad3b65", features = [ "serde" ] } [dev-dependencies] serde_json = "1" diff --git a/src/magnet.rs b/src/magnet.rs index f6c920d..c84bcdf 100644 --- a/src/magnet.rs +++ b/src/magnet.rs @@ -1,7 +1,7 @@ use fluent_uri::pct_enc::{encoder::Query, EStr}; use fluent_uri::{ParseError as UriParseError, Uri}; -use crate::{InfoHash, InfoHashError, TorrentID}; +use crate::{InfoHash, InfoHashError, TorrentID, Tracker, TrackerError}; use std::string::FromUtf8Error; @@ -36,6 +36,11 @@ pub enum MagnetLinkError { /// some implementations, but should not be encouraged/supported. #[cfg(feature = "magnet_force_name")] NoNameFound, + /// The tracker declared could not be parsed. + InvalidTracker { + tracker: String, + source: TrackerError, + }, } impl std::fmt::Display for MagnetLinkError { @@ -81,6 +86,9 @@ impl std::fmt::Display for MagnetLinkError { MagnetLinkError::NoNameFound => { write!(f, "No name found") } + MagnetLinkError::InvalidTracker { tracker, .. } => { + write!(f, "Invalid tracker: {tracker}") + } } } } @@ -108,7 +116,8 @@ impl std::error::Error for MagnetLinkError { match self { MagnetLinkError::InvalidURI { source } => Some(source), MagnetLinkError::InvalidHash { source } => Some(source), - // MagnetLinkError::InvalidURIQueryUnicode { source } => Some(source), + MagnetLinkError::InvalidURIQueryUnicode { source } => Some(source), + MagnetLinkError::InvalidTracker { source, .. } => Some(source), _ => None, } } @@ -132,6 +141,8 @@ pub struct MagnetLink { /// Name of the torrent, which may be empty unless /// `magnet_force_name` crate feature is enabled. name: String, + /// Trackers contained in the magnet link + trackers: Vec, } impl MagnetLink { @@ -166,6 +177,7 @@ impl MagnetLink { let mut name = String::new(); let mut hashes: Vec = Vec::new(); + let mut trackers: Vec = Vec::new(); let query = u.query().ok_or(MagnetLinkError::InvalidURINoQuery)?; for (key, val) in Self::unsafe_parse_query(query)? { @@ -204,7 +216,13 @@ impl MagnetLink { .to_owned(); } "tr" => { - // TODO: trackers + let tracker_uri = val.decode().into_string()?.into_owned(); + trackers.push(Tracker::new(tracker_uri.as_str()).map_err(|e| { + MagnetLinkError::InvalidTracker { + source: e, + tracker: tracker_uri, + } + })?); } _ => { continue; @@ -247,6 +265,7 @@ impl MagnetLink { hash: final_hash, name: name.to_string(), query: query.as_str().to_string(), + trackers, }) } @@ -295,6 +314,11 @@ impl MagnetLink { pub fn id(&self) -> TorrentID { self.hash.id() } + + /// Returns the list of [`Tracker`](crate::tracker::Tracker) for the MagnetLink. + pub fn trackers(&self) -> &[Tracker] { + &self.trackers + } } impl std::fmt::Display for MagnetLink { @@ -307,6 +331,8 @@ impl std::fmt::Display for MagnetLink { mod tests { use super::*; + use crate::TrackerScheme; + #[test] fn can_load_v1() { let magnet_source = @@ -471,4 +497,63 @@ mod tests { let magnet_str = magnet.to_string(); assert_eq!(&magnet_url.to_string(), &magnet_str); } + + #[test] + fn can_parse_magnet_trackers() { + let expected = &[ + "udp://tracker.coppersurfer.tk:6969/announce", + "udp://tracker.opentrackr.org:1337/announce", + "udp://exodus.desync.com:6969", + "udp://tracker.opentrackr.org:1337/announce", + "http://tracker.openbittorrent.com:80/announce", + "udp://opentracker.i2p.rocks:6969/announce", + "udp://tracker.internetwarriors.net:1337/announce", + "udp://tracker.leechers-paradise.org:6969/announce", + "udp://coppersurfer.tk:6969/announce", + "udp://tracker.zer0day.to:1337/announce", + ]; + + let magnet_url = + std::fs::read_to_string("tests/bittorrent-v1-emma-goldman.magnet").unwrap(); + let magnet = MagnetLink::new(&magnet_url).unwrap(); + let found = magnet + .trackers + .clone() + .into_iter() + .map(|tracker| tracker.url().to_string()) + .collect::>(); + + // Did we find all trackers? + assert_eq!(found.len(), expected.len(),); + // Did we find that there's 1 HTTP tracker? + assert_eq!( + magnet + .trackers + .into_iter() + .filter(|tracker| tracker.scheme() == &TrackerScheme::Http) + .collect::>() + .len(), + 1 + ); + // Do we have the correct list? + assert_eq!(found, expected); + + let magnet_url = std::fs::read_to_string("tests/bittorrent-v2-test.magnet").unwrap(); + let magnet = MagnetLink::new(&magnet_url).unwrap(); + let found = magnet + .trackers + .into_iter() + .map(|tracker| tracker.url().to_string()) + .collect::>(); + assert!(found.is_empty()); + + let magnet_url = std::fs::read_to_string("tests/bittorrent-v2-hybrid-test.magnet").unwrap(); + let magnet = MagnetLink::new(&magnet_url).unwrap(); + let found = magnet + .trackers + .into_iter() + .map(|tracker| tracker.url().to_string()) + .collect::>(); + assert!(found.is_empty()); + } } diff --git a/src/tracker.rs b/src/tracker.rs index d22a232..a780d1c 100644 --- a/src/tracker.rs +++ b/src/tracker.rs @@ -13,16 +13,32 @@ pub enum PeerSource { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Tracker { scheme: TrackerScheme, - url: String, + url: Uri, } impl Tracker { + pub fn new_http(uri: &str) -> Result { + let uri = Uri::parse(uri)?; + Ok(Self { + scheme: TrackerScheme::Http, + url: uri.into(), + }) + } + + pub fn new_udp(_uri: &str) -> Result { + unimplemented!(); + } + + pub fn new_ws(_uri: &str) -> Result { + unimplemented!(); + } + pub fn scheme(&self) -> &TrackerScheme { &self.scheme } pub fn url(&self) -> &str { - &self.url + self.url.as_str() } } @@ -111,7 +127,7 @@ impl Tracker { Ok(Tracker { scheme, - url: url.as_str().to_string(), + url: url.clone(), }) }