use std::path::Path;
use std::process::Command;
use std::io::Read;
use fstool::block::FileBackend;
use fstool::fs::fat::{Fat32, FatFormatOpts};
use tempfile::{NamedTempFile, TempDir};
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 format_empty(path: &Path, mib: u32) {
let total_sectors = mib * 1024 * 1024 / 512;
let bytes = total_sectors as u64 * 512;
let mut dev = FileBackend::create(path, bytes).expect("create image");
let opts = FatFormatOpts {
total_sectors,
volume_id: 0xCAFE_F00D,
volume_label: *b"FSTOOL ",
};
Fat32::format(&mut dev, &opts).expect("format fat32");
use fstool::block::BlockDevice;
dev.sync().expect("sync");
}
#[test]
fn empty_fat32_passes_fsck_vfat() {
let Some(_) = which("fsck.vfat") else {
eprintln!("skipping: fsck.vfat not installed");
return;
};
let tmp = NamedTempFile::new().unwrap();
format_empty(tmp.path(), 64);
let out = Command::new("fsck.vfat")
.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.vfat failed (exit {:?}):\nstdout:\n{stdout}\nstderr:\n{stderr}",
out.status.code()
);
}
#[test]
fn build_from_host_dir_passes_fsck_vfat() {
let Some(_) = which("fsck.vfat") else {
eprintln!("skipping: fsck.vfat not installed");
return;
};
let src = TempDir::new().unwrap();
std::fs::write(src.path().join("hello.txt"), b"hello, fat32\n").unwrap();
std::fs::create_dir(src.path().join("docs")).unwrap();
std::fs::write(
src.path().join("docs").join("README.md"),
b"# Long Name File\n",
)
.unwrap();
let tmp = NamedTempFile::new().unwrap();
let total_sectors = 64 * 1024 * 1024 / 512;
{
use fstool::block::BlockDevice;
let mut dev = FileBackend::create(tmp.path(), total_sectors as u64 * 512).unwrap();
Fat32::build_from_host_dir(
&mut dev,
total_sectors,
src.path(),
0xCAFE_F00D,
*b"FSTOOL ",
)
.expect("build fat32");
dev.sync().unwrap();
}
let out = Command::new("fsck.vfat")
.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.vfat failed (exit {:?}):\nstdout:\n{stdout}\nstderr:\n{stderr}",
out.status.code()
);
}
#[test]
fn host_dir_contents_visible_via_mtools() {
let Some(_) = which("mdir") else {
eprintln!("skipping: mtools not installed");
return;
};
let Some(_) = which("mtype") else {
eprintln!("skipping: mtools (mtype) not installed");
return;
};
let src = TempDir::new().unwrap();
std::fs::write(src.path().join("hello.txt"), b"hello, fat32\n").unwrap();
std::fs::create_dir(src.path().join("docs")).unwrap();
std::fs::write(src.path().join("docs").join("README.md"), b"long-name\n").unwrap();
let tmp = NamedTempFile::new().unwrap();
let total_sectors = 64 * 1024 * 1024 / 512;
{
use fstool::block::BlockDevice;
let mut dev = FileBackend::create(tmp.path(), total_sectors as u64 * 512).unwrap();
Fat32::build_from_host_dir(
&mut dev,
total_sectors,
src.path(),
0xCAFE_F00D,
*b"FSTOOL ",
)
.unwrap();
dev.sync().unwrap();
}
let cfg = src.path().join("mtoolsrc");
std::fs::write(
&cfg,
format!("drive +: file=\"{}\"\n", tmp.path().display()),
)
.unwrap();
let out = Command::new("mdir")
.env("MTOOLSRC", &cfg)
.args(["-i", &tmp.path().display().to_string(), "::/"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
out.status.success(),
"mdir failed:\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
stdout.contains("hello"),
"mdir output missing hello.txt:\n{stdout}"
);
assert!(
stdout.contains("docs"),
"mdir output missing docs/:\n{stdout}"
);
let out = Command::new("mtype")
.args(["-i", &tmp.path().display().to_string(), "::/hello.txt"])
.output()
.unwrap();
assert!(
out.status.success(),
"mtype failed:\nstderr:\n{}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(out.stdout, b"hello, fat32\n");
}
#[test]
fn open_reads_back_our_own_image() {
let src = TempDir::new().unwrap();
std::fs::write(src.path().join("hello.txt"), b"hello, fat32\n").unwrap();
std::fs::create_dir(src.path().join("docs")).unwrap();
std::fs::write(
src.path().join("docs").join("LongNameFile.md"),
b"long-name-content\n",
)
.unwrap();
let tmp = NamedTempFile::new().unwrap();
let total_sectors = 64 * 1024 * 1024 / 512;
{
use fstool::block::BlockDevice;
let mut dev = FileBackend::create(tmp.path(), total_sectors as u64 * 512).unwrap();
Fat32::build_from_host_dir(
&mut dev,
total_sectors,
src.path(),
0xDEAD_BEEF,
*b"ROUNDTRIP ",
)
.unwrap();
dev.sync().unwrap();
}
let mut dev = FileBackend::open(tmp.path()).unwrap();
let fs = Fat32::open(&mut dev).unwrap();
let root = fs.list_path(&mut dev, "/").unwrap();
let names: Vec<&str> = root.iter().map(|e| e.name.as_str()).collect();
assert!(names.iter().any(|n| n.eq_ignore_ascii_case("hello.txt")));
assert!(names.iter().any(|n| n.eq_ignore_ascii_case("docs")));
let docs = fs.list_path(&mut dev, "/docs").unwrap();
let docnames: Vec<&str> = docs.iter().map(|e| e.name.as_str()).collect();
assert!(
docnames.contains(&"LongNameFile.md"),
"long name not reconstructed: {docnames:?}"
);
let mut reader = fs.open_file_reader(&mut dev, "/hello.txt").unwrap();
let mut body = Vec::new();
reader.read_to_end(&mut body).unwrap();
assert_eq!(body, b"hello, fat32\n");
let mut reader = fs
.open_file_reader(&mut dev, "/docs/LongNameFile.md")
.unwrap();
let mut body = Vec::new();
reader.read_to_end(&mut body).unwrap();
assert_eq!(body, b"long-name-content\n");
}
#[test]
fn modify_in_place_add_and_remove() {
let Some(_) = which("fsck.vfat") else {
eprintln!("skipping: fsck.vfat not installed");
return;
};
let src = TempDir::new().unwrap();
std::fs::write(src.path().join("original.txt"), b"original\n").unwrap();
let img = NamedTempFile::new().unwrap();
let total_sectors = 64 * 1024 * 1024 / 512;
{
use fstool::block::BlockDevice;
let mut dev = FileBackend::create(img.path(), total_sectors as u64 * 512).unwrap();
Fat32::build_from_host_dir(
&mut dev,
total_sectors,
src.path(),
0x1234_5678,
*b"MUTATE ",
)
.unwrap();
dev.sync().unwrap();
}
let host = TempDir::new().unwrap();
let added_file = host.path().join("added.txt");
std::fs::write(&added_file, b"added body\n").unwrap();
let nested_file = host.path().join("A Long Name.md");
std::fs::write(&nested_file, b"nested body\n").unwrap();
{
use fstool::block::BlockDevice;
let mut dev = FileBackend::open(img.path()).unwrap();
let mut fs = Fat32::open(&mut dev).unwrap();
fs.add_file(&mut dev, "/added.txt", &added_file).unwrap();
fs.add_dir(&mut dev, "/new").unwrap();
fs.add_file(&mut dev, "/new/A Long Name.md", &nested_file)
.unwrap();
fs.remove(&mut dev, "/original.txt").unwrap();
fs.flush(&mut dev).unwrap();
dev.sync().unwrap();
}
let res = Command::new("fsck.vfat")
.args(["-n", "-v"])
.arg(img.path())
.output()
.unwrap();
assert!(
res.status.success(),
"fsck.vfat failed after modify:\n{}",
String::from_utf8_lossy(&res.stdout)
);
let mut dev = FileBackend::open(img.path()).unwrap();
let fs = Fat32::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 == "original.txt"));
assert!(root.iter().any(|n| n == "added.txt"));
assert!(root.iter().any(|n| n == "new"));
let mut reader = fs.open_file_reader(&mut dev, "/added.txt").unwrap();
let mut body = Vec::new();
reader.read_to_end(&mut body).unwrap();
assert_eq!(body, b"added body\n");
let mut reader = fs
.open_file_reader(&mut dev, "/new/A Long Name.md")
.unwrap();
let mut body = Vec::new();
reader.read_to_end(&mut body).unwrap();
assert_eq!(body, b"nested body\n");
}
#[test]
fn remove_rejects_non_empty_directory() {
let src = TempDir::new().unwrap();
std::fs::create_dir(src.path().join("dir")).unwrap();
std::fs::write(src.path().join("dir/inner.txt"), b"x\n").unwrap();
let img = NamedTempFile::new().unwrap();
let total_sectors = 64 * 1024 * 1024 / 512;
{
use fstool::block::BlockDevice;
let mut dev = FileBackend::create(img.path(), total_sectors as u64 * 512).unwrap();
Fat32::build_from_host_dir(
&mut dev,
total_sectors,
src.path(),
0x1234_5678,
*b"MUTATE ",
)
.unwrap();
dev.sync().unwrap();
}
let mut dev = FileBackend::open(img.path()).unwrap();
let mut fs = Fat32::open(&mut dev).unwrap();
let err = fs.remove(&mut dev, "/dir").unwrap_err();
assert!(
format!("{err}").contains("not empty"),
"expected non-empty error, got {err}"
);
fs.remove(&mut dev, "/dir/inner.txt").unwrap();
fs.remove(&mut dev, "/dir").unwrap();
}
#[test]
fn open_reads_back_an_mkfs_vfat_image() {
let Some(_) = which("mkfs.vfat") else {
eprintln!("skipping: mkfs.vfat not installed");
return;
};
let Some(_) = which("mcopy") else {
eprintln!("skipping: mcopy 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.vfat")
.args(["-F", "32", "-n", "MKFSVOL", "-i", "ABCDEF12"])
.arg(tmp.path())
.output()
.unwrap();
assert!(
mkfs.status.success(),
"mkfs.vfat failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&mkfs.stdout),
String::from_utf8_lossy(&mkfs.stderr),
);
let host_file = TempDir::new().unwrap();
let hostf = host_file.path().join("CopiedFile.txt");
std::fs::write(&hostf, b"copied via mtools\n").unwrap();
let mc = Command::new("mcopy")
.args(["-i", &tmp.path().display().to_string()])
.arg(&hostf)
.arg("::/CopiedFile.txt")
.output()
.unwrap();
assert!(
mc.status.success(),
"mcopy failed:\nstderr:\n{}",
String::from_utf8_lossy(&mc.stderr)
);
let mut dev = FileBackend::open(tmp.path()).unwrap();
let fs = Fat32::open(&mut dev).unwrap();
let root = fs.list_path(&mut dev, "/").unwrap();
let names: Vec<&str> = root.iter().map(|e| e.name.as_str()).collect();
assert!(
names
.iter()
.any(|n| n.eq_ignore_ascii_case("CopiedFile.txt")),
"missing CopiedFile.txt in mkfs.vfat image: {names:?}"
);
let mut reader = fs.open_file_reader(&mut dev, "/CopiedFile.txt").unwrap();
let mut body = Vec::new();
reader.read_to_end(&mut body).unwrap();
assert_eq!(body, b"copied via mtools\n");
}