use fs_ext4::capi::*;
use std::ffi::{CStr, CString};
use std::fs;
use std::io::Write;
use std::os::raw::c_void;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU32, Ordering};
const SRC: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/test-disks/ext4-basic.img");
fn scratch() -> PathBuf {
static COUNTER: AtomicU32 = AtomicU32::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let dst = PathBuf::from(format!(
"/tmp/fs_ext4_capi_wf_alloc_{}_{n}.img",
std::process::id()
));
let mut out = fs::File::create(&dst).unwrap();
out.write_all(&fs::read(SRC).unwrap()).unwrap();
dst
}
fn last_err() -> String {
unsafe {
CStr::from_ptr(fs_ext4_last_error())
.to_string_lossy()
.into_owned()
}
}
fn free_blocks(fs_h: *mut fs_ext4_fs_t) -> u64 {
let mut info: fs_ext4_volume_info_t = unsafe { std::mem::zeroed() };
let rc = unsafe { fs_ext4_get_volume_info(fs_h, &mut info) };
assert_eq!(rc, 0, "get_volume_info: {}", last_err());
info.free_blocks
}
#[test]
fn growing_write_file_does_not_leak_blocks() {
let img = scratch();
let img_c = CString::new(img.to_str().unwrap()).unwrap();
let path_c = CString::new("/test.txt").unwrap();
let payload: Vec<u8> = (0..128 * 1024).map(|i| (i & 0xFF) as u8).collect();
let fs_h = unsafe { fs_ext4_mount_rw(img_c.as_ptr()) };
assert!(!fs_h.is_null());
let before = free_blocks(fs_h);
let rc = unsafe {
fs_ext4_write_file(
fs_h,
path_c.as_ptr(),
payload.as_ptr() as *const c_void,
payload.len() as u64,
)
};
assert_eq!(rc, payload.len() as i64, "write: {}", last_err());
let after = free_blocks(fs_h);
assert!(
after <= before,
"free_blocks must not INCREASE after a grow write: before={before} after={after}"
);
eprintln!(
"free_blocks: before={before} after={after} delta={}",
before - after
);
unsafe { fs_ext4_umount(fs_h) };
let _ = fs::remove_file(&img);
}
#[test]
fn truncate_to_zero_then_write_recovers_blocks() {
let img = scratch();
let img_c = CString::new(img.to_str().unwrap()).unwrap();
let path_c = CString::new("/test.txt").unwrap();
let fs_h = unsafe { fs_ext4_mount_rw(img_c.as_ptr()) };
assert!(!fs_h.is_null());
let payload: Vec<u8> = vec![0x42u8; 64 * 1024];
let n = unsafe {
fs_ext4_write_file(
fs_h,
path_c.as_ptr(),
payload.as_ptr() as *const c_void,
payload.len() as u64,
)
};
assert_eq!(n, payload.len() as i64);
let after_grow = free_blocks(fs_h);
let rc = unsafe { fs_ext4_truncate(fs_h, path_c.as_ptr(), 0) };
assert_eq!(rc, 0, "truncate: {}", last_err());
let after_shrink = free_blocks(fs_h);
assert!(
after_shrink >= after_grow,
"shrink should not decrease free: grow={after_grow} shrink={after_shrink}"
);
unsafe { fs_ext4_umount(fs_h) };
let _ = fs::remove_file(&img);
}