#![cfg(unix)]
use std::io::Read;
use std::process::Command;
use fstool::block::{BlockDevice, FileBackend};
use fstool::fs::f2fs::{F2fs, FormatOpts};
use fstool::fs::{FileMeta, FileSource};
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_available(tool: &str) -> bool {
let probe = Command::new(tool).arg("-V").output();
match probe {
Ok(out) => {
let _ = out;
true
}
Err(_) => which(tool).is_some(),
}
}
const IMAGE_BYTES: u64 = 64 * 1024 * 1024;
#[test]
fn writer_image_passes_fsck_f2fs() {
if !tool_available("fsck.f2fs") {
eprintln!("skipping: fsck.f2fs not installed");
return;
}
let tmp = NamedTempFile::new().unwrap();
let mut dev = FileBackend::create(tmp.path(), IMAGE_BYTES).unwrap();
let opts = FormatOpts {
volume_label: "fstool-ext".into(),
..FormatOpts::default()
};
let mut fs = F2fs::format(&mut dev, &opts).unwrap();
fs.create_dir(
&mut dev,
std::path::Path::new("/etc"),
FileMeta::with_mode(0o755),
)
.unwrap();
let small = b"x=1\n";
fs.create_file(
&mut dev,
std::path::Path::new("/etc/config"),
FileSource::Reader {
reader: Box::new(std::io::Cursor::new(small.to_vec())),
len: small.len() as u64,
},
FileMeta::with_mode(0o644),
)
.unwrap();
let big: Vec<u8> = (0..(16 * 1024))
.map(|i| (i as u8).wrapping_mul(31))
.collect();
fs.create_file(
&mut dev,
std::path::Path::new("/big.bin"),
FileSource::Reader {
reader: Box::new(std::io::Cursor::new(big.clone())),
len: big.len() as u64,
},
FileMeta::with_mode(0o644),
)
.unwrap();
fs.create_symlink(
&mut dev,
std::path::Path::new("/link"),
std::path::Path::new("etc/config"),
FileMeta::with_mode(0o777),
)
.unwrap();
fs.create_hardlink(
&mut dev,
std::path::Path::new("/etc/config"),
std::path::Path::new("/etc/config.alias"),
)
.unwrap();
fs.flush(&mut dev).unwrap();
dev.sync().unwrap();
drop(dev);
let mut cmd = Command::new("fsck.f2fs");
cmd.arg("-f").arg(tmp.path());
let out = cmd.output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
out.status.success(),
"fsck.f2fs failed (exit {:?}):\nstdout:\n{stdout}\nstderr:\n{stderr}",
out.status.code(),
);
}
#[test]
fn mkfs_f2fs_image_opens_through_fstool() {
if !tool_available("mkfs.f2fs") {
eprintln!("skipping: mkfs.f2fs not installed");
return;
}
let tmp = NamedTempFile::new().unwrap();
let f = std::fs::OpenOptions::new()
.write(true)
.open(tmp.path())
.unwrap();
f.set_len(IMAGE_BYTES).unwrap();
drop(f);
let out = Command::new("mkfs.f2fs")
.args(["-f", "-l", "fstool-mkfs"])
.arg(tmp.path())
.output()
.unwrap();
if !out.status.success() {
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
panic!(
"mkfs.f2fs failed (exit {:?}):\nstdout:\n{stdout}\nstderr:\n{stderr}",
out.status.code(),
);
}
let mut dev = FileBackend::open(tmp.path()).unwrap();
let mut fs = F2fs::open(&mut dev).unwrap();
assert_eq!(fs.block_size(), 4096);
let tb = fs.total_bytes();
assert!(
tb <= IMAGE_BYTES && tb + (fs.block_size() as u64) >= IMAGE_BYTES,
"total_bytes {tb} not within one block of image size {IMAGE_BYTES}",
);
let list = fs.list_path(&mut dev, "/").unwrap();
assert!(
list.is_empty(),
"fresh mkfs.f2fs root expected empty, got {list:?}",
);
}
#[test]
fn writer_image_dump_f2fs_clean_exit() {
if !tool_available("dump.f2fs") {
eprintln!("skipping: dump.f2fs not installed");
return;
}
let tmp = NamedTempFile::new().unwrap();
let mut dev = FileBackend::create(tmp.path(), IMAGE_BYTES).unwrap();
let opts = FormatOpts::default();
let mut fs = F2fs::format(&mut dev, &opts).unwrap();
let payload = b"dump.f2fs round-trip payload\n";
fs.create_file(
&mut dev,
std::path::Path::new("/payload.txt"),
FileSource::Reader {
reader: Box::new(std::io::Cursor::new(payload.to_vec())),
len: payload.len() as u64,
},
FileMeta::with_mode(0o644),
)
.unwrap();
fs.flush(&mut dev).unwrap();
dev.sync().unwrap();
let mut r = fs.open_file_reader(&mut dev, "/payload.txt").unwrap();
let mut got = Vec::new();
r.read_to_end(&mut got).unwrap();
assert_eq!(got, payload);
drop(r);
drop(dev);
let out = Command::new("dump.f2fs")
.args(["-i", "3"])
.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(),
"dump.f2fs failed (exit {:?}):\nstdout:\n{stdout}\nstderr:\n{stderr}",
out.status.code(),
);
}