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, 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

View File

@ -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)]

View File

@ -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(

View File

@ -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
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 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;

View File

@ -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> {

View File

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

View File

@ -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(())
} }

View File

@ -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(())
}
} }

View File

@ -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())
} }

View File

@ -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()
} }
} }

View File

@ -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)
} }

View File

@ -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)?)
} }