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}