use std::process::Command;
const FSTOOL: &str = env!("CARGO_BIN_EXE_fstool");
fn which(tool: &str) -> bool {
Command::new("sh")
.arg("-c")
.arg(format!("command -v {tool}"))
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn run(args: &[&str]) -> (bool, String) {
let out = Command::new(FSTOOL)
.args(args)
.output()
.expect("spawn fstool");
(
out.status.success(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
fn stage(root: &std::path::Path) {
std::fs::create_dir_all(root.join("sub")).unwrap();
std::fs::write(root.join("top.txt"), b"top\n").unwrap();
std::fs::write(root.join("sub/deep.txt"), b"deep contents\n").unwrap();
}
#[test]
fn zip_source_repacks_to_tar() {
if !which("tar") {
eprintln!("skipping: tar not installed");
return;
}
let work = tempfile::tempdir().unwrap();
let src = work.path().join("src");
stage(&src);
let zip = work.path().join("a.zip");
assert!(
run(&[
"create",
"-t",
"zip",
src.to_str().unwrap(),
"-o",
zip.to_str().unwrap()
])
.0
);
let tar = work.path().join("out.tar");
let (ok, err) = run(&["repack", zip.to_str().unwrap(), tar.to_str().unwrap()]);
assert!(ok, "zip → tar repack failed: {err}");
let listing = Command::new("tar").arg("tf").arg(&tar).output().unwrap();
let members = String::from_utf8_lossy(&listing.stdout);
assert!(
members.contains("sub/deep.txt"),
"missing nested file:\n{members}"
);
let body = Command::new("tar")
.arg("xOf")
.arg(&tar)
.arg("sub/deep.txt")
.output()
.unwrap();
assert_eq!(body.stdout, b"deep contents\n");
}
#[test]
fn zip_source_repacks_to_ext4() {
let work = tempfile::tempdir().unwrap();
let src = work.path().join("src");
stage(&src);
let zip = work.path().join("a.zip");
assert!(
run(&[
"create",
"-t",
"zip",
src.to_str().unwrap(),
"-o",
zip.to_str().unwrap()
])
.0
);
let img = work.path().join("out.img");
let (ok, err) = run(&[
"repack",
zip.to_str().unwrap(),
img.to_str().unwrap(),
"--fs-type",
"ext4",
"--shrink",
]);
assert!(ok, "zip → ext4 repack failed: {err}");
let out = Command::new(FSTOOL)
.args(["cat", img.to_str().unwrap(), "/sub/deep.txt"])
.output()
.unwrap();
assert_eq!(out.stdout, b"deep contents\n");
}
#[test]
fn create_deferred_write_backends_from_dir() {
let work = tempfile::tempdir().unwrap();
let src = work.path().join("src");
stage(&src);
for fs in ["squashfs", "iso", "grf"] {
let out = work.path().join(format!("o.{fs}"));
let (ok, err) = run(&[
"create",
"-t",
fs,
src.to_str().unwrap(),
"-o",
out.to_str().unwrap(),
]);
assert!(ok, "create -t {fs} failed: {err}");
let cat = Command::new(FSTOOL)
.args(["cat", out.to_str().unwrap(), "/sub/deep.txt"])
.output()
.unwrap();
assert_eq!(
cat.stdout, b"deep contents\n",
"{fs}: body wrong after create"
);
}
}
#[test]
#[cfg(unix)]
fn squashfs_source_preserves_mode_into_tar() {
if !which("tar") {
eprintln!("skipping: tar not installed");
return;
}
use std::os::unix::fs::PermissionsExt;
let work = tempfile::tempdir().unwrap();
let src = work.path().join("src");
std::fs::create_dir_all(&src).unwrap();
let f = src.join("s.txt");
std::fs::write(&f, b"x\n").unwrap();
std::fs::set_permissions(&f, std::fs::Permissions::from_mode(0o640)).unwrap();
let img = work.path().join("fs.sqsh");
assert!(
run(&[
"create",
"-t",
"squashfs",
src.to_str().unwrap(),
"-o",
img.to_str().unwrap()
])
.0
);
let tar = work.path().join("out.tar");
assert!(run(&["repack", img.to_str().unwrap(), tar.to_str().unwrap()]).0);
let listing = Command::new("tar").arg("tvf").arg(&tar).output().unwrap();
let s = String::from_utf8_lossy(&listing.stdout);
let line = s.lines().find(|l| l.contains("s.txt")).unwrap_or("");
assert!(
line.contains("rw-r-----"),
"squashfs mode not preserved:\n{line}"
);
}
#[test]
#[cfg(unix)]
fn iso_rock_ridge_source_preserves_mode_into_tar() {
use std::os::unix::fs::PermissionsExt;
let tool = ["genisoimage", "mkisofs", "xorrisofs"]
.into_iter()
.find(|t| which(t));
let (Some(tool), true) = (tool, which("tar")) else {
eprintln!("skipping: no iso builder / tar");
return;
};
let work = tempfile::tempdir().unwrap();
let src = work.path().join("src");
std::fs::create_dir_all(&src).unwrap();
let f = src.join("f.txt");
std::fs::write(&f, b"x\n").unwrap();
std::fs::set_permissions(&f, std::fs::Permissions::from_mode(0o640)).unwrap();
let iso = work.path().join("rr.iso");
let ok = Command::new(tool)
.args(["-R", "-o"])
.arg(&iso)
.arg(&src)
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if !ok {
eprintln!("skipping: {tool} failed");
return;
}
let tar = work.path().join("out.tar");
assert!(run(&["repack", iso.to_str().unwrap(), tar.to_str().unwrap()]).0);
let listing = Command::new("tar").arg("tvf").arg(&tar).output().unwrap();
let s = String::from_utf8_lossy(&listing.stdout);
let line = s.lines().find(|l| l.contains("f.txt")).unwrap_or("");
assert!(
line.contains("rw-r-----"),
"iso RR mode not preserved:\n{line}"
);
}
#[test]
#[cfg(unix)]
fn ntfs_source_surfaces_times_mode_and_xattrs() {
if !which("tar") {
eprintln!("skipping: tar not installed");
return;
}
use std::os::unix::fs::PermissionsExt;
let work = tempfile::tempdir().unwrap();
let src = work.path().join("src");
std::fs::create_dir_all(&src).unwrap();
let f = src.join("n.txt");
std::fs::write(&f, b"payload\n").unwrap();
std::fs::set_permissions(&f, std::fs::Permissions::from_mode(0o444)).unwrap();
let img = work.path().join("fs.img");
assert!(
run(&[
"create",
"-t",
"ntfs",
src.to_str().unwrap(),
"-o",
img.to_str().unwrap()
])
.0
);
let tar = work.path().join("out.tar");
assert!(run(&["repack", img.to_str().unwrap(), tar.to_str().unwrap()]).0);
let body = Command::new("tar")
.arg("xOf")
.arg(&tar)
.arg("n.txt")
.output()
.unwrap();
assert_eq!(body.stdout, b"payload\n", "ntfs body wrong");
let listing = Command::new("tar").arg("tvf").arg(&tar).output().unwrap();
let s = String::from_utf8_lossy(&listing.stdout);
let line = s.lines().find(|l| l.contains("n.txt")).unwrap_or("");
assert!(
line.contains("r--r--r--"),
"ntfs read-only mode not synthesised:\n{line}"
);
assert!(
!line.contains("1970"),
"ntfs timestamp not surfaced (still epoch):\n{line}"
);
}
#[test]
#[cfg(unix)]
fn hfs_plus_source_preserves_mode_into_tar() {
if !which("tar") {
eprintln!("skipping: tar not installed");
return;
}
use std::os::unix::fs::PermissionsExt;
let work = tempfile::tempdir().unwrap();
let src = work.path().join("src");
std::fs::create_dir_all(&src).unwrap();
let f = src.join("h.txt");
std::fs::write(&f, b"x\n").unwrap();
std::fs::set_permissions(&f, std::fs::Permissions::from_mode(0o640)).unwrap();
let img = work.path().join("fs.img");
assert!(
run(&[
"create",
"-t",
"hfs+",
src.to_str().unwrap(),
"-o",
img.to_str().unwrap()
])
.0
);
let tar = work.path().join("out.tar");
assert!(run(&["repack", img.to_str().unwrap(), tar.to_str().unwrap()]).0);
let listing = Command::new("tar").arg("tvf").arg(&tar).output().unwrap();
let s = String::from_utf8_lossy(&listing.stdout);
let line = s.lines().find(|l| l.contains("h.txt")).unwrap_or("");
assert!(
line.contains("rw-r-----"),
"hfs+ mode not preserved:\n{line}"
);
}
#[test]
#[cfg(unix)]
fn apfs_source_preserves_mode_into_tar() {
if !which("tar") {
eprintln!("skipping: tar not installed");
return;
}
use std::os::unix::fs::PermissionsExt;
let work = tempfile::tempdir().unwrap();
let src = work.path().join("src");
std::fs::create_dir_all(&src).unwrap();
let f = src.join("a.txt");
std::fs::write(&f, b"x\n").unwrap();
std::fs::set_permissions(&f, std::fs::Permissions::from_mode(0o642)).unwrap();
let img = work.path().join("fs.img");
assert!(
run(&[
"create",
"-t",
"apfs",
src.to_str().unwrap(),
"-o",
img.to_str().unwrap()
])
.0
);
let tar = work.path().join("out.tar");
assert!(run(&["repack", img.to_str().unwrap(), tar.to_str().unwrap()]).0);
let listing = Command::new("tar").arg("tvf").arg(&tar).output().unwrap();
let s = String::from_utf8_lossy(&listing.stdout);
let line = s.lines().find(|l| l.contains("a.txt")).unwrap_or("");
assert!(
line.contains("rw-r---w-"),
"apfs mode not preserved:\n{line}"
);
}
#[test]
#[cfg(unix)]
fn f2fs_source_preserves_mode_into_tar() {
if !which("tar") {
eprintln!("skipping: tar not installed");
return;
}
use std::os::unix::fs::PermissionsExt;
let work = tempfile::tempdir().unwrap();
let src = work.path().join("src");
std::fs::create_dir_all(&src).unwrap();
let f = src.join("secret.txt");
std::fs::write(&f, b"x\n").unwrap();
std::fs::set_permissions(&f, std::fs::Permissions::from_mode(0o640)).unwrap();
let img = work.path().join("fs.img");
assert!(
run(&[
"create",
"-t",
"f2fs",
src.to_str().unwrap(),
"-o",
img.to_str().unwrap()
])
.0
);
let tar = work.path().join("out.tar");
let (ok, err) = run(&["repack", img.to_str().unwrap(), tar.to_str().unwrap()]);
assert!(ok, "f2fs → tar repack failed: {err}");
let listing = Command::new("tar").arg("tvf").arg(&tar).output().unwrap();
let s = String::from_utf8_lossy(&listing.stdout);
let line = s
.lines()
.find(|l| l.contains("secret.txt"))
.unwrap_or_else(|| panic!("secret.txt missing from tar:\n{s}"));
assert!(
line.contains("rw-r-----"),
"f2fs mode not preserved (expected rw-r-----):\n{line}"
);
}
#[test]
#[cfg(unix)]
fn ext_hardlinks_materialise_into_tar() {
if !which("mke2fs") || !which("tar") {
eprintln!("skipping: mke2fs/tar not installed");
return;
}
let work = tempfile::tempdir().unwrap();
let src = work.path().join("src");
std::fs::create_dir_all(&src).unwrap();
std::fs::write(src.join("a"), b"shared\n").unwrap();
std::fs::hard_link(src.join("a"), src.join("b")).unwrap();
let img = work.path().join("ext.img");
let mk = Command::new("mke2fs")
.args(["-q", "-F", "-t", "ext4", "-d"])
.arg(&src)
.arg(&img)
.arg("4M")
.output()
.unwrap();
if !mk.status.success() {
eprintln!("skipping: mke2fs failed");
return;
}
let tar = work.path().join("out.tar");
let (ok, err) = run(&["repack", img.to_str().unwrap(), tar.to_str().unwrap()]);
assert!(ok, "ext → tar repack failed: {err}");
for name in ["a", "b"] {
let body = Command::new("tar")
.arg("xOf")
.arg(&tar)
.arg(name)
.output()
.unwrap();
assert_eq!(body.stdout, b"shared\n", "hardlink {name} content wrong");
}
}