use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicU64, Ordering};
use flate2::Compression;
use flate2::write::GzEncoder;
use tar::{Builder, Header};
const STUFFR: &str = env!("CARGO_BIN_EXE_stuffr");
static RUN_TOKEN: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| {
use std::hash::{BuildHasher, Hasher};
let n = std::collections::hash_map::RandomState::new()
.build_hasher()
.finish();
format!("{}-{n:016x}", std::process::id())
});
fn tmp(name: &str) -> std::path::PathBuf {
let mut p = std::env::temp_dir();
p.push(format!("stuffr-cli-{}-{name}", *RUN_TOKEN));
p
}
#[test]
fn formats_now_reports_gzip() {
let out = Command::new(STUFFR).arg("formats").output().unwrap();
assert!(out.status.success());
let text = String::from_utf8(out.stdout).unwrap();
assert!(text.contains("gzip"), "gzip must appear: {text}");
assert!(
!text.contains("0 formats"),
"the build is no longer empty: {text}"
);
}
#[test]
fn info_names_the_format_and_the_rung() {
let src = tmp("info.txt");
let gz = tmp("info.txt.gz");
let _ = std::fs::remove_file(&gz);
std::fs::write(&src, b"payload").unwrap();
assert!(
Command::new(STUFFR)
.args(["pack", src.to_str().unwrap(), "--format", "gzip"])
.status()
.unwrap()
.success()
);
let out = Command::new(STUFFR)
.args(["info", gz.to_str().unwrap()])
.output()
.unwrap();
assert!(out.status.success());
let text = String::from_utf8(out.stdout).unwrap();
assert!(text.contains("gzip"), "{text}");
assert!(text.contains("exact"), "a file is read exactly: {text}");
let _ = std::fs::remove_file(&src);
let _ = std::fs::remove_file(&gz);
}
#[test]
fn info_json_is_structured_and_names_how_the_format_was_detected() {
let src = tmp("json2.txt");
let gz = tmp("json2.txt.gz");
let _ = std::fs::remove_file(&gz);
std::fs::write(&src, b"payload").unwrap();
assert!(
Command::new(STUFFR)
.args(["pack", src.to_str().unwrap(), "--format", "gzip"])
.status()
.unwrap()
.success()
);
let out = Command::new(STUFFR)
.args(["info", "--json", gz.to_str().unwrap()])
.output()
.unwrap();
let text = String::from_utf8(out.stdout).unwrap();
let v: serde_json::Value =
serde_json::from_str(&text).unwrap_or_else(|e| panic!("not JSON: {e}\n{text}"));
assert_eq!(v["format"], "gzip");
assert_eq!(v["rung"], "exact");
assert_eq!(
v["detected_by"], "magic",
"a real gzip file is identified by its magic, not its name"
);
assert!(v["warnings"].as_array().unwrap().is_empty());
let _ = std::fs::remove_file(&src);
let _ = std::fs::remove_file(&gz);
}
#[test]
fn info_reports_the_resolved_memory_limit() {
let src = tmp("info-mem.txt");
let gz = tmp("info-mem.txt.gz");
let _ = std::fs::remove_file(&gz);
std::fs::write(&src, b"payload").unwrap();
assert!(
Command::new(STUFFR)
.args(["pack", src.to_str().unwrap(), "--format", "gzip"])
.status()
.unwrap()
.success()
);
let out = Command::new(STUFFR)
.args(["info", "--memory-limit", "512M", gz.to_str().unwrap()])
.output()
.unwrap();
assert!(out.status.success());
let text = String::from_utf8(out.stdout).unwrap();
assert!(
text.contains("512 MiB"),
"an explicit --memory-limit must be reflected in the report: {text}"
);
let out = Command::new(STUFFR)
.args([
"info",
"--json",
"--memory-limit",
"512M",
gz.to_str().unwrap(),
])
.output()
.unwrap();
let text = String::from_utf8(out.stdout).unwrap();
let v: serde_json::Value =
serde_json::from_str(&text).unwrap_or_else(|e| panic!("not JSON: {e}\n{text}"));
assert_eq!(v["memory_limit"], 512 * 1024 * 1024);
let _ = std::fs::remove_file(&src);
let _ = std::fs::remove_file(&gz);
}
#[test]
fn info_withholds_the_fidelity_claim_only_where_a_forward_read_could_lose_something() {
let dir = tmp("info-fidelity-matrix");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let one = dir.join("one.txt");
std::fs::write(&one, b"payload").unwrap();
let mut archives = Vec::new();
for (ext, has_trailing_index) in [("zip", true), ("tar", false), ("cpio", false), ("a", false)]
{
let path = dir.join(format!("bundle.{ext}"));
assert!(
Command::new(STUFFR)
.args(["pack", one.to_str().unwrap(), "-o", path.to_str().unwrap()])
.status()
.unwrap()
.success(),
"packing a .{ext} must succeed"
);
archives.push((ext, path, has_trailing_index));
}
for (ext, path, has_trailing_index) in &archives {
let bytes = std::fs::read(path).unwrap();
let text = String::from_utf8(
Command::new(STUFFR)
.args(["info", path.to_str().unwrap()])
.output()
.unwrap()
.stdout,
)
.unwrap();
assert!(text.contains("exact"), ".{ext} from a file: {text}");
assert!(
text.contains("nothing approximated"),
".{ext} from a FILE was read exactly — the claim is true and must be made: {text}"
);
let text = info_over_stdin(&bytes);
assert!(text.contains("forward-only"), ".{ext} on a pipe: {text}");
if *has_trailing_index {
assert!(
text.contains("not evaluated"),
".{ext} keeps its index at the END of the stream, so a forward read may well \
have approximated something and info has not looked: {text}"
);
assert!(!text.contains("nothing approximated"), ".{ext}: {text}");
} else {
assert!(
text.contains("nothing approximated"),
".{ext} carries every entry's metadata inline, so a forward read loses \
NOTHING — withholding the claim here is a false negative: {text}"
);
assert!(!text.contains("not evaluated"), ".{ext}: {text}");
}
let mut child = Command::new(STUFFR)
.args(["test", "-"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child.stdin.take().unwrap().write_all(&bytes).unwrap();
let out = child.wait_with_output().unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
if *has_trailing_index {
assert!(
stderr.contains("fidelity warning"),
".{ext} on a pipe must really have warnings, or info would be right to \
claim none: {stderr}"
);
} else {
assert!(
out.status.success() && !stderr.contains("fidelity warning"),
".{ext} on a pipe must really have NO warnings, which is what makes info's \
claim true: {stderr}"
);
}
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn info_on_a_piped_zip_does_not_claim_nothing_was_approximated() {
let dir = tmp("info-zip-fidelity");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let one = dir.join("one.txt");
std::fs::write(&one, b"payload").unwrap();
let zip = dir.join("bundle.zip");
assert!(
Command::new(STUFFR)
.args(["pack", one.to_str().unwrap(), "-o", zip.to_str().unwrap()])
.status()
.unwrap()
.success()
);
let bytes = std::fs::read(&zip).unwrap();
let text = info_over_stdin(&bytes);
assert!(
text.contains("forward-only"),
"the rung is still real and still reported: {text}"
);
assert!(
!text.contains("nothing approximated"),
"info has not opened the archive, so it must not claim this: {text}"
);
assert!(
text.contains("not evaluated"),
"the line must say the conclusion was withheld, not vanish: {text}"
);
let mut child = Command::new(STUFFR)
.args(["test", "-"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child.stdin.take().unwrap().write_all(&bytes).unwrap();
let out = child.wait_with_output().unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("central/trailing index was never read")
&& stderr.contains("entry count is unknown"),
"`test` opens the archive and must still report both losses: {stderr}"
);
let out = Command::new(STUFFR)
.args(["info", zip.to_str().unwrap()])
.output()
.unwrap();
let text = String::from_utf8(out.stdout).unwrap();
assert!(text.contains("exact"), "a file is read exactly: {text}");
assert!(
text.contains("nothing approximated"),
"a seekable zip reads its central directory, so nothing is approximated: {text}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn info_on_a_bare_codec_stream_still_reports_nothing_approximated() {
let src = tmp("info-codec-fidelity.txt");
let gz = tmp("info-codec-fidelity.txt.gz");
let _ = std::fs::remove_file(&gz);
std::fs::write(&src, b"payload").unwrap();
assert!(
Command::new(STUFFR)
.args(["pack", src.to_str().unwrap(), "--format", "gzip"])
.status()
.unwrap()
.success()
);
let bytes = std::fs::read(&gz).unwrap();
for (what, text) in [
(
"a file",
String::from_utf8(
Command::new(STUFFR)
.args(["info", gz.to_str().unwrap()])
.output()
.unwrap()
.stdout,
)
.unwrap(),
),
("a pipe", info_over_stdin(&bytes)),
] {
assert!(
text.contains("nothing approximated"),
"{what}: a bare codec stream has no container to open, so this claim is real \
and must survive: {text}"
);
assert!(!text.contains("not evaluated"), "{what}: {text}");
}
let out = Command::new(STUFFR)
.args(["info", "--json", gz.to_str().unwrap()])
.output()
.unwrap();
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap())
.expect("info --json must be JSON");
assert_eq!(v["fidelity_evaluated"], true, "{v}");
let _ = std::fs::remove_file(&src);
let _ = std::fs::remove_file(&gz);
}
fn info_over_stdin(bytes: &[u8]) -> String {
let mut child = Command::new(STUFFR)
.args(["info", "-"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
child.stdin.take().unwrap().write_all(bytes).unwrap();
let out = child.wait_with_output().unwrap();
assert!(out.status.success(), "stuffr info - must succeed");
String::from_utf8(out.stdout).unwrap()
}
#[test]
fn info_over_a_pipe_reports_forward_only_not_exact() {
let src = tmp("pipe-info.txt");
let gz = tmp("pipe-info.txt.gz");
let _ = std::fs::remove_file(&gz);
std::fs::write(&src, b"payload").unwrap();
assert!(
Command::new(STUFFR)
.args(["pack", src.to_str().unwrap(), "--format", "gzip"])
.status()
.unwrap()
.success()
);
let gz_bytes = std::fs::read(&gz).unwrap();
let mut child = Command::new(STUFFR)
.args(["info", "-"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
child.stdin.take().unwrap().write_all(&gz_bytes).unwrap();
let out = child.wait_with_output().unwrap();
assert!(out.status.success());
let text = String::from_utf8(out.stdout).unwrap();
assert!(text.contains("gzip"), "{text}");
assert!(
text.contains("forward-only"),
"a pipe cannot seek, so the rung must not be exact: {text}"
);
let _ = std::fs::remove_file(&src);
let _ = std::fs::remove_file(&gz);
}
#[test]
fn cat_exits_cleanly_when_the_reader_closes_early() {
let src = tmp("broken-pipe.txt");
let gz = tmp("broken-pipe.txt.gz");
let _ = std::fs::remove_file(&gz);
let payload = "the quick brown fox jumps over the lazy dog\n".repeat(500_000);
std::fs::write(&src, payload.as_bytes()).unwrap();
assert!(
Command::new(STUFFR)
.args(["pack", src.to_str().unwrap(), "--format", "gzip"])
.status()
.unwrap()
.success()
);
let mut child = Command::new(STUFFR)
.args(["cat", gz.to_str().unwrap()])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let mut stdout = child.stdout.take().unwrap();
let mut prefix = [0u8; 16];
stdout
.read_exact(&mut prefix)
.expect("at least a few bytes before we stop reading");
drop(stdout);
let mut stderr = child.stderr.take().unwrap();
let mut err_text = String::new();
stderr.read_to_string(&mut err_text).unwrap();
let status = child.wait().unwrap();
assert!(
status.success(),
"an early-closing reader must not look like a failure: {status:?}"
);
assert!(
err_text.is_empty(),
"a clean broken-pipe exit prints nothing: {err_text}"
);
let _ = std::fs::remove_file(&src);
let _ = std::fs::remove_file(&gz);
}
#[test]
fn formats_exits_cleanly_on_a_closed_stdout() {
let mut child = Command::new(STUFFR)
.arg("formats")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
drop(child.stdout.take().unwrap());
let mut stderr = child.stderr.take().unwrap();
let mut err_text = String::new();
stderr.read_to_string(&mut err_text).unwrap();
let status = child.wait().unwrap();
assert!(
status.success(),
"an early-closing reader must not look like a failure: {status:?}"
);
assert!(
err_text.is_empty(),
"a clean broken-pipe exit prints nothing: {err_text}"
);
}
#[test]
fn pack_then_unpack_round_trips_through_the_binary() {
let src = tmp("e2e.txt");
let gz = tmp("e2e.txt.gz");
let back = tmp("e2e-back.txt");
let _ = std::fs::remove_file(&gz);
let _ = std::fs::remove_file(&back);
let plain = b"the quick brown fox ".repeat(2000);
std::fs::write(&src, &plain).unwrap();
assert!(
Command::new(STUFFR)
.args(["pack", src.to_str().unwrap()])
.status()
.unwrap()
.success()
);
assert!(
gz.exists(),
"pack must have written the .gz beside the input"
);
assert!(src.exists(), "pack must not destroy its input");
assert!(
Command::new(STUFFR)
.args(["unpack", gz.to_str().unwrap(), "-o", back.to_str().unwrap()])
.status()
.unwrap()
.success()
);
assert_eq!(
std::fs::read(&back).unwrap(),
plain,
"round trip must be byte-identical"
);
for p in [&src, &gz, &back] {
let _ = std::fs::remove_file(p);
}
}
#[test]
fn no_sync_is_accepted_by_pack_and_unpack_and_still_round_trips() {
let src = tmp("nosync-e2e.txt");
let gz = tmp("nosync-e2e.txt.gz");
let back = tmp("nosync-e2e-back.txt");
let _ = std::fs::remove_file(&gz);
let _ = std::fs::remove_file(&back);
let plain = b"the quick brown fox ".repeat(500);
std::fs::write(&src, &plain).unwrap();
assert!(
Command::new(STUFFR)
.args([
"pack",
src.to_str().unwrap(),
"--no-sync",
"--format",
"gzip"
])
.status()
.unwrap()
.success()
);
assert!(gz.exists(), "pack --no-sync must still write the output");
assert!(
Command::new(STUFFR)
.args([
"unpack",
gz.to_str().unwrap(),
"-o",
back.to_str().unwrap(),
"--no-sync",
])
.status()
.unwrap()
.success()
);
assert_eq!(
std::fs::read(&back).unwrap(),
plain,
"--no-sync must cost durability, never correctness"
);
for p in [&src, &gz, &back] {
let _ = std::fs::remove_file(p);
}
}
#[test]
fn cat_reads_a_stream_on_stdin() {
use std::io::Write;
use std::process::Stdio;
let src = tmp("pipe.txt");
let gz = tmp("pipe.txt.gz");
let _ = std::fs::remove_file(&gz);
std::fs::write(&src, b"needle in a haystack").unwrap();
assert!(
Command::new(STUFFR)
.args(["pack", src.to_str().unwrap(), "--format", "gzip"])
.status()
.unwrap()
.success()
);
let packed = std::fs::read(&gz).unwrap();
let mut child = Command::new(STUFFR)
.args(["cat", "-"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
child.stdin.as_mut().unwrap().write_all(&packed).unwrap();
let out = child.wait_with_output().unwrap();
assert!(out.status.success());
assert_eq!(out.stdout, b"needle in a haystack");
let _ = std::fs::remove_file(&src);
let _ = std::fs::remove_file(&gz);
}
#[test]
fn pack_reads_input_from_stdin() {
let out = tmp("stdin-pack.gz");
let _ = std::fs::remove_file(&out);
let plain = b"stdin plaintext, packed then verified byte for byte".repeat(200);
let mut child = Command::new(STUFFR)
.args(["pack", "-", "-o", out.to_str().unwrap()])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child.stdin.take().unwrap().write_all(&plain).unwrap();
let result = child.wait_with_output().unwrap();
assert!(
result.status.success(),
"stderr: {}",
String::from_utf8_lossy(&result.stderr)
);
assert!(out.exists(), "pack must have written the output file");
let cat_out = Command::new(STUFFR)
.args(["cat", out.to_str().unwrap()])
.output()
.unwrap();
assert!(cat_out.status.success());
assert_eq!(cat_out.stdout, plain, "round trip must be byte-identical");
let _ = std::fs::remove_file(&out);
}
#[test]
fn pack_writes_output_to_stdout() {
let src = tmp("stdout-pack.txt");
let _ = std::fs::remove_file(&src);
let plain = b"payload written straight through to stdout by pack".repeat(200);
std::fs::write(&src, &plain).unwrap();
let pack_out = Command::new(STUFFR)
.args(["pack", src.to_str().unwrap(), "--format", "gzip", "-o", "-"])
.output()
.unwrap();
assert!(
pack_out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&pack_out.stderr)
);
assert_eq!(
&pack_out.stdout[..2],
&[0x1f, 0x8b],
"must be a real gzip stream"
);
let mut child = Command::new(STUFFR)
.args(["cat", "-"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(&pack_out.stdout)
.unwrap();
let cat_out = child.wait_with_output().unwrap();
assert!(cat_out.status.success());
assert_eq!(
cat_out.stdout, plain,
"round trip through a piped stdout destination must be byte-identical"
);
let _ = std::fs::remove_file(&src);
}
#[test]
fn pack_over_a_pipe_reports_forward_only_not_exact() {
let out = tmp("pack-rung-pipe.gz");
let _ = std::fs::remove_file(&out);
let plain = b"payload".repeat(50);
let mut child = Command::new(STUFFR)
.args(["pack", "-", "-o", out.to_str().unwrap()])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child.stdin.take().unwrap().write_all(&plain).unwrap();
let result = child.wait_with_output().unwrap();
assert!(result.status.success());
let err_text = String::from_utf8_lossy(&result.stderr);
assert!(
err_text.contains("forward-only"),
"a pipe cannot seek, so pack's reported rung must not be exact: {err_text}"
);
let _ = std::fs::remove_file(&out);
}
#[test]
fn pack_of_a_seekable_file_reports_exact() {
let src = tmp("pack-rung-file.txt");
let gz = tmp("pack-rung-file.txt.gz");
let _ = std::fs::remove_file(&gz);
std::fs::write(&src, b"payload").unwrap();
let out = Command::new(STUFFR)
.args(["pack", src.to_str().unwrap()])
.output()
.unwrap();
assert!(out.status.success());
let err_text = String::from_utf8_lossy(&out.stderr);
assert!(err_text.contains("exact"), "{err_text}");
let _ = std::fs::remove_file(&src);
let _ = std::fs::remove_file(&gz);
}
#[test]
fn an_existing_output_is_refused_with_exit_two() {
let src = tmp("clash.txt");
let gz = tmp("clash.txt.gz");
std::fs::write(&src, b"payload").unwrap();
std::fs::write(&gz, b"PRE-EXISTING").unwrap();
let out = Command::new(STUFFR)
.args(["pack", src.to_str().unwrap(), "--format", "gzip"])
.output()
.unwrap();
assert_eq!(out.status.code(), Some(2), "usage errors exit 2");
assert!(String::from_utf8_lossy(&out.stderr).contains("--force"));
assert_eq!(
std::fs::read(&gz).unwrap(),
b"PRE-EXISTING",
"the refused command changed nothing"
);
let _ = std::fs::remove_file(&src);
let _ = std::fs::remove_file(&gz);
}
#[test]
fn a_bomb_exits_six_and_leaves_no_partial_output() {
let src = tmp("boom.bin");
let gz = tmp("boom.bin.gz");
let out_path = tmp("boom-out.bin");
let _ = std::fs::remove_file(&gz);
let _ = std::fs::remove_file(&out_path);
std::fs::write(&src, vec![0u8; 2 * 1024 * 1024]).unwrap();
assert!(
Command::new(STUFFR)
.args(["pack", src.to_str().unwrap(), "--format", "gzip"])
.status()
.unwrap()
.success()
);
let out = Command::new(STUFFR)
.args([
"unpack",
gz.to_str().unwrap(),
"-o",
out_path.to_str().unwrap(),
"--max-ratio",
"100",
])
.output()
.unwrap();
assert_eq!(out.status.code(), Some(6), "resource limits exit 6");
assert!(
!out_path.exists(),
"a refused decode must leave no partial file"
);
let _ = std::fs::remove_file(&src);
let _ = std::fs::remove_file(&gz);
}
#[test]
fn an_unknown_verb_exits_two() {
let out = Command::new(STUFFR).arg("bogus").output().unwrap();
assert_eq!(out.status.code(), Some(2));
}
#[test]
fn examples_page_covers_every_format_and_flag() {
use clap::CommandFactory;
use stuffr_cli::cli::Cli;
let out = Command::new(STUFFR).arg("--examples").output().unwrap();
assert!(out.status.success(), "stuffr --examples must exit 0");
let text = String::from_utf8(out.stdout).unwrap();
for row in stuffr::registry().matrix() {
let id = row.id.as_str();
assert!(
text.contains(id),
"the examples page does not mention the `{id}` format"
);
}
let root = Cli::command();
for sub in root.get_subcommands() {
let name = sub.get_name();
if name == "help" {
continue;
}
assert!(
text.contains(name),
"the examples page does not mention the `{name}` subcommand"
);
for arg in sub.get_arguments() {
let Some(long) = arg.get_long() else {
continue;
};
if long == "help" || long == "version" {
continue;
}
let flag = format!("--{long}");
assert!(
text.contains(flag.as_str()),
"the examples page does not mention {flag}"
);
}
}
}
#[test]
fn the_examples_page_cannot_carry_a_stale_count_or_a_shipped_still_to_come() {
let out = Command::new(STUFFR).arg("--examples").output().unwrap();
assert!(out.status.success());
let text = String::from_utf8(out.stdout).unwrap();
let rows = stuffr::registry().matrix();
let containers: Vec<&str> = rows
.iter()
.filter(|r| stuffr::registry().container(r.id).is_some())
.map(|r| r.id.as_str())
.collect();
let codecs: Vec<&str> = rows
.iter()
.filter(|r| stuffr::registry().container(r.id).is_none())
.map(|r| r.id.as_str())
.collect();
const LEGACY_ONLY_CONTAINERS: &[&str] = &["lha", "arj", "arc", "zoo"];
let base_containers: Vec<&str> = containers
.iter()
.copied()
.filter(|id| !LEGACY_ONLY_CONTAINERS.contains(id))
.collect();
let full_container_count = containers.len();
for sentence in text.split(['.', '\n']) {
let lower = sentence.to_lowercase();
if !(lower.contains("still to come") || lower.contains("not yet supported")) {
continue;
}
for id in rows.iter().map(|r| r.id.as_str()) {
assert!(
!sentence.contains(id),
"the page says `{id}` is still to come, but this build registers it: \
{sentence:?}"
);
}
}
const NUMBERS: &[(&str, usize)] = &[
("one", 1),
("two", 2),
("three", 3),
("four", 4),
("five", 5),
("six", 6),
("seven", 7),
("eight", 8),
("nine", 9),
("ten", 10),
("eleven", 11),
("twelve", 12),
("thirteen", 13),
("fourteen", 14),
("fifteen", 15),
("sixteen", 16),
("seventeen", 17),
("eighteen", 18),
("nineteen", 19),
("twenty", 20),
];
let flat = text.split_whitespace().collect::<Vec<_>>().join(" ");
let words: Vec<&str> = flat.split(' ').collect();
for pair in words.windows(2) {
let (word, next) = (
pair[0].trim_matches(|c: char| !c.is_alphanumeric()),
pair[1],
);
let Some((_, value)) = NUMBERS.iter().find(|(w, _)| *w == word.to_lowercase()) else {
continue;
};
let noun = next
.trim_matches(|c: char| !c.is_alphanumeric())
.to_lowercase();
if noun == "containers" {
assert!(
*value == base_containers.len() || *value == full_container_count,
"the page says `{word} containers`, but this build has {} base containers \
({} once every legacy container is counted) — the counts on this page are \
what a user trusts before running `stuffr formats`",
base_containers.len(),
full_container_count
);
continue;
}
let expected = match noun.as_str() {
"formats" | "rows" => rows.len(),
"codecs" => codecs.len(),
_ => continue,
};
assert_eq!(
*value, expected,
"the page says `{word} {noun}`, but this build has {expected} — the counts \
on this page are what a user trusts before running `stuffr formats`"
);
}
let counting_sentences: Vec<&str> = flat
.split(". ")
.filter(|sentence| {
let l = sentence.to_lowercase();
NUMBERS
.iter()
.any(|(w, _)| l.contains(&format!("{w} containers")))
})
.collect();
assert!(
!counting_sentences.is_empty(),
"the page must state how many containers this build has"
);
let names_them_all = counting_sentences.iter().any(|sentence| {
containers.iter().all(|id| {
sentence
.split(|c: char| !c.is_alphanumeric())
.any(|word| word == *id)
})
});
assert!(
names_them_all,
"no sentence both counts the containers and names every one of them \
({containers:?}); the counting sentences are {counting_sentences:?}"
);
}
#[test]
fn examples_pages_first_worked_example_runs_as_documented() {
let dir = tmp("examples-first-example-dir");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let notes = dir.join("notes.txt");
let packed = dir.join("notes.txt.gz");
std::fs::write(¬es, b"the documented example's payload").unwrap();
let out = Command::new(STUFFR)
.arg("pack")
.arg("notes.txt")
.current_dir(&dir)
.output()
.unwrap();
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
packed.exists(),
"the page says this writes notes.txt.gz beside it"
);
assert!(
notes.exists(),
"the page says the input, notes.txt, is left alone"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_symlink_in_a_walked_tree_is_never_refused_and_the_page_may_not_say_it_is() {
let dir = tmp("examples-symlink-verdict");
let _ = std::fs::remove_dir_all(&dir);
let tree = dir.join("tree");
std::fs::create_dir_all(&tree).unwrap();
std::fs::write(tree.join("file.txt"), b"payload").unwrap();
std::os::unix::fs::symlink("file.txt", tree.join("link.txt")).unwrap();
let registry = stuffr::registry();
let mut measured = 0usize;
for row in registry.matrix() {
if registry.container(row.id).is_none() || !row.write {
continue;
}
let id = row.id.as_str();
let out_path = dir.join(format!("out-{id}"));
for strict in [false, true] {
let _ = std::fs::remove_file(&out_path);
let mut cmd = Command::new(STUFFR);
cmd.arg("pack")
.arg(&tree)
.arg("-o")
.arg(&out_path)
.arg("--format")
.arg(id);
if strict {
cmd.arg("--strict-fidelity");
}
let out = cmd.output().unwrap();
let code = out.status.code();
assert!(
matches!(code, Some(0) | Some(4)),
"`pack` over a tree containing a symlink answered exit {code:?} for `{id}` \
(strict-fidelity: {strict}). A kind a container cannot store is a fidelity \
LOSS, never a refusal — and exit 3 in particular is what `examples.txt` \
wrongly promised for `arj` until Phase 3c's final review measured it. \
stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
}
measured += 1;
}
assert!(
measured >= 6,
"only {measured} writable containers were exercised; this build registers six, so the \
registry walk has stopped finding them and the measurement proves nothing"
);
let _ = std::fs::remove_dir_all(&dir);
let out = Command::new(STUFFR).arg("--examples").output().unwrap();
assert!(out.status.success(), "stuffr --examples must exit 0");
let text = String::from_utf8(out.stdout).unwrap();
let flat = text.split_whitespace().collect::<Vec<_>>().join(" ");
for sentence in flat.split(". ") {
let lower = sentence.to_lowercase();
if !lower.contains("symlink") {
continue;
}
if !(lower.contains("refused") || lower.contains("refuses")) {
continue;
}
assert!(
!lower.contains("exit 3"),
"the examples page says a symlink is refused at exit 3, which the measurement \
above proves no writable container does: {sentence:?}"
);
}
}
#[test]
fn pack_refuses_a_read_only_legacy_format_clearly() {
let registry = stuffr::registry();
let dir = tmp("legacy-format-refusal-dir");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let src = dir.join("notes.txt");
std::fs::write(&src, b"payload").unwrap();
for (name, out_ext) in [("arc", "arc"), ("zoo", "zoo")] {
let registered = registry.container(stuffr::FormatId::new(name)).is_some()
|| registry.codec(stuffr::FormatId::new(name)).is_some();
if !registered {
continue;
}
let dst = dir.join(format!("out.{out_ext}"));
let res = Command::new(STUFFR)
.args([
"pack",
src.to_str().unwrap(),
"--format",
name,
"-o",
dst.to_str().unwrap(),
])
.output()
.unwrap();
let err = String::from_utf8_lossy(&res.stderr);
assert_eq!(
res.status.code(),
Some(3),
"`--format {name}` must refuse at exit 3 (this build cannot do that), got \
{:?}; stderr: {err}",
res.status.code()
);
let lower = err.to_lowercase();
assert!(
lower.contains(&format!(
"`{name}` can be read but not written by this build"
)),
"`--format {name}`'s refusal must be the one shared capability sentence, \
naming the format, rather than a per-format phrasing: {err}"
);
assert!(
!dst.exists(),
"a refused pack must not leave a partial `{}` behind",
dst.display()
);
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn pack_writes_a_compress_stream_the_system_tool_reads() {
let uncompress_bin = require_bin("uncompress");
let dir = tmp("compress-write-dir");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let src = dir.join("notes.txt");
let plain = b"the quick brown fox jumps over the lazy dog\n".repeat(200);
std::fs::write(&src, &plain).unwrap();
let dst = dir.join("notes.txt.Z");
let res = Command::new(STUFFR)
.args([
"pack",
src.to_str().unwrap(),
"--format",
"compress",
"-o",
dst.to_str().unwrap(),
])
.output()
.unwrap();
assert!(
res.status.success(),
"pack --format compress must now succeed: {}",
String::from_utf8_lossy(&res.stderr)
);
assert!(dst.exists(), "pack must have written {}", dst.display());
let out = Command::new(&uncompress_bin)
.arg("-c")
.arg(&dst)
.output()
.unwrap();
assert!(
out.status.success(),
"the system uncompress must read this build's own .Z output: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(
out.stdout, plain,
"the system uncompress's output must match the original plaintext exactly"
);
let cat = Command::new(STUFFR)
.args(["cat", dst.to_str().unwrap()])
.output()
.unwrap();
assert!(cat.status.success());
assert_eq!(cat.stdout, plain);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn pack_writes_an_lha_archive_the_system_tool_reads() {
let lha_bin = require_lhasa();
let banner = lhasa_banner(&lha_bin);
let dir = tmp("lha-write-dir");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("tree/sub")).unwrap();
let alpha = b"the quick brown fox jumps over the lazy dog\n".repeat(120);
std::fs::write(dir.join("tree/alpha.txt"), &alpha).unwrap();
std::fs::write(dir.join("tree/sub/beta.bin"), b"beta\n").unwrap();
let dst = dir.join("out.lzh");
let res = Command::new(STUFFR)
.args([
"pack",
dir.join("tree").to_str().unwrap(),
"--format",
"lha",
"-o",
dst.to_str().unwrap(),
])
.output()
.unwrap();
assert!(
res.status.success(),
"pack --format lha must now succeed: {}",
String::from_utf8_lossy(&res.stderr)
);
assert!(dst.exists(), "pack must have written {}", dst.display());
let test = Command::new(&lha_bin).arg("t").arg(&dst).output().unwrap();
assert!(
test.status.success(),
"{banner} must accept an archive stuffr wrote: {}\n{}",
String::from_utf8_lossy(&test.stdout),
String::from_utf8_lossy(&test.stderr)
);
let out = dir.join("out");
std::fs::create_dir_all(&out).unwrap();
let extract = Command::new(&lha_bin)
.arg(format!("xfw={}", out.display()))
.arg(&dst)
.output()
.unwrap();
assert!(
extract.status.success(),
"{banner} must extract it: {}",
String::from_utf8_lossy(&extract.stderr)
);
assert_eq!(std::fs::read(out.join("tree/alpha.txt")).unwrap(), alpha);
assert_eq!(
std::fs::read(out.join("tree/sub/beta.bin")).unwrap(),
b"beta\n"
);
let list = Command::new(STUFFR)
.args(["list", dst.to_str().unwrap()])
.output()
.unwrap();
assert!(list.status.success());
let listing = String::from_utf8_lossy(&list.stdout);
for name in ["tree/alpha.txt", "tree/sub/beta.bin"] {
assert!(
listing.contains(name),
"`stuffr list` lost {name}: {listing}"
);
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn pack_writes_an_arj_archive_stuffr_reads_back() {
let dir = tmp("arj-write-dir");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("tree/sub")).unwrap();
let alpha = b"the quick brown fox jumps over the lazy dog\n".repeat(120);
std::fs::write(dir.join("tree/alpha.txt"), &alpha).unwrap();
std::fs::write(dir.join("tree/sub/beta.bin"), b"beta\n").unwrap();
let dst = dir.join("out.arj");
let res = Command::new(STUFFR)
.args([
"pack",
dir.join("tree").to_str().unwrap(),
"--format",
"arj",
"-o",
dst.to_str().unwrap(),
])
.output()
.unwrap();
assert!(
res.status.success(),
"pack --format arj must now succeed: {}",
String::from_utf8_lossy(&res.stderr)
);
assert!(dst.exists(), "pack must have written {}", dst.display());
let head = std::fs::read(&dst).unwrap();
assert_eq!(
&head[0..2],
&[0x60, 0xEA],
"an ARJ archive opens with its own header id"
);
let verify = Command::new(STUFFR)
.args(["test", dst.to_str().unwrap()])
.output()
.unwrap();
assert!(
verify.status.success(),
"`stuffr test` must accept an archive stuffr wrote: {}",
String::from_utf8_lossy(&verify.stderr)
);
let list = Command::new(STUFFR)
.args(["list", dst.to_str().unwrap()])
.output()
.unwrap();
assert!(list.status.success());
let listing = String::from_utf8_lossy(&list.stdout);
for name in ["tree/alpha.txt", "tree/sub/beta.bin"] {
assert!(
listing.contains(name),
"`stuffr list` lost {name}: {listing}"
);
}
let out = dir.join("out");
let unpack = Command::new(STUFFR)
.args(["unpack", dst.to_str().unwrap(), "-C", out.to_str().unwrap()])
.output()
.unwrap();
assert!(
unpack.status.success(),
"unpack must restore the tree: {}",
String::from_utf8_lossy(&unpack.stderr)
);
assert_eq!(std::fs::read(out.join("tree/alpha.txt")).unwrap(), alpha);
assert_eq!(
std::fs::read(out.join("tree/sub/beta.bin")).unwrap(),
b"beta\n"
);
assert!(
out.join("tree/sub").is_dir(),
"the directory entries must come back as directories, which is what \
ContainerCaps::stores_dirs claims"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_corrupt_archive_exits_five_not_one() {
let src = tmp("cli-corrupt.txt");
let gz = tmp("cli-corrupt.txt.gz");
let out = tmp("cli-corrupt-out.txt");
let _ = std::fs::remove_file(&gz);
let _ = std::fs::remove_file(&out);
std::fs::write(&src, b"the quick brown fox ".repeat(200)).unwrap();
assert!(
Command::new(STUFFR)
.args(["pack", src.to_str().unwrap(), "--format", "gzip"])
.status()
.unwrap()
.success()
);
let mut bytes = std::fs::read(&gz).unwrap();
let mid = bytes.len() / 2;
bytes[mid] ^= 0xFF;
std::fs::write(&gz, &bytes).unwrap();
let res = Command::new(STUFFR)
.args(["unpack", gz.to_str().unwrap(), "-o", out.to_str().unwrap()])
.output()
.unwrap();
assert_eq!(
res.status.code(),
Some(5),
"a corrupt archive must be distinguishable from a full disk"
);
let _ = std::fs::remove_file(&src);
let _ = std::fs::remove_file(&gz);
}
#[test]
fn unpack_accepts_an_explicit_format() {
let src = tmp("fmt-unpack.txt");
let gz = tmp("fmt-unpack.txt.gz");
let out = tmp("fmt-unpack-out.txt");
let _ = std::fs::remove_file(&gz);
let _ = std::fs::remove_file(&out);
std::fs::write(&src, b"payload").unwrap();
assert!(
Command::new(STUFFR)
.args(["pack", src.to_str().unwrap(), "--format", "gzip"])
.status()
.unwrap()
.success()
);
let res = Command::new(STUFFR)
.args([
"unpack",
gz.to_str().unwrap(),
"--format",
"gzip",
"-o",
out.to_str().unwrap(),
])
.output()
.unwrap();
assert!(
res.status.success(),
"stderr: {}",
String::from_utf8_lossy(&res.stderr)
);
assert_eq!(std::fs::read(&out).unwrap(), b"payload");
for p in [&src, &gz, &out] {
let _ = std::fs::remove_file(p);
}
}
#[test]
fn cat_accepts_an_explicit_format() {
let src = tmp("fmt-cat.txt");
let gz = tmp("fmt-cat.txt.gz");
let _ = std::fs::remove_file(&gz);
std::fs::write(&src, b"payload").unwrap();
assert!(
Command::new(STUFFR)
.args(["pack", src.to_str().unwrap(), "--format", "gzip"])
.status()
.unwrap()
.success()
);
let res = Command::new(STUFFR)
.args(["cat", gz.to_str().unwrap(), "--format", "gzip"])
.output()
.unwrap();
assert!(
res.status.success(),
"stderr: {}",
String::from_utf8_lossy(&res.stderr)
);
assert_eq!(res.stdout, b"payload");
let _ = std::fs::remove_file(&src);
let _ = std::fs::remove_file(&gz);
}
#[test]
fn pack_with_no_flags_defaults_to_gzip() {
let src = tmp("bare-default.txt");
let gz = tmp("bare-default.txt.gz");
let _ = std::fs::remove_file(&gz);
std::fs::write(&src, b"payload").unwrap();
let out = Command::new(STUFFR)
.args(["pack", src.to_str().unwrap()])
.output()
.unwrap();
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
gz.exists(),
"a bare pack with no --format must default to gzip and write {}.gz",
src.display()
);
let _ = std::fs::remove_file(&src);
let _ = std::fs::remove_file(&gz);
}
#[test]
fn unpack_format_flag_is_honoured_not_merely_accepted() {
let src = tmp("fmt-deflate.txt");
let packed = tmp("fmt-deflate.dat");
let out = tmp("fmt-deflate-out.txt");
for p in [&packed, &out] {
let _ = std::fs::remove_file(p);
}
let payload = b"the quick brown fox jumps over the lazy dog".repeat(50);
std::fs::write(&src, &payload).unwrap();
assert!(
Command::new(STUFFR)
.args([
"pack",
src.to_str().unwrap(),
"--format",
"deflate",
"-o",
packed.to_str().unwrap(),
])
.status()
.unwrap()
.success()
);
let no_format = Command::new(STUFFR)
.args([
"unpack",
packed.to_str().unwrap(),
"-o",
out.to_str().unwrap(),
"--force",
])
.output()
.unwrap();
assert!(
!no_format.status.success(),
"a raw deflate stream must NOT be detectable without --format"
);
let with_format = Command::new(STUFFR)
.args([
"unpack",
packed.to_str().unwrap(),
"--format",
"deflate",
"-o",
out.to_str().unwrap(),
"--force",
])
.output()
.unwrap();
assert!(
with_format.status.success(),
"stderr: {}",
String::from_utf8_lossy(&with_format.stderr)
);
assert_eq!(std::fs::read(&out).unwrap(), payload);
for p in [&src, &packed, &out] {
let _ = std::fs::remove_file(p);
}
}
#[test]
fn cat_format_flag_is_honoured_not_merely_accepted() {
let src = tmp("fmt-cat-deflate.txt");
let packed = tmp("fmt-cat-deflate.dat");
let _ = std::fs::remove_file(&packed);
let payload = b"the quick brown fox jumps over the lazy dog".repeat(50);
std::fs::write(&src, &payload).unwrap();
assert!(
Command::new(STUFFR)
.args([
"pack",
src.to_str().unwrap(),
"--format",
"deflate",
"-o",
packed.to_str().unwrap(),
])
.status()
.unwrap()
.success()
);
let no_format = Command::new(STUFFR)
.args(["cat", packed.to_str().unwrap()])
.output()
.unwrap();
assert!(
!no_format.status.success(),
"a raw deflate stream must NOT be detectable without --format"
);
let with_format = Command::new(STUFFR)
.args(["cat", packed.to_str().unwrap(), "--format", "deflate"])
.output()
.unwrap();
assert!(
with_format.status.success(),
"stderr: {}",
String::from_utf8_lossy(&with_format.stderr)
);
assert_eq!(with_format.stdout, payload);
for p in [&src, &packed] {
let _ = std::fs::remove_file(p);
}
}
#[test]
fn an_unknown_format_name_is_a_usage_error_on_every_verb() {
for verb in ["unpack", "cat"] {
let out = Command::new(STUFFR)
.args([verb, "/dev/null", "--format", "bogus"])
.output()
.unwrap();
assert_eq!(
out.status.code(),
Some(2),
"{verb} --format bogus must be a usage error"
);
assert!(
String::from_utf8_lossy(&out.stderr).contains("gzip"),
"the error must name what this build does have"
);
}
}
#[test]
fn cat_honours_max_ratio() {
let src = tmp("cat-bomb.bin");
let gz = tmp("cat-bomb.bin.gz");
let _ = std::fs::remove_file(&gz);
std::fs::write(&src, vec![0u8; 2 * 1024 * 1024]).unwrap();
assert!(
Command::new(STUFFR)
.args(["pack", src.to_str().unwrap(), "--format", "gzip"])
.status()
.unwrap()
.success()
);
let out = Command::new(STUFFR)
.args(["cat", gz.to_str().unwrap(), "--max-ratio", "100"])
.output()
.unwrap();
assert_eq!(out.status.code(), Some(6), "resource limits exit 6");
let _ = std::fs::remove_file(&src);
let _ = std::fs::remove_file(&gz);
}
#[test]
fn formats_lists_every_registered_format() {
let out = Command::new(STUFFR).arg("formats").output().unwrap();
let text = String::from_utf8(out.stdout).unwrap();
for row in stuffr::registry().matrix() {
assert!(
text.contains(row.id.as_str()),
"`stuffr formats` omits {}: {text}",
row.id
);
}
}
#[test]
fn the_formats_note_sits_below_the_table_after_a_blank_line() {
let out = Command::new(STUFFR).arg("formats").output().unwrap();
let text = String::from_utf8(out.stdout).unwrap();
let lines: Vec<&str> = text.lines().collect();
let note = lines
.iter()
.position(|l| l.starts_with("(WRITE shows"))
.unwrap_or_else(|| panic!("`stuffr formats` printed no WRITE note:\n{text}"));
for row in stuffr::registry().matrix() {
let at = lines
.iter()
.position(|l| l.starts_with(row.id.as_str()))
.unwrap_or_else(|| panic!("`stuffr formats` omits {}:\n{text}", row.id));
assert!(
at < note,
"{} is listed at line {at}, below the note at line {note}:\n{text}",
row.id
);
}
assert!(
note > 0,
"the note is the first line, so no table precedes it"
);
assert_eq!(
lines[note - 1],
"",
"the note must be separated from the table by a blank line, got {:?}:\n{text}",
lines[note - 1]
);
}
#[test]
fn a_malformed_memory_limit_is_a_usage_error() {
let src = tmp("mem-bad.txt");
let out_path = tmp("mem-bad.gz");
let _ = std::fs::remove_file(&out_path);
std::fs::write(&src, b"payload").unwrap();
let out = Command::new(STUFFR)
.args(["pack", "--memory-limit", "512MB", "--format", "gzip", "-o"])
.arg(&out_path)
.arg(&src)
.output()
.unwrap();
assert_eq!(out.status.code(), Some(2), "malformed size must be exit 2");
let err = String::from_utf8_lossy(&out.stderr);
assert!(
err.contains("512M") && err.contains("2G"),
"must name the accepted forms concretely: {err}"
);
assert!(!out_path.exists(), "a refused pack must leave no output");
let _ = std::fs::remove_file(&src);
let _ = std::fs::remove_file(&out_path);
}
#[test]
fn threads_is_not_offered_on_decode_subcommands() {
for sub in ["unpack", "cat"] {
let out = Command::new(STUFFR)
.args([sub, "--threads", "4", "-"])
.output()
.unwrap();
let err = String::from_utf8_lossy(&out.stderr);
assert!(
err.contains("unexpected argument") || err.contains("--threads"),
"{sub} must reject --threads, got: {err}"
);
}
}
#[test]
fn the_binary_reports_its_version() {
let want = env!("CARGO_PKG_VERSION");
let out = Command::new(STUFFR).arg("--version").output().unwrap();
let s = String::from_utf8_lossy(&out.stdout);
assert!(s.contains(want), "--version must report {want}, got: {s}");
}
static NEXT_DIR: AtomicU64 = AtomicU64::new(0);
fn tmp_dir() -> PathBuf {
let n = NEXT_DIR.fetch_add(1, Ordering::Relaxed);
let mut p = std::env::temp_dir();
p.push(format!("stuffr-cli-archive-{}-{n}", *RUN_TOKEN));
std::fs::create_dir_all(&p).unwrap();
p
}
fn append_tar_entry<W: Write>(builder: &mut Builder<W>, name: &str, data: &[u8]) {
let mut header = Header::new_gnu();
header.set_size(data.len() as u64);
header.set_mode(0o644);
builder.append_data(&mut header, name, data).unwrap();
}
fn write_fixture_tar(dir: &Path, entries: &[(&str, &[u8])]) -> PathBuf {
let path = dir.join("fixture.tar");
let mut builder = Builder::new(std::fs::File::create(&path).unwrap());
for (name, data) in entries {
append_tar_entry(&mut builder, name, data);
}
builder.finish().unwrap();
path
}
fn write_fixture_tar_gz(dir: &Path, name: &str, entries: &[(&str, &[u8])]) -> PathBuf {
let mut tar_bytes = Vec::new();
{
let mut builder = Builder::new(&mut tar_bytes);
for (n, data) in entries {
append_tar_entry(&mut builder, n, data);
}
builder.finish().unwrap();
}
let path = dir.join(name);
let mut enc = GzEncoder::new(
std::fs::File::create(&path).unwrap(),
Compression::default(),
);
enc.write_all(&tar_bytes).unwrap();
enc.finish().unwrap();
path
}
fn gzip_wrap(bytes: &[u8]) -> Vec<u8> {
let mut enc = GzEncoder::new(Vec::new(), Compression::fast());
enc.write_all(bytes).unwrap();
enc.finish().unwrap()
}
fn corrupt_midway(good: &Path) -> PathBuf {
let bytes = std::fs::read(good).unwrap();
let cut = bytes.len() / 2;
let corrupt = good.with_file_name("corrupt.tar");
std::fs::write(&corrupt, &bytes[..cut]).unwrap();
corrupt
}
fn run(args: &[&str]) -> std::process::ExitStatus {
Command::new(STUFFR).args(args).status().unwrap()
}
fn run_output(args: &[&str]) -> std::process::Output {
Command::new(STUFFR).args(args).output().unwrap()
}
fn run_with_stdin_output(args: &[&str], input: &[u8]) -> std::process::Output {
let mut child = Command::new(STUFFR)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child.stdin.take().unwrap().write_all(input).unwrap();
child.wait_with_output().unwrap()
}
fn run_with_stdin(args: &[&str], input: &[u8]) -> String {
let out = run_with_stdin_output(args, input);
String::from_utf8(out.stdout).unwrap()
}
#[test]
fn list_names_every_entry_and_extracts_nothing() {
let dir = tmp_dir();
let archive = write_fixture_tar(&dir, &[("a.txt", b"alpha"), ("b/c.bin", b"\x00\xff")]);
let out = run_output(&["list", archive.to_str().unwrap()]);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let text = String::from_utf8(out.stdout).unwrap();
assert!(text.contains("a.txt") && text.contains("b/c.bin"), "{text}");
assert!(!dir.join("a.txt").exists(), "list must not extract");
assert!(!dir.join("b").exists(), "list must not extract");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn ls_is_an_alias_for_list() {
let dir = tmp_dir();
let archive = write_fixture_tar(&dir, &[("a.txt", b"alpha")]);
let out = run_output(&["ls", archive.to_str().unwrap()]);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(String::from_utf8(out.stdout).unwrap().contains("a.txt"));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn list_json_reports_name_kind_and_size() {
let dir = tmp_dir();
let archive = write_fixture_tar(&dir, &[("a.txt", b"alpha")]);
let out = run_output(&["list", "--json", archive.to_str().unwrap()]);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let text = String::from_utf8(out.stdout).unwrap();
let v: serde_json::Value =
serde_json::from_str(&text).unwrap_or_else(|e| panic!("not JSON: {e}\n{text}"));
let rows = v.as_array().expect("a JSON array of entries");
assert_eq!(rows.len(), 1);
assert_eq!(rows[0]["name"], "a.txt");
assert_eq!(rows[0]["kind"], "file");
assert_eq!(rows[0]["size"], 5);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn test_verb_exits_five_on_a_corrupt_archive_and_zero_on_a_good_one() {
let dir = tmp_dir();
let good = write_fixture_tar(&dir, &[("a.txt", b"alpha")]);
assert_eq!(run(&["test", good.to_str().unwrap()]).code(), Some(0));
let bad = corrupt_midway(&good);
assert_eq!(run(&["test", bad.to_str().unwrap()]).code(), Some(5));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn list_works_on_a_pipe() {
let dir = tmp_dir();
let bytes = std::fs::read(write_fixture_tar(&dir, &[("a.txt", b"alpha")])).unwrap();
let text = run_with_stdin(&["list", "-"], &bytes);
assert!(text.contains("a.txt"), "{text}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn list_and_test_succeed_on_a_real_tar_gz_from_a_file() {
let dir = tmp_dir();
let archive = write_fixture_tar_gz(
&dir,
"fixture.tar.gz",
&[("a.txt", b"alpha"), ("b/c.bin", b"\x00\xff")],
);
let list_out = run_output(&["list", archive.to_str().unwrap()]);
assert!(
list_out.status.success(),
"list stderr: {}",
String::from_utf8_lossy(&list_out.stderr)
);
let text = String::from_utf8(list_out.stdout).unwrap();
assert!(text.contains("a.txt") && text.contains("b/c.bin"), "{text}");
let test_out = run_output(&["test", archive.to_str().unwrap()]);
assert!(
test_out.status.success(),
"test stderr: {}",
String::from_utf8_lossy(&test_out.stderr)
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn list_and_test_succeed_on_a_real_tar_gz_over_a_pipe() {
let dir = tmp_dir();
let archive = write_fixture_tar_gz(&dir, "fixture.tar.gz", &[("a.txt", b"alpha")]);
let bytes = std::fs::read(&archive).unwrap();
let list_out = run_with_stdin_output(&["list", "-"], &bytes);
assert!(
list_out.status.success(),
"list stderr: {}",
String::from_utf8_lossy(&list_out.stderr)
);
assert!(
String::from_utf8(list_out.stdout)
.unwrap()
.contains("a.txt")
);
let test_out = run_with_stdin_output(&["test", "-"], &bytes);
assert!(
test_out.status.success(),
"test stderr: {}",
String::from_utf8_lossy(&test_out.stderr)
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn list_and_test_succeed_on_a_real_tgz_from_a_file() {
let dir = tmp_dir();
let archive = write_fixture_tar_gz(&dir, "fixture.tgz", &[("a.txt", b"alpha")]);
let list_out = run_output(&["list", archive.to_str().unwrap()]);
assert!(
list_out.status.success(),
"list stderr: {}",
String::from_utf8_lossy(&list_out.stderr)
);
assert!(
String::from_utf8(list_out.stdout)
.unwrap()
.contains("a.txt")
);
let test_out = run_output(&["test", archive.to_str().unwrap()]);
assert!(
test_out.status.success(),
"test stderr: {}",
String::from_utf8_lossy(&test_out.stderr)
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn list_and_test_succeed_on_a_real_tgz_over_a_pipe() {
let dir = tmp_dir();
let archive = write_fixture_tar_gz(&dir, "fixture.tgz", &[("a.txt", b"alpha")]);
let bytes = std::fs::read(&archive).unwrap();
let list_out = run_with_stdin_output(&["list", "-"], &bytes);
assert!(
list_out.status.success(),
"list stderr: {}",
String::from_utf8_lossy(&list_out.stderr)
);
assert!(
String::from_utf8(list_out.stdout)
.unwrap()
.contains("a.txt")
);
let test_out = run_with_stdin_output(&["test", "-"], &bytes);
assert!(
test_out.status.success(),
"test stderr: {}",
String::from_utf8_lossy(&test_out.stderr)
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn test_catches_a_payload_truncation() {
let dir = tmp_dir();
let payload = vec![b'x'; 8192];
let full = write_fixture_tar(&dir, &[("big.bin", &payload)]);
let bytes = std::fs::read(&full).unwrap();
let cut = 512 + payload.len() / 3;
let truncated = dir.join("truncated.tar");
std::fs::write(&truncated, &bytes[..cut]).unwrap();
let test_out = run_output(&["test", truncated.to_str().unwrap()]);
assert_eq!(
test_out.status.code(),
Some(5),
"test reads the payload and must catch the truncation: {}",
String::from_utf8_lossy(&test_out.stderr)
);
let test_err = String::from_utf8_lossy(&test_out.stderr);
assert!(
test_err.contains("big.bin") && test_err.contains("short of the size"),
"must be tar's own per-entry short-read diagnosis, reclassified from a raw \
io::ErrorKind::InvalidData via Error::from_decode_io: {test_err}"
);
let list_out = run_output(&["list", truncated.to_str().unwrap()]);
assert_eq!(
list_out.status.code(),
Some(5),
"list also exits 5 here, via the tar crate's own header-skip detection, not via a \
payload read: {}",
String::from_utf8_lossy(&list_out.stderr)
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_zip_declaring_an_unreachable_local_header_offset_exits_5_not_1() {
const MINIMISED_REPRODUCER: &str = concat!(
"4b4d4d00504b010214031400000000000a3b375d0000000000000000000001010101e900000000000000237718c33a17",
"fbe047df010101010101010166722066757a7a7403034b50ffff1304504b05060000000001000008f500040000000000",
"000000000000005da481a9000007054b5000000a3b375dbc6ab89d2a00000000000000000b00000000000000000000a4",
"81250000bf5907054b5047bbd19819f9938fb5527f0414d3000000000a3b375d000000000010000000004f0102141403",
"1400000008000a3b375d06ff880a7374756666722066757a7a7403034b50ffff1304504b05060000000001000008f500",
"0000ed000000000000000000005da481a900000081a90000001400000008000a3b375d06ff880a737475666672206675",
"7a7a7403034b50ffff1304504b05060000000001000008f5000000ed000000000000000000005da481a900000081a900",
"0000000000ed000000000000000000005da481a900000081a9000000a481a900000081a900000008000a3b375d06ff88",
"0a7374756666722066757a7a7403034b50ffff1304504b05060000000001000008005da481a900000081000000000000",
"5da481a900000081a9000000000000ed000000000000000000005da481a900000081a9000000a481a900000081a90000",
"00000000000000000000005da481a900000081a9000000000000ed000000000000000000005da481a900000081a90000",
"00a481a900000081a9000000",
);
let bytes: Vec<u8> = (0..MINIMISED_REPRODUCER.len() / 2)
.map(|i| u8::from_str_radix(&MINIMISED_REPRODUCER[i * 2..i * 2 + 2], 16).unwrap())
.collect();
assert_eq!(bytes.len(), 540, "the reproducer must be carried verbatim");
let dir = tmp_dir();
let archive = dir.join("unreachable-offset.zip");
std::fs::write(&archive, &bytes).unwrap();
for verb in ["list", "test"] {
let out = run_output(&[verb, archive.to_str().unwrap()]);
let err = String::from_utf8_lossy(&out.stderr);
assert_eq!(
out.status.code(),
Some(5),
"`{verb}` must report the ARCHIVE as corrupt, never exit 1: {err}"
);
assert!(
err.contains("archive is corrupt")
&& err.contains("largest position any file can hold"),
"the refusal must name what the archive declared, not an errno: {err}"
);
assert!(
!err.contains("os error 22"),
"a bare errno reaching the user is the defect itself: {err}"
);
}
let out = run_output(&[
"unpack",
"-C",
dir.join("out").to_str().unwrap(),
archive.to_str().unwrap(),
]);
assert_eq!(
out.status.code(),
Some(5),
"`unpack` too: {}",
String::from_utf8_lossy(&out.stderr)
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn list_refuses_a_plain_codec_stream_as_not_an_archive() {
let dir = tmp_dir();
let src = dir.join("notes.txt");
let gz = dir.join("notes.txt.gz");
std::fs::write(&src, b"just plain text, not an archive").unwrap();
assert!(
Command::new(STUFFR)
.args(["pack", src.to_str().unwrap(), "--format", "gzip"])
.status()
.unwrap()
.success()
);
let out = run_output(&["list", gz.to_str().unwrap()]);
assert_eq!(
out.status.code(),
Some(2),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("not an archive"), "{err}");
assert!(
err.contains("gzip"),
"must name what the input actually resolved to: {err}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn list_refuses_nesting_past_the_depth_bound() {
let mut bytes = b"payload too shallow to be an archive".to_vec();
for _ in 0..6 {
bytes = gzip_wrap(&bytes);
}
let out = run_with_stdin_output(&["list", "-"], &bytes);
assert_eq!(
out.status.code(),
Some(6),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("nesting"), "{err}");
}
#[test]
fn list_reports_a_truncated_codec_stream_as_corrupt_not_as_an_io_failure() {
let dir = tmp_dir();
let trunc = dir.join("trunc.zz");
std::fs::write(&trunc, [0x78, 0xda, 0x0a]).unwrap();
let cat_out = run_output(&["cat", trunc.to_str().unwrap()]);
assert_eq!(
cat_out.status.code(),
Some(5),
"cat (unaffected by this bug) must still exit 5: {}",
String::from_utf8_lossy(&cat_out.stderr)
);
let list_out = run_output(&["list", trunc.to_str().unwrap()]);
assert_eq!(
list_out.status.code(),
Some(5),
"list must agree with cat on the SAME bytes — exit 1 here means stuffr claimed it \
failed, when the truth is the input is corrupt: {}",
String::from_utf8_lossy(&list_out.stderr)
);
let list_err = String::from_utf8_lossy(&list_out.stderr);
assert!(
list_err.contains("archive is corrupt"),
"must be Error::Corrupt's own wording, not Error::Io's: {list_err}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn list_reports_an_empty_lzip_stream_as_corrupt_not_as_an_io_failure() {
let dir = tmp_dir();
let empty = dir.join("empty.lz");
std::fs::write(&empty, []).unwrap();
let cat_out = run_output(&["cat", empty.to_str().unwrap()]);
assert_eq!(
cat_out.status.code(),
Some(5),
"cat (unaffected by this bug) must still exit 5: {}",
String::from_utf8_lossy(&cat_out.stderr)
);
let list_out = run_output(&["list", empty.to_str().unwrap()]);
assert_eq!(
list_out.status.code(),
Some(5),
"list must agree with cat on the SAME bytes: {}",
String::from_utf8_lossy(&list_out.stderr)
);
let list_err = String::from_utf8_lossy(&list_out.stderr);
assert!(
list_err.contains("archive is corrupt") && list_err.contains("LZIP"),
"must be Error::Corrupt's own wording, naming what lzip's decoder actually \
complained about: {list_err}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn list_reports_a_directory_as_an_io_failure_not_as_corrupt() {
let dir = tmp_dir();
let sub = dir.join("not_a_file");
std::fs::create_dir_all(&sub).unwrap();
let out = run_output(&["list", sub.to_str().unwrap()]);
assert_eq!(
out.status.code(),
Some(1),
"a genuine i/o failure on the raw source must stay exit 1, not become exit 5: {}",
String::from_utf8_lossy(&out.stderr)
);
let err = String::from_utf8_lossy(&out.stderr);
assert!(
err.starts_with("stuffr: i/o error:"),
"must be Error::Io's own wording, not Error::Corrupt's \"archive is corrupt\": {err}"
);
assert!(
!err.contains("archive is corrupt"),
"must not have been reclassified as corrupt: {err}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn list_reports_a_zstd_stream_that_decodes_into_arj_magic_as_corrupt() {
const ZSTD_OVER_ARJ_MAGIC: &[u8] = &[
0x28, 0xb5, 0x2f, 0xfd, 0x24, 0x00, 0x30, 0x00, 0x00, 0x60, 0xea, 0x05, 0x06, 0x28, 0x0a,
0x0a, 0x0a, 0x0a, 0x0a, 0x70, 0x00,
];
let dir = tmp_dir();
let repro = dir.join("repro.zst");
std::fs::write(&repro, ZSTD_OVER_ARJ_MAGIC).unwrap();
let path = repro.to_str().unwrap();
let cat_out = run_output(&["cat", path]);
assert_eq!(
cat_out.status.code(),
Some(5),
"cat was already right on these bytes and must stay right: {}",
String::from_utf8_lossy(&cat_out.stderr)
);
for verb in ["list", "test"] {
let out = run_output(&[verb, path]);
assert_eq!(
out.status.code(),
Some(5),
"`{verb}` must agree with cat on the SAME bytes — exit 1 here means stuffr claimed \
it failed, when the truth is the input is corrupt: {}",
String::from_utf8_lossy(&out.stderr)
);
let err = String::from_utf8_lossy(&out.stderr);
assert!(
err.contains("archive is corrupt"),
"must be Error::Corrupt's own wording, not Error::Io's: {err}"
);
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_ratio_refusal_below_a_container_is_a_resource_limit_not_an_io_failure() {
const ZSTD_BOMB_OVER_ARJ_MAGIC: &[u8] = &[
0x28, 0xb5, 0x2f, 0xfd, 0xa4, 0x02, 0x00, 0x10, 0x00, 0x5c, 0x00, 0x00, 0x18, 0x60, 0xea,
0x00, 0x01, 0x00, 0xfa, 0xff, 0x39, 0x18, 0x02, 0x02, 0x00, 0x10, 0x00, 0x02, 0x00, 0x10,
0x00, 0x02, 0x00, 0x10, 0x00, 0x02, 0x00, 0x10, 0x00, 0x02, 0x00, 0x10, 0x00, 0x02, 0x00,
0x10, 0x00, 0x02, 0x00, 0x10, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0xbe, 0xb2, 0xa6, 0x83,
];
let dir = tmp_dir();
let bomb = dir.join("bomb.zst");
std::fs::write(&bomb, ZSTD_BOMB_OVER_ARJ_MAGIC).unwrap();
let path = bomb.to_str().unwrap();
let cat_out = run_output(&["cat", path]);
assert_eq!(
cat_out.status.code(),
Some(6),
"cat was already right and must stay right: {}",
String::from_utf8_lossy(&cat_out.stderr)
);
for verb in ["list", "test"] {
let out = run_output(&[verb, path]);
assert_eq!(
out.status.code(),
Some(6),
"`{verb}` must agree with cat: a refused expansion bomb is a limit, never \
\"stuffr failed\": {}",
String::from_utf8_lossy(&out.stderr)
);
let err = String::from_utf8_lossy(&out.stderr);
assert!(
err.contains("resource limit exceeded") && err.contains("--max-ratio"),
"must be Error::ResourceLimit's own wording, naming the knob: {err}"
);
}
let _ = std::fs::remove_dir_all(&dir);
}
fn write_bomb_tar_gz(dir: &Path) -> PathBuf {
write_fixture_tar_gz(
dir,
"bomb.tar.gz",
&[("bomb.bin", &vec![0u8; 2 * 1024 * 1024])],
)
}
#[test]
fn list_and_test_refuse_a_bomb_tar_gz_at_a_tight_max_ratio() {
let dir = tmp_dir();
let bomb = write_bomb_tar_gz(&dir);
let list_out = run_output(&["list", bomb.to_str().unwrap(), "--max-ratio", "100"]);
assert_eq!(
list_out.status.code(),
Some(6),
"list stderr: {}",
String::from_utf8_lossy(&list_out.stderr)
);
let list_err = String::from_utf8_lossy(&list_out.stderr);
assert!(
list_err.contains("100") && list_err.contains("--max-ratio"),
"must name the limit and the flag, matching how decompress reports it: {list_err}"
);
let test_out = run_output(&["test", bomb.to_str().unwrap(), "--max-ratio", "100"]);
assert_eq!(
test_out.status.code(),
Some(6),
"test stderr: {}",
String::from_utf8_lossy(&test_out.stderr)
);
let test_err = String::from_utf8_lossy(&test_out.stderr);
assert!(
test_err.contains("100") && test_err.contains("--max-ratio"),
"must name the limit and the flag: {test_err}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn raising_max_ratio_lets_the_bomb_through() {
let dir = tmp_dir();
let bomb = write_bomb_tar_gz(&dir);
let list_out = run_output(&["list", bomb.to_str().unwrap(), "--max-ratio", "100000"]);
assert!(
list_out.status.success(),
"list stderr: {}",
String::from_utf8_lossy(&list_out.stderr)
);
let test_out = run_output(&["test", bomb.to_str().unwrap(), "--max-ratio", "100000"]);
assert!(
test_out.status.success(),
"test stderr: {}",
String::from_utf8_lossy(&test_out.stderr)
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_legitimately_compressible_tar_gz_is_not_refused_at_the_default_ratio() {
let dir = tmp_dir();
let text = "the quick brown fox jumps over the lazy dog\n".repeat(200_000);
let archive = write_fixture_tar_gz(&dir, "legit.tar.gz", &[("log.txt", text.as_bytes())]);
let list_out = run_output(&["list", archive.to_str().unwrap()]);
assert!(
list_out.status.success(),
"a legitimately compressible archive must not be refused at the default ratio: {}",
String::from_utf8_lossy(&list_out.stderr)
);
assert!(
String::from_utf8(list_out.stdout)
.unwrap()
.contains("log.txt")
);
let test_out = run_output(&["test", archive.to_str().unwrap()]);
assert!(
test_out.status.success(),
"a legitimately compressible archive must not be refused at the default ratio: {}",
String::from_utf8_lossy(&test_out.stderr)
);
let _ = std::fs::remove_dir_all(&dir);
}
const FIXTURE_MTIME: u64 = 1_600_000_000;
fn raw_header(
name: &str,
kind: tar::EntryType,
link_target: Option<&str>,
size: u64,
mode: u32,
) -> Header {
let mut header = Header::new_gnu();
header.set_size(size);
header.set_mode(mode);
header.set_mtime(FIXTURE_MTIME);
header.set_entry_type(kind);
{
let gnu = header.as_gnu_mut().expect("new_gnu is a GNU header");
let bytes = name.as_bytes();
assert!(
bytes.len() <= gnu.name.len(),
"raw fixture names must fit tar's 100-byte name field"
);
gnu.name[..bytes.len()].copy_from_slice(bytes);
if let Some(target) = link_target {
let bytes = target.as_bytes();
assert!(
bytes.len() <= gnu.linkname.len(),
"raw fixture link targets must fit tar's 100-byte linkname field"
);
gnu.linkname[..bytes.len()].copy_from_slice(bytes);
}
}
header.set_cksum();
header
}
enum Raw<'a> {
File(&'a str, &'a [u8]),
ModedFile(&'a str, &'a [u8], u32),
Dir(&'a str),
Symlink(&'a str, &'a str),
Fifo(&'a str),
}
fn write_raw_tar(path: &Path, entries: &[Raw<'_>]) -> PathBuf {
let mut builder = Builder::new(std::fs::File::create(path).unwrap());
for entry in entries {
match entry {
Raw::File(name, data) => {
let header = raw_header(
name,
tar::EntryType::Regular,
None,
data.len() as u64,
0o644,
);
builder.append(&header, *data).unwrap();
}
Raw::ModedFile(name, data, mode) => {
let header = raw_header(
name,
tar::EntryType::Regular,
None,
data.len() as u64,
*mode,
);
builder.append(&header, *data).unwrap();
}
Raw::Dir(name) => {
let header = raw_header(name, tar::EntryType::Directory, None, 0, 0o755);
builder.append(&header, std::io::empty()).unwrap();
}
Raw::Symlink(name, target) => {
let header = raw_header(name, tar::EntryType::Symlink, Some(target), 0, 0o777);
builder.append(&header, std::io::empty()).unwrap();
}
Raw::Fifo(name) => {
let header = raw_header(name, tar::EntryType::Fifo, None, 0, 0o644);
builder.append(&header, std::io::empty()).unwrap();
}
}
}
builder.finish().unwrap();
path.to_path_buf()
}
#[test]
fn a_traversing_entry_is_refused_and_writes_nothing_outside_the_destination() {
let dir = tmp_dir();
let evil = dir.join("evil.tar");
write_raw_tar(&evil, &[Raw::File("../escaped.txt", b"pwned")]);
let out = run_output(&[
"unpack",
evil.to_str().unwrap(),
"-C",
dir.join("out").to_str().unwrap(),
]);
assert_eq!(
out.status.code(),
Some(7),
"a traversing entry must be refused as an unsafe path (exit 7), stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
!dir.join("escaped.txt").exists(),
"the file escaped the destination"
);
assert!(
String::from_utf8_lossy(&out.stderr).contains("../escaped.txt"),
"the refusal must name the offending entry, got: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn a_file_entry_naming_the_destination_itself_is_refused() {
let dir = tmp_dir();
let evil = dir.join("dot.tar");
write_raw_tar(&evil, &[Raw::File(".", b"pwned")]);
let out_dir = dir.join("out");
let out = run_output(&[
"unpack",
evil.to_str().unwrap(),
"-C",
out_dir.to_str().unwrap(),
]);
assert_eq!(
out.status.code(),
Some(7),
"a file entry naming the destination must be an unsafe path (exit 7), never exit 1 \
(`stuffr failed`), stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
out_dir.is_dir(),
"the destination must still be a directory, not a file stuffr created over it"
);
let ok = dir.join("dotdir.tar");
write_raw_tar(&ok, &[Raw::Dir("."), Raw::File("a.txt", b"hi")]);
let ok_dir = dir.join("ok");
let out = run_output(&[
"unpack",
ok.to_str().unwrap(),
"-C",
ok_dir.to_str().unwrap(),
]);
assert_eq!(
out.status.code(),
Some(0),
"`tar cf x.tar .`'s own leading directory entry must still extract, stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(std::fs::read(ok_dir.join("a.txt")).unwrap(), b"hi");
}
#[test]
fn an_lha_file_entry_naming_the_destination_itself_is_refused() {
let dir = tmp_dir();
let evil = dir.join("dot.lzh");
std::fs::write(&evil, raw_lha_entry(b".", b"-lh0-", b"pwned")).unwrap();
let out_dir = dir.join("out");
let out = run_output(&[
"unpack",
evil.to_str().unwrap(),
"-C",
out_dir.to_str().unwrap(),
]);
assert_eq!(
out.status.code(),
Some(7),
"an LHA file entry naming the destination must be exit 7, never exit 1, stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
out_dir.is_dir(),
"the destination must still be a directory"
);
}
fn raw_lha_entry(name: &[u8], method: &[u8; 5], content: &[u8]) -> Vec<u8> {
let mut crc: u16 = 0;
for &b in content {
crc ^= u16::from(b);
for _ in 0..8 {
crc = if crc & 1 != 0 {
(crc >> 1) ^ 0xA001
} else {
crc >> 1
};
}
}
let size = content.len() as u32;
let mut counted: Vec<u8> = Vec::new();
counted.extend_from_slice(method);
counted.extend_from_slice(&size.to_le_bytes()); counted.extend_from_slice(&size.to_le_bytes()); counted.extend_from_slice(&0u32.to_le_bytes()); counted.push(0x20); counted.push(1); counted.push(name.len() as u8);
counted.extend_from_slice(name);
counted.extend_from_slice(&crc.to_le_bytes());
counted.push(b'U'); counted.extend_from_slice(&0u16.to_le_bytes());
let mut out = vec![
counted.len() as u8,
counted.iter().fold(0u8, |a, &b| a.wrapping_add(b)),
];
out.extend_from_slice(&counted);
out.extend_from_slice(content);
out.push(0); out
}
#[test]
fn an_absolute_entry_path_is_refused() {
let dir = tmp_dir();
let evil = dir.join("abs.tar");
let escape = std::env::temp_dir().join(format!("stuffr-abs-escape-{}.txt", std::process::id()));
let _ = std::fs::remove_file(&escape);
write_raw_tar(&evil, &[Raw::File(escape.to_str().unwrap(), b"pwned")]);
let out = run_output(&[
"unpack",
evil.to_str().unwrap(),
"-C",
dir.to_str().unwrap(),
]);
assert_eq!(
out.status.code(),
Some(7),
"an absolute entry name must be refused as an unsafe path (exit 7), stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
!escape.exists(),
"an absolute entry name wrote outside the destination: {}",
escape.display()
);
}
#[test]
fn cat_streams_one_named_entry_from_a_pipe() {
let bytes = std::fs::read(write_fixture_tar(
&tmp_dir(),
&[("a.txt", b"alpha"), ("b.txt", b"beta")],
))
.unwrap();
assert_eq!(run_with_stdin(&["cat", "-", "b.txt"], &bytes), "beta");
}
#[test]
fn pack_collects_several_paths_into_one_archive() {
let dir = tmp_dir();
std::fs::write(dir.join("one.txt"), b"1").unwrap();
std::fs::write(dir.join("two.txt"), b"2").unwrap();
let out = dir.join("bundle.tar");
let packed = run_output(&[
"pack",
dir.join("one.txt").to_str().unwrap(),
dir.join("two.txt").to_str().unwrap(),
"-o",
out.to_str().unwrap(),
]);
assert!(
packed.status.success(),
"stderr: {}",
String::from_utf8_lossy(&packed.stderr)
);
let listed = String::from_utf8(run_output(&["list", out.to_str().unwrap()]).stdout).unwrap();
assert!(
listed.contains("one.txt") && listed.contains("two.txt"),
"both entries must be listed, got: {listed}"
);
assert!(
!listed.contains(dir.to_str().unwrap()),
"absolute paths must not be stored as entry names, got: {listed}"
);
}
#[test]
fn a_symlink_whose_target_escapes_the_destination_is_refused_before_it_is_created() {
let dir = tmp_dir();
let dest = dir.join("out");
let evil = write_raw_tar(
&dir.join("link.tar"),
&[Raw::Symlink("link", "../../etc/passwd")],
);
let out = run_output(&[
"unpack",
evil.to_str().unwrap(),
"-C",
dest.to_str().unwrap(),
]);
assert_eq!(
out.status.code(),
Some(7),
"an escaping symlink target must be refused (exit 7), stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
std::fs::symlink_metadata(dest.join("link")).is_err(),
"the symlink must not have been created at all"
);
assert!(
String::from_utf8_lossy(&out.stderr).contains("../../etc/passwd"),
"the refusal must name the offending target, got: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn an_entry_written_through_an_escaping_symlink_lands_nowhere() {
let dir = tmp_dir();
let dest = dir.join("out");
let evil = write_raw_tar(
&dir.join("through.tar"),
&[
Raw::Symlink("evil", dir.to_str().unwrap()),
Raw::File("evil/pwned.txt", b"pwned"),
],
);
let out = run_output(&[
"unpack",
evil.to_str().unwrap(),
"-C",
dest.to_str().unwrap(),
]);
assert_eq!(
out.status.code(),
Some(7),
"an absolute symlink target must be refused (exit 7), stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
!dir.join("pwned.txt").exists(),
"the second entry was written through the symlink, outside the destination"
);
assert!(
std::fs::symlink_metadata(dest.join("evil")).is_err(),
"the symlink must not have been created"
);
}
#[test]
fn a_symlink_chain_cannot_walk_out_of_the_destination() {
let dir = tmp_dir();
let dest = dir.join("out");
let evil = write_raw_tar(
&dir.join("chain.tar"),
&[
Raw::Symlink("a", "b"),
Raw::Symlink("b", "../.."),
Raw::File("a/pwned.txt", b"pwned"),
],
);
let out = run_output(&[
"unpack",
evil.to_str().unwrap(),
"-C",
dest.to_str().unwrap(),
]);
assert_eq!(
out.status.code(),
Some(7),
"a chained symlink escape must be refused (exit 7), stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
!dir.join("pwned.txt").exists() && !dir.parent().unwrap().join("pwned.txt").exists(),
"the chain wrote outside the destination"
);
}
#[test]
fn a_traversal_buried_inside_a_deeper_path_is_refused() {
let dir = tmp_dir();
let dest = dir.join("out");
let evil = write_raw_tar(
&dir.join("deep.tar"),
&[Raw::File("a/b/../../../escaped.txt", b"pwned")],
);
let out = run_output(&[
"unpack",
evil.to_str().unwrap(),
"-C",
dest.to_str().unwrap(),
]);
assert_eq!(
out.status.code(),
Some(7),
"a buried traversal must be refused (exit 7), stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
!dir.join("escaped.txt").exists(),
"the buried traversal escaped the destination"
);
}
#[test]
fn a_directory_entry_that_escapes_the_destination_is_refused() {
let dir = tmp_dir();
let dest = dir.join("out");
let evil = write_raw_tar(&dir.join("dir.tar"), &[Raw::Dir("../escaped-dir/")]);
let out = run_output(&[
"unpack",
evil.to_str().unwrap(),
"-C",
dest.to_str().unwrap(),
]);
assert_eq!(
out.status.code(),
Some(7),
"an escaping directory entry must be refused (exit 7), stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
!dir.join("escaped-dir").exists(),
"the directory escaped the destination"
);
}
#[test]
fn an_entry_named_dot_extracts_alongside_the_real_entries() {
let dir = tmp_dir();
let dest = dir.join("out");
let archive = write_raw_tar(
&dir.join("dot.tar"),
&[Raw::Dir("./"), Raw::File("./a.txt", b"alpha")],
);
let out = run_output(&[
"unpack",
archive.to_str().unwrap(),
"-C",
dest.to_str().unwrap(),
]);
assert!(
out.status.success(),
"a `./` entry must be accepted, stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(std::fs::read(dest.join("a.txt")).unwrap(), b"alpha");
}
#[test]
fn a_system_tar_of_dot_extracts_cleanly() {
let Some(tar_bin) = which_tar() else {
eprintln!("system tar not found; skipping");
return;
};
let dir = tmp_dir();
let src = dir.join("src");
std::fs::create_dir_all(src.join("nested")).unwrap();
std::fs::write(src.join("top.txt"), b"top level").unwrap();
std::fs::write(src.join("nested/deep.txt"), b"nested payload").unwrap();
let archive = dir.join("dot.tar");
let status = Command::new(&tar_bin)
.arg("cf")
.arg(&archive)
.arg(".")
.current_dir(&src)
.status()
.unwrap();
assert!(
status.success(),
"the system tar failed to build the fixture"
);
let dest = dir.join("out");
let out = run_output(&[
"unpack",
archive.to_str().unwrap(),
"-C",
dest.to_str().unwrap(),
]);
assert!(
out.status.success(),
"`tar cf x.tar .` must extract cleanly, stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(std::fs::read(dest.join("top.txt")).unwrap(), b"top level");
assert_eq!(
std::fs::read(dest.join("nested/deep.txt")).unwrap(),
b"nested payload"
);
}
fn which_tar() -> Option<PathBuf> {
let path = std::env::var_os("PATH")?;
std::env::split_paths(&path)
.map(|dir| dir.join("tar"))
.find(|candidate| candidate.is_file())
}
#[cfg(unix)]
#[test]
fn extraction_writes_files_directories_and_contained_symlinks() {
let dir = tmp_dir();
let dest = dir.join("out");
let archive = write_raw_tar(
&dir.join("good.tar"),
&[
Raw::Dir("sub/"),
Raw::File("sub/a.txt", b"alpha"),
Raw::Symlink("sub/link", "a.txt"),
],
);
let out = run_output(&[
"unpack",
archive.to_str().unwrap(),
"-C",
dest.to_str().unwrap(),
]);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(dest.join("sub").is_dir());
assert_eq!(std::fs::read(dest.join("sub/a.txt")).unwrap(), b"alpha");
let link = std::fs::symlink_metadata(dest.join("sub/link")).unwrap();
assert!(
link.file_type().is_symlink(),
"a symlink entry must become a symlink"
);
assert_eq!(
std::fs::read_link(dest.join("sub/link")).unwrap(),
Path::new("a.txt")
);
assert_eq!(std::fs::read(dest.join("sub/link")).unwrap(), b"alpha");
}
#[test]
fn extraction_selects_only_the_entries_a_pattern_names() {
let dir = tmp_dir();
let dest = dir.join("out");
let archive = write_fixture_tar(
&dir,
&[
("a.txt", b"alpha"),
("b.txt", b"beta"),
("d/c.txt", b"gamma"),
],
);
let out = run_output(&[
"unpack",
archive.to_str().unwrap(),
"-C",
dest.to_str().unwrap(),
"b.txt",
"d",
]);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
!dest.join("a.txt").exists(),
"an unmatched entry was extracted"
);
assert_eq!(std::fs::read(dest.join("b.txt")).unwrap(), b"beta");
assert_eq!(std::fs::read(dest.join("d/c.txt")).unwrap(), b"gamma");
}
#[test]
fn a_pattern_matching_no_entry_is_a_usage_error_rather_than_a_silent_success() {
let dir = tmp_dir();
let archive = write_fixture_tar(&dir, &[("a.txt", b"alpha")]);
let out = run_output(&[
"unpack",
archive.to_str().unwrap(),
"-C",
dir.join("out").to_str().unwrap(),
"nosuch.txt",
]);
assert_eq!(
out.status.code(),
Some(2),
"extracting nothing at all must not report success, stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let out = run_output(&["cat", archive.to_str().unwrap(), "nosuch.txt"]);
assert_eq!(out.status.code(), Some(2), "cat must agree with unpack");
}
#[test]
fn extraction_refuses_an_existing_file_unless_force_is_given() {
let dir = tmp_dir();
let dest = dir.join("out");
std::fs::create_dir_all(&dest).unwrap();
std::fs::write(dest.join("a.txt"), b"mine").unwrap();
let archive = write_fixture_tar(&dir, &[("a.txt", b"alpha")]);
let out = run_output(&[
"unpack",
archive.to_str().unwrap(),
"-C",
dest.to_str().unwrap(),
]);
assert_eq!(
out.status.code(),
Some(2),
"an existing file must be refused like every other output, stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(std::fs::read(dest.join("a.txt")).unwrap(), b"mine");
let out = run_output(&[
"unpack",
archive.to_str().unwrap(),
"-C",
dest.to_str().unwrap(),
"--force",
]);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(std::fs::read(dest.join("a.txt")).unwrap(), b"alpha");
}
#[cfg(unix)]
#[test]
fn a_pre_existing_symlink_at_a_target_path_is_replaced_not_written_through() {
let dir = tmp_dir();
let dest = dir.join("out");
std::fs::create_dir_all(&dest).unwrap();
let outside = dir.join("outside.txt");
std::fs::write(&outside, b"untouched").unwrap();
std::os::unix::fs::symlink(&outside, dest.join("a.txt")).unwrap();
let archive = write_fixture_tar(&dir, &[("a.txt", b"alpha")]);
let out = run_output(&[
"unpack",
archive.to_str().unwrap(),
"-C",
dest.to_str().unwrap(),
"--force",
]);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(
std::fs::read(&outside).unwrap(),
b"untouched",
"the entry was written through the pre-existing symlink"
);
assert_eq!(std::fs::read(dest.join("a.txt")).unwrap(), b"alpha");
}
#[test]
fn extraction_of_a_corrupt_archive_exits_five_not_seven() {
let dir = tmp_dir();
let good = write_fixture_tar(&dir, &[("a.txt", b"alpha"), ("b.txt", b"beta")]);
let corrupt = corrupt_midway(&good);
let out = run_output(&[
"unpack",
corrupt.to_str().unwrap(),
"-C",
dir.join("out").to_str().unwrap(),
]);
assert_eq!(
out.status.code(),
Some(5),
"a corrupt archive is exit 5, stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn extraction_of_a_bomb_exits_six_at_a_tight_max_ratio() {
let dir = tmp_dir();
let bomb = write_bomb_tar_gz(&dir);
let out = run_output(&[
"unpack",
bomb.to_str().unwrap(),
"-C",
dir.join("out").to_str().unwrap(),
"--max-ratio",
"2",
]);
assert_eq!(
out.status.code(),
Some(6),
"a bomb is a resource limit (exit 6), stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn cat_streams_every_entry_a_pattern_names_and_bounds_the_ratio() {
let dir = tmp_dir();
let archive = write_fixture_tar(&dir, &[("a.txt", b"alpha"), ("b.txt", b"beta")]);
let out = run_output(&["cat", archive.to_str().unwrap(), "a.txt"]);
assert!(out.status.success());
assert_eq!(String::from_utf8(out.stdout).unwrap(), "alpha");
let bomb = write_bomb_tar_gz(&dir);
let out = run_output(&[
"cat",
bomb.to_str().unwrap(),
"bomb.bin",
"--max-ratio",
"2",
]);
assert_eq!(
out.status.code(),
Some(6),
"cat must bound an entry the same way extraction does, stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn an_archive_needs_a_destination_and_the_error_says_which_flag() {
let dir = tmp_dir();
let archive = write_fixture_tar(&dir, &[("a.txt", b"alpha")]);
let out = run_output(&["unpack", archive.to_str().unwrap()]);
assert_eq!(
out.status.code(),
Some(2),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("-C") && stderr.contains("tar"),
"the error must name both the container and the flag, got: {stderr}"
);
assert!(
!dir.join("fixture").exists(),
"nothing may be written for a refused command"
);
}
#[test]
fn pack_refuses_several_paths_when_the_output_is_not_a_container() {
let dir = tmp_dir();
std::fs::write(dir.join("one.txt"), b"1").unwrap();
std::fs::write(dir.join("two.txt"), b"2").unwrap();
let out = run_output(&[
"pack",
dir.join("one.txt").to_str().unwrap(),
dir.join("two.txt").to_str().unwrap(),
"-o",
dir.join("bundle.gz").to_str().unwrap(),
]);
assert_eq!(
out.status.code(),
Some(2),
"a codec cannot hold two inputs, stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
!dir.join("bundle.gz").exists(),
"a refused command must write nothing"
);
}
#[test]
fn pack_into_a_container_round_trips_through_extraction() {
let dir = tmp_dir();
std::fs::write(dir.join("one.txt"), b"first").unwrap();
std::fs::write(dir.join("two.txt"), b"second").unwrap();
let archive = dir.join("bundle.tar");
let out = run_output(&[
"pack",
dir.join("one.txt").to_str().unwrap(),
dir.join("two.txt").to_str().unwrap(),
"-o",
archive.to_str().unwrap(),
]);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let dest = dir.join("out");
let out = run_output(&[
"unpack",
archive.to_str().unwrap(),
"-C",
dest.to_str().unwrap(),
]);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(std::fs::read(dest.join("one.txt")).unwrap(), b"first");
assert_eq!(std::fs::read(dest.join("two.txt")).unwrap(), b"second");
}
#[test]
fn pack_refuses_two_inputs_that_would_share_one_entry_name() {
let dir = tmp_dir();
std::fs::create_dir_all(dir.join("a")).unwrap();
std::fs::create_dir_all(dir.join("b")).unwrap();
std::fs::write(dir.join("a/same.txt"), b"1").unwrap();
std::fs::write(dir.join("b/same.txt"), b"2").unwrap();
let archive = dir.join("dup.tar");
let out = run_output(&[
"pack",
dir.join("a/same.txt").to_str().unwrap(),
dir.join("b/same.txt").to_str().unwrap(),
"-o",
archive.to_str().unwrap(),
]);
assert_eq!(
out.status.code(),
Some(2),
"two entries with one name would make an archive that cannot be \
extracted without --force, stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(!archive.exists(), "a refused command must write nothing");
}
#[test]
fn pack_of_an_empty_directory_stores_the_directory_itself() {
let dir = tmp_dir();
std::fs::create_dir_all(dir.join("tree")).unwrap();
let archive = dir.join("tree.tar");
let out = run_output(&[
"pack",
dir.join("tree").to_str().unwrap(),
"-o",
archive.to_str().unwrap(),
]);
assert_eq!(
out.status.code(),
Some(0),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let listed = run_output(&["list", archive.to_str().unwrap()]);
let text = String::from_utf8_lossy(&listed.stdout);
assert!(
text.contains("tree"),
"the directory itself must be an entry, not an empty archive: {text}"
);
}
#[test]
fn flags_that_the_container_path_cannot_honour_are_refused_not_ignored() {
let dir = tmp_dir();
let archive = write_fixture_tar(&dir, &[("a.txt", b"alpha")]);
let dest = dir.join("out");
for extra in [vec!["--format", "gzip"], vec!["-o", "somewhere"]] {
let mut args = vec![
"unpack",
archive.to_str().unwrap(),
"-C",
dest.to_str().unwrap(),
];
args.extend_from_slice(&extra);
let out = run_output(&args);
assert_eq!(
out.status.code(),
Some(2),
"`{}` must be refused alongside -C, stderr: {}",
extra.join(" "),
String::from_utf8_lossy(&out.stderr)
);
}
}
#[test]
fn pack_flags_the_container_path_cannot_honour_are_refused_not_ignored() {
let dir = tmp_dir();
let src = dir.join("a.txt");
std::fs::write(&src, b"alpha").unwrap();
let archive = dir.join("bundle.tar");
for extra in [
vec!["--threads", "4"],
vec!["--turbo"],
vec!["--allow-weak-encoder"],
] {
let mut args = vec![
"pack",
src.to_str().unwrap(),
"-o",
archive.to_str().unwrap(),
];
args.extend_from_slice(&extra);
let out = run_output(&args);
assert_eq!(
out.status.code(),
Some(2),
"`{}` must be refused on the container pack path, stderr: {}",
extra.join(" "),
String::from_utf8_lossy(&out.stderr)
);
assert!(
!archive.exists(),
"a refused command must write nothing: {}",
extra.join(" ")
);
}
let out = run_output(&[
"pack",
src.to_str().unwrap(),
"--format",
"gzip",
"--threads",
"2",
"--force",
]);
assert!(
out.status.success(),
"the codec path still honours --threads, stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[cfg(unix)]
#[test]
fn an_entry_written_through_a_symlinked_path_component_is_refused() {
let dir = tmp_dir();
let dest = dir.join("out");
let evil = write_raw_tar(
&dir.join("component.tar"),
&[
Raw::Dir("a/b/"),
Raw::Symlink("a/b/up", ".."),
Raw::Symlink("a/b/up/link", "../.."),
Raw::File("a/b/up/link/pwned.txt", b"pwned"),
],
);
let out = run_output(&[
"unpack",
evil.to_str().unwrap(),
"-C",
dest.to_str().unwrap(),
]);
assert_eq!(
out.status.code(),
Some(7),
"an entry resolving through a symlinked component must be refused (exit 7), \
stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
!dir.join("pwned.txt").exists(),
"the entry escaped the destination through a symlinked path component"
);
}
#[cfg(unix)]
fn mode_of(path: &Path) -> u32 {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(path).unwrap().permissions().mode() & 0o7777
}
fn mtime_secs(path: &Path) -> u64 {
std::fs::metadata(path)
.unwrap()
.modified()
.unwrap()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
}
#[cfg(unix)]
#[test]
fn extraction_restores_modes_and_mtimes_for_files_and_directories() {
let dir = tmp_dir();
let dest = dir.join("out");
let archive = write_raw_tar(
&dir.join("meta.tar"),
&[
Raw::Dir("sub/"),
Raw::ModedFile("sub/script.sh", b"#!/bin/sh\n", 0o750),
Raw::ModedFile("readonly.txt", b"ro", 0o400),
],
);
let out = run_output(&[
"unpack",
archive.to_str().unwrap(),
"-C",
dest.to_str().unwrap(),
]);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(mode_of(&dest.join("sub/script.sh")), 0o750);
assert_eq!(mode_of(&dest.join("readonly.txt")), 0o400);
assert_eq!(mode_of(&dest.join("sub")), 0o755);
assert_eq!(mtime_secs(&dest.join("sub/script.sh")), FIXTURE_MTIME);
assert_eq!(mtime_secs(&dest.join("readonly.txt")), FIXTURE_MTIME);
assert_eq!(mtime_secs(&dest.join("sub")), FIXTURE_MTIME);
let strict = run_output(&[
"unpack",
archive.to_str().unwrap(),
"-C",
dir.join("strict").to_str().unwrap(),
"--strict-fidelity",
]);
assert!(
strict.status.success(),
"an extraction that lost nothing must pass --strict-fidelity, stderr: {}",
String::from_utf8_lossy(&strict.stderr)
);
}
#[cfg(unix)]
#[test]
fn a_setuid_bit_is_not_restored_and_strict_fidelity_fails_on_the_loss() {
let dir = tmp_dir();
let dest = dir.join("out");
let archive = write_raw_tar(
&dir.join("setuid.tar"),
&[Raw::ModedFile("suid", b"payload", 0o4755)],
);
let out = run_output(&[
"unpack",
archive.to_str().unwrap(),
"-C",
dest.to_str().unwrap(),
]);
assert!(
out.status.success(),
"the extraction itself succeeds, stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(
mode_of(&dest.join("suid")),
0o755,
"the setuid bit must not be restored"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("suid") && stderr.contains("mode"),
"the loss must be reported, naming the entry and the field: {stderr}"
);
let strict = run_output(&[
"unpack",
archive.to_str().unwrap(),
"-C",
dir.join("strict").to_str().unwrap(),
"--force",
"--strict-fidelity",
]);
assert_eq!(
strict.status.code(),
Some(4),
"a dropped mode must fail --strict-fidelity, stderr: {}",
String::from_utf8_lossy(&strict.stderr)
);
}
#[cfg(unix)]
#[test]
fn an_entry_with_no_shape_on_disk_is_skipped_and_said_out_loud() {
let dir = tmp_dir();
let dest = dir.join("out");
let archive = write_raw_tar(
&dir.join("fifo.tar"),
&[Raw::File("a.txt", b"alpha"), Raw::Fifo("pipe")],
);
let out = run_output(&[
"unpack",
archive.to_str().unwrap(),
"-C",
dest.to_str().unwrap(),
]);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(std::fs::read(dest.join("a.txt")).unwrap(), b"alpha");
assert!(
std::fs::symlink_metadata(dest.join("pipe")).is_err(),
"a fifo entry must not become a regular file"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("skipped entry") && stderr.contains("pipe"),
"the skip must name the entry and why: {stderr}"
);
let strict = run_output(&[
"unpack",
archive.to_str().unwrap(),
"-C",
dir.join("strict").to_str().unwrap(),
"--strict-fidelity",
]);
assert_eq!(
strict.status.code(),
Some(4),
"a skipped entry must fail --strict-fidelity, stderr: {}",
String::from_utf8_lossy(&strict.stderr)
);
}
#[cfg(unix)]
#[test]
fn a_symlinks_own_mode_and_mtime_are_reported_as_lost() {
let dir = tmp_dir();
let dest = dir.join("out");
let archive = write_raw_tar(
&dir.join("link.tar"),
&[Raw::File("a.txt", b"alpha"), Raw::Symlink("link", "a.txt")],
);
let out = run_output(&[
"unpack",
archive.to_str().unwrap(),
"-C",
dest.to_str().unwrap(),
"--strict-fidelity",
]);
assert_eq!(
out.status.code(),
Some(4),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("link") && stderr.contains("mtime"),
"the loss must name the entry and the field: {stderr}"
);
assert_eq!(
std::fs::read_link(dest.join("link")).unwrap(),
Path::new("a.txt")
);
}
#[test]
fn test_verb_passes_strict_fidelity_on_an_ordinary_tarball() {
let dir = tmp_dir();
let archive = write_fixture_tar(&dir, &[("a.txt", b"alpha")]);
let out = run_output(&["test", archive.to_str().unwrap(), "--strict-fidelity"]);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[cfg(unix)]
#[test]
fn an_st_mode_shaped_header_mode_raises_no_warning() {
let dir = tmp_dir();
let dest = dir.join("out");
let archive = write_raw_tar(
&dir.join("commons.tar"),
&[Raw::ModedFile("a.txt", b"alpha", 0o100_644)],
);
let out = run_output(&[
"unpack",
archive.to_str().unwrap(),
"-C",
dest.to_str().unwrap(),
"--strict-fidelity",
]);
assert!(
out.status.success(),
"an st_mode-shaped mode field must not fail the gate, stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("missing metadata"),
"nothing was lost, so nothing may be reported: {stderr}"
);
assert_eq!(
mode_of(&dest.join("a.txt")),
0o644,
"the permission bits apply; the type bits are not permissions"
);
let suid = write_raw_tar(
&dir.join("suid.tar"),
&[Raw::ModedFile("b.txt", b"beta", 0o104_755)],
);
let out = run_output(&[
"unpack",
suid.to_str().unwrap(),
"-C",
dir.join("suid-out").to_str().unwrap(),
"--strict-fidelity",
]);
assert_eq!(
out.status.code(),
Some(4),
"setuid inside an st_mode-shaped field is still a dropped mode, stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
}
fn craft_a_tar_lz_declaring_a_huge_dictionary(tag: &str) -> (PathBuf, PathBuf) {
let dir = tmp(tag);
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let tar_path = dir.join("bundle.tar");
write_raw_tar(
&tar_path,
&[Raw::File("a.txt", b"hello from inside the tar")],
);
assert!(
Command::new(STUFFR)
.args([
"pack",
tar_path.to_str().unwrap(),
"--format",
"lzip",
"--force"
])
.status()
.unwrap()
.success(),
"packing the fixture must succeed"
);
let legit = dir.join("bundle.tar.lz");
let mut bytes = std::fs::read(&legit).unwrap();
assert_eq!(&bytes[..4], b"LZIP", "fixture must really be lzip");
bytes[5] = 0x1D;
let crafted = dir.join("crafted.tar.lz");
std::fs::write(&crafted, &bytes).unwrap();
(legit, crafted)
}
#[test]
fn a_container_under_a_codec_declaring_a_huge_dictionary_is_refused_on_every_verb() {
let (_legit, crafted) = craft_a_tar_lz_declaring_a_huge_dictionary("c1-refuse");
let path = crafted.to_str().unwrap();
const LIMIT: &str = "64M";
for args in [
vec!["list", path, "--memory-limit", LIMIT],
vec!["test", path, "--memory-limit", LIMIT],
vec!["cat", path, "a.txt", "--memory-limit", LIMIT],
] {
let out = Command::new(STUFFR).args(&args).output().unwrap();
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
assert_eq!(
out.status.code(),
Some(6),
"`stuffr {}` must refuse a 512 MiB dictionary declaration at exit 6, \
not allocate for it; stderr: {stderr}",
args.join(" ")
);
assert!(
stderr.contains("memory-limit"),
"the refusal must name the flag that raises it; stderr: {stderr}"
);
}
let out = Command::new(STUFFR)
.args([
"unpack",
path,
"-o",
"/dev/null",
"--force",
"--memory-limit",
LIMIT,
])
.output()
.unwrap();
assert_eq!(
out.status.code(),
Some(6),
"the single-stream path's existing refusal must be unchanged; stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let mut child = Command::new(STUFFR)
.args(["list", "-", "--memory-limit", LIMIT])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let bytes = std::fs::read(&crafted).unwrap();
child.stdin.take().unwrap().write_all(&bytes).unwrap();
let out = child.wait_with_output().unwrap();
assert_eq!(
out.status.code(),
Some(6),
"a piped crafted archive must be refused too; stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn a_legitimate_archive_of_the_same_size_still_works_on_every_verb() {
let (legit, crafted) = craft_a_tar_lz_declaring_a_huge_dictionary("c1-allow");
let path = legit.to_str().unwrap();
assert_eq!(
std::fs::metadata(&legit).unwrap().len(),
std::fs::metadata(&crafted).unwrap().len(),
"the two fixtures must differ only in that one byte"
);
const LIMIT: &str = "64M";
let out = Command::new(STUFFR)
.args(["list", path, "--memory-limit", LIMIT])
.output()
.unwrap();
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
String::from_utf8_lossy(&out.stdout).contains("a.txt"),
"list must still show the entry"
);
let out = Command::new(STUFFR)
.args(["test", path, "--memory-limit", LIMIT])
.output()
.unwrap();
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let out = Command::new(STUFFR)
.args(["cat", path, "a.txt", "--memory-limit", LIMIT])
.output()
.unwrap();
assert!(out.status.success());
assert_eq!(out.stdout, b"hello from inside the tar");
let dest = tmp("c1-allow-dest");
let _ = std::fs::remove_dir_all(&dest);
let out = Command::new(STUFFR)
.args([
"unpack",
path,
"-C",
dest.to_str().unwrap(),
"--memory-limit",
LIMIT,
])
.output()
.unwrap();
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(dest.join("a.txt").exists());
let _ = std::fs::remove_dir_all(&dest);
}
#[test]
fn memory_limit_is_honoured_rather_than_refused_on_the_container_path() {
let (_legit, crafted) = craft_a_tar_lz_declaring_a_huge_dictionary("c1-flag");
let path = crafted.to_str().unwrap();
for args in [
vec!["list", path, "--memory-limit", "1G"],
vec!["test", path, "--memory-limit", "1G"],
vec!["cat", path, "a.txt", "--memory-limit", "1G"],
] {
let out = Command::new(STUFFR).args(&args).output().unwrap();
assert!(
out.status.success(),
"`stuffr {}` must succeed once the limit is raised past the declaration; \
stderr: {}",
args.join(" "),
String::from_utf8_lossy(&out.stderr)
);
}
let dest = tmp("c1-flag-dest");
let _ = std::fs::remove_dir_all(&dest);
let out = Command::new(STUFFR)
.args([
"unpack",
path,
"-C",
dest.to_str().unwrap(),
"--memory-limit",
"1G",
])
.output()
.unwrap();
assert!(
out.status.success(),
"unpack -C must accept --memory-limit rather than refusing it; stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let _ = std::fs::remove_dir_all(&dest);
}
#[test]
fn a_symlink_pointing_at_the_extraction_root_is_extracted_not_refused() {
let dir = tmp_dir();
let dest = dir.join("out");
let archive = write_raw_tar(
&dir.join("root-link.tar"),
&[
Raw::Dir("sub"),
Raw::Symlink("sub/top", ".."),
Raw::File("sub/a.txt", b"alpha"),
],
);
let out = run_output(&[
"unpack",
archive.to_str().unwrap(),
"-C",
dest.to_str().unwrap(),
]);
assert!(
out.status.success(),
"a symlink netting to the destination is contained, not an escape; stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let link = dest.join("sub/top");
assert!(
std::fs::symlink_metadata(&link).is_ok(),
"the symlink must have been created"
);
assert_eq!(std::fs::read_link(&link).unwrap(), Path::new(".."));
assert!(dest.join("sub/a.txt").exists(), "extraction must not abort");
}
#[test]
fn a_symlink_one_step_past_the_extraction_root_is_still_refused() {
for (tag, target) in [
("i1-two-up", "../.."),
("i1-deep", "../../../etc"),
("i1-mixed", "../sub/../.."),
] {
let dir = tmp_dir();
let dest = dir.join(tag);
let archive = write_raw_tar(
&dir.join(format!("{tag}.tar")),
&[Raw::Dir("sub"), Raw::Symlink("sub/top", target)],
);
let out = run_output(&[
"unpack",
archive.to_str().unwrap(),
"-C",
dest.to_str().unwrap(),
]);
assert_eq!(
out.status.code(),
Some(7),
"`{target}` escapes and must still be refused, stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
assert!(
stderr.contains(target),
"the refusal must name the archive's own target `{target}`, got: {stderr}"
);
assert!(
std::fs::symlink_metadata(dest.join("sub/top")).is_err(),
"the symlink must not have been created"
);
}
}
#[test]
fn info_does_not_claim_nothing_was_approximated_for_a_container_under_a_codec() {
let dir = tmp("info-codec-wrapped-matrix");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let one = dir.join("one.txt");
std::fs::write(&one, b"payload").unwrap();
for (ext, has_trailing_index) in [("zip", true), ("tar", false), ("cpio", false), ("a", false)]
{
let bare = dir.join(format!("bundle.{ext}"));
assert!(
Command::new(STUFFR)
.args(["pack", one.to_str().unwrap(), "-o", bare.to_str().unwrap()])
.status()
.unwrap()
.success(),
"packing a .{ext} must succeed"
);
assert!(
Command::new(STUFFR)
.args([
"pack",
bare.to_str().unwrap(),
"--format",
"gzip",
"--force"
])
.status()
.unwrap()
.success(),
"wrapping the .{ext} in gzip must succeed"
);
let wrapped = dir.join(format!("bundle.{ext}.gz"));
let bytes = std::fs::read(&wrapped).unwrap();
let strict = Command::new(STUFFR)
.args(["test", wrapped.to_str().unwrap(), "--strict-fidelity"])
.output()
.unwrap();
let strict_err = String::from_utf8_lossy(&strict.stderr).to_string();
let really_loses = !strict.status.success();
assert_eq!(
really_loses, has_trailing_index,
".{ext}.gz: only a trailing index can be missed by a forward read; \
stderr: {strict_err}"
);
let text = String::from_utf8(
Command::new(STUFFR)
.args(["info", wrapped.to_str().unwrap()])
.output()
.unwrap()
.stdout,
)
.unwrap();
assert!(
text.contains("exact"),
".{ext}.gz from a file: the rung is real and unchanged: {text}"
);
assert!(
text.contains(ext),
".{ext}.gz from a file must resolve the whole chain: {text}"
);
if has_trailing_index {
assert!(
text.contains("not evaluated"),
".{ext}.gz from a file: the zip is read out of a gzip decoder, which is \
forward-only however seekable the FILE is — `test` on these very bytes \
exits 4. info has not looked and must not claim: {text}"
);
assert!(!text.contains("nothing approximated"), "{text}");
} else {
assert!(
text.contains("nothing approximated"),
".{ext}.gz from a file carries every entry's metadata inline, so a forward \
read loses nothing — withholding here would be a false negative, the \
round-3 defect repeating: {text}"
);
assert!(!text.contains("not evaluated"), "{text}");
}
let text = info_over_stdin(&bytes);
assert!(text.contains("forward-only"), ".{ext}.gz on a pipe: {text}");
assert!(
text.contains("chain: gzip"),
".{ext}.gz on a pipe resolves to the codec alone — info does not decode to \
look inside: {text}"
);
let text = String::from_utf8(
Command::new(STUFFR)
.args(["info", bare.to_str().unwrap()])
.output()
.unwrap()
.stdout,
)
.unwrap();
assert!(
text.contains("nothing approximated"),
"a bare seekable .{ext} is unaffected by the codec-wrapped rule: {text}"
);
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn every_entry_aware_verb_applies_the_same_expansion_bound() {
let dir = tmp_dir();
let payload = vec![0u8; 8 * 1024 * 1024];
let big = dir.join("big.bin");
std::fs::write(&big, &payload).unwrap();
let bomb = dir.join("bomb.zip");
assert!(
Command::new(STUFFR)
.args(["pack", big.to_str().unwrap(), "-o", bomb.to_str().unwrap()])
.status()
.unwrap()
.success()
);
let compressed = std::fs::metadata(&bomb).unwrap().len();
assert!(
compressed * 10 < payload.len() as u64,
"the fixture must actually exceed a ratio of 10 ({compressed} compressed)"
);
let dest = dir.join("out");
for args in [
vec!["test", bomb.to_str().unwrap(), "--max-ratio", "10"],
vec![
"cat",
bomb.to_str().unwrap(),
"big.bin",
"--max-ratio",
"10",
],
vec![
"unpack",
bomb.to_str().unwrap(),
"-C",
dest.to_str().unwrap(),
"--max-ratio",
"10",
],
] {
let out = Command::new(STUFFR).args(&args).output().unwrap();
assert_eq!(
out.status.code(),
Some(6),
"`stuffr {}` must refuse the same bomb the other verbs refuse; stderr: {}",
args[0],
String::from_utf8_lossy(&out.stderr)
);
}
let _ = std::fs::remove_dir_all(&dest);
for args in [
vec!["test", bomb.to_str().unwrap()],
vec!["cat", bomb.to_str().unwrap(), "big.bin"],
vec![
"unpack",
bomb.to_str().unwrap(),
"-C",
dest.to_str().unwrap(),
],
] {
let out = Command::new(STUFFR).args(&args).output().unwrap();
assert!(
out.status.success(),
"`stuffr {}` must still accept a legitimate archive under the default ratio; \
stderr: {}",
args[0],
String::from_utf8_lossy(&out.stderr)
);
}
}
#[test]
fn a_piped_archive_larger_than_the_ratio_floor_is_not_refused() {
let dir = tmp_dir();
let payload: Vec<u8> = (0..3u32 * 1024 * 1024).map(|i| (i % 251) as u8).collect();
let src = dir.join("big.bin");
std::fs::write(&src, &payload).unwrap();
let archive = dir.join("big.tar");
assert!(
Command::new(STUFFR)
.args([
"pack",
src.to_str().unwrap(),
"-o",
archive.to_str().unwrap()
])
.status()
.unwrap()
.success()
);
let bytes = std::fs::read(&archive).unwrap();
assert!(
bytes.len() as u64 > stuffr::RATIO_FLOOR,
"the fixture must exceed the floor or this test proves nothing"
);
let piped = |args: &[&str], input: &[u8]| {
let mut child = Command::new(STUFFR)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let mut sin = child.stdin.take().unwrap();
let data = input.to_vec();
let writer = std::thread::spawn(move || {
let _ = sin.write_all(&data);
});
let out = child.wait_with_output().unwrap();
writer.join().unwrap();
out
};
let dest = dir.join("out");
for args in [
vec!["test", "-"],
vec!["cat", "-", "big.bin"],
vec!["unpack", "-", "-C", dest.to_str().unwrap()],
] {
let out = piped(&args, &bytes);
assert!(
out.status.success(),
"`stuffr {}` over a pipe must not refuse a {}-byte archive; stderr: {}",
args.join(" "),
bytes.len(),
String::from_utf8_lossy(&out.stderr)
);
}
let zeros = vec![0u8; 8 * 1024 * 1024];
let zsrc = dir.join("zeros.bin");
std::fs::write(&zsrc, &zeros).unwrap();
let bomb = dir.join("bomb.zip");
assert!(
Command::new(STUFFR)
.args(["pack", zsrc.to_str().unwrap(), "-o", bomb.to_str().unwrap()])
.status()
.unwrap()
.success()
);
let bomb_bytes = std::fs::read(&bomb).unwrap();
let piped_test = |extra: &[&str]| {
let mut argv = vec!["test", "-"];
argv.extend_from_slice(extra);
piped(&argv, &bomb_bytes)
};
assert_eq!(
piped_test(&["--max-ratio", "10"]).status.code(),
Some(6),
"a bomb piped in must still be refused"
);
assert!(
piped_test(&["--max-ratio", "100000"]).status.success(),
"raising --max-ratio must change the outcome on a pipe — that is what \
proves the flag reaches this path rather than being parsed and dropped"
);
}
#[test]
fn a_valid_odc_cpio_archive_reports_a_capability_limit_not_corruption() {
let dir = tmp_dir();
let src = dir.join("f.txt");
std::fs::write(&src, b"payload").unwrap();
let newc = dir.join("archive.cpio");
assert!(
Command::new(STUFFR)
.args(["pack", src.to_str().unwrap(), "-o", newc.to_str().unwrap()])
.status()
.unwrap()
.success()
);
let mut bytes = std::fs::read(&newc).unwrap();
assert_eq!(&bytes[..6], b"070701");
bytes[..6].copy_from_slice(b"070707");
let odc = dir.join("odc.cpio");
std::fs::write(&odc, &bytes).unwrap();
let out = Command::new(STUFFR)
.args(["list", odc.to_str().unwrap()])
.output()
.unwrap();
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
assert_eq!(
out.status.code(),
Some(3),
"an intact archive in an unreadable variant is a capability limit, \
not damage; stderr: {stderr}"
);
assert!(
stderr.contains("odc"),
"the message must name the variant: {stderr}"
);
assert!(
!stderr.contains("corrupt"),
"the message must not suggest damage: {stderr}"
);
let mut broken = std::fs::read(&newc).unwrap();
broken.truncate(30);
let cut = dir.join("cut.cpio");
std::fs::write(&cut, &broken).unwrap();
let out = Command::new(STUFFR)
.args(["list", cut.to_str().unwrap()])
.output()
.unwrap();
assert_eq!(
out.status.code(),
Some(5),
"a truncated archive really is damaged; stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn pointing_a_verb_at_the_wrong_file_always_exits_two() {
let dir = tmp_dir();
let plain = dir.join("plain.txt");
std::fs::write(&plain, b"just some text, no format at all").unwrap();
let gz = dir.join("plain.txt.gz");
assert!(
Command::new(STUFFR)
.args([
"pack",
plain.to_str().unwrap(),
"--format",
"gzip",
"-o",
gz.to_str().unwrap()
])
.status()
.unwrap()
.success()
);
for (label, path) in [
("a codec stream, which is not an archive", &gz),
("a file of no recognised format", &plain),
] {
let out = Command::new(STUFFR)
.args(["list", path.to_str().unwrap()])
.output()
.unwrap();
assert_eq!(
out.status.code(),
Some(2),
"{label}: both are the caller pointing `list` at the wrong file, \
so both are exit 2; stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
}
}
#[test]
fn no_cli_message_contains_a_run_of_collapsed_indentation() {
let dir = tmp_dir();
let a = dir.join("a.txt");
let b = dir.join("b.txt");
std::fs::write(&a, b"alpha").unwrap();
std::fs::write(&b, b"beta").unwrap();
let archive = write_fixture_tar(&dir, &[("x.txt", b"x")]);
let cases: Vec<Vec<String>> = vec![
vec![
"pack".into(),
a.to_str().unwrap().into(),
b.to_str().unwrap().into(),
],
vec![
"pack".into(),
a.to_str().unwrap().into(),
b.to_str().unwrap().into(),
"--format".into(),
"tar".into(),
],
vec![
"unpack".into(),
archive.to_str().unwrap().into(),
"x.txt".into(),
],
];
for args in cases {
let out = Command::new(STUFFR).args(&args).output().unwrap();
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
assert_eq!(
out.status.code(),
Some(2),
"`stuffr {}` must be a usage error; stderr: {stderr}",
args.join(" ")
);
assert!(
!stderr.contains(" "),
"a CLI message must not carry a run of collapsed continuation \
indentation; `stuffr {}` printed: {stderr:?}",
args.join(" ")
);
assert!(
!stderr.trim().is_empty(),
"and it must actually say something: {stderr:?}"
);
}
let out = Command::new(STUFFR)
.args(["pack", a.to_str().unwrap(), b.to_str().unwrap()])
.output()
.unwrap();
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
assert!(
stderr.contains("`-o bundle.tar`, or --format tar"),
"the phrase either side of the old collapse must read as one sentence: {stderr:?}"
);
}
#[test]
fn a_container_over_a_codec_is_written_in_one_step() {
let dir = tmp_dir();
let src = dir.join("notes.txt");
std::fs::write(&src, b"hello from inside the tar\n").unwrap();
let out = dir.join("bundle.tar.gz");
let st = Command::new(STUFFR)
.args(["pack", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
.output()
.unwrap();
assert!(
st.status.success(),
"stderr: {}",
String::from_utf8_lossy(&st.stderr)
);
let t = Command::new("gunzip")
.args(["-t", out.to_str().unwrap()])
.output()
.unwrap();
assert!(
t.status.success(),
"gunzip -t rejected it: {}",
String::from_utf8_lossy(&t.stderr)
);
let l = Command::new("tar")
.args(["tzf", out.to_str().unwrap()])
.output()
.unwrap();
assert!(
l.status.success(),
"tar tzf failed: {}",
String::from_utf8_lossy(&l.stderr)
);
assert!(String::from_utf8_lossy(&l.stdout).contains("notes.txt"));
let r = Command::new(STUFFR)
.args(["list", out.to_str().unwrap()])
.output()
.unwrap();
assert!(r.status.success());
assert!(String::from_utf8_lossy(&r.stdout).contains("notes.txt"));
}
#[test]
fn a_dot_tar_dot_gz_output_is_never_silently_the_wrong_format() {
let dir = tmp_dir();
let src = dir.join("notes.txt");
std::fs::write(&src, b"payload\n").unwrap();
for (name, extra) in [
("a.tar.gz", vec![]),
("b.tgz", vec![]),
("c.tar.gz", vec!["--format", "tar"]),
] {
let out = dir.join(name);
let mut args = vec!["pack", src.to_str().unwrap(), "-o", out.to_str().unwrap()];
args.extend(extra);
let st = Command::new(STUFFR).args(&args).output().unwrap();
assert!(
st.status.success(),
"{name}: {}",
String::from_utf8_lossy(&st.stderr)
);
let t = Command::new("gunzip")
.args(["-t", out.to_str().unwrap()])
.output()
.unwrap();
assert!(
t.status.success(),
"{name} is not valid gzip: {}",
String::from_utf8_lossy(&t.stderr)
);
let l = Command::new("tar")
.args(["tzf", out.to_str().unwrap()])
.output()
.unwrap();
assert!(
l.status.success(),
"{name} has no tar inside: {}",
String::from_utf8_lossy(&l.stderr)
);
assert!(
String::from_utf8_lossy(&l.stdout).contains("notes.txt"),
"{name}"
);
}
}
#[test]
fn a_codec_only_or_container_only_output_is_unaffected() {
let dir = tmp_dir();
let src = dir.join("notes.txt");
std::fs::write(&src, b"payload\n").unwrap();
let gz = dir.join("plain.gz");
assert!(
Command::new(STUFFR)
.args(["pack", src.to_str().unwrap(), "-o", gz.to_str().unwrap()])
.status()
.unwrap()
.success()
);
let out = Command::new("gunzip")
.args(["-c", gz.to_str().unwrap()])
.output()
.unwrap();
assert_eq!(out.stdout, b"payload\n");
let tar = dir.join("plain.tar");
assert!(
Command::new(STUFFR)
.args(["pack", src.to_str().unwrap(), "-o", tar.to_str().unwrap()])
.status()
.unwrap()
.success()
);
let l = Command::new("tar")
.args(["tf", tar.to_str().unwrap()])
.output()
.unwrap();
assert!(String::from_utf8_lossy(&l.stdout).contains("notes.txt"));
}
#[test]
fn a_zip_round_trips_bare_and_inside_a_codec() {
let dir = tmp_dir();
let src = dir.join("notes.txt");
std::fs::write(&src, b"zip payload\n").unwrap();
for name in ["bare.zip", "wrapped.zip.gz"] {
let out = dir.join(name);
let st = Command::new(STUFFR)
.args(["pack", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
.output()
.unwrap();
assert!(
st.status.success(),
"{name}: {}",
String::from_utf8_lossy(&st.stderr)
);
if name.ends_with(".gz") {
let t = Command::new("gunzip")
.args(["-t", out.to_str().unwrap()])
.output()
.unwrap();
assert!(
t.status.success(),
"{name} is not valid gzip: {}",
String::from_utf8_lossy(&t.stderr)
);
}
let r = Command::new(STUFFR)
.args(["list", out.to_str().unwrap()])
.output()
.unwrap();
assert!(
r.status.success(),
"{name}: {}",
String::from_utf8_lossy(&r.stderr)
);
assert!(
String::from_utf8_lossy(&r.stdout).contains("notes.txt"),
"{name}"
);
}
}
#[test]
fn a_format_flag_contradicting_the_output_name_is_refused_before_anything_is_written() {
let dir = tmp_dir();
let src = dir.join("notes.txt");
std::fs::write(&src, b"payload\n").unwrap();
for (fmt, name) in [
("zip", "x.tar.gz"),
("tar", "x.zip.gz"),
("tar", "x.cpio"),
("cpio", "x.tgz"),
] {
let out = dir.join(name);
let o = Command::new(STUFFR)
.args([
"pack",
src.to_str().unwrap(),
"--format",
fmt,
"-o",
out.to_str().unwrap(),
])
.output()
.unwrap();
assert_eq!(
o.status.code(),
Some(2),
"--format {fmt} -o {name} must be a usage error, not a written file"
);
let stderr = String::from_utf8_lossy(&o.stderr).to_string();
assert!(
stderr.contains(fmt) && stderr.contains(name),
"the message must name both sides of the contradiction: {stderr:?}"
);
assert!(!out.exists(), "--format {fmt} -o {name} left a file behind");
}
for (fmt, name) in [("tar", "ok.tar.gz"), ("tar", "ok.gz"), ("zip", "ok.zip.gz")] {
let out = dir.join(name);
let o = Command::new(STUFFR)
.args([
"pack",
src.to_str().unwrap(),
"--format",
fmt,
"-o",
out.to_str().unwrap(),
])
.output()
.unwrap();
assert!(
o.status.success(),
"--format {fmt} -o {name} must still work: {}",
String::from_utf8_lossy(&o.stderr)
);
let l = Command::new(STUFFR)
.args(["list", out.to_str().unwrap()])
.output()
.unwrap();
assert!(
l.status.success(),
"stuffr must be able to read back what it wrote to {name}: {}",
String::from_utf8_lossy(&l.stderr)
);
assert!(
String::from_utf8_lossy(&l.stdout).contains("notes.txt"),
"{name}"
);
}
}
#[test]
fn the_composed_path_honours_stuffr_threads_now_that_a_codec_is_present() {
let dir = tmp_dir();
let src = dir.join("big.txt");
let mut data = Vec::with_capacity(4 << 20);
let mut x: u32 = 0x1234_5678;
while data.len() < (4 << 20) {
x = x.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
data.extend_from_slice(format!("line {} of the corpus\n", x >> 16).as_bytes());
}
std::fs::write(&src, &data).unwrap();
let plain = dir.join("plain.tar.xz");
let env = dir.join("env.tar.xz");
for (out, threads) in [(&plain, None), (&env, Some("4"))] {
let mut cmd = Command::new(STUFFR);
cmd.args([
"pack",
src.to_str().unwrap(),
"-o",
out.to_str().unwrap(),
"--level",
"1",
]);
match threads {
Some(n) => cmd.env("STUFFR_THREADS", n),
None => cmd.env_remove("STUFFR_THREADS"),
};
let o = cmd.output().unwrap();
assert!(
o.status.success(),
"pack failed: {}",
String::from_utf8_lossy(&o.stderr)
);
}
assert_ne!(
std::fs::read(&plain).unwrap(),
std::fs::read(&env).unwrap(),
"STUFFR_THREADS=4 must now change the bytes on a composed write with a codec \
layer, the same way it already does on a single-stream one; identical bytes \
here means the governor silently went back to None"
);
let flagged = Command::new(STUFFR)
.args([
"pack",
src.to_str().unwrap(),
"-o",
dir.join("flag.tar.xz").to_str().unwrap(),
"--threads",
"4",
])
.output()
.unwrap();
assert!(
flagged.status.success(),
"--threads must be honoured where a codec is present: {}",
String::from_utf8_lossy(&flagged.stderr)
);
}
#[test]
fn encoder_flags_are_refused_only_where_there_is_no_codec_to_honour_them() {
let dir = tmp_dir();
let src = dir.join("notes.txt");
std::fs::write(&src, b"payload\n").unwrap();
let st = Command::new(STUFFR)
.args([
"pack",
src.to_str().unwrap(),
"-o",
dir.join("a.tar").to_str().unwrap(),
"--threads",
"2",
])
.output()
.unwrap();
assert_eq!(
st.status.code(),
Some(2),
"a bare container has no codec to hand threads to"
);
let out = dir.join("b.tar.gz");
let st = Command::new(STUFFR)
.args([
"pack",
src.to_str().unwrap(),
"-o",
out.to_str().unwrap(),
"--threads",
"2",
])
.output()
.unwrap();
assert!(
st.status.success(),
"a composed write HAS a codec: {}",
String::from_utf8_lossy(&st.stderr)
);
assert!(
Command::new("gunzip")
.args(["-t", out.to_str().unwrap()])
.status()
.unwrap()
.success()
);
let big = dir.join("big.txt");
let mut payload = String::new();
for i in 0..4000 {
payload.push_str(&format!(
"line {i} of a log with some repeated shape {}\n",
i % 97
));
}
std::fs::write(&big, payload.as_bytes()).unwrap();
let mut sizes = Vec::new();
for level in ["1", "9"] {
let out = dir.join(format!("c{level}.tar.gz"));
assert!(
Command::new(STUFFR)
.args([
"pack",
big.to_str().unwrap(),
"-o",
out.to_str().unwrap(),
"--level",
level,
])
.status()
.unwrap()
.success(),
"--level {level} must be accepted on a composed write"
);
assert!(
Command::new("gunzip")
.args(["-t", out.to_str().unwrap()])
.status()
.unwrap()
.success(),
"--level {level} must still produce valid gzip"
);
sizes.push(std::fs::metadata(&out).unwrap().len());
}
assert!(
sizes[0] > sizes[1],
"--level must reach the codec beneath the container: level 1 produced \
{} bytes and level 9 produced {} — equal sizes mean the flag was \
accepted and dropped",
sizes[0],
sizes[1]
);
assert!(
Command::new(STUFFR)
.args([
"pack",
src.to_str().unwrap(),
"-o",
dir.join("d.gz").to_str().unwrap(),
"--threads",
"2",
])
.status()
.unwrap()
.success()
);
}
#[test]
fn pack_walks_a_directory_and_round_trips_it() {
let dir = tmp_dir();
let root = dir.join("proj");
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::create_dir(root.join("empty")).unwrap();
std::fs::write(root.join("README.md"), b"readme").unwrap();
std::fs::write(root.join("src/main.rs"), b"fn main() {}").unwrap();
let out = dir.join("p.tar");
let st = Command::new(STUFFR)
.args(["pack", root.to_str().unwrap(), "-o", out.to_str().unwrap()])
.output()
.unwrap();
assert!(
st.status.success(),
"stderr: {}",
String::from_utf8_lossy(&st.stderr)
);
let back = dir.join("back");
assert!(
Command::new(STUFFR)
.args([
"unpack",
out.to_str().unwrap(),
"-C",
back.to_str().unwrap()
])
.status()
.unwrap()
.success()
);
assert_eq!(
std::fs::read(back.join("proj/README.md")).unwrap(),
b"readme"
);
assert_eq!(
std::fs::read(back.join("proj/src/main.rs")).unwrap(),
b"fn main() {}"
);
assert!(
back.join("proj/empty").is_dir(),
"an empty directory must survive the round trip"
);
}
#[test]
fn pack_walks_a_directory_into_a_container_inside_a_codec() {
let dir = tmp_dir();
let root = dir.join("proj");
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::write(root.join("src/main.rs"), b"fn main() {}").unwrap();
let out = dir.join("backup.tar.gz");
let st = Command::new(STUFFR)
.args(["pack", root.to_str().unwrap(), "-o", out.to_str().unwrap()])
.output()
.unwrap();
assert!(
st.status.success(),
"stderr: {}",
String::from_utf8_lossy(&st.stderr)
);
let back = dir.join("back");
assert!(
Command::new(STUFFR)
.args([
"unpack",
out.to_str().unwrap(),
"-C",
back.to_str().unwrap()
])
.status()
.unwrap()
.success()
);
assert_eq!(
std::fs::read(back.join("proj/src/main.rs")).unwrap(),
b"fn main() {}"
);
}
#[cfg(unix)]
#[test]
fn pack_stores_a_symlink_inside_a_directory_rather_than_following_it() {
let dir = tmp_dir();
let root = dir.join("proj");
std::fs::create_dir(&root).unwrap();
std::fs::write(root.join("real.txt"), b"real").unwrap();
std::os::unix::fs::symlink("real.txt", root.join("link.txt")).unwrap();
let out = dir.join("p.tar");
assert!(
Command::new(STUFFR)
.args(["pack", root.to_str().unwrap(), "-o", out.to_str().unwrap()])
.status()
.unwrap()
.success()
);
let l = Command::new("tar")
.args(["tvf", out.to_str().unwrap()])
.output()
.expect("the system `tar` is this test's reference authority");
let text = String::from_utf8_lossy(&l.stdout);
assert!(
text.contains("link.txt -> real.txt"),
"tar tvf said: {text}"
);
}
#[cfg(unix)]
#[test]
fn pack_follows_a_symlink_named_on_the_command_line() {
let dir = tmp_dir();
std::fs::write(dir.join("real.txt"), b"real").unwrap();
std::os::unix::fs::symlink("real.txt", dir.join("link.txt")).unwrap();
let out = dir.join("named.tar");
assert!(
Command::new(STUFFR)
.args([
"pack",
dir.join("link.txt").to_str().unwrap(),
"-o",
out.to_str().unwrap()
])
.status()
.unwrap()
.success()
);
let back = dir.join("back");
assert!(
Command::new(STUFFR)
.args([
"unpack",
out.to_str().unwrap(),
"-C",
back.to_str().unwrap()
])
.status()
.unwrap()
.success()
);
assert_eq!(
std::fs::read(back.join("link.txt")).unwrap(),
b"real",
"a named symlink is followed, so the entry holds the target's bytes"
);
}
#[test]
fn packing_a_directory_into_a_container_that_cannot_hold_it_warns_rather_than_refusing() {
let dir = tmp_dir();
let root = dir.join("proj");
std::fs::create_dir_all(root.join("sub")).unwrap();
std::fs::write(root.join("sub/a.txt"), b"a").unwrap();
let out = dir.join("p.a");
let st = Command::new(STUFFR)
.args(["pack", root.to_str().unwrap(), "-o", out.to_str().unwrap()])
.output()
.unwrap();
assert!(
st.status.success(),
"must not refuse: {}",
String::from_utf8_lossy(&st.stderr)
);
let err = String::from_utf8_lossy(&st.stderr);
assert!(
err.contains("proj/sub"),
"it must SAY WHAT it dropped, by name; stderr: {err}"
);
let out2 = dir.join("p2.a");
let st2 = Command::new(STUFFR)
.args([
"pack",
root.to_str().unwrap(),
"-o",
out2.to_str().unwrap(),
"--strict-fidelity",
])
.output()
.unwrap();
assert_eq!(
st2.status.code(),
Some(4),
"strict fidelity must exit 4 on the write side too"
);
}
#[test]
fn strict_fidelity_passes_on_a_pack_that_lost_nothing() {
let dir = tmp_dir();
let root = dir.join("proj");
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::write(root.join("src/main.rs"), b"fn main() {}").unwrap();
#[cfg(unix)]
std::os::unix::fs::symlink("main.rs", root.join("src/alias.rs")).unwrap();
let archive = dir.join("clean.tar");
let st = Command::new(STUFFR)
.args([
"pack",
root.to_str().unwrap(),
"-o",
archive.to_str().unwrap(),
"--strict-fidelity",
])
.output()
.unwrap();
assert!(
st.status.success(),
"an ordinary tree into tar loses nothing, symlink included: {}",
String::from_utf8_lossy(&st.stderr)
);
#[cfg(unix)]
{
let listed = Command::new("tar")
.args(["tvf", archive.to_str().unwrap()])
.output()
.expect("the system `tar` is this test's reference authority");
let text = String::from_utf8_lossy(&listed.stdout);
assert!(
text.contains("alias.rs -> main.rs"),
"the symlink must be stored as a link, not dropped or copied: {text}"
);
}
}
#[test]
fn two_directories_with_the_same_final_component_are_refused_before_writing() {
let dir = tmp_dir();
std::fs::create_dir_all(dir.join("a/proj")).unwrap();
std::fs::create_dir_all(dir.join("b/proj")).unwrap();
std::fs::write(dir.join("a/proj/x.txt"), b"x").unwrap();
std::fs::write(dir.join("b/proj/y.txt"), b"y").unwrap();
let out = dir.join("dup.tar");
let st = Command::new(STUFFR)
.args([
"pack",
dir.join("a/proj").to_str().unwrap(),
dir.join("b/proj").to_str().unwrap(),
"-o",
out.to_str().unwrap(),
])
.output()
.unwrap();
assert_eq!(
st.status.code(),
Some(2),
"a name collision is a usage error"
);
assert!(
!out.exists(),
"nothing may be written: the refusal happens before the destination is opened"
);
}
#[test]
fn pack_of_dot_names_entries_after_the_directory_it_resolves_to() {
let dir = tmp_dir();
let root = dir.join("proj");
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::write(root.join("src/main.rs"), b"fn main() {}").unwrap();
let out = dir.join("dot.tar");
let st = Command::new(STUFFR)
.current_dir(&root)
.args(["pack", ".", "-o", out.to_str().unwrap()])
.output()
.unwrap();
assert!(
st.status.success(),
"`pack .` must work: {}",
String::from_utf8_lossy(&st.stderr)
);
let listed = run_output(&["list", out.to_str().unwrap()]);
let text = String::from_utf8_lossy(&listed.stdout);
assert!(
text.contains("proj/src/main.rs"),
"`.` resolves to its canonical final component, so entries sit under \
`proj/` exactly as `pack ../proj` would name them: {text}"
);
}
#[test]
fn pack_of_dotdot_names_entries_after_the_parent_it_resolves_to() {
let dir = tmp_dir();
let root = dir.join("proj");
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::write(root.join("src/main.rs"), b"fn main() {}").unwrap();
let out = dir.join("dotdot.tar");
let st = Command::new(STUFFR)
.current_dir(root.join("src"))
.args(["pack", "..", "-o", out.to_str().unwrap()])
.output()
.unwrap();
assert!(
st.status.success(),
"`pack ..` must work: {}",
String::from_utf8_lossy(&st.stderr)
);
let listed = run_output(&["list", out.to_str().unwrap()]);
let text = String::from_utf8_lossy(&listed.stdout);
assert!(
text.contains("proj/src/main.rs"),
"`..` must name entries after the directory it resolves to: {text}"
);
}
#[test]
fn a_trailing_slash_on_a_packed_directory_is_noise() {
let dir = tmp_dir();
let root = dir.join("proj");
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::write(root.join("src/main.rs"), b"fn main() {}").unwrap();
let out = dir.join("slash.tar");
let with_slash = format!("{}/", root.to_str().unwrap());
let st = Command::new(STUFFR)
.args(["pack", &with_slash, "-o", out.to_str().unwrap()])
.output()
.unwrap();
assert!(
st.status.success(),
"a trailing slash must not change anything: {}",
String::from_utf8_lossy(&st.stderr)
);
let listed = run_output(&["list", out.to_str().unwrap()]);
let text = String::from_utf8_lossy(&listed.stdout);
assert!(
text.contains("proj/src/main.rs"),
"entries must sit under `proj/`, not under an empty name: {text}"
);
}
#[test]
fn pack_excludes_its_own_output_from_the_walk() {
let dir = tmp_dir();
let root = dir.join("proj");
std::fs::create_dir_all(&root).unwrap();
std::fs::write(root.join("a.txt"), b"a").unwrap();
let st = Command::new(STUFFR)
.current_dir(&root)
.args(["pack", ".", "-o", "backup.tar"])
.output()
.unwrap();
assert!(
st.status.success(),
"stderr: {}",
String::from_utf8_lossy(&st.stderr)
);
let out = root.join("backup.tar");
let first_len = std::fs::metadata(&out).unwrap().len();
let st = Command::new(STUFFR)
.current_dir(&root)
.args(["pack", ".", "-o", "backup.tar", "--force"])
.output()
.unwrap();
assert!(
st.status.success(),
"stderr: {}",
String::from_utf8_lossy(&st.stderr)
);
let err = String::from_utf8_lossy(&st.stderr);
assert!(
err.contains("proj/backup.tar"),
"it must SAY WHAT it excluded, by name; stderr: {err}"
);
let second_len = std::fs::metadata(&out).unwrap().len();
assert_eq!(
first_len, second_len,
"a --force re-run must not nest the previous archive inside the new one"
);
let listed = run_output(&["list", out.to_str().unwrap()]);
let text = String::from_utf8_lossy(&listed.stdout);
assert!(
!text.contains("backup.tar"),
"the archive must not contain an entry naming itself: {text}"
);
assert!(
text.contains("proj/a.txt"),
"everything else in the directory is still stored: {text}"
);
let st3 = Command::new(STUFFR)
.current_dir(&root)
.args([
"pack",
".",
"-o",
"backup.tar",
"--force",
"--strict-fidelity",
])
.output()
.unwrap();
assert_eq!(
st3.status.code(),
Some(0),
"excluding the archive from its own walk costs no fidelity, so the strict \
gate must not fire on it: {}",
String::from_utf8_lossy(&st3.stderr)
);
}
#[test]
fn pack_refuses_when_the_only_input_is_the_output_and_leaves_it_untouched() {
let dir = tmp_dir();
let root = dir.join("backups");
std::fs::create_dir_all(&root).unwrap();
std::fs::write(root.join("data.txt"), b"last night's data").unwrap();
let st = Command::new(STUFFR)
.current_dir(&root)
.args(["pack", "data.txt", "-o", "backup.tar"])
.output()
.unwrap();
assert!(
st.status.success(),
"stderr: {}",
String::from_utf8_lossy(&st.stderr)
);
let archive = root.join("backup.tar");
let before = std::fs::read(&archive).unwrap();
assert!(
before.len() > 1024,
"the fixture must be a real archive, not an empty one: {} bytes",
before.len()
);
let st = Command::new(STUFFR)
.current_dir(&root)
.args([
"pack",
"backup.tar",
"-o",
"backup.tar",
"--force",
"--strict-fidelity",
])
.output()
.unwrap();
let err = String::from_utf8_lossy(&st.stderr);
assert_eq!(
st.status.code(),
Some(2),
"an empty plan is a usage refusal, not a successful empty archive: {err}"
);
assert!(
err.contains("nothing to pack") && err.contains("backup.tar"),
"the refusal must name the cause and the destination: {err}"
);
assert_eq!(
std::fs::read(&archive).unwrap(),
before,
"refusing before the destination is opened means no temp file and no \
rename, so the existing archive must be byte-identical"
);
}
#[test]
fn pack_refuses_a_nightly_over_a_directory_holding_only_its_own_archive() {
let dir = tmp_dir();
let root = dir.join("backups");
std::fs::create_dir_all(&root).unwrap();
std::fs::write(dir.join("data.txt"), b"last night's data").unwrap();
let st = Command::new(STUFFR)
.current_dir(&dir)
.args(["pack", "data.txt", "-o", "backups/nightly.tar"])
.output()
.unwrap();
assert!(
st.status.success(),
"stderr: {}",
String::from_utf8_lossy(&st.stderr)
);
let archive = root.join("nightly.tar");
let before = std::fs::read(&archive).unwrap();
let st = Command::new(STUFFR)
.current_dir(&dir)
.args([
"pack",
"backups",
"-o",
"backups/nightly.tar",
"--force",
"--strict-fidelity",
])
.output()
.unwrap();
let err = String::from_utf8_lossy(&st.stderr);
assert_eq!(
st.status.code(),
Some(2),
"a plan reduced to the directory the output was removed from is a usage \
refusal, not an archive of one empty directory: {err}"
);
assert!(
err.contains("No archive was written") && err.contains("is unchanged"),
"the refusal must say plainly that the destination survives: {err}"
);
assert_eq!(
std::fs::read(&archive).unwrap(),
before,
"last night's archive must be byte-identical, not replaced by a shell"
);
}
#[test]
fn test_reports_a_zip_whose_index_declares_more_records_than_are_reachable() {
let dir = tmp_dir();
let src = dir.join("a.txt");
std::fs::write(&src, b"alpha").unwrap();
let zip_path = dir.join("dup.zip");
let st = Command::new(STUFFR)
.current_dir(&dir)
.args(["pack", "a.txt", "-o", "dup.zip"])
.output()
.unwrap();
assert!(
st.status.success(),
"stderr: {}",
String::from_utf8_lossy(&st.stderr)
);
let st = Command::new(STUFFR)
.args(["test", zip_path.to_str().unwrap(), "--strict-fidelity"])
.output()
.unwrap();
let err = String::from_utf8_lossy(&st.stderr);
assert_eq!(st.status.code(), Some(0), "a clean zip is clean: {err}");
assert!(err.contains("exact fidelity"), "and still says so: {err}");
let mut bytes = std::fs::read(&zip_path).unwrap();
let eocd = bytes.len() - 22;
assert_eq!(&bytes[eocd..eocd + 4], b"PK\x05\x06", "no EOCD");
let le16 = |b: &[u8]| u16::from_le_bytes([b[0], b[1]]);
let cd_size = u32::from_le_bytes(bytes[eocd + 12..eocd + 16].try_into().unwrap()) as usize;
let cd_at = u32::from_le_bytes(bytes[eocd + 16..eocd + 20].try_into().unwrap()) as usize;
let cd = bytes[cd_at..cd_at + cd_size].to_vec();
let first = 46 + le16(&cd[28..]) as usize + le16(&cd[30..]) as usize + le16(&cd[32..]) as usize;
let entries = le16(&bytes[eocd + 10..]);
let mut out = bytes[..cd_at + cd_size].to_vec();
out.extend_from_slice(&cd[..first]);
let tail = &mut bytes[eocd..];
tail[8..10].copy_from_slice(&(entries + 1).to_le_bytes());
tail[10..12].copy_from_slice(&(entries + 1).to_le_bytes());
tail[12..16].copy_from_slice(&((cd_size + first) as u32).to_le_bytes());
out.extend_from_slice(tail);
let forged = dir.join("shadowed.zip");
std::fs::write(&forged, &out).unwrap();
let st = Command::new(STUFFR)
.args(["test", forged.to_str().unwrap()])
.output()
.unwrap();
let err = String::from_utf8_lossy(&st.stderr);
assert_eq!(
st.status.code(),
Some(0),
"a shadowed record is a fidelity loss, not corruption: {err}"
);
assert!(
err.contains("declares 2") && err.contains("only 1"),
"the warning must name both counts: {err}"
);
assert!(
!err.contains("exact fidelity"),
"a read that reached one of two records must not announce exact \
fidelity: {err}"
);
let st = Command::new(STUFFR)
.args(["test", forged.to_str().unwrap(), "--strict-fidelity"])
.output()
.unwrap();
assert_eq!(
st.status.code(),
Some(4),
"--strict-fidelity must refuse it: {}",
String::from_utf8_lossy(&st.stderr)
);
}
fn which(bin: &str) -> Option<PathBuf> {
let path = std::env::var_os("PATH")?;
std::env::split_paths(&path).find_map(|dir| {
let candidate = dir.join(bin);
candidate.is_file().then_some(candidate)
})
}
fn require_bin(bin: &str) -> PathBuf {
which(bin).unwrap_or_else(|| {
panic!(
"no reference `{bin}` tool found on PATH — this test proved nothing, which is \
worth knowing rather than passing silently"
)
})
}
fn require_lhasa() -> PathBuf {
which("lha").or_else(|| which("lhasa")).unwrap_or_else(|| {
panic!(
"no reference lhasa tool found on PATH under either of its two names, `lha` \
(Homebrew) or `lhasa` (Debian/Ubuntu) — this test proved nothing, which is worth \
knowing rather than passing silently"
)
})
}
fn lhasa_banner(bin: &std::path::Path) -> String {
let out = Command::new(bin)
.output()
.expect("run lhasa with no arguments for its banner");
let text = if out.stdout.is_empty() {
String::from_utf8_lossy(&out.stderr).into_owned()
} else {
String::from_utf8_lossy(&out.stdout).into_owned()
};
text.lines().next().unwrap_or("<no banner>").to_string()
}
#[cfg(unix)]
fn perm_bits(md: &std::fs::Metadata) -> u32 {
use std::os::unix::fs::PermissionsExt;
md.permissions().mode() & 0o7777
}
#[cfg(unix)]
fn build_reference_tree(root: &Path) {
use std::os::unix::fs::PermissionsExt;
std::fs::create_dir_all(root.join("sub")).unwrap();
std::fs::create_dir(root.join("empty")).unwrap();
std::fs::write(root.join("a.txt"), b"alpha").unwrap();
std::fs::write(root.join("sub/b.bin"), b"\x00\xff\x00beta").unwrap();
std::os::unix::fs::symlink("a.txt", root.join("link")).unwrap();
for (p, mode) in [
(root.to_path_buf(), 0o700),
(root.join("sub"), 0o700),
(root.join("empty"), 0o700),
(root.join("a.txt"), 0o600),
(root.join("sub/b.bin"), 0o600),
] {
std::fs::set_permissions(&p, std::fs::Permissions::from_mode(mode)).unwrap();
}
}
#[cfg(unix)]
fn tree_snapshot(root: &Path) -> Vec<String> {
let mut out = Vec::new();
let mut stack = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {
for entry in std::fs::read_dir(&dir).unwrap() {
let path = entry.unwrap().path();
let rel = path.strip_prefix(root).unwrap().display().to_string();
let md = std::fs::symlink_metadata(&path).unwrap();
if md.is_symlink() {
out.push(format!(
"{rel}: symlink -> {}",
std::fs::read_link(&path).unwrap().display()
));
} else if md.is_dir() {
out.push(format!("{rel}: dir mode {:o}", perm_bits(&md)));
stack.push(path);
} else {
out.push(format!(
"{rel}: file mode {:o} bytes {:?}",
perm_bits(&md),
std::fs::read(&path).unwrap()
));
}
}
}
out.sort();
out
}
fn run_tool(bin: &Path, args: &[&std::ffi::OsStr], cwd: &Path, stdin: &[u8]) -> Vec<u8> {
let mut child = Command::new(bin)
.args(args)
.current_dir(cwd)
.env("COPYFILE_DISABLE", "1")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child.stdin.take().unwrap().write_all(stdin).unwrap();
let out = child.wait_with_output().unwrap();
assert!(
out.status.success(),
"reference tool {} {:?} failed: {}",
bin.display(),
args,
String::from_utf8_lossy(&out.stderr)
);
out.stdout
}
fn os(s: &impl AsRef<std::ffi::OsStr>) -> &std::ffi::OsStr {
s.as_ref()
}
fn pack_ok(args: &[&std::ffi::OsStr]) -> String {
let out = Command::new(STUFFR)
.arg("pack")
.args(args)
.output()
.unwrap();
assert!(
out.status.success(),
"stuffr pack {:?} failed: {}",
args,
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stderr).to_string()
}
fn unpack_ok(archive: &Path, dest: &Path) {
let out = Command::new(STUFFR)
.args([
std::ffi::OsStr::new("unpack"),
archive.as_ref(),
std::ffi::OsStr::new("-C"),
dest.as_ref(),
])
.output()
.unwrap();
assert!(
out.status.success(),
"stuffr unpack {} failed: {}",
archive.display(),
String::from_utf8_lossy(&out.stderr)
);
}
#[cfg(unix)]
#[test]
fn every_container_round_trips_a_tree_against_its_reference_tool() {
let tar_bin = require_bin("tar");
let zip_bin = require_bin("zip");
let unzip_bin = require_bin("unzip");
let cpio_bin = require_bin("cpio");
let ar_bin = require_bin("ar");
let dir = tmp_dir();
let tree = dir.join("tree");
let root = tree.join("proj");
build_reference_tree(&root);
let want = tree_snapshot(&root);
assert_eq!(
want,
vec![
"a.txt: file mode 600 bytes [97, 108, 112, 104, 97]".to_string(),
"empty: dir mode 700".to_string(),
"link: symlink -> a.txt".to_string(),
"sub/b.bin: file mode 600 bytes [0, 255, 0, 98, 101, 116, 97]".to_string(),
"sub: dir mode 700".to_string(),
],
"the fixture must carry a nested dir, an EMPTY dir, a symlink and two \
differently-named files, or the comparisons below prove less than they claim"
);
let out_tar = dir.join("out.tar");
pack_ok(&[os(&root), os(&"-o"), os(&out_tar)]);
let back = dir.join("back-tar");
std::fs::create_dir(&back).unwrap();
run_tool(
&tar_bin,
&[os(&"-xf"), os(&out_tar), os(&"-C"), os(&back)],
&dir,
b"",
);
assert_eq!(
tree_snapshot(&back.join("proj")),
want,
"the system tar read our tar back as a different tree"
);
let ref_tar = dir.join("ref.tar");
run_tool(
&tar_bin,
&[os(&"-cf"), os(&ref_tar), os(&"proj")],
&tree,
b"",
);
let back = dir.join("back-ref-tar");
unpack_ok(&ref_tar, &back);
assert_eq!(
tree_snapshot(&back.join("proj")),
want,
"we read a tar the system tar wrote as a different tree"
);
let out_zip = dir.join("out.zip");
pack_ok(&[os(&root), os(&"-o"), os(&out_zip)]);
let back = dir.join("back-zip");
run_tool(
&unzip_bin,
&[os(&"-q"), os(&out_zip), os(&"-d"), os(&back)],
&dir,
b"",
);
assert_eq!(
tree_snapshot(&back.join("proj")),
want,
"unzip read our zip back as a different tree"
);
let ref_zip = dir.join("ref.zip");
run_tool(
&zip_bin,
&[os(&"-qry"), os(&ref_zip), os(&"proj")],
&tree,
b"",
);
let back = dir.join("back-ref-zip");
unpack_ok(&ref_zip, &back);
assert_eq!(
tree_snapshot(&back.join("proj")),
want,
"we read a zip the system zip wrote as a different tree"
);
let out_cpio = dir.join("out.cpio");
pack_ok(&[os(&root), os(&"-o"), os(&out_cpio)]);
let back = dir.join("back-cpio");
std::fs::create_dir(&back).unwrap();
run_tool(
&cpio_bin,
&[os(&"-i"), os(&"-d"), os(&"--quiet")],
&back,
&std::fs::read(&out_cpio).unwrap(),
);
assert_eq!(
tree_snapshot(&back.join("proj")),
want,
"the system cpio read our cpio back as a different tree"
);
let names = b"proj\nproj/a.txt\nproj/empty\nproj/link\nproj/sub\nproj/sub/b.bin\n";
let ref_cpio_bytes = run_tool(
&cpio_bin,
&[os(&"-o"), os(&"--format=newc"), os(&"--quiet")],
&tree,
names,
);
let ref_cpio = dir.join("ref.cpio");
std::fs::write(&ref_cpio, &ref_cpio_bytes).unwrap();
let back = dir.join("back-ref-cpio");
unpack_ok(&ref_cpio, &back);
assert_eq!(
tree_snapshot(&back.join("proj")),
want,
"we read a cpio the system cpio wrote as a different tree"
);
let out_ar = dir.join("out.a");
let stderr = pack_ok(&[os(&root), os(&"-o"), os(&out_ar)]);
for dropped in ["proj/empty", "proj/link", "proj/sub"] {
assert!(
stderr.contains(dropped),
"`ar` dropping `{dropped}` must be named in the fidelity report; stderr: {stderr}"
);
}
let listed = run_tool(&ar_bin, &[os(&"t"), os(&out_ar)], &dir, b"");
let mut members: Vec<String> = String::from_utf8_lossy(&listed)
.lines()
.map(str::to_string)
.collect();
members.sort();
assert_eq!(
members,
vec!["proj/a.txt".to_string(), "proj/sub/b.bin".to_string()],
"the system ar must list exactly the regular files, and nothing standing in for \
the directories or the symlink"
);
let ref_ar = dir.join("ref.a");
run_tool(
&ar_bin,
&[os(&"rcS"), os(&ref_ar), os(&"a.txt")],
&root,
b"",
);
let back = dir.join("back-ref-ar");
unpack_ok(&ref_ar, &back);
assert_eq!(
std::fs::read(back.join("a.txt")).unwrap(),
b"alpha",
"we read an ar the system ar wrote as different bytes"
);
}
#[test]
fn packing_the_same_tree_twice_produces_identical_bytes() {
let dir = tmp_dir();
let root = dir.join("proj");
std::fs::create_dir_all(root.join("b")).unwrap();
for n in ["z.txt", "m.txt", "a.txt"] {
std::fs::write(root.join(n), n.as_bytes()).unwrap();
}
std::fs::write(root.join("b/inner.txt"), b"inner").unwrap();
let readdir_order: Vec<String> = std::fs::read_dir(&root)
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().to_string())
.collect();
let mut sorted = readdir_order.clone();
sorted.sort();
assert_ne!(
readdir_order, sorted,
"this filesystem hands back directory entries already sorted, so the ordering \
assertion below could not fail; the fixture needs a name order this filesystem \
does not reproduce"
);
let listing = dir.join("order.tar");
let st = Command::new(STUFFR)
.args([
"pack",
root.to_str().unwrap(),
"-o",
listing.to_str().unwrap(),
])
.output()
.unwrap();
assert!(
st.status.success(),
"stderr: {}",
String::from_utf8_lossy(&st.stderr)
);
let listed = run_output(&["list", listing.to_str().unwrap()]);
let names: Vec<String> = String::from_utf8_lossy(&listed.stdout)
.lines()
.map(|l| l.split_whitespace().last().unwrap_or_default().to_string())
.collect();
assert_eq!(
names,
vec![
"proj",
"proj/a.txt",
"proj/b",
"proj/b/inner.txt",
"proj/m.txt",
"proj/z.txt",
],
"entries must be written in sorted, depth-first order — `readdir` order was {readdir_order:?}"
);
for ext in ["tar", "tar.gz", "zip"] {
let one = dir.join(format!("one.{ext}"));
let two = dir.join(format!("two.{ext}"));
for out in [&one, &two] {
let st = Command::new(STUFFR)
.args(["pack", root.to_str().unwrap(), "-o", out.to_str().unwrap()])
.output()
.unwrap();
assert!(
st.status.success(),
"stderr: {}",
String::from_utf8_lossy(&st.stderr)
);
}
assert_eq!(
std::fs::read(&one).unwrap(),
std::fs::read(&two).unwrap(),
"the same tree must pack to the same `.{ext}` bytes; readdir order is not stable"
);
}
}
#[cfg(unix)]
#[test]
fn a_failed_write_never_replaces_an_existing_archive() {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let dir = tmp_dir();
let root = dir.join("proj");
std::fs::create_dir(&root).unwrap();
std::fs::write(root.join("a.txt"), b"alpha").unwrap();
let big = root.join("zbig.bin");
let f = std::fs::File::create(&big).unwrap();
f.set_len(u64::from(u32::MAX) + 1).unwrap();
drop(f);
let md = std::fs::metadata(&big).unwrap();
assert!(
md.len() > u64::from(u32::MAX),
"the premise: the entry must exceed cpio's u32 size field"
);
assert!(
md.blocks() * 512 < md.len(),
"this filesystem did not make the fixture sparse ({} blocks for {} bytes); a \
non-sparse one would write 4 GiB to disk here",
md.blocks(),
md.len()
);
let out = dir.join("existing.cpio");
std::fs::write(&out, b"PRECIOUS EXISTING CONTENT").unwrap();
let st = Command::new(STUFFR)
.args([
"pack",
root.to_str().unwrap(),
"-o",
out.to_str().unwrap(),
"--force",
])
.output()
.unwrap();
assert_eq!(
st.status.code(),
Some(3),
"an entry this container cannot express is a capability limit, exit 3; stderr: {}",
String::from_utf8_lossy(&st.stderr)
);
assert_eq!(
std::fs::read(&out).unwrap(),
b"PRECIOUS EXISTING CONTENT",
"a pack that failed mid-write must not have replaced the archive already there"
);
let mut siblings: Vec<String> = std::fs::read_dir(&dir)
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().to_string())
.collect();
siblings.sort();
assert_eq!(
siblings,
vec!["existing.cpio".to_string(), "proj".to_string()],
"a failed pack must leave no temp file behind"
);
let ro = dir.join("readonly");
std::fs::create_dir(&ro).unwrap();
std::fs::set_permissions(&ro, std::fs::Permissions::from_mode(0o500)).unwrap();
assert!(
std::fs::File::create(ro.join("probe")).is_err(),
"this test needs a non-root user; the read-only directory is still writable"
);
let target = ro.join("out.tar");
let st = Command::new(STUFFR)
.args([
"pack",
root.join("a.txt").to_str().unwrap(),
"-o",
target.to_str().unwrap(),
])
.output()
.unwrap();
assert_eq!(
st.status.code(),
Some(1),
"a destination that cannot be created is an i/o failure, exit 1; stderr: {}",
String::from_utf8_lossy(&st.stderr)
);
assert!(!target.exists(), "a failed pack must publish nothing");
std::fs::set_permissions(&ro, std::fs::Permissions::from_mode(0o700)).unwrap();
}
#[cfg(unix)]
#[test]
fn an_unreadable_file_is_skipped_with_a_warning_rather_than_failing_the_pack() {
use std::os::unix::fs::PermissionsExt;
let dir = tmp_dir();
let root = dir.join("proj");
std::fs::create_dir(&root).unwrap();
std::fs::write(root.join("a.txt"), b"alpha").unwrap();
let unreadable = root.join("secret.txt");
std::fs::write(&unreadable, b"beta").unwrap();
std::fs::set_permissions(&unreadable, std::fs::Permissions::from_mode(0o000)).unwrap();
assert!(
std::fs::File::open(&unreadable).is_err(),
"this test needs a non-root user; the unreadable file is still readable"
);
let out = dir.join("backup.tar");
let st = Command::new(STUFFR)
.args(["pack", root.to_str().unwrap(), "-o", out.to_str().unwrap()])
.output()
.unwrap();
assert!(
st.status.success(),
"one unreadable file must not lose the whole backup: {}",
String::from_utf8_lossy(&st.stderr)
);
let stderr = String::from_utf8_lossy(&st.stderr);
assert!(
stderr.contains("proj/secret.txt"),
"it must SAY WHAT it skipped, by name; stderr: {stderr}"
);
let listed = run_output(&["list", out.to_str().unwrap()]);
let text = String::from_utf8_lossy(&listed.stdout);
assert!(text.contains("proj/a.txt"), "{text}");
assert!(
!text.contains("secret.txt"),
"an entry that could not be read must not be written as an empty one: {text}"
);
let strict = dir.join("strict.tar");
let st = Command::new(STUFFR)
.args([
"pack",
root.to_str().unwrap(),
"-o",
strict.to_str().unwrap(),
"--strict-fidelity",
])
.output()
.unwrap();
assert_eq!(
st.status.code(),
Some(4),
"strict fidelity must refuse a pack that skipped a file; stderr: {}",
String::from_utf8_lossy(&st.stderr)
);
std::fs::set_permissions(&unreadable, std::fs::Permissions::from_mode(0o600)).unwrap();
}
fn workspace_source_files() -> Vec<PathBuf> {
let crates = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
let mut out = Vec::new();
let mut stack: Vec<PathBuf> = std::fs::read_dir(crates)
.unwrap()
.map(|e| e.unwrap().path().join("src"))
.filter(|p| p.is_dir())
.collect();
while let Some(dir) = stack.pop() {
for entry in std::fs::read_dir(&dir).unwrap() {
let path = entry.unwrap().path();
if path.is_dir() {
stack.push(path);
} else if path.extension().is_some_and(|e| e == "rs") {
out.push(path);
}
}
}
out.sort();
out
}
fn rust_string_literals(src: &str) -> Vec<(usize, String)> {
let c: Vec<char> = src.chars().collect();
let mut out = Vec::new();
let mut i = 0usize;
let mut line = 1usize;
while i < c.len() {
if c[i] == '\n' {
line += 1;
i += 1;
continue;
}
if c[i] == '/' && c.get(i + 1) == Some(&'/') {
while i < c.len() && c[i] != '\n' {
i += 1;
}
continue;
}
if c[i] == '/' && c.get(i + 1) == Some(&'*') {
let mut depth = 1usize;
i += 2;
while i < c.len() && depth > 0 {
if c[i] == '/' && c.get(i + 1) == Some(&'*') {
depth += 1;
i += 2;
} else if c[i] == '*' && c.get(i + 1) == Some(&'/') {
depth -= 1;
i += 2;
} else {
if c[i] == '\n' {
line += 1;
}
i += 1;
}
}
continue;
}
if c[i] == '\'' {
if c.get(i + 1) == Some(&'\\') {
let mut j = i + 2;
while j < c.len() && c[j] != '\'' {
j += 1;
}
i = j + 1;
continue;
}
if c.get(i + 2) == Some(&'\'') {
i += 3;
continue;
}
i += 1;
continue;
}
let ident_before = i > 0 && (c[i - 1].is_alphanumeric() || c[i - 1] == '_');
if (c[i] == 'r' || c[i] == 'b') && !ident_before {
let mut j = i;
if c[j] == 'b' {
j += 1;
}
if c.get(j) == Some(&'r') {
j += 1;
let mut hashes = 0usize;
while c.get(j) == Some(&'#') {
hashes += 1;
j += 1;
}
if c.get(j) == Some(&'"') {
let start_line = line;
j += 1;
let mut text = String::new();
loop {
if j >= c.len() {
break;
}
if c[j] == '"' && c[j + 1..].iter().take(hashes).all(|h| *h == '#') {
j += 1 + hashes;
break;
}
if c[j] == '\n' {
line += 1;
}
text.push(c[j]);
j += 1;
}
out.push((start_line, text));
i = j;
continue;
}
}
}
if c[i] == '"' {
let start_line = line;
let mut text = String::new();
let mut j = i + 1;
while j < c.len() && c[j] != '"' {
if c[j] == '\\' && c.get(j + 1) == Some(&'\n') {
line += 1;
j += 2;
while matches!(c.get(j), Some(' ' | '\t' | '\r' | '\n')) {
if c[j] == '\n' {
line += 1;
}
j += 1;
}
continue;
}
if c[j] == '\\' {
text.push(c[j]);
if let Some(&e) = c.get(j + 1) {
text.push(e);
}
j += 2;
continue;
}
if c[j] == '\n' {
line += 1;
}
text.push(c[j]);
j += 1;
}
out.push((start_line, text));
i = j + 1;
continue;
}
i += 1;
}
out
}
fn first_space_run(text: &str) -> Option<usize> {
text.as_bytes().windows(3).position(|w| w == b" ")
}
fn is_column_label(prefix: &str) -> bool {
let tail = prefix
.rsplit('\n')
.next()
.unwrap_or(prefix)
.rsplit("\\n")
.next()
.unwrap_or(prefix);
match tail.strip_suffix(':') {
Some(label) => {
!label.is_empty()
&& !label.contains(' ')
&& label
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-')
}
None => false,
}
}
#[test]
fn no_message_literal_in_the_workspace_carries_a_run_of_collapsed_indentation() {
let defective = "fn f() { err(\"packing paths needs a container (`-o bundle.tar`, or --format tar)\"); }";
let found = rust_string_literals(defective);
assert_eq!(
found.len(),
1,
"the scanner must find the literal: {found:?}"
);
assert!(
first_space_run(&found[0].1).is_some_and(|at| !is_column_label(&found[0].1[..at])),
"the scanner must flag the historical defect: {:?}",
found[0].1
);
let continued = "fn f() { err(\"packing paths needs a container \\\n (`-o bundle.tar`, or --format tar)\"); }";
let found = rust_string_literals(continued);
assert_eq!(found.len(), 1);
assert_eq!(
found[0].1, "packing paths needs a container (`-o bundle.tar`, or --format tar)",
"a `\\`-continued literal carries no indentation, and the scanner must apply that \
rule rather than report the source text"
);
assert!(first_space_run(&found[0].1).is_none());
let aligned = "fn f() { println!(\"format: {}\", x); }";
let found = rust_string_literals(aligned);
let at = first_space_run(&found[0].1).expect("the column really is three spaces");
assert!(
is_column_label(&found[0].1[..at]),
"a deliberate column label must not be reported as a collapse"
);
let colon_prose = "fn f() { err(\"stuffr cannot do this: try force\"); }";
let found = rust_string_literals(colon_prose);
assert_eq!(found.len(), 1);
let at = first_space_run(&found[0].1).expect("the run is there");
assert!(
!is_column_label(&found[0].1[..at]),
"a prose clause ending in a colon is not a column label: {:?}",
found[0].1
);
assert!(is_column_label("MemAvailable:"));
assert!(!is_column_label("two words:"));
let quoted_char = "fn f() { let q = '\"'; err(\"a collapse\"); }";
let found = rust_string_literals(quoted_char);
assert_eq!(
found.iter().map(|(_, t)| t.as_str()).collect::<Vec<_>>(),
vec!["a collapse"],
"a `\"` inside a char literal must not desynchronise the scan"
);
let files = workspace_source_files();
assert!(
files.len() > 20,
"the walk found only {} source files, which is not this workspace: {files:?}",
files.len()
);
assert!(
files.iter().any(|p| p.ends_with("stuffr-cli/src/main.rs")),
"the walk must reach the crate every user-facing message is printed from"
);
let mut scanned = 0usize;
let mut offenders = Vec::new();
for file in &files {
let src = std::fs::read_to_string(file).unwrap();
for (line, text) in rust_string_literals(&src) {
scanned += 1;
let Some(at) = first_space_run(&text) else {
continue;
};
if is_column_label(&text[..at]) {
continue;
}
offenders.push(format!("{}:{line}: {text:?}", file.display()));
}
}
assert!(
scanned > 1000,
"only {scanned} literals scanned, which is not this workspace"
);
assert!(
offenders.is_empty(),
"a string literal carries a run of three or more spaces, which reaches the user as \
collapsed continuation indentation mid-sentence. Re-join the sentence and end each \
continued line with `\\` (which strips the newline AND the next line's indentation); \
a deliberate column is written `label: value`. Offenders:\n{}",
offenders.join("\n")
);
}
#[test]
fn packing_the_filesystem_root_is_a_usage_error_that_says_why() {
let dir = tmp_dir();
let out = dir.join("root.tar");
let st = Command::new(STUFFR)
.args(["pack", "/", "-o", out.to_str().unwrap()])
.output()
.unwrap();
assert_eq!(
st.status.code(),
Some(2),
"packing `/` is the caller's mistake, not an i/o failure; stderr: {}",
String::from_utf8_lossy(&st.stderr)
);
let stderr = String::from_utf8_lossy(&st.stderr);
assert!(
stderr.contains("no final path component"),
"the message must say what is wrong with `/`: {stderr}"
);
assert!(
!out.exists(),
"the refusal happens before the destination is opened"
);
}
#[test]
fn a_nonexistent_dotdot_path_is_reported_as_an_io_error() {
let dir = tmp_dir();
let out = dir.join("dd.tar");
let missing = dir.join("nosuch/..");
let st = Command::new(STUFFR)
.args([
"pack",
missing.to_str().unwrap(),
"-o",
out.to_str().unwrap(),
])
.output()
.unwrap();
assert_eq!(
st.status.code(),
Some(1),
"a path that does not exist is an i/o error, whatever its final component; \
stderr: {}",
String::from_utf8_lossy(&st.stderr)
);
assert!(!out.exists());
}
#[cfg(unix)]
#[test]
fn pack_of_dot_from_a_symlinked_directory_names_entries_after_the_real_one() {
let dir = tmp_dir();
std::fs::create_dir_all(dir.join("real/src")).unwrap();
std::fs::write(dir.join("real/src/main.rs"), b"fn main() {}").unwrap();
std::os::unix::fs::symlink("real", dir.join("proj")).unwrap();
let out = dir.join("sym.tar");
let st = Command::new(STUFFR)
.current_dir(dir.join("proj"))
.args(["pack", ".", "-o", out.to_str().unwrap()])
.output()
.unwrap();
assert!(
st.status.success(),
"stderr: {}",
String::from_utf8_lossy(&st.stderr)
);
let listed = run_output(&["list", out.to_str().unwrap()]);
let text = String::from_utf8_lossy(&listed.stdout);
assert!(
text.contains("real/src/main.rs"),
"`.` resolves through the symlink, so entries sit under the real directory's \
name: {text}"
);
assert!(
!text.contains("proj/"),
"nothing may be named after the link itself: {text}"
);
}
#[test]
fn excluding_the_output_from_its_own_walk_is_a_note_not_a_fidelity_loss() {
let dir = tmp_dir();
let root = dir.join("proj");
std::fs::create_dir_all(&root).unwrap();
std::fs::write(root.join("a.txt"), b"payload\n").unwrap();
let out = root.join("backup.tar");
for run in 1..=3 {
let st = Command::new(STUFFR)
.args([
"pack",
root.to_str().unwrap(),
"-o",
out.to_str().unwrap(),
"--force",
"--strict-fidelity",
])
.output()
.unwrap();
let stderr = String::from_utf8_lossy(&st.stderr).to_string();
assert_eq!(
st.status.code(),
Some(0),
"run {run}: declining to store the archive inside itself is not a \
fidelity loss, so the strict gate must stay green; stderr: {stderr}"
);
if run > 1 {
assert!(
stderr.contains("proj/backup.tar"),
"run {run}: the user must still be told, by name: {stderr}"
);
}
assert!(
stderr.contains("no fidelity loss"),
"run {run}: the count in the summary must agree with the (empty) \
warning list: {stderr}"
);
}
let listed = run_output(&["list", out.to_str().unwrap()]);
let text = String::from_utf8_lossy(&listed.stdout);
assert!(text.contains("proj/a.txt"), "{text}");
assert!(
!text.contains("backup.tar"),
"the archive must not be inside itself: {text}"
);
}
#[test]
fn a_tbz_output_writes_tar_inside_bzip2_and_not_a_bare_gzip() {
let dir = tmp_dir();
let src = dir.join("notes.txt");
std::fs::write(&src, b"payload\n").unwrap();
let out = dir.join("f.tbz");
let st = Command::new(STUFFR)
.args(["pack", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
.output()
.unwrap();
assert!(
st.status.success(),
"stderr: {}",
String::from_utf8_lossy(&st.stderr)
);
let bytes = std::fs::read(&out).unwrap();
assert_eq!(
&bytes[..3],
b"BZh",
"a .tbz must be bzip2 — it was gzip (1f 8b) before this: {:02x?}",
&bytes[..4.min(bytes.len())]
);
let listed = run_output(&["list", out.to_str().unwrap()]);
assert!(
listed.status.success(),
"a .tbz must open as an archive: {}",
String::from_utf8_lossy(&listed.stderr)
);
assert!(
String::from_utf8_lossy(&listed.stdout).contains("notes.txt"),
"the tar layer must be there: {}",
String::from_utf8_lossy(&listed.stdout)
);
let out2 = dir.join("f.tbz2");
assert!(
Command::new(STUFFR)
.args(["pack", src.to_str().unwrap(), "-o", out2.to_str().unwrap()])
.status()
.unwrap()
.success()
);
assert_eq!(&std::fs::read(&out2).unwrap()[..3], b"BZh");
let out3 = dir.join("f.bz");
assert!(
Command::new(STUFFR)
.args(["pack", src.to_str().unwrap(), "-o", out3.to_str().unwrap()])
.status()
.unwrap()
.success()
);
assert_ne!(
&std::fs::read(&out3).unwrap()[..3],
b"BZh",
"`bz` must not resolve to bzip2 outside the `t`-prefixed compound form"
);
}
#[test]
fn a_directory_on_the_single_stream_path_is_a_usage_error_naming_the_fix() {
let dir = tmp_dir();
let root = dir.join("proj");
std::fs::create_dir_all(&root).unwrap();
std::fs::write(root.join("a.txt"), b"payload\n").unwrap();
let out = dir.join("proj.gz");
let st = Command::new(STUFFR)
.args(["pack", root.to_str().unwrap(), "-o", out.to_str().unwrap()])
.output()
.unwrap();
let stderr = String::from_utf8_lossy(&st.stderr).to_string();
assert_eq!(
st.status.code(),
Some(2),
"asking a codec for a tree is the caller's mistake, not an i/o \
failure; stderr: {stderr}"
);
assert!(
!stderr.contains("os error 21"),
"the raw EISDIR must not reach the user: {stderr}"
);
assert!(
stderr.contains("proj.tar.gz"),
"the message must name the output that works: {stderr}"
);
assert!(
!out.exists(),
"the refusal happens before the destination is touched"
);
let good = dir.join("proj.tar.gz");
assert!(
Command::new(STUFFR)
.args(["pack", root.to_str().unwrap(), "-o", good.to_str().unwrap()])
.status()
.unwrap()
.success()
);
}
#[test]
fn the_pack_summary_counts_losses_rather_than_claiming_exact_fidelity() {
let dir = tmp_dir();
let root = dir.join("proj");
std::fs::create_dir_all(root.join("sub")).unwrap();
std::fs::write(root.join("sub").join("a.txt"), b"payload\n").unwrap();
let out = dir.join("p.a");
let st = Command::new(STUFFR)
.args(["pack", root.to_str().unwrap(), "-o", out.to_str().unwrap()])
.output()
.unwrap();
assert!(
st.status.success(),
"stderr: {}",
String::from_utf8_lossy(&st.stderr)
);
let stderr = String::from_utf8_lossy(&st.stderr).to_string();
assert!(
!stderr.contains("exact fidelity"),
"a pack that lost entries must not announce exact fidelity: {stderr}"
);
let reported: usize = stderr
.lines()
.find_map(|l| l.split_once(" fidelity loss(es)"))
.and_then(|(head, _)| head.rsplit(", ").next())
.and_then(|n| n.trim().parse().ok())
.unwrap_or_else(|| panic!("the summary must carry a loss count: {stderr}"));
let listed: usize = stderr
.lines()
.find_map(|l| l.strip_prefix("stuffr: "))
.and_then(|l| l.split_once(" fidelity warning(s):"))
.and_then(|(n, _)| n.parse().ok())
.unwrap_or_else(|| panic!("the warning header must carry a count: {stderr}"));
assert_eq!(
reported, listed,
"the summary's count and the warning list's count are the same fact: {stderr}"
);
assert!(
reported > 0,
"packing a tree into `ar` really does lose the \
directory entries: {stderr}"
);
let clean = dir.join("p.tar");
let st = Command::new(STUFFR)
.args([
"pack",
root.to_str().unwrap(),
"-o",
clean.to_str().unwrap(),
])
.output()
.unwrap();
let stderr = String::from_utf8_lossy(&st.stderr).to_string();
assert!(
stderr.contains("no fidelity loss"),
"a lossless pack must say so: {stderr}"
);
}
fn write_zip_via_pack(dir: &Path, entries: &[(&str, &[u8])]) -> PathBuf {
let src = dir.join("src");
std::fs::create_dir_all(&src).unwrap();
let mut paths = Vec::new();
for (name, data) in entries {
let p = src.join(name);
std::fs::write(&p, data).unwrap();
paths.push(p);
}
let zip = dir.join("bundle.zip");
let mut args: Vec<String> = vec!["pack".into()];
args.extend(paths.iter().map(|p| p.to_string_lossy().into_owned()));
args.push("-o".into());
args.push(zip.to_string_lossy().into_owned());
let out = Command::new(STUFFR).args(&args).output().unwrap();
assert!(
out.status.success(),
"building the zip fixture failed: {}",
String::from_utf8_lossy(&out.stderr)
);
zip
}
#[test]
fn the_index_list_prints_is_the_index_cat_selects_by() {
let dir = tmp_dir();
let archive = write_fixture_tar(
&dir,
&[
("a.txt", b"alpha"),
("b.txt", b"bravo"),
("c.txt", b"charlie"),
],
);
let listing =
String::from_utf8(run_output(&["list", archive.to_str().unwrap()]).stdout).unwrap();
let rows: Vec<&str> = listing.lines().collect();
assert_eq!(rows.len(), 3, "{listing}");
for (want_index, (_, payload)) in [
("a.txt", &b"alpha"[..]),
("b.txt", &b"bravo"[..]),
("c.txt", &b"charlie"[..]),
]
.iter()
.enumerate()
{
let printed = rows[want_index].split_whitespace().next().unwrap();
assert_eq!(
printed,
want_index.to_string(),
"list's first column must be the 0-based archive position: {listing}"
);
let out = run_output(&[
"cat",
archive.to_str().unwrap(),
"--index",
&want_index.to_string(),
]);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(
out.stdout, *payload,
"row {want_index} of the listing and `--index {want_index}` must be \
the same entry"
);
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn index_selection_agrees_between_a_seekable_zip_and_the_same_zip_on_a_pipe() {
let dir = tmp_dir();
let payloads: [&[u8]; 4] = [b"first", b"second!!", b"third:::", b"fourth"];
let zip = write_zip_via_pack(
&dir,
&[
("one.txt", payloads[0]),
("two.txt", payloads[1]),
("three.txt", payloads[2]),
("four.txt", payloads[3]),
],
);
let bytes = std::fs::read(&zip).unwrap();
for (i, payload) in payloads.iter().enumerate() {
let n = i.to_string();
let seekable = run_output(&["cat", zip.to_str().unwrap(), "--index", &n]);
assert!(
seekable.status.success(),
"index {n} on a file: {}",
String::from_utf8_lossy(&seekable.stderr)
);
let piped = run_with_stdin_output(&["cat", "-", "--index", &n], &bytes);
assert!(
piped.status.success(),
"index {n} on a pipe: {}",
String::from_utf8_lossy(&piped.stderr)
);
assert_eq!(
seekable.stdout, piped.stdout,
"the random-access and counted routes disagreed at index {n}"
);
assert_eq!(
seekable.stdout,
payload.to_vec(),
"index {n} did not reach the entry the archive was built with"
);
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn repeated_index_flags_are_deduplicated_and_delivered_in_archive_order() {
let dir = tmp_dir();
let archive = write_fixture_tar(
&dir,
&[
("a.txt", b"alpha"),
("b.txt", b"bravo"),
("c.txt", b"charlie"),
],
);
let out = run_output(&[
"cat",
archive.to_str().unwrap(),
"--index",
"2",
"--index",
"0",
"--index",
"2",
]);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(
out.stdout, b"alphacharlie",
"archive order, once each — not typed order, and not twice"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn an_index_past_the_end_is_a_usage_error_naming_the_real_entry_count() {
let dir = tmp_dir();
let archive = write_fixture_tar(&dir, &[("a.txt", b"alpha"), ("b.txt", b"bravo")]);
let zip = write_zip_via_pack(&dir, &[("one.txt", b"first"), ("two.txt", b"second")]);
for path in [&archive, &zip] {
let out = run_output(&["cat", path.to_str().unwrap(), "--index", "9"]);
assert_eq!(
out.status.code(),
Some(2),
"{}: stderr {}",
path.display(),
String::from_utf8_lossy(&out.stderr)
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("#9") && stderr.contains("0-1"),
"the refusal must name the index asked for and the VALID range, \
whichever route found it: {stderr}"
);
assert!(
out.stdout.is_empty(),
"an out-of-range index must not stream anything first"
);
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn index_and_patterns_together_are_a_usage_error() {
let dir = tmp_dir();
let archive = write_fixture_tar(&dir, &[("a.txt", b"alpha"), ("b.txt", b"bravo")]);
let dest = dir.join("out");
let out = run_output(&["cat", archive.to_str().unwrap(), "a.txt", "--index", "1"]);
assert_eq!(out.status.code(), Some(2));
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("--index"), "{stderr}");
let out = run_output(&[
"unpack",
archive.to_str().unwrap(),
"a.txt",
"-C",
dest.to_str().unwrap(),
"--index",
"1",
]);
assert_eq!(out.status.code(), Some(2));
assert!(
!dest.exists(),
"the refusal must come before anything is created"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn unpack_by_index_without_a_directory_names_the_index_in_its_refusal() {
let dir = tmp_dir();
let archive = write_fixture_tar(&dir, &[("a.txt", b"alpha"), ("b.txt", b"bravo")]);
let out = run_output(&["unpack", archive.to_str().unwrap(), "--index", "1"]);
assert_eq!(out.status.code(), Some(2));
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("--index 1") && stderr.contains("-C"),
"the refusal must name what was typed and what to do instead: {stderr}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn unpack_by_index_extracts_only_the_selected_entry() {
let dir = tmp_dir();
let archive = write_fixture_tar(
&dir,
&[
("a.txt", b"alpha"),
("b.txt", b"bravo"),
("c.txt", b"charlie"),
],
);
let dest = dir.join("out");
let out = run_output(&[
"unpack",
archive.to_str().unwrap(),
"-C",
dest.to_str().unwrap(),
"--index",
"1",
]);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(std::fs::read(dest.join("b.txt")).unwrap(), b"bravo");
assert!(!dest.join("a.txt").exists(), "only the selected entry");
assert!(!dest.join("c.txt").exists(), "only the selected entry");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn list_json_carries_the_archive_index_of_every_entry() {
let dir = tmp_dir();
let archive = write_fixture_tar(&dir, &[("a.txt", b"alpha"), ("b.txt", b"bravo")]);
let out = run_output(&["list", "--json", archive.to_str().unwrap()]);
let text = String::from_utf8(out.stdout).unwrap();
let v: serde_json::Value = serde_json::from_str(&text).unwrap();
let rows = v.as_array().unwrap();
assert_eq!(rows[0]["index"], 0);
assert_eq!(rows[0]["name"], "a.txt");
assert_eq!(rows[1]["index"], 1);
assert_eq!(rows[1]["name"], "b.txt");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn list_reports_the_same_fidelity_warnings_test_does() {
let dir = tmp_dir();
let zip = write_zip_via_pack(&dir, &[("one.txt", b"first"), ("two.txt", b"second")]);
let bytes = std::fs::read(&zip).unwrap();
let listed = run_with_stdin_output(&["list", "-"], &bytes);
assert!(
listed.status.success(),
"a warning is not a failure without --strict-fidelity"
);
let list_err = String::from_utf8_lossy(&listed.stderr).to_string();
let tested = run_with_stdin_output(&["test", "-"], &bytes);
let test_err = String::from_utf8_lossy(&tested.stderr).to_string();
assert!(
test_err.contains("fidelity warning(s)"),
"the fixture must be a shape that really does lose something: {test_err}"
);
assert!(
list_err.contains("fidelity warning(s)"),
"`list` must report what it lost, not print rows in silence: {list_err}"
);
let warnings_of = |s: &str| -> Vec<String> {
s.lines()
.filter(|l| l.trim_start().starts_with("- "))
.map(|l| l.trim().to_string())
.collect()
};
assert_eq!(
warnings_of(&list_err),
warnings_of(&test_err),
"list:\n{list_err}\ntest:\n{test_err}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn list_strict_fidelity_turns_a_warning_into_exit_four() {
let dir = tmp_dir();
let zip = write_zip_via_pack(&dir, &[("one.txt", b"first")]);
let bytes = std::fs::read(&zip).unwrap();
let lenient = run_with_stdin_output(&["list", "-"], &bytes);
assert_eq!(lenient.status.code(), Some(0));
assert!(
String::from_utf8_lossy(&lenient.stdout).contains("one.txt"),
"the listing itself is still printed"
);
let strict = run_with_stdin_output(&["list", "-", "--strict-fidelity"], &bytes);
assert_eq!(
strict.status.code(),
Some(4),
"stderr: {}",
String::from_utf8_lossy(&strict.stderr)
);
assert!(
String::from_utf8_lossy(&strict.stdout).contains("one.txt"),
"the gate is a verdict on a listing that was still produced"
);
let clean = run_output(&["list", zip.to_str().unwrap(), "--strict-fidelity"]);
assert_eq!(
clean.status.code(),
Some(0),
"a seekable zip loses nothing: {}",
String::from_utf8_lossy(&clean.stderr)
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_temp_directory_name_is_not_reproducible_from_the_pid_and_a_counter() {
let dir = tmp_dir();
assert_eq!(
std::fs::read_dir(&dir).unwrap().count(),
0,
"a fresh temp directory must be empty — inheriting one with contents \
in it is the whole failure this guards against"
);
let name = dir.file_name().unwrap().to_string_lossy().into_owned();
let derivable = format!("stuffr-cli-archive-{}-", std::process::id());
let tail = name
.strip_prefix(&derivable)
.unwrap_or_else(|| panic!("unexpected temp directory name: {name}"));
assert!(
tail.contains('-') && !tail.chars().all(|c| c.is_ascii_digit()),
"everything after the pid was the counter alone, so a second run on a \
recycled pid reproduced this name exactly: {name}"
);
let other = tmp("token-check");
let other_name = other.file_name().unwrap().to_string_lossy().into_owned();
assert_ne!(
other_name,
format!("stuffr-cli-{}-token-check", std::process::id()),
"tmp() must carry the run token too"
);
assert!(
other_name.ends_with("-token-check"),
"…and must still end in the caller's own name: {other_name}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn documented_install_commands_name_features_this_crate_declares() {
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let manifest = std::fs::read_to_string(manifest_dir.join("Cargo.toml")).unwrap();
let features: Vec<String> = manifest
.split("\n[")
.find(|section| section.starts_with("features]"))
.expect(
"stuffr-cli must declare a [features] section: without one, every \
documented `cargo install stuffr-cli --features …` fails",
)
.lines()
.filter_map(|line| line.split_once('='))
.filter(|(_, rhs)| rhs.trim_start().starts_with('['))
.map(|(name, _)| name.trim().to_string())
.collect();
assert!(
features.contains(&"legacy".to_string()),
"the parse found {features:?}; if `legacy` is missing the scan itself is broken, \
not the manifest"
);
let docs = user_facing_documents();
let mut commands_checked = 0usize;
for doc in docs {
let text = std::fs::read_to_string(&doc).unwrap();
for line in text.lines() {
let Some(rest) = line.split("cargo install stuffr-cli").nth(1) else {
continue;
};
let Some(after) = rest.split("--features").nth(1) else {
continue;
};
let name = after.split_whitespace().next().unwrap_or("");
commands_checked += 1;
assert!(
features.contains(&name.to_string()),
"{} prints `cargo install stuffr-cli --features {name}`, but stuffr-cli \
declares only {features:?} — that command fails with \"the package \
'stuffr-cli' does not contain this feature\"",
doc.display()
);
}
}
assert!(
commands_checked >= 2,
"expected the READMEs to print at least the c-backed and legacy install \
commands; found {commands_checked} — the scan has stopped finding them"
);
}
fn user_facing_documents() -> Vec<PathBuf> {
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let root = manifest_dir.parent().unwrap().parent().unwrap();
vec![
manifest_dir.join("README.md"),
manifest_dir.join("src").join("examples.txt"),
root.join("README.md"),
root.join("CONTRIBUTING.md"),
root.join("crates").join("stuffr").join("README.md"),
root.join("crates").join("stuffr-core").join("README.md"),
root.join("crates").join("stuffr-formats").join("README.md"),
]
}
#[test]
fn no_document_carries_a_stale_unqualified_codec_or_container_count() {
const LEGACY_ONLY_CODECS: &[&str] = &["compress"];
const LEGACY_ONLY_CONTAINERS: &[&str] = &["lha", "arj", "arc", "zoo"];
const CUES: &[&str] = &[
"pure",
"pure-tier",
"writable",
"round-trip",
"read-only",
"legacy",
"no-default-features",
];
const QUALIFIERS: &[&str] = &["round-trip", "read-only", "pure-tier"];
const CUE_WINDOW: usize = 8;
let registry = stuffr::registry();
let rows = registry.matrix();
let is_container = |id| registry.container(id).is_some();
let count = |container: bool, pred: &dyn Fn(&stuffr::FormatRow) -> bool| {
rows.iter()
.filter(|r| is_container(r.id) == container && pred(r))
.count()
};
let legacy = |container: bool, id: &str| {
if container {
LEGACY_ONLY_CONTAINERS.contains(&id)
} else {
LEGACY_ONLY_CODECS.contains(&id)
}
};
let full = |container: bool| count(container, &|_| true);
let narrowed = |container: bool| {
[
count(container, &|r| r.write),
count(container, &|r| !r.write),
count(container, &|r| !legacy(container, r.id.as_str())),
]
};
const NUMBERS: &[(&str, usize)] = &[
("one", 1),
("two", 2),
("three", 3),
("four", 4),
("five", 5),
("six", 6),
("seven", 7),
("eight", 8),
("nine", 9),
("ten", 10),
("eleven", 11),
("twelve", 12),
("thirteen", 13),
("fourteen", 14),
("fifteen", 15),
("sixteen", 16),
("seventeen", 17),
("eighteen", 18),
("nineteen", 19),
("twenty", 20),
];
let mut claims_checked = 0usize;
for doc in user_facing_documents() {
let text = std::fs::read_to_string(&doc).unwrap();
let flat = text.split_whitespace().collect::<Vec<_>>().join(" ");
for clause in flat.split(". ").flat_map(|s| s.split('|')) {
let words: Vec<String> = clause
.split(' ')
.map(|w| {
w.trim_matches(|c: char| !c.is_alphanumeric() && c != '-')
.to_lowercase()
})
.collect();
for i in 0..words.len() {
let Some((_, value)) = NUMBERS.iter().find(|(w, _)| *w == words[i]) else {
continue;
};
let Some(noun) = words.get(i + 1) else {
continue;
};
if QUALIFIERS.contains(&noun.as_str()) {
continue;
}
let container = match noun.as_str() {
"containers" => true,
"codecs" => false,
_ => continue,
};
claims_checked += 1;
let from = i.saturating_sub(CUE_WINDOW);
let cued = words[from..=i + 1]
.iter()
.any(|w| CUES.contains(&w.as_str()));
let want_full = full(container);
let ok = if cued {
*value == want_full || narrowed(container).contains(value)
} else {
*value == want_full
};
assert!(
ok,
"{} says `{} {noun}`, but this build registers {want_full} {noun} \
(narrowed counts, allowed only in a clause naming a narrowing — \
{CUES:?}, within {CUE_WINDOW} words — are {:?}). The clause is \
{clause:?}",
doc.display(),
words[i],
narrowed(container),
);
}
}
}
assert!(
claims_checked >= 5,
"the scan found only {claims_checked} unqualified counts — it is matching nothing \
and proving nothing"
);
}
#[test]
fn no_document_carries_a_stale_round_trip_or_read_only_count() {
const LEGACY_ONLY_CODECS: &[&str] = &["compress"];
const LEGACY_ONLY_CONTAINERS: &[&str] = &["lha", "arj", "arc", "zoo"];
let registry = stuffr::registry();
let rows = registry.matrix();
let is_container = |id| registry.container(id).is_some();
let count = |container: bool, pred: &dyn Fn(&stuffr::FormatRow) -> bool| {
rows.iter()
.filter(|r| is_container(r.id) == container && pred(r))
.count()
};
let legacy = |container: bool, id: &str| {
if container {
LEGACY_ONLY_CONTAINERS.contains(&id)
} else {
LEGACY_ONLY_CODECS.contains(&id)
}
};
let expected = |qualifier: &str, container: bool| -> usize {
match qualifier {
"round-trip" => count(container, &|r| r.write),
"read-only" => count(container, &|r| !r.write),
"pure-tier" => count(container, &|r| !legacy(container, r.id.as_str())),
other => unreachable!("unhandled qualifier {other}"),
}
};
const NUMBERS: &[(&str, usize)] = &[
("one", 1),
("two", 2),
("three", 3),
("four", 4),
("five", 5),
("six", 6),
("seven", 7),
("eight", 8),
("nine", 9),
("ten", 10),
("eleven", 11),
("twelve", 12),
("thirteen", 13),
("fourteen", 14),
("fifteen", 15),
("sixteen", 16),
("seventeen", 17),
("eighteen", 18),
("nineteen", 19),
("twenty", 20),
];
let docs = user_facing_documents();
let mut claims_checked = 0usize;
for doc in &docs {
let text = std::fs::read_to_string(doc).unwrap();
let flat = text.split_whitespace().collect::<Vec<_>>().join(" ");
let words: Vec<String> = flat
.split(' ')
.map(|w| {
w.trim_matches(|c: char| !c.is_alphanumeric() && c != '-')
.to_lowercase()
})
.collect();
for i in 0..words.len() {
let Some((_, value)) = NUMBERS.iter().find(|(w, _)| *w == words[i]) else {
continue;
};
let Some(qualifier) = words.get(i + 1) else {
continue;
};
if !["round-trip", "read-only", "pure-tier"].contains(&qualifier.as_str()) {
continue;
}
let noun = match words.get(i + 2).map(String::as_str) {
Some("legacy") => words.get(i + 3),
other => other.and_then(|_| words.get(i + 2)),
};
let container = match noun.map(String::as_str) {
Some("containers") => true,
Some("codecs") => false,
_ => continue,
};
claims_checked += 1;
let want = expected(qualifier, container);
assert_eq!(
*value,
want,
"{} says `{} {qualifier}{} {}`, but this build has {want}. This is the \
check that closes the class three stale write-capability claims were found \
by hand in: the row-count guard cannot see the WRITE column, so nothing \
else would say so",
doc.display(),
words[i],
if words.get(i + 2).map(String::as_str) == Some("legacy") {
" legacy"
} else {
""
},
if container { "containers" } else { "codecs" },
);
}
}
assert!(
claims_checked >= 8,
"the scan found only {claims_checked} qualified counts across the {} documents — \
it is matching nothing and proving nothing",
docs.len()
);
}
fn salvage_fixture_crc32(data: &[u8]) -> u32 {
let mut crc: u32 = 0xFFFF_FFFF;
for &byte in data {
crc ^= u32::from(byte);
for _ in 0..8 {
let mask = (crc & 1).wrapping_neg();
crc = (crc >> 1) ^ (0xEDB8_8320 & mask);
}
}
!crc
}
struct SalvageFixtureLocalRecord {
offset: u32,
name: &'static str,
crc: u32,
len: u32,
bytes: Vec<u8>,
}
fn salvage_fixture_local_record(
offset: u32,
name: &'static str,
payload: &'static [u8],
) -> SalvageFixtureLocalRecord {
let crc = salvage_fixture_crc32(payload);
let len = u32::try_from(payload.len()).unwrap();
let mut bytes = Vec::new();
bytes.extend_from_slice(&[0x50, 0x4b, 0x03, 0x04]); bytes.extend_from_slice(&20u16.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(&0x21u16.to_le_bytes()); bytes.extend_from_slice(&crc.to_le_bytes());
bytes.extend_from_slice(&len.to_le_bytes()); bytes.extend_from_slice(&len.to_le_bytes()); bytes.extend_from_slice(&u16::try_from(name.len()).unwrap().to_le_bytes());
bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(name.as_bytes());
bytes.extend_from_slice(payload);
SalvageFixtureLocalRecord {
offset,
name,
crc,
len,
bytes,
}
}
fn salvage_fixture_central_record(rec: &SalvageFixtureLocalRecord) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&[0x50, 0x4b, 0x01, 0x02]); bytes.extend_from_slice(&20u16.to_le_bytes()); bytes.extend_from_slice(&20u16.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(&0x21u16.to_le_bytes()); bytes.extend_from_slice(&rec.crc.to_le_bytes());
bytes.extend_from_slice(&rec.len.to_le_bytes()); bytes.extend_from_slice(&rec.len.to_le_bytes()); bytes.extend_from_slice(&u16::try_from(rec.name.len()).unwrap().to_le_bytes());
bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(&0u32.to_le_bytes()); bytes.extend_from_slice(&rec.offset.to_le_bytes()); bytes.extend_from_slice(rec.name.as_bytes());
bytes
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SalvageFixtureDuplicates {
Identical,
Distinct,
}
fn build_shadowing_zip_fixture() -> Vec<u8> {
build_duplicating_zip_fixture(SalvageFixtureDuplicates::Identical)
}
fn build_duplicate_name_zip_fixture() -> Vec<u8> {
build_duplicating_zip_fixture(SalvageFixtureDuplicates::Distinct)
}
fn build_duplicating_zip_fixture(duplicates: SalvageFixtureDuplicates) -> Vec<u8> {
let originals_src: [(&str, &[u8]); 6] = [
("one.txt", b"payload for entry one"),
("two.txt", b"payload for entry two"),
(
"shadowed-a.bin",
b"payload for the first record that will be shadowed",
),
(
"shadowed-b.bin",
b"payload for the second record that will be shadowed",
),
("five.txt", b"payload for entry five"),
("six.txt", b"payload for entry six"),
];
let mut out = Vec::new();
let mut originals = Vec::new();
for (name, payload) in originals_src {
let rec = salvage_fixture_local_record(u32::try_from(out.len()).unwrap(), name, payload);
out.extend_from_slice(&rec.bytes);
originals.push(rec);
}
const DIFFERENT_A: &[u8] = b"DIFFERENT bytes under the first duplicated name!!!";
const DIFFERENT_B: &[u8] = b"DIFFERENT bytes under the second duplicated name!!!";
let (payload_a, payload_b) = match duplicates {
SalvageFixtureDuplicates::Identical => (originals_src[2].1, originals_src[3].1),
SalvageFixtureDuplicates::Distinct => {
assert_eq!(originals_src[2].1.len(), DIFFERENT_A.len());
assert_eq!(originals_src[3].1.len(), DIFFERENT_B.len());
(DIFFERENT_A, DIFFERENT_B)
}
};
let dup_a = salvage_fixture_local_record(
u32::try_from(out.len()).unwrap(),
"shadowed-a.bin",
payload_a,
);
out.extend_from_slice(&dup_a.bytes);
let dup_b = salvage_fixture_local_record(
u32::try_from(out.len()).unwrap(),
"shadowed-b.bin",
payload_b,
);
out.extend_from_slice(&dup_b.bytes);
let cd_start = u32::try_from(out.len()).unwrap();
for rec in &originals {
out.extend_from_slice(&salvage_fixture_central_record(rec));
}
out.extend_from_slice(&salvage_fixture_central_record(&dup_a));
out.extend_from_slice(&salvage_fixture_central_record(&dup_b));
let cd_size = u32::try_from(out.len()).unwrap() - cd_start;
out.extend_from_slice(&[0x50, 0x4b, 0x05, 0x06]); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&8u16.to_le_bytes()); out.extend_from_slice(&8u16.to_le_bytes()); out.extend_from_slice(&cd_size.to_le_bytes());
out.extend_from_slice(&cd_start.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out
}
#[test]
fn unpack_refuses_a_nul_bearing_entry_name_rather_than_failing() {
let dir = tmp_dir();
let rec = salvage_fixture_local_record(0, "zt\0ee\0a.txt", b"hello world\n");
let mut out = rec.bytes.clone();
let cd_start = u32::try_from(out.len()).unwrap();
out.extend_from_slice(&salvage_fixture_central_record(&rec));
let cd_size = u32::try_from(out.len()).unwrap() - cd_start;
out.extend_from_slice(&[0x50, 0x4b, 0x05, 0x06]);
out.extend_from_slice(&0u16.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out.extend_from_slice(&1u16.to_le_bytes());
out.extend_from_slice(&1u16.to_le_bytes());
out.extend_from_slice(&cd_size.to_le_bytes());
out.extend_from_slice(&cd_start.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
let archive = dir.join("nul.zip");
std::fs::write(&archive, &out).unwrap();
let listed = run_output(&["list", archive.to_str().unwrap()]);
assert_eq!(listed.status.code(), Some(0), "list reads the archive fine");
let unpacked = run_output(&[
"unpack",
archive.to_str().unwrap(),
"-C",
dir.join("out").to_str().unwrap(),
]);
let stderr = String::from_utf8(unpacked.stderr).unwrap();
assert_eq!(
unpacked.status.code(),
Some(7),
"hostile input must never be exit 1: {stderr}"
);
assert!(
stderr.contains("NUL"),
"and the refusal must name what it refused: {stderr}"
);
}
fn write_shadowing_zip_fixture(dir: &Path) -> PathBuf {
let path = dir.join("shadowing.zip");
std::fs::write(&path, build_shadowing_zip_fixture()).unwrap();
path
}
fn write_duplicate_name_zip_fixture(dir: &Path) -> PathBuf {
let path = dir.join("duplicate-names.zip");
std::fs::write(&path, build_duplicate_name_zip_fixture()).unwrap();
path
}
#[test]
fn the_two_salvage_duplicate_fixtures_share_a_shape_and_differ_in_content() {
let identical = build_shadowing_zip_fixture();
let distinct = build_duplicate_name_zip_fixture();
assert_eq!(
identical.len(),
distinct.len(),
"length-preserving by construction"
);
assert_ne!(identical, distinct, "but not the same bytes");
let first_dup_at = identical
.windows(4)
.enumerate()
.filter(|(_, w)| *w == b"PK\x03\x04")
.map(|(i, _)| i)
.nth(6)
.expect("the fixture has eight local headers");
assert_eq!(identical[..first_dup_at], distinct[..first_dup_at]);
}
#[test]
fn the_duplicate_name_fixture_reads_the_same_as_the_identical_one() {
let dir = tmp_dir();
let archive = write_duplicate_name_zip_fixture(&dir);
let list_out = run_output(&["list", archive.to_str().unwrap()]);
let listing = String::from_utf8(list_out.stdout).unwrap();
assert_eq!(
listing.lines().count(),
6,
"`list` collapses a repeated name whatever the bytes under it: {listing}"
);
let warnings = String::from_utf8(list_out.stderr).unwrap();
assert!(
warnings.contains("8") && warnings.contains("6"),
"`list` must still warn that the index declares more records than it reaches: {warnings}"
);
let salvage_out = run_output(&["salvage", archive.to_str().unwrap(), "--list"]);
let stdout = String::from_utf8(salvage_out.stdout).unwrap();
assert_eq!(
stdout.lines().count(),
8,
"`salvage --list` must reach every scanned record here too: {stdout}"
);
assert_eq!(
stdout.matches("Intact").count(),
8,
"every record verifies: the duplicates carry their own correct CRC-32, which is what \
makes losing one of them a real loss: {stdout}"
);
}
#[test]
fn salvage_recovers_both_records_under_a_repeated_name() {
let dir = tmp_dir();
let archive = write_duplicate_name_zip_fixture(&dir);
let out_dir = dir.join("recovered");
let out = run_output(&[
"salvage",
archive.to_str().unwrap(),
"-C",
out_dir.to_str().unwrap(),
]);
assert_eq!(
out.status.code(),
Some(4),
"a run that had to change a name must not exit 0: {}",
String::from_utf8_lossy(&out.stderr)
);
let mut names: Vec<String> = std::fs::read_dir(&out_dir)
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
.collect();
names.sort();
assert_eq!(
names,
[
"five.txt",
"one.txt",
"shadowed-a.bin",
"shadowed-a.bin.salvaged-6",
"shadowed-b.bin",
"shadowed-b.bin.salvaged-7",
"six.txt",
"two.txt",
],
"eight records written must be eight files, with the later record of each pair \
under a name derived from its own scan position"
);
let first = std::fs::read(out_dir.join("shadowed-a.bin")).unwrap();
let second = std::fs::read(out_dir.join("shadowed-a.bin.salvaged-6")).unwrap();
assert_eq!(
first, b"payload for the first record that will be shadowed",
"record 2 must keep its own name and its own bytes"
);
assert_eq!(
second, b"DIFFERENT bytes under the first duplicated name!!!",
"record 6's bytes must be on disk too, not lost to an overwrite"
);
assert_ne!(first, second, "the fixture's whole premise");
let summary = String::from_utf8(out.stderr).unwrap();
assert!(
summary.contains("8 written"),
"every written record is counted, and now there are eight files to match: {summary}"
);
assert!(
summary.contains("2 renamed to avoid a name collision"),
"the summary must say a name had to be changed: {summary}"
);
}
#[test]
fn a_proven_duplicate_is_still_skipped_rather_than_renamed() {
let dir = tmp_dir();
let archive = write_shadowing_zip_fixture(&dir);
let out_dir = dir.join("recovered");
let out = run_output(&[
"salvage",
archive.to_str().unwrap(),
"-C",
out_dir.to_str().unwrap(),
]);
assert_eq!(out.status.code(), Some(4));
assert_eq!(
std::fs::read_dir(&out_dir).unwrap().count(),
6,
"six distinct names, and the two byte-identical duplicates add nothing"
);
let summary = String::from_utf8(out.stderr).unwrap();
assert!(
summary.contains("2 skipped"),
"a proven copy is skipped, as it always was: {summary}"
);
assert!(
summary.contains("0 renamed to avoid a name collision"),
"and nothing needed renaming: {summary}"
);
}
#[test]
fn a_name_collision_and_a_shadow_are_two_different_annotations() {
let dir = tmp_dir();
let identical = write_shadowing_zip_fixture(&dir);
let distinct = write_duplicate_name_zip_fixture(&dir);
let id_rows =
String::from_utf8(run_output(&["salvage", identical.to_str().unwrap(), "--list"]).stdout)
.unwrap();
assert_eq!(
id_rows.matches("[shadowed: dup of #").count(),
2,
"byte-identical duplicates are shadows: {id_rows}"
);
assert_eq!(
id_rows.matches("[name collision:").count(),
0,
"a proven copy reports the stronger fact and only that one: {id_rows}"
);
let di_rows =
String::from_utf8(run_output(&["salvage", distinct.to_str().unwrap(), "--list"]).stdout)
.unwrap();
assert_eq!(
di_rows.matches("[shadowed: dup of #").count(),
0,
"no checksum agrees here, so nothing is a proven copy: {di_rows}"
);
assert!(
di_rows.contains("[name collision: #2 uses this name too]"),
"record 6 repeats record 2's name: {di_rows}"
);
assert!(
di_rows.contains("[name collision: #3 uses this name too]"),
"record 7 repeats record 3's name: {di_rows}"
);
assert_eq!(
di_rows.matches("[name collision:").count(),
2,
"exactly the two repeated names, never the six originals: {di_rows}"
);
}
#[test]
fn salvage_list_reaches_every_record() {
let dir = tmp_dir();
let archive = write_shadowing_zip_fixture(&dir);
let listing =
String::from_utf8(run_output(&["list", archive.to_str().unwrap()]).stdout).unwrap();
assert_eq!(
listing.lines().count(),
6,
"sanity check on the fixture itself: `list` must collapse to 6 rows: {listing}"
);
let out_dir = dir.join("recovered");
let salvage_out = run_output(&[
"salvage",
archive.to_str().unwrap(),
"-C",
out_dir.to_str().unwrap(),
"--list",
]);
let stdout = String::from_utf8(salvage_out.stdout).unwrap();
assert_eq!(
stdout.lines().count(),
8,
"`salvage --list` must reach every scanned record, shadowed ones included: {stdout}"
);
let shadow_lines: Vec<&str> = stdout
.lines()
.filter(|l| l.contains("[shadowed: dup of #"))
.collect();
assert_eq!(
shadow_lines.len(),
2,
"exactly the two duplicate records must be annotated as shadows: {stdout}"
);
assert!(
shadow_lines[0].contains("[shadowed: dup of #2]"),
"the first shadow must name scan position 2 (shadowed-a.bin) as its original: {stdout}"
);
assert!(
shadow_lines[1].contains("[shadowed: dup of #3]"),
"the second shadow must name scan position 3 (shadowed-b.bin) as its original: {stdout}"
);
}
#[test]
fn salvage_index_names_a_scan_position_not_a_list_index() {
let dir = tmp_dir();
let archive = write_shadowing_zip_fixture(&dir);
let list_out = run_output(&["list", archive.to_str().unwrap(), "--index", "6"]);
assert!(
!list_out.status.success(),
"list's own numbering has only 6 rows (0-5); --index 6 must not succeed"
);
let out_dir = dir.join("recovered");
let out = run_output(&[
"salvage",
archive.to_str().unwrap(),
"-C",
out_dir.to_str().unwrap(),
"--index",
"6",
"--list",
]);
assert_eq!(
out.status.code(),
Some(4),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8(out.stdout).unwrap();
let rows: Vec<&str> = stdout.lines().collect();
assert_eq!(
rows.len(),
8,
"--list must keep showing the full scan under --index too: {stdout}"
);
for (pos, row) in rows.iter().enumerate() {
assert!(
row.starts_with(&format!("{pos} ")),
"row {pos} must lead with its own scan position: {stdout}"
);
match pos {
6 => assert!(
row.contains("[shadowed: dup of #2]") && !row.contains("[not selected]"),
"the selected row must show its shadow annotation, not a not-selected one: \
{row}"
),
7 => assert!(
row.contains("[shadowed: dup of #3]") && !row.contains("[not selected]"),
"an unselected shadow must still show its shadow annotation: {row}"
),
_ => assert!(
row.contains("[not selected]"),
"every non-shadow position other than the selected one must be tagged \
not selected: {row}"
),
}
}
let out_dir2 = dir.join("recovered2");
let out2 = run_output(&[
"salvage",
archive.to_str().unwrap(),
"-C",
out_dir2.to_str().unwrap(),
"--index",
"0",
]);
assert!(
out2.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out2.stderr)
);
let names: Vec<String> = std::fs::read_dir(&out_dir2)
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
.collect();
assert_eq!(
names,
vec!["one.txt".to_string()],
"-C DIR --index 0 must leave ONLY the selected record in DIR; found {names:?}"
);
let bad = run_output(&[
"salvage",
archive.to_str().unwrap(),
"-C",
out_dir.to_str().unwrap(),
"--index",
"99",
]);
assert!(!bad.status.success());
let stderr = String::from_utf8_lossy(&bad.stderr);
assert!(
stderr.contains("scan position"),
"an out-of-range --index must name it a scan position: {stderr}"
);
assert!(
!stderr.to_lowercase().contains("index "),
"the message must never say \"index\", to avoid being read as list's numbering: {stderr}"
);
}
#[test]
fn salvage_without_directory_is_refused() {
let dir = tmp_dir();
let archive = write_shadowing_zip_fixture(&dir);
let out = run_output(&["salvage", archive.to_str().unwrap()]);
assert!(!out.status.success());
assert_eq!(out.status.code(), Some(2), "missing -C is a usage error");
}
#[test]
fn salvage_default_run_recovers_and_reports_the_shadow_skips() {
let dir = tmp_dir();
let archive = write_shadowing_zip_fixture(&dir);
let out_dir = dir.join("recovered");
let out = run_output(&[
"salvage",
archive.to_str().unwrap(),
"-C",
out_dir.to_str().unwrap(),
]);
assert_eq!(
out.status.code(),
Some(4),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
out.stdout.is_empty(),
"the default report goes to stderr, not stdout: {:?}",
String::from_utf8_lossy(&out.stdout)
);
let stderr = String::from_utf8(out.stderr).unwrap();
assert!(
stderr.contains("8 scanned")
&& stderr.contains("6 written")
&& stderr.contains("2 skipped"),
"the summary must count the two shadow skips: {stderr}"
);
assert_eq!(
std::fs::read(out_dir.join("one.txt")).unwrap(),
b"payload for entry one"
);
assert_eq!(
std::fs::read(out_dir.join("shadowed-a.bin")).unwrap(),
b"payload for the first record that will be shadowed"
);
assert_eq!(std::fs::read_dir(&out_dir).unwrap().count(), 6);
}
#[test]
fn salvage_recovers_past_an_entry_whose_name_containment_refuses() {
let dir = tmp_dir();
let mut bytes = Vec::new();
for (name, payload) in [
("first.txt", &b"AAAAAAAAAA"[..]),
("", &b"BBBBBBBBBB"[..]),
("third.txt", &b"CCCCCCCCCC"[..]),
] {
let rec = salvage_fixture_local_record(u32::try_from(bytes.len()).unwrap(), name, payload);
bytes.extend_from_slice(&rec.bytes);
}
let archive = dir.join("midbad.zip");
std::fs::write(&archive, &bytes).unwrap();
let out_dir = dir.join("recovered");
let out = run_output(&[
"salvage",
archive.to_str().unwrap(),
"-C",
out_dir.to_str().unwrap(),
]);
let stderr = String::from_utf8(out.stderr).unwrap();
assert_eq!(
out.status.code(),
Some(7),
"the hostile-path contract is unchanged — the RUN still answers 7: {stderr}"
);
assert_eq!(
std::fs::read(out_dir.join("third.txt")).unwrap(),
b"CCCCCCCCCC",
"the record AFTER the refused one is exactly what the old behaviour lost"
);
assert_eq!(
std::fs::read(out_dir.join("first.txt")).unwrap(),
b"AAAAAAAAAA"
);
assert_eq!(
std::fs::read_dir(&out_dir).unwrap().count(),
2,
"and nothing at all is written for the refused entry"
);
assert!(
stderr.contains("3 scanned")
&& stderr.contains("2 written")
&& stderr.contains("1 skipped"),
"the summary must print — a run that returns Err never prints one, so a user was \
never told what WAS recovered: {stderr}"
);
assert!(
stderr.contains("empty entry name"),
"and the refusal must still be named, on stderr, every run: {stderr}"
);
}
fn salvage_fixture_local_record_with(
offset: u32,
name: &'static str,
payload: &'static [u8],
method: u16,
crc: u32,
) -> SalvageFixtureLocalRecord {
let len = u32::try_from(payload.len()).unwrap();
let mut bytes = Vec::new();
bytes.extend_from_slice(&[0x50, 0x4b, 0x03, 0x04]);
bytes.extend_from_slice(&20u16.to_le_bytes());
bytes.extend_from_slice(&0u16.to_le_bytes());
bytes.extend_from_slice(&method.to_le_bytes());
bytes.extend_from_slice(&0u16.to_le_bytes());
bytes.extend_from_slice(&0x21u16.to_le_bytes());
bytes.extend_from_slice(&crc.to_le_bytes());
bytes.extend_from_slice(&len.to_le_bytes());
bytes.extend_from_slice(&len.to_le_bytes());
bytes.extend_from_slice(&u16::try_from(name.len()).unwrap().to_le_bytes());
bytes.extend_from_slice(&0u16.to_le_bytes());
bytes.extend_from_slice(name.as_bytes());
bytes.extend_from_slice(payload);
SalvageFixtureLocalRecord {
offset,
name,
crc,
len,
bytes,
}
}
fn build_partial_and_unverified_zip_fixture() -> Vec<u8> {
let mismatched_payload: &[u8] = b"this payload's crc field below is wrong on purpose";
let undecodable_payload: &[u8] = b"this method is recognised but this build cannot decode it";
let mut out = Vec::new();
let rec0 = salvage_fixture_local_record_with(
u32::try_from(out.len()).unwrap(),
"mismatched.bin",
mismatched_payload,
0, 0xDEAD_BEEF,
);
out.extend_from_slice(&rec0.bytes);
let rec1 = salvage_fixture_local_record_with(
u32::try_from(out.len()).unwrap(),
"undecodable.bin",
undecodable_payload,
99, salvage_fixture_crc32(undecodable_payload),
);
out.extend_from_slice(&rec1.bytes);
let cd_start = u32::try_from(out.len()).unwrap();
out.extend_from_slice(&salvage_fixture_central_record(&rec0));
out.extend_from_slice(&salvage_fixture_central_record(&rec1));
let cd_size = u32::try_from(out.len()).unwrap() - cd_start;
out.extend_from_slice(&[0x50, 0x4b, 0x05, 0x06]);
out.extend_from_slice(&0u16.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out.extend_from_slice(&2u16.to_le_bytes());
out.extend_from_slice(&2u16.to_le_bytes());
out.extend_from_slice(&cd_size.to_le_bytes());
out.extend_from_slice(&cd_start.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out
}
#[test]
fn salvage_list_names_partial_and_unverified_causes() {
let dir = tmp_dir();
let path = dir.join("mixed.zip");
std::fs::write(&path, build_partial_and_unverified_zip_fixture()).unwrap();
let out_dir = dir.join("recovered");
let out = run_output(&[
"salvage",
path.to_str().unwrap(),
"-C",
out_dir.to_str().unwrap(),
"--list",
]);
assert_eq!(
out.status.code(),
Some(3),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8(out.stdout).unwrap();
let rows: Vec<&str> = stdout.lines().collect();
assert_eq!(rows.len(), 2, "{stdout}");
assert!(
rows[0].contains("Partial (checksum mismatch)"),
"a full decode that disagrees with the declared CRC-32 must name that cause: {stdout}"
);
assert!(
rows[1].contains("Unverified (undecodable method)"),
"a recognised-but-undecodable method must name that cause: {stdout}"
);
assert_eq!(
std::fs::read(out_dir.join("mismatched.bin.partial")).unwrap(),
b"this payload's crc field below is wrong on purpose"
);
assert!(!out_dir.join("mismatched.bin").exists());
assert!(!out_dir.join("undecodable.bin").exists());
}
#[test]
fn salvage_skipped_partial_names_its_cause() {
let dir = tmp_dir();
let path = dir.join("mixed.zip");
std::fs::write(&path, build_partial_and_unverified_zip_fixture()).unwrap();
let out_dir = dir.join("recovered");
let out = run_output(&[
"salvage",
path.to_str().unwrap(),
"-C",
out_dir.to_str().unwrap(),
"--partial=skip",
"--list",
]);
assert_eq!(
out.status.code(),
Some(3),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8(out.stdout).unwrap();
let rows: Vec<&str> = stdout.lines().collect();
assert_eq!(rows.len(), 2, "{stdout}");
assert!(
rows[0].contains("Partial (checksum mismatch)"),
"--partial=skip must still name the cause, not fall back to bare \"Partial\": {stdout}"
);
assert!(!out_dir.join("mismatched.bin").exists());
assert!(!out_dir.join("mismatched.bin.partial").exists());
}
#[test]
fn salvage_list_works_with_no_destination() {
let dir = tmp_dir();
let archive = write_shadowing_zip_fixture(&dir);
let out = run_output(&["salvage", archive.to_str().unwrap(), "--list"]);
assert_eq!(
out.status.code(),
Some(4),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8(out.stdout).unwrap();
assert_eq!(
stdout.lines().count(),
8,
"a report-only run must still list the full scan: {stdout}"
);
assert_eq!(
std::fs::read_dir(&dir).unwrap().count(),
1,
"report-only `--list` must touch nothing on disk but the archive itself"
);
}
#[test]
fn salvage_output_recovers_one_entry_to_a_named_file() {
let dir = tmp_dir();
let archive = write_shadowing_zip_fixture(&dir);
let out_file = dir.join("recovered-one.txt");
let out = run_output(&[
"salvage",
archive.to_str().unwrap(),
"-o",
out_file.to_str().unwrap(),
"--index",
"0",
]);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(std::fs::read(&out_file).unwrap(), b"payload for entry one");
let too_many = run_output(&[
"salvage",
archive.to_str().unwrap(),
"-o",
out_file.to_str().unwrap(),
"--index",
"0",
"--index",
"1",
]);
assert_eq!(too_many.status.code(), Some(2));
let both = run_output(&[
"salvage",
archive.to_str().unwrap(),
"-o",
out_file.to_str().unwrap(),
"-C",
dir.join("out").to_str().unwrap(),
"--index",
"0",
]);
assert_eq!(both.status.code(), Some(2));
}
#[test]
fn salvage_output_suffixes_a_partial_entry() {
let dir = tmp_dir();
let path = dir.join("mixed.zip");
std::fs::write(&path, build_partial_and_unverified_zip_fixture()).unwrap();
let out_file = dir.join("wanted.bin");
let out = run_output(&[
"salvage",
path.to_str().unwrap(),
"-o",
out_file.to_str().unwrap(),
"--index",
"0",
]);
assert_eq!(
out.status.code(),
Some(4),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
!out_file.exists(),
"a Partial entry must never land under the literal -o path"
);
let partial_path = dir.join("wanted.bin.partial");
assert_eq!(
std::fs::read(&partial_path).unwrap(),
b"this payload's crc field below is wrong on purpose"
);
}
struct RealZipLocalEntry {
offset: usize,
name: String,
crc_offset: usize,
payload: std::ops::Range<usize>,
record_len: usize,
}
fn parse_real_zip_local_entries(bytes: &[u8]) -> Vec<RealZipLocalEntry> {
let mut out = Vec::new();
let mut i = 0usize;
while i + 30 <= bytes.len() && bytes[i..i + 4] == [0x50, 0x4b, 0x03, 0x04] {
let compressed_size =
u32::from_le_bytes(bytes[i + 18..i + 22].try_into().unwrap()) as usize;
let name_len = u16::from_le_bytes([bytes[i + 26], bytes[i + 27]]) as usize;
let extra_len = u16::from_le_bytes([bytes[i + 28], bytes[i + 29]]) as usize;
let name_start = i + 30;
let name = String::from_utf8(bytes[name_start..name_start + name_len].to_vec())
.expect("this section's own fixture names are plain ASCII");
let payload_start = name_start + name_len + extra_len;
let payload = payload_start..payload_start + compressed_size;
let record_len = payload.end - i;
out.push(RealZipLocalEntry {
offset: i,
name,
crc_offset: i + 14,
payload,
record_len,
});
i += record_len;
}
out
}
fn find_real_zip_eocd(bytes: &[u8]) -> usize {
bytes
.windows(4)
.rposition(|w| w == b"PK\x05\x06")
.expect("every archive this section builds must carry an EOCD record")
}
fn build_real_healthy_zip(dir: &Path) -> (PathBuf, [(&'static str, &'static [u8]); 3]) {
let entries: [(&str, &[u8]); 3] = [
("alpha.txt", b"alpha entry payload, first in the archive"),
("beta.txt", b"beta entry payload, sits in the middle"),
(
"gamma.bin",
b"gamma entry payload, the last one in the archive",
),
];
let src = dir.join("src");
std::fs::create_dir(&src).unwrap();
for (name, data) in &entries {
std::fs::write(src.join(name), data).unwrap();
}
let zip_bin = require_bin("zip");
let archive = dir.join("healthy.zip");
run_tool(
&zip_bin,
&[
os(&"-q"),
os(&"-0"),
os(&"-X"),
os(&archive),
os(&"alpha.txt"),
os(&"beta.txt"),
os(&"gamma.bin"),
],
&src,
b"",
);
(archive, entries)
}
fn salvage_list_rows(archive: &Path) -> Vec<String> {
let out = run_output(&["salvage", archive.to_str().unwrap(), "--list"]);
String::from_utf8(out.stdout)
.unwrap()
.lines()
.map(str::to_string)
.collect()
}
fn mutate_zeroed_central_directory(healthy: &[u8]) -> Vec<u8> {
let mut bytes = healthy.to_vec();
let eocd = find_real_zip_eocd(&bytes);
let cd_offset = u32::from_le_bytes(bytes[eocd + 16..eocd + 20].try_into().unwrap()) as usize;
for b in &mut bytes[cd_offset..] {
*b = 0;
}
bytes
}
fn mutate_hole_punched_mid_payload(healthy: &[u8]) -> Vec<u8> {
let mut bytes = healthy.to_vec();
let real_entries = parse_real_zip_local_entries(&bytes);
let beta = &real_entries[1];
assert!(
beta.payload.len() >= 15,
"sanity: room to punch a hole without running past this entry's own payload"
);
for b in &mut bytes[beta.payload.start + 5..beta.payload.start + 15] {
*b ^= 0xFF;
}
bytes
}
fn mutate_one_crc_corrupted(healthy: &[u8]) -> Vec<u8> {
let mut bytes = healthy.to_vec();
let real_entries = parse_real_zip_local_entries(&bytes);
let alpha_crc_offset = real_entries[0].crc_offset;
bytes[alpha_crc_offset] ^= 0xFF;
bytes
}
fn duplicate_first_central_directory_record(healthy: &[u8]) -> Vec<u8> {
let bytes = healthy.to_vec();
let eocd = find_real_zip_eocd(&bytes);
let disk_no = u16::from_le_bytes(bytes[eocd + 4..eocd + 6].try_into().unwrap());
let disk_cd = u16::from_le_bytes(bytes[eocd + 6..eocd + 8].try_into().unwrap());
let cd_total = u16::from_le_bytes(bytes[eocd + 10..eocd + 12].try_into().unwrap());
let cd_size = u32::from_le_bytes(bytes[eocd + 12..eocd + 16].try_into().unwrap()) as usize;
let cd_offset = u32::from_le_bytes(bytes[eocd + 16..eocd + 20].try_into().unwrap()) as usize;
let comment_len = u16::from_le_bytes(bytes[eocd + 20..eocd + 22].try_into().unwrap());
let real_entries = parse_real_zip_local_entries(&bytes);
let first = &real_entries[0];
let duplicate_record = bytes[first.offset..first.offset + first.record_len].to_vec();
let old_cd = bytes[cd_offset..cd_offset + cd_size].to_vec();
assert_eq!(
&old_cd[0..4],
&[0x50, 0x4b, 0x01, 0x02],
"sanity: the central directory's first record"
);
let name_len = u16::from_le_bytes(old_cd[28..30].try_into().unwrap()) as usize;
let extra_len = u16::from_le_bytes(old_cd[30..32].try_into().unwrap()) as usize;
let comment_len_rec = u16::from_le_bytes(old_cd[32..34].try_into().unwrap()) as usize;
let first_cd_len = 46 + name_len + extra_len + comment_len_rec;
let mut duplicate_cd_record = old_cd[0..first_cd_len].to_vec();
let new_local_offset = cd_offset as u32;
duplicate_cd_record[42..46].copy_from_slice(&new_local_offset.to_le_bytes());
let new_cd_start = cd_offset + duplicate_record.len();
let mut new_cd = old_cd.clone();
new_cd.extend_from_slice(&duplicate_cd_record);
let new_total = cd_total + 1;
let mut out = bytes[..cd_offset].to_vec();
out.extend_from_slice(&duplicate_record);
out.extend_from_slice(&new_cd);
out.extend_from_slice(&[0x50, 0x4b, 0x05, 0x06]);
out.extend_from_slice(&disk_no.to_le_bytes());
out.extend_from_slice(&disk_cd.to_le_bytes());
out.extend_from_slice(&new_total.to_le_bytes());
out.extend_from_slice(&new_total.to_le_bytes());
out.extend_from_slice(&(new_cd.len() as u32).to_le_bytes());
out.extend_from_slice(&(new_cd_start as u32).to_le_bytes());
out.extend_from_slice(&comment_len.to_le_bytes());
out
}
#[test]
fn damage_catalogue_truncated_tail() {
let dir = tmp_dir();
let (archive, _entries) = build_real_healthy_zip(&dir);
let healthy = std::fs::read(&archive).unwrap();
let real_entries = parse_real_zip_local_entries(&healthy);
assert_eq!(
real_entries
.iter()
.map(|e| e.name.as_str())
.collect::<Vec<_>>(),
vec!["alpha.txt", "beta.txt", "gamma.bin"],
"sanity: the reference archive's own three entries, in file order"
);
let gamma = &real_entries[2];
let cut = gamma.payload.start + gamma.payload.len() / 2;
let path = dir.join("truncated.zip");
std::fs::write(&path, &healthy[..cut]).unwrap();
let rows = salvage_list_rows(&path);
assert_eq!(
rows.len(),
3,
"all three local headers are there, and the third one being uncompletable is a \
fact worth a row: {rows:?}"
);
assert!(
rows[0].contains("alpha.txt") && rows[0].contains("Intact"),
"{rows:?}"
);
assert!(
rows[1].contains("beta.txt") && rows[1].contains("Intact"),
"{rows:?}"
);
assert!(
rows[2].contains("gamma.bin") && rows[2].contains("Partial (truncated)"),
"the truncated entry must be named, and named as truncated rather than as a bad \
checksum — the payload ran out, nothing disagreed: {rows:?}"
);
let out_dir = dir.join("recovered");
let out = run_output(&[
"salvage",
path.to_str().unwrap(),
"-C",
out_dir.to_str().unwrap(),
]);
assert_eq!(
out.status.code(),
Some(4),
"a run that dropped something must not exit 0: {}",
String::from_utf8_lossy(&out.stderr)
);
let mut names: Vec<String> = std::fs::read_dir(&out_dir)
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
.collect();
names.sort();
assert_eq!(
names,
["alpha.txt", "beta.txt", "gamma.bin.partial"],
"the surviving prefix lands under `.partial`, never under `gamma.bin`"
);
let recovered = std::fs::read(out_dir.join("gamma.bin.partial")).unwrap();
let declared = gamma.payload.len();
assert!(
!recovered.is_empty() && recovered.len() < declared,
"{} recovered bytes against a declared {declared}",
recovered.len()
);
assert_eq!(
recovered[..],
healthy[gamma.payload.start..cut],
"every recovered byte must come from the truncated file itself — `zip -FF` fills \
the gap from its own buffer and calls the result fixed"
);
}
#[test]
fn every_truncation_point_inside_the_last_entry_is_reported() {
let dir = tmp_dir();
let (archive, _entries) = build_real_healthy_zip(&dir);
let healthy = std::fs::read(&archive).unwrap();
let real_entries = parse_real_zip_local_entries(&healthy);
let gamma = &real_entries[2];
let declared = gamma.payload.len();
for keep in [0, 1, declared / 4, declared / 2, declared - 1] {
let path = dir.join(format!("truncated-{keep}.zip"));
std::fs::write(&path, &healthy[..gamma.payload.start + keep]).unwrap();
let rows = salvage_list_rows(&path);
assert_eq!(
rows.len(),
3,
"keep={keep}: the third header is there and must be counted: {rows:?}"
);
assert!(
rows[2].contains("Partial (truncated)"),
"keep={keep}: {rows:?}"
);
let out_dir = dir.join(format!("out-{keep}"));
let out = run_output(&[
"salvage",
path.to_str().unwrap(),
"-C",
out_dir.to_str().unwrap(),
]);
assert_eq!(
out.status.code(),
Some(4),
"keep={keep}: must not exit 0: {}",
String::from_utf8_lossy(&out.stderr)
);
}
let whole = dir.join("whole.zip");
std::fs::write(&whole, &healthy).unwrap();
let out = run_output(&["salvage", whole.to_str().unwrap(), "--list"]);
assert_eq!(out.status.code(), Some(0));
let rows = String::from_utf8(out.stdout).unwrap();
assert_eq!(rows.matches("Partial").count(), 0, "{rows}");
}
#[test]
fn damage_catalogue_zeroed_central_directory() {
let dir = tmp_dir();
let (archive, _entries) = build_real_healthy_zip(&dir);
let healthy = std::fs::read(&archive).unwrap();
let path = dir.join("zeroed_cd.zip");
std::fs::write(&path, mutate_zeroed_central_directory(&healthy)).unwrap();
let rows = salvage_list_rows(&path);
assert_eq!(rows.len(), 3, "{rows:?}");
for (row, name) in rows.iter().zip(["alpha.txt", "beta.txt", "gamma.bin"]) {
assert!(
row.contains(name) && row.contains("Intact"),
"every entry must still be found by the raw scan and verify Intact: {row}"
);
}
}
#[test]
fn damage_catalogue_hole_punched_mid_payload() {
let dir = tmp_dir();
let (archive, _entries) = build_real_healthy_zip(&dir);
let healthy = std::fs::read(&archive).unwrap();
let path = dir.join("hole_punched.zip");
std::fs::write(&path, mutate_hole_punched_mid_payload(&healthy)).unwrap();
let rows = salvage_list_rows(&path);
assert_eq!(rows.len(), 3, "{rows:?}");
assert!(
rows[0].contains("alpha.txt") && rows[0].contains("Intact"),
"{rows:?}"
);
assert!(
rows[1].contains("beta.txt") && rows[1].contains("Partial"),
"the punched entry's content no longer matches its own declared CRC-32: {rows:?}"
);
assert!(
rows[2].contains("gamma.bin") && rows[2].contains("Intact"),
"{rows:?}"
);
}
#[test]
fn damage_catalogue_one_crc_corrupted() {
let dir = tmp_dir();
let (archive, _entries) = build_real_healthy_zip(&dir);
let healthy = std::fs::read(&archive).unwrap();
let path = dir.join("crc_corrupted.zip");
std::fs::write(&path, mutate_one_crc_corrupted(&healthy)).unwrap();
let rows = salvage_list_rows(&path);
assert_eq!(rows.len(), 3, "{rows:?}");
assert!(
rows[0].contains("alpha.txt") && rows[0].contains("Partial"),
"the corrupted entry's declared checksum no longer matches its (untouched) payload: \
{rows:?}"
);
assert!(
rows[1].contains("beta.txt") && rows[1].contains("Intact"),
"{rows:?}"
);
assert!(
rows[2].contains("gamma.bin") && rows[2].contains("Intact"),
"{rows:?}"
);
}
#[test]
fn damage_catalogue_duplicated_record() {
let dir = tmp_dir();
let (archive, _entries) = build_real_healthy_zip(&dir);
let healthy = std::fs::read(&archive).unwrap();
let path = dir.join("duplicated.zip");
std::fs::write(&path, duplicate_first_central_directory_record(&healthy)).unwrap();
let rows = salvage_list_rows(&path);
assert_eq!(rows.len(), 4, "3 names, 4 physical records: {rows:?}");
assert!(
rows[0].contains("alpha.txt") && rows[0].contains("Intact"),
"{rows:?}"
);
assert!(
rows[1].contains("beta.txt") && rows[1].contains("Intact"),
"{rows:?}"
);
assert!(
rows[2].contains("gamma.bin") && rows[2].contains("Intact"),
"{rows:?}"
);
assert!(
rows[3].contains("alpha.txt")
&& rows[3].contains("Intact")
&& rows[3].contains("[shadowed: dup of #0]"),
"the extra physical copy must be reported Intact (nothing in it is damaged) AND \
flagged as shadowing scan position 0: {rows:?}"
);
}
#[cfg(unix)]
fn run_zip_ff(zip_bin: &Path, archive: &Path, out: &Path) -> std::process::Output {
let mut child = Command::new(zip_bin)
.args([os(&"-FF"), os(&archive), os(&"--out"), os(&out)])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(b"y\ny\ny\ny\ny\n")
.unwrap();
let pid = child.id();
let finished = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let finished_for_watchdog = finished.clone();
let watchdog = std::thread::spawn(move || {
for _ in 0..200 {
if finished_for_watchdog.load(Ordering::Relaxed) {
return;
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
let _ = Command::new("kill").args(["-9", &pid.to_string()]).status();
});
let output = child.wait_with_output().unwrap();
finished.store(true, Ordering::Relaxed);
watchdog.join().unwrap();
output
}
fn unzip_test_lines(unzip_bin: &Path, archive: &Path) -> Vec<String> {
let out = Command::new(unzip_bin)
.args(["-t", archive.to_str().unwrap()])
.output()
.unwrap();
String::from_utf8_lossy(&out.stdout)
.lines()
.map(str::to_string)
.collect()
}
fn unzip_test_line_for<'a>(lines: &'a [String], name: &str) -> &'a str {
lines
.iter()
.map(String::as_str)
.find(|l| l.contains(name))
.unwrap_or_else(|| panic!("`unzip -t` must still report on `{name}`: {lines:?}"))
}
#[cfg(unix)]
#[test]
fn info_zip_recovers_the_same_entries_we_do() {
let zip_bin = require_bin("zip");
let unzip_bin = require_bin("unzip");
let dir = tmp_dir();
let (archive, _entries) = build_real_healthy_zip(&dir);
let healthy = std::fs::read(&archive).unwrap();
let damaged = dir.join("zeroed_cd_for_witness.zip");
std::fs::write(&damaged, mutate_zeroed_central_directory(&healthy)).unwrap();
let rows = salvage_list_rows(&damaged);
assert_eq!(rows.len(), 3, "{rows:?}");
for (row, name) in rows.iter().zip(["alpha.txt", "beta.txt", "gamma.bin"]) {
assert!(
row.contains(name) && row.contains("Intact"),
"our own recovery must find every entry, Intact: {row}"
);
}
let fixed = dir.join("witness_fixed.zip");
let out = run_zip_ff(&zip_bin, &damaged, &fixed);
assert!(
out.status.success(),
"zip -FF failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let test_out = Command::new(&unzip_bin)
.args(["-t", fixed.to_str().unwrap()])
.output()
.unwrap();
assert!(
test_out.status.success(),
"unzip -t reported a problem with zip -FF's own recovered archive: {}",
String::from_utf8_lossy(&test_out.stdout)
);
let test_text = String::from_utf8_lossy(&test_out.stdout);
for name in ["alpha.txt", "beta.txt", "gamma.bin"] {
let line = test_text
.lines()
.find(|l| l.contains(name))
.unwrap_or_else(|| {
panic!("zip -FF's own recovery must still contain `{name}`: {test_text}")
});
assert!(
line.contains("OK"),
"we call `{name}` Intact; Info-Zip's own independent recovery must agree it \
decodes cleanly: {line}"
);
}
}
#[cfg(unix)]
#[test]
fn info_zip_agrees_on_a_hole_punched_payload() {
let unzip_bin = require_bin("unzip");
let dir = tmp_dir();
let (archive, _entries) = build_real_healthy_zip(&dir);
let healthy = std::fs::read(&archive).unwrap();
let path = dir.join("hole_punched_for_witness.zip");
std::fs::write(&path, mutate_hole_punched_mid_payload(&healthy)).unwrap();
let rows = salvage_list_rows(&path);
assert_eq!(rows.len(), 3, "{rows:?}");
let lines = unzip_test_lines(&unzip_bin, &path);
for (name, intact) in [
("alpha.txt", true),
("beta.txt", false),
("gamma.bin", true),
] {
let our_row = rows
.iter()
.find(|r| r.contains(name))
.unwrap_or_else(|| panic!("our own recovery must still report `{name}`: {rows:?}"));
assert_eq!(
our_row.contains("Intact"),
intact,
"our own verdict for `{name}`: {our_row}"
);
let their_line = unzip_test_line_for(&lines, name);
assert_eq!(
their_line.contains("OK"),
intact,
"Info-Zip's own independent verdict for `{name}` must agree with ours: {their_line}"
);
}
}
#[cfg(unix)]
#[test]
fn info_zip_agrees_on_a_corrupted_crc() {
let unzip_bin = require_bin("unzip");
let dir = tmp_dir();
let (archive, _entries) = build_real_healthy_zip(&dir);
let healthy = std::fs::read(&archive).unwrap();
let path = dir.join("crc_corrupted_for_witness.zip");
std::fs::write(&path, mutate_one_crc_corrupted(&healthy)).unwrap();
let rows = salvage_list_rows(&path);
assert_eq!(rows.len(), 3, "{rows:?}");
let lines = unzip_test_lines(&unzip_bin, &path);
for (name, intact) in [
("alpha.txt", false),
("beta.txt", true),
("gamma.bin", true),
] {
let our_row = rows
.iter()
.find(|r| r.contains(name))
.unwrap_or_else(|| panic!("our own recovery must still report `{name}`: {rows:?}"));
assert_eq!(
our_row.contains("Intact"),
intact,
"our own verdict for `{name}`: {our_row}"
);
let their_line = unzip_test_line_for(&lines, name);
assert_eq!(
their_line.contains("OK"),
intact,
"Info-Zip's own independent verdict for `{name}` must agree with ours: {their_line}"
);
}
}
#[test]
fn a_salvage_entry_that_cannot_be_written_is_named_on_stderr_without_list() {
fn stored_local_record(name: &str, payload: &[u8]) -> Vec<u8> {
let crc = salvage_fixture_crc32(payload);
let len = u32::try_from(payload.len()).unwrap();
let mut bytes = Vec::new();
bytes.extend_from_slice(&[0x50, 0x4b, 0x03, 0x04]);
bytes.extend_from_slice(&20u16.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(&0x21u16.to_le_bytes()); bytes.extend_from_slice(&crc.to_le_bytes());
bytes.extend_from_slice(&len.to_le_bytes());
bytes.extend_from_slice(&len.to_le_bytes());
bytes.extend_from_slice(&u16::try_from(name.len()).unwrap().to_le_bytes());
bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(name.as_bytes());
bytes.extend_from_slice(payload);
bytes
}
let dir = tmp_dir();
let long_name = format!("{}.txt", "L".repeat(400));
let mut zip = stored_local_record("a.txt", b"AAAA");
zip.extend_from_slice(&stored_local_record(&long_name, b"BBBB"));
zip.extend_from_slice(&stored_local_record("z.txt", b"ZZZZ"));
let archive = dir.join("unwritable-name.zip");
std::fs::write(&archive, &zip).unwrap();
let out_dir = dir.join("recovered");
let out = run_output(&[
"salvage",
archive.to_str().unwrap(),
"--format",
"zip",
"-C",
out_dir.to_str().unwrap(),
]);
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
assert_eq!(
out.status.code(),
Some(4),
"one unwritable name is a degraded run, never a failed one (and never exit 1, the \
code reserved for stuffr itself failing, on input the archive chose): {stderr}"
);
assert!(
stderr.contains("could not be written"),
"the run must SAY an entry was not written, on stderr, without --list — the whole \
fold rests on this not being silent: {stderr}"
);
assert!(
stderr.contains("#1"),
"and it must name WHICH entry by scan position, or a user cannot act on it: {stderr}"
);
assert_eq!(std::fs::read(out_dir.join("a.txt")).unwrap(), b"AAAA");
assert_eq!(std::fs::read(out_dir.join("z.txt")).unwrap(), b"ZZZZ");
}
#[test]
fn the_documented_salvage_scanner_list_matches_what_the_binary_actually_scans() {
fn documented(text: &str, label: &str) -> Vec<String> {
let at = text
.find("Scanners exist for:")
.unwrap_or_else(|| panic!("{label} must carry a `Scanners exist for:` sentence"));
let rest = &text[at + "Scanners exist for:".len()..];
let end = rest.find('.').unwrap_or_else(|| {
panic!("{label}'s `Scanners exist for:` sentence must end in a `.`")
});
let mut names: Vec<String> = rest[..end]
.split(',')
.map(|n| n.trim().to_string())
.filter(|n| !n.is_empty())
.collect();
names.sort();
names
}
let tmp = tmp_dir();
let junk = tmp.join("salvage-doc-guard-not-an-archive.bin");
std::fs::write(&junk, b"these bytes are no format's magic at all\n").unwrap();
let usage = run_output(&[
"salvage",
"--format",
"definitely-not-a-format",
junk.to_str().unwrap(),
"--list",
]);
let usage_text = String::from_utf8_lossy(&usage.stderr).to_string();
let recognised: Vec<String> = usage_text
.split_once("salvage recognizes:")
.unwrap_or_else(|| panic!("expected the `salvage recognizes:` usage message: {usage_text}"))
.1
.split(',')
.map(|n| n.trim().trim_end_matches('\n').to_string())
.filter(|n| !n.is_empty())
.collect();
assert!(
recognised.len() >= 8,
"the parse found {recognised:?}; if that is short the scan is broken, not the table"
);
let mut wired: Vec<String> = Vec::new();
for name in &recognised {
let out = run_output(&[
"salvage",
"--format",
name,
junk.to_str().unwrap(),
"--list",
]);
let stderr = String::from_utf8_lossy(&out.stderr);
let unwired = stderr.contains("salvage has no scanner for");
if !unwired {
assert!(
stderr.contains("found nothing recoverable"),
"`--format {name}` over junk answered neither refusal nor an empty scan, so \
this test can no longer tell wired from unwired: {stderr}"
);
wired.push(name.clone());
}
}
wired.sort();
assert!(
!wired.is_empty(),
"no format scanned at all — the discriminator is broken, not the docs"
);
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let examples = std::fs::read_to_string(manifest_dir.join("src").join("examples.txt")).unwrap();
let help = String::from_utf8_lossy(&run_output(&["salvage", "--help"]).stdout).to_string();
assert_eq!(
documented(&examples, "examples.txt"),
wired,
"`examples.txt` names a different set of salvage scanners than this binary has"
);
assert_eq!(
documented(&help, "`salvage --format`'s help"),
wired,
"`--format`'s help names a different set of salvage scanners than this binary has"
);
}
fn legacy_fixture(rel: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("stuffr-formats")
.join("fixtures")
.join("legacy")
.join(rel)
}
fn legacy_truncated_tail_row(
label: &str,
fixture: &str,
drop_bytes: usize,
expected_names: &[&str],
) {
let dir = tmp_dir();
let healthy_path = legacy_fixture(fixture);
let healthy = std::fs::read(&healthy_path).unwrap();
let rows = salvage_list_rows(&healthy_path);
assert_eq!(
rows.len(),
expected_names.len(),
"{label}: sanity, the UNDAMAGED archive's own row count: {rows:?}"
);
for (row, name) in rows.iter().zip(expected_names) {
assert!(
row.contains(name) && row.contains("Intact"),
"{label}: every entry of the undamaged archive must verify Intact: {row}"
);
}
let last_index = expected_names.len() - 1;
let out = run_output(&[
"cat",
"--index",
&last_index.to_string(),
healthy_path.to_str().unwrap(),
]);
assert_eq!(
out.status.code(),
Some(0),
"{label}: the undamaged archive's last entry must read back cleanly: {}",
String::from_utf8_lossy(&out.stderr)
);
let went_in = out.stdout;
assert!(
!went_in.is_empty(),
"{label}: the last entry must carry a payload for this row to mean anything"
);
let cut = healthy.len() - drop_bytes;
let damaged = dir.join(format!("truncated-{}", fixture.replace('/', "-")));
std::fs::write(&damaged, &healthy[..cut]).unwrap();
let rows = salvage_list_rows(&damaged);
assert_eq!(
rows.len(),
expected_names.len(),
"{label}: the cut entry's header is still there and its being uncompletable is a \
fact worth a row, never silence: {rows:?}"
);
for (i, (row, name)) in rows.iter().zip(expected_names).enumerate() {
assert!(row.contains(name), "{label}: {row}");
if i == last_index {
assert!(
row.contains("Partial (truncated)"),
"{label}: the cut entry must be named as truncated: {row}"
);
} else {
assert!(
row.contains("Intact"),
"{label}: an entry before the cut is untouched: {row}"
);
}
}
let out_dir = dir.join(format!("recovered-{}", fixture.replace('/', "-")));
let out = run_output(&[
"salvage",
damaged.to_str().unwrap(),
"-C",
out_dir.to_str().unwrap(),
]);
assert_eq!(
out.status.code(),
Some(4),
"{label}: a run that could not complete something must not exit 0: {}",
String::from_utf8_lossy(&out.stderr)
);
let partial = out_dir.join(format!("{}.partial", expected_names[last_index]));
assert!(
partial.is_file(),
"{label}: expected {} on disk",
partial.display()
);
assert!(
!out_dir.join(expected_names[last_index]).exists(),
"{label}: an incomplete entry must never land under its real name"
);
for name in &expected_names[..last_index] {
let recovered = std::fs::read(out_dir.join(name)).unwrap_or_else(|e| {
panic!("{label}: an entry before the cut must be recovered whole: {e}")
});
assert!(!recovered.is_empty(), "{label}: `{name}` came back empty");
}
let recovered = std::fs::read(&partial).unwrap();
assert!(
!recovered.is_empty() && recovered.len() < went_in.len(),
"{label}: {} recovered bytes against {} that went in",
recovered.len(),
went_in.len()
);
assert_eq!(
recovered[..],
went_in[..recovered.len()],
"{label}: every recovered byte must be one the archive itself still held"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn damage_catalogue_arc_truncated_tail() {
legacy_truncated_tail_row("arc", "arc/cpm.arc", 40, &["DDTZ.COM", "READ.COM"]);
}
#[test]
fn damage_catalogue_arc_truncated_tail_over_a_stored_entry() {
legacy_truncated_tail_row("arc/stored", "arc/store.arc", 5_000, &["LICENSE"]);
}
#[test]
fn damage_catalogue_zoo_truncated_tail() {
legacy_truncated_tail_row("zoo", "zoo/store.zoo", 5_000, &["license"]);
}
#[test]
fn damage_catalogue_lha_truncated_tail() {
legacy_truncated_tail_row(
"lha",
"sample.lzh",
4,
&["sample/hello.txt", "sample/sub/b.bin"],
);
}
#[test]
fn damage_catalogue_arj_truncated_tail() {
legacy_truncated_tail_row(
"arj",
"sample.arj",
7,
&["sample/hello.txt", "sample/sub/b.bin"],
);
}
#[test]
fn lhasa_agrees_with_the_lha_damage_catalogue() {
let lha_bin = require_lhasa();
let banner = lhasa_banner(&lha_bin);
let dir = tmp_dir();
std::fs::create_dir_all(dir.join("tree/sub")).unwrap();
let alpha = b"alpha payload, the first entry, repeated often.\n".repeat(120);
let beta = b"beta payload, the middle entry, repeated often.\n".repeat(90);
let gamma = b"gamma payload, the last entry, repeated often.\n".repeat(70);
std::fs::write(dir.join("tree/alpha.txt"), &alpha).unwrap();
std::fs::write(dir.join("tree/beta.txt"), &beta).unwrap();
std::fs::write(dir.join("tree/sub/gamma.bin"), &gamma).unwrap();
let archive = dir.join("healthy.lzh");
let packed = run_output(&[
"pack",
dir.join("tree").to_str().unwrap(),
"--format",
"lha",
"-o",
archive.to_str().unwrap(),
]);
assert_eq!(packed.status.code(), Some(0));
fn lhasa_verdicts(output: &[u8]) -> Vec<(String, bool)> {
let text = String::from_utf8_lossy(output).replace('\r', "\n");
let mut out: Vec<(String, bool)> = Vec::new();
for chunk in text.split('\n') {
for (marker, ok) in [("\t- Tested", true), ("\t- CRC error", false)] {
if let Some(at) = chunk.find(marker) {
let name = chunk[..at].rsplit(':').next().unwrap().trim().to_string();
if !name.is_empty() && !out.iter().any(|(n, _)| *n == name) {
out.push((name, ok));
}
}
}
}
out
}
let healthy_test = Command::new(&lha_bin)
.arg("t")
.arg(&archive)
.output()
.unwrap();
assert!(
healthy_test.status.success(),
"{banner} must accept the undamaged archive: {}{}",
String::from_utf8_lossy(&healthy_test.stdout),
String::from_utf8_lossy(&healthy_test.stderr)
);
let mut combined = healthy_test.stdout.clone();
combined.extend_from_slice(&healthy_test.stderr);
let before = lhasa_verdicts(&combined);
assert_eq!(
before
.iter()
.map(|(n, _)| n.as_str())
.collect::<Vec<_>>()
.join(","),
"tree/alpha.txt,tree/beta.txt,tree/sub/gamma.bin",
"sanity: {banner} must report on all three entries — otherwise the comparison below \
is against a partial account"
);
assert!(
before.iter().all(|(_, ok)| *ok),
"{banner} must call every entry of the undamaged archive good: {before:?}"
);
let healthy = std::fs::read(&archive).unwrap();
let beta_header = healthy
.windows(5)
.enumerate()
.filter(|(_, w)| *w == b"-lh5-")
.map(|(i, _)| i)
.nth(1)
.expect("the middle entry's own method identifier");
let mut bytes = healthy.clone();
let beta_start = beta_header - 2;
let payload_at = beta_start + bytes[beta_start] as usize + 2 + 4;
bytes[payload_at] ^= 0xFF;
let damaged = dir.join("damaged.lzh");
std::fs::write(&damaged, &bytes).unwrap();
let damaged_test = Command::new(&lha_bin)
.arg("t")
.arg(&damaged)
.output()
.unwrap();
assert!(
!damaged_test.status.success(),
"{banner} must refuse the damaged archive, or this test proved nothing"
);
let mut combined = damaged_test.stdout.clone();
combined.extend_from_slice(&damaged_test.stderr);
let after = lhasa_verdicts(&combined);
let condemned: Vec<&str> = after
.iter()
.filter(|(_, ok)| !*ok)
.map(|(n, _)| n.as_str())
.collect();
assert_eq!(
condemned,
["tree/beta.txt"],
"{banner} must condemn exactly the entry this test damaged: {after:?}"
);
let rows = salvage_list_rows(&damaged);
assert_eq!(
rows.len(),
5,
"three files and two directory entries: {rows:?}"
);
for (name, ok) in &after {
let row = rows
.iter()
.find(|r| r.contains(name.as_str()))
.unwrap_or_else(|| {
panic!("salvage lost `{name}` that {banner} still reports: {rows:?}")
});
if *ok {
assert!(
row.contains("Intact"),
"{banner} calls `{name}` good and salvage does not: {row}"
);
} else {
assert!(
row.contains("Partial"),
"{banner} calls `{name}` bad and salvage calls it whole: {row}"
);
}
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn no_dedicated_arj_tool_is_obtainable_to_witness_this_catalogue() {
for candidate in ["arj", "unarj", "arj32"] {
assert!(
which(candidate).is_none(),
"`{candidate}` is on PATH now. ARJ's catalogue has been checked against nothing \
but this project's own reading of the spec since Phase 3b — promote \
`damage_catalogue_arj_truncated_tail` and \
`legacy::arj_salvage`'s own `mod damage_catalogue` to use this tool as a real \
external witness (the shape `lhasa_agrees_with_the_lha_damage_catalogue` \
above already has), and delete this test."
);
}
let sample = std::fs::read(legacy_fixture("sample.arj")).unwrap();
assert_eq!(
sample.len(),
173,
"`sample.arj` is no longer the 173-byte fixture MANIFEST.md describes; re-read its \
provenance before trusting anything this catalogue asserts"
);
}
#[test]
fn damage_catalogue_the_three_partial_causes_are_distinguishable() {
let dir = tmp_dir();
let stored = std::fs::read(legacy_fixture("zoo/store.zoo")).unwrap();
let lzd = std::fs::read(legacy_fixture("zoo/default.zoo")).unwrap();
let payload_at = |bytes: &[u8]| -> usize {
u32::from_le_bytes(bytes[42 + 10..42 + 14].try_into().unwrap()) as usize
};
let declared_len = |bytes: &[u8]| -> usize {
u32::from_le_bytes(bytes[42 + 24..42 + 28].try_into().unwrap()) as usize
};
let cut = dir.join("cut.zoo");
std::fs::write(&cut, &stored[..payload_at(&stored) + 100]).unwrap();
let mut bytes = lzd.clone();
let at = payload_at(&lzd) + declared_len(&lzd) / 2;
bytes[at] ^= 0xFF;
let damaged_lzd = dir.join("lzd.zoo");
std::fs::write(&damaged_lzd, &bytes).unwrap();
let mut bytes = stored.clone();
let at = payload_at(&stored) + declared_len(&stored) / 2;
bytes[at] ^= 0xFF;
let damaged_stored = dir.join("stored.zoo");
std::fs::write(&damaged_stored, &bytes).unwrap();
for (path, cause, why) in [
(
&cut,
"Partial (truncated)",
"bytes this entry declares are genuinely missing from the file",
),
(
&damaged_lzd,
"Partial (decode failed)",
"every declared byte is present and the decoder could not turn them into the \
declared content — saying `truncated` here sent a user looking for a longer \
copy of a file that is not short",
),
(
&damaged_stored,
"Partial (checksum mismatch)",
"every declared byte is present, it all decoded, and it is not what was written",
),
] {
let rows = salvage_list_rows(path);
assert_eq!(rows.len(), 1, "{}: {rows:?}", path.display());
assert!(
rows[0].contains(cause),
"{}: expected `{cause}` — {why}: {}",
path.display(),
rows[0]
);
for other in [
"Partial (truncated)",
"Partial (decode failed)",
"Partial (checksum mismatch)",
] {
if other != cause {
assert!(
!rows[0].contains(other),
"{}: row claims `{other}` as well as `{cause}`: {}",
path.display(),
rows[0]
);
}
}
}
for (label, bytes) in [("store.zoo", &stored), ("default.zoo", &lzd)] {
let whole = dir.join(format!("whole-{label}"));
std::fs::write(&whole, bytes).unwrap();
let rows = salvage_list_rows(&whole);
assert_eq!(rows.len(), 1);
assert!(
rows[0].contains("Intact"),
"{label} must be healthy before anything is done to it: {}",
rows[0]
);
}
let _ = std::fs::remove_dir_all(&dir);
}