use fs_ext4::block_io::FileDevice;
use fs_ext4::dir;
use fs_ext4::features::FsFlavor;
use fs_ext4::file_io;
use fs_ext4::fs::Filesystem;
use fs_ext4::inode::{Inode, InodeFlags};
use fs_ext4::mkfs::format_filesystem_with_flavor;
use fs_ext4::verify;
use std::io::{Seek, SeekFrom, Write};
use std::sync::Arc;
const ROOT_INODE: u32 = 2;
struct ScratchGuard(std::path::PathBuf);
impl Drop for ScratchGuard {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
fn mkfs_ext2_image(path: &std::path::Path, size_bytes: u64, block_size: u32) {
let f = std::fs::OpenOptions::new()
.create(true)
.truncate(true)
.read(true)
.write(true)
.open(path)
.expect("create scratch image");
f.set_len(size_bytes).expect("set_len");
drop(f);
let dev = FileDevice::open_rw(path.to_str().unwrap()).expect("open scratch rw");
format_filesystem_with_flavor(
&dev,
Some("EXT2TEST"),
None,
size_bytes,
block_size,
FsFlavor::Ext2,
)
.expect("mkfs ext2");
}
fn scratch_path(stem: &str) -> std::path::PathBuf {
let pid = std::process::id();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
std::env::temp_dir().join(format!("rust-fs-ext4-{stem}-{pid}-{nanos}.img"))
}
#[test]
fn mkfs_ext2_then_mount_and_read_root() {
let path = scratch_path("ext2-basic");
let size: u64 = 4 * 1024 * 1024; let block_size: u32 = 1024;
mkfs_ext2_image(&path, size, block_size);
let _cleanup = ScratchGuard(path.clone());
let dev = Arc::new(FileDevice::open(path.to_str().unwrap()).expect("open ro"));
let fs = Filesystem::mount(dev).expect("mount ext2");
assert_eq!(fs.flavor, FsFlavor::Ext2, "flavor must detect as ext2");
assert_eq!(fs.sb.volume_name, "EXT2TEST");
assert_eq!(fs.sb.block_size(), block_size);
assert_eq!(fs.sb.inode_size, 128, "ext2 default inode size");
assert_eq!(fs.sb.desc_size, 32, "ext2 must use 32-byte BGDs");
let root_raw = fs.read_inode_raw(ROOT_INODE).expect("read root inode");
let root = Inode::parse(&root_raw).expect("parse root inode");
assert!(root.is_dir(), "root must be a directory");
assert!(
(root.flags & InodeFlags::EXTENTS.bits()) == 0,
"ext2 root inode must NOT carry EXTENTS_FL (got flags 0x{:x})",
root.flags,
);
let i_block_0 = u32::from_le_bytes(root.block[0..4].try_into().unwrap());
assert!(
i_block_0 != 0,
"ext2 root inode i_block[0] must point at the dir data block"
);
let dir_data = file_io::read_all(&fs, &root).expect("read root dir via indirect");
assert_eq!(
dir_data.len() as u32,
block_size,
"single-block root directory"
);
let entries = dir::parse_block(&dir_data, true).expect("parse dir entries");
let mut saw_dot = false;
let mut saw_dotdot = false;
for e in &entries {
let name = std::str::from_utf8(&e.name).unwrap_or("<bad utf8>");
match name {
"." => {
saw_dot = true;
assert_eq!(e.inode, ROOT_INODE, ". entry must point at root");
}
".." => {
saw_dotdot = true;
assert_eq!(e.inode, ROOT_INODE, ".. of root must point at root");
}
other => panic!("unexpected entry in fresh root dir: {other:?}"),
}
}
assert!(saw_dot && saw_dotdot, "root must contain `.` and `..`");
}
#[test]
fn mkfs_ext2_then_write_and_read_back_each_tier() {
let path = scratch_path("ext2-write");
let size: u64 = 8 * 1024 * 1024; let block_size: u32 = 1024;
mkfs_ext2_image(&path, size, block_size);
let _cleanup = ScratchGuard(path.clone());
let dev_rw = Arc::new(FileDevice::open_rw(path.to_str().unwrap()).expect("open rw"));
let fs = Filesystem::mount(dev_rw).expect("mount ext2 rw");
assert_eq!(fs.flavor, FsFlavor::Ext2);
assert!(
fs.dev.is_writable(),
"test requires writable device for apply_create / apply_replace"
);
let cases = [
("/tiny.bin", 200usize), ("/direct.bin", 12 * block_size as usize), ("/single.bin", 15 * block_size as usize), ("/double.bin", 271 * block_size as usize), ];
for (name, byte_count) in cases {
let name_hash: u32 = name.bytes().map(|b| b as u32).sum();
let payload: Vec<u8> = (0..byte_count)
.map(|i| ((i as u32).wrapping_mul(31).wrapping_add(name_hash) & 0xFF) as u8)
.collect();
let new_ino = fs.apply_create(name, 0o644).expect("apply_create");
let (inode_after_create, _) = fs.read_inode_verified(new_ino).expect("read new inode");
assert!(
(inode_after_create.flags & InodeFlags::EXTENTS.bits()) == 0,
"{name}: freshly-created ext2 file inode must NOT carry EXTENTS_FL"
);
assert_eq!(inode_after_create.size, 0, "{name}: starts empty");
let written = fs
.apply_replace_file_content(name, &payload)
.expect("apply_replace_file_content");
assert_eq!(
written as usize, byte_count,
"{name}: apply_replace_file_content returned wrong length"
);
let (inode_after_write, _) = fs.read_inode_verified(new_ino).expect("re-read inode");
assert_eq!(
inode_after_write.size, byte_count as u64,
"{name}: inode size mismatch after write"
);
assert!(
(inode_after_write.flags & InodeFlags::EXTENTS.bits()) == 0,
"{name}: write must not have flipped EXTENTS_FL on (we use indirect)"
);
let read_back = file_io::read_all(&fs, &inode_after_write).expect("read_all via indirect");
assert_eq!(
read_back.len(),
byte_count,
"{name}: read-back length mismatch"
);
assert_eq!(
read_back, payload,
"{name}: byte-for-byte content mismatch through ext2 indirect roundtrip"
);
}
let report = verify::verify(&fs).expect("verify walked the volume");
assert!(
report.is_clean(),
"structural verifier rejected the post-write volume: {}\nerrors:\n {}",
report.summary(),
report.errors.join("\n ")
);
}
const HAS_JOURNAL_BIT: u32 = 0x0004;
fn flip_to_ext3_flavor(path: &std::path::Path) {
let mut f = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(path)
.expect("open image rw for patch");
let off = 1024 + 0x5C;
f.seek(SeekFrom::Start(off)).expect("seek sb feat_compat");
let mut buf = [0u8; 4];
use std::io::Read;
f.read_exact(&mut buf).expect("read feat_compat");
let mut feat = u32::from_le_bytes(buf);
feat |= HAS_JOURNAL_BIT;
f.seek(SeekFrom::Start(off)).expect("seek sb feat_compat");
f.write_all(&feat.to_le_bytes()).expect("write feat_compat");
f.sync_all().expect("sync");
}
#[test]
fn ext3_ro_mount_succeeds_via_flavor_detection() {
let path = scratch_path("ext3-ro");
let size: u64 = 4 * 1024 * 1024;
let block_size: u32 = 1024;
mkfs_ext2_image(&path, size, block_size);
let _cleanup = ScratchGuard(path.clone());
flip_to_ext3_flavor(&path);
let dev = Arc::new(FileDevice::open(path.to_str().unwrap()).expect("open ro"));
let fs = Filesystem::mount(dev).expect("ext3 RO mount must succeed");
assert_eq!(
fs.flavor,
FsFlavor::Ext3,
"HAS_JOURNAL bit should yield Ext3 flavor"
);
assert!(!fs.dev.is_writable(), "test invariant: RO device");
assert!(
fs.journal.is_none(),
"RO mount must not open the journal writer"
);
let root_raw = fs.read_inode_raw(ROOT_INODE).expect("read root inode");
let root = Inode::parse(&root_raw).expect("parse root inode");
let dir_data = file_io::read_all(&fs, &root).expect("read root dir");
let entries = dir::parse_block(&dir_data, true).expect("parse dir entries");
assert!(entries.iter().any(|e| e.name == b"."));
assert!(entries.iter().any(|e| e.name == b".."));
}
#[test]
fn mkfs_ext3_then_mount_ro_and_read_root() {
let path = scratch_path("ext3-mkfs");
let size: u64 = 4 * 1024 * 1024;
let block_size: u32 = 1024;
{
let f = std::fs::OpenOptions::new()
.create(true)
.truncate(true)
.read(true)
.write(true)
.open(&path)
.expect("create scratch");
f.set_len(size).expect("set_len");
}
let dev_rw = FileDevice::open_rw(path.to_str().unwrap()).expect("open rw for mkfs");
format_filesystem_with_flavor(
&dev_rw,
Some("EXT3MKFS"),
None,
size,
block_size,
FsFlavor::Ext3,
)
.expect("mkfs ext3");
drop(dev_rw);
let _cleanup = ScratchGuard(path.clone());
let dev = Arc::new(FileDevice::open(path.to_str().unwrap()).expect("open ro"));
let fs = Filesystem::mount(dev).expect("mount ext3 ro");
assert_eq!(fs.flavor, FsFlavor::Ext3, "mkfs must produce Ext3 flavor");
assert_eq!(fs.sb.volume_name, "EXT3MKFS");
assert_eq!(fs.sb.journal_inode, 8, "s_journal_inum must be 8");
let raw = fs.read_inode_raw(8).expect("read journal inode");
let jinode = Inode::parse(&raw).expect("parse journal inode");
assert_eq!(
jinode.mode, 0,
"journal inode i_mode must be 0 (Linux convention)"
);
assert_eq!(jinode.links_count, 1);
assert!(
(jinode.flags & InodeFlags::EXTENTS.bits()) == 0,
"ext3 journal inode must NOT carry EXTENTS_FL"
);
assert_eq!(
jinode.size,
1024 * block_size as u64,
"journal i_size must equal data-blocks * block_size"
);
let report = verify::verify(&fs).expect("verify");
assert!(
report.is_clean(),
"fresh ext3 mkfs failed verify: {}\nerrors:\n {}",
report.summary(),
report.errors.join("\n ")
);
}
#[test]
fn mkfs_ext3_rw_roundtrip_create_write_read() {
let path = scratch_path("ext3-rw-roundtrip");
let size: u64 = 4 * 1024 * 1024;
let block_size: u32 = 1024;
{
let f = std::fs::OpenOptions::new()
.create(true)
.truncate(true)
.read(true)
.write(true)
.open(&path)
.expect("create scratch");
f.set_len(size).expect("set_len");
}
let dev_rw = FileDevice::open_rw(path.to_str().unwrap()).expect("open rw for mkfs");
format_filesystem_with_flavor(
&dev_rw,
Some("EXT3RW"),
None,
size,
block_size,
FsFlavor::Ext3,
)
.expect("mkfs ext3");
drop(dev_rw);
let _cleanup = ScratchGuard(path.clone());
let dev = Arc::new(FileDevice::open_rw(path.to_str().unwrap()).expect("open rw"));
let fs = Filesystem::mount(dev).expect("ext3 RW mount must succeed in Phase B");
assert_eq!(fs.flavor, FsFlavor::Ext3);
assert!(fs.dev.is_writable());
assert!(
fs.journal.is_some(),
"ext3 RW mount must open the JournalWriter (proves indirect-block dispatch works)"
);
let payload: Vec<u8> = (0..15 * block_size as usize)
.map(|i| ((i as u32).wrapping_mul(31) & 0xFF) as u8)
.collect();
let _ino = fs.apply_create("/hello.bin", 0o644).expect("apply_create");
let written = fs
.apply_replace_file_content("/hello.bin", &payload)
.expect("apply_replace_file_content");
assert_eq!(written as usize, payload.len());
let mut reader = |ino: u32| fs.read_inode_verified(ino).map(|(i, _)| i);
let ino =
fs_ext4::path::lookup(fs.dev.as_ref(), &fs.sb, &mut reader, "/hello.bin").expect("lookup");
let (inode, _) = fs.read_inode_verified(ino).expect("read inode");
assert!(
(inode.flags & InodeFlags::EXTENTS.bits()) == 0,
"ext3 file inode must not carry EXTENTS_FL"
);
let read_back = file_io::read_all(&fs, &inode).expect("read_all");
assert_eq!(read_back, payload, "ext3 write+read roundtrip mismatch");
let report = verify::verify(&fs).expect("verify");
assert!(
report.is_clean(),
"ext3 RW post-write verify failed: {}\nerrors:\n {}",
report.summary(),
report.errors.join("\n ")
);
}