#![cfg(unix)]
use std::io::Read;
use std::path::Path;
use std::process::Command;
use fstool::block::{BlockDevice, FileBackend};
use fstool::fs::exfat::Exfat;
use fstool::fs::exfat::format::FormatOpts;
use tempfile::NamedTempFile;
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 tool_present(tool: &str) -> bool {
if which(tool).is_none() {
return false;
}
let _ = Command::new(tool).arg("--version").output();
true
}
fn format_volume(path: &Path, mib: u32, label: &str) -> Exfat {
let bytes = mib as u64 * 1024 * 1024;
let mut dev = FileBackend::create(path, bytes).expect("create image");
let opts = FormatOpts {
bytes_per_sector_shift: 9, sectors_per_cluster_shift: 3, volume_serial_number: 0xCAFE_F00D,
volume_label: label.to_string(),
};
let fs = Exfat::format(&mut dev, &opts).expect("format exfat");
dev.sync().expect("sync");
drop(dev);
fs
}
#[test]
fn writer_image_passes_fsck_exfat() {
if !tool_present("fsck.exfat") {
eprintln!("skipping: fsck.exfat not installed");
return;
}
let tmp = NamedTempFile::new().unwrap();
let _ = format_volume(tmp.path(), 64, "FSTOOLEXF");
{
let mut dev = FileBackend::open(tmp.path()).unwrap();
let mut fs = Exfat::open(&mut dev).unwrap();
let p1: &[u8] = b"hello, exfat external\n";
let mut r1: &[u8] = p1;
fs.create_file(&mut dev, "/hello.txt", &mut r1, p1.len() as u64, 0)
.unwrap();
fs.create_dir(&mut dev, "/docs", 0).unwrap();
let p2: &[u8] = b"# Long Name File\nNested under /docs.\n";
let mut r2: &[u8] = p2;
fs.create_file(
&mut dev,
"/docs/A Long Readme.md",
&mut r2,
p2.len() as u64,
0,
)
.unwrap();
fs.create_dir(&mut dev, "/docs/nested", 0).unwrap();
let p3: &[u8] = b"deeply nested body\n";
let mut r3: &[u8] = p3;
fs.create_file(
&mut dev,
"/docs/nested/inside.bin",
&mut r3,
p3.len() as u64,
0,
)
.unwrap();
let p4: &[u8] = b"konnichiwa\n";
let mut r4: &[u8] = p4;
fs.create_file(
&mut dev,
"/\u{3053}\u{3093}\u{306B}\u{3061}\u{306F}.txt",
&mut r4,
p4.len() as u64,
0,
)
.unwrap();
fs.flush(&mut dev).unwrap();
dev.sync().unwrap();
}
let out = Command::new("fsck.exfat")
.args(["-n", "-v"])
.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(),
"fsck.exfat failed (exit {:?}):\nstdout:\n{stdout}\nstderr:\n{stderr}",
out.status.code()
);
}
#[test]
fn open_reads_back_an_mkfs_exfat_image() {
if !tool_present("mkfs.exfat") {
eprintln!("skipping: mkfs.exfat not installed");
return;
}
let tmp = NamedTempFile::new().unwrap();
let bytes = 64u64 * 1024 * 1024;
std::fs::File::create(tmp.path())
.unwrap()
.set_len(bytes)
.unwrap();
let mkfs = Command::new("mkfs.exfat")
.args(["-L", "TEST-EXFAT"])
.arg(tmp.path())
.output()
.unwrap();
assert!(
mkfs.status.success(),
"mkfs.exfat failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&mkfs.stdout),
String::from_utf8_lossy(&mkfs.stderr),
);
let mut dev = FileBackend::open(tmp.path()).unwrap();
let fs = Exfat::open(&mut dev).expect("open mkfs.exfat image");
assert_eq!(
fs.volume_label(),
"TEST-EXFAT",
"volume label round-trip mismatch"
);
let root = fs.list_path(&mut dev, "/").unwrap();
let names: Vec<&str> = root.iter().map(|e| e.name.as_str()).collect();
let only_known = names.iter().all(|n| {
n.eq_ignore_ascii_case("System Volume Information")
|| n.eq_ignore_ascii_case("$RECYCLE.BIN")
});
assert!(
names.is_empty() || only_known,
"unexpected entries in fresh mkfs.exfat root: {names:?}"
);
}
#[test]
fn writer_image_fsck_verbose_simulates_mount_check() {
if !tool_present("fsck.exfat") {
eprintln!("skipping: fsck.exfat not installed");
return;
}
let tmp = NamedTempFile::new().unwrap();
let _ = format_volume(tmp.path(), 32, "MOUNTSIM");
{
let mut dev = FileBackend::open(tmp.path()).unwrap();
let mut fs = Exfat::open(&mut dev).unwrap();
let body: Vec<u8> = (0..(12 * 1024)).map(|i| (i % 251) as u8).collect();
let mut reader: &[u8] = &body;
fs.create_file(&mut dev, "/multi.bin", &mut reader, body.len() as u64, 0)
.unwrap();
let mut empty: &[u8] = &[];
fs.create_file(&mut dev, "/zero.bin", &mut empty, 0, 0)
.unwrap();
fs.create_dir(&mut dev, "/lvl1", 0).unwrap();
fs.create_dir(&mut dev, "/lvl1/lvl2", 0).unwrap();
let leaf: &[u8] = b"leaf\n";
let mut rl: &[u8] = leaf;
fs.create_file(
&mut dev,
"/lvl1/lvl2/leaf.txt",
&mut rl,
leaf.len() as u64,
0,
)
.unwrap();
fs.flush(&mut dev).unwrap();
dev.sync().unwrap();
}
{
let mut dev = FileBackend::open(tmp.path()).unwrap();
let fs = Exfat::open(&mut dev).unwrap();
let root: Vec<String> = fs
.list_path(&mut dev, "/")
.unwrap()
.into_iter()
.map(|e| e.name)
.collect();
assert!(root.iter().any(|n| n == "multi.bin"));
assert!(root.iter().any(|n| n == "zero.bin"));
assert!(root.iter().any(|n| n == "lvl1"));
let mut r = fs.open_file_reader(&mut dev, "/multi.bin").unwrap();
let mut total: u64 = 0;
let mut buf = [0u8; 4096];
loop {
let n = r.read(&mut buf).unwrap();
if n == 0 {
break;
}
total += n as u64;
}
assert_eq!(total, 12 * 1024);
}
let out = Command::new("fsck.exfat")
.args(["-n", "-v"])
.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(),
"fsck.exfat -nv failed (exit {:?}):\nstdout:\n{stdout}\nstderr:\n{stderr}",
out.status.code()
);
}