magnet-uri-0.2.0/.gitignore010064400007650000024000000005001336327543500137760ustar0000000000000000# Generated by Cargo # will have compiled files and executables /target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk magnet-uri-0.2.0/.travis.yml010064400007650000024000000002041337352640500141150ustar0000000000000000language: rust rust: - stable - beta - nightly matrix: allow_failures: - rust: nightly fast_finish: true cache: cargo magnet-uri-0.2.0/Cargo.toml.orig010064400007650000024000000006771364131503600147030ustar0000000000000000[package] name = "magnet-uri" version = "0.2.0" authors = ["Jakub Jaroszewski "] description = "A library for parsing Magnet URI scheme" repository = "https://github.com/pierd/magnet-uri" keywords = ["magnet", "bittorrent"] categories = ["network-programming"] readme = "README.md" license = "MIT" [dependencies] serde_urlencoded = "0.5.3" [badges] travis-ci = { repository = "pierd/magnet-uri", branch = "master" } magnet-uri-0.2.0/Cargo.toml0000644000000017131364131523500112040ustar00# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies # # If you believe there's an error in this file please file an # issue against the rust-lang/cargo repository. If you're # editing this file be aware that the upstream Cargo.toml # will likely look very different (and much more reasonable) [package] name = "magnet-uri" version = "0.2.0" authors = ["Jakub Jaroszewski "] description = "A library for parsing Magnet URI scheme" readme = "README.md" keywords = ["magnet", "bittorrent"] categories = ["network-programming"] license = "MIT" repository = "https://github.com/pierd/magnet-uri" [dependencies.serde_urlencoded] version = "0.5.3" [badges.travis-ci] branch = "master" repository = "pierd/magnet-uri" magnet-uri-0.2.0/LICENSE010064400007650000024000000020451336327511200130110ustar0000000000000000MIT License Copyright (c) 2018 Kuba Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. magnet-uri-0.2.0/README.md010064400007650000024000000000651336327511200132630ustar0000000000000000# magnet-uri A library for parsing Magnet URI scheme magnet-uri-0.2.0/src/lib.rs010064400007650000024000000316671364131472500137260ustar0000000000000000extern crate serde_urlencoded; use serde_urlencoded::de::Error as UrlEncodeError; use std::fmt; use std::str::FromStr; const SCHEME: &str = "magnet:?"; pub(self) mod field_name { pub const NAME: &str = "dn"; pub const LENGTH: &str = "xl"; pub const TOPIC: &str = "xt"; pub const ACCEPTABLE_SOURCE: &str = "as"; pub const EXACT_SOURCE: &str = "xs"; pub const KEYWORD: &str = "kt"; pub const MANIFEST: &str = "mt"; pub const ADDRESS_TRACKER: &str = "tr"; pub const EXTENSION_PREFIX: &str = "x."; } pub(self) mod exact_topic_urn { pub const TIGER_TREE_HASH: &str = "urn:tree:tiger:"; pub const SHA1: &str = "urn:sha1:"; pub const BIT_PRINT: &str = "urn:bitprint:"; pub const ED2K: &str = "urn:ed2k:"; pub const AICH: &str = "urn:aich:"; pub const KAZAA: &str = "urn:kzhash:"; pub const BITTORRENT_INFO_HASH: &str = "urn:btih:"; pub const MD5: &str = "urn:md5:"; } #[derive(Debug)] pub enum Error { Scheme, UrlEncode(UrlEncodeError), Field(String, String), ExactTopic(String), } impl Error { fn with_field(key: &str, val: &str) -> Self { Error::Field(key.to_owned(), val.to_owned()) } } /// A struct holding fields stored in a Magnet URI #[derive(Debug, Default)] pub struct MagnetURI { fields: Vec, } impl MagnetURI { pub fn has_extensions(&self) -> bool { self.fields.iter().any(Field::is_extension) } pub fn has_unknown_fields(&self) -> bool { self.fields.iter().any(Field::is_unknown) } pub fn has_topic_conflict(&self) -> bool { self.iter_topics().any(|topic1| { self.iter_topics() .any(|topic2| Topic::conflicts(topic1, topic2)) }) } pub fn is_strictly_valid(&self) -> bool { !self.has_unknown_fields() && self.length() != None && !self.has_topic_conflict() } pub fn names(&self) -> Vec<&str> { self.iter_field_values(Field::name).collect() } pub fn name(&self) -> Option<&str> { self.iter_field_values(Field::name).next() } pub fn dn(&self) -> Option<&str> { self.name() } pub fn length(&self) -> Option { self.iter_field_values(Field::length).next() } pub fn xl(&self) -> Option { self.length() } pub fn iter_topics(&self) -> impl Iterator { self.iter_field_values(Field::topic) } pub fn topics(&self) -> Vec<&Topic> { self.iter_topics().collect() } pub fn info_hashes(&self) -> Vec<&BTInfoHash> { self.iter_field_values(Field::info_hash).collect() } pub fn info_hash(&self) -> Option<&BTInfoHash> { self.iter_field_values(Field::info_hash).next() } fn iter_field_values<'a, F, T>(&'a self, f: F) -> impl Iterator + 'a where F: Fn(&'a Field) -> Option + Sized + 'a, T: 'a, { self.fields .iter() .map(f) .filter(Option::is_some) .map(Option::unwrap) } pub fn add_field(&mut self, f: Field) -> &Self { self.fields.push(f); self } pub fn add_name(&mut self, name: &str) -> &Self { self.add_field(Field::Name(name.to_owned())) } pub fn add_topic(&mut self, xt: Topic) -> &Self { self.add_field(Field::Topic(xt)) } pub fn add_extension(&mut self, ext_name: &str, val: &str) -> &Self { self.add_field(Field::Extension(ext_name.to_owned(), val.to_owned())) } pub fn set_name(&mut self, name: &str) -> &Self { self.set_unique_field(|f| f.name().is_none(), Field::Name(name.to_owned())) } pub fn set_info_hash(&mut self, btih: BTInfoHash) -> &Self { self.set_unique_field( |f| match f { Field::Topic(Topic::BitTorrentInfoHash(_)) => false, _ => true, }, Field::Topic(Topic::BitTorrentInfoHash(btih)), ) } fn set_unique_field(&mut self, retain_filter: F, field: Field) -> &Self where F: FnMut(&Field) -> bool, { self.fields.retain(retain_filter); self.add_field(field) } } impl FromStr for MagnetURI { type Err = Error; fn from_str(s: &str) -> Result { if !s.starts_with(SCHEME) { return Err(Error::Scheme); } let (_, qs) = s.split_at(SCHEME.len()); let parse_result: Result, _> = serde_urlencoded::from_str(qs); match parse_result { Err(e) => Err(Error::UrlEncode(e)), Ok(parts) => Ok(MagnetURI { fields: parts .iter() .map(|(k, v)| Field::from_str(k, v)) .collect::, _>>()?, }), } } } /// Field of a Magnet URI #[derive(Debug, PartialEq)] pub enum Field { Name(String), Length(u64), Topic(Topic), AcceptableSource(String), ExactSource(String), Keyword(String), Manifest(String), AddressTracker(String), Extension(String, String), Unknown(String, String), } impl Field { fn from_str(key: &str, val: &str) -> Result { use field_name::*; use Field::*; match key { NAME => Ok(Name(val.to_owned())), LENGTH => match u64::from_str(val) { Err(_) => Err(Error::with_field(key, val)), Ok(l) => Ok(Length(l)), }, TOPIC => Ok(Topic(self::Topic::from_str(val)?)), ACCEPTABLE_SOURCE => Ok(AcceptableSource(val.to_owned())), EXACT_SOURCE => Ok(ExactSource(val.to_owned())), KEYWORD => Ok(Keyword(val.to_owned())), MANIFEST => Ok(Manifest(val.to_owned())), ADDRESS_TRACKER => Ok(AddressTracker(val.to_owned())), _ => { if key.starts_with(EXTENSION_PREFIX) { let (_, ext_name) = key.split_at(EXTENSION_PREFIX.len()); Ok(Extension(ext_name.to_owned(), val.to_owned())) } else { Ok(Unknown(key.to_owned(), val.to_owned())) } } } } fn is_extension(&self) -> bool { match self { Field::Extension(_, _) => true, _ => false, } } fn is_unknown(&self) -> bool { match self { Field::Unknown(_, _) => true, _ => false, } } fn name(&self) -> Option<&str> { match self { Field::Name(ref name) => Some(name), _ => None, } } fn length(&self) -> Option { match self { Field::Length(len) => Some(*len), _ => None, } } fn topic(&self) -> Option<&Topic> { match self { Field::Topic(topic) => Some(topic), _ => None, } } fn info_hash(&self) -> Option<&BTInfoHash> { match self { Field::Topic(Topic::BitTorrentInfoHash(ref hash)) => Some(hash), _ => None, } } } // TODO: use more specific types type TTHHash = String; type SHA1Hash = String; type ED2KHash = String; type AICHHash = String; type KazaaHash = String; type BTInfoHash = String; type MD5Hash = String; /// Topic (hash) of a Magnet URI #[derive(Debug, PartialEq)] pub enum Topic { /// urn:tree:tiger:TTHHash TigerTreeHash(TTHHash), /// urn:sha1:SHA1Hash SHA1(SHA1Hash), /// urn:bitprint:SHA1Hash.TTHHash BitPrint(SHA1Hash, TTHHash), /// urn:ed2k:ED2KHash ED2K(ED2KHash), /// urn:aich:AICHHash AICH(AICHHash), /// urn:kzhash:KazaaHash Kazaa(KazaaHash), /// urn:btih:BTInfoHash BitTorrentInfoHash(BTInfoHash), /// urn:md5:MD5Hash MD5(MD5Hash), } impl Topic { fn conflicts(&self, other: &Topic) -> bool { use Topic::*; match (self, other) { (TigerTreeHash(h1), TigerTreeHash(h2)) => h1 != h2, (SHA1(h1), SHA1(h2)) => h1 != h2, (BitPrint(sha1, tth1), BitPrint(sha2, tth2)) => sha1 != sha2 || tth1 != tth2, (ED2K(h1), ED2K(h2)) => h1 != h2, (AICH(h1), AICH(h2)) => h1 != h2, (Kazaa(h1), Kazaa(h2)) => h1 != h2, (BitTorrentInfoHash(h1), BitTorrentInfoHash(h2)) => h1 != h2, (MD5(h1), MD5(h2)) => h1 != h2, (TigerTreeHash(tth1), BitPrint(_, tth2)) => tth1 != tth2, (BitPrint(_, tth1), TigerTreeHash(tth2)) => tth1 != tth2, (SHA1(sha1), BitPrint(sha2, _)) => sha1 != sha2, (BitPrint(sha1, _), SHA1(sha2)) => sha1 != sha2, _ => false, } } } impl FromStr for Topic { type Err = Error; fn from_str(s: &str) -> Result { use Topic::*; if let Some(hash) = match_prefix(s, exact_topic_urn::TIGER_TREE_HASH) { Ok(TigerTreeHash(hash.to_owned())) } else if let Some(hash) = match_prefix(s, exact_topic_urn::SHA1) { Ok(SHA1(hash.to_owned())) } else if let Some(hashes) = match_prefix(s, exact_topic_urn::BIT_PRINT) { let mut parts = hashes.split('.'); if let (Some(sha_hash), Some(tth_hash), None) = (parts.next(), parts.next(), parts.next()) { Ok(BitPrint(sha_hash.to_owned(), tth_hash.to_owned())) } else { Err(Error::ExactTopic(s.to_owned())) } } else if let Some(hash) = match_prefix(s, exact_topic_urn::ED2K) { Ok(ED2K(hash.to_owned())) } else if let Some(hash) = match_prefix(s, exact_topic_urn::AICH) { Ok(AICH(hash.to_owned())) } else if let Some(hash) = match_prefix(s, exact_topic_urn::KAZAA) { Ok(Kazaa(hash.to_owned())) } else if let Some(hash) = match_prefix(s, exact_topic_urn::BITTORRENT_INFO_HASH) { Ok(BitTorrentInfoHash(hash.to_owned())) } else if let Some(hash) = match_prefix(s, exact_topic_urn::MD5) { Ok(MD5(hash.to_owned())) } else { Err(Error::ExactTopic(s.to_owned())) } } } impl fmt::Display for Topic { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use Topic::*; match self { TigerTreeHash(hash) => write!(f, "{}{}", exact_topic_urn::TIGER_TREE_HASH, hash), SHA1(hash) => write!(f, "{}{}", exact_topic_urn::SHA1, hash), BitPrint(hash1, hash2) => { write!(f, "{}{}.{}", exact_topic_urn::BIT_PRINT, hash1, hash2) } ED2K(hash) => write!(f, "{}{}", exact_topic_urn::ED2K, hash), AICH(hash) => write!(f, "{}{}", exact_topic_urn::AICH, hash), Kazaa(hash) => write!(f, "{}{}", exact_topic_urn::KAZAA, hash), BitTorrentInfoHash(hash) => { write!(f, "{}{}", exact_topic_urn::BITTORRENT_INFO_HASH, hash) } MD5(hash) => write!(f, "{}{}", exact_topic_urn::MD5, hash), } } } fn match_prefix<'a>(s: &'a str, prefix: &str) -> Option<&'a str> { if s.starts_with(prefix) { let (_, postfix) = s.split_at(prefix.len()); Some(postfix) } else { None } } #[cfg(test)] mod tests { use super::*; #[test] fn test_match_prefix() { assert_eq!(match_prefix("foobar", "foobar"), Some("")); assert_eq!(match_prefix("foobar", "foo"), Some("bar")); assert_eq!(match_prefix("foobar", "foob"), Some("ar")); assert_eq!(match_prefix("foobar", "baz"), None); } #[test] fn test_zero_file_parsing() { 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(); assert!(uri.is_strictly_valid()); assert_eq!(uri.length(), Some(0)); assert_eq!( uri.topics(), vec![ &Topic::ED2K("31D6CFE0D16AE931B73C59D7E0C089C0".to_owned()), &Topic::BitPrint( "3I42H3S6NNFQ2MSVX7XZKYAYSCX5QBYJ".to_owned(), "LWPNACQDBZRYXW3VHJVCJ64QBZNGHOHHHZWCLNQ".to_owned() ), &Topic::MD5("D41D8CD98F00B204E9800998ECF8427E".to_owned()), ] ); } #[test] fn test_invalid_non_matching_hashes() { 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(); assert!(!uri.is_strictly_valid()); } #[test] fn test_invalid_no_length() { let uri = MagnetURI::from_str("magnet:?xt=urn:ed2k:31D6CFE0D16AE931B73C59D7E0C089C0&dn=zero_len.fil&xt=urn:bitprint:3I42H3S6NNFQ2MSVX7XZKYAYSCX5QBYJ.LWPNACQDBZRYXW3VHJVCJ64QBZNGHOHHHZWCLNQ&xt=urn:md5:D41D8CD98F00B204E9800998ECF8427E").unwrap(); assert!(!uri.is_strictly_valid()); assert_eq!(uri.length(), None); } } magnet-uri-0.2.0/.cargo_vcs_info.json0000644000000001121364131523500131760ustar00{ "git": { "sha1": "db420c1a4eccdba0c7197a03ebe5554b47af994d" } }