mtorrent_core/input/
magnet.rs1use std::error::Error;
2use std::net::SocketAddr;
3use std::{iter, net, str};
4use thiserror::Error;
5
6#[derive(Clone)]
8pub struct MagnetLink {
9 info_hash: [u8; 20],
10 name: Option<String>,
11 trackers: Vec<String>,
12 peers: Vec<SocketAddr>,
13}
14
15#[derive(Debug, Error)]
17pub enum ParseError {
18 #[error("malformed uri: {0}")]
19 MalformedUri(#[from] url::ParseError),
20 #[error("invalid scheme: {0}")]
21 UnsupportedScheme(String),
22 #[error("no info hash")]
23 NoInfoHash,
24 #[error("invalid info hash: {0}")]
25 InvalidInfoHash(#[source] Box<dyn Error + Send + Sync>),
26 #[error("invalid peer addr: {0}")]
27 InvalidPeerAddr(#[from] net::AddrParseError),
28}
29
30impl str::FromStr for MagnetLink {
31 type Err = ParseError;
32
33 fn from_str(s: &str) -> Result<Self, Self::Err> {
34 let parsed = url::Url::parse(s)?;
35 if parsed.scheme() != "magnet" {
36 return Err(ParseError::UnsupportedScheme(parsed.scheme().to_string()));
37 }
38 let mut info_hash = None;
39 let mut name = None;
40 let mut trackers = Vec::<String>::new();
41 let mut peers = Vec::<SocketAddr>::new();
42 for (key, value) in parsed.query_pairs() {
43 match key.as_ref() {
44 "xt" => match value.strip_prefix("urn:btih:") {
45 Some(hex_str) if hex_str.len() == 40 => {
46 let mut bytes = [0u8; 20];
47 for (src, dest) in
48 iter::zip(hex_str.as_bytes().chunks_exact(2), bytes.iter_mut())
49 {
50 let src_str = str::from_utf8(src)
51 .map_err(|e| ParseError::InvalidInfoHash(Box::new(e)))?;
52 *dest = u8::from_str_radix(src_str, 16)
53 .map_err(|e| ParseError::InvalidInfoHash(Box::new(e)))?;
54 }
55 info_hash = Some(bytes);
56 }
57 _ => return Err(ParseError::InvalidInfoHash(format!("{value}").into())),
58 },
59 "dn" => name = Some(value.to_string()),
60 "tr" => trackers.push(value.to_string()),
61 "x.pe" => peers.push(value.parse()?),
62 _ => (),
63 }
64 }
65 Ok(Self {
66 info_hash: info_hash.ok_or(ParseError::NoInfoHash)?,
67 name,
68 trackers,
69 peers,
70 })
71 }
72}
73
74impl MagnetLink {
75 pub fn info_hash(&self) -> &[u8; 20] {
77 &self.info_hash
78 }
79
80 pub fn name(&self) -> Option<&str> {
82 self.name.as_deref()
83 }
84
85 pub fn trackers(&self) -> impl ExactSizeIterator<Item = &str> {
87 self.trackers.iter().map(AsRef::as_ref)
88 }
89
90 pub fn peers(&self) -> impl ExactSizeIterator<Item = &SocketAddr> {
92 self.peers.iter()
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn parse_magnet_link() {
102 let link = "magnet:?xt=urn:btih:1EBD3DBFBB25C1333F51C99C7EE670FC2A1727C9&dn=Dune.Part.Two.2024.1080p.HD-TS.X264-EMIN3M%5BTGx%5D&tr=udp%3A%2F%2Fopen.stealth.si%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.tiny-vps.com%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.torrent.eu.org%3A451%2Fannounce&tr=udp%3A%2F%2Fexplodie.org%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.cyberia.is%3A6969%2Fannounce&tr=udp%3A%2F%2Fipv4.tracker.harry.lu%3A80%2Fannounce&tr=udp%3A%2F%2Fp4p.arenabg.com%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.birkenwald.de%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.moeking.me%3A6969%2Fannounce&tr=udp%3A%2F%2Fopentor.org%3A2710%2Fannounce&tr=udp%3A%2F%2Ftracker.dler.org%3A6969%2Fannounce&tr=udp%3A%2F%2Fuploads.gamecoast.net%3A6969%2Fannounce&tr=https%3A%2F%2Ftracker.foreverpirates.co%3A443%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=http%3A%2F%2Ftracker.openbittorrent.com%3A80%2Fannounce&tr=udp%3A%2F%2Fopentracker.i2p.rocks%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.internetwarriors.net%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969%2Fannounce&tr=udp%3A%2F%2Fcoppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.zer0day.to%3A1337%2Fannounce";
103
104 let magnet = link.parse::<MagnetLink>().unwrap();
105
106 assert_eq!(
107 &[
108 30, 189, 61, 191, 187, 37, 193, 51, 63, 81, 201, 156, 126, 230, 112, 252, 42, 23,
109 39, 201
110 ],
111 magnet.info_hash()
112 );
113 assert_eq!(Some("Dune.Part.Two.2024.1080p.HD-TS.X264-EMIN3M[TGx]"), magnet.name());
114 assert_eq!(
115 vec![
116 "udp://open.stealth.si:80/announce",
117 "udp://tracker.tiny-vps.com:6969/announce",
118 "udp://tracker.opentrackr.org:1337/announce",
119 "udp://tracker.torrent.eu.org:451/announce",
120 "udp://explodie.org:6969/announce",
121 "udp://tracker.cyberia.is:6969/announce",
122 "udp://ipv4.tracker.harry.lu:80/announce",
123 "udp://p4p.arenabg.com:1337/announce",
124 "udp://tracker.birkenwald.de:6969/announce",
125 "udp://tracker.moeking.me:6969/announce",
126 "udp://opentor.org:2710/announce",
127 "udp://tracker.dler.org:6969/announce",
128 "udp://uploads.gamecoast.net:6969/announce",
129 "https://tracker.foreverpirates.co:443/announce",
130 "udp://tracker.opentrackr.org:1337/announce",
131 "http://tracker.openbittorrent.com:80/announce",
132 "udp://opentracker.i2p.rocks:6969/announce",
133 "udp://tracker.internetwarriors.net:1337/announce",
134 "udp://tracker.leechers-paradise.org:6969/announce",
135 "udp://coppersurfer.tk:6969/announce",
136 "udp://tracker.zer0day.to:1337/announce"
137 ],
138 magnet.trackers().collect::<Vec<_>>()
139 );
140 }
141
142 #[test]
143 fn parse_magnet_link_with_invalid_info_hash() {
144 let link = "magnet:?xt=urn:btih:1EBD3DBFBB25C1333F51C99C7EE670FC2A1727C"; assert!(matches!(link.parse::<MagnetLink>(), Err(ParseError::InvalidInfoHash(_))));
146
147 let link = "magnet:?xt=urn:btih:1EBD3DBFBB25C1333F51C99C7EE670FC2A1727C99"; assert!(matches!(link.parse::<MagnetLink>(), Err(ParseError::InvalidInfoHash(_))));
149 }
150}