Allow reading torrent metainfo from stdin
Torrent metainfo can be read from standard input by passing `-`: cat a.torrent | imdl torrent verify --input - cat a.torrent | imdl torrent link --input - cat a.torrent | imdl torrent show --input - type: added
This commit is contained in:
parent
1c84172ad4
commit
498549b35c
|
@ -61,12 +61,12 @@ pub(crate) use crate::{
|
||||||
arguments::Arguments, bytes::Bytes, env::Env, error::Error, file_error::FileError,
|
arguments::Arguments, bytes::Bytes, env::Env, error::Error, file_error::FileError,
|
||||||
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,
|
||||||
lint::Lint, linter::Linter, magnet_link::MagnetLink, md5_digest::Md5Digest, metainfo::Metainfo,
|
input::Input, input_target::InputTarget, lint::Lint, linter::Linter, magnet_link::MagnetLink,
|
||||||
metainfo_error::MetainfoError, mode::Mode, options::Options, output_stream::OutputStream,
|
md5_digest::Md5Digest, metainfo::Metainfo, metainfo_error::MetainfoError, mode::Mode,
|
||||||
output_target::OutputTarget, piece_length_picker::PieceLengthPicker, piece_list::PieceList,
|
options::Options, output_stream::OutputStream, output_target::OutputTarget,
|
||||||
platform::Platform, sha1_digest::Sha1Digest, status::Status, style::Style,
|
piece_length_picker::PieceLengthPicker, piece_list::PieceList, platform::Platform,
|
||||||
subcommand::Subcommand, table::Table, torrent_summary::TorrentSummary, use_color::UseColor,
|
sha1_digest::Sha1Digest, status::Status, style::Style, subcommand::Subcommand, table::Table,
|
||||||
verifier::Verifier, walker::Walker,
|
torrent_summary::TorrentSummary, use_color::UseColor, verifier::Verifier, walker::Walker,
|
||||||
};
|
};
|
||||||
|
|
||||||
// type aliases
|
// type aliases
|
||||||
|
|
42
src/env.rs
42
src/env.rs
|
@ -3,6 +3,7 @@ use crate::common::*;
|
||||||
pub(crate) struct Env {
|
pub(crate) struct Env {
|
||||||
args: Vec<OsString>,
|
args: Vec<OsString>,
|
||||||
dir: PathBuf,
|
dir: PathBuf,
|
||||||
|
input: Box<dyn Read>,
|
||||||
err: OutputStream,
|
err: OutputStream,
|
||||||
out: OutputStream,
|
out: OutputStream,
|
||||||
}
|
}
|
||||||
|
@ -20,7 +21,13 @@ impl Env {
|
||||||
let out_stream = OutputStream::stdout(style);
|
let out_stream = OutputStream::stdout(style);
|
||||||
let err_stream = OutputStream::stderr(style);
|
let err_stream = OutputStream::stderr(style);
|
||||||
|
|
||||||
Self::new(dir, env::args(), out_stream, err_stream)
|
Self::new(
|
||||||
|
dir,
|
||||||
|
env::args(),
|
||||||
|
Box::new(io::stdin()),
|
||||||
|
out_stream,
|
||||||
|
err_stream,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn run(&mut self) -> Result<(), Error> {
|
pub(crate) fn run(&mut self) -> Result<(), Error> {
|
||||||
|
@ -69,13 +76,20 @@ impl Env {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new<S, I>(dir: PathBuf, args: I, out: OutputStream, err: OutputStream) -> Self
|
pub(crate) fn new<S, I>(
|
||||||
|
dir: PathBuf,
|
||||||
|
args: I,
|
||||||
|
input: Box<dyn Read>,
|
||||||
|
out: OutputStream,
|
||||||
|
err: OutputStream,
|
||||||
|
) -> Self
|
||||||
where
|
where
|
||||||
S: Into<OsString>,
|
S: Into<OsString>,
|
||||||
I: IntoIterator<Item = S>,
|
I: IntoIterator<Item = S>,
|
||||||
{
|
{
|
||||||
Self {
|
Self {
|
||||||
args: args.into_iter().map(Into::into).collect(),
|
args: args.into_iter().map(Into::into).collect(),
|
||||||
|
input,
|
||||||
dir,
|
dir,
|
||||||
out,
|
out,
|
||||||
err,
|
err,
|
||||||
|
@ -129,10 +143,6 @@ impl Env {
|
||||||
&self.dir
|
&self.dir
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn resolve(&self, path: impl AsRef<Path>) -> PathBuf {
|
|
||||||
self.dir().join(path).clean()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn err(&self) -> &OutputStream {
|
pub(crate) fn err(&self) -> &OutputStream {
|
||||||
&self.err
|
&self.err
|
||||||
}
|
}
|
||||||
|
@ -148,6 +158,26 @@ impl Env {
|
||||||
pub(crate) fn out_mut(&mut self) -> &mut OutputStream {
|
pub(crate) fn out_mut(&mut self) -> &mut OutputStream {
|
||||||
&mut self.out
|
&mut self.out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn resolve(&self, path: impl AsRef<Path>) -> PathBuf {
|
||||||
|
self.dir().join(path).clean()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read(&mut self, source: InputTarget) -> Result<Input> {
|
||||||
|
let data = match &source {
|
||||||
|
InputTarget::File(path) => {
|
||||||
|
let absolute = self.resolve(path);
|
||||||
|
fs::read(absolute).context(error::Filesystem { path })?
|
||||||
|
}
|
||||||
|
InputTarget::Stdin => {
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
self.input.read_to_end(&mut buffer).context(error::Stdin)?;
|
||||||
|
buffer
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Input::new(source, data))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
14
src/error.rs
14
src/error.rs
|
@ -7,21 +7,21 @@ use structopt::clap;
|
||||||
pub(crate) enum Error {
|
pub(crate) enum Error {
|
||||||
#[snafu(display("Failed to parse announce URL: {}", source))]
|
#[snafu(display("Failed to parse announce URL: {}", source))]
|
||||||
AnnounceUrlParse { source: url::ParseError },
|
AnnounceUrlParse { source: url::ParseError },
|
||||||
#[snafu(display("Failed to deserialize torrent metainfo from `{}`: {}", path.display(), source))]
|
#[snafu(display("Failed to deserialize torrent metainfo from {}: {}", input, source))]
|
||||||
MetainfoDeserialize {
|
MetainfoDeserialize {
|
||||||
source: bendy::serde::Error,
|
source: bendy::serde::Error,
|
||||||
path: PathBuf,
|
input: InputTarget,
|
||||||
},
|
},
|
||||||
#[snafu(display("Failed to serialize torrent metainfo: {}", source))]
|
#[snafu(display("Failed to serialize torrent metainfo: {}", source))]
|
||||||
MetainfoSerialize { source: bendy::serde::Error },
|
MetainfoSerialize { source: bendy::serde::Error },
|
||||||
#[snafu(display("Failed to decode torrent metainfo from `{}`: {}", path.display(), error))]
|
#[snafu(display("Failed to decode torrent metainfo from {}: {}", input, error))]
|
||||||
MetainfoDecode {
|
MetainfoDecode {
|
||||||
path: PathBuf,
|
input: InputTarget,
|
||||||
error: bendy::decoding::Error,
|
error: bendy::decoding::Error,
|
||||||
},
|
},
|
||||||
#[snafu(display("Metainfo from `{}` failed to validate: {}", path.display(), source))]
|
#[snafu(display("Metainfo from {} failed to validate: {}", input, source))]
|
||||||
MetainfoValidate {
|
MetainfoValidate {
|
||||||
path: PathBuf,
|
input: InputTarget,
|
||||||
source: MetainfoError,
|
source: MetainfoError,
|
||||||
},
|
},
|
||||||
#[snafu(display("Failed to parse byte count `{}`: {}", text, source))]
|
#[snafu(display("Failed to parse byte count `{}`: {}", text, source))]
|
||||||
|
@ -107,6 +107,8 @@ pub(crate) enum Error {
|
||||||
PrivateTrackerless,
|
PrivateTrackerless,
|
||||||
#[snafu(display("Failed to write to standard error: {}", source))]
|
#[snafu(display("Failed to write to standard error: {}", source))]
|
||||||
Stderr { source: io::Error },
|
Stderr { source: io::Error },
|
||||||
|
#[snafu(display("Failed to read from standard input: {}", source))]
|
||||||
|
Stdin { source: io::Error },
|
||||||
#[snafu(display("Failed to write to standard output: {}", source))]
|
#[snafu(display("Failed to write to standard output: {}", source))]
|
||||||
Stdout { source: io::Error },
|
Stdout { source: io::Error },
|
||||||
#[snafu(display(
|
#[snafu(display(
|
||||||
|
|
|
@ -6,11 +6,9 @@ pub(crate) struct Infohash {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Infohash {
|
impl Infohash {
|
||||||
pub(crate) fn load(path: &Path) -> Result<Infohash, Error> {
|
pub(crate) fn from_input(input: &Input) -> Result<Infohash, Error> {
|
||||||
let bytes = fs::read(path).context(error::Filesystem { path })?;
|
let value = Value::from_bencode(input.data()).map_err(|error| Error::MetainfoDecode {
|
||||||
|
input: input.source().clone(),
|
||||||
let value = Value::from_bencode(&bytes).map_err(|error| Error::MetainfoDecode {
|
|
||||||
path: path.to_owned(),
|
|
||||||
error,
|
error,
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
@ -20,7 +18,7 @@ impl Infohash {
|
||||||
.iter()
|
.iter()
|
||||||
.find(|pair: &(&Cow<[u8]>, &Value)| pair.0.as_ref() == b"info")
|
.find(|pair: &(&Cow<[u8]>, &Value)| pair.0.as_ref() == b"info")
|
||||||
.ok_or_else(|| Error::MetainfoValidate {
|
.ok_or_else(|| Error::MetainfoValidate {
|
||||||
path: path.to_owned(),
|
input: input.source().clone(),
|
||||||
source: MetainfoError::InfoMissing,
|
source: MetainfoError::InfoMissing,
|
||||||
})?
|
})?
|
||||||
.1;
|
.1;
|
||||||
|
@ -33,13 +31,13 @@ impl Infohash {
|
||||||
Ok(Self::from_bencoded_info_dict(&encoded))
|
Ok(Self::from_bencoded_info_dict(&encoded))
|
||||||
} else {
|
} else {
|
||||||
Err(Error::MetainfoValidate {
|
Err(Error::MetainfoValidate {
|
||||||
path: path.to_owned(),
|
input: input.source().clone(),
|
||||||
source: MetainfoError::InfoType,
|
source: MetainfoError::InfoType,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => Err(Error::MetainfoValidate {
|
_ => Err(Error::MetainfoValidate {
|
||||||
path: path.to_owned(),
|
input: input.source().clone(),
|
||||||
source: MetainfoError::Type,
|
source: MetainfoError::Type,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
@ -50,6 +48,12 @@ impl Infohash {
|
||||||
inner: Sha1Digest::from_data(info),
|
inner: Sha1Digest::from_data(info),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn load(path: &Path) -> Result<Infohash, Error> {
|
||||||
|
let input = Input::from_path(path)?;
|
||||||
|
Self::from_input(&input)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<Sha1Digest> for Infohash {
|
impl Into<Sha1Digest> for Infohash {
|
||||||
|
@ -74,12 +78,12 @@ mod tests {
|
||||||
foo: "x",
|
foo: "x",
|
||||||
};
|
};
|
||||||
|
|
||||||
let input = tempdir.path().join("foo");
|
let path = tempdir.path().join("foo");
|
||||||
|
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
Infohash::load(&input),
|
Infohash::load(&path),
|
||||||
Err(Error::MetainfoDecode{path, .. })
|
Err(Error::MetainfoDecode{input, .. })
|
||||||
if path == input
|
if input == path
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,12 +93,12 @@ mod tests {
|
||||||
foo: "i0e",
|
foo: "i0e",
|
||||||
};
|
};
|
||||||
|
|
||||||
let input = tempdir.path().join("foo");
|
let path = tempdir.path().join("foo");
|
||||||
|
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
Infohash::load(&input),
|
Infohash::load(&path),
|
||||||
Err(Error::MetainfoValidate{path, source: MetainfoError::Type})
|
Err(Error::MetainfoValidate{input, source: MetainfoError::Type})
|
||||||
if path == input
|
if input == path
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,12 +108,12 @@ mod tests {
|
||||||
foo: "de",
|
foo: "de",
|
||||||
};
|
};
|
||||||
|
|
||||||
let input = tempdir.path().join("foo");
|
let path = tempdir.path().join("foo");
|
||||||
|
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
Infohash::load(&input),
|
Infohash::load(&path),
|
||||||
Err(Error::MetainfoValidate{path, source: MetainfoError::InfoMissing})
|
Err(Error::MetainfoValidate{input, source: MetainfoError::InfoMissing})
|
||||||
if path == input
|
if input == path
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,12 +123,12 @@ mod tests {
|
||||||
foo: "d4:infoi0ee",
|
foo: "d4:infoi0ee",
|
||||||
};
|
};
|
||||||
|
|
||||||
let input = tempdir.path().join("foo");
|
let path = tempdir.path().join("foo");
|
||||||
|
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
Infohash::load(&input),
|
Infohash::load(&path),
|
||||||
Err(Error::MetainfoValidate{path, source: MetainfoError::InfoType})
|
Err(Error::MetainfoValidate{input, source: MetainfoError::InfoType})
|
||||||
if path == input
|
if input == path
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
29
src/input.rs
Normal file
29
src/input.rs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
use crate::common::*;
|
||||||
|
|
||||||
|
pub(crate) struct Input {
|
||||||
|
source: InputTarget,
|
||||||
|
data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Input {
|
||||||
|
pub(crate) fn new(source: InputTarget, data: Vec<u8>) -> Input {
|
||||||
|
Self { source, data }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn data(&self) -> &[u8] {
|
||||||
|
&self.data
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn source(&self) -> &InputTarget {
|
||||||
|
&self.source
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn from_path(path: &Path) -> Result<Input> {
|
||||||
|
let data = fs::read(path).context(error::Filesystem { path })?;
|
||||||
|
Ok(Input {
|
||||||
|
source: InputTarget::File(path.to_owned()),
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
69
src/input_target.rs
Normal file
69
src/input_target.rs
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
use crate::common::*;
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
|
pub(crate) enum InputTarget {
|
||||||
|
File(PathBuf),
|
||||||
|
Stdin,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&OsStr> for InputTarget {
|
||||||
|
fn from(text: &OsStr) -> Self {
|
||||||
|
if text == OsStr::new("-") {
|
||||||
|
Self::Stdin
|
||||||
|
} else {
|
||||||
|
Self::File(text.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for InputTarget {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Stdin => write!(f, "standard input"),
|
||||||
|
Self::File(path) => write!(f, "`{}`", path.display()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
impl<P: AsRef<Path>> PartialEq<P> for InputTarget {
|
||||||
|
fn eq(&self, other: &P) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::File(path) => path == other.as_ref(),
|
||||||
|
Self::Stdin => Path::new("-") == other.as_ref(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn file() {
|
||||||
|
assert_eq!(
|
||||||
|
InputTarget::from(OsStr::new("foo")),
|
||||||
|
InputTarget::File("foo".into())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stdio() {
|
||||||
|
assert_eq!(InputTarget::from(OsStr::new("-")), InputTarget::Stdin);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn display_file() {
|
||||||
|
let path = PathBuf::from("./path");
|
||||||
|
let have = InputTarget::File(path).to_string();
|
||||||
|
let want = "`./path`";
|
||||||
|
assert_eq!(have, want);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn display_stdio() {
|
||||||
|
let have = InputTarget::Stdin.to_string();
|
||||||
|
let want = "standard input";
|
||||||
|
assert_eq!(have, want);
|
||||||
|
}
|
||||||
|
}
|
|
@ -64,6 +64,8 @@ mod host_port;
|
||||||
mod host_port_parse_error;
|
mod host_port_parse_error;
|
||||||
mod info;
|
mod info;
|
||||||
mod infohash;
|
mod infohash;
|
||||||
|
mod input;
|
||||||
|
mod input_target;
|
||||||
mod into_u64;
|
mod into_u64;
|
||||||
mod into_usize;
|
mod into_usize;
|
||||||
mod lint;
|
mod lint;
|
||||||
|
|
|
@ -51,16 +51,14 @@ pub(crate) struct Metainfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Metainfo {
|
impl Metainfo {
|
||||||
pub(crate) fn load(path: impl AsRef<Path>) -> Result<Metainfo, Error> {
|
pub(crate) fn from_input(input: &Input) -> Result<Metainfo> {
|
||||||
let path = path.as_ref();
|
Self::deserialize(input.source(), input.data())
|
||||||
let bytes = fs::read(path).context(error::Filesystem { path })?;
|
|
||||||
Self::deserialize(path, &bytes)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn deserialize(path: impl AsRef<Path>, bytes: &[u8]) -> Result<Metainfo, Error> {
|
pub(crate) fn deserialize(source: &InputTarget, data: &[u8]) -> Result<Metainfo, Error> {
|
||||||
let path = path.as_ref();
|
let metainfo = bendy::serde::de::from_bytes(&data).context(error::MetainfoDeserialize {
|
||||||
let metainfo =
|
input: source.clone(),
|
||||||
bendy::serde::de::from_bytes(&bytes).context(error::MetainfoDeserialize { path })?;
|
})?;
|
||||||
Ok(metainfo)
|
Ok(metainfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +76,7 @@ impl Metainfo {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) fn from_bytes(bytes: &[u8]) -> Metainfo {
|
pub(crate) fn from_bytes(bytes: &[u8]) -> Metainfo {
|
||||||
Self::deserialize("<TEST>", bytes).unwrap()
|
Self::deserialize(&InputTarget::File("<TEST>".into()), bytes).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn verify(&self, base: &Path, progress_bar: Option<ProgressBar>) -> Result<Status> {
|
pub(crate) fn verify(&self, base: &Path, progress_bar: Option<ProgressBar>) -> Result<Status> {
|
||||||
|
|
|
@ -11,10 +11,11 @@ pub(crate) struct Link {
|
||||||
long = "input",
|
long = "input",
|
||||||
short = "i",
|
short = "i",
|
||||||
value_name = "METAINFO",
|
value_name = "METAINFO",
|
||||||
help = "Generate magnet link from metainfo at `PATH`.",
|
help = "Generate magnet link from metainfo at `PATH`. If `PATH` is `-`, read metainfo from \
|
||||||
|
standard input.",
|
||||||
parse(from_os_str)
|
parse(from_os_str)
|
||||||
)]
|
)]
|
||||||
input: PathBuf,
|
input: InputTarget,
|
||||||
#[structopt(
|
#[structopt(
|
||||||
long = "open",
|
long = "open",
|
||||||
short = "O",
|
short = "O",
|
||||||
|
@ -33,9 +34,10 @@ pub(crate) struct Link {
|
||||||
|
|
||||||
impl Link {
|
impl Link {
|
||||||
pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> {
|
pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> {
|
||||||
let input = env.resolve(&self.input);
|
let input = env.read(self.input.clone())?;
|
||||||
let infohash = Infohash::load(&input)?;
|
|
||||||
let metainfo = Metainfo::load(&input)?;
|
let infohash = Infohash::from_input(&input)?;
|
||||||
|
let metainfo = Metainfo::from_input(&input)?;
|
||||||
|
|
||||||
let mut link = MagnetLink::with_infohash(infohash);
|
let mut link = MagnetLink::with_infohash(infohash);
|
||||||
|
|
||||||
|
@ -233,8 +235,8 @@ mod tests {
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
env.run(), Err(Error::MetainfoValidate { path, source: MetainfoError::Type })
|
env.run(), Err(Error::MetainfoValidate { input, source: MetainfoError::Type })
|
||||||
if path == env.resolve("foo.torrent")
|
if input == "foo.torrent"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,16 +11,17 @@ pub(crate) struct Show {
|
||||||
long = "input",
|
long = "input",
|
||||||
short = "i",
|
short = "i",
|
||||||
value_name = "PATH",
|
value_name = "PATH",
|
||||||
help = "Show information about torrent at `PATH`.",
|
help = "Show information about torrent at `PATH`. If `Path` is `-`, read torrent metainfo \
|
||||||
|
from standard input.",
|
||||||
parse(from_os_str)
|
parse(from_os_str)
|
||||||
)]
|
)]
|
||||||
input: PathBuf,
|
input: InputTarget,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Show {
|
impl Show {
|
||||||
pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> {
|
pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> {
|
||||||
let input = env.resolve(&self.input);
|
let input = env.read(self.input)?;
|
||||||
let summary = TorrentSummary::load(&input)?;
|
let summary = TorrentSummary::from_input(&input)?;
|
||||||
summary.write(env)?;
|
summary.write(env)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,10 +14,11 @@ pub(crate) struct Verify {
|
||||||
long = "input",
|
long = "input",
|
||||||
short = "i",
|
short = "i",
|
||||||
value_name = "METAINFO",
|
value_name = "METAINFO",
|
||||||
help = "Verify torrent contents against torrent metainfo in `FILE`.",
|
help = "Verify torrent contents against torrent metainfo in `METAINFO`. If `METAINFO` is `-`, \
|
||||||
|
read metainfo from standard input.",
|
||||||
parse(from_os_str)
|
parse(from_os_str)
|
||||||
)]
|
)]
|
||||||
metainfo: PathBuf,
|
metainfo: InputTarget,
|
||||||
#[structopt(
|
#[structopt(
|
||||||
long = "content",
|
long = "content",
|
||||||
short = "c",
|
short = "c",
|
||||||
|
@ -31,18 +32,22 @@ pub(crate) struct Verify {
|
||||||
|
|
||||||
impl Verify {
|
impl Verify {
|
||||||
pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> {
|
pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> {
|
||||||
let metainfo_path = env.resolve(&self.metainfo);
|
|
||||||
let metainfo = Metainfo::load(&metainfo_path)?;
|
|
||||||
|
|
||||||
VerifyStep::Loading {
|
VerifyStep::Loading {
|
||||||
metainfo: &metainfo_path,
|
metainfo: &self.metainfo,
|
||||||
}
|
}
|
||||||
.print(env)?;
|
.print(env)?;
|
||||||
|
|
||||||
let base = if let Some(content) = &self.content {
|
let input = env.read(self.metainfo.clone())?;
|
||||||
env.resolve(content)
|
|
||||||
|
let metainfo = Metainfo::from_input(&input)?;
|
||||||
|
|
||||||
|
let content = if let Some(content) = &self.content {
|
||||||
|
content.clone()
|
||||||
} else {
|
} else {
|
||||||
metainfo_path.parent().unwrap().join(&metainfo.info.name)
|
match &self.metainfo {
|
||||||
|
InputTarget::File(path) => path.parent().unwrap().join(&metainfo.info.name),
|
||||||
|
InputTarget::Stdin => PathBuf::from(&metainfo.info.name),
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let progress_bar = if env.err().is_styled_term() {
|
let progress_bar = if env.err().is_styled_term() {
|
||||||
|
@ -59,9 +64,9 @@ impl Verify {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
VerifyStep::Verifying { content: &base }.print(env)?;
|
VerifyStep::Verifying { content: &content }.print(env)?;
|
||||||
|
|
||||||
let status = metainfo.verify(&base, progress_bar)?;
|
let status = metainfo.verify(&env.resolve(content), progress_bar)?;
|
||||||
|
|
||||||
status.print(env)?;
|
status.print(env)?;
|
||||||
|
|
||||||
|
@ -257,6 +262,58 @@ mod tests {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_stdin() -> Result<()> {
|
||||||
|
let mut create_env = test_env! {
|
||||||
|
args: [
|
||||||
|
"torrent",
|
||||||
|
"create",
|
||||||
|
"--input",
|
||||||
|
"foo",
|
||||||
|
"--announce",
|
||||||
|
"https://bar",
|
||||||
|
],
|
||||||
|
tree: {
|
||||||
|
foo: {
|
||||||
|
a: "abc",
|
||||||
|
d: "efg",
|
||||||
|
h: "ijk",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
create_env.run()?;
|
||||||
|
|
||||||
|
let torrent = create_env.resolve("foo.torrent");
|
||||||
|
let content = create_env.resolve("foo");
|
||||||
|
|
||||||
|
let mut verify_env = test_env! {
|
||||||
|
args: [
|
||||||
|
"torrent",
|
||||||
|
"verify",
|
||||||
|
"--input",
|
||||||
|
"-",
|
||||||
|
"--content",
|
||||||
|
&content,
|
||||||
|
],
|
||||||
|
input: fs::read(&torrent).unwrap(),
|
||||||
|
tree: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_matches!(verify_env.run(), Ok(()));
|
||||||
|
|
||||||
|
let want = format!(
|
||||||
|
"[1/2] \u{1F4BE} Loading metainfo from standard input…\n[2/2] \u{1F9EE} Verifying pieces \
|
||||||
|
from `{}`…\n\u{2728}\u{2728} Verification succeeded! \u{2728}\u{2728}\n",
|
||||||
|
content.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(verify_env.err(), want);
|
||||||
|
assert_eq!(verify_env.out(), "");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn output_multiple() -> Result<()> {
|
fn output_multiple() -> Result<()> {
|
||||||
let mut create_env = test_env! {
|
let mut create_env = test_env! {
|
||||||
|
@ -511,4 +568,55 @@ mod tests {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stdin_uses_name() -> Result<()> {
|
||||||
|
let mut create_env = test_env! {
|
||||||
|
args: [
|
||||||
|
"torrent",
|
||||||
|
"create",
|
||||||
|
"--input",
|
||||||
|
"foo",
|
||||||
|
"--announce",
|
||||||
|
"https://bar",
|
||||||
|
],
|
||||||
|
tree: {
|
||||||
|
foo: {
|
||||||
|
a: "abc",
|
||||||
|
d: "efg",
|
||||||
|
h: "ijk",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
create_env.run()?;
|
||||||
|
|
||||||
|
let torrent = create_env.resolve("foo.torrent");
|
||||||
|
|
||||||
|
let metainfo = fs::read(torrent).unwrap();
|
||||||
|
|
||||||
|
let mut verify_env = test_env! {
|
||||||
|
args: [
|
||||||
|
"torrent",
|
||||||
|
"verify",
|
||||||
|
"--input",
|
||||||
|
"-",
|
||||||
|
],
|
||||||
|
input: metainfo,
|
||||||
|
tree: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
fs::rename(create_env.resolve("foo"), verify_env.resolve("foo")).unwrap();
|
||||||
|
|
||||||
|
assert_matches!(verify_env.run(), Ok(()));
|
||||||
|
|
||||||
|
let want = format!(
|
||||||
|
"[1/2] \u{1F4BE} Loading metainfo from standard input…\n[2/2] \u{1F9EE} Verifying pieces \
|
||||||
|
from `foo`…\n\u{2728}\u{2728} Verification succeeded! \u{2728}\u{2728}\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(verify_env.err(), want);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ use crate::common::*;
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub(crate) enum VerifyStep<'a> {
|
pub(crate) enum VerifyStep<'a> {
|
||||||
Loading { metainfo: &'a Path },
|
Loading { metainfo: &'a InputTarget },
|
||||||
Verifying { content: &'a Path },
|
Verifying { content: &'a Path },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,9 +27,7 @@ impl<'a> Step for VerifyStep<'a> {
|
||||||
|
|
||||||
fn write_message(&self, write: &mut dyn Write) -> io::Result<()> {
|
fn write_message(&self, write: &mut dyn Write) -> io::Result<()> {
|
||||||
match self {
|
match self {
|
||||||
Self::Loading { metainfo } => {
|
Self::Loading { metainfo } => write!(write, "Loading metainfo from {}…", metainfo),
|
||||||
write!(write, "Loading metainfo from `{}`…", metainfo.display())
|
|
||||||
}
|
|
||||||
Self::Verifying { content } => {
|
Self::Verifying { content } => {
|
||||||
write!(write, "Verifying pieces from `{}`…", content.display())
|
write!(write, "Verifying pieces from `{}`…", content.display())
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ macro_rules! test_env {
|
||||||
{
|
{
|
||||||
args: [$($arg:expr),* $(,)?],
|
args: [$($arg:expr),* $(,)?],
|
||||||
$(cwd: $cwd:expr,)?
|
$(cwd: $cwd:expr,)?
|
||||||
|
$(input: $input:expr,)?
|
||||||
$(err_style: $err_style:expr,)?
|
$(err_style: $err_style:expr,)?
|
||||||
tree: {
|
tree: {
|
||||||
$($tree:tt)*
|
$($tree:tt)*
|
||||||
|
@ -15,6 +16,7 @@ macro_rules! test_env {
|
||||||
TestEnvBuilder::new()
|
TestEnvBuilder::new()
|
||||||
$(.current_dir(tempdir.path().join($cwd)))?
|
$(.current_dir(tempdir.path().join($cwd)))?
|
||||||
$(.err_style($err_style))?
|
$(.err_style($err_style))?
|
||||||
|
$(.input($input))?
|
||||||
.tempdir(tempdir)
|
.tempdir(tempdir)
|
||||||
.arg("imdl")
|
.arg("imdl")
|
||||||
$(.arg($arg))*
|
$(.arg($arg))*
|
||||||
|
@ -73,8 +75,9 @@ impl TestEnv {
|
||||||
fs::set_permissions(self.env.resolve(path), permissions).unwrap();
|
fs::set_permissions(self.env.resolve(path), permissions).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn load_metainfo(&self, filename: impl AsRef<Path>) -> Metainfo {
|
pub(crate) fn load_metainfo(&mut self, filename: impl AsRef<Path>) -> Metainfo {
|
||||||
Metainfo::load(self.env.resolve(filename.as_ref())).unwrap()
|
let input = self.env.read(filename.as_ref().as_os_str().into()).unwrap();
|
||||||
|
Metainfo::from_input(&input).unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,11 @@ use crate::common::*;
|
||||||
pub(crate) struct TestEnvBuilder {
|
pub(crate) struct TestEnvBuilder {
|
||||||
args: Vec<OsString>,
|
args: Vec<OsString>,
|
||||||
current_dir: Option<PathBuf>,
|
current_dir: Option<PathBuf>,
|
||||||
|
err_style: bool,
|
||||||
|
input: Option<Box<dyn Read>>,
|
||||||
out_is_term: bool,
|
out_is_term: bool,
|
||||||
tempdir: Option<TempDir>,
|
tempdir: Option<TempDir>,
|
||||||
use_color: bool,
|
use_color: bool,
|
||||||
err_style: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestEnvBuilder {
|
impl TestEnvBuilder {
|
||||||
|
@ -14,10 +15,11 @@ impl TestEnvBuilder {
|
||||||
TestEnvBuilder {
|
TestEnvBuilder {
|
||||||
args: Vec::new(),
|
args: Vec::new(),
|
||||||
current_dir: None,
|
current_dir: None,
|
||||||
|
err_style: false,
|
||||||
|
input: None,
|
||||||
out_is_term: false,
|
out_is_term: false,
|
||||||
tempdir: None,
|
tempdir: None,
|
||||||
use_color: false,
|
use_color: false,
|
||||||
err_style: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,6 +33,11 @@ impl TestEnvBuilder {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn input(mut self, input: impl AsRef<[u8]>) -> Self {
|
||||||
|
self.input = Some(Box::new(io::Cursor::new(input.as_ref().to_owned())));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn arg(mut self, arg: impl Into<OsString>) -> Self {
|
pub(crate) fn arg(mut self, arg: impl Into<OsString>) -> Self {
|
||||||
self.args.push(arg.into());
|
self.args.push(arg.into());
|
||||||
self
|
self
|
||||||
|
@ -73,7 +80,13 @@ impl TestEnvBuilder {
|
||||||
|
|
||||||
let err_stream = OutputStream::new(Box::new(err.clone()), self.err_style, false);
|
let err_stream = OutputStream::new(Box::new(err.clone()), self.err_style, false);
|
||||||
|
|
||||||
let env = Env::new(current_dir, self.args, out_stream, err_stream);
|
let env = Env::new(
|
||||||
|
current_dir,
|
||||||
|
self.args,
|
||||||
|
self.input.unwrap_or_else(|| Box::new(io::empty())),
|
||||||
|
out_stream,
|
||||||
|
err_stream,
|
||||||
|
);
|
||||||
|
|
||||||
TestEnv::new(tempdir, env, err, out)
|
TestEnv::new(tempdir, env, err, out)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,10 +22,8 @@ impl TorrentSummary {
|
||||||
Ok(Self::new(metainfo, infohash, size))
|
Ok(Self::new(metainfo, infohash, size))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn load(path: &Path) -> Result<Self> {
|
pub(crate) fn from_input(input: &Input) -> Result<Self> {
|
||||||
let bytes = fs::read(path).context(error::Filesystem { path })?;
|
let metainfo = Metainfo::from_input(input)?;
|
||||||
|
|
||||||
let metainfo = Metainfo::deserialize(path, &bytes)?;
|
|
||||||
|
|
||||||
Ok(Self::from_metainfo(metainfo)?)
|
Ok(Self::from_metainfo(metainfo)?)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user