diff --git a/CHANGELOG.md b/CHANGELOG.md index a80d90c..97d8057 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Categories Used: ## [Unreleased](https://github.com/ouch-org/ouch/compare/0.6.1...HEAD) ### New Features +- Add `--no-smart-unpack` flag to decompression command to disable smart unpack [\#809](https://github.com/ouch-org/ouch/pull/809) ([talis-fb](https://github.com/talis-fb)) ### Improvements ### Bug Fixes ### Tweaks diff --git a/src/cli/args.rs b/src/cli/args.rs index c72d5d3..b28156c 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -100,6 +100,10 @@ pub enum Subcommand { /// Remove the source file after successful decompression #[arg(short = 'r', long)] remove: bool, + + /// Disable Smart Unpack + #[arg(long)] + no_smart_unpack: bool, }, /// List contents of an archive #[command(visible_aliases = ["l", "ls"])] @@ -156,6 +160,7 @@ mod tests { files: vec!["\x00\x11\x22".into()], output_dir: None, remove: false, + no_smart_unpack: false, }, } } @@ -169,6 +174,7 @@ mod tests { files: to_paths(["file.tar.gz"]), output_dir: None, remove: false, + no_smart_unpack: false, }, ..mock_cli_args() } @@ -180,6 +186,7 @@ mod tests { files: to_paths(["file.tar.gz"]), output_dir: None, remove: false, + no_smart_unpack: false, }, ..mock_cli_args() } @@ -191,6 +198,7 @@ mod tests { files: to_paths(["a", "b", "c"]), output_dir: None, remove: false, + no_smart_unpack: false, }, ..mock_cli_args() } diff --git a/src/commands/decompress.rs b/src/commands/decompress.rs index 036d1d2..5c264ef 100644 --- a/src/commands/decompress.rs +++ b/src/commands/decompress.rs @@ -32,6 +32,7 @@ pub struct DecompressOptions<'a> { pub output_dir: &'a Path, pub output_file_path: PathBuf, pub is_output_dir_provided: bool, + pub is_smart_unpack: bool, pub question_policy: QuestionPolicy, pub quiet: bool, pub password: Option<&'a [u8]>, @@ -75,6 +76,7 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> { &options.output_file_path, options.question_policy, options.is_output_dir_provided, + options.is_smart_unpack, )? { files } else { @@ -153,6 +155,7 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> { &options.output_file_path, options.question_policy, options.is_output_dir_provided, + options.is_smart_unpack, )? { files } else { @@ -187,6 +190,7 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> { &options.output_file_path, options.question_policy, options.is_output_dir_provided, + options.is_smart_unpack, )? { files } else { @@ -219,6 +223,7 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> { &options.output_file_path, options.question_policy, options.is_output_dir_provided, + options.is_smart_unpack, )? { files } else { @@ -261,6 +266,7 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> { &options.output_file_path, options.question_policy, options.is_output_dir_provided, + options.is_smart_unpack, )? { files } else { @@ -296,12 +302,19 @@ fn execute_decompression( output_file_path: &Path, question_policy: QuestionPolicy, is_output_dir_provided: bool, + is_smart_unpack: bool, ) -> crate::Result> { - if is_output_dir_provided { - unpack(unpack_fn, output_dir, question_policy) - } else { - smart_unpack(unpack_fn, output_dir, output_file_path, question_policy) + if is_smart_unpack { + return smart_unpack(unpack_fn, output_dir, output_file_path, question_policy); } + + let target_output_dir = if is_output_dir_provided { + output_dir + } else { + output_file_path + }; + + unpack(unpack_fn, target_output_dir, question_policy) } /// Unpacks an archive creating the output directory, this function will create the output_dir diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 3e8718b..dadfd95 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -148,6 +148,7 @@ pub fn run( files, output_dir, remove, + no_smart_unpack, } => { let mut output_paths = vec![]; let mut formats = vec![]; @@ -176,9 +177,11 @@ pub fn run( check::check_missing_formats_when_decompressing(&files, &formats)?; + let is_output_dir_provided = output_dir.is_some(); + let is_smart_unpack = !is_output_dir_provided && !no_smart_unpack; + // The directory that will contain the output files // We default to the current directory if the user didn't specify an output directory with --dir - let is_output_dir_provided = output_dir.is_some(); let output_dir = if let Some(dir) = output_dir { utils::create_dir_if_non_existent(&dir)?; dir @@ -200,9 +203,10 @@ pub fn run( decompress_file(DecompressOptions { input_file_path: input_path, formats, + is_output_dir_provided, output_dir: &output_dir, output_file_path, - is_output_dir_provided, + is_smart_unpack, question_policy, quiet: args.quiet, password: args.password.as_deref().map(|str| { diff --git a/tests/integration.rs b/tests/integration.rs index 52413a7..ea82326 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -367,6 +367,201 @@ fn multiple_files_with_conflict_and_choice_to_rename_with_already_a_renamed( assert_same_directory(src_files_path, dest_files_path_renamed.join("src_files"), false); } +#[proptest(cases = 25)] +fn smart_unpack_with_single_file( + ext: DirectoryExtension, + #[any(size_range(0..1).lift())] extra_extensions: Vec, +) { + let temp_dir = tempdir().unwrap(); + let root_path = temp_dir.path(); + + let src_files_path = root_path.join("src_files"); + fs::create_dir_all(&src_files_path).unwrap(); + + let files_path = ["file1.txt"] + .into_iter() + .map(|f| src_files_path.join(f)) + .inspect(|path| { + let mut file = fs::File::create(path).unwrap(); + file.write_all("Some content".as_bytes()).unwrap(); + }) + .collect::>(); + + let archive = &root_path.join(format!("archive.{}", merge_extensions(&ext, extra_extensions))); + + crate::utils::cargo_bin() + .arg("compress") + .args(files_path) + .arg(archive) + .assert() + .success(); + + let output_file = root_path.join("file1.txt"); + assert!(!output_file.exists()); + + // Decompress the archive with Smart Unpack + crate::utils::cargo_bin() + .current_dir(root_path) + .arg("decompress") + .arg(archive) + .assert() + .success(); + + assert!(output_file.exists()); + + let output_content = fs::read_to_string(&output_file).unwrap(); + assert_eq!(output_content, "Some content"); +} + +#[proptest(cases = 25)] +fn smart_unpack_with_multiple_files( + ext: DirectoryExtension, + #[any(size_range(0..1).lift())] extra_extensions: Vec, +) { + let temp_dir = tempdir().unwrap(); + let root_path = temp_dir.path(); + + let src_files_path = root_path.join("src_files"); + fs::create_dir_all(&src_files_path).unwrap(); + + ["file1.txt", "file2.txt", "file3.txt", "file4.txt", "file5.txt"] + .into_iter() + .map(|f| src_files_path.join(f)) + .for_each(|path| { + let mut file = fs::File::create(&path).unwrap(); + file.write_all("Some content".as_bytes()).unwrap(); + }); + + let input_files = src_files_path + .read_dir() + .unwrap() + .map(|entry| entry.unwrap().path()) + .collect::>(); + + let archive = &root_path.join(format!("archive.{}", merge_extensions(&ext, extra_extensions))); + + let output_path = root_path.join("archive"); + assert!(!output_path.exists()); + + crate::utils::cargo_bin() + .arg("compress") + .args(input_files) + .arg(archive) + .assert() + .success(); + + crate::utils::cargo_bin() + .current_dir(root_path) + .arg("decompress") + .arg(archive) + .assert() + .success(); + + assert!(output_path.exists(), "Output directory does not exist"); + + assert_same_directory(src_files_path, output_path, false); +} + +#[proptest(cases = 25)] +fn no_smart_unpack_with_single_file( + ext: DirectoryExtension, + #[any(size_range(0..1).lift())] extra_extensions: Vec, +) { + let temp_dir = tempdir().unwrap(); + let root_path = temp_dir.path(); + + let src_files_path = root_path.join("src_files"); + fs::create_dir_all(&src_files_path).unwrap(); + + ["file1.txt"] + .into_iter() + .map(|f| src_files_path.join(f)) + .for_each(|path| { + let mut file = fs::File::create(&path).unwrap(); + file.write_all("Some content".as_bytes()).unwrap(); + }); + + let input_files = src_files_path + .read_dir() + .unwrap() + .map(|entry| entry.unwrap().path()) + .collect::>(); + + let archive = &root_path.join(format!("archive.{}", merge_extensions(&ext, extra_extensions))); + + let output_path = root_path.join("archive"); + assert!(!output_path.exists()); + + crate::utils::cargo_bin() + .arg("compress") + .args(input_files) + .arg(archive) + .assert() + .success(); + + crate::utils::cargo_bin() + .current_dir(root_path) + .arg("decompress") + .arg("--no-smart-unpack") + .arg(archive) + .assert() + .success(); + + assert!(output_path.exists(), "Output directory does not exist"); + + assert_same_directory(src_files_path, output_path, false); +} + +#[proptest(cases = 25)] +fn no_smart_unpack_with_multiple_files( + ext: DirectoryExtension, + #[any(size_range(0..1).lift())] extra_extensions: Vec, +) { + let temp_dir = tempdir().unwrap(); + let root_path = temp_dir.path(); + + let src_files_path = root_path.join("src_files"); + fs::create_dir_all(&src_files_path).unwrap(); + + ["file1.txt", "file2.txt", "file3.txt", "file4.txt", "file5.txt"] + .into_iter() + .map(|f| src_files_path.join(f)) + .for_each(|path| { + let mut file = fs::File::create(&path).unwrap(); + file.write_all("Some content".as_bytes()).unwrap(); + }); + + let input_files = src_files_path + .read_dir() + .unwrap() + .map(|entry| entry.unwrap().path()) + .collect::>(); + + let archive = &root_path.join(format!("archive.{}", merge_extensions(&ext, extra_extensions))); + + let output_path = root_path.join("archive"); + assert!(!output_path.exists()); + + crate::utils::cargo_bin() + .arg("compress") + .args(input_files) + .arg(archive) + .assert() + .success(); + + crate::utils::cargo_bin() + .current_dir(root_path) + .arg("decompress") + .arg("--no-smart-unpack") + .arg(archive) + .assert() + .success(); + + assert!(output_path.exists(), "Output directory does not exist"); + + assert_same_directory(src_files_path, output_path, false); +} + #[proptest(cases = 25)] fn multiple_files_with_disabled_smart_unpack_by_dir( ext: DirectoryExtension, @@ -490,10 +685,9 @@ fn symlink_pack_and_unpack( let mut files_path = ["file1.txt", "file2.txt", "file3.txt", "file4.txt", "file5.txt"] .into_iter() .map(|f| src_files_path.join(f)) - .map(|path| { - let mut file = fs::File::create(&path).unwrap(); + .inspect(|path| { + let mut file = fs::File::create(path).unwrap(); file.write_all("Some content".as_bytes()).unwrap(); - path }) .collect::>();