#![cfg(unix)]
use std::io::Write;
use std::path::Path;
use std::process::Command;
use fstool::block::FileBackend;
use fstool::fs::ext::{Ext, FormatOpts};
use fstool::fs::rootdevs::RootDevs;
use fstool::fs::{DeviceKind, 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 build_empty_ext2(path: &Path, opts: &FormatOpts) {
let mut dev = FileBackend::create(path, opts.blocks_count as u64 * opts.block_size as u64)
.expect("create image");
Ext::format_with(&mut dev, opts).expect("format ext2");
use fstool::block::BlockDevice;
dev.sync().expect("sync");
drop(dev);
}
#[test]
fn empty_ext2_passes_e2fsck() {
let Some(_) = which("e2fsck") else {
eprintln!("skipping: e2fsck not installed");
return;
};
let tmp = NamedTempFile::new().unwrap();
let opts = FormatOpts::default();
build_empty_ext2(tmp.path(), &opts);
let out = Command::new("e2fsck")
.arg("-fn")
.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(),
"e2fsck failed (exit {:?}):\nstdout:\n{stdout}\nstderr:\n{stderr}",
out.status.code()
);
}
#[test]
fn empty_ext2_lists_root_via_debugfs() {
let Some(_) = which("debugfs") else {
eprintln!("skipping: debugfs not installed");
return;
};
let tmp = NamedTempFile::new().unwrap();
let opts = FormatOpts::default();
build_empty_ext2(tmp.path(), &opts);
let out = Command::new("debugfs")
.arg("-R")
.arg("ls /")
.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(),
"debugfs failed:\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
stdout.contains("lost+found"),
"expected lost+found in root listing:\n{stdout}"
);
}
#[test]
fn populated_ext2_passes_e2fsck_and_debugfs() {
let Some(_) = which("e2fsck") else {
eprintln!("skipping: e2fsck not installed");
return;
};
let Some(_) = which("debugfs") else {
eprintln!("skipping: debugfs not installed");
return;
};
let tmp = NamedTempFile::new().unwrap();
let opts = FormatOpts {
inodes_count: 64,
..FormatOpts::default()
};
let size = opts.blocks_count as u64 * opts.block_size as u64;
use fstool::block::BlockDevice;
let mut dev = FileBackend::create(tmp.path(), size).unwrap();
let mut ext = Ext::format_with(&mut dev, &opts).unwrap();
let mut src_file = NamedTempFile::new().unwrap();
src_file
.as_file_mut()
.write_all(b"hello, fstool\n")
.unwrap();
let src_path = src_file.path().to_path_buf();
ext.add_file_to(
&mut dev,
2, b"hello.txt",
FileSource::HostPath(src_path),
FileMeta {
mode: 0o644,
mtime: 0,
..Default::default()
},
)
.unwrap();
let etc_ino = ext
.add_dir_to(&mut dev, 2, b"etc", FileMeta::with_mode(0o755))
.unwrap();
let mut conf_src = NamedTempFile::new().unwrap();
conf_src.as_file_mut().write_all(b"answer=42\n").unwrap();
ext.add_file_to(
&mut dev,
etc_ino,
b"conf",
FileSource::HostPath(conf_src.path().to_path_buf()),
FileMeta::with_mode(0o644),
)
.unwrap();
ext.add_symlink_to(&mut dev, 2, b"bin", b"/usr/bin", FileMeta::with_mode(0o777))
.unwrap();
let dev_ino = ext
.add_dir_to(&mut dev, 2, b"dev", FileMeta::with_mode(0o755))
.unwrap();
ext.add_device_to(
&mut dev,
dev_ino,
b"null",
DeviceKind::Char,
1,
3,
FileMeta::with_mode(0o666),
)
.unwrap();
ext.flush(&mut dev).unwrap();
dev.sync().unwrap();
drop(dev);
let out = Command::new("e2fsck")
.arg("-fn")
.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(),
"e2fsck failed:\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let out = Command::new("debugfs")
.arg("-R")
.arg("ls /")
.arg(tmp.path())
.output()
.unwrap();
let listing = String::from_utf8_lossy(&out.stdout);
for entry in ["hello.txt", "etc", "bin", "dev"] {
assert!(listing.contains(entry), "missing /{entry} in:\n{listing}");
}
let out = Command::new("debugfs")
.arg("-R")
.arg("ls /etc")
.arg(tmp.path())
.output()
.unwrap();
let listing = String::from_utf8_lossy(&out.stdout);
assert!(listing.contains("conf"), "missing conf:\n{listing}");
let out = Command::new("debugfs")
.arg("-R")
.arg("cat /hello.txt")
.arg(tmp.path())
.output()
.unwrap();
let body = String::from_utf8_lossy(&out.stdout);
assert!(
body.contains("hello, fstool"),
"hello.txt content mismatch:\n{body}"
);
let out = Command::new("debugfs")
.arg("-R")
.arg("stat /bin")
.arg(tmp.path())
.output()
.unwrap();
let stat = String::from_utf8_lossy(&out.stdout);
assert!(
stat.contains("Fast symlink") || stat.contains("/usr/bin"),
"symlink not recognised:\n{stat}"
);
}
#[test]
fn ext2_with_standard_rootdevs_passes_e2fsck() {
let Some(_) = which("e2fsck") else {
eprintln!("skipping: e2fsck not installed");
return;
};
let Some(_) = which("debugfs") else {
eprintln!("skipping: debugfs not installed");
return;
};
use fstool::block::BlockDevice;
let tmp = NamedTempFile::new().unwrap();
let opts = FormatOpts {
blocks_count: 4096,
inodes_count: 128,
..FormatOpts::default()
};
let size = opts.blocks_count as u64 * opts.block_size as u64;
let mut dev = FileBackend::create(tmp.path(), size).unwrap();
let mut ext = Ext::format_with(&mut dev, &opts).unwrap();
ext.populate_rootdevs(&mut dev, RootDevs::Standard, 0, 0, 0)
.unwrap();
ext.flush(&mut dev).unwrap();
dev.sync().unwrap();
drop(dev);
let out = Command::new("e2fsck")
.arg("-fn")
.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(),
"e2fsck failed:\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let out = Command::new("debugfs")
.arg("-R")
.arg("ls /dev")
.arg(tmp.path())
.output()
.unwrap();
let listing = String::from_utf8_lossy(&out.stdout);
for entry in [
"console", "null", "zero", "ptmx", "tty", "fuse", "random", "urandom", "tty0", "tty15",
"ttyS0", "ttyS3", "kmsg", "mem", "port", "hda", "hda4", "hdd", "sda", "sda1", "sdd4",
] {
assert!(
listing.contains(entry),
"missing /dev/{entry} in:\n{listing}"
);
}
let out = Command::new("debugfs")
.arg("-R")
.arg("stat /dev/null")
.arg(tmp.path())
.output()
.unwrap();
let stat = String::from_utf8_lossy(&out.stdout);
assert!(
stat.contains("Device major/minor number: 01:03")
|| stat.contains("Major: 1") && stat.contains("Minor: 3"),
"wrong device numbers for /dev/null:\n{stat}"
);
}
#[test]
fn ext2_open_lists_and_reads_what_was_written() {
use fstool::block::BlockDevice;
let tmp = NamedTempFile::new().unwrap();
let opts = FormatOpts {
inodes_count: 64,
..FormatOpts::default()
};
let size = opts.blocks_count as u64 * opts.block_size as u64;
let mut dev = FileBackend::create(tmp.path(), size).unwrap();
{
let mut ext = Ext::format_with(&mut dev, &opts).unwrap();
let mut src = NamedTempFile::new().unwrap();
src.as_file_mut()
.write_all(b"the quick brown fox\n")
.unwrap();
ext.add_file_to(
&mut dev,
2,
b"fox.txt",
FileSource::HostPath(src.path().to_path_buf()),
FileMeta::with_mode(0o644),
)
.unwrap();
ext.add_dir_to(&mut dev, 2, b"etc", FileMeta::with_mode(0o755))
.unwrap();
ext.flush(&mut dev).unwrap();
dev.sync().unwrap();
}
let ext = Ext::open(&mut dev).unwrap();
let entries = ext.list_inode(&mut dev, 2).unwrap();
let names: std::collections::HashSet<_> = entries.iter().map(|e| e.name.clone()).collect();
for n in ["lost+found", "fox.txt", "etc"] {
assert!(names.contains(n), "missing {n}: {names:?}");
}
let fox = ext.path_to_inode(&mut dev, "/fox.txt").unwrap();
let mut reader = ext.open_file_reader(&mut dev, fox).unwrap();
let mut content = Vec::new();
use std::io::Read as _;
reader.read_to_end(&mut content).unwrap();
assert_eq!(content, b"the quick brown fox\n");
}
#[test]
fn ext2_via_filesystem_trait() {
use fstool::block::BlockDevice;
use fstool::fs::{Filesystem, FilesystemFactory};
use std::io::Read;
use std::path::Path;
let tmp = NamedTempFile::new().unwrap();
let opts = FormatOpts {
inodes_count: 64,
..FormatOpts::default()
};
let size = opts.blocks_count as u64 * opts.block_size as u64;
let mut dev = FileBackend::create(tmp.path(), size).unwrap();
let mut ext = <Ext as FilesystemFactory>::format(&mut dev, &opts).unwrap();
let mut src = NamedTempFile::new().unwrap();
src.as_file_mut()
.write_all(b"trait-impl content\n")
.unwrap();
ext.create_dir(&mut dev, Path::new("/etc"), FileMeta::with_mode(0o755))
.unwrap();
ext.create_file(
&mut dev,
Path::new("/etc/conf"),
FileSource::HostPath(src.path().to_path_buf()),
FileMeta::with_mode(0o644),
)
.unwrap();
ext.create_symlink(
&mut dev,
Path::new("/conf"),
Path::new("/etc/conf"),
FileMeta::with_mode(0o777),
)
.unwrap();
ext.flush(&mut dev).unwrap();
dev.sync().unwrap();
let entries = ext.list(&mut dev, Path::new("/etc")).unwrap();
let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
assert!(names.contains(&"conf"));
{
let mut reader = ext.read_file(&mut dev, Path::new("/etc/conf")).unwrap();
let mut body = Vec::new();
reader.read_to_end(&mut body).unwrap();
assert_eq!(body, b"trait-impl content\n");
}
let attrs = ext.getattr(&mut dev, Path::new("/etc/conf")).unwrap();
assert_eq!(attrs.kind, fstool::fs::EntryKind::Regular);
assert_eq!(attrs.size, b"trait-impl content\n".len() as u64);
assert_eq!(attrs.mode, 0o644);
assert!(attrs.inode >= 2);
let set = fstool::fs::SetAttrs {
mode: Some(0o600),
uid: Some(42),
gid: Some(7),
mtime: Some(1234567890),
..Default::default()
};
ext.set_attrs(&mut dev, Path::new("/etc/conf"), set)
.unwrap();
let attrs = ext.getattr(&mut dev, Path::new("/etc/conf")).unwrap();
assert_eq!(attrs.mode, 0o600);
assert_eq!(attrs.uid, 42);
assert_eq!(attrs.gid, 7);
assert_eq!(attrs.mtime, 1234567890);
<Ext as Filesystem>::truncate(&mut ext, &mut dev, Path::new("/etc/conf"), 8).unwrap();
let attrs = ext.getattr(&mut dev, Path::new("/etc/conf")).unwrap();
assert_eq!(attrs.size, 8);
<Ext as Filesystem>::rename(
&mut ext,
&mut dev,
Path::new("/etc/conf"),
Path::new("/etc/renamed"),
)
.unwrap();
let entries = ext.list(&mut dev, Path::new("/etc")).unwrap();
let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
assert!(names.contains(&"renamed"));
assert!(!names.contains(&"conf"));
let pre = ext.getattr(&mut dev, Path::new("/etc/renamed")).unwrap();
ext.hardlink(&mut dev, Path::new("/etc/renamed"), Path::new("/etc/alias"))
.unwrap();
let post = ext.getattr(&mut dev, Path::new("/etc/renamed")).unwrap();
let alias = ext.getattr(&mut dev, Path::new("/etc/alias")).unwrap();
assert_eq!(alias.inode, pre.inode);
assert_eq!(post.nlink, pre.nlink + 1);
let stat = ext.statfs(&mut dev).unwrap();
assert_eq!(stat.block_size, opts.block_size);
assert_eq!(stat.blocks, opts.blocks_count as u64);
assert_eq!(stat.name_max, 255);
}
#[test]
fn ext2_sparse_file_uses_holes() {
use fstool::block::BlockDevice;
let Some(_) = which("e2fsck") else {
eprintln!("skipping: e2fsck not installed");
return;
};
let mut body = vec![b'X'; 1024];
body.extend(std::iter::repeat_n(0u8, 198 * 1024));
body.extend(std::iter::repeat_n(b'Y', 1024));
let srcfile = NamedTempFile::new().unwrap();
std::fs::write(srcfile.path(), &body).unwrap();
let opts = FormatOpts {
blocks_count: 8192,
inodes_count: 64,
sparse: true,
..FormatOpts::default()
};
let tmp = NamedTempFile::new().unwrap();
let mut dev = FileBackend::create(
tmp.path(),
opts.blocks_count as u64 * opts.block_size as u64,
)
.unwrap();
let mut ext = Ext::format_with(&mut dev, &opts).unwrap();
ext.add_file_to(
&mut dev,
2,
b"holey",
FileSource::HostPath(srcfile.path().to_path_buf()),
FileMeta::with_mode(0o644),
)
.unwrap();
ext.flush(&mut dev).unwrap();
dev.sync().unwrap();
let ino = ext.path_to_inode(&mut dev, "/holey").unwrap();
let mut got = Vec::new();
use std::io::Read;
ext.open_file_reader(&mut dev, ino)
.unwrap()
.read_to_end(&mut got)
.unwrap();
assert_eq!(got, body);
let inode = ext.read_inode(&mut dev, ino).unwrap();
assert!(
inode.blocks_512 < 32,
"sparse ext2 file used {} sectors",
inode.blocks_512
);
drop(dev);
let out = Command::new("e2fsck")
.arg("-fn")
.arg(tmp.path())
.output()
.unwrap();
assert!(
out.status.success(),
"e2fsck failed on sparse ext2:\n{}",
String::from_utf8_lossy(&out.stdout)
);
}
#[test]
fn fstool_can_modify_a_mke2fs_image() {
let Some(_) = which("mke2fs") else {
eprintln!("skipping: mke2fs not installed");
return;
};
let Some(_) = which("e2fsck") else {
eprintln!("skipping: e2fsck not installed");
return;
};
let Some(_) = which("debugfs") else {
eprintln!("skipping: debugfs not installed");
return;
};
let tmp = NamedTempFile::new().unwrap();
let out = Command::new("mke2fs")
.args([
"-F",
"-t",
"ext2",
"-b",
"1024",
"-L",
"",
"-U",
"00000000-0000-0000-0000-000000000000",
"-E",
"nodiscard",
"-O",
"none",
"-N",
"64",
])
.arg(tmp.path())
.arg("8192")
.output()
.unwrap();
assert!(
out.status.success(),
"mke2fs failed:\n{}",
String::from_utf8_lossy(&out.stderr)
);
use fstool::block::BlockDevice;
let mut dev = FileBackend::open(tmp.path()).unwrap();
let mut ext = Ext::open(&mut dev).unwrap();
let mut src = NamedTempFile::new().unwrap();
src.as_file_mut()
.write_all(b"injected by fstool\n")
.unwrap();
ext.add_file_to(
&mut dev,
2,
b"injected.txt",
FileSource::HostPath(src.path().to_path_buf()),
FileMeta::with_mode(0o644),
)
.unwrap();
ext.flush(&mut dev).unwrap();
dev.sync().unwrap();
drop(dev);
let out = Command::new("e2fsck")
.arg("-fn")
.arg(tmp.path())
.output()
.unwrap();
assert!(
out.status.success(),
"e2fsck failed after fstool modify:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
let out = Command::new("debugfs")
.arg("-R")
.arg("cat /injected.txt")
.arg(tmp.path())
.output()
.unwrap();
let body = String::from_utf8_lossy(&out.stdout);
assert!(
body.contains("injected by fstool"),
"injected file body wrong:\n{body}"
);
}
#[test]
fn ext2_build_from_host_dir_auto_size() {
let Some(_) = which("e2fsck") else {
eprintln!("skipping: e2fsck not installed");
return;
};
let Some(_) = which("debugfs") else {
eprintln!("skipping: debugfs not installed");
return;
};
use fstool::block::BlockDevice;
use fstool::fs::ext::{Ext, FsKind};
let tmpdir = tempfile::tempdir().unwrap();
let src = tmpdir.path();
std::fs::create_dir_all(src.join("etc")).unwrap();
std::fs::create_dir_all(src.join("usr/bin")).unwrap();
std::fs::write(src.join("hello.txt"), b"hello, world\n").unwrap();
std::fs::write(src.join("etc/conf"), b"answer = 42\n").unwrap();
std::os::unix::fs::symlink("/usr/bin", src.join("bin")).unwrap();
std::os::unix::fs::symlink(
"/very/long/path/that/exceeds/sixty/characters/for/sure/yes/indeed",
src.join("slowlink"),
)
.unwrap();
let tmp = NamedTempFile::new().unwrap();
let mut plan = fstool::fs::ext::BuildPlan::new(1024, FsKind::Ext2);
plan.scan_host_path(src).unwrap();
let opts = plan.to_format_opts();
let size = opts.blocks_count as u64 * opts.block_size as u64;
let mut dev = FileBackend::create(tmp.path(), size).unwrap();
Ext::build_from_host_dir(&mut dev, src, FsKind::Ext2, 1024).unwrap();
dev.sync().unwrap();
drop(dev);
let out = Command::new("e2fsck")
.arg("-fn")
.arg(tmp.path())
.output()
.unwrap();
assert!(
out.status.success(),
"e2fsck failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
let out = Command::new("debugfs")
.arg("-R")
.arg("ls /")
.arg(tmp.path())
.output()
.unwrap();
let listing = String::from_utf8_lossy(&out.stdout);
for entry in ["hello.txt", "etc", "usr", "bin", "slowlink"] {
assert!(listing.contains(entry), "missing /{entry}: {listing}");
}
let out = Command::new("debugfs")
.arg("-R")
.arg("cat /etc/conf")
.arg(tmp.path())
.output()
.unwrap();
assert!(
String::from_utf8_lossy(&out.stdout).contains("answer = 42"),
"/etc/conf body wrong"
);
}
#[test]
fn empty_ext2_dumpe2fs_clean() {
let Some(_) = which("dumpe2fs") else {
eprintln!("skipping: dumpe2fs not installed");
return;
};
let tmp = NamedTempFile::new().unwrap();
let opts = FormatOpts::default();
build_empty_ext2(tmp.path(), &opts);
let out = Command::new("dumpe2fs")
.arg("-h")
.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(),
"dumpe2fs failed:\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
stdout.contains("Filesystem magic number: 0xEF53"),
"missing magic line:\n{stdout}"
);
assert!(stdout.contains("Block size: 1024"));
assert!(stdout.contains("Inode count: 16"));
assert!(stdout.contains("Filesystem state: clean"));
}
#[test]
fn ext2_large_directory_uses_indirect_block() {
let Some(_) = which("e2fsck") else {
eprintln!("skipping: e2fsck not installed");
return;
};
let Some(_) = which("debugfs") else {
eprintln!("skipping: debugfs not installed");
return;
};
let opts = FormatOpts {
block_size: 1024,
blocks_count: 16 * 1024,
inodes_count: 4096,
..FormatOpts::default()
};
let tmp = NamedTempFile::new().unwrap();
let size = opts.blocks_count as u64 * opts.block_size as u64;
use fstool::block::BlockDevice;
let mut dev = FileBackend::create(tmp.path(), size).unwrap();
let mut ext = Ext::format_with(&mut dev, &opts).unwrap();
let bigdir = ext
.add_dir_to(&mut dev, 2, b"bigdir", FileMeta::with_mode(0o755))
.unwrap();
let n = 900u32;
for i in 0..n {
let name = format!("f{i:04}");
let mut src = NamedTempFile::new().unwrap();
src.as_file_mut().write_all(b"").unwrap();
ext.add_file_to(
&mut dev,
bigdir,
name.as_bytes(),
FileSource::HostPath(src.path().to_path_buf()),
FileMeta::with_mode(0o644),
)
.unwrap();
}
ext.flush(&mut dev).unwrap();
dev.sync().unwrap();
let inode = ext.read_inode(&mut dev, bigdir).unwrap();
assert!(
inode.size >= opts.block_size * 12,
"expected dir > 12 blocks, got size={} (block={})",
inode.size,
opts.block_size
);
assert!(
inode.block[12] != 0,
"expected single-indirect block allocated, got 0 (block[]={:?})",
inode.block
);
let entries = ext.list_inode(&mut dev, bigdir).unwrap();
let names: std::collections::HashSet<_> = entries
.iter()
.map(|e| e.name.clone())
.filter(|n| n != "." && n != "..")
.collect();
assert_eq!(names.len() as u32, n, "fstool ls miscounted");
drop(dev);
let fsck = Command::new("e2fsck")
.arg("-fn")
.arg(tmp.path())
.output()
.unwrap();
assert!(
fsck.status.success(),
"e2fsck failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&fsck.stdout),
String::from_utf8_lossy(&fsck.stderr)
);
let out = Command::new("debugfs")
.arg("-R")
.arg("ls -l /bigdir")
.arg(tmp.path())
.output()
.unwrap();
let listing = String::from_utf8_lossy(&out.stdout);
let count = listing
.lines()
.filter(|l| {
let first = l.split_whitespace().next().unwrap_or("");
first.parse::<u32>().is_ok()
&& !l.contains(" . ")
&& !l.ends_with(" .")
&& !l.contains(" .. ")
&& !l.ends_with(" ..")
})
.count();
assert_eq!(count as u32, n, "debugfs counted {count}, expected {n}");
}
#[test]
fn prezeroed_skips_full_device_zero_and_stays_sparse() {
let Some(_) = which("e2fsck") else {
eprintln!("skipping: e2fsck not installed");
return;
};
let tmp = NamedTempFile::new().unwrap();
let bs: u32 = 4096;
let blocks: u32 = (128 * 1024 * 1024) / bs;
let mut opts = FormatOpts {
kind: fstool::fs::ext::FsKind::Ext4,
block_size: bs,
blocks_count: blocks,
inodes_count: 4096,
sparse_super: true,
prezeroed: true,
..FormatOpts::default()
};
opts.log_groups_per_flex =
FormatOpts::default_log_groups_per_flex(opts.blocks_count.div_ceil(8 * opts.block_size));
build_empty_ext2(tmp.path(), &opts);
let out = Command::new("e2fsck")
.arg("-fn")
.arg(tmp.path())
.output()
.unwrap();
assert!(
out.status.success(),
"e2fsck failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
let meta = std::fs::metadata(tmp.path()).unwrap();
let apparent = meta.len();
assert_eq!(apparent, 128 * 1024 * 1024);
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
let on_disk = meta.blocks() * 512;
assert!(
on_disk < 16 * 1024 * 1024,
"prezeroed image is not sparse: {on_disk} bytes on disk (apparent {apparent})",
);
}
}
#[test]
fn large_file_round_trips_through_triple_indirect() {
let Some(_) = which("e2fsck") else {
eprintln!("skipping: e2fsck not installed");
return;
};
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("big.bin");
{
use std::io::Seek as _;
use std::io::SeekFrom;
let mut f = std::fs::File::create(&src).unwrap();
let len: u64 = 4_400 * 1024 * 1024;
f.set_len(len).unwrap();
f.seek(SeekFrom::Start(0)).unwrap();
f.write_all(&[0xABu8; 4096]).unwrap();
f.seek(SeekFrom::Start(len / 2)).unwrap();
f.write_all(&[0xCDu8; 4096]).unwrap();
f.seek(SeekFrom::Start(len - 4096)).unwrap();
f.write_all(&[0xEFu8; 4096]).unwrap();
f.sync_all().unwrap();
}
let tar = dir.path().join("big.tar");
let st = Command::new("tar")
.arg("-cf")
.arg(&tar)
.arg("-C")
.arg(dir.path())
.arg("big.bin")
.status()
.unwrap();
assert!(st.success(), "tar failed");
let img = dir.path().join("out.img");
let bin = env!("CARGO_BIN_EXE_fstool");
let r = Command::new(bin)
.args(["repack", "--size", "8G", "--fs-type", "ext2"])
.arg(&tar)
.arg(&img)
.output()
.unwrap();
assert!(
r.status.success(),
"repack failed:\n{}",
String::from_utf8_lossy(&r.stderr)
);
let out = Command::new("e2fsck")
.arg("-fn")
.arg(&img)
.output()
.unwrap();
assert!(
out.status.success(),
"e2fsck failed on > 4 GiB file:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
let Some(_) = which("md5sum") else {
eprintln!("skipping checksum check: md5sum not installed");
return;
};
let m = Command::new("md5sum").arg(&src).output().unwrap();
let want = String::from_utf8(m.stdout).unwrap();
let want_hash = want.split_whitespace().next().unwrap().to_string();
let mut cat = std::process::Command::new(bin)
.args(["cat"])
.arg(&img)
.arg("/big.bin")
.stdout(std::process::Stdio::piped())
.spawn()
.unwrap();
let cat_out = cat.stdout.take().unwrap();
let md5 = std::process::Command::new("md5sum")
.stdin(std::process::Stdio::from(cat_out))
.stdout(std::process::Stdio::piped())
.spawn()
.unwrap();
let md5_out = md5.wait_with_output().unwrap();
let cat_status = cat.wait().unwrap();
assert!(cat_status.success(), "fstool cat exited non-zero");
assert!(md5_out.status.success(), "md5sum exited non-zero");
let got = String::from_utf8(md5_out.stdout).unwrap();
let got_hash = got.split_whitespace().next().unwrap();
assert_eq!(got_hash, want_hash, "round-trip checksum mismatch");
}