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:
Casey Rodarmor 2020-03-18 02:48:57 -07:00
parent 1c84172ad4
commit 498549b35c
No known key found for this signature in database
GPG Key ID: 556186B153EC6FE0
15 changed files with 343 additions and 86 deletions

View File

@ -61,12 +61,12 @@ pub(crate) use crate::{
arguments::Arguments, bytes::Bytes, env::Env, error::Error, file_error::FileError,
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,
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, status::Status, style::Style,
subcommand::Subcommand, table::Table, torrent_summary::TorrentSummary, use_color::UseColor,
verifier::Verifier, walker::Walker,
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, status::Status, style::Style, subcommand::Subcommand, table::Table,
torrent_summary::TorrentSummary, use_color::UseColor, verifier::Verifier, walker::Walker,
};
// type aliases

View File

@ -3,6 +3,7 @@ use crate::common::*;
pub(crate) struct Env {
args: Vec<OsString>,
dir: PathBuf,
input: Box<dyn Read>,
err: OutputStream,
out: OutputStream,
}
@ -20,7 +21,13 @@ impl Env {
let out_stream = OutputStream::stdout(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> {
@ -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
S: Into<OsString>,
I: IntoIterator<Item = S>,
{
Self {
args: args.into_iter().map(Into::into).collect(),
input,
dir,
out,
err,
@ -129,10 +143,6 @@ impl Env {
&self.dir
}
pub(crate) fn resolve(&self, path: impl AsRef<Path>) -> PathBuf {
self.dir().join(path).clean()
}
pub(crate) fn err(&self) -> &OutputStream {
&self.err
}
@ -148,6 +158,26 @@ impl Env {
pub(crate) fn out_mut(&mut self) -> &mut OutputStream {
&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)]

View File

@ -7,21 +7,21 @@ use structopt::clap;
pub(crate) enum Error {
#[snafu(display("Failed to parse announce URL: {}", source))]
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 {
source: bendy::serde::Error,
path: PathBuf,
input: InputTarget,
},
#[snafu(display("Failed to serialize torrent metainfo: {}", source))]
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 {
path: PathBuf,
input: InputTarget,
error: bendy::decoding::Error,
},
#[snafu(display("Metainfo from `{}` failed to validate: {}", path.display(), source))]
#[snafu(display("Metainfo from {} failed to validate: {}", input, source))]
MetainfoValidate {
path: PathBuf,
input: InputTarget,
source: MetainfoError,
},
#[snafu(display("Failed to parse byte count `{}`: {}", text, source))]
@ -107,6 +107,8 @@ pub(crate) enum Error {
PrivateTrackerless,
#[snafu(display("Failed to write to standard error: {}", source))]
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))]
Stdout { source: io::Error },
#[snafu(display(

View File

@ -6,11 +6,9 @@ pub(crate) struct Infohash {
}
impl Infohash {
pub(crate) fn load(path: &Path) -> Result<Infohash, Error> {
let bytes = fs::read(path).context(error::Filesystem { path })?;
let value = Value::from_bencode(&bytes).map_err(|error| Error::MetainfoDecode {
path: path.to_owned(),
pub(crate) fn from_input(input: &Input) -> Result<Infohash, Error> {
let value = Value::from_bencode(input.data()).map_err(|error| Error::MetainfoDecode {
input: input.source().clone(),
error,
})?;
@ -20,7 +18,7 @@ impl Infohash {
.iter()
.find(|pair: &(&Cow<[u8]>, &Value)| pair.0.as_ref() == b"info")
.ok_or_else(|| Error::MetainfoValidate {
path: path.to_owned(),
input: input.source().clone(),
source: MetainfoError::InfoMissing,
})?
.1;
@ -33,13 +31,13 @@ impl Infohash {
Ok(Self::from_bencoded_info_dict(&encoded))
} else {
Err(Error::MetainfoValidate {
path: path.to_owned(),
input: input.source().clone(),
source: MetainfoError::InfoType,
})
}
}
_ => Err(Error::MetainfoValidate {
path: path.to_owned(),
input: input.source().clone(),
source: MetainfoError::Type,
}),
}
@ -50,6 +48,12 @@ impl Infohash {
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 {
@ -74,12 +78,12 @@ mod tests {
foo: "x",
};
let input = tempdir.path().join("foo");
let path = tempdir.path().join("foo");
assert_matches!(
Infohash::load(&input),
Err(Error::MetainfoDecode{path, .. })
if path == input
Infohash::load(&path),
Err(Error::MetainfoDecode{input, .. })
if input == path
);
}
@ -89,12 +93,12 @@ mod tests {
foo: "i0e",
};
let input = tempdir.path().join("foo");
let path = tempdir.path().join("foo");
assert_matches!(
Infohash::load(&input),
Err(Error::MetainfoValidate{path, source: MetainfoError::Type})
if path == input
Infohash::load(&path),
Err(Error::MetainfoValidate{input, source: MetainfoError::Type})
if input == path
);
}
@ -104,12 +108,12 @@ mod tests {
foo: "de",
};
let input = tempdir.path().join("foo");
let path = tempdir.path().join("foo");
assert_matches!(
Infohash::load(&input),
Err(Error::MetainfoValidate{path, source: MetainfoError::InfoMissing})
if path == input
Infohash::load(&path),
Err(Error::MetainfoValidate{input, source: MetainfoError::InfoMissing})
if input == path
);
}
@ -119,12 +123,12 @@ mod tests {
foo: "d4:infoi0ee",
};
let input = tempdir.path().join("foo");
let path = tempdir.path().join("foo");
assert_matches!(
Infohash::load(&input),
Err(Error::MetainfoValidate{path, source: MetainfoError::InfoType})
if path == input
Infohash::load(&path),
Err(Error::MetainfoValidate{input, source: MetainfoError::InfoType})
if input == path
);
}
}

29
src/input.rs Normal file
View 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
View 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);
}
}

View File

@ -64,6 +64,8 @@ mod host_port;
mod host_port_parse_error;
mod info;
mod infohash;
mod input;
mod input_target;
mod into_u64;
mod into_usize;
mod lint;

View File

@ -51,16 +51,14 @@ pub(crate) struct Metainfo {
}
impl Metainfo {
pub(crate) fn load(path: impl AsRef<Path>) -> Result<Metainfo, Error> {
let path = path.as_ref();
let bytes = fs::read(path).context(error::Filesystem { path })?;
Self::deserialize(path, &bytes)
pub(crate) fn from_input(input: &Input) -> Result<Metainfo> {
Self::deserialize(input.source(), input.data())
}
pub(crate) fn deserialize(path: impl AsRef<Path>, bytes: &[u8]) -> Result<Metainfo, Error> {
let path = path.as_ref();
let metainfo =
bendy::serde::de::from_bytes(&bytes).context(error::MetainfoDeserialize { path })?;
pub(crate) fn deserialize(source: &InputTarget, data: &[u8]) -> Result<Metainfo, Error> {
let metainfo = bendy::serde::de::from_bytes(&data).context(error::MetainfoDeserialize {
input: source.clone(),
})?;
Ok(metainfo)
}
@ -78,7 +76,7 @@ impl Metainfo {
#[cfg(test)]
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> {

View File

@ -11,10 +11,11 @@ pub(crate) struct Link {
long = "input",
short = "i",
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)
)]
input: PathBuf,
input: InputTarget,
#[structopt(
long = "open",
short = "O",
@ -33,9 +34,10 @@ pub(crate) struct Link {
impl Link {
pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> {
let input = env.resolve(&self.input);
let infohash = Infohash::load(&input)?;
let metainfo = Metainfo::load(&input)?;
let input = env.read(self.input.clone())?;
let infohash = Infohash::from_input(&input)?;
let metainfo = Metainfo::from_input(&input)?;
let mut link = MagnetLink::with_infohash(infohash);
@ -233,8 +235,8 @@ mod tests {
};
assert_matches!(
env.run(), Err(Error::MetainfoValidate { path, source: MetainfoError::Type })
if path == env.resolve("foo.torrent")
env.run(), Err(Error::MetainfoValidate { input, source: MetainfoError::Type })
if input == "foo.torrent"
);
}

View File

@ -11,16 +11,17 @@ pub(crate) struct Show {
long = "input",
short = "i",
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)
)]
input: PathBuf,
input: InputTarget,
}
impl Show {
pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> {
let input = env.resolve(&self.input);
let summary = TorrentSummary::load(&input)?;
let input = env.read(self.input)?;
let summary = TorrentSummary::from_input(&input)?;
summary.write(env)?;
Ok(())
}

View File

@ -14,10 +14,11 @@ pub(crate) struct Verify {
long = "input",
short = "i",
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)
)]
metainfo: PathBuf,
metainfo: InputTarget,
#[structopt(
long = "content",
short = "c",
@ -31,18 +32,22 @@ pub(crate) struct Verify {
impl Verify {
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 {
metainfo: &metainfo_path,
metainfo: &self.metainfo,
}
.print(env)?;
let base = if let Some(content) = &self.content {
env.resolve(content)
let input = env.read(self.metainfo.clone())?;
let metainfo = Metainfo::from_input(&input)?;
let content = if let Some(content) = &self.content {
content.clone()
} 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() {
@ -59,9 +64,9 @@ impl Verify {
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)?;
@ -257,6 +262,58 @@ mod tests {
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]
fn output_multiple() -> Result<()> {
let mut create_env = test_env! {
@ -511,4 +568,55 @@ mod tests {
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(())
}
}

View File

@ -2,7 +2,7 @@ use crate::common::*;
#[derive(Clone, Copy)]
pub(crate) enum VerifyStep<'a> {
Loading { metainfo: &'a Path },
Loading { metainfo: &'a InputTarget },
Verifying { content: &'a Path },
}
@ -27,9 +27,7 @@ impl<'a> Step for VerifyStep<'a> {
fn write_message(&self, write: &mut dyn Write) -> io::Result<()> {
match self {
Self::Loading { metainfo } => {
write!(write, "Loading metainfo from `{}`…", metainfo.display())
}
Self::Loading { metainfo } => write!(write, "Loading metainfo from {}…", metainfo),
Self::Verifying { content } => {
write!(write, "Verifying pieces from `{}`…", content.display())
}

View File

@ -4,6 +4,7 @@ macro_rules! test_env {
{
args: [$($arg:expr),* $(,)?],
$(cwd: $cwd:expr,)?
$(input: $input:expr,)?
$(err_style: $err_style:expr,)?
tree: {
$($tree:tt)*
@ -15,6 +16,7 @@ macro_rules! test_env {
TestEnvBuilder::new()
$(.current_dir(tempdir.path().join($cwd)))?
$(.err_style($err_style))?
$(.input($input))?
.tempdir(tempdir)
.arg("imdl")
$(.arg($arg))*
@ -73,8 +75,9 @@ impl TestEnv {
fs::set_permissions(self.env.resolve(path), permissions).unwrap();
}
pub(crate) fn load_metainfo(&self, filename: impl AsRef<Path>) -> Metainfo {
Metainfo::load(self.env.resolve(filename.as_ref())).unwrap()
pub(crate) fn load_metainfo(&mut self, filename: impl AsRef<Path>) -> Metainfo {
let input = self.env.read(filename.as_ref().as_os_str().into()).unwrap();
Metainfo::from_input(&input).unwrap()
}
}

View File

@ -3,10 +3,11 @@ use crate::common::*;
pub(crate) struct TestEnvBuilder {
args: Vec<OsString>,
current_dir: Option<PathBuf>,
err_style: bool,
input: Option<Box<dyn Read>>,
out_is_term: bool,
tempdir: Option<TempDir>,
use_color: bool,
err_style: bool,
}
impl TestEnvBuilder {
@ -14,10 +15,11 @@ impl TestEnvBuilder {
TestEnvBuilder {
args: Vec::new(),
current_dir: None,
err_style: false,
input: None,
out_is_term: false,
tempdir: None,
use_color: false,
err_style: false,
}
}
@ -31,6 +33,11 @@ impl TestEnvBuilder {
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 {
self.args.push(arg.into());
self
@ -73,7 +80,13 @@ impl TestEnvBuilder {
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)
}

View File

@ -22,10 +22,8 @@ impl TorrentSummary {
Ok(Self::new(metainfo, infohash, size))
}
pub(crate) fn load(path: &Path) -> Result<Self> {
let bytes = fs::read(path).context(error::Filesystem { path })?;
let metainfo = Metainfo::deserialize(path, &bytes)?;
pub(crate) fn from_input(input: &Input) -> Result<Self> {
let metainfo = Metainfo::from_input(input)?;
Ok(Self::from_metainfo(metainfo)?)
}