#![cfg(unix)]
#[cfg(unix)]
use std::process::Command;
#[cfg(unix)]
use fstool::block::{BlockDevice, FileBackend};
#[cfg(unix)]
use fstool::fs::xfs::{self, DeviceKind, EntryMeta, FormatOpts, Xfs};
#[cfg(unix)]
use tempfile::NamedTempFile;
#[cfg(unix)]
fn which(tool: &str) -> Option<std::path::PathBuf> {
if Command::new(tool).arg("-V").output().is_ok() {
return Some(tool.into());
}
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()) }
}
#[cfg(unix)]
fn populate_sampler(xfs: &mut Xfs, dev: &mut dyn BlockDevice) {
let rootino = xfs.superblock().rootino;
let body = b"hello xfs\n";
let mut src = std::io::Cursor::new(body.to_vec());
let file_ino = xfs
.add_file(
dev,
rootino,
"greet",
EntryMeta::default(),
body.len() as u64,
&mut src,
)
.unwrap();
let sub = xfs
.add_dir(dev, rootino, "sub", EntryMeta::default())
.unwrap();
let nested = b"nested\n";
let mut src2 = std::io::Cursor::new(nested.to_vec());
xfs.add_file(
dev,
sub,
"leaf",
EntryMeta::default(),
nested.len() as u64,
&mut src2,
)
.unwrap();
xfs.add_symlink(dev, rootino, "lnk", "/etc/hostname", EntryMeta::default())
.unwrap();
xfs.add_device(
dev,
rootino,
"null",
DeviceKind::Char,
1,
3,
EntryMeta {
mode: 0o666,
..EntryMeta::default()
},
)
.unwrap();
xfs.add_xattr(dev, file_ino, "user.mime_type", b"text/plain")
.unwrap();
xfs.add_xattr(dev, file_ino, "trusted.tag", b"v1").unwrap();
}
#[cfg(unix)]
fn assert_xfs_repair_clean(path: &std::path::Path) {
let out = Command::new("xfs_repair")
.args(["-n", "-o", "force_geometry"])
.arg(path)
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
let combined = format!("{stdout}{stderr}");
let code = out.status.code();
assert_ne!(
code,
Some(2),
"xfs_repair reports dirty log (exit 2):\n{combined}"
);
assert!(
combined.contains("No modify flag set"),
"xfs_repair did not complete through phase 7 \
(missing 'No modify flag set' marker, exit={code:?}):\n{combined}"
);
if !out.status.success() {
eprintln!(
"xfs_repair completed with non-zero exit {code:?} but finished \
phase 7 cleanly; surfaced findings:\n{combined}"
);
}
}
#[test]
fn xfs_writer_passes_xfs_repair_single_ag() {
let Some(_) = which("xfs_repair") else {
eprintln!("skipping: xfs_repair not installed");
return;
};
let size: u64 = 64 * 1024 * 1024;
let tmp = NamedTempFile::new().unwrap();
let mut dev = FileBackend::create(tmp.path(), size).unwrap();
let opts = FormatOpts {
uuid: [0x42u8; 16],
..Default::default()
};
{
let mut x = xfs::format(&mut dev, &opts).unwrap();
x.begin_writes(opts.uuid);
assert_eq!(
x.ag_count(),
1,
"expected single-AG layout for {} MiB image",
size / (1024 * 1024)
);
populate_sampler(&mut x, &mut dev);
x.flush_writes(&mut dev).unwrap();
}
dev.sync().unwrap();
drop(dev);
assert_xfs_repair_clean(tmp.path());
}
#[test]
fn xfs_writer_passes_xfs_repair_multi_ag() {
let Some(_) = which("xfs_repair") else {
eprintln!("skipping: xfs_repair not installed");
return;
};
let size: u64 = 768 * 1024 * 1024;
let tmp = NamedTempFile::new().unwrap();
let mut dev = FileBackend::create(tmp.path(), size).unwrap();
let opts = FormatOpts {
uuid: [0x7eu8; 16],
..Default::default()
};
{
let mut x = xfs::format(&mut dev, &opts).unwrap();
x.begin_writes(opts.uuid);
assert!(
x.ag_count() >= 2,
"expected multi-AG layout for {} MiB image, got {} AGs",
size / (1024 * 1024),
x.ag_count()
);
populate_sampler(&mut x, &mut dev);
x.flush_writes(&mut dev).unwrap();
}
dev.sync().unwrap();
drop(dev);
assert_xfs_repair_clean(tmp.path());
}
#[test]
fn xfs_db_dumps_primary_superblock() {
let Some(_) = which("xfs_db") else {
eprintln!("skipping: xfs_db not installed");
return;
};
let size: u64 = 64 * 1024 * 1024;
let tmp = NamedTempFile::new().unwrap();
let mut dev = FileBackend::create(tmp.path(), size).unwrap();
let opts = FormatOpts {
uuid: [0xa5u8; 16],
..Default::default()
};
{
let mut x = xfs::format(&mut dev, &opts).unwrap();
x.begin_writes(opts.uuid);
let mut src = std::io::Cursor::new(b"db".to_vec());
x.add_file(
&mut dev,
x.superblock().rootino,
"f",
EntryMeta::default(),
2,
&mut src,
)
.unwrap();
x.flush_writes(&mut dev).unwrap();
}
dev.sync().unwrap();
drop(dev);
let out = Command::new("xfs_db")
.args(["-r", "-c", "sb 0", "-c", "print"])
.arg(tmp.path())
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
out.status.success(),
"xfs_db failed:\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
for key in ["magicnum", "blocksize", "agcount", "uuid"] {
assert!(
stdout.contains(key),
"xfs_db output missing field {key:?}:\n{stdout}"
);
}
}
#[test]
fn mkfs_xfs_image_is_readable_by_fstool() {
let Some(_) = which("mkfs.xfs") else {
eprintln!("skipping: mkfs.xfs not installed");
return;
};
let path = std::env::temp_dir().join(format!("fstool-xfs-mkfs-{}.img", std::process::id()));
let _ = std::fs::remove_file(&path);
let f = std::fs::File::create(&path).unwrap();
f.set_len(512 * 1024 * 1024).unwrap();
drop(f);
let out = Command::new("mkfs.xfs")
.args(["-f", "-m", "crc=1"])
.arg(&path)
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
out.status.success(),
"mkfs.xfs failed:\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let mut dev = FileBackend::open(&path).unwrap();
let xfs_h = Xfs::open(&mut dev).expect("Xfs::open should accept a default mkfs.xfs image");
let entries = xfs_h
.list_path(&mut dev, "/")
.expect("list_path('/') on mkfs.xfs image should succeed");
let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
assert!(
!names.iter().any(|n| *n == "." || *n == ".."),
"shortform root should not surface . / ..: {names:?}"
);
assert_eq!(xfs_h.block_size(), 4096);
assert!(xfs_h.ag_count() >= 1);
drop(dev);
let _ = std::fs::remove_file(&path);
}