diff --git a/rustfmt.toml b/rustfmt.toml index 02f007c..847db9d 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -10,7 +10,6 @@ merge_imports = true newline_style = "Unix" normalize_comments = true reorder_impl_items = true -required_version = "1.4.12" tab_spaces = 2 unstable_features = true use_field_init_shorthand = true diff --git a/src/common.rs b/src/common.rs index 2ff4900..f0eb76a 100644 --- a/src/common.rs +++ b/src/common.rs @@ -4,7 +4,7 @@ pub(crate) use std::{ char, cmp::{Ordering, Reverse}, collections::{BTreeMap, BTreeSet, HashMap, HashSet}, - convert::TryInto, + convert::{TryFrom, TryInto}, env, ffi::{OsStr, OsString}, fmt::{self, Display, Formatter}, diff --git a/src/env.rs b/src/env.rs index e5d1990..ca8adc6 100644 --- a/src/env.rs +++ b/src/env.rs @@ -163,14 +163,20 @@ impl Env { &mut self.out } - pub(crate) fn resolve(&self, path: impl AsRef) -> PathBuf { - self.dir().join(path).clean() + pub(crate) fn resolve(&self, path: impl AsRef) -> Result { + 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 { let data = match &source { InputTarget::Path(path) => { - let absolute = self.resolve(path); + let absolute = self.resolve(path)?; fs::read(absolute).context(error::Filesystem { path })? } InputTarget::Stdin => { diff --git a/src/error.rs b/src/error.rs index d26532a..b20123e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -30,6 +30,8 @@ pub(crate) enum Error { GlobParse { source: globset::Error }, #[snafu(display("Failed to serialize torrent info dictionary: {}", source))] InfoSerialize { source: bendy::serde::Error }, + #[snafu(display("Input target empty"))] + InputTargetEmpty, #[snafu(display( "Interal error, this may indicate a bug in intermodal: {}\n\ Consider filing an issue: https://github.com/casey/imdl/issues/new", @@ -61,6 +63,8 @@ pub(crate) enum Error { OpenerExitStatus { exit_status: ExitStatus }, #[snafu(display("Output path already exists: `{}`", path.display()))] OutputExists { path: PathBuf }, + #[snafu(display("Output target empty"))] + OutputTargetEmpty, #[snafu(display( "Path `{}` contains non-normal component: {}", path.display(), diff --git a/src/input_target.rs b/src/input_target.rs index d4e689e..878b6e8 100644 --- a/src/input_target.rs +++ b/src/input_target.rs @@ -7,20 +7,32 @@ pub(crate) enum InputTarget { } impl InputTarget { - pub(crate) fn resolve(&self, env: &Env) -> Self { + pub(crate) fn resolve(&self, env: &Env) -> Result { match self { - Self::Path(path) => Self::Path(env.resolve(path)), - Self::Stdin => Self::Stdin, + Self::Path(path) => Ok(Self::Path(env.resolve(path)?)), + Self::Stdin => Ok(Self::Stdin), } } + + pub(crate) fn try_from_os_str(text: &OsStr) -> Result { + text + .try_into() + .map_err(|err: Error| OsString::from(err.to_string())) + } } -impl From<&OsStr> for InputTarget { - fn from(text: &OsStr) -> Self { +impl TryFrom<&OsStr> for InputTarget { + type Error = Error; + + fn try_from(text: &OsStr) -> Result { + if text.is_empty() { + return Err(Error::InputTargetEmpty); + }; + if text == OsStr::new("-") { - Self::Stdin + Ok(Self::Stdin) } else { - Self::Path(text.into()) + Ok(Self::Path(text.into())) } } } @@ -50,14 +62,17 @@ mod tests { #[test] fn file() { assert_eq!( - InputTarget::from(OsStr::new("foo")), - InputTarget::Path("foo".into()) + InputTarget::try_from(OsStr::new("foo")).unwrap(), + InputTarget::Path("foo".into()), ); } #[test] fn stdio() { - assert_eq!(InputTarget::from(OsStr::new("-")), InputTarget::Stdin); + assert_eq!( + InputTarget::try_from(OsStr::new("-")).unwrap(), + InputTarget::Stdin + ); } #[test] diff --git a/src/output_target.rs b/src/output_target.rs index c9a7ec8..38248cf 100644 --- a/src/output_target.rs +++ b/src/output_target.rs @@ -2,25 +2,37 @@ use crate::common::*; #[derive(PartialEq, Debug)] pub(crate) enum OutputTarget { - File(PathBuf), + Path(PathBuf), Stdout, } impl OutputTarget { - pub(crate) fn resolve(&self, env: &Env) -> Self { + pub(crate) fn resolve(&self, env: &Env) -> Result { match self { - Self::File(path) => Self::File(env.resolve(path)), - Self::Stdout => Self::Stdout, + Self::Path(path) => Ok(Self::Path(env.resolve(path)?)), + Self::Stdout => Ok(Self::Stdout), } } + + pub(crate) fn try_from_os_str(text: &OsStr) -> Result { + text + .try_into() + .map_err(|err: Error| OsString::from(err.to_string())) + } } -impl From<&OsStr> for OutputTarget { - fn from(text: &OsStr) -> Self { +impl TryFrom<&OsStr> for OutputTarget { + type Error = Error; + + fn try_from(text: &OsStr) -> Result { + if text.is_empty() { + return Err(Error::OutputTargetEmpty); + }; + if text == OsStr::new("-") { - Self::Stdout + Ok(Self::Stdout) } 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 { match self { 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] fn file() { assert_eq!( - OutputTarget::from(OsStr::new("foo")), - OutputTarget::File("foo".into()) + OutputTarget::try_from(OsStr::new("foo")).unwrap(), + OutputTarget::Path("foo".into()) ); } #[test] fn stdio() { - assert_eq!(OutputTarget::from(OsStr::new("-")), OutputTarget::Stdout); + assert_eq!( + OutputTarget::try_from(OsStr::new("-")).unwrap(), + OutputTarget::Stdout + ); } #[test] fn display_file() { let path = PathBuf::from("./path"); - let have = OutputTarget::File(path).to_string(); + let have = OutputTarget::Path(path).to_string(); let want = "`./path`"; assert_eq!(have, want); } diff --git a/src/subcommand/torrent/create.rs b/src/subcommand/torrent/create.rs index d53185f..4e6e2f5 100644 --- a/src/subcommand/torrent/create.rs +++ b/src/subcommand/torrent/create.rs @@ -116,11 +116,12 @@ Examples: long = "input", short = "i", 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 \ 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 \ standard input if `--piece-length` is not given.", - parse(from_os_str) )] input: InputTarget, #[structopt( @@ -187,11 +188,12 @@ Sort in ascending order by size, break ties in descending path order: long = "output", short = "o", 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 `-`. \ Defaults to the argument to `--input` with an `.torrent` extension appended. Required \ when `--input -`.", - required_if("input", "-"), - parse(from_os_str) )] output: Option, #[structopt( @@ -240,8 +242,11 @@ Sort in ascending order by size, break ties in descending path order: impl Create { pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> { - let input = self.input.resolve(env); - let output = self.output.map(|output| output.resolve(env)); + let input = self.input.resolve(env)?; + let output = match self.output { + Some(output) => Some(output.resolve(env)?), + None => None, + }; let mut linter = Linter::new(); linter.allow(self.allowed_lints.iter().cloned()); @@ -320,15 +325,12 @@ impl Create { files = Some(files_inner); - output - .as_ref() - .map(|output| output.resolve(env)) - .unwrap_or_else(|| { - let mut torrent_name = name.to_owned(); - torrent_name.push_str(".torrent"); + output.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 => { @@ -363,7 +365,7 @@ impl Create { return Err(Error::PieceLengthSmall); } - if let OutputTarget::File(path) = &output { + if let OutputTarget::Path(path) = &output { if !self.force && path.exists() { return Err(Error::OutputExists { path: path.to_owned(), @@ -441,7 +443,7 @@ impl Create { if !self.dry_run { match &output { - OutputTarget::File(path) => { + OutputTarget::Path(path) => { let mut open_options = fs::OpenOptions::new(); if self.force { @@ -490,7 +492,7 @@ impl Create { outln!(env, "{}", link)?; } - if let OutputTarget::File(path) = output { + if let OutputTarget::Path(path) = output { if self.open { Platform::open_file(&path)?; } @@ -546,7 +548,7 @@ mod tests { } #[test] - fn torrent_file_is_bencode_dict() { + fn torrent_file_is_bencode_dict() -> Result<()> { let mut env = test_env! { args: [ "torrent", @@ -561,10 +563,11 @@ mod tests { } }; env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); + let torrent = env.resolve("foo.torrent")?; let bytes = fs::read(torrent).unwrap(); let value = Value::from_bencode(&bytes).unwrap(); assert!(matches!(value, Value::Dict(_))); + Ok(()) } #[test] @@ -1531,7 +1534,7 @@ mod tests { } #[test] - fn output() { + fn output() -> Result<()> { let mut env = TestEnvBuilder::new() .arg_slice(&[ "imdl", @@ -1546,17 +1549,18 @@ mod tests { .out_is_term() .build(); - let dir = env.resolve("foo"); + let dir = env.resolve("foo")?; fs::create_dir(&dir).unwrap(); fs::write(dir.join("a"), "abc").unwrap(); fs::write(dir.join("x"), "xyz").unwrap(); fs::write(dir.join("h"), "hij").unwrap(); env.run().unwrap(); assert_eq!(env.out(), ""); + Ok(()) } #[test] - fn show() { + fn show() -> Result<()> { let mut env = TestEnvBuilder::new() .arg_slice(&[ "imdl", @@ -1572,7 +1576,7 @@ mod tests { .out_is_term() .build(); - let dir = env.resolve("foo"); + let dir = env.resolve("foo")?; fs::create_dir(&dir).unwrap(); fs::write(dir.join("a"), "abc").unwrap(); fs::write(dir.join("x"), "xyz").unwrap(); @@ -1600,6 +1604,7 @@ Content Size 9 bytes 212 + consts::CREATED_BY_DEFAULT.len() ); assert_eq!(have, want); + Ok(()) } #[test] @@ -1625,7 +1630,7 @@ Content Size 9 bytes } #[test] - fn force_default() { + fn force_default() -> Result<()> { let mut env = test_env! { args: [ "torrent", @@ -1643,8 +1648,9 @@ Content Size 9 bytes assert_matches!( env.run().unwrap_err(), Error::OutputExists {path} - if path == env.resolve("foo.torrent") - ) + if path == env.resolve("foo.torrent")? + ); + Ok(()) } #[test] @@ -1724,7 +1730,7 @@ Content Size 9 bytes } #[test] - fn skip_hidden() { + fn skip_hidden() -> Result<()> { let mut env = test_env! { args: [ "torrent", @@ -1745,17 +1751,17 @@ Content Size 9 bytes if cfg!(target_os = "windows") { Command::new("attrib") .arg("+h") - .arg(env.resolve("foo/hidden")) + .arg(env.resolve("foo/hidden")?) .status() .unwrap(); } else if cfg!(target_os = "macos") { Command::new("chflags") .arg("hidden") - .arg(env.resolve("foo/hidden")) + .arg(env.resolve("foo/hidden")?) .status() .unwrap(); } else { - fs::remove_file(env.resolve("foo/hidden")).unwrap(); + fs::remove_file(env.resolve("foo/hidden")?).unwrap(); } env.run().unwrap(); @@ -1767,10 +1773,11 @@ Content Size 9 bytes Mode::Multiple { files } if files.len() == 0 ); assert_eq!(metainfo.info.pieces, PieceList::new()); + Ok(()) } #[test] - fn include_hidden() { + fn include_hidden() -> Result<()> { let mut env = test_env! { args: [ "torrent", @@ -1792,13 +1799,13 @@ Content Size 9 bytes if cfg!(target_os = "windows") { Command::new("attrib") .arg("+h") - .arg(env.resolve("foo/hidden")) + .arg(env.resolve("foo/hidden")?) .status() .unwrap(); } else if cfg!(target_os = "macos") { Command::new("chflags") .arg("hidden") - .arg(env.resolve("foo/hidden")) + .arg(env.resolve("foo/hidden")?) .status() .unwrap(); } @@ -1810,12 +1817,13 @@ Content Size 9 bytes Mode::Multiple { files } if files.len() == 2 ); assert_eq!(metainfo.info.pieces, PieceList::from_pieces(&["abcabc"])); + Ok(()) } - fn populate_symlinks(env: &Env) { - let dir = env.resolve("foo"); - let file_src = env.resolve("bar"); - let dir_src = env.resolve("dir-src"); + fn populate_symlinks(env: &Env) -> Result<()> { + let dir = env.resolve("foo")?; + let file_src = env.resolve("bar")?; + let dir_src = env.resolve("dir-src")?; let dir_contents = dir_src.join("baz"); fs::create_dir(&dir_src).unwrap(); fs::write(dir_contents, "baz").unwrap(); @@ -1824,8 +1832,8 @@ Content Size 9 bytes fs::write(file_src, "bar").unwrap(); #[cfg(unix)] { - let file_link = env.resolve("foo/bar"); - let dir_link = env.resolve("foo/dir"); + let file_link = env.resolve("foo/bar")?; + let dir_link = env.resolve("foo/dir")?; Command::new("ln") .arg("-s") .arg("../bar") @@ -1840,10 +1848,12 @@ Content Size 9 bytes .status() .unwrap(); } + + Ok(()) } #[test] - fn skip_symlinks() { + fn skip_symlinks() -> Result<()> { let mut env = test_env! { args: [ "torrent", @@ -1856,7 +1866,7 @@ Content Size 9 bytes ], tree: {}, }; - populate_symlinks(&env); + populate_symlinks(&env)?; env.run().unwrap(); let metainfo = env.load_metainfo("foo.torrent"); assert_matches!( @@ -1864,11 +1874,12 @@ Content Size 9 bytes Mode::Multiple { files } if files.is_empty() ); assert_eq!(metainfo.info.pieces, PieceList::new()); + Ok(()) } #[test] #[cfg(unix)] - fn follow_symlinks() { + fn follow_symlinks() -> Result<()> { let mut env = test_env! { args: [ "torrent", @@ -1882,7 +1893,7 @@ Content Size 9 bytes ], tree: {}, }; - populate_symlinks(&env); + populate_symlinks(&env)?; env.run().unwrap(); let metainfo = env.load_metainfo("foo.torrent"); let mut pieces = PieceList::new(); @@ -1908,11 +1919,12 @@ Content Size 9 bytes } _ => panic!("Expected multi-file torrent"), } + Ok(()) } #[test] #[cfg(unix)] - fn symlink_root() { + fn symlink_root() -> Result<()> { let mut env = test_env! { args: [ "torrent", @@ -1926,8 +1938,8 @@ Content Size 9 bytes tree: {}, }; - let file_src = env.resolve("bar"); - let file_link = env.resolve("foo"); + let file_src = env.resolve("bar")?; + let file_link = env.resolve("foo")?; Command::new("ln") .arg("-s") @@ -1937,6 +1949,7 @@ Content Size 9 bytes .unwrap(); assert_matches!(env.run().unwrap_err(), Error::SymlinkRoot { root } if root == file_link); + Ok(()) } #[test] @@ -1967,9 +1980,8 @@ Content Size 9 bytes ); assert_eq!(metainfo.info.pieces, PieceList::new()); } - #[test] - fn skip_hidden_attribute_dir_contents() { + fn skip_hidden_attribute_dir_contents() -> Result<()> { let mut env = test_env! { args: [ "torrent", @@ -1990,7 +2002,7 @@ Content Size 9 bytes #[cfg(target_os = "windows")] { env.write("foo/bar/baz", "baz"); - let path = env.resolve("foo/bar"); + let path = env.resolve("foo/bar")?; Command::new("attrib") .arg("+h") .arg(&path) @@ -2001,7 +2013,7 @@ Content Size 9 bytes #[cfg(target_os = "macos")] { env.write("foo/bar/baz", "baz"); - let path = env.resolve("foo/bar"); + let path = env.resolve("foo/bar")?; Command::new("chflags") .arg("hidden") .arg(&path) @@ -2016,6 +2028,7 @@ Content Size 9 bytes Mode::Multiple { files } if files.is_empty() ); assert_eq!(metainfo.info.pieces, PieceList::new()); + Ok(()) } #[test] @@ -2255,7 +2268,7 @@ Content Size 9 bytes } #[test] - fn create_progress_messages() { + fn create_progress_messages() -> Result<()> { let mut env = TestEnvBuilder::new() .arg_slice(&[ "imdl", @@ -2268,17 +2281,18 @@ Content Size 9 bytes ]) .build(); - fs::write(env.resolve("foo"), "").unwrap(); + fs::write(env.resolve("foo")?, "").unwrap(); let want = format!( "[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", - env.resolve("foo.torrent").display() + env.resolve("foo.torrent")?.display() ); env.run().unwrap(); assert_eq!(env.err(), want); + Ok(()) } #[test] @@ -2436,7 +2450,7 @@ Content Size 9 bytes } #[test] - fn dry_run_skips_torrent_file_creation() { + fn dry_run_skips_torrent_file_creation() -> Result<()> { let mut env = test_env! { args: [ "torrent", @@ -2450,9 +2464,10 @@ Content Size 9 bytes } }; assert_matches!(env.run(), Ok(())); - let torrent = env.resolve("foo.torrent"); + let torrent = env.resolve("foo.torrent")?; let err = fs::read(torrent).unwrap_err(); assert_eq!(err.kind(), io::ErrorKind::NotFound); + Ok(()) } #[test] diff --git a/src/subcommand/torrent/link.rs b/src/subcommand/torrent/link.rs index 885f1f5..e137c64 100644 --- a/src/subcommand/torrent/link.rs +++ b/src/subcommand/torrent/link.rs @@ -11,9 +11,10 @@ pub(crate) struct Link { long = "input", short = "i", 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 \ standard input.", - parse(from_os_str) )] input: InputTarget, #[structopt( diff --git a/src/subcommand/torrent/show.rs b/src/subcommand/torrent/show.rs index a48361d..1380f7a 100644 --- a/src/subcommand/torrent/show.rs +++ b/src/subcommand/torrent/show.rs @@ -11,9 +11,10 @@ pub(crate) struct Show { long = "input", short = "i", 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 \ from standard input.", - parse(from_os_str) )] input: InputTarget, } @@ -34,7 +35,7 @@ mod tests { use pretty_assertions::assert_eq; #[test] - fn output() { + fn output() -> Result<()> { let metainfo = Metainfo { announce: Some("announce".into()), announce_list: Some(vec![vec!["announce".into(), "b".into()], vec!["c".into()]]), @@ -66,7 +67,7 @@ mod tests { .out_is_term() .build(); - let path = env.resolve("foo.torrent"); + let path = env.resolve("foo.torrent")?; metainfo.dump(path).unwrap(); @@ -103,7 +104,7 @@ Announce List Tier 1: announce .arg_slice(&["imdl", "torrent", "show", "--input", "foo.torrent"]) .build(); - let path = env.resolve("foo.torrent"); + let path = env.resolve("foo.torrent")?; metainfo.dump(path).unwrap(); @@ -131,10 +132,12 @@ files\tfoo assert_eq!(have, want); } + + Ok(()) } #[test] - fn tier_list_with_main() { + fn tier_list_with_main() -> Result<()> { let metainfo = Metainfo { announce: Some("a".into()), announce_list: Some(vec![vec!["x".into()], vec!["y".into()], vec!["z".into()]]), @@ -166,7 +169,7 @@ files\tfoo .out_is_term() .build(); - let path = env.resolve("foo.torrent"); + let path = env.resolve("foo.torrent")?; metainfo.dump(path).unwrap(); @@ -203,7 +206,7 @@ Announce List Tier 1: x .arg_slice(&["imdl", "torrent", "show", "--input", "foo.torrent"]) .build(); - let path = env.resolve("foo.torrent"); + let path = env.resolve("foo.torrent")?; metainfo.dump(path).unwrap(); @@ -231,10 +234,12 @@ files\tfoo assert_eq!(have, want); } + + Ok(()) } #[test] - fn tier_list_without_main() { + fn tier_list_without_main() -> Result<()> { let metainfo = Metainfo { announce: Some("a".into()), announce_list: Some(vec![vec!["b".into()], vec!["c".into()], vec!["a".into()]]), @@ -266,7 +271,7 @@ files\tfoo .out_is_term() .build(); - let path = env.resolve("foo.torrent"); + let path = env.resolve("foo.torrent")?; metainfo.dump(path).unwrap(); @@ -303,7 +308,7 @@ Announce List Tier 1: b .arg_slice(&["imdl", "torrent", "show", "--input", "foo.torrent"]) .build(); - let path = env.resolve("foo.torrent"); + let path = env.resolve("foo.torrent")?; metainfo.dump(path).unwrap(); @@ -331,10 +336,12 @@ files\tfoo assert_eq!(have, want); } + + Ok(()) } #[test] - fn trackerless() { + fn trackerless() -> Result<()> { let metainfo = Metainfo { announce: None, announce_list: None, @@ -366,7 +373,7 @@ files\tfoo .out_is_term() .build(); - let path = env.resolve("foo.torrent"); + let path = env.resolve("foo.torrent")?; 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"]) .build(); - let path = env.resolve("foo.torrent"); + let path = env.resolve("foo.torrent")?; metainfo.dump(path).unwrap(); @@ -425,5 +432,7 @@ files\tfoo assert_eq!(have, want); } + + Ok(()) } } diff --git a/src/subcommand/torrent/stats.rs b/src/subcommand/torrent/stats.rs index 3fa421c..8cf19e6 100644 --- a/src/subcommand/torrent/stats.rs +++ b/src/subcommand/torrent/stats.rs @@ -19,6 +19,7 @@ pub(crate) struct Stats { long = "extract-pattern", short = "e", value_name = "REGEX", + empty_values(false), 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 \ delmited by `*`. For example, given the following bencoded dictionary `{\"foo\": \ @@ -31,8 +32,9 @@ pub(crate) struct Stats { long = "input", short = "i", value_name = "PATH", - help = "Search `PATH` for torrents. May be a directory or a single torrent file.", - parse(from_os_str) + empty_values(false), + parse(from_os_str), + help = "Search `PATH` for torrents. May be a directory or a single torrent file." )] input: PathBuf, #[structopt( @@ -47,7 +49,7 @@ impl Stats { pub(crate) fn run(self, env: &mut Env, options: &Options) -> Result<(), Error> { 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); diff --git a/src/subcommand/torrent/verify.rs b/src/subcommand/torrent/verify.rs index 449e1f9..b70114d 100644 --- a/src/subcommand/torrent/verify.rs +++ b/src/subcommand/torrent/verify.rs @@ -14,18 +14,20 @@ pub(crate) struct Verify { long = "input", short = "i", 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 `-`, \ read metainfo from standard input.", - parse(from_os_str) )] metainfo: InputTarget, #[structopt( long = "content", short = "c", value_name = "PATH", + empty_values(false), + parse(from_os_str), help = "Verify torrent content at `PATH` against torrent metainfo. Defaults to `name` field \ - of torrent info dictionary.", - parse(from_os_str) + of torrent info dictionary." )] content: Option, } @@ -66,7 +68,7 @@ impl Verify { 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)?; @@ -119,7 +121,7 @@ mod tests { create_env.run()?; - let torrent = create_env.resolve("foo.torrent"); + let torrent = create_env.resolve("foo.torrent")?; let mut verify_env = test_env! { args: [ @@ -137,7 +139,7 @@ mod tests { "[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", torrent.display(), - create_env.resolve("foo").display() + create_env.resolve("foo")?.display() ); assert_eq!(verify_env.err(), want); @@ -170,7 +172,7 @@ mod tests { 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! { args: [ @@ -191,7 +193,7 @@ mod tests { ), &format!( "[2/2] \u{1F9EE} Verifying pieces from `{}`…", - create_env.resolve("foo").display() + create_env.resolve("foo")?.display() ), "Pieces corrupted.", "error: Torrent verification failed.", @@ -227,11 +229,11 @@ mod tests { 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(); @@ -284,8 +286,8 @@ mod tests { create_env.run()?; - let torrent = create_env.resolve("foo.torrent"); - let content = create_env.resolve("foo"); + let torrent = create_env.resolve("foo.torrent")?; + let content = create_env.resolve("foo")?; let mut verify_env = test_env! { args: [ @@ -340,7 +342,7 @@ mod tests { 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/d", "efgg"); @@ -377,7 +379,7 @@ mod tests { ), &format!( "[2/2] \u{1F9EE} Verifying pieces from `{}`…", - create_env.resolve("foo").display() + create_env.resolve("foo")?.display() ), "a: MD5 checksum mismatch: d16fb36f0911f878998c136191af705e (expected \ 900150983cd24fb0d6963f7d28e17f72)", @@ -425,7 +427,7 @@ mod tests { 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/d", "efgg"); @@ -482,7 +484,7 @@ mod tests { style.dim().paint("[2/2]"), style.message().paint(format!( "Verifying pieces from `{}`…", - create_env.resolve("foo").display() + create_env.resolve("foo")?.display() )) ), &format!( @@ -531,7 +533,7 @@ mod tests { create_env.run()?; - let torrent = create_env.resolve("foo.torrent"); + let torrent = create_env.resolve("foo.torrent")?; create_env.write("foo", "abcxyz"); @@ -554,7 +556,7 @@ mod tests { ), &format!( "[2/2] \u{1F9EE} Verifying pieces from `{}`…", - create_env.resolve("foo").display() + create_env.resolve("foo")?.display() ), "3 bytes too long", "Pieces corrupted.", @@ -591,7 +593,7 @@ mod tests { create_env.run()?; - let torrent = create_env.resolve("foo.torrent"); + let torrent = create_env.resolve("foo.torrent")?; let metainfo = fs::read(torrent).unwrap(); @@ -606,7 +608,7 @@ mod tests { 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(())); diff --git a/src/test_env.rs b/src/test_env.rs index dfe9396..5c86b05 100644 --- a/src/test_env.rs +++ b/src/test_env.rs @@ -56,25 +56,25 @@ impl TestEnv { } pub(crate) fn write(&self, path: impl AsRef, 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) { - 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) { - fs::create_dir(self.env.resolve(path)).unwrap(); + fs::create_dir(self.env.resolve(path).unwrap()).unwrap(); } #[cfg(unix)] pub(crate) fn metadata(&self, path: impl AsRef) -> fs::Metadata { - fs::metadata(self.env.resolve(path)).unwrap() + fs::metadata(self.env.resolve(path).unwrap()).unwrap() } #[cfg(unix)] pub(crate) fn set_permissions(&self, path: impl AsRef, 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) { @@ -90,7 +90,9 @@ impl TestEnv { } pub(crate) fn load_metainfo(&mut self, filename: impl AsRef) -> 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() } } diff --git a/src/verifier.rs b/src/verifier.rs index b131934..bfbe87c 100644 --- a/src/verifier.rs +++ b/src/verifier.rs @@ -146,7 +146,7 @@ mod tests { let metainfo = env.load_metainfo("foo.torrent"); - assert!(metainfo.verify(&env.resolve("foo"), None)?.good()); + assert!(metainfo.verify(&env.resolve("foo")?, None)?.good()); Ok(()) } @@ -177,7 +177,7 @@ mod tests { 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);