magnet_uri/
lib.rs

1extern crate serde_urlencoded;
2
3use serde_urlencoded::de::Error as UrlEncodeError;
4use std::fmt;
5use std::str::FromStr;
6
7const SCHEME: &str = "magnet:?";
8
9pub(self) mod field_name {
10    pub const NAME: &str = "dn";
11    pub const LENGTH: &str = "xl";
12    pub const TOPIC: &str = "xt";
13    pub const ACCEPTABLE_SOURCE: &str = "as";
14    pub const EXACT_SOURCE: &str = "xs";
15    pub const KEYWORD: &str = "kt";
16    pub const MANIFEST: &str = "mt";
17    pub const ADDRESS_TRACKER: &str = "tr";
18    pub const EXTENSION_PREFIX: &str = "x.";
19}
20
21pub(self) mod exact_topic_urn {
22    pub const TIGER_TREE_HASH: &str = "urn:tree:tiger:";
23    pub const SHA1: &str = "urn:sha1:";
24    pub const BIT_PRINT: &str = "urn:bitprint:";
25    pub const ED2K: &str = "urn:ed2k:";
26    pub const AICH: &str = "urn:aich:";
27    pub const KAZAA: &str = "urn:kzhash:";
28    pub const BITTORRENT_INFO_HASH: &str = "urn:btih:";
29    pub const MD5: &str = "urn:md5:";
30}
31
32#[derive(Debug)]
33pub enum Error {
34    Scheme,
35    UrlEncode(UrlEncodeError),
36    Field(String, String),
37    ExactTopic(String),
38}
39
40impl Error {
41    fn with_field(key: &str, val: &str) -> Self {
42        Error::Field(key.to_owned(), val.to_owned())
43    }
44}
45
46/// A struct holding fields stored in a Magnet URI
47#[derive(Debug, Default)]
48pub struct MagnetURI {
49    fields: Vec<Field>,
50}
51
52impl MagnetURI {
53    pub fn has_extensions(&self) -> bool {
54        self.fields.iter().any(Field::is_extension)
55    }
56
57    pub fn has_unknown_fields(&self) -> bool {
58        self.fields.iter().any(Field::is_unknown)
59    }
60
61    pub fn has_topic_conflict(&self) -> bool {
62        self.iter_topics().any(|topic1| {
63            self.iter_topics()
64                .any(|topic2| Topic::conflicts(topic1, topic2))
65        })
66    }
67
68    pub fn is_strictly_valid(&self) -> bool {
69        !self.has_unknown_fields() && self.length() != None && !self.has_topic_conflict()
70    }
71
72    pub fn names(&self) -> Vec<&str> {
73        self.iter_field_values(Field::name).collect()
74    }
75
76    pub fn name(&self) -> Option<&str> {
77        self.iter_field_values(Field::name).next()
78    }
79
80    pub fn dn(&self) -> Option<&str> {
81        self.name()
82    }
83
84    pub fn length(&self) -> Option<u64> {
85        self.iter_field_values(Field::length).next()
86    }
87
88    pub fn xl(&self) -> Option<u64> {
89        self.length()
90    }
91
92    pub fn iter_topics(&self) -> impl Iterator<Item = &Topic> {
93        self.iter_field_values(Field::topic)
94    }
95
96    pub fn topics(&self) -> Vec<&Topic> {
97        self.iter_topics().collect()
98    }
99
100    pub fn info_hashes(&self) -> Vec<&BTInfoHash> {
101        self.iter_field_values(Field::info_hash).collect()
102    }
103
104    pub fn info_hash(&self) -> Option<&BTInfoHash> {
105        self.iter_field_values(Field::info_hash).next()
106    }
107
108    fn iter_field_values<'a, F, T>(&'a self, f: F) -> impl Iterator<Item = T> + 'a
109    where
110        F: Fn(&'a Field) -> Option<T> + Sized + 'a,
111        T: 'a,
112    {
113        self.fields
114            .iter()
115            .map(f)
116            .filter(Option::is_some)
117            .map(Option::unwrap)
118    }
119
120    pub fn add_field(&mut self, f: Field) -> &Self {
121        self.fields.push(f);
122        self
123    }
124
125    pub fn add_name(&mut self, name: &str) -> &Self {
126        self.add_field(Field::Name(name.to_owned()))
127    }
128
129    pub fn add_topic(&mut self, xt: Topic) -> &Self {
130        self.add_field(Field::Topic(xt))
131    }
132
133    pub fn add_extension(&mut self, ext_name: &str, val: &str) -> &Self {
134        self.add_field(Field::Extension(ext_name.to_owned(), val.to_owned()))
135    }
136
137    pub fn set_name(&mut self, name: &str) -> &Self {
138        self.set_unique_field(|f| f.name().is_none(), Field::Name(name.to_owned()))
139    }
140
141    pub fn set_info_hash(&mut self, btih: BTInfoHash) -> &Self {
142        self.set_unique_field(
143            |f| match f {
144                Field::Topic(Topic::BitTorrentInfoHash(_)) => false,
145                _ => true,
146            },
147            Field::Topic(Topic::BitTorrentInfoHash(btih)),
148        )
149    }
150
151    fn set_unique_field<F>(&mut self, retain_filter: F, field: Field) -> &Self
152    where
153        F: FnMut(&Field) -> bool,
154    {
155        self.fields.retain(retain_filter);
156        self.add_field(field)
157    }
158}
159
160impl FromStr for MagnetURI {
161    type Err = Error;
162
163    fn from_str(s: &str) -> Result<Self, Self::Err> {
164        if !s.starts_with(SCHEME) {
165            return Err(Error::Scheme);
166        }
167        let (_, qs) = s.split_at(SCHEME.len());
168        let parse_result: Result<Vec<(String, String)>, _> = serde_urlencoded::from_str(qs);
169        match parse_result {
170            Err(e) => Err(Error::UrlEncode(e)),
171            Ok(parts) => Ok(MagnetURI {
172                fields: parts
173                    .iter()
174                    .map(|(k, v)| Field::from_str(k, v))
175                    .collect::<Result<Vec<_>, _>>()?,
176            }),
177        }
178    }
179}
180
181/// Field of a Magnet URI
182#[derive(Debug, PartialEq)]
183pub enum Field {
184    Name(String),
185    Length(u64),
186    Topic(Topic),
187    AcceptableSource(String),
188    ExactSource(String),
189    Keyword(String),
190    Manifest(String),
191    AddressTracker(String),
192    Extension(String, String),
193    Unknown(String, String),
194}
195
196impl Field {
197    fn from_str(key: &str, val: &str) -> Result<Self, Error> {
198        use field_name::*;
199        use Field::*;
200
201        match key {
202            NAME => Ok(Name(val.to_owned())),
203            LENGTH => match u64::from_str(val) {
204                Err(_) => Err(Error::with_field(key, val)),
205                Ok(l) => Ok(Length(l)),
206            },
207            TOPIC => Ok(Topic(self::Topic::from_str(val)?)),
208            ACCEPTABLE_SOURCE => Ok(AcceptableSource(val.to_owned())),
209            EXACT_SOURCE => Ok(ExactSource(val.to_owned())),
210            KEYWORD => Ok(Keyword(val.to_owned())),
211            MANIFEST => Ok(Manifest(val.to_owned())),
212            ADDRESS_TRACKER => Ok(AddressTracker(val.to_owned())),
213            _ => {
214                if key.starts_with(EXTENSION_PREFIX) {
215                    let (_, ext_name) = key.split_at(EXTENSION_PREFIX.len());
216                    Ok(Extension(ext_name.to_owned(), val.to_owned()))
217                } else {
218                    Ok(Unknown(key.to_owned(), val.to_owned()))
219                }
220            }
221        }
222    }
223
224    fn is_extension(&self) -> bool {
225        match self {
226            Field::Extension(_, _) => true,
227            _ => false,
228        }
229    }
230
231    fn is_unknown(&self) -> bool {
232        match self {
233            Field::Unknown(_, _) => true,
234            _ => false,
235        }
236    }
237
238    fn name(&self) -> Option<&str> {
239        match self {
240            Field::Name(ref name) => Some(name),
241            _ => None,
242        }
243    }
244
245    fn length(&self) -> Option<u64> {
246        match self {
247            Field::Length(len) => Some(*len),
248            _ => None,
249        }
250    }
251
252    fn topic(&self) -> Option<&Topic> {
253        match self {
254            Field::Topic(topic) => Some(topic),
255            _ => None,
256        }
257    }
258
259    fn info_hash(&self) -> Option<&BTInfoHash> {
260        match self {
261            Field::Topic(Topic::BitTorrentInfoHash(ref hash)) => Some(hash),
262            _ => None,
263        }
264    }
265}
266
267// TODO: use more specific types
268type TTHHash = String;
269type SHA1Hash = String;
270type ED2KHash = String;
271type AICHHash = String;
272type KazaaHash = String;
273type BTInfoHash = String;
274type MD5Hash = String;
275
276/// Topic (hash) of a Magnet URI
277#[derive(Debug, PartialEq)]
278pub enum Topic {
279    /// urn:tree:tiger:TTHHash
280    TigerTreeHash(TTHHash),
281    /// urn:sha1:SHA1Hash
282    SHA1(SHA1Hash),
283    /// urn:bitprint:SHA1Hash.TTHHash
284    BitPrint(SHA1Hash, TTHHash),
285    /// urn:ed2k:ED2KHash
286    ED2K(ED2KHash),
287    /// urn:aich:AICHHash
288    AICH(AICHHash),
289    /// urn:kzhash:KazaaHash
290    Kazaa(KazaaHash),
291    /// urn:btih:BTInfoHash
292    BitTorrentInfoHash(BTInfoHash),
293    /// urn:md5:MD5Hash
294    MD5(MD5Hash),
295}
296
297impl Topic {
298    fn conflicts(&self, other: &Topic) -> bool {
299        use Topic::*;
300
301        match (self, other) {
302            (TigerTreeHash(h1), TigerTreeHash(h2)) => h1 != h2,
303            (SHA1(h1), SHA1(h2)) => h1 != h2,
304            (BitPrint(sha1, tth1), BitPrint(sha2, tth2)) => sha1 != sha2 || tth1 != tth2,
305            (ED2K(h1), ED2K(h2)) => h1 != h2,
306            (AICH(h1), AICH(h2)) => h1 != h2,
307            (Kazaa(h1), Kazaa(h2)) => h1 != h2,
308            (BitTorrentInfoHash(h1), BitTorrentInfoHash(h2)) => h1 != h2,
309            (MD5(h1), MD5(h2)) => h1 != h2,
310
311            (TigerTreeHash(tth1), BitPrint(_, tth2)) => tth1 != tth2,
312            (BitPrint(_, tth1), TigerTreeHash(tth2)) => tth1 != tth2,
313            (SHA1(sha1), BitPrint(sha2, _)) => sha1 != sha2,
314            (BitPrint(sha1, _), SHA1(sha2)) => sha1 != sha2,
315
316            _ => false,
317        }
318    }
319}
320
321impl FromStr for Topic {
322    type Err = Error;
323
324    fn from_str(s: &str) -> Result<Self, Self::Err> {
325        use Topic::*;
326
327        if let Some(hash) = match_prefix(s, exact_topic_urn::TIGER_TREE_HASH) {
328            Ok(TigerTreeHash(hash.to_owned()))
329        } else if let Some(hash) = match_prefix(s, exact_topic_urn::SHA1) {
330            Ok(SHA1(hash.to_owned()))
331        } else if let Some(hashes) = match_prefix(s, exact_topic_urn::BIT_PRINT) {
332            let mut parts = hashes.split('.');
333            if let (Some(sha_hash), Some(tth_hash), None) =
334                (parts.next(), parts.next(), parts.next())
335            {
336                Ok(BitPrint(sha_hash.to_owned(), tth_hash.to_owned()))
337            } else {
338                Err(Error::ExactTopic(s.to_owned()))
339            }
340        } else if let Some(hash) = match_prefix(s, exact_topic_urn::ED2K) {
341            Ok(ED2K(hash.to_owned()))
342        } else if let Some(hash) = match_prefix(s, exact_topic_urn::AICH) {
343            Ok(AICH(hash.to_owned()))
344        } else if let Some(hash) = match_prefix(s, exact_topic_urn::KAZAA) {
345            Ok(Kazaa(hash.to_owned()))
346        } else if let Some(hash) = match_prefix(s, exact_topic_urn::BITTORRENT_INFO_HASH) {
347            Ok(BitTorrentInfoHash(hash.to_owned()))
348        } else if let Some(hash) = match_prefix(s, exact_topic_urn::MD5) {
349            Ok(MD5(hash.to_owned()))
350        } else {
351            Err(Error::ExactTopic(s.to_owned()))
352        }
353    }
354}
355
356impl fmt::Display for Topic {
357    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
358        use Topic::*;
359        match self {
360            TigerTreeHash(hash) => write!(f, "{}{}", exact_topic_urn::TIGER_TREE_HASH, hash),
361            SHA1(hash) => write!(f, "{}{}", exact_topic_urn::SHA1, hash),
362            BitPrint(hash1, hash2) => {
363                write!(f, "{}{}.{}", exact_topic_urn::BIT_PRINT, hash1, hash2)
364            }
365            ED2K(hash) => write!(f, "{}{}", exact_topic_urn::ED2K, hash),
366            AICH(hash) => write!(f, "{}{}", exact_topic_urn::AICH, hash),
367            Kazaa(hash) => write!(f, "{}{}", exact_topic_urn::KAZAA, hash),
368            BitTorrentInfoHash(hash) => {
369                write!(f, "{}{}", exact_topic_urn::BITTORRENT_INFO_HASH, hash)
370            }
371            MD5(hash) => write!(f, "{}{}", exact_topic_urn::MD5, hash),
372        }
373    }
374}
375
376fn match_prefix<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
377    if s.starts_with(prefix) {
378        let (_, postfix) = s.split_at(prefix.len());
379        Some(postfix)
380    } else {
381        None
382    }
383}
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388
389    #[test]
390    fn test_match_prefix() {
391        assert_eq!(match_prefix("foobar", "foobar"), Some(""));
392        assert_eq!(match_prefix("foobar", "foo"), Some("bar"));
393        assert_eq!(match_prefix("foobar", "foob"), Some("ar"));
394        assert_eq!(match_prefix("foobar", "baz"), None);
395    }
396
397    #[test]
398    fn test_zero_file_parsing() {
399        let uri = MagnetURI::from_str("magnet:?xt=urn:ed2k:31D6CFE0D16AE931B73C59D7E0C089C0&xl=0&dn=zero_len.fil&xt=urn:bitprint:3I42H3S6NNFQ2MSVX7XZKYAYSCX5QBYJ.LWPNACQDBZRYXW3VHJVCJ64QBZNGHOHHHZWCLNQ&xt=urn:md5:D41D8CD98F00B204E9800998ECF8427E").unwrap();
400        assert!(uri.is_strictly_valid());
401        assert_eq!(uri.length(), Some(0));
402        assert_eq!(
403            uri.topics(),
404            vec![
405                &Topic::ED2K("31D6CFE0D16AE931B73C59D7E0C089C0".to_owned()),
406                &Topic::BitPrint(
407                    "3I42H3S6NNFQ2MSVX7XZKYAYSCX5QBYJ".to_owned(),
408                    "LWPNACQDBZRYXW3VHJVCJ64QBZNGHOHHHZWCLNQ".to_owned()
409                ),
410                &Topic::MD5("D41D8CD98F00B204E9800998ECF8427E".to_owned()),
411            ]
412        );
413    }
414
415    #[test]
416    fn test_invalid_non_matching_hashes() {
417        let uri = MagnetURI::from_str("magnet:?xt=urn:md5:31D6CFE0D16AE931B73C59D7E0C089C0&xl=0&dn=zero_len.fil&xt=urn:bitprint:3I42H3S6NNFQ2MSVX7XZKYAYSCX5QBYJ.LWPNACQDBZRYXW3VHJVCJ64QBZNGHOHHHZWCLNQ&xt=urn:md5:D41D8CD98F00B204E9800998ECF8427E").unwrap();
418        assert!(!uri.is_strictly_valid());
419    }
420
421    #[test]
422    fn test_invalid_no_length() {
423        let uri = MagnetURI::from_str("magnet:?xt=urn:ed2k:31D6CFE0D16AE931B73C59D7E0C089C0&dn=zero_len.fil&xt=urn:bitprint:3I42H3S6NNFQ2MSVX7XZKYAYSCX5QBYJ.LWPNACQDBZRYXW3VHJVCJ64QBZNGHOHHHZWCLNQ&xt=urn:md5:D41D8CD98F00B204E9800998ECF8427E").unwrap();
424        assert!(!uri.is_strictly_valid());
425        assert_eq!(uri.length(), None);
426    }
427}