use crate::common::*;
use std::process::Command;
#[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();
}