use std::process::Command;
fn which(tool: &str) -> Option<std::path::PathBuf> {
let out = Command::new("sh")
.arg("-c")
.arg(format!("command -v {tool}"))
.output()
.ok()?;
if !out.status.success() {
return None;
}
let s = String::from_utf8(out.stdout).ok()?;
let p = s.trim();
if p.is_empty() { None } else { Some(p.into()) }
}
fn fstool() -> std::path::PathBuf {
let mut p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
p.push("target");
p.push(if cfg!(debug_assertions) {
"debug"
} else {
"release"
});
p.push("fstool");
p
}
fn ensure_built() {
if fstool().exists() {
return;
}
let status = Command::new("cargo")
.args(["build", "--bin", "fstool"])
.current_dir(env!("CARGO_MANIFEST_DIR"))
.status()
.expect("spawn cargo build");
assert!(status.success(), "cargo build fstool failed");
}
fn make_tar(dir: &std::path::Path, members: &[(&str, &[u8])]) -> std::path::PathBuf {
let stage = tempfile::tempdir().unwrap();
for (name, body) in members {
let p = stage.path().join(name);
if let Some(parent) = p.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(&p, body).unwrap();
}
let tar_path = dir.join(format!(
"layer-{}.tar",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let args: Vec<String> = members
.iter()
.map(|(name, _)| name.to_string())
.collect();
let mut cmd = Command::new("tar");
cmd.arg("cf")
.arg(&tar_path)
.current_dir(stage.path())
.args(&args);
let status = cmd.status().expect("spawn tar");
assert!(status.success(), "tar cf failed");
tar_path
}
fn list_tar(path: &std::path::Path) -> Vec<String> {
let out = Command::new("tar")
.arg("tf")
.arg(path)
.output()
.expect("spawn tar tf");
assert!(out.status.success(), "tar tf failed");
String::from_utf8(out.stdout)
.unwrap()
.lines()
.map(|s| {
s.trim_end_matches('/')
.trim_start_matches('/')
.to_string()
})
.collect()
}
fn extract_tar_file(path: &std::path::Path, member: &str) -> Vec<u8> {
let candidate = format!("/{member}");
let out = Command::new("tar")
.arg("xOf")
.arg(path)
.arg(&candidate)
.output()
.expect("spawn tar xOf");
if out.status.success() && !out.stdout.is_empty() {
return out.stdout;
}
let out = Command::new("tar")
.arg("xOf")
.arg(path)
.arg(member)
.output()
.expect("spawn tar xOf");
assert!(out.status.success(), "tar xOf {member} failed");
out.stdout
}
#[test]
fn merge_two_tars_override_and_whiteout() {
if which("tar").is_none() {
eprintln!("skipping: tar not installed");
return;
}
ensure_built();
let work = tempfile::tempdir().unwrap();
let base = make_tar(
work.path(),
&[
("etc/conf", b"v1"),
("etc/keep", b"k"),
("etc/sub/x", b"x1"),
],
);
let top = make_tar(
work.path(),
&[
("etc/conf", b"v2"),
("etc/.wh.keep", b""),
("etc/new", b"n"),
],
);
let out_tar = work.path().join("merged.tar");
let status = Command::new(fstool())
.arg("repack")
.arg(&base)
.arg(&top)
.arg(&out_tar)
.status()
.expect("spawn fstool repack");
assert!(status.success(), "fstool repack failed");
let members = list_tar(&out_tar);
assert!(members.iter().any(|m| m == "etc/conf"), "etc/conf present");
assert!(members.iter().any(|m| m == "etc/new"), "etc/new present");
assert!(members.iter().any(|m| m == "etc/sub/x"), "etc/sub/x kept");
assert!(
!members.iter().any(|m| m == "etc/keep"),
"etc/keep should be deleted by .wh.keep"
);
assert!(
!members.iter().any(|m| m.contains(".wh.")),
"no .wh.* tombstones leak through"
);
let conf = extract_tar_file(&out_tar, "etc/conf");
assert_eq!(conf, b"v2", "etc/conf must come from top layer");
}
#[test]
fn merge_opaque_dir_drops_lower_children() {
if which("tar").is_none() {
eprintln!("skipping: tar not installed");
return;
}
ensure_built();
let work = tempfile::tempdir().unwrap();
let base = make_tar(work.path(), &[("etc/a", b"A"), ("etc/b", b"B")]);
let top = make_tar(
work.path(),
&[("etc/.wh..wh..opq", b""), ("etc/c", b"C")],
);
let out_tar = work.path().join("merged.tar");
let status = Command::new(fstool())
.arg("repack")
.arg(&base)
.arg(&top)
.arg(&out_tar)
.status()
.expect("spawn fstool repack");
assert!(status.success(), "fstool repack failed");
let members = list_tar(&out_tar);
assert!(members.iter().any(|m| m == "etc/c"), "etc/c kept");
assert!(
!members.iter().any(|m| m == "etc/a"),
"etc/a wiped by opaque marker"
);
assert!(
!members.iter().any(|m| m == "etc/b"),
"etc/b wiped by opaque marker"
);
assert!(
!members.iter().any(|m| m.contains(".wh.")),
"no .wh.* tombstones leak through"
);
}