#![allow(non_camel_case_types)]
#![allow(clippy::missing_safety_doc)]
use crate::block_io::{BlockDevice, CallbackDevice, FileDevice};
use crate::dir::{self, DirBlockIter, DirEntryType};
use crate::error::errno::{EINVAL, EISDIR, ENAMETOOLONG, ENOENT, ENOSYS, ENOTDIR};
use crate::error::{Error, Result};
use crate::extent;
use crate::features;
use crate::file_io;
use crate::fs::Filesystem;
use crate::inode::{Inode, S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK};
use crate::path as path_mod;
use crate::xattr;
use std::cell::RefCell;
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int, c_void};
use std::panic::AssertUnwindSafe;
use std::sync::Arc;
thread_local! {
static LAST_ERROR: RefCell<CString> = RefCell::new(CString::new("").unwrap());
static LAST_ERRNO: RefCell<c_int> = const { RefCell::new(0) };
}
fn set_last_error<E: std::fmt::Display>(e: E) {
let msg = format!("{e}");
LAST_ERROR.with(|cell| {
*cell.borrow_mut() =
CString::new(msg).unwrap_or_else(|_| CString::new("unknown error").unwrap());
});
}
fn set_last_errno(errno: c_int) {
LAST_ERRNO.with(|cell| *cell.borrow_mut() = errno);
}
fn set_err_from(err: &Error, context: &str) {
set_last_error(format!("{context}: {err}"));
set_last_errno(err.to_errno());
}
fn set_err_msg(msg: &str, errno: c_int) {
set_last_error(msg);
set_last_errno(errno);
}
fn clear_last_error() {
LAST_ERROR.with(|cell| {
*cell.borrow_mut() = CString::new("").unwrap();
});
LAST_ERRNO.with(|cell| *cell.borrow_mut() = 0);
}
fn ffi_guard<T>(fail: T, body: impl FnOnce() -> T + std::panic::UnwindSafe) -> T {
match std::panic::catch_unwind(body) {
Ok(v) => v,
Err(panic) => {
let msg = if let Some(s) = panic.downcast_ref::<&'static str>() {
format!("panic: {s}")
} else if let Some(s) = panic.downcast_ref::<String>() {
format!("panic: {s}")
} else {
"panic: (non-string payload)".to_string()
};
set_err_msg(&msg, crate::error::errno::EIO);
fail
}
}
}
#[no_mangle]
pub extern "C" fn fs_ext4_last_error() -> *const c_char {
LAST_ERROR.with(|cell| cell.borrow().as_ptr())
}
#[no_mangle]
pub extern "C" fn fs_ext4_last_errno() -> c_int {
LAST_ERRNO.with(|cell| *cell.borrow())
}
#[repr(C)]
#[derive(Copy, Clone, Debug)]
pub enum fs_ext4_file_type_t {
Unknown = 0,
RegFile = 1,
Dir = 2,
ChrDev = 3,
BlkDev = 4,
Fifo = 5,
Sock = 6,
Symlink = 7,
}
#[repr(C)]
pub struct fs_ext4_attr_t {
pub inode: u32,
pub mode: u16,
pub uid: u32,
pub gid: u32,
pub size: u64,
pub atime: i64,
pub mtime: i64,
pub ctime: i64,
pub crtime: i64,
pub link_count: u16,
pub file_type: fs_ext4_file_type_t,
pub atime_nsec: u32,
pub mtime_nsec: u32,
pub ctime_nsec: u32,
pub crtime_nsec: u32,
pub inode_flags: u32,
pub generation: u32,
pub blocks_512: u64,
}
#[repr(C)]
pub struct fs_ext4_dirent_t {
pub inode: u32,
pub file_type: u8,
pub name_len: u8,
pub name: [c_char; 256],
}
#[repr(C)]
pub struct fs_ext4_volume_info_t {
pub volume_name: [c_char; 16],
pub uuid: [u8; 16],
pub last_mounted: [c_char; 64],
pub block_size: u32,
pub total_blocks: u64,
pub free_blocks: u64,
pub reserved_blocks: u64,
pub total_inodes: u32,
pub free_inodes: u32,
pub inode_size: u16,
pub first_inode: u32,
pub blocks_per_group: u32,
pub inodes_per_group: u32,
pub creator_os: u32,
pub rev_level: u32,
pub minor_rev_level: u16,
pub feature_compat: u32,
pub feature_incompat: u32,
pub feature_ro_compat: u32,
pub desc_size: u16,
pub default_hash_version: u8,
pub state: u16,
pub errors_behavior: u16,
pub last_mount_time: u32,
pub last_write_time: u32,
pub last_check_time: u32,
pub check_interval: u32,
pub mount_count: u16,
pub max_mount_count: u16,
pub def_resuid: u16,
pub def_resgid: u16,
pub mounted_dirty: u8,
}
pub type fs_ext4_read_fn = Option<
unsafe extern "C" fn(context: *mut c_void, buf: *mut c_void, offset: u64, length: u64) -> c_int,
>;
pub type fs_ext4_write_fn = Option<
unsafe extern "C" fn(
context: *mut c_void,
buf: *const c_void,
offset: u64,
length: u64,
) -> c_int,
>;
pub type fs_ext4_flush_fn = Option<unsafe extern "C" fn(context: *mut c_void) -> c_int>;
#[repr(C)]
pub struct fs_ext4_blockdev_cfg_t {
pub read: fs_ext4_read_fn,
pub context: *mut c_void,
pub size_bytes: u64,
pub block_size: u32,
pub write: fs_ext4_write_fn,
pub flush: fs_ext4_flush_fn,
}
pub struct fs_ext4_fs_t {
fs: Filesystem,
}
pub struct fs_ext4_dir_iter_t {
entries: Vec<fs_ext4_dirent_t>,
position: usize,
current: fs_ext4_dirent_t,
}
pub(crate) const FFI_PATH_MAX: usize = 4096;
unsafe fn cstr_to_str<'a>(p: *const c_char) -> &'a str {
if p.is_null() {
return "";
}
let cstr = CStr::from_ptr(p);
if cstr.to_bytes().len() > FFI_PATH_MAX {
return "";
}
cstr.to_str().unwrap_or("")
}
#[allow(dead_code)]
unsafe fn cstr_to_str_strict<'a>(p: *const c_char) -> std::result::Result<&'a str, &'static str> {
if p.is_null() {
return Err("null pointer");
}
let cstr = CStr::from_ptr(p);
if cstr.to_bytes().len() > FFI_PATH_MAX {
return Err("string exceeds FFI_PATH_MAX");
}
cstr.to_str().map_err(|_| "invalid UTF-8")
}
fn mode_to_file_type(mode: u16) -> fs_ext4_file_type_t {
match mode & S_IFMT {
S_IFREG => fs_ext4_file_type_t::RegFile,
S_IFDIR => fs_ext4_file_type_t::Dir,
S_IFLNK => fs_ext4_file_type_t::Symlink,
S_IFCHR => fs_ext4_file_type_t::ChrDev,
S_IFBLK => fs_ext4_file_type_t::BlkDev,
S_IFIFO => fs_ext4_file_type_t::Fifo,
S_IFSOCK => fs_ext4_file_type_t::Sock,
_ => fs_ext4_file_type_t::Unknown,
}
}
fn fill_attr(out: &mut fs_ext4_attr_t, ino: u32, inode: &Inode) {
out.inode = ino;
out.mode = inode.mode & 0x0FFF; out.uid = inode.uid;
out.gid = inode.gid;
out.size = inode.size;
out.atime = inode.atime;
out.mtime = inode.mtime;
out.ctime = inode.ctime;
out.crtime = inode.crtime;
out.link_count = inode.links_count;
out.file_type = mode_to_file_type(inode.mode);
out.atime_nsec = inode.atime_nsec;
out.mtime_nsec = inode.mtime_nsec;
out.ctime_nsec = inode.ctime_nsec;
out.crtime_nsec = inode.crtime_nsec;
out.inode_flags = inode.flags;
out.generation = inode.generation;
out.blocks_512 = inode.blocks;
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_mount(device_path: *const c_char) -> *mut fs_ext4_fs_t {
ffi_guard(
std::ptr::null_mut(),
AssertUnwindSafe(|| {
clear_last_error();
let path = cstr_to_str(device_path);
if path.is_empty() {
set_err_msg("null or empty device_path", EINVAL);
return std::ptr::null_mut();
}
let dev = match FileDevice::open(path) {
Ok(d) => Arc::new(d) as Arc<dyn BlockDevice>,
Err(e) => {
set_err_from(&e, &format!("open {path}"));
return std::ptr::null_mut();
}
};
match Filesystem::mount(dev) {
Ok(fs) => Box::into_raw(Box::new(fs_ext4_fs_t { fs })),
Err(e) => {
set_err_from(&e, &format!("mount {path}"));
std::ptr::null_mut()
}
}
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_mount_with_callbacks(
cfg: *const fs_ext4_blockdev_cfg_t,
) -> *mut fs_ext4_fs_t {
ffi_guard(
std::ptr::null_mut(),
AssertUnwindSafe(|| mount_with_callbacks_inner(cfg)),
)
}
unsafe fn mount_with_callbacks_inner(cfg: *const fs_ext4_blockdev_cfg_t) -> *mut fs_ext4_fs_t {
clear_last_error();
if cfg.is_null() {
set_err_msg("null cfg", EINVAL);
return std::ptr::null_mut();
}
let cfg = &*cfg;
let Some(read_fn) = cfg.read else {
set_err_msg("cfg.read is null", EINVAL);
return std::ptr::null_mut();
};
let ctx_addr = cfg.context as usize;
let size = cfg.size_bytes;
let dev = CallbackDevice {
size,
read: Box::new(move |offset, buf| {
let rc = unsafe {
read_fn(
ctx_addr as *mut c_void,
buf.as_mut_ptr() as *mut c_void,
offset,
buf.len() as u64,
)
};
if rc != 0 {
Err(std::io::Error::other(format!("callback returned {rc}")))
} else {
Ok(())
}
}),
write: None,
flush: None,
};
match Filesystem::mount(Arc::new(dev) as Arc<dyn BlockDevice>) {
Ok(fs) => Box::into_raw(Box::new(fs_ext4_fs_t { fs })),
Err(e) => {
set_err_from(&e, "mount (callback)");
std::ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_mount_with_fs_core_device(
handle: *mut fs_core::ffi::FsCoreDevice,
) -> *mut fs_ext4_fs_t {
ffi_guard(
std::ptr::null_mut(),
AssertUnwindSafe(|| {
clear_last_error();
if handle.is_null() {
set_err_msg("null fs_core handle", EINVAL);
return std::ptr::null_mut();
}
let inner = (*handle).inner().clone();
let adapter = crate::fs_core_bridge::CoreDevice::new(inner);
let dev: Arc<dyn BlockDevice> = Arc::new(adapter);
match Filesystem::mount(dev) {
Ok(fs) => Box::into_raw(Box::new(fs_ext4_fs_t { fs })),
Err(e) => {
set_err_from(&e, "mount via fs_core handle");
std::ptr::null_mut()
}
}
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_mount_with_fs_core_device_lazy(
handle: *mut fs_core::ffi::FsCoreDevice,
) -> *mut fs_ext4_fs_t {
ffi_guard(
std::ptr::null_mut(),
AssertUnwindSafe(|| {
clear_last_error();
if handle.is_null() {
set_err_msg("null fs_core handle", EINVAL);
return std::ptr::null_mut();
}
let inner = (*handle).inner().clone();
let adapter = crate::fs_core_bridge::CoreDevice::new(inner);
let dev: Arc<dyn BlockDevice> = Arc::new(adapter);
match Filesystem::mount_lazy(dev) {
Ok(fs) => Box::into_raw(Box::new(fs_ext4_fs_t { fs })),
Err(e) => {
set_err_from(&e, "mount_lazy via fs_core handle");
std::ptr::null_mut()
}
}
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_mount_rw_with_callbacks(
cfg: *const fs_ext4_blockdev_cfg_t,
) -> *mut fs_ext4_fs_t {
ffi_guard(
std::ptr::null_mut(),
AssertUnwindSafe(|| mount_rw_with_callbacks_inner(cfg)),
)
}
unsafe fn mount_rw_with_callbacks_inner(cfg: *const fs_ext4_blockdev_cfg_t) -> *mut fs_ext4_fs_t {
clear_last_error();
if cfg.is_null() {
set_err_msg("null cfg", EINVAL);
return std::ptr::null_mut();
}
let cfg = &*cfg;
let Some(read_fn) = cfg.read else {
set_err_msg("cfg.read is null", EINVAL);
return std::ptr::null_mut();
};
let Some(write_fn) = cfg.write else {
set_err_msg("cfg.write is null (required for RW callback mount)", EINVAL);
return std::ptr::null_mut();
};
let flush_fn = cfg.flush;
let ctx_addr = cfg.context as usize;
let size = cfg.size_bytes;
let read_closure = move |offset: u64, buf: &mut [u8]| -> std::io::Result<()> {
let rc = unsafe {
read_fn(
ctx_addr as *mut c_void,
buf.as_mut_ptr() as *mut c_void,
offset,
buf.len() as u64,
)
};
if rc != 0 {
Err(std::io::Error::other(format!(
"read callback returned {rc}"
)))
} else {
Ok(())
}
};
let write_closure = move |offset: u64, buf: &[u8]| -> std::io::Result<()> {
let rc = unsafe {
write_fn(
ctx_addr as *mut c_void,
buf.as_ptr() as *const c_void,
offset,
buf.len() as u64,
)
};
if rc != 0 {
Err(std::io::Error::other(format!(
"write callback returned {rc}"
)))
} else {
Ok(())
}
};
let flush_closure: Option<crate::block_io::FlushCb> = flush_fn.map(|f| {
let cb: crate::block_io::FlushCb = Box::new(move || -> std::io::Result<()> {
let rc = unsafe { f(ctx_addr as *mut c_void) };
if rc != 0 {
Err(std::io::Error::other(format!(
"flush callback returned {rc}"
)))
} else {
Ok(())
}
});
cb
});
let dev = CallbackDevice {
size,
read: Box::new(read_closure),
write: Some(Box::new(write_closure)),
flush: flush_closure,
};
match Filesystem::mount(Arc::new(dev) as Arc<dyn BlockDevice>) {
Ok(fs) => Box::into_raw(Box::new(fs_ext4_fs_t { fs })),
Err(e) => {
set_err_from(&e, "mount_rw (callback)");
std::ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_mount_rw_with_callbacks_lazy(
cfg: *const fs_ext4_blockdev_cfg_t,
) -> *mut fs_ext4_fs_t {
ffi_guard(
std::ptr::null_mut(),
AssertUnwindSafe(|| mount_rw_with_callbacks_lazy_inner(cfg)),
)
}
unsafe fn mount_rw_with_callbacks_lazy_inner(
cfg: *const fs_ext4_blockdev_cfg_t,
) -> *mut fs_ext4_fs_t {
clear_last_error();
if cfg.is_null() {
set_err_msg("null cfg", EINVAL);
return std::ptr::null_mut();
}
let cfg = &*cfg;
let Some(read_fn) = cfg.read else {
set_err_msg("cfg.read is null", EINVAL);
return std::ptr::null_mut();
};
let Some(write_fn) = cfg.write else {
set_err_msg("cfg.write is null (required for RW callback mount)", EINVAL);
return std::ptr::null_mut();
};
let flush_fn = cfg.flush;
let ctx_addr = cfg.context as usize;
let size = cfg.size_bytes;
let read_closure = move |offset: u64, buf: &mut [u8]| -> std::io::Result<()> {
let rc = unsafe {
read_fn(
ctx_addr as *mut c_void,
buf.as_mut_ptr() as *mut c_void,
offset,
buf.len() as u64,
)
};
if rc != 0 {
Err(std::io::Error::other(format!(
"read callback returned {rc}"
)))
} else {
Ok(())
}
};
let write_closure = move |offset: u64, buf: &[u8]| -> std::io::Result<()> {
let rc = unsafe {
write_fn(
ctx_addr as *mut c_void,
buf.as_ptr() as *const c_void,
offset,
buf.len() as u64,
)
};
if rc != 0 {
Err(std::io::Error::other(format!(
"write callback returned {rc}"
)))
} else {
Ok(())
}
};
let flush_closure: Option<crate::block_io::FlushCb> = flush_fn.map(|f| {
let cb: crate::block_io::FlushCb = Box::new(move || -> std::io::Result<()> {
let rc = unsafe { f(ctx_addr as *mut c_void) };
if rc != 0 {
Err(std::io::Error::other(format!(
"flush callback returned {rc}"
)))
} else {
Ok(())
}
});
cb
});
let dev = CallbackDevice {
size,
read: Box::new(read_closure),
write: Some(Box::new(write_closure)),
flush: flush_closure,
};
match Filesystem::mount_lazy(Arc::new(dev) as Arc<dyn BlockDevice>) {
Ok(fs) => Box::into_raw(Box::new(fs_ext4_fs_t { fs })),
Err(e) => {
set_err_from(&e, "mount_rw_lazy (callback)");
std::ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_replay_journal_if_dirty(fs: *mut fs_ext4_fs_t) -> c_int {
ffi_guard(
-1,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() {
set_err_msg("null fs handle", EINVAL);
return -1;
}
let fs_ref = &(*fs).fs;
match fs_ref.replay_journal_if_dirty() {
Ok(_) => 0,
Err(e) => {
set_err_from(&e, "replay_journal_if_dirty");
-1
}
}
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_umount(fs: *mut fs_ext4_fs_t) {
ffi_guard(
(),
AssertUnwindSafe(|| {
if !fs.is_null() {
drop(Box::from_raw(fs));
}
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_get_volume_info(
fs: *mut fs_ext4_fs_t,
info: *mut fs_ext4_volume_info_t,
) -> c_int {
ffi_guard(
-1,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() || info.is_null() {
set_err_msg("null fs or info", EINVAL);
return -1;
}
let fs = &(*fs).fs;
let info = &mut *info;
std::ptr::write_bytes(info as *mut fs_ext4_volume_info_t, 0, 1);
let name_bytes = fs.sb.volume_name.as_bytes();
let copy_len = name_bytes.len().min(15);
for (i, &b) in name_bytes[..copy_len].iter().enumerate() {
info.volume_name[i] = b as c_char;
}
info.volume_name[copy_len] = 0;
info.uuid = fs.sb.uuid;
let lm_bytes = fs.sb.last_mounted.as_bytes();
let lm_copy = lm_bytes.len().min(63);
for (i, &b) in lm_bytes[..lm_copy].iter().enumerate() {
info.last_mounted[i] = b as c_char;
}
info.last_mounted[lm_copy] = 0;
info.block_size = fs.sb.block_size();
info.total_blocks = fs.sb.blocks_count;
info.free_blocks = fs.sb.free_blocks_count;
info.reserved_blocks = fs.sb.r_blocks_count;
info.total_inodes = fs.sb.inodes_count;
info.free_inodes = fs.sb.free_inodes_count;
info.inode_size = fs.sb.inode_size;
info.first_inode = fs.sb.first_inode;
info.blocks_per_group = fs.sb.blocks_per_group;
info.inodes_per_group = fs.sb.inodes_per_group;
info.creator_os = fs.sb.creator_os;
info.rev_level = fs.sb.rev_level;
info.minor_rev_level = fs.sb.minor_rev_level;
info.feature_compat = fs.sb.feature_compat;
info.feature_incompat = fs.sb.feature_incompat;
info.feature_ro_compat = fs.sb.feature_ro_compat;
info.desc_size = fs.sb.desc_size;
info.default_hash_version = fs.sb.default_hash_version;
info.state = fs.sb.state;
info.errors_behavior = fs.sb.errors_behavior;
info.last_mount_time = fs.sb.mtime;
info.last_write_time = fs.sb.wtime;
info.last_check_time = fs.sb.lastcheck;
info.check_interval = fs.sb.checkinterval;
info.mount_count = fs.sb.mnt_count;
info.max_mount_count = fs.sb.max_mnt_count;
info.def_resuid = fs.sb.def_resuid;
info.def_resgid = fs.sb.def_resgid;
info.mounted_dirty = if fs.sb.is_clean() { 0 } else { 1 };
0
}),
)
}
fn resolve_path(fs: &Filesystem, path: &str) -> Result<u32> {
let mut reader = |ino: u32| fs.read_inode_verified(ino).map(|(inode, _)| inode);
let ino = path_mod::lookup_with_csum(fs.dev.as_ref(), &fs.sb, &mut reader, path, &fs.csum)?;
if path.ends_with('/') && path != "/" {
let (inode, _raw) = fs.read_inode_verified(ino)?;
if !inode.is_dir() {
return Err(Error::NotADirectory);
}
}
Ok(ino)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_stat(
fs: *mut fs_ext4_fs_t,
path: *const c_char,
attr: *mut fs_ext4_attr_t,
) -> c_int {
ffi_guard(
-1,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() || path.is_null() || attr.is_null() {
set_err_msg("null fs, path, or attr", EINVAL);
return -1;
}
let fs = &(*fs).fs;
let path = cstr_to_str(path);
let attr = &mut *attr;
let ino = match resolve_path(fs, path) {
Ok(n) => n,
Err(e) => {
set_err_from(&e, &format!("stat {path}"));
return -1;
}
};
let (inode, _raw) = match fs.read_inode_verified(ino) {
Ok(p) => p,
Err(e) => {
set_err_from(&e, &format!("read inode {ino}"));
return -1;
}
};
fill_attr(attr, ino, &inode);
0
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_dir_open(
fs: *mut fs_ext4_fs_t,
path: *const c_char,
) -> *mut fs_ext4_dir_iter_t {
ffi_guard(
std::ptr::null_mut(),
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() || path.is_null() {
set_err_msg("null fs or path", EINVAL);
return std::ptr::null_mut();
}
let fs_ref = &(*fs).fs;
let path_str = cstr_to_str(path);
let ino = match resolve_path(fs_ref, path_str) {
Ok(n) => n,
Err(e) => {
set_err_from(&e, &format!("dir_open {path_str}"));
return std::ptr::null_mut();
}
};
let (inode, _raw) = match fs_ref.read_inode_verified(ino) {
Ok(p) => p,
Err(e) => {
set_err_from(&e, &format!("read inode {ino}"));
return std::ptr::null_mut();
}
};
if !inode.is_dir() {
set_err_msg(&format!("dir_open {path_str}: not a directory"), ENOTDIR);
return std::ptr::null_mut();
}
let entries = match collect_dir_entries(fs_ref, &inode) {
Ok(e) => e,
Err(e) => {
set_err_from(&e, &format!("read directory {path_str}"));
return std::ptr::null_mut();
}
};
let iter = Box::new(fs_ext4_dir_iter_t {
entries,
position: 0,
current: std::mem::zeroed(),
});
Box::into_raw(iter)
}),
)
}
fn collect_dir_entries(fs: &Filesystem, inode: &Inode) -> Result<Vec<fs_ext4_dirent_t>> {
if !inode.has_extents() {
return Err(Error::Corrupt("legacy (non-extent) dirs not yet supported"));
}
let block_size = fs.sb.block_size();
let has_filetype = fs.sb.feature_incompat & features::Incompat::FILETYPE.bits() != 0;
const MAX_DIR_ENTRIES: usize = 1_000_000;
let mut entries = Vec::new();
if inode.has_inline_data() {
for entry in DirBlockIter::new(&inode.block, has_filetype) {
let e = entry?;
if entries.len() >= MAX_DIR_ENTRIES {
return Err(Error::Corrupt("dir entries exceed MAX_DIR_ENTRIES"));
}
entries.push(dir_entry_to_bridge(&e));
}
return Ok(entries);
}
let total_blocks = inode.size.div_ceil(block_size as u64);
let mut block_buf = vec![0u8; block_size as usize];
for logical in 0..total_blocks {
let phys = match extent::map_logical(&inode.block, fs.dev.as_ref(), block_size, logical)? {
Some(p) => p,
None => continue, };
fs.dev.read_at(phys * block_size as u64, &mut block_buf)?;
for entry in DirBlockIter::new(&block_buf, has_filetype) {
let e = entry?;
if entries.len() >= MAX_DIR_ENTRIES {
return Err(Error::Corrupt("dir entries exceed MAX_DIR_ENTRIES"));
}
entries.push(dir_entry_to_bridge(&e));
}
}
Ok(entries)
}
fn dir_entry_to_bridge(e: &dir::DirEntry) -> fs_ext4_dirent_t {
let mut name = [0 as c_char; 256];
let copy_len = e.name.len().min(255);
for (i, &b) in e.name[..copy_len].iter().enumerate() {
name[i] = b as c_char;
}
name[copy_len] = 0;
let file_type = match e.file_type {
DirEntryType::RegFile => 1u8,
DirEntryType::Directory => 2,
DirEntryType::CharDev => 3,
DirEntryType::BlockDev => 4,
DirEntryType::Fifo => 5,
DirEntryType::Socket => 6,
DirEntryType::Symlink => 7,
DirEntryType::Unknown => 0,
};
fs_ext4_dirent_t {
inode: e.inode,
file_type,
name_len: copy_len as u8,
name,
}
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_dir_next(
iter: *mut fs_ext4_dir_iter_t,
) -> *const fs_ext4_dirent_t {
ffi_guard(
std::ptr::null(),
AssertUnwindSafe(|| {
if iter.is_null() {
return std::ptr::null();
}
let iter = &mut *iter;
if iter.position >= iter.entries.len() {
return std::ptr::null();
}
iter.current = fs_ext4_dirent_t {
inode: iter.entries[iter.position].inode,
file_type: iter.entries[iter.position].file_type,
name_len: iter.entries[iter.position].name_len,
name: iter.entries[iter.position].name,
};
iter.position += 1;
&iter.current
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_dir_close(iter: *mut fs_ext4_dir_iter_t) {
ffi_guard(
(),
AssertUnwindSafe(|| {
if !iter.is_null() {
drop(Box::from_raw(iter));
}
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_read_file(
fs: *mut fs_ext4_fs_t,
path: *const c_char,
buf: *mut c_void,
offset: u64,
length: u64,
) -> i64 {
ffi_guard(
-1,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() || path.is_null() || buf.is_null() {
set_err_msg("null fs, path, or buf", EINVAL);
return -1;
}
let fs_ref = &(*fs).fs;
let path_str = cstr_to_str(path);
let ino = match resolve_path(fs_ref, path_str) {
Ok(n) => n,
Err(e) => {
set_err_from(&e, &format!("read_file {path_str}"));
return -1;
}
};
let (inode, inode_raw) = match fs_ref.read_inode_verified(ino) {
Ok(p) => p,
Err(e) => {
set_err_from(&e, &format!("read inode {ino}"));
return -1;
}
};
if !inode.is_file() {
set_err_msg(&format!("read_file {path_str}: not a regular file"), EINVAL);
return -1;
}
let length = length.min(inode.size).min(usize::MAX as u64);
let out = std::slice::from_raw_parts_mut(buf as *mut u8, length as usize);
match file_io::read_with_raw_verified(
fs_ref, &inode, &inode_raw, ino, offset, length, out,
) {
Ok(n) => n as i64,
Err(e) => {
set_err_from(&e, &format!("read_file {path_str}"));
-1
}
}
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_readlink(
fs: *mut fs_ext4_fs_t,
path: *const c_char,
buf: *mut c_char,
bufsize: usize,
) -> c_int {
ffi_guard(
-1,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() || path.is_null() || buf.is_null() || bufsize == 0 {
set_err_msg("null fs/path/buf or zero bufsize", EINVAL);
return -1;
}
let fs_ref = &(*fs).fs;
let path_str = cstr_to_str(path);
let ino = match resolve_path(fs_ref, path_str) {
Ok(n) => n,
Err(e) => {
set_err_from(&e, &format!("readlink {path_str}"));
return -1;
}
};
let (inode, _raw) = match fs_ref.read_inode_verified(ino) {
Ok(p) => p,
Err(e) => {
set_err_from(&e, &format!("read inode {ino}"));
return -1;
}
};
if !inode.is_symlink() {
set_err_msg(&format!("readlink {path_str}: not a symlink"), EINVAL);
return -1;
}
if inode.size > 4096 {
set_last_error(format!(
"readlink {path_str}: target of {} bytes is longer than any path",
inode.size
));
return -1;
}
let target = if inode.size < 60 {
inode.block[..inode.size as usize].to_vec()
} else {
let mut out = vec![0u8; inode.size as usize];
match file_io::read_verified(fs_ref, &inode, ino, 0, inode.size, &mut out) {
Ok(_) => out,
Err(e) => {
set_err_from(&e, &format!("readlink {path_str}"));
return -1;
}
}
};
let copy_len = target.len().min(bufsize - 1);
let out = std::slice::from_raw_parts_mut(buf as *mut u8, bufsize);
out[..copy_len].copy_from_slice(&target[..copy_len]);
out[copy_len] = 0;
0
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_listxattr(
fs: *mut fs_ext4_fs_t,
path: *const c_char,
buf: *mut c_char,
bufsize: usize,
) -> i64 {
ffi_guard(
-1,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() || path.is_null() {
set_err_msg("null fs or path", EINVAL);
return -1;
}
let fs_ref = &(*fs).fs;
let path_str = cstr_to_str(path);
let ino = match resolve_path(fs_ref, path_str) {
Ok(n) => n,
Err(e) => {
set_err_from(&e, &format!("listxattr {path_str}"));
return -1;
}
};
let (inode, inode_raw) = match fs_ref.read_inode_verified(ino) {
Ok(p) => p,
Err(e) => {
set_err_from(&e, &format!("read inode {ino}"));
return -1;
}
};
let entries = match xattr::read_all(
fs_ref.dev.as_ref(),
&inode,
&inode_raw,
fs_ref.sb.inode_size,
fs_ref.sb.block_size(),
) {
Ok(v) => v,
Err(e) => {
set_err_from(&e, &format!("listxattr {path_str}"));
return -1;
}
};
let required: usize = entries.iter().map(|e| e.name.len() + 1).sum();
if !buf.is_null() && bufsize > 0 {
let out = std::slice::from_raw_parts_mut(buf as *mut u8, bufsize);
let mut pos = 0;
for e in &entries {
let name_bytes = e.name.as_bytes();
let needed = name_bytes.len() + 1;
if pos + needed > bufsize {
break;
}
out[pos..pos + name_bytes.len()].copy_from_slice(name_bytes);
out[pos + name_bytes.len()] = 0;
pos += needed;
}
}
required as i64
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_getxattr(
fs: *mut fs_ext4_fs_t,
path: *const c_char,
name: *const c_char,
buf: *mut c_void,
bufsize: usize,
) -> i64 {
ffi_guard(
-1,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() || path.is_null() || name.is_null() {
set_err_msg("null fs, path, or name", EINVAL);
return -1;
}
let fs_ref = &(*fs).fs;
let path_str = cstr_to_str(path);
let name_str = cstr_to_str(name);
let ino = match resolve_path(fs_ref, path_str) {
Ok(n) => n,
Err(e) => {
set_err_from(&e, &format!("getxattr {path_str}"));
return -1;
}
};
let (inode, inode_raw) = match fs_ref.read_inode_verified(ino) {
Ok(p) => p,
Err(e) => {
set_err_from(&e, &format!("read inode {ino}"));
return -1;
}
};
let value = match xattr::get(
fs_ref.dev.as_ref(),
&inode,
&inode_raw,
fs_ref.sb.inode_size,
fs_ref.sb.block_size(),
name_str,
) {
Ok(Some(v)) => v,
Ok(None) => {
set_err_msg(
&format!("getxattr {path_str}: {name_str} not found"),
ENOENT,
);
return -1;
}
Err(e) => {
set_err_from(&e, &format!("getxattr {path_str} {name_str}"));
return -1;
}
};
if !buf.is_null() && bufsize > 0 {
let copy_len = value.len().min(bufsize);
let out = std::slice::from_raw_parts_mut(buf as *mut u8, bufsize);
out[..copy_len].copy_from_slice(&value[..copy_len]);
}
value.len() as i64
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_truncate(
fs: *mut fs_ext4_fs_t,
path: *const c_char,
new_size: u64,
) -> c_int {
ffi_guard(
-1,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() || path.is_null() {
set_err_msg("null fs/path", EINVAL);
return -1;
}
let fs_ref = &(*fs).fs;
let path_str = cstr_to_str(path);
let ino = match resolve_path(fs_ref, path_str) {
Ok(n) => n,
Err(e) => {
set_err_from(&e, &format!("truncate {path_str}"));
return -1;
}
};
let inode = match fs_ref.read_inode_verified(ino) {
Ok((i, _)) => i,
Err(e) => {
set_err_from(&e, &format!("read inode {ino}"));
return -1;
}
};
if inode.is_dir() {
set_err_msg(&format!("truncate {path_str}: is a directory"), EISDIR);
return -1;
}
if !inode.is_file() {
set_err_msg(&format!("truncate {path_str}: not a regular file"), EINVAL);
return -1;
}
let res = if new_size >= inode.size {
fs_ref.apply_truncate_grow(ino, new_size)
} else {
fs_ref.apply_truncate_shrink(ino, new_size)
};
match res {
Ok(()) => 0,
Err(e) => {
set_err_from(&e, &format!("truncate {path_str} -> {new_size}"));
-1
}
}
}),
)
}
pub const FS_EXT4_FALLOC_FL_KEEP_SIZE: c_int = 0x01;
pub const FS_EXT4_FALLOC_FL_PUNCH_HOLE: c_int = 0x02;
pub const FS_EXT4_FALLOC_FL_ZERO_RANGE: c_int = 0x10;
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_fallocate(
fs: *mut fs_ext4_fs_t,
path: *const c_char,
offset: u64,
len: u64,
flags: c_int,
) -> c_int {
ffi_guard(
-1,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() || path.is_null() {
set_err_msg("null fs/path", EINVAL);
return -1;
}
let fs_ref = &(*fs).fs;
let path_str = cstr_to_str(path);
let ino = match resolve_path(fs_ref, path_str) {
Ok(n) => n,
Err(e) => {
set_err_from(&e, &format!("fallocate {path_str}"));
return -1;
}
};
let is_punch = flags & FS_EXT4_FALLOC_FL_PUNCH_HOLE != 0;
let is_zero = flags & FS_EXT4_FALLOC_FL_ZERO_RANGE != 0;
let is_keep = flags & FS_EXT4_FALLOC_FL_KEEP_SIZE != 0;
let result = if is_zero {
fs_ref.apply_fallocate_zero_range(ino, offset, len)
} else if is_punch {
if !is_keep {
set_err_msg("fallocate: PUNCH_HOLE requires KEEP_SIZE", EINVAL);
return -1;
}
fs_ref.apply_fallocate_punch_hole(ino, offset, len)
} else if flags == FS_EXT4_FALLOC_FL_KEEP_SIZE {
fs_ref.apply_fallocate_keep_size(ino, offset, len)
} else {
set_err_msg(
"fallocate: unsupported flag combination (KEEP_SIZE / PUNCH_HOLE+KEEP_SIZE / ZERO_RANGE only)",
ENOSYS,
);
return -1;
};
match result {
Ok(()) => 0,
Err(e) => {
set_err_from(&e, &format!("fallocate {path_str} @{offset}+{len}"));
-1
}
}
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_unlink(fs: *mut fs_ext4_fs_t, path: *const c_char) -> c_int {
ffi_guard(
-1,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() || path.is_null() {
set_err_msg("null fs/path", EINVAL);
return -1;
}
let fs_ref = &(*fs).fs;
let path_str = cstr_to_str(path);
match fs_ref.apply_unlink(path_str) {
Ok(()) => 0,
Err(e) => {
set_err_from(&e, &format!("unlink {path_str}"));
-1
}
}
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_create(
fs: *mut fs_ext4_fs_t,
path: *const c_char,
mode: u16,
) -> u32 {
ffi_guard(
0u32,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() || path.is_null() {
set_err_msg("null fs/path", EINVAL);
return 0u32;
}
let fs_ref = &(*fs).fs;
let path_str = cstr_to_str(path);
match fs_ref.apply_create(path_str, mode) {
Ok(ino) => ino,
Err(e) => {
set_err_from(&e, &format!("create {path_str}"));
0u32
}
}
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_write_file(
fs: *mut fs_ext4_fs_t,
path: *const c_char,
data: *const c_void,
len: u64,
) -> i64 {
ffi_guard(
-1i64,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() || path.is_null() {
set_err_msg("null fs/path", EINVAL);
return -1;
}
if data.is_null() && len > 0 {
set_err_msg("null data with non-zero len", EINVAL);
return -1;
}
const MAX_WRITE_LEN: u64 = 1 << 30;
if len > MAX_WRITE_LEN {
set_err_msg(
&format!("write_file: len {len} exceeds {MAX_WRITE_LEN}"),
EINVAL,
);
return -1;
}
let fs_ref = &(*fs).fs;
let path_str = cstr_to_str(path);
let ino = match resolve_path(fs_ref, path_str) {
Ok(n) => n,
Err(e) => {
set_err_from(&e, &format!("write_file {path_str}"));
return -1;
}
};
let inode = match fs_ref.read_inode_verified(ino) {
Ok((i, _)) => i,
Err(e) => {
set_err_from(&e, &format!("read inode {ino}"));
return -1;
}
};
if inode.is_dir() {
set_err_msg(&format!("write_file {path_str}: is a directory"), EISDIR);
return -1;
}
if !inode.is_file() {
set_err_msg(
&format!("write_file {path_str}: not a regular file"),
EINVAL,
);
return -1;
}
let slice: &[u8] = if len == 0 {
&[]
} else {
std::slice::from_raw_parts(data as *const u8, len as usize)
};
match fs_ref.apply_replace_file_content(path_str, slice) {
Ok(new_size) => new_size as i64,
Err(e) => {
set_err_from(&e, &format!("write_file {path_str} ({len} bytes)"));
-1
}
}
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_pwrite(
fs: *mut fs_ext4_fs_t,
path: *const c_char,
data: *const c_void,
len: u64,
offset: u64,
) -> i64 {
ffi_guard(
-1i64,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() || path.is_null() {
set_err_msg("null fs/path", EINVAL);
return -1;
}
if data.is_null() && len > 0 {
set_err_msg("null data with non-zero len", EINVAL);
return -1;
}
const MAX_PWRITE_LEN: u64 = 1 << 30;
if len > MAX_PWRITE_LEN {
set_err_msg(
&format!("pwrite: len {len} exceeds {MAX_PWRITE_LEN}"),
EINVAL,
);
return -1;
}
if offset.checked_add(len).is_none() {
set_err_msg("pwrite: offset+len overflow", EINVAL);
return -1;
}
let fs_ref = &(*fs).fs;
let path_str = cstr_to_str(path);
let slice: &[u8] = if len == 0 {
&[]
} else {
std::slice::from_raw_parts(data as *const u8, len as usize)
};
match fs_ref.apply_pwrite(path_str, offset, slice) {
Ok(new_size) => new_size as i64,
Err(e) => {
set_err_from(&e, &format!("pwrite {path_str} @{offset}+{len}"));
-1
}
}
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_mount_rw(device_path: *const c_char) -> *mut fs_ext4_fs_t {
ffi_guard(
std::ptr::null_mut(),
AssertUnwindSafe(|| {
clear_last_error();
let path = cstr_to_str(device_path);
if path.is_empty() {
set_err_msg("null or empty device_path", EINVAL);
return std::ptr::null_mut();
}
let dev = match FileDevice::open_rw(path) {
Ok(d) => Arc::new(d) as Arc<dyn BlockDevice>,
Err(e) => {
set_err_from(&e, &format!("open_rw {path}"));
return std::ptr::null_mut();
}
};
match Filesystem::mount(dev) {
Ok(fs) => Box::into_raw(Box::new(fs_ext4_fs_t { fs })),
Err(e) => {
set_err_from(&e, &format!("mount_rw {path}"));
std::ptr::null_mut()
}
}
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_link(
fs: *mut fs_ext4_fs_t,
src: *const c_char,
dst: *const c_char,
) -> c_int {
ffi_guard(
-1,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() || src.is_null() || dst.is_null() {
set_err_msg("null fs/src/dst", EINVAL);
return -1;
}
let fs_ref = &(*fs).fs;
let src_str = cstr_to_str(src);
let dst_str = cstr_to_str(dst);
match fs_ref.apply_link(src_str, dst_str) {
Ok(()) => 0,
Err(e) => {
set_err_from(&e, &format!("link {src_str} -> {dst_str}"));
-1
}
}
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_rename(
fs: *mut fs_ext4_fs_t,
src: *const c_char,
dst: *const c_char,
) -> c_int {
ffi_guard(
-1,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() || src.is_null() || dst.is_null() {
set_err_msg("null fs/src/dst", EINVAL);
return -1;
}
let fs_ref = &(*fs).fs;
let src_str = cstr_to_str(src);
let dst_str = cstr_to_str(dst);
match fs_ref.apply_rename(src_str, dst_str, false) {
Ok(()) => 0,
Err(e) => {
set_err_from(&e, &format!("rename {src_str} -> {dst_str}"));
-1
}
}
}),
)
}
pub const FS_EXT4_RENAME_REPLACE: c_int = 0x01;
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_rename2(
fs: *mut fs_ext4_fs_t,
src: *const c_char,
dst: *const c_char,
flags: c_int,
) -> c_int {
ffi_guard(
-1,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() || src.is_null() || dst.is_null() {
set_err_msg("null fs/src/dst", EINVAL);
return -1;
}
let known = FS_EXT4_RENAME_REPLACE;
if flags & !known != 0 {
set_err_msg("rename2: unknown flag bits", EINVAL);
return -1;
}
let replace = flags & FS_EXT4_RENAME_REPLACE != 0;
let fs_ref = &(*fs).fs;
let src_str = cstr_to_str(src);
let dst_str = cstr_to_str(dst);
match fs_ref.apply_rename(src_str, dst_str, replace) {
Ok(()) => 0,
Err(e) => {
set_err_from(&e, &format!("rename2 {src_str} -> {dst_str}"));
-1
}
}
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_mkdir(
fs: *mut fs_ext4_fs_t,
path: *const c_char,
mode: u16,
) -> u32 {
ffi_guard(
0u32,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() || path.is_null() {
set_err_msg("null fs/path", EINVAL);
return 0u32;
}
let fs_ref = &(*fs).fs;
let path_str = cstr_to_str(path);
match fs_ref.apply_mkdir(path_str, mode) {
Ok(ino) => ino,
Err(e) => {
set_err_from(&e, &format!("mkdir {path_str}"));
0u32
}
}
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_rmdir(fs: *mut fs_ext4_fs_t, path: *const c_char) -> c_int {
ffi_guard(
-1,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() || path.is_null() {
set_err_msg("null fs/path", EINVAL);
return -1;
}
let fs_ref = &(*fs).fs;
let path_str = cstr_to_str(path);
match fs_ref.apply_rmdir(path_str) {
Ok(()) => 0,
Err(e) => {
set_err_from(&e, &format!("rmdir {path_str}"));
-1
}
}
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_chmod(
fs: *mut fs_ext4_fs_t,
path: *const c_char,
mode: u16,
) -> c_int {
ffi_guard(
-1,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() || path.is_null() {
set_err_msg("null fs/path", EINVAL);
return -1;
}
let fs_ref = &(*fs).fs;
let path_str = cstr_to_str(path);
match fs_ref.apply_chmod(path_str, mode) {
Ok(()) => 0,
Err(e) => {
set_err_from(&e, &format!("chmod {path_str}"));
-1
}
}
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_chown(
fs: *mut fs_ext4_fs_t,
path: *const c_char,
uid: u32,
gid: u32,
) -> c_int {
ffi_guard(
-1,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() || path.is_null() {
set_err_msg("null fs/path", EINVAL);
return -1;
}
let fs_ref = &(*fs).fs;
let path_str = cstr_to_str(path);
match fs_ref.apply_chown(path_str, uid, gid) {
Ok(()) => 0,
Err(e) => {
set_err_from(&e, &format!("chown {path_str}"));
-1
}
}
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_mknod(
fs: *mut fs_ext4_fs_t,
path: *const c_char,
mode: u16,
major: u32,
minor: u32,
) -> u32 {
ffi_guard(
0u32,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() || path.is_null() {
set_err_msg("null fs/path", EINVAL);
return 0u32;
}
let fs_ref = &(*fs).fs;
let path_str = cstr_to_str(path);
match fs_ref.apply_mknod(path_str, mode, major, minor) {
Ok(ino) => ino,
Err(e) => {
set_err_from(&e, &format!("mknod {path_str}"));
0u32
}
}
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_set_flags(
fs: *mut fs_ext4_fs_t,
path: *const c_char,
flags: u32,
) -> c_int {
ffi_guard(
-1,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() || path.is_null() {
set_err_msg("null fs/path", EINVAL);
return -1;
}
let fs_ref = &(*fs).fs;
let path_str = cstr_to_str(path);
match fs_ref.apply_set_flags(path_str, flags) {
Ok(()) => 0,
Err(e) => {
set_err_from(&e, &format!("set_flags {path_str}"));
-1
}
}
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_utimens(
fs: *mut fs_ext4_fs_t,
path: *const c_char,
atime_sec: i64,
atime_nsec: u32,
mtime_sec: i64,
mtime_nsec: u32,
) -> c_int {
ffi_guard(
-1,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() || path.is_null() {
set_err_msg("null fs/path", EINVAL);
return -1;
}
let fs_ref = &(*fs).fs;
let path_str = cstr_to_str(path);
match fs_ref.apply_utimens(path_str, atime_sec, atime_nsec, mtime_sec, mtime_nsec) {
Ok(()) => 0,
Err(e) => {
set_err_from(&e, &format!("utimens {path_str}"));
-1
}
}
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_symlink(
fs: *mut fs_ext4_fs_t,
target: *const c_char,
linkpath: *const c_char,
) -> u32 {
ffi_guard(
0u32,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() || target.is_null() || linkpath.is_null() {
set_err_msg("null fs/target/linkpath", EINVAL);
return 0u32;
}
let target_bytes = CStr::from_ptr(target).to_bytes();
if target_bytes.len() > FFI_PATH_MAX {
set_err_msg(
&format!(
"symlink target length {} exceeds FFI_PATH_MAX {FFI_PATH_MAX}",
target_bytes.len()
),
ENAMETOOLONG,
);
return 0u32;
}
let linkpath_bytes = CStr::from_ptr(linkpath).to_bytes();
if linkpath_bytes.len() > FFI_PATH_MAX {
set_err_msg(
&format!(
"symlink linkpath length {} exceeds FFI_PATH_MAX {FFI_PATH_MAX}",
linkpath_bytes.len()
),
ENAMETOOLONG,
);
return 0u32;
}
let fs_ref = &(*fs).fs;
let target_str = cstr_to_str(target);
let linkpath_str = cstr_to_str(linkpath);
match fs_ref.apply_symlink(target_str, linkpath_str) {
Ok(ino) => ino,
Err(e) => {
set_err_from(&e, &format!("symlink {linkpath_str} -> {target_str}"));
0u32
}
}
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_removexattr(
fs: *mut fs_ext4_fs_t,
path: *const c_char,
name: *const c_char,
) -> c_int {
ffi_guard(
-1,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() || path.is_null() || name.is_null() {
set_err_msg("null fs/path/name", EINVAL);
return -1;
}
let fs_ref = &(*fs).fs;
let path_str = cstr_to_str(path);
let name_str = cstr_to_str(name);
match fs_ref.apply_removexattr(path_str, name_str) {
Ok(()) => 0,
Err(e) => {
set_err_from(&e, &format!("removexattr {path_str} {name_str}"));
-1
}
}
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_setxattr(
fs: *mut fs_ext4_fs_t,
path: *const c_char,
name: *const c_char,
value: *const c_void,
value_len: usize,
) -> c_int {
ffi_guard(
-1,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() || path.is_null() || name.is_null() {
set_err_msg("null fs/path/name", EINVAL);
return -1;
}
if value.is_null() && value_len > 0 {
set_err_msg("null value with nonzero len", EINVAL);
return -1;
}
const MAX_XATTR_VALUE_LEN: usize = 64 * 1024;
if value_len > MAX_XATTR_VALUE_LEN {
set_err_msg(
&format!("setxattr: value_len {value_len} exceeds {MAX_XATTR_VALUE_LEN}"),
EINVAL,
);
return -1;
}
let fs_ref = &(*fs).fs;
let path_str = cstr_to_str(path);
let name_str = cstr_to_str(name);
let value_bytes = if value_len == 0 {
&[][..]
} else {
std::slice::from_raw_parts(value as *const u8, value_len)
};
match fs_ref.apply_setxattr(path_str, name_str, value_bytes) {
Ok(()) => 0,
Err(e) => {
set_err_from(&e, &format!("setxattr {path_str} {name_str}"));
-1
}
}
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_mkfs(
cfg: *const fs_ext4_blockdev_cfg_t,
label: *const c_char,
uuid: *const u8,
) -> c_int {
ffi_guard(
-EINVAL,
AssertUnwindSafe(|| {
clear_last_error();
if cfg.is_null() {
set_err_msg("null cfg", EINVAL);
return -EINVAL;
}
let cfg = &*cfg;
let Some(read_fn) = cfg.read else {
set_err_msg("cfg.read is null", EINVAL);
return -EINVAL;
};
let Some(write_fn) = cfg.write else {
set_err_msg("cfg.write is null (mkfs needs writes)", EINVAL);
return -EINVAL;
};
let flush_fn = cfg.flush; if cfg.size_bytes == 0 {
set_err_msg("cfg.size_bytes is 0", EINVAL);
return -EINVAL;
}
let block_size = if cfg.block_size == 0 {
4096
} else {
cfg.block_size
};
let ctx_addr = cfg.context as usize;
let read_closure = move |offset: u64, buf: &mut [u8]| -> std::io::Result<()> {
let rc = unsafe {
read_fn(
ctx_addr as *mut c_void,
buf.as_mut_ptr() as *mut c_void,
offset,
buf.len() as u64,
)
};
if rc != 0 {
Err(std::io::Error::other(format!("read cb {rc}")))
} else {
Ok(())
}
};
let write_closure = move |offset: u64, buf: &[u8]| -> std::io::Result<()> {
let rc = unsafe {
write_fn(
ctx_addr as *mut c_void,
buf.as_ptr() as *const c_void,
offset,
buf.len() as u64,
)
};
if rc != 0 {
Err(std::io::Error::other(format!("write cb {rc}")))
} else {
Ok(())
}
};
let flush_closure: Option<crate::block_io::FlushCb> = flush_fn.map(|f| {
let cb: crate::block_io::FlushCb = Box::new(move || -> std::io::Result<()> {
let rc = unsafe { f(ctx_addr as *mut c_void) };
if rc != 0 {
Err(std::io::Error::other(format!("flush cb {rc}")))
} else {
Ok(())
}
});
cb
});
let dev = CallbackDevice {
size: cfg.size_bytes,
read: Box::new(read_closure),
write: Some(Box::new(write_closure)),
flush: flush_closure,
};
let label_str: Option<&str> = if label.is_null() {
None
} else {
Some(cstr_to_str(label))
};
let uuid_arr: Option<[u8; 16]> = if uuid.is_null() {
None
} else {
let mut tmp = [0u8; 16];
std::ptr::copy_nonoverlapping(uuid, tmp.as_mut_ptr(), 16);
Some(tmp)
};
match crate::mkfs::format_filesystem(
&dev,
label_str,
uuid_arr,
cfg.size_bytes,
block_size,
) {
Ok(()) => 0,
Err(e) => {
let errno = e.to_errno();
set_err_from(&e, "mkfs");
-errno
}
}
}),
)
}
#[repr(C)]
#[derive(Copy, Clone)]
pub enum fs_ext4_fsck_phase_t {
Superblock = 0,
Journal = 1,
Directory = 2,
Inodes = 3,
Finalize = 4,
}
pub type fs_ext4_fsck_progress_fn = Option<
unsafe extern "C" fn(
context: *mut c_void,
phase: fs_ext4_fsck_phase_t,
phase_name: *const c_char,
done: u64,
total: u64,
),
>;
pub type fs_ext4_fsck_finding_fn = Option<
unsafe extern "C" fn(
context: *mut c_void,
kind: *const c_char,
inode: u32,
detail: *const c_char,
),
>;
#[repr(C)]
pub struct fs_ext4_fsck_options_t {
pub read_only: u8,
pub replay_journal: u8,
pub max_dirs: u32,
pub max_entries_per_dir: u32,
pub on_progress: fs_ext4_fsck_progress_fn,
pub on_finding: fs_ext4_fsck_finding_fn,
pub context: *mut c_void,
pub repair: u8,
}
#[repr(C)]
pub struct fs_ext4_fsck_report_t {
pub inodes_visited: u64,
pub directories_scanned: u64,
pub entries_scanned: u64,
pub anomalies_found: u64,
pub was_dirty: u8,
pub dirty_cleared: u8,
pub repaired_count: u64,
pub initial_anomalies_count: u64,
}
fn anomaly_to_capi(a: &crate::fsck::Anomaly) -> (&'static str, u32, String) {
use crate::fsck::Anomaly;
match a {
&Anomaly::LinkCountTooLow {
ino,
stored,
observed,
} => (
"link_count_low",
ino,
format!("stored={stored} observed={observed}"),
),
&Anomaly::LinkCountTooHigh {
ino,
stored,
observed,
} => (
"link_count_high",
ino,
format!("stored={stored} observed={observed}"),
),
&Anomaly::DanglingEntry {
parent_ino,
child_ino,
observed,
} => (
"dangling_entry",
child_ino,
format!("parent_ino={parent_ino} observed={observed}"),
),
&Anomaly::WrongDotDot {
dir_ino,
claims,
actual_parent,
} => (
"wrong_dotdot",
dir_ino,
format!("claims={claims} actual_parent={actual_parent}"),
),
Anomaly::BogusEntry {
parent_ino,
child_ino,
name,
} => (
"bogus_entry",
*child_ino,
format!(
"parent_ino={parent_ino} name={}",
String::from_utf8_lossy(name)
),
),
&Anomaly::BlockGroupFreeCountDrift {
group_index,
stored_blocks,
observed_blocks,
stored_inodes,
observed_inodes,
} => (
"block_group_free_count_drift",
group_index,
format!(
"stored_blocks={stored_blocks} observed_blocks={observed_blocks} \
stored_inodes={stored_inodes} observed_inodes={observed_inodes}"
),
),
&Anomaly::SuperblockFreeCountDrift {
stored_blocks,
observed_blocks,
stored_inodes,
observed_inodes,
} => (
"superblock_free_count_drift",
0,
format!(
"stored_blocks={stored_blocks} observed_blocks={observed_blocks} \
stored_inodes={stored_inodes} observed_inodes={observed_inodes}"
),
),
Anomaly::DuplicateDirentForDirInode { ino, dirents } => {
let detail = dirents
.iter()
.map(|(p, n)| format!("{p}:{n}"))
.collect::<Vec<_>>()
.join(",");
("duplicate_dir_inode", *ino, format!("aliases={detail}"))
}
}
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_fsck_run(
fs: *mut fs_ext4_fs_t,
opts: *const fs_ext4_fsck_options_t,
report: *mut fs_ext4_fsck_report_t,
) -> c_int {
ffi_guard(
-1,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() || opts.is_null() || report.is_null() {
set_err_msg("null fs/opts/report", EINVAL);
return -1;
}
let fs_ref = &(*fs).fs;
let opts_ref = &*opts;
if opts_ref.read_only > 1 {
set_err_msg("fsck: opts.read_only must be 0 or 1", EINVAL);
return -1;
}
if opts_ref.replay_journal > 1 {
set_err_msg("fsck: opts.replay_journal must be 0 or 1", EINVAL);
return -1;
}
if opts_ref.repair > 1 {
set_err_msg("fsck: opts.repair must be 0 or 1", EINVAL);
return -1;
}
if opts_ref.read_only == 1 && opts_ref.repair == 1 {
set_err_msg("fsck: opts.repair = 1 requires opts.read_only = 0", EINVAL);
return -1;
}
let repair_requested = opts_ref.repair == 1;
std::ptr::write_bytes(report, 0, 1);
let report_out = &mut *report;
report_out.was_dirty = if fs_ref.sb.is_clean() { 0 } else { 1 };
let progress_cb = opts_ref.on_progress;
let finding_cb = opts_ref.on_finding;
let ctx = opts_ref.context;
let emit_progress = |phase: crate::fsck::FsckPhase, done: u64, total: u64| {
if let Some(cb) = progress_cb {
let phase_c = match phase {
crate::fsck::FsckPhase::Superblock => fs_ext4_fsck_phase_t::Superblock,
crate::fsck::FsckPhase::Journal => fs_ext4_fsck_phase_t::Journal,
crate::fsck::FsckPhase::Directory => fs_ext4_fsck_phase_t::Directory,
crate::fsck::FsckPhase::Inodes => fs_ext4_fsck_phase_t::Inodes,
crate::fsck::FsckPhase::Finalize => fs_ext4_fsck_phase_t::Finalize,
};
let name = CString::new(phase.name()).unwrap();
cb(ctx, phase_c, name.as_ptr(), done, total);
}
};
if opts_ref.replay_journal != 0 {
emit_progress(crate::fsck::FsckPhase::Journal, 0, 1);
if let Err(e) = fs_ref.replay_journal_if_dirty() {
set_err_from(&e, "fsck: replay_journal_if_dirty");
return -1;
}
emit_progress(crate::fsck::FsckPhase::Journal, 1, 1);
}
let max_dirs = if opts_ref.max_dirs == 0 {
u32::MAX
} else {
opts_ref.max_dirs
};
let max_entries = if opts_ref.max_entries_per_dir == 0 {
u32::MAX
} else {
opts_ref.max_entries_per_dir
};
let emit_finding = |a: &crate::fsck::Anomaly| {
if let Some(cb) = finding_cb {
let (kind, ino, detail) = anomaly_to_capi(a);
let kind_c = CString::new(kind).unwrap_or_else(|_| CString::new("?").unwrap());
let detail_c =
CString::new(detail).unwrap_or_else(|_| CString::new("").unwrap());
cb(ctx, kind_c.as_ptr(), ino, detail_c.as_ptr());
}
};
let result = crate::fsck::audit_with_repair(
fs_ref,
max_dirs,
max_entries,
emit_progress,
emit_finding,
repair_requested,
);
match result {
Ok(audit_report) => {
report_out.inodes_visited = audit_report.inodes_visited as u64;
report_out.directories_scanned = audit_report.directories_scanned as u64;
report_out.entries_scanned = audit_report.entries_scanned;
report_out.anomalies_found = audit_report.anomalies_count;
report_out.initial_anomalies_count = audit_report.initial_anomalies_count;
report_out.repaired_count = audit_report.repaired_count;
let post_sb_clean = if repair_requested && report_out.was_dirty == 1 {
crate::superblock::Superblock::read(fs_ref.dev.as_ref())
.map(|sb| sb.is_clean())
.unwrap_or(false)
} else {
false
};
report_out.dirty_cleared = if post_sb_clean { 1 } else { 0 };
0
}
Err(e) => {
set_err_from(&e, "fsck: audit");
-1
}
}
}),
)
}