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",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "humansize"
|
name = "humansize"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
@ -654,6 +660,7 @@ dependencies = [
|
||||||
"console",
|
"console",
|
||||||
"criterion",
|
"criterion",
|
||||||
"globset",
|
"globset",
|
||||||
|
"hex",
|
||||||
"ignore",
|
"ignore",
|
||||||
"imdl-indicatif",
|
"imdl-indicatif",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
|
|
@ -22,6 +22,7 @@ atty = "0.2.0"
|
||||||
chrono = "0.4.1"
|
chrono = "0.4.1"
|
||||||
console = "0.12.0"
|
console = "0.12.0"
|
||||||
globset = "0.4.0"
|
globset = "0.4.0"
|
||||||
|
hex = "0.4.2"
|
||||||
ignore = "0.4.14"
|
ignore = "0.4.14"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
lexiclean = "0.0.1"
|
lexiclean = "0.0.1"
|
||||||
|
|
|
@ -52,7 +52,7 @@ pub(crate) use url::{Host, Url};
|
||||||
pub(crate) use log::trace;
|
pub(crate) use log::trace;
|
||||||
|
|
||||||
// modules
|
// 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
|
// functions
|
||||||
pub(crate) use crate::xor_args::xor_args;
|
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,
|
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,
|
host_port::HostPort, host_port_parse_error::HostPortParseError, info::Info, infohash::Infohash,
|
||||||
input::Input, input_target::InputTarget, lint::Lint, linter::Linter, magnet_link::MagnetLink,
|
input::Input, input_target::InputTarget, lint::Lint, linter::Linter, magnet_link::MagnetLink,
|
||||||
md5_digest::Md5Digest, metainfo::Metainfo, metainfo_error::MetainfoError, mode::Mode,
|
magnet_link_parse_error::MagnetLinkParseError, md5_digest::Md5Digest, metainfo::Metainfo,
|
||||||
options::Options, output_stream::OutputStream, output_target::OutputTarget,
|
metainfo_error::MetainfoError, mode::Mode, options::Options, output_stream::OutputStream,
|
||||||
piece_length_picker::PieceLengthPicker, piece_list::PieceList, platform::Platform,
|
output_target::OutputTarget, piece_length_picker::PieceLengthPicker, piece_list::PieceList,
|
||||||
sha1_digest::Sha1Digest, shell::Shell, sort_key::SortKey, sort_order::SortOrder,
|
platform::Platform, sha1_digest::Sha1Digest, shell::Shell, sort_key::SortKey,
|
||||||
sort_spec::SortSpec, status::Status, style::Style, subcommand::Subcommand, table::Table,
|
sort_order::SortOrder, sort_spec::SortSpec, status::Status, style::Style, subcommand::Subcommand,
|
||||||
torrent_summary::TorrentSummary, use_color::UseColor, verifier::Verifier, walker::Walker,
|
table::Table, torrent_summary::TorrentSummary, use_color::UseColor, verifier::Verifier,
|
||||||
|
walker::Walker,
|
||||||
};
|
};
|
||||||
|
|
||||||
// type aliases
|
// type aliases
|
||||||
|
|
|
@ -44,6 +44,11 @@ pub(crate) enum Error {
|
||||||
Internal { message: String },
|
Internal { message: String },
|
||||||
#[snafu(display("Unknown lint: {}", text))]
|
#[snafu(display("Unknown lint: {}", text))]
|
||||||
LintUnknown { text: String },
|
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))]
|
#[snafu(display("Failed to deserialize torrent metainfo from {}: {}", input, source))]
|
||||||
MetainfoDeserialize {
|
MetainfoDeserialize {
|
||||||
source: bendy::serde::Error,
|
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 {
|
impl Into<Sha1Digest> for Infohash {
|
||||||
fn into(self) -> Sha1Digest {
|
fn into(self) -> Sha1Digest {
|
||||||
self.inner
|
self.inner
|
||||||
|
|
|
@ -75,6 +75,7 @@ mod invariant;
|
||||||
mod lint;
|
mod lint;
|
||||||
mod linter;
|
mod linter;
|
||||||
mod magnet_link;
|
mod magnet_link;
|
||||||
|
mod magnet_link_parse_error;
|
||||||
mod md5_digest;
|
mod md5_digest;
|
||||||
mod metainfo;
|
mod metainfo;
|
||||||
mod metainfo_error;
|
mod metainfo_error;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
pub(crate) struct MagnetLink {
|
pub(crate) struct MagnetLink {
|
||||||
infohash: Infohash,
|
infohash: Infohash,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
|
@ -22,7 +23,7 @@ impl MagnetLink {
|
||||||
Ok(link)
|
Ok(link)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn with_infohash(infohash: Infohash) -> MagnetLink {
|
pub(crate) fn with_infohash(infohash: Infohash) -> Self {
|
||||||
MagnetLink {
|
MagnetLink {
|
||||||
infohash,
|
infohash,
|
||||||
name: None,
|
name: None,
|
||||||
|
@ -37,7 +38,6 @@ impl MagnetLink {
|
||||||
self.name = Some(name.into());
|
self.name = Some(name.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) fn add_peer(&mut self, peer: HostPort) {
|
pub(crate) fn add_peer(&mut self, peer: HostPort) {
|
||||||
self.peers.push(peer);
|
self.peers.push(peer);
|
||||||
}
|
}
|
||||||
|
@ -84,6 +84,74 @@ impl MagnetLink {
|
||||||
|
|
||||||
url
|
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 {
|
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