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#[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#[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
267type TTHHash = String;
269type SHA1Hash = String;
270type ED2KHash = String;
271type AICHHash = String;
272type KazaaHash = String;
273type BTInfoHash = String;
274type MD5Hash = String;
275
276#[derive(Debug, PartialEq)]
278pub enum Topic {
279 TigerTreeHash(TTHHash),
281 SHA1(SHA1Hash),
283 BitPrint(SHA1Hash, TTHHash),
285 ED2K(ED2KHash),
287 AICH(AICHHash),
289 Kazaa(KazaaHash),
291 BitTorrentInfoHash(BTInfoHash),
293 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}