diff --git a/src/common.rs b/src/common.rs index e30883db..fa636fc4 100644 --- a/src/common.rs +++ b/src/common.rs @@ -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 diff --git a/src/env.rs b/src/env.rs index 1789cd07..4acd1a5b 100644 --- a/src/env.rs +++ b/src/env.rs @@ -3,6 +3,7 @@ use crate::common::*; pub(crate) struct Env { args: Vec, dir: PathBuf, + input: Box, 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(dir: PathBuf, args: I, out: OutputStream, err: OutputStream) -> Self + pub(crate) fn new( + dir: PathBuf, + args: I, + input: Box, + out: OutputStream, + err: OutputStream, + ) -> Self where S: Into, I: IntoIterator, { 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) -> 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) -> PathBuf { + self.dir().join(path).clean() + } + + pub(crate) fn read(&mut self, source: InputTarget) -> Result { + 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)] diff --git a/src/error.rs b/src/error.rs index 4ff1308b..00bc8b71 100644 --- a/src/error.rs +++ b/src/error.rs @@ -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( diff --git a/src/infohash.rs b/src/infohash.rs index 3aa089d7..d4825a4e 100644 --- a/src/infohash.rs +++ b/src/infohash.rs @@ -6,11 +6,9 @@ pub(crate) struct Infohash { } impl Infohash { - pub(crate) fn load(path: &Path) -> Result { - 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 { + 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 { + let input = Input::from_path(path)?; + Self::from_input(&input) + } } impl Into 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 ); } } diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 00000000..d03b7ace --- /dev/null +++ b/src/input.rs @@ -0,0 +1,29 @@ +use crate::common::*; + +pub(crate) struct Input { + source: InputTarget, + data: Vec, +} + +impl Input { + pub(crate) fn new(source: InputTarget, data: Vec) -> 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 { + let data = fs::read(path).context(error::Filesystem { path })?; + Ok(Input { + source: InputTarget::File(path.to_owned()), + data, + }) + } +} diff --git a/src/input_target.rs b/src/input_target.rs new file mode 100644 index 00000000..75269e80 --- /dev/null +++ b/src/input_target.rs @@ -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> PartialEq

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); + } +} diff --git a/src/main.rs b/src/main.rs index fef05519..13857f4d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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; diff --git a/src/metainfo.rs b/src/metainfo.rs index 57cb45a3..cb5be9c5 100644 --- a/src/metainfo.rs +++ b/src/metainfo.rs @@ -51,16 +51,14 @@ pub(crate) struct Metainfo { } impl Metainfo { - pub(crate) fn load(path: impl AsRef) -> Result { - 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 { + Self::deserialize(input.source(), input.data()) } - pub(crate) fn deserialize(path: impl AsRef, bytes: &[u8]) -> Result { - 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 { + 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("", bytes).unwrap() + Self::deserialize(&InputTarget::File("".into()), bytes).unwrap() } pub(crate) fn verify(&self, base: &Path, progress_bar: Option) -> Result { diff --git a/src/subcommand/torrent/link.rs b/src/subcommand/torrent/link.rs index 01a7e1eb..776e14ec 100644 --- a/src/subcommand/torrent/link.rs +++ b/src/subcommand/torrent/link.rs @@ -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" ); } diff --git a/src/subcommand/torrent/show.rs b/src/subcommand/torrent/show.rs index b9102ff8..a48361de 100644 --- a/src/subcommand/torrent/show.rs +++ b/src/subcommand/torrent/show.rs @@ -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(()) } diff --git a/src/subcommand/torrent/verify.rs b/src/subcommand/torrent/verify.rs index 38697a13..a5ff2f96 100644 --- a/src/subcommand/torrent/verify.rs +++ b/src/subcommand/torrent/verify.rs @@ -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(()) + } } diff --git a/src/subcommand/torrent/verify/verify_step.rs b/src/subcommand/torrent/verify/verify_step.rs index 6f570521..4e846ae0 100644 --- a/src/subcommand/torrent/verify/verify_step.rs +++ b/src/subcommand/torrent/verify/verify_step.rs @@ -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()) } diff --git a/src/test_env.rs b/src/test_env.rs index b851772e..b3b2cc80 100644 --- a/src/test_env.rs +++ b/src/test_env.rs @@ -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) -> Metainfo { - Metainfo::load(self.env.resolve(filename.as_ref())).unwrap() + pub(crate) fn load_metainfo(&mut self, filename: impl AsRef) -> Metainfo { + let input = self.env.read(filename.as_ref().as_os_str().into()).unwrap(); + Metainfo::from_input(&input).unwrap() } } diff --git a/src/test_env_builder.rs b/src/test_env_builder.rs index f5df9b15..583fc1e9 100644 --- a/src/test_env_builder.rs +++ b/src/test_env_builder.rs @@ -3,10 +3,11 @@ use crate::common::*; pub(crate) struct TestEnvBuilder { args: Vec, current_dir: Option, + err_style: bool, + input: Option>, out_is_term: bool, tempdir: Option, 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) -> 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) } diff --git a/src/torrent_summary.rs b/src/torrent_summary.rs index 56370a84..0aa3e2e0 100644 --- a/src/torrent_summary.rs +++ b/src/torrent_summary.rs @@ -22,10 +22,8 @@ impl TorrentSummary { Ok(Self::new(metainfo, infohash, size)) } - pub(crate) fn load(path: &Path) -> Result { - let bytes = fs::read(path).context(error::Filesystem { path })?; - - let metainfo = Metainfo::deserialize(path, &bytes)?; + pub(crate) fn from_input(input: &Input) -> Result { + let metainfo = Metainfo::from_input(input)?; Ok(Self::from_metainfo(metainfo)?) }