use fs_ext4::block_io::FileDevice;
use fs_ext4::extent::ExtentHeader;
use fs_ext4::path as path_mod;
use fs_ext4::Filesystem;
use std::fs;
use std::sync::Arc;
fn copy_to_tmp(name: &str) -> String {
use std::sync::atomic::{AtomicU32, Ordering};
static COUNTER: AtomicU32 = AtomicU32::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let src = fs_ext4_test_support::fixture(env!("CARGO_MANIFEST_DIR"), name);
let dst = fs_ext4_test_support::temp_path!(
"fs_ext4_multilvl_{}_{n}_{}.img",
std::process::id(),
name
);
fs::copy(&src, &dst).unwrap_or_else(|e| panic!("copy {src} -> {dst}: {e}"));
dst
}
fn resolve(fs: &Filesystem, path: &str) -> Option<u32> {
let mut reader = |ino: u32| fs.read_inode_verified(ino).map(|(i, _)| i);
path_mod::lookup(fs.dev.as_ref(), &fs.sb, &mut reader, path).ok()
}
fn create_until_promotion(fs: &Filesystem, target: &str, max_entries: usize) {
let bs = fs.sb.block_size() as u64;
let dir_ino = resolve(fs, target).expect("resolve target");
let mut prev_size = fs.read_inode_verified(dir_ino).expect("read").0.size;
let mut gap_counter = 0u32;
for i in 0..max_entries {
let name = format!("{target}/entry_{i:05}_pad.txt");
fs.apply_create(&name, 0o644)
.unwrap_or_else(|e| panic!("create #{i} {name}: {e}"));
let new_size = fs.read_inode_verified(dir_ino).expect("re-read").0.size;
if new_size > prev_size {
prev_size = new_size;
let gap = format!("/fs_ext4_gap_{gap_counter:04}.bin");
gap_counter += 1;
fs.apply_create(&gap, 0o644).expect("create gap");
fs.apply_replace_file_content(&gap, &vec![0xABu8; bs as usize])
.expect("write gap");
}
let (dir_inode, _) = fs.read_inode_verified(dir_ino).expect("re-read");
let hdr = ExtentHeader::parse(&dir_inode.block).expect("parse");
if hdr.depth >= 1 {
return;
}
}
let (dir_inode, _) = fs.read_inode_verified(dir_ino).expect("final read");
let hdr = ExtentHeader::parse(&dir_inode.block).expect("parse");
panic!(
"never promoted after {max_entries} creates (depth={} entries={} max={} size={})",
hdr.depth, hdr.entries, hdr.max, dir_inode.size
);
}
#[test]
fn extending_dir_past_four_noncontiguous_extents_triggers_promotion() {
let path = copy_to_tmp("ext4-basic.img");
let dev = FileDevice::open_rw(&path).expect("open rw");
let fs = Filesystem::mount(Arc::new(dev)).expect("mount");
let target = "/big_dir";
fs.apply_mkdir(target, 0o755).expect("mkdir target");
create_until_promotion(&fs, target, 2000);
let dir_ino = resolve(&fs, target).expect("resolve");
let (dir_inode, _) = fs.read_inode_verified(dir_ino).expect("read");
let hdr = ExtentHeader::parse(&dir_inode.block).expect("parse");
assert_eq!(hdr.depth, 1);
assert_eq!(hdr.entries, 1);
drop(fs);
let dev = FileDevice::open(&path).expect("open ro");
let fs = Filesystem::mount(Arc::new(dev)).expect("remount");
let dir_ino = resolve(&fs, target).expect("resolve target (ro)");
let (dir_inode, _) = fs.read_inode_verified(dir_ino).expect("read");
let hdr = ExtentHeader::parse(&dir_inode.block).expect("parse");
assert_eq!(hdr.depth, 1, "promotion must persist across remount");
let n_blocks = dir_inode.size.div_ceil(fs.sb.block_size() as u64);
for logical in 0..n_blocks {
let phys = fs_ext4::extent::map_logical(
&dir_inode.block,
fs.dev.as_ref(),
fs.sb.block_size(),
logical,
)
.expect("map_logical through depth-1");
assert!(phys.is_some(), "logical {logical} must map post-promotion");
}
fs::remove_file(path).ok();
}
#[test]
fn directory_growth_continues_past_promotion() {
let path = copy_to_tmp("ext4-basic.img");
let dev = FileDevice::open_rw(&path).expect("open rw");
let fs = Filesystem::mount(Arc::new(dev)).expect("mount");
let bs = fs.sb.block_size() as u64;
let target = "/post_promo_dir";
fs.apply_mkdir(target, 0o755).expect("mkdir target");
create_until_promotion(&fs, target, 2000);
let dir_ino = resolve(&fs, target).expect("resolve");
let (dir_inode, _) = fs.read_inode_verified(dir_ino).expect("read");
let hdr = ExtentHeader::parse(&dir_inode.block).expect("parse");
assert_eq!(hdr.depth, 1);
let size_at_promo = dir_inode.size;
let mut post_extensions = 0;
let mut prev_size = size_at_promo;
let mut gap_counter = 0u32;
for i in 0..2500 {
let name = format!("{target}/post_{i:05}_extra.txt");
fs.apply_create(&name, 0o644)
.unwrap_or_else(|e| panic!("post-promotion create #{i}: {e}"));
let (di, _) = fs.read_inode_verified(dir_ino).expect("read");
if di.size > prev_size {
prev_size = di.size;
post_extensions += 1;
let gap = format!("/post_gap_{gap_counter:04}.bin");
gap_counter += 1;
fs.apply_create(&gap, 0o644).expect("create gap");
fs.apply_replace_file_content(&gap, &vec![0xCDu8; bs as usize])
.expect("write gap");
if post_extensions >= 2 {
break;
}
}
}
assert!(
post_extensions >= 2,
"expected at least 2 post-promotion extensions, got {post_extensions}"
);
let (dir_inode, _) = fs.read_inode_verified(dir_ino).expect("final read");
let hdr = ExtentHeader::parse(&dir_inode.block).expect("parse");
assert_eq!(hdr.depth, 1, "still at depth 1 after additional grows");
assert!(dir_inode.size > size_at_promo, "dir size grew");
drop(fs);
let dev = FileDevice::open(&path).expect("open ro");
let fs = Filesystem::mount(Arc::new(dev)).expect("remount");
let dir_ino = resolve(&fs, target).expect("resolve");
let (dir_inode, _) = fs.read_inode_verified(dir_ino).expect("read");
let n_blocks = dir_inode.size.div_ceil(fs.sb.block_size() as u64);
for logical in 0..n_blocks {
let phys = fs_ext4::extent::map_logical(
&dir_inode.block,
fs.dev.as_ref(),
fs.sb.block_size(),
logical,
)
.expect("map_logical through depth-1 leaf");
assert!(phys.is_some(), "logical {logical} resolves post-remount");
}
fs::remove_file(path).ok();
}
#[test]
fn verified_read_survives_promotion() {
let path = copy_to_tmp("ext4-basic.img");
let dev = FileDevice::open_rw(&path).expect("open rw");
let fs = Filesystem::mount(Arc::new(dev)).expect("mount");
let target = "/promo_dir";
fs.apply_mkdir(target, 0o755).expect("mkdir target");
create_until_promotion(&fs, target, 2000);
let canary = format!("{target}/ZZZ_canary.marker");
fs.apply_create(&canary, 0o644).expect("create canary");
assert!(
resolve(&fs, &canary).is_some(),
"canary resolves pre-remount"
);
drop(fs);
let dev = FileDevice::open(&path).expect("open ro");
let fs = Filesystem::mount(Arc::new(dev)).expect("remount");
assert!(
resolve(&fs, &canary).is_some(),
"canary resolves post-remount"
);
fs::remove_file(path).ok();
}