use std::fs::File;
use std::io::Write;
use assert_cmd::cargo::cargo_bin_cmd;
use serde_json::Value;
use tempfile::TempDir;
use zip::ZipWriter;
use zip::write::SimpleFileOptions;
fn create_zip(workspace: &TempDir, source_name: &str, archive_name: &str) {
let source = workspace.path().join(source_name);
std::fs::create_dir_all(&source).expect("create source directory");
std::fs::write(source.join("data.txt"), format!("{source_name} payload\n"))
.expect("write source data");
let output = cargo_bin_cmd!("arcthis")
.current_dir(workspace.path())
.args(["pack", source_name, "--output", archive_name, "--json"])
.output()
.expect("run pack");
assert!(
output.status.success(),
"stdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
fn create_zip_with_corrupted_unselected_entry(path: &std::path::Path) {
let file = File::create(path).expect("create ZIP fixture");
let mut archive = ZipWriter::new(file);
let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
archive
.start_file("selected.txt", options)
.expect("start selected entry");
archive
.write_all(b"selected payload")
.expect("write selected entry");
archive
.start_file("corrupted.txt", options)
.expect("start corrupted entry");
archive
.write_all(b"corrupt me")
.expect("write corrupted entry");
archive.finish().expect("finish ZIP fixture");
let mut bytes = std::fs::read(path).expect("read ZIP fixture");
let offset = bytes
.windows(b"corrupt me".len())
.position(|window| window == b"corrupt me")
.expect("find corruptible payload");
bytes[offset] ^= 0xff;
std::fs::write(path, bytes).expect("write corrupted ZIP fixture");
}
#[test]
fn extract_dry_run_reports_plan_without_writing_or_deleting() {
let workspace = TempDir::new().expect("create test directory");
create_zip(&workspace, "project", "bundle.zip");
std::fs::remove_dir_all(workspace.path().join("project")).expect("remove source fixture");
let output = cargo_bin_cmd!("arcthis")
.current_dir(workspace.path())
.args([
"extract",
"bundle.zip",
"--dry-run",
"--delete-source",
"--json",
])
.output()
.expect("run dry-run");
assert!(output.status.success());
assert!(workspace.path().join("bundle.zip").is_file());
assert!(!workspace.path().join("project").exists());
let value: Value = serde_json::from_slice(&output.stdout).expect("parse plan JSON");
assert_eq!(value["operation"], "extract");
assert_eq!(value["plan"]["will_delete_source_after_success"], true);
}
#[test]
fn extract_collision_policies_are_transactional_and_explicit() {
let workspace = TempDir::new().expect("create test directory");
create_zip(&workspace, "project", "bundle.zip");
let destination = workspace.path().join("destination");
std::fs::create_dir(&destination).expect("create collision directory");
std::fs::write(destination.join("keep.txt"), b"keep").expect("write marker");
let skipped = cargo_bin_cmd!("arcthis")
.current_dir(workspace.path())
.args([
"extract",
"bundle.zip",
"--output",
"destination",
"--skip-existing",
"--delete-source",
"--json",
])
.output()
.expect("run skipped extraction");
assert!(skipped.status.success());
assert_eq!(
std::fs::read(destination.join("keep.txt")).expect("read marker"),
b"keep"
);
assert!(workspace.path().join("bundle.zip").is_file());
let overwritten = cargo_bin_cmd!("arcthis")
.current_dir(workspace.path())
.args([
"extract",
"bundle.zip",
"--output",
"destination",
"--overwrite",
"--json",
])
.output()
.expect("run overwrite extraction");
assert!(overwritten.status.success());
assert!(!destination.join("keep.txt").exists());
assert!(destination.join("project/data.txt").is_file());
std::fs::create_dir(workspace.path().join("renamed")).expect("create rename collision");
let renamed = cargo_bin_cmd!("arcthis")
.current_dir(workspace.path())
.args([
"extract",
"bundle.zip",
"--output",
"renamed",
"--rename",
"--json",
])
.output()
.expect("run renamed extraction");
assert!(renamed.status.success());
assert!(
workspace
.path()
.join("renamed.1/project/data.txt")
.is_file()
);
}
#[test]
fn pack_delete_source_happens_only_after_verified_commit() {
let workspace = TempDir::new().expect("create test directory");
let source = workspace.path().join("project");
std::fs::create_dir(&source).expect("create source directory");
std::fs::write(source.join("data.txt"), b"payload").expect("write source");
let failed = cargo_bin_cmd!("arcthis")
.current_dir(workspace.path())
.args([
"pack",
"project",
"--output",
"unsupported.bin",
"--delete-source",
"--json",
])
.output()
.expect("run failed pack");
assert!(!failed.status.success());
assert!(source.join("data.txt").is_file());
let success = cargo_bin_cmd!("arcthis")
.current_dir(workspace.path())
.args([
"pack",
"project",
"--output",
"project.7z",
"--delete-source",
"--json",
])
.output()
.expect("run successful pack");
assert!(success.status.success());
assert!(!source.exists());
assert!(workspace.path().join("project.7z").is_file());
}
#[test]
fn destructive_lifecycle_rejects_source_destination_overlap() {
let workspace = TempDir::new().expect("create test directory");
let source = workspace.path().join("project");
std::fs::create_dir(&source).expect("create source directory");
std::fs::write(source.join("data.txt"), b"payload").expect("write source");
let nested_pack = cargo_bin_cmd!("arcthis")
.current_dir(workspace.path())
.args([
"pack",
"project",
"--output",
"project/backup.zip",
"--delete-source",
"--json",
])
.output()
.expect("run nested pack");
assert_eq!(nested_pack.status.code(), Some(9));
assert!(source.join("data.txt").is_file());
assert!(!source.join("backup.zip").exists());
let archive = workspace.path().join("self.zip");
create_zip(&workspace, "archive-source", "self.zip");
let self_pack = cargo_bin_cmd!("arcthis")
.current_dir(workspace.path())
.args([
"pack",
"self.zip",
"--output",
"self.zip",
"--overwrite",
"--delete-source",
"--json",
])
.output()
.expect("run aliased pack");
assert_eq!(self_pack.status.code(), Some(9));
assert!(archive.is_file());
let self_extract = cargo_bin_cmd!("arcthis")
.current_dir(workspace.path())
.args([
"extract",
"self.zip",
"archive-source/data.txt",
"--output",
"self.zip",
"--overwrite",
"--delete-source",
"--json",
])
.output()
.expect("run aliased extraction");
assert_eq!(self_extract.status.code(), Some(9));
assert!(archive.is_file());
let error: Value = serde_json::from_slice(&self_extract.stderr).expect("parse collision error");
assert_eq!(error["error"]["code"], "collision");
}
#[test]
fn overwrite_rejects_destination_ancestors_without_delete_source() {
let workspace = TempDir::new().expect("create test directory");
let extract_parent = workspace.path().join("extract-parent");
std::fs::create_dir(&extract_parent).expect("create extract parent");
create_zip(&workspace, "extract-source", "extract-parent/source.zip");
let extract_source = extract_parent.join("source.zip");
let extract = cargo_bin_cmd!("arcthis")
.args([
"extract",
extract_source.to_str().expect("extract source path"),
"--output",
extract_parent.to_str().expect("extract destination path"),
"--overwrite",
"--json",
])
.output()
.expect("run overlapping extraction");
assert_eq!(extract.status.code(), Some(9));
assert!(extract_source.is_file());
let pack_parent = workspace.path().join("pack-parent.zip");
let pack_source = pack_parent.join("source");
std::fs::create_dir_all(&pack_source).expect("create nested pack source");
std::fs::write(pack_source.join("data.txt"), b"pack payload").expect("write pack source");
let pack = cargo_bin_cmd!("arcthis")
.args([
"pack",
pack_source.to_str().expect("pack source path"),
"--output",
pack_parent.to_str().expect("pack destination path"),
"--overwrite",
"--json",
])
.output()
.expect("run overlapping pack");
assert_eq!(pack.status.code(), Some(9));
assert!(pack_source.join("data.txt").is_file());
let convert_parent = workspace.path().join("convert-parent.tar");
std::fs::create_dir(&convert_parent).expect("create convert parent");
create_zip(
&workspace,
"convert-source",
"convert-parent.tar/source.zip",
);
let convert_source = convert_parent.join("source.zip");
let convert = cargo_bin_cmd!("arcthis")
.args([
"convert",
convert_source.to_str().expect("convert source path"),
"--output",
convert_parent.to_str().expect("convert destination path"),
"--overwrite",
"--json",
])
.output()
.expect("run overlapping conversion");
assert_eq!(
convert.status.code(),
Some(9),
"stderr: {}",
String::from_utf8_lossy(&convert.stderr)
);
assert!(convert_source.is_file());
}
#[test]
fn selected_extract_verifies_entire_archive_before_deleting_source() {
let workspace = TempDir::new().expect("create test directory");
let archive = workspace.path().join("corrupted.zip");
let destination = workspace.path().join("selected.txt");
create_zip_with_corrupted_unselected_entry(&archive);
let output = cargo_bin_cmd!("arcthis")
.args([
"extract",
archive.to_str().expect("archive path"),
"selected.txt",
"--output",
destination.to_str().expect("destination path"),
"--delete-source",
"--json",
])
.output()
.expect("run selected extraction");
assert_eq!(output.status.code(), Some(11));
assert!(archive.is_file());
assert!(!destination.exists());
let error: Value = serde_json::from_slice(&output.stderr).expect("parse verification error");
assert_eq!(error["error"]["code"], "verification_failed");
}
#[test]
fn extract_all_honors_recursive_workers_and_delete_source() {
let workspace = TempDir::new().expect("create test directory");
create_zip(&workspace, "alpha", "alpha.zip");
std::fs::remove_dir_all(workspace.path().join("alpha")).expect("remove alpha source");
std::fs::create_dir(workspace.path().join("nested")).expect("create nested directory");
create_zip(&workspace, "beta", "beta.zip");
std::fs::remove_dir_all(workspace.path().join("beta")).expect("remove beta source");
std::fs::rename(
workspace.path().join("beta.zip"),
workspace.path().join("nested/beta.zip"),
)
.expect("move nested archive");
let plan = cargo_bin_cmd!("arcthis")
.current_dir(workspace.path())
.args([
"extract-all",
".",
"--recursive",
"--workers",
"2",
"--dry-run",
"--delete-source",
"--json",
])
.output()
.expect("run batch plan");
assert!(plan.status.success());
let value: Value = serde_json::from_slice(&plan.stdout).expect("parse batch plan");
assert_eq!(value["plan"]["archives"].as_array().map(Vec::len), Some(2));
assert!(workspace.path().join("alpha.zip").is_file());
let execute = cargo_bin_cmd!("arcthis")
.current_dir(workspace.path())
.args([
"extract-all",
".",
"--recursive",
"--workers",
"2",
"--delete-source",
"--json",
])
.output()
.expect("run batch extraction");
assert!(
execute.status.success(),
"stderr: {}",
String::from_utf8_lossy(&execute.stderr)
);
assert!(!workspace.path().join("alpha.zip").exists());
assert!(!workspace.path().join("nested/beta.zip").exists());
assert!(workspace.path().join("alpha/data.txt").is_file());
assert!(workspace.path().join("nested/beta/data.txt").is_file());
}
#[test]
fn extract_all_rejects_cross_archive_source_destination_overlap() {
let workspace = TempDir::new().expect("create test directory");
create_zip(&workspace, "outer", "outer.zip");
std::fs::remove_dir_all(workspace.path().join("outer")).expect("remove outer pack source");
let outer_destination = workspace.path().join("outer");
std::fs::create_dir(&outer_destination).expect("create outer destination");
create_zip(&workspace, "inner", "inner.zip");
std::fs::remove_dir_all(workspace.path().join("inner")).expect("remove inner pack source");
let inner_archive = outer_destination.join("inner.zip");
std::fs::rename(workspace.path().join("inner.zip"), &inner_archive)
.expect("move inner archive into outer destination");
let output = cargo_bin_cmd!("arcthis")
.current_dir(workspace.path())
.args([
"extract-all",
".",
"--recursive",
"--overwrite",
"--workers",
"2",
"--json",
])
.output()
.expect("run overlapping batch extraction");
assert_eq!(
output.status.code(),
Some(9),
"stdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(workspace.path().join("outer.zip").is_file());
assert!(inner_archive.is_file());
}