From 8e3f5516aff8c89289203a2bc1b46505410c5f1f Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Mon, 6 Apr 2020 09:11:17 -0700 Subject: [PATCH] Use attractive paths in user-facing messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If a user passes `--input foo`, print "Searching `foo` for files…", instead of the resolved, absolute path to `foo`, since the former is what the user typed in. This was way harder, and had way more edge cases, than I thought it would be! One takaway, lexical path cleaning is excellent. type: changed fixes: - https://github.com/casey/intermodal/issues/252 - https://github.com/casey/intermodal/issues/332 --- src/error.rs | 6 +- src/input_target.rs | 7 - src/output_target.rs | 2 +- src/path_ext.rs | 78 ++- src/subcommand/torrent/create.rs | 484 +++++++++++------- .../torrent/create/create_content.rs | 153 ++++++ src/subcommand/torrent/create/create_step.rs | 18 +- 7 files changed, 526 insertions(+), 222 deletions(-) create mode 100644 src/subcommand/torrent/create/create_content.rs diff --git a/src/error.rs b/src/error.rs index b20123e..ec112d7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -18,11 +18,11 @@ pub(crate) enum Error { CommandInvoke { command: String, source: io::Error }, #[snafu(display("Command `{}` returned bad exit status: {}", command, status))] CommandStatus { command: String, status: ExitStatus }, - #[snafu(display("Filename was not valid unicode: {}", filename.display()))] + #[snafu(display("Filename was not valid unicode: `{}`", filename.display()))] FilenameDecode { filename: PathBuf }, - #[snafu(display("Path had no file name: {}", path.display()))] + #[snafu(display("Path had no file name: `{}`", path.display()))] FilenameExtract { path: PathBuf }, - #[snafu(display("Unknown file ordering: {}", text))] + #[snafu(display("Unknown file ordering: `{}`", text))] FileOrderUnknown { text: String }, #[snafu(display("I/O error at `{}`: {}", path.display(), source))] Filesystem { source: io::Error, path: PathBuf }, diff --git a/src/input_target.rs b/src/input_target.rs index 878b6e8..64e8991 100644 --- a/src/input_target.rs +++ b/src/input_target.rs @@ -7,13 +7,6 @@ pub(crate) enum InputTarget { } impl InputTarget { - pub(crate) fn resolve(&self, env: &Env) -> Result { - match self { - 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() diff --git a/src/output_target.rs b/src/output_target.rs index 38248cf..e695e80 100644 --- a/src/output_target.rs +++ b/src/output_target.rs @@ -1,6 +1,6 @@ use crate::common::*; -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Clone)] pub(crate) enum OutputTarget { Path(PathBuf), Stdout, diff --git a/src/path_ext.rs b/src/path_ext.rs index 02d799b..7d259fc 100644 --- a/src/path_ext.rs +++ b/src/path_ext.rs @@ -8,12 +8,23 @@ pub(crate) trait PathExt { impl PathExt for &Path { fn clean(self) -> PathBuf { + if self.components().count() <= 1 { + return self.to_owned(); + } + let mut components = Vec::new(); - for component in self.components() { + for component in self + .components() + .filter(|component| component != &Component::CurDir) + { if component == Component::ParentDir { - if let Some(Component::Normal(_)) = components.last() { - components.pop(); + match components.last() { + Some(Component::Normal(_)) => { + components.pop(); + } + Some(Component::ParentDir) | None => components.push(component), + _ => {} } } else { components.push(component); @@ -29,27 +40,50 @@ mod tests { use super::*; #[test] - fn clean() { - let cases = &[ - ("/", "foo", "/foo"), - ("/", ".", "/"), - ("/", "foo/./bar", "/foo/bar"), - ("/foo/./bar", ".", "/foo/bar"), - ("/bar", "/foo", "/foo"), - ("//foo", "bar//baz", "/foo/bar/baz"), - ("/", "..", "/"), - ("/", "/..", "/"), - ("/..", "", "/"), - ("/../../../..", "../../../", "/"), - ("/.", "./", "/"), - ("/foo/../", "bar", "/bar"), - ("/foo/bar", "..", "/foo"), - ("/foo/bar/", "..", "/foo"), - ]; - - for (prefix, suffix, want) in cases { + #[rustfmt::skip] + fn prefix_suffix() { + fn case(prefix: &str, suffix: &str, want: &str) { let have = Path::new(prefix).join(Path::new(suffix)).clean(); assert_eq!(have, Path::new(want)); } + + { + case("/", "foo", "/foo"); + case("/", "." , "/"); + case("/", "foo/./bar", "/foo/bar"); + case("/foo/./bar", ".", "/foo/bar"); + case("/bar", "/foo", "/foo"); + case("//foo", "bar//baz", "/foo/bar/baz"); + case("/", "..", "/"); + case("/", "/..", "/"); + case("/..", "", "/"); + case("/../../../..", "../../../", "/"); + case("/.", "./", "/"); + case("/foo/../", "bar", "/bar"); + case("/foo/bar", "..", "/foo"); + case("/foo/bar/", "..", "/foo"); + } + } + + #[test] + #[rustfmt::skip] + fn simple() { + fn case(path: &str, want: &str) { + assert_eq!(Path::new(path).clean(), Path::new(want)); + } + + case("./..", ".."); + case("./././.", "."); + case("./../.", ".."); + case("..", ".."); + case("", ""); + case("foo", "foo"); + case(".", "."); + case("foo/./bar", "foo/bar"); + case("/foo", "/foo"); + case("bar//baz", "bar/baz"); + case("/..", "/"); + case("../../../", "../../.."); + case("./", "."); } } diff --git a/src/subcommand/torrent/create.rs b/src/subcommand/torrent/create.rs index 4e6e2f5..406094a 100644 --- a/src/subcommand/torrent/create.rs +++ b/src/subcommand/torrent/create.rs @@ -1,6 +1,8 @@ use crate::common::*; +use create_content::CreateContent; use create_step::CreateStep; +mod create_content; mod create_step; #[derive(StructOpt)] @@ -242,12 +244,6 @@ 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 = match self.output { - Some(output) => Some(output.resolve(env)?), - None => None, - }; - let mut linter = Linter::new(); linter.allow(self.allowed_lints.iter().cloned()); @@ -268,100 +264,24 @@ impl Create { return Err(Error::PrivateTrackerless); } - CreateStep::Searching.print(env)?; + CreateStep::Searching { input: &self.input }.print(env)?; - let spinner = if env.err().is_styled_term() { - let style = ProgressStyle::default_spinner() - .template("{spinner:.green} {msg:.bold}…") - .tick_chars(consts::TICK_CHARS); + let content = CreateContent::from_create(&self, env)?; - Some(ProgressBar::new_spinner().with_style(style)) - } else { - None - }; + let output = content.output.resolve(env)?; - let files; - let piece_length; - let progress_bar; - let name; - let output = match &input { - InputTarget::Path(path) => { - let files_inner = Walker::new(&path) - .include_junk(self.include_junk) - .include_hidden(self.include_hidden) - .follow_symlinks(self.follow_symlinks) - .sort_by(self.sort_by) - .globs(&self.globs)? - .spinner(spinner) - .files()?; - - piece_length = self - .piece_length - .unwrap_or_else(|| PieceLengthPicker::from_content_size(files_inner.total_size())); - - let style = ProgressStyle::default_bar() - .template( - "{spinner:.green} ⟪{elapsed_precise}⟫ ⟦{bar:40.cyan}⟧ \ - {binary_bytes}/{binary_total_bytes} ⟨{binary_bytes_per_sec}, {eta}⟩", - ) - .tick_chars(consts::TICK_CHARS) - .progress_chars(consts::PROGRESS_CHARS); - - progress_bar = ProgressBar::new(files_inner.total_size().count()).with_style(style); - - let filename = path - .file_name() - .ok_or_else(|| Error::FilenameExtract { path: path.clone() })?; - - name = match &self.name { - Some(name) => name.clone(), - None => filename - .to_str() - .ok_or_else(|| Error::FilenameDecode { - filename: PathBuf::from(filename), - })? - .to_owned(), - }; - - files = Some(files_inner); - - output.unwrap_or_else(|| { - let mut torrent_name = name.to_owned(); - torrent_name.push_str(".torrent"); - - OutputTarget::Path(path.parent().unwrap().join(torrent_name)) - }) - } - - InputTarget::Stdin => { - files = None; - piece_length = self.piece_length.unwrap_or(Bytes::kib() * 256); - - let style = ProgressStyle::default_bar() - .template("{spinner:.green} ⟪{elapsed_precise}⟫ {binary_bytes} ⟨{binary_bytes_per_sec}⟩") - .tick_chars(consts::TICK_CHARS); - - progress_bar = ProgressBar::new_spinner().with_style(style); - - name = self - .name - .ok_or_else(|| Error::internal("Expected `--name` to be set when `--input -`."))?; - - output.ok_or_else(|| Error::internal("Expected `--output` to be set when `--input -`."))? - } - }; - - if piece_length.count() == 0 { + if content.piece_length.count() == 0 { return Err(Error::PieceLengthZero); } - if linter.is_denied(Lint::UnevenPieceLength) && !piece_length.count().is_power_of_two() { + if linter.is_denied(Lint::UnevenPieceLength) && !content.piece_length.count().is_power_of_two() + { return Err(Error::PieceLengthUneven { - bytes: piece_length, + bytes: content.piece_length, }); } - if linter.is_denied(Lint::SmallPieceLength) && piece_length.count() < 16 * 1024 { + if linter.is_denied(Lint::SmallPieceLength) && content.piece_length.count() < 16 * 1024 { return Err(Error::PieceLengthSmall); } @@ -395,28 +315,31 @@ impl Create { let hasher = Hasher::new( self.md5sum, - piece_length.as_piece_length()?.into_usize(), + content.piece_length.as_piece_length()?.into_usize(), if env.err().is_styled_term() { - Some(progress_bar) + Some(content.progress_bar) } else { None }, ); - let (mode, pieces) = if let Some(files) = files { + let (mode, pieces) = if let Some(files) = content.files { hasher.hash_files(&files)? } else { hasher.hash_stdin(&mut env.input())? }; - CreateStep::Writing { output: &output }.print(env)?; + CreateStep::Writing { + output: &content.output, + } + .print(env)?; let info = Info { source: self.source, - piece_length, + piece_length: content.piece_length, + name: content.name, mode, pieces, - name, private, }; @@ -463,12 +386,12 @@ impl Create { #[cfg(test)] { - if let InputTarget::Path(path) = &input { + if let InputTarget::Path(path) = &self.input { let deserialized = bendy::serde::de::from_bytes::(&bytes).unwrap(); assert_eq!(deserialized, metainfo); - let status = metainfo.verify(path, None)?; + let status = metainfo.verify(&env.resolve(path)?, None)?; status.print(env)?; @@ -562,7 +485,7 @@ mod tests { foo: "", } }; - env.run().unwrap(); + env.assert_ok(); let torrent = env.resolve("foo.torrent")?; let bytes = fs::read(torrent).unwrap(); let value = Value::from_bencode(&bytes).unwrap(); @@ -588,10 +511,11 @@ mod tests { }, } }; - env.run().unwrap(); - let metainfo = env.load_metainfo("../dir.torrent"); - assert_eq!(metainfo.info.name, "dir"); - assert_matches!(metainfo.info.mode, Mode::Multiple{files} if files.len() == 1); + env.assert_ok(); + // let metainfo = env.load_metainfo("../dir.torrent"); + // assert_eq!(metainfo.info.name, "dir"); + // assert_matches!(metainfo.info.mode, Mode::Multiple{files} if files.len() + // == 1); } #[test] @@ -614,7 +538,7 @@ mod tests { }, } }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("../../a.torrent"); assert_eq!(metainfo.info.name, "a"); assert_matches!(metainfo.info.mode, Mode::Multiple{files} if files.len() == 1); @@ -628,7 +552,7 @@ mod tests { foo: "", } }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.info.private, None); } @@ -641,7 +565,7 @@ mod tests { foo: "", } }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.info.private, Some(true)); } @@ -665,7 +589,7 @@ mod tests { foo: "", } }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.announce, Some("http://bar/".into())); assert!(metainfo.announce_list.is_none()); @@ -686,7 +610,7 @@ mod tests { foo: "", } }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_eq!( metainfo.announce.as_deref(), @@ -710,7 +634,7 @@ mod tests { foo: "", }, }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_eq!( metainfo.announce.as_deref(), @@ -736,7 +660,7 @@ mod tests { foo: "", }, }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.announce.as_deref(), Some("http://bar/")); assert_eq!( @@ -764,7 +688,7 @@ mod tests { foo: "", }, }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.announce.as_deref(), Some("http://bar/")); assert_eq!( @@ -791,7 +715,7 @@ mod tests { foo: "", }, }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.comment, None); } @@ -813,7 +737,7 @@ mod tests { foo: "", }, }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.comment.unwrap(), "Hello, world!"); } @@ -833,7 +757,7 @@ mod tests { foo: "", }, }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.info.piece_length, Bytes::from(16 * 2u32.pow(10))); } @@ -855,7 +779,7 @@ mod tests { foo: "", }, }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.info.piece_length, Bytes(64 * 1024)); } @@ -877,7 +801,7 @@ mod tests { foo: "", }, }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.info.piece_length, Bytes(512 * 1024)); } @@ -899,7 +823,7 @@ mod tests { foo: "", }, }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.info.name, "foo"); } @@ -923,7 +847,7 @@ mod tests { }, }, }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo/bar.torrent"); assert_eq!(metainfo.info.name, "bar"); } @@ -945,7 +869,7 @@ mod tests { foo: "", }, }; - env.run().unwrap(); + env.assert_ok(); env.load_metainfo("x.torrent"); } @@ -964,7 +888,7 @@ mod tests { foo: "", }, }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.created_by.unwrap(), consts::CREATED_BY_DEFAULT); } @@ -985,7 +909,7 @@ mod tests { foo: "", }, }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.created_by, None); } @@ -1005,7 +929,7 @@ mod tests { foo: "", }, }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.encoding, Some("UTF-8".into())); } @@ -1029,7 +953,7 @@ mod tests { .duration_since(SystemTime::UNIX_EPOCH) .unwrap() .as_secs(); - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert!(metainfo.creation_date.unwrap() < now + 10); assert!(metainfo.creation_date.unwrap() > now - 10); @@ -1051,7 +975,7 @@ mod tests { foo: "", }, }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.creation_date, None); } @@ -1075,7 +999,7 @@ mod tests { foo: "123", }, }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.info.pieces, PieceList::from_pieces(&["123"])); assert_eq!( @@ -1106,7 +1030,7 @@ mod tests { foo: "1234", }, }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.info.pieces, PieceList::from_pieces(&["1234"])); assert_eq!( @@ -1137,7 +1061,7 @@ mod tests { foo: "1234", }, }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.info.pieces, PieceList::from_pieces(&["12", "34"])); assert_eq!( @@ -1172,7 +1096,7 @@ mod tests { }, }, }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("dir.torrent"); assert_eq!(metainfo.info.pieces, PieceList::from_pieces(&["56781234"])); assert_eq!( @@ -1209,7 +1133,7 @@ mod tests { foo: "bar", }, }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.info.pieces, PieceList::from_pieces(&["bar"])); assert_eq!( @@ -1240,7 +1164,7 @@ mod tests { foo: "bar", }, }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_eq!( metainfo.info.pieces, @@ -1270,7 +1194,7 @@ mod tests { foo: "", }, }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.info.pieces.count(), 0); assert_eq!( @@ -1297,7 +1221,7 @@ mod tests { foo: {}, }, }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.info.pieces.count(), 0); assert_eq!(metainfo.info.mode, Mode::Multiple { files: Vec::new() }) @@ -1321,7 +1245,7 @@ mod tests { }, }, }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.info.pieces, PieceList::from_pieces(&["bar"])); match metainfo.info.mode { @@ -1356,7 +1280,7 @@ mod tests { }, }, }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.info.pieces, PieceList::from_pieces(&["bar"])); match metainfo.info.mode { @@ -1466,7 +1390,7 @@ mod tests { foo: {}, }, }; - env.run().unwrap(); + env.assert_ok(); env.load_metainfo("foo.torrent"); } @@ -1529,7 +1453,7 @@ mod tests { foo: {}, } }; - env.run().unwrap(); + env.assert_ok(); env.load_metainfo("foo.torrent"); } @@ -1554,7 +1478,7 @@ mod tests { fs::write(dir.join("a"), "abc").unwrap(); fs::write(dir.join("x"), "xyz").unwrap(); fs::write(dir.join("h"), "hij").unwrap(); - env.run().unwrap(); + env.assert_ok(); assert_eq!(env.out(), ""); Ok(()) } @@ -1581,7 +1505,7 @@ mod tests { fs::write(dir.join("a"), "abc").unwrap(); fs::write(dir.join("x"), "xyz").unwrap(); fs::write(dir.join("h"), "hij").unwrap(); - env.run().unwrap(); + env.assert_ok(); let have = env.out(); #[rustfmt::skip] let want = format!( @@ -1624,7 +1548,8 @@ Content Size 9 bytes foo: "", }, }; - env.run().unwrap(); + env.assert_ok(); + let bytes = env.out_bytes(); Metainfo::from_bytes(&bytes); } @@ -1670,7 +1595,7 @@ Content Size 9 bytes "foo.torrent": "foo", }, }; - env.run().unwrap(); + env.assert_ok(); env.load_metainfo("foo.torrent"); } @@ -1692,7 +1617,7 @@ Content Size 9 bytes }, }, }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_matches!( metainfo.info.mode, @@ -1720,7 +1645,7 @@ Content Size 9 bytes }, }, }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_matches!( metainfo.info.mode, @@ -1764,7 +1689,7 @@ Content Size 9 bytes fs::remove_file(env.resolve("foo/hidden")?).unwrap(); } - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); @@ -1810,7 +1735,7 @@ Content Size 9 bytes .unwrap(); } - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_matches!( metainfo.info.mode, @@ -1867,7 +1792,7 @@ Content Size 9 bytes tree: {}, }; populate_symlinks(&env)?; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_matches!( metainfo.info.mode, @@ -1894,7 +1819,7 @@ Content Size 9 bytes tree: {}, }; populate_symlinks(&env)?; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); let mut pieces = PieceList::new(); pieces.push(Sha1::from("barbaz").digest().into()); @@ -2021,7 +1946,7 @@ Content Size 9 bytes .unwrap(); } - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_matches!( metainfo.info.mode, @@ -2052,7 +1977,7 @@ Content Size 9 bytes }, } }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_matches!( metainfo.info.mode, @@ -2085,7 +2010,7 @@ Content Size 9 bytes } }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_matches!( metainfo.info.mode, @@ -2117,7 +2042,7 @@ Content Size 9 bytes }, } }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_matches!( metainfo.info.mode, @@ -2149,7 +2074,7 @@ Content Size 9 bytes }, } }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_matches!( metainfo.info.mode, @@ -2183,7 +2108,7 @@ Content Size 9 bytes }, } }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_matches!( metainfo.info.mode, @@ -2207,7 +2132,7 @@ Content Size 9 bytes foo: "", } }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert!(metainfo.nodes.is_none()); } @@ -2253,7 +2178,7 @@ Content Size 9 bytes foo: "", }, }; - env.run().unwrap(); + env.assert_ok(); let metainfo = env.load_metainfo("foo.torrent"); assert_eq!( metainfo.nodes, @@ -2267,34 +2192,6 @@ Content Size 9 bytes ); } - #[test] - fn create_progress_messages() -> Result<()> { - let mut env = TestEnvBuilder::new() - .arg_slice(&[ - "imdl", - "torrent", - "create", - "--input", - "foo", - "--announce", - "http://bar", - ]) - .build(); - - 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.run().unwrap(); - - assert_eq!(env.err(), want); - Ok(()) - } - #[test] fn private_requires_announce() { let mut env = test_env! { @@ -2714,4 +2611,227 @@ Content Size 9 bytes } ); } + + #[test] + fn create_messages_path() -> Result<()> { + let mut env = test_env! { + args: [ + "torrent", + "create", + "--input", + "foo", + "--announce", + "https://bar", + ], + tree: { + foo: "", + } + }; + + let want = format!( + "[1/3] \u{1F9FF} Searching `foo` for files…\n[2/3] \u{1F9EE} Hashing pieces…\n[3/3] \ + \u{1F4BE} Writing metainfo to `foo.torrent`…\n\u{2728}\u{2728} Done! \u{2728}\u{2728}\n", + ); + + env.assert_ok(); + + assert_eq!(env.err(), want); + + Ok(()) + } + + #[test] + fn create_messages_subdir() -> Result<()> { + let mut env = test_env! { + args: [ + "torrent", + "create", + "--input", + "foo/bar", + "--announce", + "https://bar", + ], + tree: { + foo: { + bar: "", + }, + } + }; + + let want = format!( + "[1/3] \u{1F9FF} Searching `foo/bar` 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", + Path::new("foo").join("bar.torrent").display(), + ); + + env.assert_ok(); + + assert_eq!(env.err(), want); + + Ok(()) + } + + #[test] + fn create_messages_dot() -> Result<()> { + let mut env = test_env! { + args: [ + "torrent", + "create", + "--input", + ".", + "--announce", + "https://bar", + ], + cwd: "dir", + tree: { + dir: { + foo: "", + }, + } + }; + + env.assert_ok(); + let metainfo = env.load_metainfo("../dir.torrent"); + assert_eq!(metainfo.info.name, "dir"); + assert_matches!(metainfo.info.mode, Mode::Multiple{files} if files.len() == 1); + + 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", + Path::new("..").join("dir.torrent").display(), + ); + + assert_eq!(env.err(), want); + + Ok(()) + } + + #[test] + fn create_messages_dot_dot() -> Result<()> { + let mut env = test_env! { + args: [ + "torrent", + "create", + "--input", + "..", + "--announce", + "https://bar", + ], + cwd: "a/b", + tree: { + a: { + b: { + foo: "", + }, + }, + } + }; + env.assert_ok(); + let metainfo = env.load_metainfo("../../a.torrent"); + assert_eq!(metainfo.info.name, "a"); + assert_matches!(metainfo.info.mode, Mode::Multiple{files} if files.len() == 1); + + 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", + Path::new("..").join("..").join("a.torrent").display(), + ); + + assert_eq!(env.err(), want); + + Ok(()) + } + + #[test] + fn create_messages_absolute() -> Result<()> { + let dir = TempDir::new().unwrap(); + + let input = dir.path().join("foo"); + + fs::write(&input, "").unwrap(); + + let mut env = test_env! { + args: [ + "torrent", + "create", + "--input", + &input, + "--announce", + "https://bar", + ], + tree: { + } + }; + + let torrent = dir.path().join("foo.torrent"); + + env.assert_ok(); + + let metainfo = env.load_metainfo(&torrent); + assert_eq!(metainfo.info.name, "foo"); + + 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", + input.display(), + torrent.display(), + ); + + assert_eq!(env.err(), want); + + Ok(()) + } + + #[test] + fn create_messages_stdio() -> Result<()> { + let dir = TempDir::new().unwrap(); + + let input = dir.path().join("foo"); + + fs::write(&input, "").unwrap(); + + let mut env = test_env! { + args: [ + "torrent", + "create", + "--input", + "-", + "--announce", + "https://bar", + "--name", + "foo", + "--output", + "-", + "--md5", + ], + input: "hello", + tree: { + } + }; + + env.assert_ok(); + + let bytes = env.out_bytes(); + let metainfo = Metainfo::from_bytes(&bytes); + + assert_eq!(metainfo.info.pieces, PieceList::from_pieces(&["hello"])); + + assert_eq!( + metainfo.info.mode, + Mode::Single { + length: Bytes(5), + md5sum: Some(Md5Digest::from_data("hello")), + } + ); + + let want = format!( + "[1/3] \u{1F9FF} Creating single-file torrent from standard input…\n[2/3] \u{1F9EE} Hashing \ + pieces…\n[3/3] \u{1F4BE} Writing metainfo to standard output…\n\u{2728}\u{2728} Done! \ + \u{2728}\u{2728}\n", + ); + + assert_eq!(env.err(), want); + + Ok(()) + } } diff --git a/src/subcommand/torrent/create/create_content.rs b/src/subcommand/torrent/create/create_content.rs new file mode 100644 index 0000000..17cc1c3 --- /dev/null +++ b/src/subcommand/torrent/create/create_content.rs @@ -0,0 +1,153 @@ +use crate::common::*; + +use super::Create; + +pub(crate) struct CreateContent { + pub(crate) files: Option, + pub(crate) piece_length: Bytes, + pub(crate) progress_bar: ProgressBar, + pub(crate) name: String, + pub(crate) output: OutputTarget, +} + +impl CreateContent { + pub(crate) fn from_create(create: &Create, env: &mut Env) -> Result { + match &create.input { + InputTarget::Path(path) => { + let spinner = if env.err().is_styled_term() { + let style = ProgressStyle::default_spinner() + .template("{spinner:.green} {msg:.bold}…") + .tick_chars(consts::TICK_CHARS); + + Some(ProgressBar::new_spinner().with_style(style)) + } else { + None + }; + + let files = Walker::new(&env.resolve(path)?) + .include_junk(create.include_junk) + .include_hidden(create.include_hidden) + .follow_symlinks(create.follow_symlinks) + .sort_by(create.sort_by.clone()) + .globs(&create.globs)? + .spinner(spinner) + .files()?; + + let piece_length = create + .piece_length + .unwrap_or_else(|| PieceLengthPicker::from_content_size(files.total_size())); + + let style = ProgressStyle::default_bar() + .template( + "{spinner:.green} ⟪{elapsed_precise}⟫ ⟦{bar:40.cyan}⟧ \ + {binary_bytes}/{binary_total_bytes} ⟨{binary_bytes_per_sec}, {eta}⟩", + ) + .tick_chars(consts::TICK_CHARS) + .progress_chars(consts::PROGRESS_CHARS); + + let progress_bar = ProgressBar::new(files.total_size().count()).with_style(style); + + let resolved = env.resolve(path)?; + + let filename = resolved + .file_name() + .ok_or_else(|| Error::FilenameExtract { path: path.clone() })?; + + let name = match &create.name { + Some(name) => name.clone(), + None => filename + .to_str() + .ok_or_else(|| Error::FilenameDecode { + filename: PathBuf::from(filename), + })? + .to_owned(), + }; + + let output = create + .output + .clone() + .unwrap_or_else(|| OutputTarget::Path(Self::torrent_path(path, &name))); + + Ok(Self { + files: Some(files), + piece_length, + progress_bar, + name, + output, + }) + } + + InputTarget::Stdin => { + let files = None; + let piece_length = create.piece_length.unwrap_or(Bytes::kib() * 256); + + let style = ProgressStyle::default_bar() + .template("{spinner:.green} ⟪{elapsed_precise}⟫ {binary_bytes} ⟨{binary_bytes_per_sec}⟩") + .tick_chars(consts::TICK_CHARS); + + let progress_bar = ProgressBar::new_spinner().with_style(style); + + let name = create + .name + .clone() + .ok_or_else(|| Error::internal("Expected `--name` to be set when `--input -`."))?; + + let output = create + .output + .clone() + .ok_or_else(|| Error::internal("Expected `--output` to be set when `--input -`."))?; + + Ok(Self { + files, + piece_length, + progress_bar, + name, + output, + }) + } + } + } + + fn torrent_path(input: &Path, name: &str) -> PathBuf { + input.join("..").clean().join(format!("{}.torrent", name)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use pretty_assertions::assert_eq; + + #[test] + fn torrent_path() { + fn case(path: &str, name: &str, expected: impl AsRef) { + let expected = expected.as_ref(); + assert_eq!( + CreateContent::torrent_path(Path::new(path), name), + expected, + "{} + {} != {}", + path, + name, + expected.display(), + ); + } + + use path::Component; + + case("foo", "foo", "foo.torrent"); + case("foo", "foo", "foo.torrent"); + case("foo", "bar", "bar.torrent"); + case("foo/bar", "foo", Path::new("foo").join("foo.torrent")); + case("foo/bar", "bar", Path::new("foo").join("bar.torrent")); + case( + "/foo/bar", + "bar", + Path::new(&Component::RootDir) + .join("foo") + .join("bar.torrent"), + ); + case(".", "foo", Path::new("..").join("foo.torrent")); + case("..", "foo", Path::new("..").join("..").join("foo.torrent")); + } +} diff --git a/src/subcommand/torrent/create/create_step.rs b/src/subcommand/torrent/create/create_step.rs index 002c5d4..2ea6073 100644 --- a/src/subcommand/torrent/create/create_step.rs +++ b/src/subcommand/torrent/create/create_step.rs @@ -1,16 +1,16 @@ use crate::common::*; #[derive(Clone, Copy)] -pub(crate) enum CreateStep<'output> { - Searching, +pub(crate) enum CreateStep<'a> { + Searching { input: &'a InputTarget }, Hashing, - Writing { output: &'output OutputTarget }, + Writing { output: &'a OutputTarget }, } -impl<'output> Step for CreateStep<'output> { +impl<'a> Step for CreateStep<'a> { fn n(&self) -> usize { match self { - Self::Searching => 1, + Self::Searching { .. } => 1, Self::Hashing => 2, Self::Writing { .. } => 3, } @@ -18,7 +18,7 @@ impl<'output> Step for CreateStep<'output> { fn symbol(&self) -> &str { match self { - Self::Searching => "\u{1F9FF}", + Self::Searching { .. } => "\u{1F9FF}", Self::Hashing => "\u{1F9EE}", Self::Writing { .. } => "\u{1F4BE}", } @@ -30,7 +30,11 @@ impl<'output> Step for CreateStep<'output> { fn write_message(&self, write: &mut dyn Write) -> io::Result<()> { match self { - Self::Searching => write!(write, "Searching for files…"), + Self::Searching { input } => match input { + InputTarget::Path(path) => write!(write, "Searching `{}` for files…", path.display()), + InputTarget::Stdin => write!(write, "Creating single-file torrent from standard input…"), + }, + Self::Hashing => write!(write, "Hashing pieces…"), Self::Writing { output } => write!(write, "Writing metainfo to {}…", output), }