#[macro_use]
mod utils;
use std::{
io::Write,
iter::once,
path::{Path, PathBuf},
};
use anyhow::Result;
use bstr::ByteSlice;
use fs_err as fs;
use itertools::Itertools;
use memchr::memmem;
use parse_display::Display;
use pretty_assertions::assert_eq;
use proptest::sample::size_range;
use rand::{Rng, SeedableRng, rngs::SmallRng};
use strum::IntoEnumIterator as _;
use test_strategy::{Arbitrary, proptest};
use crate::utils::{assert_same_directory, testdir, write_random_content};
#[derive(Arbitrary, Clone, Copy, Debug, Display)]
#[display(style = "lowercase")]
enum DirectoryExtension {
#[display("7z")]
SevenZ,
Tar,
Tbz,
Tbz2,
#[cfg(feature = "bzip3")]
Tbz3,
Tgz,
Tlz,
Tlz4,
Tlzma,
Tsz,
Txz,
Tzst,
Zip,
}
#[derive(Arbitrary, Clone, Copy, Debug, Display, strum::EnumIter)]
#[display(style = "lowercase")]
enum MainDirectoryExtension {
#[display("7z")]
SevenZ,
Tar,
Zip,
}
#[derive(Arbitrary, Debug, Display)]
#[display(style = "lowercase")]
enum FileExtension {
Bz,
Bz2,
#[cfg(feature = "bzip3")]
Bz3,
Gz,
Lz,
Lz4,
Lzma,
Sz,
Xz,
Zst,
Br,
}
#[derive(Arbitrary, Debug, Display)]
#[display("{0}")]
enum Extension {
Directory(DirectoryExtension),
File(FileExtension),
}
fn merge_extensions(ext: impl ToString, exts: &[FileExtension]) -> String {
once(ext.to_string())
.chain(exts.iter().map(|x| x.to_string()))
.collect::<Vec<_>>()
.join(".")
}
fn create_random_files(dir: impl Into<PathBuf>, depth: u8, rng: &mut SmallRng) {
if depth == 0 {
return;
}
let dir = &dir.into();
for _ in 0..rng.gen_range(0..=4u32) {
write_random_content(
&mut tempfile::Builder::new().tempfile_in(dir).unwrap().keep().unwrap().0,
rng,
);
}
for _ in 0..rng.gen_range(0..=2u32) {
create_random_files(tempfile::tempdir_in(dir).unwrap().into_path(), depth - 1, rng);
}
}
fn create_n_random_files(n: usize, dir: impl Into<PathBuf>, rng: &mut SmallRng) {
let dir: &PathBuf = &dir.into();
for _ in 0..n {
write_random_content(
&mut tempfile::Builder::new()
.prefix("file")
.tempfile_in(dir)
.unwrap()
.keep()
.unwrap()
.0,
rng,
);
}
}
#[proptest(cases = 200)]
fn single_empty_file(ext: Extension, #[any(size_range(0..8).lift())] exts: Vec<FileExtension>) {
let (_tempdir, dir) = testdir().unwrap();
let before = &dir.join("before");
fs::create_dir(before).unwrap();
let before_file = &before.join("file");
let archive = &dir.join(format!("file.{}", merge_extensions(ext, &exts)));
let after = &dir.join("after");
fs::write(before_file, []).unwrap();
ouch!("-A", "c", before_file, archive);
ouch!("-A", "d", archive, "-d", after);
assert_same_directory(before, after, false);
}
#[proptest(cases = 150)]
fn single_file(
ext: Extension,
#[any(size_range(0..6).lift())] exts: Vec<FileExtension>,
#[cfg_attr(not(any(target_arch = "arm", target_abi = "eabihf")), strategy(proptest::option::of(0i16..12)))]
#[cfg_attr(target_arch = "arm", strategy(proptest::option::of(0i16..6)))]
level: Option<i16>,
) {
let (_tempdir, dir) = testdir().unwrap();
let before = &dir.join("before");
fs::create_dir(before).unwrap();
let before_file = &before.join("file");
let archive = &dir.join(format!("file.{}", merge_extensions(ext, &exts)));
let after = &dir.join("after");
write_random_content(
&mut fs::File::create(before_file).unwrap(),
&mut SmallRng::from_entropy(),
);
if let Some(level) = level {
ouch!("-A", "c", "-l", level.to_string(), before_file, archive);
} else {
ouch!("-A", "c", before_file, archive);
}
ouch!("-A", "d", archive, "-d", after);
assert_same_directory(before, after, false);
}
#[proptest(cases = 200)]
fn single_file_stdin(
ext: Extension,
#[any(size_range(0..8).lift())] exts: Vec<FileExtension>,
#[cfg_attr(not(any(target_arch = "arm", target_abi = "eabihf")), strategy(proptest::option::of(0i16..12)))]
#[cfg_attr(target_arch = "arm", strategy(proptest::option::of(0i16..6)))]
level: Option<i16>,
) {
let (_tempdir, dir) = testdir().unwrap();
let before = &dir.join("before");
fs::create_dir(before).unwrap();
let before_file = &before.join("file");
let format = merge_extensions(&ext, &exts);
let archive = &dir.join(format!("file.{format}"));
let after = &dir.join("after");
write_random_content(
&mut fs::File::create(before_file).unwrap(),
&mut SmallRng::from_entropy(),
);
if let Some(level) = level {
ouch!("-A", "c", "-l", level.to_string(), before_file, archive);
} else {
ouch!("-A", "c", before_file, archive);
}
crate::utils::cargo_bin()
.args(["-A", "-y", "d", "-", "-d", after.to_str().unwrap(), "--format", &format])
.pipe_stdin(archive)
.unwrap()
.assert()
.success();
match ext {
Extension::Directory(_) => {}
Extension::File(_) => fs::rename(before_file, before_file.with_file_name("ouch-output")).unwrap(),
};
assert_same_directory(before, after, false);
}
#[proptest(cases = 25)]
fn multiple_files(
ext: DirectoryExtension,
#[any(size_range(0..1).lift())] extra_extensions: Vec<FileExtension>,
#[strategy(0u8..3)] depth: u8,
) {
let (_tempdir, dir) = testdir().unwrap();
let before = &dir.join("before");
let before_dir = &before.join("dir");
fs::create_dir_all(before_dir).unwrap();
let archive = &dir.join(format!("archive.{}", merge_extensions(ext, &extra_extensions)));
let after = &dir.join("after");
create_random_files(before_dir, depth, &mut SmallRng::from_entropy());
ouch!("-A", "c", before_dir, archive);
ouch!("-A", "d", archive, "-d", after);
assert_same_directory(before, after, !matches!(ext, DirectoryExtension::Zip));
}
#[proptest(cases = 25)]
fn multiple_files_with_conflict_and_choice_to_overwrite(
ext: DirectoryExtension,
#[any(size_range(0..1).lift())] extra_extensions: Vec<FileExtension>,
#[strategy(0u8..3)] depth: u8,
) {
let (_tempdir, dir) = testdir().unwrap();
let before = &dir.join("before");
let before_dir = &before.join("dir");
fs::create_dir_all(before_dir).unwrap();
create_random_files(before_dir, depth, &mut SmallRng::from_entropy());
let after = &dir.join("after");
let after_dir = &after.join("dir");
fs::create_dir_all(after_dir).unwrap();
create_random_files(after_dir, depth, &mut SmallRng::from_entropy());
let archive = &dir.join(format!("archive.{}", merge_extensions(ext, &extra_extensions)));
ouch!("-A", "c", before_dir, archive);
crate::utils::cargo_bin()
.arg("decompress")
.arg(archive)
.arg("-d")
.arg(after)
.write_stdin("y")
.assert()
.success();
assert_same_directory(before, after, false);
}
#[proptest(cases = 25)]
fn multiple_files_with_conflict_and_choice_to_not_overwrite(
ext: DirectoryExtension,
#[any(size_range(0..1).lift())] extra_extensions: Vec<FileExtension>,
#[strategy(0u8..3)] depth: u8,
) {
let (_tempdir, dir) = testdir().unwrap();
let before = &dir.join("before");
let before_dir = &before.join("dir");
fs::create_dir_all(before_dir).unwrap();
create_random_files(before_dir, depth, &mut SmallRng::from_entropy());
let after = &dir.join("after");
let after_dir = &after.join("dir");
fs::create_dir_all(after_dir).unwrap();
let after_backup = &dir.join("after_backup");
let after_backup_dir = &after_backup.join("dir");
fs::create_dir_all(after_backup_dir).unwrap();
fs::write(after_dir.join("something.txt"), "Some content").unwrap();
fs::copy(after_dir.join("something.txt"), after_backup_dir.join("something.txt")).unwrap();
let archive = &dir.join(format!("archive.{}", merge_extensions(ext, &extra_extensions)));
ouch!("-A", "c", before_dir, archive);
crate::utils::cargo_bin()
.arg("decompress")
.arg(archive)
.arg("-d")
.arg(after)
.arg("--no")
.assert()
.success();
assert_same_directory(after, after_backup, false);
}
#[proptest(cases = 25)]
fn multiple_files_with_conflict_and_choice_to_rename(
ext: DirectoryExtension,
#[any(size_range(0..1).lift())] extra_extensions: Vec<FileExtension>,
) {
let (_tempdir, root_path) = testdir().unwrap();
let src_files_path = root_path.join("src_files");
fs::create_dir_all(&src_files_path).unwrap();
create_n_random_files(5, &src_files_path, &mut SmallRng::from_entropy());
let dest_files_path = root_path.join("dest_files");
fs::create_dir_all(&dest_files_path).unwrap();
create_n_random_files(5, &dest_files_path, &mut SmallRng::from_entropy());
let archive = &root_path.join(format!("archive.{}", merge_extensions(ext, &extra_extensions)));
ouch!("-A", "c", &src_files_path, archive);
let dest_files_path_renamed = &root_path.join("dest_files_1");
assert_eq!(false, dest_files_path_renamed.exists());
crate::utils::cargo_bin()
.arg("decompress")
.arg(archive)
.arg("-d")
.arg(&dest_files_path)
.write_stdin("r")
.assert()
.success();
assert_same_directory(src_files_path, dest_files_path_renamed.join("src_files"), false);
}
#[proptest(cases = 25)]
fn multiple_files_with_conflict_and_choice_to_rename_with_already_a_renamed(
ext: DirectoryExtension,
#[any(size_range(0..1).lift())] extra_extensions: Vec<FileExtension>,
) {
let (_tempdir, root_path) = testdir().unwrap();
let src_files_path = root_path.join("src_files");
fs::create_dir_all(&src_files_path).unwrap();
create_n_random_files(5, &src_files_path, &mut SmallRng::from_entropy());
let dest_files_path = root_path.join("dest_files");
fs::create_dir_all(&dest_files_path).unwrap();
create_n_random_files(5, &dest_files_path, &mut SmallRng::from_entropy());
let dest_files_path_1 = root_path.join("dest_files_1");
fs::create_dir_all(&dest_files_path_1).unwrap();
create_n_random_files(5, &dest_files_path_1, &mut SmallRng::from_entropy());
let archive = &root_path.join(format!("archive.{}", merge_extensions(ext, &extra_extensions)));
ouch!("-A", "c", &src_files_path, archive);
let dest_files_path_renamed = &root_path.join("dest_files_2");
assert_eq!(false, dest_files_path_renamed.exists());
crate::utils::cargo_bin()
.arg("decompress")
.arg(archive)
.arg("-d")
.arg(&dest_files_path)
.write_stdin("r")
.assert()
.success();
assert_same_directory(src_files_path, dest_files_path_renamed.join("src_files"), false);
}
#[cfg(feature = "unrar")]
#[test]
fn unpack_rar() -> Result<(), Box<dyn std::error::Error>> {
fn test_unpack_rar_single(input: &std::path::Path) -> Result<(), Box<dyn std::error::Error>> {
let (_tempdir, dirpath) = testdir()?;
let unpacked_path = &dirpath.join("testfile.txt");
ouch!("-A", "d", input, "-d", dirpath);
let content = fs::read_to_string(unpacked_path)?;
assert_eq!(content, "Testing 123\n");
Ok(())
}
let mut datadir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR")?);
datadir.push("tests/data");
["testfile.rar3.rar.gz", "testfile.rar5.rar"]
.iter()
.try_for_each(|path| test_unpack_rar_single(&datadir.join(path)))?;
Ok(())
}
#[cfg(feature = "unrar")]
#[test]
fn unpack_rar_stdin() -> Result<(), Box<dyn std::error::Error>> {
fn test_unpack_rar_single(input: &std::path::Path, format: &str) -> Result<(), Box<dyn std::error::Error>> {
let (_tempdir, dirpath) = testdir()?;
let unpacked_path = &dirpath.join("testfile.txt");
crate::utils::cargo_bin()
.args([
"-A",
"-y",
"d",
"-",
"-d",
dirpath.to_str().unwrap(),
"--format",
format,
])
.pipe_stdin(input)
.unwrap()
.assert()
.success();
let content = fs::read_to_string(unpacked_path)?;
assert_eq!(content, "Testing 123\n");
Ok(())
}
let mut datadir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR")?);
datadir.push("tests/data");
[("testfile.rar3.rar.gz", "rar.gz"), ("testfile.rar5.rar", "rar")]
.iter()
.try_for_each(|(path, format)| test_unpack_rar_single(&datadir.join(path), format))?;
Ok(())
}
#[cfg(unix)]
#[test]
fn symlink_pack_and_unpack() -> Result<()> {
for ext in MainDirectoryExtension::iter() {
if let MainDirectoryExtension::SevenZ = ext {
continue;
}
eprintln!("ext = {ext}");
let (_tempdir, root_path) = testdir()?;
let src_files_path = root_path.join("src_files");
let folder_path = src_files_path.join("folder");
fs::create_dir_all(&folder_path)?;
let mut files_path = ["file1.txt", "file2.txt", "file3.txt", "file4.txt", "file5.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::<Vec<_>>();
let dest_files_path = root_path.join("dest_files");
fs::create_dir_all(&dest_files_path)?;
let symlink_path = src_files_path.join(Path::new("symlink"));
let symlink_folder_path = src_files_path.join(Path::new("symlink_folder"));
std::os::unix::fs::symlink(&files_path[0], &symlink_path)?;
std::os::unix::fs::symlink(&folder_path, &symlink_folder_path)?;
files_path.push(symlink_path);
let archive = &root_path.join(format!("archive.{ext}"));
crate::utils::cargo_bin()
.arg("compress")
.args(files_path.clone())
.arg(archive)
.assert()
.success();
crate::utils::cargo_bin()
.arg("decompress")
.arg(archive)
.arg("-d")
.arg(&dest_files_path)
.assert()
.success();
for f in dest_files_path.as_path().read_dir()? {
let f = f?;
if f.file_name() == "symlink" || f.file_name() == "symlink_folder" {
assert!(f.file_type()?.is_symlink())
}
}
fs::remove_file(archive)?;
fs::remove_dir_all(&dest_files_path)?;
crate::utils::cargo_bin()
.arg("compress")
.arg("--follow-symlinks")
.args(files_path)
.arg(archive)
.assert()
.success();
crate::utils::cargo_bin()
.arg("decompress")
.arg(archive)
.arg("-d")
.arg(&dest_files_path)
.assert()
.success();
for f in dest_files_path.as_path().read_dir()? {
let f = f?;
assert!(!f.file_type().unwrap().is_symlink())
}
}
Ok(())
}
#[cfg(unix)]
#[test]
fn broken_symlink_stored_successfully_when_format_supports_it() -> Result<()> {
for ext in MainDirectoryExtension::iter() {
eprintln!("ext = {ext}");
let (_tempdir, dir) = testdir().unwrap();
let broken_symlink = dir.join("broken_link");
let broken_target = "/nonexistent/path";
fs::os::unix::fs::symlink(broken_target, &broken_symlink).unwrap();
let archive = dir.join(format!("archive.{ext}"));
let output = dir.join("output");
assert!(broken_symlink.is_symlink());
let result = crate::utils::cargo_bin()
.arg("compress")
.arg(broken_symlink)
.arg(&archive)
.assert();
match ext {
MainDirectoryExtension::SevenZ => {
result.failure();
continue;
}
MainDirectoryExtension::Tar | MainDirectoryExtension::Zip => {
result.success();
}
}
crate::utils::cargo_bin()
.arg("decompress")
.arg(&archive)
.arg("--dir")
.arg(&output)
.assert()
.success();
let target = fs::read_link(output.join("broken_link")).unwrap();
assert_eq!(Path::new(&target), broken_target);
}
Ok(())
}
#[cfg(unix)]
#[test]
fn broken_symlink_error_when_compressing_with_follow_symlinks() {
for ext in MainDirectoryExtension::iter() {
eprintln!("ext = {ext}");
let (_tempdir, dir) = testdir().unwrap();
let input = dir.join("input");
let output = dir.join("output");
fs::create_dir_all(&input).unwrap();
fs::create_dir_all(&output).unwrap();
let broken_symlink = input.join("broken_link");
fs::os::unix::fs::symlink("/nonexistent/path", &broken_symlink).unwrap();
let archive = dir.join(format!("archive.{ext}"));
crate::utils::cargo_bin()
.arg("compress")
.arg("--follow-symlinks")
.arg(&input)
.arg(&archive)
.assert()
.failure();
}
}
#[cfg(unix)]
#[test]
fn symlink_treatment_inside_nested_dirs_with_follow_symlinks_flag() {
for (ext, follow_symlinks_flag) in MainDirectoryExtension::iter().cartesian_product([false, true]) {
if let MainDirectoryExtension::SevenZ = ext {
continue;
}
eprintln!("ext = {ext}");
let (_tempdir, dir) = testdir().unwrap();
let input_a = dir.join("input_a");
let input_b = dir.join("input_b");
fs::create_dir_all(&input_a).unwrap();
fs::create_dir_all(&input_b).unwrap();
let input1_nested_dir = dir.join("input_a/dir1/dir2");
fs::create_dir_all(&input1_nested_dir).unwrap();
fs::os::unix::fs::symlink(input_b.join("target_here"), dir.join("input_a/dir1/dir2/dir3")).unwrap();
let input_b_nested_dir = dir.join("input_b/target_here/dir4/dir5");
let input_b_file = dir.join("input_b/target_here/dir4/dir5/file");
fs::create_dir_all(&input_b_nested_dir).unwrap();
fs::write(input_b_file, "contents").unwrap();
let archive = dir.join(format!("archive.{ext}"));
let mut cmd = crate::utils::cargo_bin();
cmd.arg("compress");
if follow_symlinks_flag {
cmd.arg("--follow-symlinks");
}
cmd.arg(&input_a).arg(&archive).assert().success();
let output = dir.join("output");
crate::utils::cargo_bin()
.arg("decompress")
.arg(archive)
.arg("--dir")
.arg(&output)
.assert()
.success();
assert_eq!(
"contents",
fs::read_to_string(output.join("input_a/dir1/dir2/dir3/dir4/dir5/file")).unwrap(),
);
assert_eq!(
!follow_symlinks_flag,
output.join("input_a/dir1/dir2/dir3").is_symlink(),
);
}
}
#[test]
fn no_git_folder_after_decompression_with_gitignore_flag_active() {
use std::process::Command;
let (_tempdir, dir_path) = testdir().unwrap();
let before = dir_path.join("before");
let decompressed = dir_path.join("decompressed");
fs::create_dir(&before).unwrap();
fs::write(before.join("hello.txt"), b"Hello, world!").unwrap();
Command::new("git")
.arg("init")
.current_dir(&before)
.output()
.expect("failed to run git init");
assert!(before.join(".git").exists(), ".git folder should exist after git init");
let archive = dir_path.join("archive.zip");
ouch!("c", &before, &archive, "--gitignore");
ouch!("d", &archive, "-d", &decompressed);
let decompressed_subdir = fs::read_dir(&decompressed)
.unwrap()
.find_map(Result::ok)
.map(|entry| entry.path())
.expect("Expected one directory inside decompressed");
assert!(
!decompressed_subdir.join(".git").exists(),
".git folder should not exist after decompression"
);
}
#[proptest(cases = 25)]
fn enable_gitignore_flag_should_work_without_git(
ext: DirectoryExtension,
#[any(size_range(0..1).lift())] extra_extensions: Vec<FileExtension>,
) {
let (_tempdir, root_path) = testdir()?;
let source_path = root_path.join(format!("in_{}", merge_extensions(ext, &extra_extensions)));
fs::create_dir_all(&source_path)?;
let out_path = root_path.join(format!("out_{}", merge_extensions(ext, &extra_extensions)));
fs::create_dir_all(&out_path)?;
let mut gitignore_file = fs::File::create(source_path.join(".gitignore"))?;
gitignore_file.write_all(b"a")?;
let mut ignore_file = fs::File::create(source_path.join(".ignore"))?;
ignore_file.write_all(b"b")?;
fs::File::create(source_path.join("a"))?;
fs::File::create(source_path.join("b"))?;
fs::File::create(source_path.join("c"))?;
let archive = root_path.join(format!("archive.{}", merge_extensions(ext, &extra_extensions)));
crate::utils::cargo_bin()
.arg("compress")
.arg("--gitignore")
.arg("--hidden")
.arg(&source_path)
.arg(&archive)
.assert()
.success();
crate::utils::cargo_bin()
.arg("decompress")
.arg(archive)
.arg("-d")
.arg(&out_path)
.assert()
.success();
assert_eq!(
1,
out_path
.join(format!("in_{}", merge_extensions(ext, &extra_extensions)))
.as_path()
.read_dir()?
.count()
);
}
#[proptest(cases = 25)]
fn unpack_multiple_sources_into_the_same_destination_with_merge(
ext: DirectoryExtension,
#[any(size_range(0..1).lift())] extra_extensions: Vec<FileExtension>,
) {
let (_tempdir, root_path) = testdir()?;
let source_path = root_path
.join(format!("example_{}", merge_extensions(ext, &extra_extensions)))
.join("sub_a")
.join("sub_b")
.join("sub_c");
fs::create_dir_all(&source_path)?;
let archive = root_path.join(format!("archive.{}", merge_extensions(ext, &extra_extensions)));
crate::utils::cargo_bin()
.arg("compress")
.args([
fs::File::create(source_path.join("file1.txt"))?.path(),
fs::File::create(source_path.join("file2.txt"))?.path(),
fs::File::create(source_path.join("file3.txt"))?.path(),
])
.arg(&archive)
.assert()
.success();
fs::remove_dir_all(&source_path)?;
fs::create_dir_all(&source_path)?;
let archive1 = root_path.join(format!("archive1.{}", merge_extensions(ext, &extra_extensions)));
crate::utils::cargo_bin()
.arg("compress")
.args([
fs::File::create(source_path.join("file3.txt"))?.path(),
fs::File::create(source_path.join("file4.txt"))?.path(),
fs::File::create(source_path.join("file5.txt"))?.path(),
])
.arg(&archive1)
.assert()
.success();
let out_path = root_path.join(format!("out_{}", merge_extensions(ext, &extra_extensions)));
fs::create_dir_all(&out_path)?;
crate::utils::cargo_bin()
.arg("decompress")
.arg(archive)
.arg("-d")
.arg(&out_path)
.assert()
.success();
crate::utils::cargo_bin()
.arg("decompress")
.arg(archive1)
.arg("-d")
.arg(&out_path)
.write_stdin("m")
.assert()
.success();
assert_eq!(5, out_path.as_path().read_dir()?.count());
}
#[test]
fn reading_nested_archives_with_two_archive_extensions_adjacent() {
let archive_formats = MainDirectoryExtension::iter();
for (first_archive, second_archive) in archive_formats.clone().cartesian_product(archive_formats.rev()) {
let (_tempdir, dir) = testdir().unwrap();
let in_dir = |path: &str| format!("{}/{}", dir.display(), path);
fs::write(in_dir("a.txt"), "contents").unwrap();
let files = [
"a.txt",
&format!("b.{first_archive}"),
&format!("c.{first_archive}.{second_archive}"),
];
let transformations = [first_archive, second_archive];
let compressed_path = in_dir(files.last().unwrap());
for (window, format) in files.windows(2).zip(transformations.iter()) {
let [a, b] = [window[0], window[1]].map(in_dir);
crate::utils::cargo_bin()
.args(["compress", &a, &b, "--format", &format.to_string()])
.assert()
.success();
}
let output = crate::utils::cargo_bin()
.args(["list", &compressed_path, "--yes"])
.assert()
.failure()
.get_output()
.clone();
let stderr = output.stderr.to_str().unwrap();
assert!(memmem::find(stderr.as_bytes(), b"use `--format` to specify what format to use").is_some());
let output = crate::utils::cargo_bin()
.args(["decompress", &compressed_path, "--dir", &in_dir("out"), "--yes"])
.assert()
.failure()
.get_output()
.clone();
let stderr = output.stderr.to_str().unwrap();
assert!(memmem::find(stderr.as_bytes(), b"use `--format` to specify what format to use").is_some());
}
}
#[test]
fn reading_nested_archives_with_two_archive_extensions_interleaved() {
let archive_formats = MainDirectoryExtension::iter();
for (first_archive, second_archive) in archive_formats.clone().cartesian_product(archive_formats.rev()) {
let (_tempdir, dir) = testdir().unwrap();
let in_dir = |path: &str| format!("{}/{}", dir.display(), path);
fs::write(in_dir("a.txt"), "contents").unwrap();
let files = [
"a.txt",
&format!("c.{first_archive}"),
&format!("d.{first_archive}.zst"),
&format!("e.{first_archive}.zst.{second_archive}"),
&format!("f.{first_archive}.zst.{second_archive}.lz4"),
];
let transformations = [&first_archive.to_string(), "zst", &second_archive.to_string(), "lz4"];
let compressed_path = in_dir(files.last().unwrap());
for (window, format) in files.windows(2).zip(transformations.iter()) {
let [a, b] = [window[0], window[1]].map(in_dir);
crate::utils::cargo_bin()
.args(["compress", &a, &b, "--format", format])
.assert()
.success();
}
let output = crate::utils::cargo_bin()
.args(["list", &compressed_path, "--yes"])
.assert()
.failure()
.get_output()
.clone();
let stderr = output.stderr.to_str().unwrap();
assert!(memmem::find(stderr.as_bytes(), b"use `--format` to specify what format to use").is_some());
let output = crate::utils::cargo_bin()
.args(["decompress", &compressed_path, "--dir", &in_dir("out"), "--yes"])
.assert()
.failure()
.get_output()
.clone();
let stderr = output.stderr.to_str().unwrap();
assert!(memmem::find(stderr.as_bytes(), b"use `--format` to specify what format to use").is_some());
}
}
#[test]
fn compressing_archive_with_two_archive_formats() {
let archive_formats = MainDirectoryExtension::iter();
for (first_archive, second_archive) in archive_formats.clone().cartesian_product(archive_formats.rev()) {
let (_tempdir, dir_path) = testdir().unwrap();
let dir = dir_path.display().to_string();
let output = crate::utils::cargo_bin()
.args([
"compress",
"README.md",
&format!("{dir}/out.{first_archive}.{second_archive}"),
"--yes",
])
.assert()
.failure()
.get_output()
.clone();
let stderr = output.stderr.to_str().unwrap();
assert!(memmem::find(stderr.as_bytes(), b"use `--format` to specify what format to use").is_some());
let output = crate::utils::cargo_bin()
.args([
"compress",
"README.md",
&format!("{dir}/out.{first_archive}.{second_archive}"),
"--yes",
"--format",
&format!("{first_archive}.{second_archive}"),
])
.assert()
.failure()
.get_output()
.clone();
let stderr = output.stderr.to_str().unwrap();
assert!(
memmem::find(
stderr.as_bytes(),
b"can only be used at the start of the file extension",
)
.is_some()
);
crate::utils::cargo_bin()
.args([
"compress",
"README.md",
&format!("{dir}/out.{first_archive}.{second_archive}"),
"--yes",
"--format",
&first_archive.to_string(),
])
.assert()
.success();
}
}
#[test]
fn fail_when_compressing_archive_as_the_second_extension() {
for archive_format in MainDirectoryExtension::iter() {
let (_tempdir, dir_path) = testdir().unwrap();
let dir = dir_path.display().to_string();
let output = crate::utils::cargo_bin()
.args([
"compress",
"README.md",
&format!("{dir}/out.zst.{archive_format}"),
"--yes",
])
.assert()
.failure()
.get_output()
.clone();
let stderr = output.stderr.to_str().unwrap();
assert!(memmem::find(stderr.as_bytes(), b"use `--format` to specify what format to use").is_some());
let output = crate::utils::cargo_bin()
.args([
"compress",
"README.md",
&format!("{dir}/out_file"),
"--yes",
"--format",
&format!("zst.{archive_format}"),
])
.assert()
.failure()
.get_output()
.clone();
let stderr = output.stderr.to_str().unwrap();
assert!(
memmem::find(
stderr.as_bytes(),
format!("'{archive_format}' can only be used at the start of the file extension").as_bytes(),
)
.is_some()
);
}
}
#[test]
fn sevenz_list_should_not_failed() {
let (_tempdir, root_path) = testdir().unwrap();
let src_files_path = root_path.join("src_files");
fs::create_dir_all(&src_files_path).unwrap();
let archive = root_path.join("archive.7z.gz");
crate::utils::cargo_bin()
.arg("compress")
.arg("--yes")
.arg(fs::File::create(src_files_path.join("README.md")).unwrap().path())
.arg(&archive)
.assert()
.success();
let res = crate::utils::cargo_bin()
.arg("list")
.arg("--yes")
.arg(&archive)
.assert()
.success();
assert!(res.get_output().stdout.find(b"README.md").is_some());
}
#[cfg(unix)]
#[test]
fn tar_hardlink_pack_and_unpack() {
use std::{fs::hard_link, os::unix::fs::MetadataExt};
let (_tempdir, root_path) = testdir().unwrap();
let source_path = root_path.join("hardlink");
fs::create_dir_all(&source_path).unwrap();
let out_path = root_path.join("out");
fs::create_dir_all(&out_path).unwrap();
let source = fs::File::create(source_path.join("source")).unwrap();
let link1 = source_path.join("link1");
let link2 = source_path.join("link2");
hard_link(source.path(), link1.as_path()).unwrap();
hard_link(source.path(), link2.as_path()).unwrap();
let archive = root_path.join("archive.tar.gz");
crate::utils::cargo_bin()
.arg("compress")
.arg(&source_path)
.arg(&archive)
.assert()
.success();
crate::utils::cargo_bin()
.arg("decompress")
.arg(archive)
.arg("-d")
.arg(&out_path)
.assert()
.success();
let out_source_meta = fs::File::open(out_path.join("hardlink").join("source"))
.unwrap()
.metadata()
.unwrap();
let out_link1_meta = fs::File::open(out_path.join("hardlink").join("link1"))
.unwrap()
.metadata()
.unwrap();
let out_link2_meta = fs::File::open(out_path.join("hardlink").join("link2"))
.unwrap()
.metadata()
.unwrap();
assert!(out_source_meta.nlink() > 1);
assert!(out_link1_meta.nlink() > 1);
assert!(out_link2_meta.nlink() > 1);
assert_eq!(out_source_meta.dev(), out_link1_meta.dev());
assert_eq!(out_link1_meta.dev(), out_link2_meta.dev());
assert_eq!(out_source_meta.ino(), out_link1_meta.ino());
assert_eq!(out_link1_meta.ino(), out_link2_meta.ino());
}
#[test]
fn compress_with_rename_conflict() {
let (_tempdir, root_path) = testdir().unwrap();
let file_path = root_path.join("file.txt");
fs::write(&file_path, "content").unwrap();
let archive = root_path.join("archive.tar.gz");
for _ in 0..3 {
crate::utils::cargo_bin()
.arg("compress")
.arg(&file_path)
.arg(&archive)
.write_stdin("r\n")
.assert()
.success();
}
assert!(root_path.join("archive.tar.gz").exists());
assert!(root_path.join("archive_1.tar.gz").exists());
assert!(root_path.join("archive_2.tar.gz").exists());
}
#[test]
fn decompress_with_mismatched_extension_should_use_detected_format() {
let (_tempdir, test_dir) = testdir().unwrap();
let original_file = test_dir.join("input.txt");
fs::write(&original_file, "Hello, world!").unwrap();
let gzip_archive = test_dir.join("archive.gz");
let misnamed_archive = test_dir.join("archive.zst");
crate::utils::cargo_bin()
.arg("compress")
.arg(&original_file)
.arg(&gzip_archive)
.assert()
.success();
fs::rename(&gzip_archive, &misnamed_archive).unwrap();
let output_dir = test_dir.join("output");
fs::create_dir(&output_dir).unwrap();
let output = crate::utils::cargo_bin()
.arg("decompress")
.arg(misnamed_archive)
.arg("--dir")
.arg(output_dir)
.assert()
.failure()
.get_output()
.clone();
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(stderr.contains("Format mismatch"), "Expected format mismatch error");
assert!(stderr.contains("--format"), "Expected hint about --format flag");
}
#[proptest(cases = 10)]
fn decompress_with_unknown_extension_should_detect_format_and_ask(
ext: FileExtension,
contains_extension_in_filename: bool,
) {
let (_tempdir, test_dir) = testdir()?;
if let FileExtension::Br = ext {
return Ok(());
}
let original_file = test_dir.join("input.txt");
let original_content = "Hello, world!";
fs::write(&original_file, original_content)?;
let compressed_archive = test_dir.join(format!("file.{ext}"));
crate::utils::cargo_bin()
.arg("compress")
.arg(&original_file)
.arg(&compressed_archive)
.assert()
.success();
let erased_ext_filename = if contains_extension_in_filename {
"file.unknown"
} else {
"file"
};
let unknown_path = test_dir.join(erased_ext_filename);
fs::rename(&compressed_archive, &unknown_path)?;
let output_dir = test_dir.join("output");
fs::create_dir(&output_dir)?;
crate::utils::cargo_bin()
.arg("decompress")
.arg(&unknown_path)
.arg("--dir")
.arg(&output_dir)
.write_stdin("y\n")
.assert()
.success();
let decompressed_file = output_dir.join("file-output");
let decompressed_content = fs::read_to_string(&decompressed_file)?;
assert_eq!(decompressed_content, original_content);
}
fn test_concatenated_streams(extension: &str, compress_chunk: impl Fn(&[u8]) -> Vec<u8>) {
use std::io::Write;
let (_tempdir, root_path) = testdir().unwrap();
let chunks: &[&[u8]] = &[
b"First stream content - this is stream 1\n",
b"Second stream content - this is stream 2\n",
b"Third stream content - this is stream 3\n",
];
let concatenated_path = root_path.join(format!("concatenated.{extension}"));
{
let mut file = fs::File::create(&concatenated_path).unwrap();
for chunk in chunks {
file.write_all(&compress_chunk(chunk)).unwrap();
}
}
crate::utils::cargo_bin()
.arg("decompress")
.arg(&concatenated_path)
.arg("-d")
.arg(root_path)
.arg("--yes")
.assert()
.success();
let output_path = root_path.join("concatenated");
let output_content = fs::read(&output_path).unwrap();
let expected_content: Vec<u8> = chunks.iter().flat_map(|c| c.iter().copied()).collect();
assert_eq!(
output_content, expected_content,
"Decompressed content should contain all concatenated {extension} streams"
);
}
#[test]
fn yes_flag_merges_into_nonempty_dir() {
let (_tempdir, dir) = testdir().unwrap();
let src = dir.join("src");
fs::create_dir_all(&src).unwrap();
fs::write(src.join("new_file.txt"), "new content").unwrap();
let archive = dir.join("archive.tar.gz");
ouch!("-A", "c", &src, &archive);
let output = dir.join("output");
fs::create_dir_all(&output).unwrap();
fs::write(output.join("important.txt"), "keep this").unwrap();
crate::utils::cargo_bin()
.current_dir(&output)
.arg("decompress")
.arg(&archive)
.arg("--yes")
.assert()
.success();
assert!(
output.join("important.txt").exists(),
"--yes wiped the output directory instead of merging"
);
assert!(
output.join("src").join("new_file.txt").exists(),
"archive contents were not extracted"
);
}
#[test]
fn cwd_guard_blocks_explicit_overwrite() {
let (_tempdir, dir) = testdir().unwrap();
let src = dir.join("src");
fs::create_dir_all(&src).unwrap();
fs::write(src.join("file.txt"), "content").unwrap();
let archive = dir.join("archive.tar.gz");
ouch!("-A", "c", &src, &archive);
let cwd = dir.join("cwd");
fs::create_dir_all(&cwd).unwrap();
fs::write(cwd.join("important.txt"), "keep this").unwrap();
crate::utils::cargo_bin()
.current_dir(&cwd)
.arg("decompress")
.arg(&archive)
.write_stdin("y")
.assert()
.failure();
assert!(cwd.exists(), "CWD was deleted despite guard");
assert!(
cwd.join("important.txt").exists(),
"CWD contents were deleted despite guard"
);
}
#[test]
fn decompress_concatenated_gzip_streams() {
use std::io::Write;
use flate2::{Compression, write::GzEncoder};
test_concatenated_streams("gz", |data| {
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
encoder.write_all(data).unwrap();
encoder.finish().unwrap()
});
}
#[test]
fn decompress_concatenated_bzip2_streams() {
use std::io::Write;
use bzip2::{Compression, write::BzEncoder};
test_concatenated_streams("bz2", |data| {
let mut encoder = BzEncoder::new(Vec::new(), Compression::default());
encoder.write_all(data).unwrap();
encoder.finish().unwrap()
});
}
#[test]
fn decompress_concatenated_lz4_frames() {
use std::io::Write;
use lz4_flex::frame::FrameEncoder;
test_concatenated_streams("lz4", |data| {
let mut encoder = FrameEncoder::new(Vec::new());
encoder.write_all(data).unwrap();
encoder.finish().unwrap()
});
}