Skip to content

Commit

Permalink
Allow reading torrent metainfo from stdin
Browse files Browse the repository at this point in the history
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
  • Loading branch information
casey committed Apr 8, 2020
1 parent 1c84172 commit 498549b
Show file tree
Hide file tree
Showing 15 changed files with 343 additions and 86 deletions.
12 changes: 6 additions & 6 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 36 additions & 6 deletions src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::common::*;
pub(crate) struct Env {
args: Vec<OsString>,
dir: PathBuf,
input: Box<dyn Read>,
err: OutputStream,
out: OutputStream,
}
Expand All @@ -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> {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
Expand All @@ -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)]
Expand Down
14 changes: 8 additions & 6 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
Expand Down Expand Up @@ -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(
Expand Down
52 changes: 28 additions & 24 deletions src/infohash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})?;

Expand All @@ -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;
Expand All @@ -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,
}),
}
Expand All @@ -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 {
Expand All @@ -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
);
}

Expand All @@ -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
);
}

Expand All @@ -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
);
}

Expand All @@ -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 changes: 29 additions & 0 deletions src/input.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 69 additions & 0 deletions src/input_target.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 498549b

Please sign in to comment.