use fs_ext4::capi::*;
use std::ffi::{CStr, CString};
use std::os::raw::c_void;
const TEST_IMAGE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/test-disks/ext4-basic.img");
fn last_err_str() -> String {
unsafe {
let p = fs_ext4_last_error();
if p.is_null() {
return "<null>".into();
}
CStr::from_ptr(p).to_string_lossy().into_owned()
}
}
#[test]
fn mount_and_umount_basic_image() {
let path = CString::new(TEST_IMAGE).unwrap();
let fs = unsafe { fs_ext4_mount(path.as_ptr()) };
assert!(!fs.is_null(), "mount returned NULL: {}", last_err_str());
unsafe { fs_ext4_umount(fs) };
}
#[test]
fn mount_rejects_missing_file() {
let path = CString::new("/tmp/definitely-does-not-exist-xyz").unwrap();
let fs = unsafe { fs_ext4_mount(path.as_ptr()) };
assert!(fs.is_null(), "mount should have failed");
let err = last_err_str();
assert!(
err.contains("open") || err.contains("No such"),
"err was: {err}"
);
}
#[test]
fn volume_info_reports_expected_fields() {
let path = CString::new(TEST_IMAGE).unwrap();
let fs = unsafe { fs_ext4_mount(path.as_ptr()) };
assert!(!fs.is_null(), "mount failed: {}", last_err_str());
let mut info = unsafe { std::mem::zeroed::<fs_ext4_volume_info_t>() };
let rc = unsafe { fs_ext4_get_volume_info(fs, &mut info) };
assert_eq!(rc, 0, "get_volume_info failed: {}", last_err_str());
assert_eq!(info.block_size, 4096, "expected 4KB blocks");
assert!(info.total_blocks > 0, "total_blocks should be > 0");
assert!(info.total_inodes > 0, "total_inodes should be > 0");
assert_eq!(info.mounted_dirty, 0, "fresh image should be clean");
unsafe { fs_ext4_umount(fs) };
}
#[test]
fn volume_info_flags_dirty_image() {
use std::fs;
use std::io::{Read, Seek, SeekFrom, Write};
let src = concat!(env!("CARGO_MANIFEST_DIR"), "/test-disks/ext4-no-csum.img");
let tmp = std::env::temp_dir().join("fs_ext4-dirty-fixture.img");
fs::copy(src, &tmp).expect("copy no-csum image");
{
let mut f = fs::OpenOptions::new()
.read(true)
.write(true)
.open(&tmp)
.expect("open copy rw");
f.seek(SeekFrom::Start(1024 + 0x3A)).unwrap();
let mut cur = [0u8; 2];
f.read_exact(&mut cur).unwrap();
assert_eq!(u16::from_le_bytes(cur), 1, "fixture preconditions");
f.seek(SeekFrom::Start(1024 + 0x3A)).unwrap();
f.write_all(&0u16.to_le_bytes()).unwrap();
}
let path = CString::new(tmp.to_str().unwrap()).unwrap();
let fs = unsafe { fs_ext4_mount(path.as_ptr()) };
assert!(
!fs.is_null(),
"dirty image should still mount read-only: {}",
last_err_str()
);
let mut info = unsafe { std::mem::zeroed::<fs_ext4_volume_info_t>() };
let rc = unsafe { fs_ext4_get_volume_info(fs, &mut info) };
assert_eq!(rc, 0, "get_volume_info on dirty fs failed");
assert_eq!(
info.mounted_dirty, 1,
"flipped s_state should surface as dirty"
);
unsafe { fs_ext4_umount(fs) };
fs::remove_file(&tmp).ok();
}
#[test]
fn stat_root_returns_directory() {
let path = CString::new(TEST_IMAGE).unwrap();
let fs = unsafe { fs_ext4_mount(path.as_ptr()) };
assert!(!fs.is_null(), "mount failed: {}", last_err_str());
let root = CString::new("/").unwrap();
let mut attr = unsafe { std::mem::zeroed::<fs_ext4_attr_t>() };
let rc = unsafe { fs_ext4_stat(fs, root.as_ptr(), &mut attr) };
assert_eq!(rc, 0, "stat / failed: {}", last_err_str());
assert_eq!(attr.inode, 2, "root inode should be 2");
assert!(attr.link_count >= 2, "root dir should have link_count >= 2");
let ft = attr.file_type as u32;
assert_eq!(ft, fs_ext4_file_type_t::Dir as u32, "root file_type != Dir");
unsafe { fs_ext4_umount(fs) };
}
#[test]
fn null_inputs_return_error_not_crash() {
unsafe {
let fs = fs_ext4_mount(std::ptr::null());
assert!(fs.is_null());
let mut info = std::mem::zeroed::<fs_ext4_volume_info_t>();
let rc = fs_ext4_get_volume_info(std::ptr::null_mut(), &mut info);
assert_eq!(rc, -1);
let mut attr = std::mem::zeroed::<fs_ext4_attr_t>();
let rc = fs_ext4_stat(std::ptr::null_mut(), std::ptr::null(), &mut attr);
assert_eq!(rc, -1);
}
}
#[test]
fn dir_open_root_lists_entries() {
let path = CString::new(TEST_IMAGE).unwrap();
let fs = unsafe { fs_ext4_mount(path.as_ptr()) };
assert!(!fs.is_null(), "mount failed: {}", last_err_str());
let root = CString::new("/").unwrap();
let iter = unsafe { fs_ext4_dir_open(fs, root.as_ptr()) };
assert!(!iter.is_null(), "dir_open / failed: {}", last_err_str());
let mut names = Vec::new();
loop {
let de = unsafe { fs_ext4_dir_next(iter) };
if de.is_null() {
break;
}
let name_ptr = unsafe { &(*de).name[0] as *const _ };
let name = unsafe { CStr::from_ptr(name_ptr).to_string_lossy().into_owned() };
names.push(name);
}
unsafe { fs_ext4_dir_close(iter) };
assert!(
names.iter().any(|n| n == "."),
"missing '.', got: {:?}",
names
);
assert!(
names.iter().any(|n| n == ".."),
"missing '..', got: {:?}",
names
);
assert!(!names.is_empty(), "no entries returned");
unsafe { fs_ext4_umount(fs) };
}
#[test]
fn stat_non_root_path() {
let path = CString::new(TEST_IMAGE).unwrap();
let fs = unsafe { fs_ext4_mount(path.as_ptr()) };
assert!(!fs.is_null());
let root = CString::new("/").unwrap();
let iter = unsafe { fs_ext4_dir_open(fs, root.as_ptr()) };
assert!(!iter.is_null());
let mut found_file: Option<String> = None;
loop {
let de = unsafe { fs_ext4_dir_next(iter) };
if de.is_null() {
break;
}
if unsafe { (*de).file_type } == 1 {
let name_ptr = unsafe { &(*de).name[0] as *const _ };
let name = unsafe { CStr::from_ptr(name_ptr).to_string_lossy().into_owned() };
found_file = Some(name);
break;
}
}
unsafe { fs_ext4_dir_close(iter) };
if let Some(name) = found_file {
let p = CString::new(format!("/{}", name)).unwrap();
let mut attr = unsafe { std::mem::zeroed::<fs_ext4_attr_t>() };
let rc = unsafe { fs_ext4_stat(fs, p.as_ptr(), &mut attr) };
assert_eq!(rc, 0, "stat /{} failed: {}", name, last_err_str());
assert_eq!(attr.file_type as u32, fs_ext4_file_type_t::RegFile as u32);
}
unsafe { fs_ext4_umount(fs) };
}
#[test]
fn stat_missing_path_returns_error() {
let path = CString::new(TEST_IMAGE).unwrap();
let fs = unsafe { fs_ext4_mount(path.as_ptr()) };
assert!(!fs.is_null());
let missing = CString::new("/definitely-not-there-xyz-987").unwrap();
let mut attr = unsafe { std::mem::zeroed::<fs_ext4_attr_t>() };
let rc = unsafe { fs_ext4_stat(fs, missing.as_ptr(), &mut attr) };
assert_eq!(rc, -1);
assert!(last_err_str().contains("not found") || last_err_str().contains("stat"));
unsafe { fs_ext4_umount(fs) };
}
#[test]
fn read_file_returns_expected_content() {
let path = CString::new(TEST_IMAGE).unwrap();
let fs = unsafe { fs_ext4_mount(path.as_ptr()) };
assert!(!fs.is_null());
let file_path = CString::new("/test.txt").unwrap();
let mut buf = [0u8; 256];
let n = unsafe {
fs_ext4_read_file(
fs,
file_path.as_ptr(),
buf.as_mut_ptr() as *mut c_void,
0,
buf.len() as u64,
)
};
if n > 0 {
let content = std::str::from_utf8(&buf[..n as usize]).unwrap_or("");
println!("/test.txt content: {:?} ({} bytes)", content, n);
assert!(
content.contains("hello"),
"expected 'hello' in {:?}",
content
);
} else {
eprintln!("skip: read_file returned {n}: {}", last_err_str());
}
unsafe { fs_ext4_umount(fs) };
}