Implement FromStr for MagnetLink
type: added
This commit is contained in:
parent
a787d6a964
commit
97ab785b7c
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::common::*;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct MagnetLink {
|
||||
infohash: Infohash,
|
||||
name: Option<String>,
|
||||
|
@ -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<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 {
|
||||
|
@ -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
src/magnet_link_parse_error.rs
Normal file
32
src/magnet_link_parse_error.rs
Normal file
|
@ -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 },
|
||||
}
|
Loading…
Reference in New Issue
Block a user