use fs_ext4::block_io::{BlockDevice, FileDevice};
use fs_ext4::error::Result;
use fs_ext4::fs::Filesystem;
use fs_ext4::inode::InodeFlags;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
fn run(tool: &str, args: &[&str]) -> (Option<i32>, String) {
let out = fs_ext4_test_support::oracle(tool).args(args).output();
(
out.status.code(),
format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
),
)
}
fn indexed_volume(tag: &str, names: usize) -> String {
let root = fs_ext4_test_support::temp_path!("fs_ext4_split_cut_{tag}_{}", std::process::id());
let bigdir = std::path::Path::new(&root).join("bigdir");
std::fs::create_dir_all(&bigdir).unwrap();
for i in 0..names {
std::fs::write(bigdir.join(format!("existing_file_{i:05}")), b"").unwrap();
}
let image = format!("{root}.img");
std::fs::File::create(&image)
.and_then(|f| f.set_len(64 * 1024 * 1024))
.unwrap();
let (code, log) = run(
"mkfs.ext4",
&[
"-q",
"-F",
"-b",
"1024",
"-O",
"metadata_csum,has_journal",
"-d",
&root,
&image,
],
);
assert_eq!(code, Some(0), "mkfs.ext4: {log}");
fs_ext4_test_support::oracle("e2fsck")
.args(["-fyD", &image])
.judged()
.repaired("e2fsck -fyD");
let _ = std::fs::remove_dir_all(&root);
image
}
struct CutDevice {
inner: FileDevice,
budget: AtomicUsize,
writes: AtomicUsize,
}
impl BlockDevice for CutDevice {
fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
self.inner.read_at(offset, buf)
}
fn size_bytes(&self) -> u64 {
self.inner.size_bytes()
}
fn write_at(&self, offset: u64, buf: &[u8]) -> Result<()> {
let n = self.writes.fetch_add(1, Ordering::SeqCst);
if n >= self.budget.load(Ordering::SeqCst) {
return Ok(());
}
self.inner.write_at(offset, buf)
}
fn flush(&self) -> Result<()> {
self.inner.flush()
}
fn is_writable(&self) -> bool {
true
}
}
fn index_shape(fs: &Filesystem) -> Option<(u8, u16)> {
let mut reader = |ino: u32| fs.read_inode_verified(ino).map(|(inode, _)| inode);
let ino = fs_ext4::path::lookup(fs.dev.as_ref(), &fs.sb, &mut reader, "/bigdir").unwrap();
let (inode, _) = fs.read_inode_verified(ino).unwrap();
if inode.flags & InodeFlags::INDEX.bits() == 0 {
return None;
}
let root = fs
.read_block(fs.map_inode_logical(&inode, 0).unwrap().unwrap())
.unwrap();
Some((root[30], u16::from_le_bytes([root[34], root[35]])))
}
#[derive(Clone, Copy, PartialEq, Debug)]
enum Effect {
Cut,
Unchanged,
Split,
Dropped,
}
fn create_with_cut(fixture: &str, name: &str, cut: usize, tag: &str) -> (String, usize, Effect) {
let image = format!("{fixture}.{tag}");
std::fs::copy(fixture, &image).unwrap();
let device = Arc::new(CutDevice {
inner: FileDevice::open_rw(&image).unwrap(),
budget: AtomicUsize::new(usize::MAX),
writes: AtomicUsize::new(0),
});
let fs = Filesystem::mount(device.clone()).expect("mount");
let before = index_shape(&fs);
let start = device.writes.load(Ordering::SeqCst);
device
.budget
.store(start.saturating_add(cut), Ordering::SeqCst);
let created = fs.apply_create(name, 0o644);
let issued = device.writes.load(Ordering::SeqCst) - start;
let effect = if cut == usize::MAX {
created.expect("create");
match (before, index_shape(&fs)) {
(Some(_), None) => Effect::Dropped,
(Some((0, was)), Some((0, now))) if now > was => Effect::Split,
_ => Effect::Unchanged,
}
} else {
Effect::Cut
};
drop(fs);
(image, issued, effect)
}
fn rejected_cuts(fixture: &str, name: &str, writes: usize) -> Vec<String> {
let mut rejected = Vec::new();
for cut in 0..=writes {
let (image, _, _) = create_with_cut(fixture, name, cut, &format!("cut{cut}"));
drop(Filesystem::mount(Arc::new(FileDevice::open_rw(&image).unwrap())).expect("remount"));
let judged = fs_ext4_test_support::oracle("e2fsck")
.args(["-fn", &image])
.judged();
match judged.verdict {
fs_ext4_test_support::Verdict::Clean => {}
fs_ext4_test_support::Verdict::Findings(report) => {
rejected.push(format!("--- cut after write {cut} of {writes}:\n{report}"));
}
fs_ext4_test_support::Verdict::NotAVerdict(why) => {
rejected.push(format!(
"--- cut after write {cut} of {writes}: not a verdict: {why}"
));
}
}
let _ = std::fs::remove_file(image);
}
rejected
}
#[test]
fn every_write_cut_inside_an_htree_leaf_split_leaves_a_clean_volume() {
let fixture = indexed_volume("csum", 600);
let (name, writes) = (0..800)
.find_map(|i| {
let name = format!("/bigdir/a_longer_name_to_fill_leaves_{i:05}");
let (image, writes, effect) = create_with_cut(&fixture, &name, usize::MAX, "probe");
if effect == Effect::Split {
let _ = std::fs::remove_file(image);
Some((name, writes))
} else {
assert_eq!(effect, Effect::Unchanged, "fixture: {name} did not split");
std::fs::rename(image, &fixture).unwrap();
None
}
})
.expect("fixture: 800 creates split no leaf");
assert!(writes > 0, "the split issued no writes");
let rejected = rejected_cuts(&fixture, &name, writes);
let _ = std::fs::remove_file(&fixture);
assert!(
rejected.is_empty(),
"e2fsck rejected {} of {} cut points:\n{}",
rejected.len(),
writes + 1,
rejected.join("\n")
);
}
#[test]
fn every_write_cut_inside_an_htree_index_drop_leaves_a_clean_volume() {
let fixture = indexed_volume("drop", 6000);
let name = |i: usize| format!("/bigdir/a_longer_name_to_fill_leaves_{i:05}");
let probe = format!("{fixture}.probe");
std::fs::copy(&fixture, &probe).unwrap();
let drops_at = {
let fs = Filesystem::mount(Arc::new(FileDevice::open_rw(&probe).unwrap())).unwrap();
assert_eq!(
index_shape(&fs).map(|(levels, _)| levels),
Some(1),
"fixture: expected a two-level index"
);
(0..3000)
.find(|&i| {
fs.apply_create(&name(i), 0o644).expect("create");
index_shape(&fs).is_none()
})
.expect("fixture: 3000 creates never dropped the index")
};
let _ = std::fs::remove_file(&probe);
{
let fs = Filesystem::mount(Arc::new(FileDevice::open_rw(&fixture).unwrap())).unwrap();
for i in 0..drops_at {
fs.apply_create(&name(i), 0o644).expect("create");
}
}
let (image, writes, effect) = create_with_cut(&fixture, &name(drops_at), usize::MAX, "probe");
let _ = std::fs::remove_file(image);
assert_eq!(
effect,
Effect::Dropped,
"fixture: the create did not drop the index"
);
assert!(writes > 0, "the drop issued no writes");
let rejected = rejected_cuts(&fixture, &name(drops_at), writes);
let _ = std::fs::remove_file(&fixture);
assert!(
rejected.is_empty(),
"e2fsck rejected {} of {} cut points:\n{}",
rejected.len(),
writes + 1,
rejected.join("\n")
);
}