magnet_url/lib.rs
1use regex::Regex;
2use std::fmt;
3
4#[macro_use]
5extern crate lazy_static;
6
7///The regexes used to identify specific parts of the magnet
8const DISPLAY_NAME_RE_STR: &str = r"dn=([A-Za-z0-9!@#$%^:*<>,?/()_+=.{}\{}\-]*)(&|$|\s)";
9const EXACT_TOPIC_RE_STR: &str = r"xt=urn:(sha1|btih|ed2k|aich|kzhash|md5|tree:tiger):([A-Fa-f0-9]+|[A-Za-z2-7]+)";
10const ADDRESS_TRACKER_RE_STR: &str = r"tr=([A-Za-z0-9!@#$%^:*<>,?/()_+=.{}\{}\-]*)(&|$|\s)";
11const KEYWORD_TOPIC_RE_STR: &str = r"kt=([A-Za-z0-9!@#$%^:*<>,?/()_+=.{}\{}\-]*)(&|$|\s)";
12const EXACT_SOURCE_RE_STR: &str = r"xs=((\w+)[A-Za-z0-9!@#$%^:*<>,?/()_+=.{}\\-]*)(&|$|\s)";
13const EXACT_LENGTH_RE_STR: &str = r"xl=(\d*)(&|$|\s)";
14const WEB_SEED_RE_STR: &str = r"ws=([A-Za-z0-9!@#$%^:*<>,?/()_+=.{}\{}\-]*)(&|$|\s)";
15const ACCEPTABLE_SOURCE_RE_STR: &str = r"as=((\w+)[A-Za-z0-9!@#$%^:*<>,?/()_+=.{}\\-]*)(&|$|\s)";
16const MANIFEST_TOPIC_RE_STR: &str = r"mt=((\w+)[A-Za-z0-9!@#$%^:*<>,?/()_+=.{}\\-]*|urn:(sha1|btih|ed2k|aich|kzhash|md5|tree:tiger):([A-Fa-f0-9]+|[A-Za-z2-7]+))(&|$|\s)";
17
18///# What is a magnet url
19///A magnet is a URI scheme that identifies files by their hash,
20/// normally used in peer to peer file sharing networks (like
21/// Bittorrent). Basically, a magnet link identifies a torrent you
22/// want to download, and tells the torrent client how to download
23/// it. They make it very easy to share files over the internet,
24/// and use a combination of DHT and trackers to tell your torrent
25/// client where other peers who can share the file with you are.
26///
27///# Why is magnet_url
28///While working on a side project, I realized that I had the
29/// misfortune of trying to get the component parts of a magnet-url
30/// and then do further processing of them. I quickly wrote some
31/// Regex for it, but then I realized that this would be really
32/// useful for other projects that are dealing with torrents in
33/// Rust. By making it modifiable, too, it would allow for the
34/// creation of custom magnet links, which would also be useful for
35/// torrent based projects.
36///
37///# Why use magnet_url
38/// magnet_url has the goal of, as you may have guessed, parsing the parts of magnets. It does
39/// this using some relatively simple regexes. The crate is designed to be very simple and efficient,
40/// with a lot of flexibility. It's also designed to be relatively easy to handle errors, and
41/// modification of its source is greatly encouraged through documentation and its license.
42///
43/// ## How to use this crate
44/// Parsing a magnet is very simple:
45///
46/// ```
47/// use magnet_url::Magnet;
48/// let magneturl = Magnet::new("magnet:?xt=urn:btih:08ada5a7a6183aae1e09d831df6748d566095a10&dn=Sintel&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fsintel.torrent");
49/// ```
50///
51/// This returns the Magnet struct, which is made up of the fields listed below this section, wrapped aroud a Result<Magnet, MagnetError>. To
52/// access one of these fields is also very simple:
53///
54/// ```
55/// use magnet_url::Magnet;
56/// let magneturl = Magnet::new("magnet:?xt=urn:btih:08ada5a7a6183aae1e09d831df6748d566095a10&dn=Sintel&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fsintel.torrent").unwrap();
57/// println!("{:?}", magneturl.dn);
58/// ```
59///
60/// If you'd like to modify parts of the magnet_url to customize it, that can be done as well!
61///
62/// ```
63/// use magnet_url::Magnet;
64/// let mut magneturl = Magnet::new("magnet:?xt=urn:btih:08ada5a7a6183aae1e09d831df6748d566095a10&dn=Sintel&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fsintel.torrent").unwrap();
65/// println!("{:?}", magneturl.dn);
66/// magneturl.dn = Some(String::from("hello_world"));
67/// println!("{:?}", magneturl.dn);
68/// ```
69///
70/// In fact, you can construct your own magnet url as well, as long as you fill in all the
71/// parameters!
72///
73/// ```
74/// use magnet_url::Magnet;
75/// //Note, this magnet won't actually download, sorry :/
76/// Magnet {
77/// dn: Some("hello_world".to_string()),
78/// hash_type: Some("sha1".to_string()),
79/// xt: Some("2aae6c35c94fcfb415dbe95f408b9ce91ee846ed".to_string()),
80/// xl: Some(1234567890),
81/// tr: vec!["https://example.com/".to_string()],
82/// kt: Some("cool+stuff".to_string()),
83/// ws: None,
84/// acceptable_source: None,
85/// mt: None,
86/// xs: None,
87/// };
88/// ```
89///
90/// From a Magnet struct, you can generate a magnet string again
91///
92/// ```
93/// use magnet_url::Magnet;
94/// //Note, this magnet won't actually download, sorry :/
95/// let magnet_struct = Magnet {
96/// dn: Some("hello_world".to_string()),
97/// hash_type: Some("sha1".to_string()),
98/// xt: Some("2aae6c35c94fcfb415dbe95f408b9ce91ee846ed".to_string()),
99/// xl: Some(1234567890),
100/// tr: vec!["https://example.com/".to_string()],
101/// kt: Some("cool+stuff".to_string()),
102/// ws: None,
103/// acceptable_source: None,
104/// mt: None,
105/// xs: None,
106/// };
107///
108/// let magnet_string = magnet_struct.to_string();
109/// println!("{}", magnet_string);
110/// ```
111///
112/// Invalid magnet url's will result in an Error, which can be handled appropriately
113/// ```#[should_panic]
114/// use magnet_url::Magnet;
115/// let _magnet_link = Magnet::new("https://example.com").unwrap();
116/// ```
117
118/// The various ways the new function can fail
119#[derive(Debug, Clone, Hash, PartialEq)]
120pub enum MagnetError {
121 NotAMagnetURL,
122}
123
124#[derive(Debug, Clone, Hash, PartialEq)]
125pub struct Magnet {
126 ///Display Name of the torrent
127 pub dn: Option<String>,
128 ///type of hash used in the exact topic
129 pub hash_type: Option<String>,
130 ///eXact Topic: URN containing the file hash. The URN is specific to the protocol so a file hash
131 /// URN under btih (BitTorrent) would be completely different than the file hash URN for ed2k
132 pub xt: Option<String>,
133 ///eXact Length: The size (in bytes)
134 pub xl: Option<u64>,
135 ///eXact Source: Either an HTTP (or HTTPS, FTP, FTPS, etc.) download source for the file pointed
136 /// to by the Magnet link, the address of a P2P source for the file or the address of a hub (in
137 /// the case of DC++), by which a client tries to connect directly, asking for the file and/or
138 /// its sources. This field is commonly used by P2P clients to store the source, and may include
139 /// the file hash.
140 pub xs: Option<String>,
141 ///address TRacker: Tracker URL; used to obtain resources for BitTorrent downloads without a
142 /// need for DHT support. The value must be URL encoded
143 pub tr: Vec<String>,
144 ///Keyword Topic: Specifies a string of search keywords to search for in P2P networks, rather
145 /// than a particular file. Also set as a vector since there will likely be more than one
146 pub kt: Option<String>,
147 ///Web Seed: The payload data served over HTTP(S)
148 pub ws: Option<String>,
149 ///Acceptable Source: Refers to a direct download from a web server. Regarded as only a
150 /// fall-back source in case a client is unable to locate and/or download the linked-to file in its supported P2P network(s)
151 ///as is a reserved keyword in Rust, so unfortunately this library must use the full name
152 pub acceptable_source: Option<String>,
153 ///Manifest Topic: Link to the metafile that contains a list of magneto (MAGMA –
154 /// MAGnet MAnifest); i.e. a link to a list of links
155 pub mt: Option<String>,
156}
157
158impl Magnet {
159
160 /**Given a magnet URL, identify the specific parts, and return the Magnet struct. If the program
161 can't identify a specific part of the magnet, then it will either give an empty version of what
162 its value would normally be (such as an empty string, an empty vector, or in the case of xl, -1).
163 It also doesn't validate whether the magnet url is good, which makes it faster, but dangerous!
164 Only use this function if you know for certain that the magnet url given is valid.
165 */
166 pub fn new_no_validation (magnet_str: &str) -> Magnet {
167 lazy_static! {
168 static ref DISPLAY_NAME_RE: Regex = Regex::new(DISPLAY_NAME_RE_STR).unwrap();
169 static ref EXACT_TOPIC_RE: Regex = Regex::new(EXACT_TOPIC_RE_STR).unwrap();
170 static ref EXACT_LENGTH_RE: Regex = Regex::new(EXACT_LENGTH_RE_STR).unwrap();
171 static ref ADDRESS_TRACKER_RE: Regex = Regex::new(ADDRESS_TRACKER_RE_STR).unwrap();
172 static ref KEYWORD_TOPIC_RE: Regex = Regex::new(KEYWORD_TOPIC_RE_STR).unwrap();
173 static ref EXACT_SOURCE_RE: Regex = Regex::new(EXACT_SOURCE_RE_STR).unwrap();
174 static ref WEB_SEED_RE: Regex = Regex::new(WEB_SEED_RE_STR).unwrap();
175 static ref ACCEPTABLE_SOURCE_RE: Regex = Regex::new(ACCEPTABLE_SOURCE_RE_STR).unwrap();
176 static ref MANIFEST_TOPIC_RE: Regex = Regex::new(MANIFEST_TOPIC_RE_STR).unwrap();
177 }
178
179 let validate_regex = |regex: &Regex, re_group_index| -> Option<String> {
180 match regex.captures(magnet_str) {
181 Some(m) => m.get(re_group_index).map_or(None, |m| Some(m.as_str().to_string())),
182 None => None
183 }
184
185 };
186
187 Magnet {
188 dn: validate_regex(&DISPLAY_NAME_RE, 1),
189 hash_type: validate_regex(&EXACT_TOPIC_RE, 1),
190 xt: validate_regex(&EXACT_TOPIC_RE, 2),
191 // Using a slightly modified match statement so it doesn't parse from str to String to int
192 xl: {
193 match &EXACT_LENGTH_RE.captures(magnet_str) {
194 Some(m) => m.get(1).map_or(None, |m| Some(m.as_str().parse().unwrap())),
195 None => None,
196 }
197
198 },
199 xs: validate_regex(&EXACT_SOURCE_RE, 1),
200 tr: {
201 let mut tr_vec: Vec<String> = Vec::new();
202 // Since tr is a vector, I can't just use the validate_regex function
203 for tr in ADDRESS_TRACKER_RE.captures_iter(magnet_str) {
204 tr_vec.push(tr.get(1).map_or(String::new(), |m| m.as_str().to_string()));
205 }
206
207 tr_vec
208
209 },
210 kt: validate_regex(&KEYWORD_TOPIC_RE, 1),
211 ws: validate_regex(&WEB_SEED_RE, 1),
212 acceptable_source: validate_regex(&ACCEPTABLE_SOURCE_RE, 1),
213 mt: validate_regex(&MANIFEST_TOPIC_RE, 1),
214
215 }
216 }
217
218 /// The recommended way of creating magnets. The same as new_no_validation, but does validation
219 #[inline]
220 pub fn new(magnet_str: &str) -> Result<Magnet, MagnetError> {
221 if !magnet_str.starts_with("magnet:?") {
222 Err(MagnetError::NotAMagnetURL)
223
224 } else {
225 Ok(Magnet::new_no_validation(magnet_str))
226
227 }
228
229 }
230}
231
232impl fmt::Display for Magnet {
233 /*
234 This generates a magnet url string given a Magnet struct
235 */
236 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
237 let mut magnet_string = String::from("magnet:?");
238
239 if let Some(xt) = &self.xt {
240 magnet_string = format!("{}{}{}:{}", magnet_string, "xt=urn:", self.hash_type.as_ref().unwrap_or(&String::new()), xt);
241 }
242
243 let add_to_mag_string = |p_name: String, p_val: &Option<String>| -> String {
244 if let Some(p_val) = p_val {
245 format!("&{}={}", p_name, p_val)
246
247 } else {
248 String::new()
249
250 }
251 };
252
253 magnet_string = format!("{}{}", magnet_string, add_to_mag_string(String::from("dn"), &self.dn));
254
255 if let Some(xl) = &self.xl {
256 magnet_string = format!("{}&xl={}", magnet_string, xl);
257 }
258
259 magnet_string = {
260 let mut tr_string = String::new();
261 for tracker in &self.tr {
262 tr_string = format!("{}&tr={}", tr_string, tracker);
263 }
264
265 format!("{}{}", magnet_string, tr_string)
266 };
267
268 magnet_string = format!("{}{}", magnet_string, add_to_mag_string(String::from("ws"), &self.ws));
269 magnet_string = format!("{}{}", magnet_string, add_to_mag_string(String::from("xs"), &self.xs));
270 magnet_string = format!("{}{}", magnet_string, add_to_mag_string(String::from("kt"), &self.kt));
271 magnet_string = format!("{}{}", magnet_string, add_to_mag_string(String::from("as"), &self.acceptable_source));
272 magnet_string = format!("{}{}", magnet_string, add_to_mag_string(String::from("mt"), &self.mt));
273
274
275 write!(f, "{}", magnet_string)
276
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use crate::{Magnet, MagnetError};
283
284 #[test]
285 fn sintel_test() {
286 const MAGNET_STR: &str = "magnet:?xt=urn:btih:08ada5a7a6183aae1e09d831df6748d566095a10&dn=Sintel&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fsintel.torrent";
287 let magnet_link = Magnet::new(MAGNET_STR).unwrap();
288
289 assert_eq!(magnet_link.dn, Some("Sintel".to_string()));
290 assert_eq!(magnet_link.hash_type, Some("btih".to_string()));
291 assert_eq!(magnet_link.xt, Some("08ada5a7a6183aae1e09d831df6748d566095a10".to_string()));
292 assert_eq!(magnet_link.xl, None);
293 assert_eq!(magnet_link.xs, Some("https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fsintel.torrent".to_string()));
294 assert_eq!(magnet_link.tr[0], "udp%3A%2F%2Fexplodie.org%3A6969".to_string());
295 assert_eq!(magnet_link.tr[1], "udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969".to_string());
296 assert_eq!(magnet_link.tr[2], "udp%3A%2F%2Ftracker.empire-js.us%3A1337".to_string());
297 assert_eq!(magnet_link.tr[3], "udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969".to_string());
298 assert_eq!(magnet_link.tr[4], "udp%3A%2F%2Ftracker.opentrackr.org%3A1337".to_string());
299 assert_eq!(magnet_link.tr[5], "wss%3A%2F%2Ftracker.btorrent.xyz".to_string());
300 assert_eq!(magnet_link.tr[6], "wss%3A%2F%2Ftracker.fastcast.nz".to_string());
301 assert_eq!(magnet_link.tr[7], "wss%3A%2F%2Ftracker.openwebtorrent.com".to_string());
302 assert_eq!(magnet_link.ws, Some("https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F".to_string()));
303 assert_eq!(magnet_link.xs, Some("https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fsintel.torrent".to_string()));
304 assert_eq!(magnet_link.kt, None);
305 assert_eq!(magnet_link.acceptable_source, None);
306 assert_eq!(magnet_link.mt, None);
307
308 //Need to recreate a magnet struct from the string, since the elements could be in any order
309 assert_eq!(Magnet::new(&magnet_link.to_string()).unwrap(), magnet_link);
310 //Also tests PartialEq
311 assert_eq!(Magnet::new(&magnet_link.to_string()).unwrap() == magnet_link, true);
312 }
313
314 #[test]
315 fn invalid_magnet_test() {
316 assert_eq!(Magnet::new("https://example.com"), Err(MagnetError::NotAMagnetURL));
317
318 }
319
320 #[test]
321 fn not_equal_magnet_test() {
322 //These two torrents aren't even close to equal
323 const MAGNET_STR_1: &str = "magnet:?xt=urn:btih:08ada5a7a6183aae1e09d831df6748d566095a10&dn=Sintel&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fsintel.torrent";
324 const MAGNET_STR_2: &str = "magnet:?xt=urn:btih:da826adb2ba4933500d83c19bbdfa73ee28f34d5&dn=devuan%5Fbeowulf&tr=udp%3A%2F%2F9.rarbg.me%3A2710%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.cyberia.is%3A6969%2Fannounce";
325
326 let magnet_link_1 = Magnet::new(MAGNET_STR_1);
327 let magnet_link_2 = Magnet::new(MAGNET_STR_2);
328
329 //These two torrents, on the other hand, are very similar
330 const MAGNET_STR_3: &str = "magnet:?xt=urn:btih:da826adb2ba4933500d83c19bbdfa73ee28f34d5&dn=devuan%5Fbeowulf&tr=udp%3A%2F%2F9.rarbg.me%3A2710%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.cyberia.is%3A6969%2Fannounce";
331 const MAGNET_STR_4: &str = "magnet:?xt=urn:btih:da826adb2ba4933500d83c19bbdfa73ee28f34d5&dn=devuan%5Fbeowulf&tr=udp%3A%2F%2F9.rarbg.me%3A2710%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.cyberia.is%3A6969%2Fannounce&tr=https://example.com/fake_tracker";
332
333 let magnet_link_3 = Magnet::new(MAGNET_STR_3);
334 let magnet_link_4 = Magnet::new(MAGNET_STR_4);
335
336 assert_ne!(magnet_link_1, magnet_link_2);
337 assert_ne!(magnet_link_3, magnet_link_4);
338
339 //magnet_link_2 and magnet_link_3 are exactly the same
340 assert_eq!(magnet_link_2, magnet_link_3);
341 //Tests PartialEq instead of Debug
342 assert_eq!(magnet_link_2 == magnet_link_3, true);
343
344 }
345}