From 97ab785b7c229013217fc5c2534120b951caebe7 Mon Sep 17 00:00:00 2001 From: Thomas Gardner Date: Sat, 3 Oct 2020 13:28:17 +1000 Subject: [PATCH] Implement FromStr for MagnetLink type: added --- Cargo.lock | 7 ++ Cargo.toml | 1 + src/common.rs | 15 +-- src/error.rs | 5 + src/infohash.rs | 6 ++ src/lib.rs | 1 + src/magnet_link.rs | 182 ++++++++++++++++++++++++++++++++- src/magnet_link_parse_error.rs | 32 ++++++ 8 files changed, 240 insertions(+), 9 deletions(-) create mode 100644 src/magnet_link_parse_error.rs diff --git a/Cargo.lock b/Cargo.lock index 920d1d32..69ba787c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -593,6 +593,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" + [[package]] name = "humansize" version = "1.1.0" @@ -654,6 +660,7 @@ dependencies = [ "console", "criterion", "globset", + "hex", "ignore", "imdl-indicatif", "lazy_static", diff --git a/Cargo.toml b/Cargo.toml index 634b4197..ee445216 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ atty = "0.2.0" chrono = "0.4.1" console = "0.12.0" globset = "0.4.0" +hex = "0.4.2" ignore = "0.4.14" lazy_static = "1.4.0" lexiclean = "0.0.1" diff --git a/src/common.rs b/src/common.rs index 9fe28ad5..6726f7e1 100644 --- a/src/common.rs +++ b/src/common.rs @@ -52,7 +52,7 @@ pub(crate) use url::{Host, Url}; pub(crate) use log::trace; // modules -pub(crate) use crate::{consts, error, host_port_parse_error}; +pub(crate) use crate::{consts, error, host_port_parse_error, magnet_link_parse_error}; // functions pub(crate) use crate::xor_args::xor_args; @@ -69,12 +69,13 @@ pub(crate) use crate::{ file_info::FileInfo, file_path::FilePath, file_status::FileStatus, files::Files, hasher::Hasher, host_port::HostPort, host_port_parse_error::HostPortParseError, info::Info, infohash::Infohash, input::Input, input_target::InputTarget, lint::Lint, linter::Linter, magnet_link::MagnetLink, - md5_digest::Md5Digest, metainfo::Metainfo, metainfo_error::MetainfoError, mode::Mode, - options::Options, output_stream::OutputStream, output_target::OutputTarget, - piece_length_picker::PieceLengthPicker, piece_list::PieceList, platform::Platform, - sha1_digest::Sha1Digest, shell::Shell, sort_key::SortKey, sort_order::SortOrder, - sort_spec::SortSpec, status::Status, style::Style, subcommand::Subcommand, table::Table, - torrent_summary::TorrentSummary, use_color::UseColor, verifier::Verifier, walker::Walker, + magnet_link_parse_error::MagnetLinkParseError, md5_digest::Md5Digest, metainfo::Metainfo, + metainfo_error::MetainfoError, mode::Mode, options::Options, output_stream::OutputStream, + output_target::OutputTarget, piece_length_picker::PieceLengthPicker, piece_list::PieceList, + platform::Platform, sha1_digest::Sha1Digest, shell::Shell, sort_key::SortKey, + sort_order::SortOrder, sort_spec::SortSpec, status::Status, style::Style, subcommand::Subcommand, + table::Table, torrent_summary::TorrentSummary, use_color::UseColor, verifier::Verifier, + walker::Walker, }; // type aliases diff --git a/src/error.rs b/src/error.rs index 219e2267..5aab92d3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -44,6 +44,11 @@ pub(crate) enum Error { Internal { message: String }, #[snafu(display("Unknown lint: {}", text))] LintUnknown { text: String }, + #[snafu(display("Failed to parse magnet link `{}`: {}", text, source))] + MagnetLinkParse { + text: String, + source: MagnetLinkParseError, + }, #[snafu(display("Failed to deserialize torrent metainfo from {}: {}", input, source))] MetainfoDeserialize { source: bendy::serde::Error, diff --git a/src/infohash.rs b/src/infohash.rs index 5b2d966d..d773f2c3 100644 --- a/src/infohash.rs +++ b/src/infohash.rs @@ -56,6 +56,12 @@ impl Infohash { } } +impl From for Infohash { + fn from(inner: Sha1Digest) -> Self { + Self { inner } + } +} + impl Into for Infohash { fn into(self) -> Sha1Digest { self.inner diff --git a/src/lib.rs b/src/lib.rs index e0fd9001..a15023c0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,6 +75,7 @@ mod invariant; mod lint; mod linter; mod magnet_link; +mod magnet_link_parse_error; mod md5_digest; mod metainfo; mod metainfo_error; diff --git a/src/magnet_link.rs b/src/magnet_link.rs index f7b19937..35ed1d74 100644 --- a/src/magnet_link.rs +++ b/src/magnet_link.rs @@ -1,5 +1,6 @@ use crate::common::*; +#[derive(Debug, PartialEq)] pub(crate) struct MagnetLink { infohash: Infohash, name: Option, @@ -22,7 +23,7 @@ impl MagnetLink { Ok(link) } - pub(crate) fn with_infohash(infohash: Infohash) -> MagnetLink { + pub(crate) fn with_infohash(infohash: Infohash) -> Self { MagnetLink { infohash, name: None, @@ -37,7 +38,6 @@ impl MagnetLink { self.name = Some(name.into()); } - #[allow(dead_code)] pub(crate) fn add_peer(&mut self, peer: HostPort) { self.peers.push(peer); } @@ -84,6 +84,74 @@ impl MagnetLink { url } + + fn parse(text: &str) -> Result { + let url = Url::parse(&text).context(magnet_link_parse_error::URL)?; + + if url.scheme() != "magnet" { + return Err(MagnetLinkParseError::Scheme { + scheme: url.scheme().into(), + }); + } + + let mut link = None; + for (k, v) in url.query_pairs() { + if k.as_ref() == "xt" { + if let Some(infohash) = v.strip_prefix("urn:btih:") { + if infohash.len() != 40 { + return Err(MagnetLinkParseError::InfohashLength { + text: infohash.into(), + }); + } + + let buf = hex::decode(infohash).context(magnet_link_parse_error::HexParse { + text: infohash.to_string(), + })?; + + link = Some(MagnetLink::with_infohash( + Sha1Digest::from_bytes( + buf + .as_slice() + .try_into() + .invariant_unwrap("bounds are checked above"), + ) + .into(), + )); + + break; + } + } + } + + let mut link = link.ok_or(MagnetLinkParseError::TopicMissing)?; + + for (k, v) in url.query_pairs() { + match k.as_ref() { + "tr" => link.add_tracker(Url::parse(&v).context( + magnet_link_parse_error::TrackerAddress { + text: v.to_string(), + }, + )?), + "dn" => link.set_name(v), + "x.pe" => link.add_peer(HostPort::from_str(&v).context( + magnet_link_parse_error::PeerAddress { + text: v.to_string(), + }, + )?), + _ => {} + } + } + + Ok(link) + } +} + +impl FromStr for MagnetLink { + type Err = Error; + + fn from_str(text: &str) -> Result { + Self::parse(text).context(error::MagnetLinkParse { text }) + } } impl Display for MagnetLink { @@ -179,4 +247,114 @@ mod tests { ), ); } + + #[test] + fn link_from_str_round_trip() { + let mut link_to = MagnetLink::with_infohash(Infohash::from_bencoded_info_dict("".as_bytes())); + + link_to.set_name("foo"); + link_to.add_tracker(Url::parse("http://foo.com/announce").unwrap()); + link_to.add_tracker(Url::parse("http://bar.net/announce").unwrap()); + link_to.add_peer("foo.com:1337".parse().unwrap()); + link_to.add_peer("bar.net:666".parse().unwrap()); + + let link_from = MagnetLink::from_str(&link_to.to_url().to_string()).unwrap(); + + assert_eq!(link_to, link_from); + } + + #[test] + fn link_from_str_url_error() { + let link = "%imdl.io"; + let e = MagnetLink::from_str(link).unwrap_err(); + + assert_matches!(e, Error::MagnetLinkParse { + text, + source: MagnetLinkParseError::URL { .. }, + } if text == link); + } + + #[test] + fn link_from_str_scheme_error() { + let link = "mailto:?alice@imdl.io"; + + let e = MagnetLink::from_str(link).unwrap_err(); + assert_matches!(e, Error::MagnetLinkParse { + text, + source: MagnetLinkParseError::Scheme { scheme }, + } if text == link && scheme == "mailto"); + } + + #[test] + fn link_from_str_infohash_length_error() { + let infohash = "123456789abcedf"; + let link = format!("magnet:?xt=urn:btih:{}", infohash); + let e = MagnetLink::from_str(&link).unwrap_err(); + + assert_matches!(e, Error::MagnetLinkParse { + text, + source: MagnetLinkParseError::InfohashLength { text: ih }, + } if text == link && infohash == ih); + } + + #[test] + fn link_from_str_infohash_bad_hex() { + let infohash = "laaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + let link = format!("magnet:?xt=urn:btih:{}", infohash); + let e = MagnetLink::from_str(&link).unwrap_err(); + + assert_matches!(e, Error::MagnetLinkParse { + text, + source: MagnetLinkParseError::HexParse { + text: ih, + source: _, + }} if text == link && infohash == ih); + } + + #[test] + fn link_from_str_topic_missing() { + let link = "magnet:?"; + let e = MagnetLink::from_str(&link).unwrap_err(); + + assert_matches!(e, + Error::MagnetLinkParse { + text, + source: MagnetLinkParseError::TopicMissing, + } if text == link); + } + + #[test] + fn link_from_str_tracker_address() { + let infohash = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + let bad_addr = "%imdl.io/announce"; + let link = format!("magnet:?xt=urn:btih:{}&tr={}", infohash, bad_addr); + let e = MagnetLink::from_str(&link).unwrap_err(); + + assert_matches!(e, + Error::MagnetLinkParse { + text, + source: MagnetLinkParseError::TrackerAddress { + text: addr, + source: _, + }, + } if text == link && addr == bad_addr); + } + + #[test] + fn link_from_str_peer_address() { + let infohash = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + let bad_addr = "%imdl.io:13337"; + let link = format!("magnet:?xt=urn:btih:{}&x.pe={}", infohash, bad_addr); + let e = MagnetLink::from_str(&link).unwrap_err(); + + assert_matches!(e, + Error::MagnetLinkParse { + text, + source: MagnetLinkParseError::PeerAddress { + text: addr, + source: _, + } + } if text == link && addr == bad_addr + ); + } } diff --git a/src/magnet_link_parse_error.rs b/src/magnet_link_parse_error.rs new file mode 100644 index 00000000..9196e9bd --- /dev/null +++ b/src/magnet_link_parse_error.rs @@ -0,0 +1,32 @@ +use crate::common::*; + +#[derive(Debug, Snafu)] +#[snafu(visibility(pub(crate)))] +pub(crate) enum MagnetLinkParseError { + #[snafu(display("Failed to parse hex string `{}`: {}", text, source))] + HexParse { + text: String, + source: hex::FromHexError, + }, + #[snafu(display("Hex-encoded infohash, `{}`, is not 40 characters long", text))] + InfohashLength { text: String }, + #[snafu(display("Failed to parse peer address `{}`: {}", text, source))] + PeerAddress { + text: String, + source: HostPortParseError, + }, + #[snafu(display( + "Invalid scheme: `{}`. Magnet links must use the `magnet:` scheme", + scheme + ))] + Scheme { scheme: String }, + #[snafu(display("Magnet link must have a topic that begins with `urn:btih:`"))] + TopicMissing, + #[snafu(display("Failed to parse tracker address `{}`: {}", text, source))] + TrackerAddress { + text: String, + source: url::ParseError, + }, + #[snafu(display("Failed to parse URL: {}", source))] + URL { source: url::ParseError }, +}