Navigation Menu

Skip to content

Commit

Permalink
Implement FromStr for MagnetLink
Browse files Browse the repository at this point in the history
type: added
  • Loading branch information
atomgardner authored and casey committed Oct 18, 2020
1 parent a787d6a commit 97ab785
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 9 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Expand Up @@ -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"
Expand Down
15 changes: 8 additions & 7 deletions src/common.rs
Expand Up @@ -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;
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/error.rs
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions src/infohash.rs
Expand Up @@ -56,6 +56,12 @@ impl Infohash {
}
}

impl From<Sha1Digest> for Infohash {
fn from(inner: Sha1Digest) -> Self {
Self { inner }
}
}

impl Into<Sha1Digest> for Infohash {
fn into(self) -> Sha1Digest {
self.inner
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Expand Up @@ -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;
Expand Down
182 changes: 180 additions & 2 deletions src/magnet_link.rs
@@ -1,5 +1,6 @@
use crate::common::*;

#[derive(Debug, PartialEq)]
pub(crate) struct MagnetLink {
infohash: Infohash,
name: Option<String>,
Expand All @@ -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,
Expand All @@ -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);
}
Expand Down Expand Up @@ -84,6 +84,74 @@ impl MagnetLink {

url
}

fn parse(text: &str) -> Result<Self, MagnetLinkParseError> {
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, Self::Err> {
Self::parse(text).context(error::MagnetLinkParse { text })
}
}

impl Display for MagnetLink {
Expand Down Expand Up @@ -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
);
}
}
32 changes: 32 additions & 0 deletions 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 },
}

0 comments on commit 97ab785

Please sign in to comment.