use fs_ext4::block_io::{BlockDevice, FileDevice};
use fs_ext4::capi::*;
use fs_ext4::Filesystem;
use std::ffi::CString;
use std::os::raw::{c_int, c_void};
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
const IMAGE: &str = "ext4-basic.img";
const EINVAL: c_int = 22;
const EIO: c_int = 5;
struct DevCtx {
bytes: Mutex<Vec<u8>>,
flushes: AtomicU64,
fail_next_write: AtomicBool,
}
extern "C" fn read_cb(ctx: *mut c_void, buf: *mut c_void, offset: u64, length: u64) -> c_int {
let dev = unsafe { &*(ctx as *const DevCtx) };
let bytes = dev.bytes.lock().unwrap();
let (off, len) = (offset as usize, length as usize);
if off + len > bytes.len() {
return 1;
}
unsafe { std::ptr::copy_nonoverlapping(bytes.as_ptr().add(off), buf as *mut u8, len) };
0
}
extern "C" fn write_cb(ctx: *mut c_void, buf: *const c_void, offset: u64, length: u64) -> c_int {
let dev = unsafe { &*(ctx as *const DevCtx) };
if dev.fail_next_write.swap(false, Ordering::SeqCst) {
return 1;
}
let mut bytes = dev.bytes.lock().unwrap();
let (off, len) = (offset as usize, length as usize);
if off + len > bytes.len() {
return 1;
}
unsafe { std::ptr::copy_nonoverlapping(buf as *const u8, bytes.as_mut_ptr().add(off), len) };
0
}
extern "C" fn flush_cb(ctx: *mut c_void) -> c_int {
let dev = unsafe { &*(ctx as *const DevCtx) };
dev.flushes.fetch_add(1, Ordering::SeqCst);
0
}
fn dev_from(bytes: Vec<u8>) -> Arc<DevCtx> {
Arc::new(DevCtx {
bytes: Mutex::new(bytes),
flushes: AtomicU64::new(0),
fail_next_write: AtomicBool::new(false),
})
}
fn fixture_bytes() -> Vec<u8> {
let path = fs_ext4_test_support::fixture(env!("CARGO_MANIFEST_DIR"), IMAGE);
std::fs::read(&path).unwrap_or_else(|e| panic!("read fixture {path}: {e}"))
}
fn cfg(dev: &Arc<DevCtx>) -> fs_ext4_blockdev_cfg_t {
fs_ext4_blockdev_cfg_t {
read: Some(read_cb),
context: Arc::as_ptr(dev) as *mut c_void,
size_bytes: dev.bytes.lock().unwrap().len() as u64,
block_size: 512,
write: Some(write_cb),
flush: Some(flush_cb),
}
}
fn mount_rw(dev: &Arc<DevCtx>) -> *mut fs_ext4_fs_t {
let c = cfg(dev);
let fs = unsafe { fs_ext4_mount_rw_with_callbacks(&c) };
assert!(!fs.is_null(), "rw mount: {}", last_error());
fs
}
fn last_error() -> String {
unsafe { std::ffi::CStr::from_ptr(fs_ext4_last_error()) }
.to_string_lossy()
.into_owned()
}
fn create_and_write(fs: *mut fs_ext4_fs_t, path: &str, data: &[u8]) -> Result<(), c_int> {
let p = CString::new(path).unwrap();
if unsafe { fs_ext4_create(fs, p.as_ptr(), 0o644) } == 0 {
return Err(fs_ext4_last_errno());
}
let n = unsafe {
fs_ext4_pwrite(
fs,
p.as_ptr(),
data.as_ptr() as *const c_void,
data.len() as u64,
0,
)
};
if n != data.len() as i64 {
return Err(fs_ext4_last_errno());
}
Ok(())
}
fn read_whole(fs: *mut fs_ext4_fs_t, path: &str, len: usize) -> Option<Vec<u8>> {
let p = CString::new(path).unwrap();
let mut buf = vec![0u8; len + 16];
let n = unsafe {
fs_ext4_read_file(
fs,
p.as_ptr(),
buf.as_mut_ptr() as *mut c_void,
0,
buf.len() as u64,
)
};
if n < 0 {
return None;
}
buf.truncate(n as usize);
Some(buf)
}
fn payload() -> Vec<u8> {
(0..10_000u32).map(|i| (i * 31 % 251) as u8).collect()
}
#[test]
fn flush_puts_every_change_on_the_device_without_unmounting() {
let dev = dev_from(fixture_bytes());
let fs = mount_rw(&dev);
let data = payload();
create_and_write(fs, "/flushed.bin", &data).expect("create + pwrite");
let before = dev.flushes.load(Ordering::SeqCst);
let rc = unsafe { fs_ext4_flush(fs) };
assert_eq!(rc, 0, "fs_ext4_flush: {}", last_error());
assert_eq!(fs_ext4_last_errno(), 0);
assert!(
dev.flushes.load(Ordering::SeqCst) > before,
"fs_ext4_flush did not reach the host's flush callback"
);
let copy = dev_from(dev.bytes.lock().unwrap().clone());
let c = cfg(©);
let ro = unsafe { fs_ext4_mount_with_callbacks(&c) };
assert!(!ro.is_null(), "ro mount of the copy: {}", last_error());
assert_eq!(
read_whole(ro, "/flushed.bin", data.len()).as_deref(),
Some(&data[..]),
"the device copy taken after flush does not hold the file"
);
unsafe { fs_ext4_umount(ro) };
create_and_write(fs, "/after.bin", b"still mounted").expect("write after flush");
assert_eq!(unsafe { fs_ext4_flush(fs) }, 0, "{}", last_error());
unsafe { fs_ext4_umount(fs) };
}
#[test]
fn flush_after_a_failed_write_is_eio() {
let dev = dev_from(fixture_bytes());
let fs = mount_rw(&dev);
create_and_write(fs, "/first.bin", b"first").expect("first write");
assert_eq!(unsafe { fs_ext4_flush(fs) }, 0, "{}", last_error());
dev.fail_next_write.store(true, Ordering::SeqCst);
assert!(
create_and_write(fs, "/second.bin", &payload()).is_err(),
"the mutation succeeded although its device write failed"
);
assert_eq!(
unsafe { fs_ext4_flush(fs) },
-1,
"flush after a failed write"
);
assert_eq!(fs_ext4_last_errno(), EIO, "{}", last_error());
unsafe { fs_ext4_umount(fs) };
}
#[test]
fn null_handles_are_einval() {
assert_eq!(unsafe { fs_ext4_flush(std::ptr::null_mut()) }, -1);
assert_eq!(fs_ext4_last_errno(), EINVAL);
assert_eq!(unsafe { fs_ext4_fresh_read(std::ptr::null_mut()) }, -1);
assert_eq!(fs_ext4_last_errno(), EINVAL);
}
#[test]
fn flush_on_a_read_only_mount_succeeds() {
let dev = dev_from(fixture_bytes());
let c = cfg(&dev);
let ro = unsafe { fs_ext4_mount_with_callbacks(&c) };
assert!(!ro.is_null(), "{}", last_error());
assert_eq!(unsafe { fs_ext4_flush(ro) }, 0, "{}", last_error());
unsafe { fs_ext4_umount(ro) };
}
#[test]
fn fresh_read_sees_what_the_host_wrote_underneath() {
let dev = dev_from(fixture_bytes());
let first = mount_rw(&dev);
let name = CString::new("/underneath.bin").unwrap();
let mut attr = unsafe { std::mem::zeroed::<fs_ext4_attr_t>() };
assert_eq!(unsafe { fs_ext4_stat(first, name.as_ptr(), &mut attr) }, -1);
assert_eq!(unsafe { fs_ext4_flush(first) }, 0, "{}", last_error());
let data = payload();
let second = mount_rw(&dev);
create_and_write(second, "/underneath.bin", &data).expect("second mount write");
unsafe { fs_ext4_umount(second) };
let rc = unsafe { fs_ext4_fresh_read(first) };
assert_eq!(rc, 0, "fs_ext4_fresh_read: {}", last_error());
assert_eq!(
read_whole(first, "/underneath.bin", data.len()).as_deref(),
Some(&data[..]),
"after fresh_read the mount does not see the device's current state"
);
unsafe { fs_ext4_umount(first) };
}
struct CutAfterDirtyJournal {
inner: Arc<dyn BlockDevice>,
armed: AtomicBool,
flushes: AtomicUsize,
}
impl BlockDevice for CutAfterDirtyJournal {
fn read_at(&self, offset: u64, buf: &mut [u8]) -> fs_ext4::error::Result<()> {
self.inner.read_at(offset, buf)
}
fn size_bytes(&self) -> u64 {
self.inner.size_bytes()
}
fn write_at(&self, offset: u64, buf: &[u8]) -> fs_ext4::error::Result<()> {
if self.armed.load(Ordering::SeqCst) && self.flushes.load(Ordering::SeqCst) >= 2 {
return Ok(());
}
self.inner.write_at(offset, buf)
}
fn flush(&self) -> fs_ext4::error::Result<()> {
if self.armed.load(Ordering::SeqCst) {
self.flushes.fetch_add(1, Ordering::SeqCst);
}
self.inner.flush()
}
fn is_writable(&self) -> bool {
true
}
}
#[test]
fn fresh_read_on_a_read_only_replay_is_eio_and_keeps_the_replayed_view() {
let image = fs_ext4_test_support::temp_path!("fs_ext4_capi_flush_{}.img", std::process::id());
std::fs::write(&image, fixture_bytes()).unwrap();
{
let dev = Arc::new(CutAfterDirtyJournal {
inner: Arc::new(FileDevice::open_rw(&image).unwrap()),
armed: AtomicBool::new(false),
flushes: AtomicUsize::new(0),
});
let fs = Filesystem::mount(dev.clone()).expect("mount rw");
assert!(fs.journal.is_some(), "the fixture must be journaled");
fs.apply_mkdir("/warmup", 0o755).expect("warm-up mkdir");
dev.armed.store(true, Ordering::SeqCst);
fs.apply_mkdir("/committed", 0o755).expect("mkdir");
}
let path = CString::new(image.as_str()).unwrap();
let committed = CString::new("/committed").unwrap();
let mut attr = unsafe { std::mem::zeroed::<fs_ext4_attr_t>() };
let ro = unsafe { fs_ext4_mount(path.as_ptr()) };
assert!(!ro.is_null(), "{}", last_error());
assert_eq!(
unsafe { fs_ext4_stat(ro, committed.as_ptr(), &mut attr) },
0,
"the read-only mount did not replay the committed mkdir"
);
assert_eq!(unsafe { fs_ext4_fresh_read(ro) }, -1);
assert_eq!(fs_ext4_last_errno(), EIO, "{}", last_error());
assert_eq!(
unsafe { fs_ext4_stat(ro, committed.as_ptr(), &mut attr) },
0,
"the refused fresh_read discarded the replayed view"
);
unsafe { fs_ext4_umount(ro) };
let _ = std::fs::remove_file(&image);
}