Forbid empty input, output, and path targets

When an empty path is passed to `Env::resolve`, the result is the
current working directory. This is bad, so forbid the user to pass in
empty paths.

type: fixed
This commit is contained in:
Casey Rodarmor 2020-04-05 18:17:38 -07:00
parent c23b0635ee
commit 1cfc021453
No known key found for this signature in database
GPG Key ID: 556186B153EC6FE0
13 changed files with 198 additions and 128 deletions

View File

@ -10,7 +10,6 @@ merge_imports = true
newline_style = "Unix" newline_style = "Unix"
normalize_comments = true normalize_comments = true
reorder_impl_items = true reorder_impl_items = true
required_version = "1.4.12"
tab_spaces = 2 tab_spaces = 2
unstable_features = true unstable_features = true
use_field_init_shorthand = true use_field_init_shorthand = true

View File

@ -4,7 +4,7 @@ pub(crate) use std::{
char, char,
cmp::{Ordering, Reverse}, cmp::{Ordering, Reverse},
collections::{BTreeMap, BTreeSet, HashMap, HashSet}, collections::{BTreeMap, BTreeSet, HashMap, HashSet},
convert::TryInto, convert::{TryFrom, TryInto},
env, env,
ffi::{OsStr, OsString}, ffi::{OsStr, OsString},
fmt::{self, Display, Formatter}, fmt::{self, Display, Formatter},

View File

@ -163,14 +163,20 @@ impl Env {
&mut self.out &mut self.out
} }
pub(crate) fn resolve(&self, path: impl AsRef<Path>) -> PathBuf { pub(crate) fn resolve(&self, path: impl AsRef<Path>) -> Result<PathBuf> {
self.dir().join(path).clean() let path = path.as_ref();
if path.components().count() == 0 {
return Err(Error::internal("Empty path passed to resolve"));
}
Ok(self.dir().join(path).clean())
} }
pub(crate) fn read(&mut self, source: InputTarget) -> Result<Input> { pub(crate) fn read(&mut self, source: InputTarget) -> Result<Input> {
let data = match &source { let data = match &source {
InputTarget::Path(path) => { InputTarget::Path(path) => {
let absolute = self.resolve(path); let absolute = self.resolve(path)?;
fs::read(absolute).context(error::Filesystem { path })? fs::read(absolute).context(error::Filesystem { path })?
} }
InputTarget::Stdin => { InputTarget::Stdin => {

View File

@ -30,6 +30,8 @@ pub(crate) enum Error {
GlobParse { source: globset::Error }, GlobParse { source: globset::Error },
#[snafu(display("Failed to serialize torrent info dictionary: {}", source))] #[snafu(display("Failed to serialize torrent info dictionary: {}", source))]
InfoSerialize { source: bendy::serde::Error }, InfoSerialize { source: bendy::serde::Error },
#[snafu(display("Input target empty"))]
InputTargetEmpty,
#[snafu(display( #[snafu(display(
"Interal error, this may indicate a bug in intermodal: {}\n\ "Interal error, this may indicate a bug in intermodal: {}\n\
Consider filing an issue: https://github.com/casey/imdl/issues/new", Consider filing an issue: https://github.com/casey/imdl/issues/new",
@ -61,6 +63,8 @@ pub(crate) enum Error {
OpenerExitStatus { exit_status: ExitStatus }, OpenerExitStatus { exit_status: ExitStatus },
#[snafu(display("Output path already exists: `{}`", path.display()))] #[snafu(display("Output path already exists: `{}`", path.display()))]
OutputExists { path: PathBuf }, OutputExists { path: PathBuf },
#[snafu(display("Output target empty"))]
OutputTargetEmpty,
#[snafu(display( #[snafu(display(
"Path `{}` contains non-normal component: {}", "Path `{}` contains non-normal component: {}",
path.display(), path.display(),

View File

@ -7,20 +7,32 @@ pub(crate) enum InputTarget {
} }
impl InputTarget { impl InputTarget {
pub(crate) fn resolve(&self, env: &Env) -> Self { pub(crate) fn resolve(&self, env: &Env) -> Result<Self> {
match self { match self {
Self::Path(path) => Self::Path(env.resolve(path)), Self::Path(path) => Ok(Self::Path(env.resolve(path)?)),
Self::Stdin => Self::Stdin, Self::Stdin => Ok(Self::Stdin),
} }
} }
pub(crate) fn try_from_os_str(text: &OsStr) -> Result<Self, OsString> {
text
.try_into()
.map_err(|err: Error| OsString::from(err.to_string()))
}
} }
impl From<&OsStr> for InputTarget { impl TryFrom<&OsStr> for InputTarget {
fn from(text: &OsStr) -> Self { type Error = Error;
fn try_from(text: &OsStr) -> Result<Self, Self::Error> {
if text.is_empty() {
return Err(Error::InputTargetEmpty);
};
if text == OsStr::new("-") { if text == OsStr::new("-") {
Self::Stdin Ok(Self::Stdin)
} else { } else {
Self::Path(text.into()) Ok(Self::Path(text.into()))
} }
} }
} }
@ -50,14 +62,17 @@ mod tests {
#[test] #[test]
fn file() { fn file() {
assert_eq!( assert_eq!(
InputTarget::from(OsStr::new("foo")), InputTarget::try_from(OsStr::new("foo")).unwrap(),
InputTarget::Path("foo".into()) InputTarget::Path("foo".into()),
); );
} }
#[test] #[test]
fn stdio() { fn stdio() {
assert_eq!(InputTarget::from(OsStr::new("-")), InputTarget::Stdin); assert_eq!(
InputTarget::try_from(OsStr::new("-")).unwrap(),
InputTarget::Stdin
);
} }
#[test] #[test]

View File

@ -2,25 +2,37 @@ use crate::common::*;
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
pub(crate) enum OutputTarget { pub(crate) enum OutputTarget {
File(PathBuf), Path(PathBuf),
Stdout, Stdout,
} }
impl OutputTarget { impl OutputTarget {
pub(crate) fn resolve(&self, env: &Env) -> Self { pub(crate) fn resolve(&self, env: &Env) -> Result<Self> {
match self { match self {
Self::File(path) => Self::File(env.resolve(path)), Self::Path(path) => Ok(Self::Path(env.resolve(path)?)),
Self::Stdout => Self::Stdout, Self::Stdout => Ok(Self::Stdout),
} }
} }
pub(crate) fn try_from_os_str(text: &OsStr) -> Result<Self, OsString> {
text
.try_into()
.map_err(|err: Error| OsString::from(err.to_string()))
}
} }
impl From<&OsStr> for OutputTarget { impl TryFrom<&OsStr> for OutputTarget {
fn from(text: &OsStr) -> Self { type Error = Error;
fn try_from(text: &OsStr) -> Result<Self, Self::Error> {
if text.is_empty() {
return Err(Error::OutputTargetEmpty);
};
if text == OsStr::new("-") { if text == OsStr::new("-") {
Self::Stdout Ok(Self::Stdout)
} else { } else {
Self::File(text.into()) Ok(Self::Path(text.into()))
} }
} }
} }
@ -29,7 +41,7 @@ impl Display for OutputTarget {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self { match self {
Self::Stdout => write!(f, "standard output"), Self::Stdout => write!(f, "standard output"),
Self::File(path) => write!(f, "`{}`", path.display()), Self::Path(path) => write!(f, "`{}`", path.display()),
} }
} }
} }
@ -41,20 +53,23 @@ mod tests {
#[test] #[test]
fn file() { fn file() {
assert_eq!( assert_eq!(
OutputTarget::from(OsStr::new("foo")), OutputTarget::try_from(OsStr::new("foo")).unwrap(),
OutputTarget::File("foo".into()) OutputTarget::Path("foo".into())
); );
} }
#[test] #[test]
fn stdio() { fn stdio() {
assert_eq!(OutputTarget::from(OsStr::new("-")), OutputTarget::Stdout); assert_eq!(
OutputTarget::try_from(OsStr::new("-")).unwrap(),
OutputTarget::Stdout
);
} }
#[test] #[test]
fn display_file() { fn display_file() {
let path = PathBuf::from("./path"); let path = PathBuf::from("./path");
let have = OutputTarget::File(path).to_string(); let have = OutputTarget::Path(path).to_string();
let want = "`./path`"; let want = "`./path`";
assert_eq!(have, want); assert_eq!(have, want);
} }

View File

@ -116,11 +116,12 @@ Examples:
long = "input", long = "input",
short = "i", short = "i",
value_name = "PATH", value_name = "PATH",
empty_values(false),
parse(try_from_os_str = InputTarget::try_from_os_str),
help = "Read torrent contents from `PATH`. If `PATH` is a file, torrent will be a single-file \ help = "Read torrent contents from `PATH`. If `PATH` is a file, torrent will be a single-file \
torrent. If `PATH` is a directory, torrent will be a multi-file torrent. If `PATH` \ torrent. If `PATH` is a directory, torrent will be a multi-file torrent. If `PATH` \
is `-`, read from standard input. Piece length defaults to 256KiB when reading from \ is `-`, read from standard input. Piece length defaults to 256KiB when reading from \
standard input if `--piece-length` is not given.", standard input if `--piece-length` is not given.",
parse(from_os_str)
)] )]
input: InputTarget, input: InputTarget,
#[structopt( #[structopt(
@ -187,11 +188,12 @@ Sort in ascending order by size, break ties in descending path order:
long = "output", long = "output",
short = "o", short = "o",
value_name = "TARGET", value_name = "TARGET",
empty_values(false),
parse(try_from_os_str = OutputTarget::try_from_os_str),
required_if("input", "-"),
help = "Save `.torrent` file to `TARGET`, or print to standard output if `TARGET` is `-`. \ help = "Save `.torrent` file to `TARGET`, or print to standard output if `TARGET` is `-`. \
Defaults to the argument to `--input` with an `.torrent` extension appended. Required \ Defaults to the argument to `--input` with an `.torrent` extension appended. Required \
when `--input -`.", when `--input -`.",
required_if("input", "-"),
parse(from_os_str)
)] )]
output: Option<OutputTarget>, output: Option<OutputTarget>,
#[structopt( #[structopt(
@ -240,8 +242,11 @@ Sort in ascending order by size, break ties in descending path order:
impl Create { impl Create {
pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> { pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> {
let input = self.input.resolve(env); let input = self.input.resolve(env)?;
let output = self.output.map(|output| output.resolve(env)); let output = match self.output {
Some(output) => Some(output.resolve(env)?),
None => None,
};
let mut linter = Linter::new(); let mut linter = Linter::new();
linter.allow(self.allowed_lints.iter().cloned()); linter.allow(self.allowed_lints.iter().cloned());
@ -320,15 +325,12 @@ impl Create {
files = Some(files_inner); files = Some(files_inner);
output output.unwrap_or_else(|| {
.as_ref() let mut torrent_name = name.to_owned();
.map(|output| output.resolve(env)) torrent_name.push_str(".torrent");
.unwrap_or_else(|| {
let mut torrent_name = name.to_owned();
torrent_name.push_str(".torrent");
OutputTarget::File(path.parent().unwrap().join(torrent_name)) OutputTarget::Path(path.parent().unwrap().join(torrent_name))
}) })
} }
InputTarget::Stdin => { InputTarget::Stdin => {
@ -363,7 +365,7 @@ impl Create {
return Err(Error::PieceLengthSmall); return Err(Error::PieceLengthSmall);
} }
if let OutputTarget::File(path) = &output { if let OutputTarget::Path(path) = &output {
if !self.force && path.exists() { if !self.force && path.exists() {
return Err(Error::OutputExists { return Err(Error::OutputExists {
path: path.to_owned(), path: path.to_owned(),
@ -441,7 +443,7 @@ impl Create {
if !self.dry_run { if !self.dry_run {
match &output { match &output {
OutputTarget::File(path) => { OutputTarget::Path(path) => {
let mut open_options = fs::OpenOptions::new(); let mut open_options = fs::OpenOptions::new();
if self.force { if self.force {
@ -490,7 +492,7 @@ impl Create {
outln!(env, "{}", link)?; outln!(env, "{}", link)?;
} }
if let OutputTarget::File(path) = output { if let OutputTarget::Path(path) = output {
if self.open { if self.open {
Platform::open_file(&path)?; Platform::open_file(&path)?;
} }
@ -546,7 +548,7 @@ mod tests {
} }
#[test] #[test]
fn torrent_file_is_bencode_dict() { fn torrent_file_is_bencode_dict() -> Result<()> {
let mut env = test_env! { let mut env = test_env! {
args: [ args: [
"torrent", "torrent",
@ -561,10 +563,11 @@ mod tests {
} }
}; };
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let torrent = env.resolve("foo.torrent")?;
let bytes = fs::read(torrent).unwrap(); let bytes = fs::read(torrent).unwrap();
let value = Value::from_bencode(&bytes).unwrap(); let value = Value::from_bencode(&bytes).unwrap();
assert!(matches!(value, Value::Dict(_))); assert!(matches!(value, Value::Dict(_)));
Ok(())
} }
#[test] #[test]
@ -1531,7 +1534,7 @@ mod tests {
} }
#[test] #[test]
fn output() { fn output() -> Result<()> {
let mut env = TestEnvBuilder::new() let mut env = TestEnvBuilder::new()
.arg_slice(&[ .arg_slice(&[
"imdl", "imdl",
@ -1546,17 +1549,18 @@ mod tests {
.out_is_term() .out_is_term()
.build(); .build();
let dir = env.resolve("foo"); let dir = env.resolve("foo")?;
fs::create_dir(&dir).unwrap(); fs::create_dir(&dir).unwrap();
fs::write(dir.join("a"), "abc").unwrap(); fs::write(dir.join("a"), "abc").unwrap();
fs::write(dir.join("x"), "xyz").unwrap(); fs::write(dir.join("x"), "xyz").unwrap();
fs::write(dir.join("h"), "hij").unwrap(); fs::write(dir.join("h"), "hij").unwrap();
env.run().unwrap(); env.run().unwrap();
assert_eq!(env.out(), ""); assert_eq!(env.out(), "");
Ok(())
} }
#[test] #[test]
fn show() { fn show() -> Result<()> {
let mut env = TestEnvBuilder::new() let mut env = TestEnvBuilder::new()
.arg_slice(&[ .arg_slice(&[
"imdl", "imdl",
@ -1572,7 +1576,7 @@ mod tests {
.out_is_term() .out_is_term()
.build(); .build();
let dir = env.resolve("foo"); let dir = env.resolve("foo")?;
fs::create_dir(&dir).unwrap(); fs::create_dir(&dir).unwrap();
fs::write(dir.join("a"), "abc").unwrap(); fs::write(dir.join("a"), "abc").unwrap();
fs::write(dir.join("x"), "xyz").unwrap(); fs::write(dir.join("x"), "xyz").unwrap();
@ -1600,6 +1604,7 @@ Content Size 9 bytes
212 + consts::CREATED_BY_DEFAULT.len() 212 + consts::CREATED_BY_DEFAULT.len()
); );
assert_eq!(have, want); assert_eq!(have, want);
Ok(())
} }
#[test] #[test]
@ -1625,7 +1630,7 @@ Content Size 9 bytes
} }
#[test] #[test]
fn force_default() { fn force_default() -> Result<()> {
let mut env = test_env! { let mut env = test_env! {
args: [ args: [
"torrent", "torrent",
@ -1643,8 +1648,9 @@ Content Size 9 bytes
assert_matches!( assert_matches!(
env.run().unwrap_err(), env.run().unwrap_err(),
Error::OutputExists {path} Error::OutputExists {path}
if path == env.resolve("foo.torrent") if path == env.resolve("foo.torrent")?
) );
Ok(())
} }
#[test] #[test]
@ -1724,7 +1730,7 @@ Content Size 9 bytes
} }
#[test] #[test]
fn skip_hidden() { fn skip_hidden() -> Result<()> {
let mut env = test_env! { let mut env = test_env! {
args: [ args: [
"torrent", "torrent",
@ -1745,17 +1751,17 @@ Content Size 9 bytes
if cfg!(target_os = "windows") { if cfg!(target_os = "windows") {
Command::new("attrib") Command::new("attrib")
.arg("+h") .arg("+h")
.arg(env.resolve("foo/hidden")) .arg(env.resolve("foo/hidden")?)
.status() .status()
.unwrap(); .unwrap();
} else if cfg!(target_os = "macos") { } else if cfg!(target_os = "macos") {
Command::new("chflags") Command::new("chflags")
.arg("hidden") .arg("hidden")
.arg(env.resolve("foo/hidden")) .arg(env.resolve("foo/hidden")?)
.status() .status()
.unwrap(); .unwrap();
} else { } else {
fs::remove_file(env.resolve("foo/hidden")).unwrap(); fs::remove_file(env.resolve("foo/hidden")?).unwrap();
} }
env.run().unwrap(); env.run().unwrap();
@ -1767,10 +1773,11 @@ Content Size 9 bytes
Mode::Multiple { files } if files.len() == 0 Mode::Multiple { files } if files.len() == 0
); );
assert_eq!(metainfo.info.pieces, PieceList::new()); assert_eq!(metainfo.info.pieces, PieceList::new());
Ok(())
} }
#[test] #[test]
fn include_hidden() { fn include_hidden() -> Result<()> {
let mut env = test_env! { let mut env = test_env! {
args: [ args: [
"torrent", "torrent",
@ -1792,13 +1799,13 @@ Content Size 9 bytes
if cfg!(target_os = "windows") { if cfg!(target_os = "windows") {
Command::new("attrib") Command::new("attrib")
.arg("+h") .arg("+h")
.arg(env.resolve("foo/hidden")) .arg(env.resolve("foo/hidden")?)
.status() .status()
.unwrap(); .unwrap();
} else if cfg!(target_os = "macos") { } else if cfg!(target_os = "macos") {
Command::new("chflags") Command::new("chflags")
.arg("hidden") .arg("hidden")
.arg(env.resolve("foo/hidden")) .arg(env.resolve("foo/hidden")?)
.status() .status()
.unwrap(); .unwrap();
} }
@ -1810,12 +1817,13 @@ Content Size 9 bytes
Mode::Multiple { files } if files.len() == 2 Mode::Multiple { files } if files.len() == 2
); );
assert_eq!(metainfo.info.pieces, PieceList::from_pieces(&["abcabc"])); assert_eq!(metainfo.info.pieces, PieceList::from_pieces(&["abcabc"]));
Ok(())
} }
fn populate_symlinks(env: &Env) { fn populate_symlinks(env: &Env) -> Result<()> {
let dir = env.resolve("foo"); let dir = env.resolve("foo")?;
let file_src = env.resolve("bar"); let file_src = env.resolve("bar")?;
let dir_src = env.resolve("dir-src"); let dir_src = env.resolve("dir-src")?;
let dir_contents = dir_src.join("baz"); let dir_contents = dir_src.join("baz");
fs::create_dir(&dir_src).unwrap(); fs::create_dir(&dir_src).unwrap();
fs::write(dir_contents, "baz").unwrap(); fs::write(dir_contents, "baz").unwrap();
@ -1824,8 +1832,8 @@ Content Size 9 bytes
fs::write(file_src, "bar").unwrap(); fs::write(file_src, "bar").unwrap();
#[cfg(unix)] #[cfg(unix)]
{ {
let file_link = env.resolve("foo/bar"); let file_link = env.resolve("foo/bar")?;
let dir_link = env.resolve("foo/dir"); let dir_link = env.resolve("foo/dir")?;
Command::new("ln") Command::new("ln")
.arg("-s") .arg("-s")
.arg("../bar") .arg("../bar")
@ -1840,10 +1848,12 @@ Content Size 9 bytes
.status() .status()
.unwrap(); .unwrap();
} }
Ok(())
} }
#[test] #[test]
fn skip_symlinks() { fn skip_symlinks() -> Result<()> {
let mut env = test_env! { let mut env = test_env! {
args: [ args: [
"torrent", "torrent",
@ -1856,7 +1866,7 @@ Content Size 9 bytes
], ],
tree: {}, tree: {},
}; };
populate_symlinks(&env); populate_symlinks(&env)?;
env.run().unwrap(); env.run().unwrap();
let metainfo = env.load_metainfo("foo.torrent"); let metainfo = env.load_metainfo("foo.torrent");
assert_matches!( assert_matches!(
@ -1864,11 +1874,12 @@ Content Size 9 bytes
Mode::Multiple { files } if files.is_empty() Mode::Multiple { files } if files.is_empty()
); );
assert_eq!(metainfo.info.pieces, PieceList::new()); assert_eq!(metainfo.info.pieces, PieceList::new());
Ok(())
} }
#[test] #[test]
#[cfg(unix)] #[cfg(unix)]
fn follow_symlinks() { fn follow_symlinks() -> Result<()> {
let mut env = test_env! { let mut env = test_env! {
args: [ args: [
"torrent", "torrent",
@ -1882,7 +1893,7 @@ Content Size 9 bytes
], ],
tree: {}, tree: {},
}; };
populate_symlinks(&env); populate_symlinks(&env)?;
env.run().unwrap(); env.run().unwrap();
let metainfo = env.load_metainfo("foo.torrent"); let metainfo = env.load_metainfo("foo.torrent");
let mut pieces = PieceList::new(); let mut pieces = PieceList::new();
@ -1908,11 +1919,12 @@ Content Size 9 bytes
} }
_ => panic!("Expected multi-file torrent"), _ => panic!("Expected multi-file torrent"),
} }
Ok(())
} }
#[test] #[test]
#[cfg(unix)] #[cfg(unix)]
fn symlink_root() { fn symlink_root() -> Result<()> {
let mut env = test_env! { let mut env = test_env! {
args: [ args: [
"torrent", "torrent",
@ -1926,8 +1938,8 @@ Content Size 9 bytes
tree: {}, tree: {},
}; };
let file_src = env.resolve("bar"); let file_src = env.resolve("bar")?;
let file_link = env.resolve("foo"); let file_link = env.resolve("foo")?;
Command::new("ln") Command::new("ln")
.arg("-s") .arg("-s")
@ -1937,6 +1949,7 @@ Content Size 9 bytes
.unwrap(); .unwrap();
assert_matches!(env.run().unwrap_err(), Error::SymlinkRoot { root } if root == file_link); assert_matches!(env.run().unwrap_err(), Error::SymlinkRoot { root } if root == file_link);
Ok(())
} }
#[test] #[test]
@ -1967,9 +1980,8 @@ Content Size 9 bytes
); );
assert_eq!(metainfo.info.pieces, PieceList::new()); assert_eq!(metainfo.info.pieces, PieceList::new());
} }
#[test] #[test]
fn skip_hidden_attribute_dir_contents() { fn skip_hidden_attribute_dir_contents() -> Result<()> {
let mut env = test_env! { let mut env = test_env! {
args: [ args: [
"torrent", "torrent",
@ -1990,7 +2002,7 @@ Content Size 9 bytes
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
env.write("foo/bar/baz", "baz"); env.write("foo/bar/baz", "baz");
let path = env.resolve("foo/bar"); let path = env.resolve("foo/bar")?;
Command::new("attrib") Command::new("attrib")
.arg("+h") .arg("+h")
.arg(&path) .arg(&path)
@ -2001,7 +2013,7 @@ Content Size 9 bytes
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
env.write("foo/bar/baz", "baz"); env.write("foo/bar/baz", "baz");
let path = env.resolve("foo/bar"); let path = env.resolve("foo/bar")?;
Command::new("chflags") Command::new("chflags")
.arg("hidden") .arg("hidden")
.arg(&path) .arg(&path)
@ -2016,6 +2028,7 @@ Content Size 9 bytes
Mode::Multiple { files } if files.is_empty() Mode::Multiple { files } if files.is_empty()
); );
assert_eq!(metainfo.info.pieces, PieceList::new()); assert_eq!(metainfo.info.pieces, PieceList::new());
Ok(())
} }
#[test] #[test]
@ -2255,7 +2268,7 @@ Content Size 9 bytes
} }
#[test] #[test]
fn create_progress_messages() { fn create_progress_messages() -> Result<()> {
let mut env = TestEnvBuilder::new() let mut env = TestEnvBuilder::new()
.arg_slice(&[ .arg_slice(&[
"imdl", "imdl",
@ -2268,17 +2281,18 @@ Content Size 9 bytes
]) ])
.build(); .build();
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo")?, "").unwrap();
let want = format!( let want = format!(
"[1/3] \u{1F9FF} Searching for files…\n[2/3] \u{1F9EE} Hashing pieces…\n[3/3] \u{1F4BE} \ "[1/3] \u{1F9FF} Searching for files…\n[2/3] \u{1F9EE} Hashing pieces…\n[3/3] \u{1F4BE} \
Writing metainfo to `{}`\n\u{2728}\u{2728} Done! \u{2728}\u{2728}\n", Writing metainfo to `{}`\n\u{2728}\u{2728} Done! \u{2728}\u{2728}\n",
env.resolve("foo.torrent").display() env.resolve("foo.torrent")?.display()
); );
env.run().unwrap(); env.run().unwrap();
assert_eq!(env.err(), want); assert_eq!(env.err(), want);
Ok(())
} }
#[test] #[test]
@ -2436,7 +2450,7 @@ Content Size 9 bytes
} }
#[test] #[test]
fn dry_run_skips_torrent_file_creation() { fn dry_run_skips_torrent_file_creation() -> Result<()> {
let mut env = test_env! { let mut env = test_env! {
args: [ args: [
"torrent", "torrent",
@ -2450,9 +2464,10 @@ Content Size 9 bytes
} }
}; };
assert_matches!(env.run(), Ok(())); assert_matches!(env.run(), Ok(()));
let torrent = env.resolve("foo.torrent"); let torrent = env.resolve("foo.torrent")?;
let err = fs::read(torrent).unwrap_err(); let err = fs::read(torrent).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::NotFound); assert_eq!(err.kind(), io::ErrorKind::NotFound);
Ok(())
} }
#[test] #[test]

View File

@ -11,9 +11,10 @@ pub(crate) struct Link {
long = "input", long = "input",
short = "i", short = "i",
value_name = "METAINFO", value_name = "METAINFO",
empty_values(false),
parse(try_from_os_str = InputTarget::try_from_os_str),
help = "Generate magnet link from metainfo at `PATH`. If `PATH` is `-`, read metainfo from \ help = "Generate magnet link from metainfo at `PATH`. If `PATH` is `-`, read metainfo from \
standard input.", standard input.",
parse(from_os_str)
)] )]
input: InputTarget, input: InputTarget,
#[structopt( #[structopt(

View File

@ -11,9 +11,10 @@ pub(crate) struct Show {
long = "input", long = "input",
short = "i", short = "i",
value_name = "PATH", value_name = "PATH",
empty_values(false),
parse(try_from_os_str = InputTarget::try_from_os_str),
help = "Show information about torrent at `PATH`. If `Path` is `-`, read torrent metainfo \ help = "Show information about torrent at `PATH`. If `Path` is `-`, read torrent metainfo \
from standard input.", from standard input.",
parse(from_os_str)
)] )]
input: InputTarget, input: InputTarget,
} }
@ -34,7 +35,7 @@ mod tests {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
#[test] #[test]
fn output() { fn output() -> Result<()> {
let metainfo = Metainfo { let metainfo = Metainfo {
announce: Some("announce".into()), announce: Some("announce".into()),
announce_list: Some(vec![vec!["announce".into(), "b".into()], vec!["c".into()]]), announce_list: Some(vec![vec!["announce".into(), "b".into()], vec!["c".into()]]),
@ -66,7 +67,7 @@ mod tests {
.out_is_term() .out_is_term()
.build(); .build();
let path = env.resolve("foo.torrent"); let path = env.resolve("foo.torrent")?;
metainfo.dump(path).unwrap(); metainfo.dump(path).unwrap();
@ -103,7 +104,7 @@ Announce List Tier 1: announce
.arg_slice(&["imdl", "torrent", "show", "--input", "foo.torrent"]) .arg_slice(&["imdl", "torrent", "show", "--input", "foo.torrent"])
.build(); .build();
let path = env.resolve("foo.torrent"); let path = env.resolve("foo.torrent")?;
metainfo.dump(path).unwrap(); metainfo.dump(path).unwrap();
@ -131,10 +132,12 @@ files\tfoo
assert_eq!(have, want); assert_eq!(have, want);
} }
Ok(())
} }
#[test] #[test]
fn tier_list_with_main() { fn tier_list_with_main() -> Result<()> {
let metainfo = Metainfo { let metainfo = Metainfo {
announce: Some("a".into()), announce: Some("a".into()),
announce_list: Some(vec![vec!["x".into()], vec!["y".into()], vec!["z".into()]]), announce_list: Some(vec![vec!["x".into()], vec!["y".into()], vec!["z".into()]]),
@ -166,7 +169,7 @@ files\tfoo
.out_is_term() .out_is_term()
.build(); .build();
let path = env.resolve("foo.torrent"); let path = env.resolve("foo.torrent")?;
metainfo.dump(path).unwrap(); metainfo.dump(path).unwrap();
@ -203,7 +206,7 @@ Announce List Tier 1: x
.arg_slice(&["imdl", "torrent", "show", "--input", "foo.torrent"]) .arg_slice(&["imdl", "torrent", "show", "--input", "foo.torrent"])
.build(); .build();
let path = env.resolve("foo.torrent"); let path = env.resolve("foo.torrent")?;
metainfo.dump(path).unwrap(); metainfo.dump(path).unwrap();
@ -231,10 +234,12 @@ files\tfoo
assert_eq!(have, want); assert_eq!(have, want);
} }
Ok(())
} }
#[test] #[test]
fn tier_list_without_main() { fn tier_list_without_main() -> Result<()> {
let metainfo = Metainfo { let metainfo = Metainfo {
announce: Some("a".into()), announce: Some("a".into()),
announce_list: Some(vec![vec!["b".into()], vec!["c".into()], vec!["a".into()]]), announce_list: Some(vec![vec!["b".into()], vec!["c".into()], vec!["a".into()]]),
@ -266,7 +271,7 @@ files\tfoo
.out_is_term() .out_is_term()
.build(); .build();
let path = env.resolve("foo.torrent"); let path = env.resolve("foo.torrent")?;
metainfo.dump(path).unwrap(); metainfo.dump(path).unwrap();
@ -303,7 +308,7 @@ Announce List Tier 1: b
.arg_slice(&["imdl", "torrent", "show", "--input", "foo.torrent"]) .arg_slice(&["imdl", "torrent", "show", "--input", "foo.torrent"])
.build(); .build();
let path = env.resolve("foo.torrent"); let path = env.resolve("foo.torrent")?;
metainfo.dump(path).unwrap(); metainfo.dump(path).unwrap();
@ -331,10 +336,12 @@ files\tfoo
assert_eq!(have, want); assert_eq!(have, want);
} }
Ok(())
} }
#[test] #[test]
fn trackerless() { fn trackerless() -> Result<()> {
let metainfo = Metainfo { let metainfo = Metainfo {
announce: None, announce: None,
announce_list: None, announce_list: None,
@ -366,7 +373,7 @@ files\tfoo
.out_is_term() .out_is_term()
.build(); .build();
let path = env.resolve("foo.torrent"); let path = env.resolve("foo.torrent")?;
metainfo.dump(path).unwrap(); metainfo.dump(path).unwrap();
@ -399,7 +406,7 @@ Creation Date 1970-01-01 00:00:01 UTC
.arg_slice(&["imdl", "torrent", "show", "--input", "foo.torrent"]) .arg_slice(&["imdl", "torrent", "show", "--input", "foo.torrent"])
.build(); .build();
let path = env.resolve("foo.torrent"); let path = env.resolve("foo.torrent")?;
metainfo.dump(path).unwrap(); metainfo.dump(path).unwrap();
@ -425,5 +432,7 @@ files\tfoo
assert_eq!(have, want); assert_eq!(have, want);
} }
Ok(())
} }
} }

View File

@ -19,6 +19,7 @@ pub(crate) struct Stats {
long = "extract-pattern", long = "extract-pattern",
short = "e", short = "e",
value_name = "REGEX", value_name = "REGEX",
empty_values(false),
help = "Extract and display values under key paths that match `REGEX`. Subkeys of a \ help = "Extract and display values under key paths that match `REGEX`. Subkeys of a \
bencodeded dictionary are delimited by `/`, and values of a bencoded list are \ bencodeded dictionary are delimited by `/`, and values of a bencoded list are \
delmited by `*`. For example, given the following bencoded dictionary `{\"foo\": \ delmited by `*`. For example, given the following bencoded dictionary `{\"foo\": \
@ -31,8 +32,9 @@ pub(crate) struct Stats {
long = "input", long = "input",
short = "i", short = "i",
value_name = "PATH", value_name = "PATH",
help = "Search `PATH` for torrents. May be a directory or a single torrent file.", empty_values(false),
parse(from_os_str) parse(from_os_str),
help = "Search `PATH` for torrents. May be a directory or a single torrent file."
)] )]
input: PathBuf, input: PathBuf,
#[structopt( #[structopt(
@ -47,7 +49,7 @@ impl Stats {
pub(crate) fn run(self, env: &mut Env, options: &Options) -> Result<(), Error> { pub(crate) fn run(self, env: &mut Env, options: &Options) -> Result<(), Error> {
options.require_unstable("torrent stats subcommand")?; options.require_unstable("torrent stats subcommand")?;
let path = env.resolve(self.input); let path = env.resolve(self.input)?;
let mut extractor = Extractor::new(self.print, &self.extract_patterns); let mut extractor = Extractor::new(self.print, &self.extract_patterns);

View File

@ -14,18 +14,20 @@ pub(crate) struct Verify {
long = "input", long = "input",
short = "i", short = "i",
value_name = "METAINFO", value_name = "METAINFO",
empty_values(false),
parse(try_from_os_str = InputTarget::try_from_os_str),
help = "Verify torrent contents against torrent metainfo in `METAINFO`. If `METAINFO` is `-`, \ help = "Verify torrent contents against torrent metainfo in `METAINFO`. If `METAINFO` is `-`, \
read metainfo from standard input.", read metainfo from standard input.",
parse(from_os_str)
)] )]
metainfo: InputTarget, metainfo: InputTarget,
#[structopt( #[structopt(
long = "content", long = "content",
short = "c", short = "c",
value_name = "PATH", value_name = "PATH",
empty_values(false),
parse(from_os_str),
help = "Verify torrent content at `PATH` against torrent metainfo. Defaults to `name` field \ help = "Verify torrent content at `PATH` against torrent metainfo. Defaults to `name` field \
of torrent info dictionary.", of torrent info dictionary."
parse(from_os_str)
)] )]
content: Option<PathBuf>, content: Option<PathBuf>,
} }
@ -66,7 +68,7 @@ impl Verify {
VerifyStep::Verifying { content: &content }.print(env)?; VerifyStep::Verifying { content: &content }.print(env)?;
let status = metainfo.verify(&env.resolve(content), progress_bar)?; let status = metainfo.verify(&env.resolve(content)?, progress_bar)?;
status.print(env)?; status.print(env)?;
@ -119,7 +121,7 @@ mod tests {
create_env.run()?; create_env.run()?;
let torrent = create_env.resolve("foo.torrent"); let torrent = create_env.resolve("foo.torrent")?;
let mut verify_env = test_env! { let mut verify_env = test_env! {
args: [ args: [
@ -137,7 +139,7 @@ mod tests {
"[1/2] \u{1F4BE} Loading metainfo from `{}`…\n[2/2] \u{1F9EE} Verifying pieces from \ "[1/2] \u{1F4BE} Loading metainfo from `{}`…\n[2/2] \u{1F9EE} Verifying pieces from \
`{}`\n\u{2728}\u{2728} Verification succeeded! \u{2728}\u{2728}\n", `{}`\n\u{2728}\u{2728} Verification succeeded! \u{2728}\u{2728}\n",
torrent.display(), torrent.display(),
create_env.resolve("foo").display() create_env.resolve("foo")?.display()
); );
assert_eq!(verify_env.err(), want); assert_eq!(verify_env.err(), want);
@ -170,7 +172,7 @@ mod tests {
create_env.write("foo/a", "xyz"); create_env.write("foo/a", "xyz");
let torrent = create_env.resolve("foo.torrent"); let torrent = create_env.resolve("foo.torrent")?;
let mut verify_env = test_env! { let mut verify_env = test_env! {
args: [ args: [
@ -191,7 +193,7 @@ mod tests {
), ),
&format!( &format!(
"[2/2] \u{1F9EE} Verifying pieces from `{}`…", "[2/2] \u{1F9EE} Verifying pieces from `{}`…",
create_env.resolve("foo").display() create_env.resolve("foo")?.display()
), ),
"Pieces corrupted.", "Pieces corrupted.",
"error: Torrent verification failed.", "error: Torrent verification failed.",
@ -227,11 +229,11 @@ mod tests {
create_env.run()?; create_env.run()?;
let torrent = create_env.resolve("foo.torrent"); let torrent = create_env.resolve("foo.torrent")?;
let foo = create_env.resolve("foo"); let foo = create_env.resolve("foo")?;
let bar = create_env.resolve("bar"); let bar = create_env.resolve("bar")?;
fs::rename(&foo, &bar).unwrap(); fs::rename(&foo, &bar).unwrap();
@ -284,8 +286,8 @@ mod tests {
create_env.run()?; create_env.run()?;
let torrent = create_env.resolve("foo.torrent"); let torrent = create_env.resolve("foo.torrent")?;
let content = create_env.resolve("foo"); let content = create_env.resolve("foo")?;
let mut verify_env = test_env! { let mut verify_env = test_env! {
args: [ args: [
@ -340,7 +342,7 @@ mod tests {
create_env.run()?; create_env.run()?;
let torrent = create_env.resolve("foo.torrent"); let torrent = create_env.resolve("foo.torrent")?;
create_env.write("foo/a", "xyz"); create_env.write("foo/a", "xyz");
create_env.write("foo/d", "efgg"); create_env.write("foo/d", "efgg");
@ -377,7 +379,7 @@ mod tests {
), ),
&format!( &format!(
"[2/2] \u{1F9EE} Verifying pieces from `{}`…", "[2/2] \u{1F9EE} Verifying pieces from `{}`…",
create_env.resolve("foo").display() create_env.resolve("foo")?.display()
), ),
"a: MD5 checksum mismatch: d16fb36f0911f878998c136191af705e (expected \ "a: MD5 checksum mismatch: d16fb36f0911f878998c136191af705e (expected \
900150983cd24fb0d6963f7d28e17f72)", 900150983cd24fb0d6963f7d28e17f72)",
@ -425,7 +427,7 @@ mod tests {
create_env.run()?; create_env.run()?;
let torrent = create_env.resolve("foo.torrent"); let torrent = create_env.resolve("foo.torrent")?;
create_env.write("foo/a", "xyz"); create_env.write("foo/a", "xyz");
create_env.write("foo/d", "efgg"); create_env.write("foo/d", "efgg");
@ -482,7 +484,7 @@ mod tests {
style.dim().paint("[2/2]"), style.dim().paint("[2/2]"),
style.message().paint(format!( style.message().paint(format!(
"Verifying pieces from `{}`…", "Verifying pieces from `{}`…",
create_env.resolve("foo").display() create_env.resolve("foo")?.display()
)) ))
), ),
&format!( &format!(
@ -531,7 +533,7 @@ mod tests {
create_env.run()?; create_env.run()?;
let torrent = create_env.resolve("foo.torrent"); let torrent = create_env.resolve("foo.torrent")?;
create_env.write("foo", "abcxyz"); create_env.write("foo", "abcxyz");
@ -554,7 +556,7 @@ mod tests {
), ),
&format!( &format!(
"[2/2] \u{1F9EE} Verifying pieces from `{}`…", "[2/2] \u{1F9EE} Verifying pieces from `{}`…",
create_env.resolve("foo").display() create_env.resolve("foo")?.display()
), ),
"3 bytes too long", "3 bytes too long",
"Pieces corrupted.", "Pieces corrupted.",
@ -591,7 +593,7 @@ mod tests {
create_env.run()?; create_env.run()?;
let torrent = create_env.resolve("foo.torrent"); let torrent = create_env.resolve("foo.torrent")?;
let metainfo = fs::read(torrent).unwrap(); let metainfo = fs::read(torrent).unwrap();
@ -606,7 +608,7 @@ mod tests {
tree: {}, tree: {},
}; };
fs::rename(create_env.resolve("foo"), verify_env.resolve("foo")).unwrap(); fs::rename(create_env.resolve("foo")?, verify_env.resolve("foo")?).unwrap();
assert_matches!(verify_env.run(), Ok(())); assert_matches!(verify_env.run(), Ok(()));

View File

@ -56,25 +56,25 @@ impl TestEnv {
} }
pub(crate) fn write(&self, path: impl AsRef<Path>, bytes: impl AsRef<[u8]>) { pub(crate) fn write(&self, path: impl AsRef<Path>, bytes: impl AsRef<[u8]>) {
fs::write(self.env.resolve(path), bytes.as_ref()).unwrap(); fs::write(self.env.resolve(path).unwrap(), bytes.as_ref()).unwrap();
} }
pub(crate) fn remove_file(&self, path: impl AsRef<Path>) { pub(crate) fn remove_file(&self, path: impl AsRef<Path>) {
fs::remove_file(self.env.resolve(path)).unwrap(); fs::remove_file(self.env.resolve(path).unwrap()).unwrap();
} }
pub(crate) fn create_dir(&self, path: impl AsRef<Path>) { pub(crate) fn create_dir(&self, path: impl AsRef<Path>) {
fs::create_dir(self.env.resolve(path)).unwrap(); fs::create_dir(self.env.resolve(path).unwrap()).unwrap();
} }
#[cfg(unix)] #[cfg(unix)]
pub(crate) fn metadata(&self, path: impl AsRef<Path>) -> fs::Metadata { pub(crate) fn metadata(&self, path: impl AsRef<Path>) -> fs::Metadata {
fs::metadata(self.env.resolve(path)).unwrap() fs::metadata(self.env.resolve(path).unwrap()).unwrap()
} }
#[cfg(unix)] #[cfg(unix)]
pub(crate) fn set_permissions(&self, path: impl AsRef<Path>, permissions: fs::Permissions) { pub(crate) fn set_permissions(&self, path: impl AsRef<Path>, permissions: fs::Permissions) {
fs::set_permissions(self.env.resolve(path), permissions).unwrap(); fs::set_permissions(self.env.resolve(path).unwrap(), permissions).unwrap();
} }
pub(crate) fn assert_ok(&mut self) { pub(crate) fn assert_ok(&mut self) {
@ -90,7 +90,9 @@ impl TestEnv {
} }
pub(crate) fn load_metainfo(&mut self, filename: impl AsRef<Path>) -> Metainfo { 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(); let path = filename.as_ref();
let target = InputTarget::try_from(path.as_os_str()).unwrap();
let input = self.env.read(target).unwrap();
Metainfo::from_input(&input).unwrap() Metainfo::from_input(&input).unwrap()
} }
} }

View File

@ -146,7 +146,7 @@ mod tests {
let metainfo = env.load_metainfo("foo.torrent"); let metainfo = env.load_metainfo("foo.torrent");
assert!(metainfo.verify(&env.resolve("foo"), None)?.good()); assert!(metainfo.verify(&env.resolve("foo")?, None)?.good());
Ok(()) Ok(())
} }
@ -177,7 +177,7 @@ mod tests {
let metainfo = env.load_metainfo("foo.torrent"); let metainfo = env.load_metainfo("foo.torrent");
let status = metainfo.verify(&env.resolve("foo"), None)?; let status = metainfo.verify(&env.resolve("foo")?, None)?;
assert_eq!(status.count_bad(), 0); assert_eq!(status.count_bad(), 0);