use crate::common::*;
use std::process::Command;
#[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("but 1 were enumerated"),
"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)
);
}