#![allow(non_camel_case_types)]
#![allow(clippy::missing_safety_doc)]
use crate::block_io::{AlignedDevice, BlockDevice, CallbackDevice, FileDevice};
use crate::dir::{self, DirEntryType};
use crate::error::errno::{EINVAL, EISDIR, ENAMETOOLONG, ENOENT, ENOSYS, ERANGE};
use crate::error::{Error, Result};
use crate::fs::{Filesystem, InodeRef};
use crate::inode::{Inode, S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK};
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; 17],
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_path<'a>(p: *const c_char, what: &str) -> Option<&'a [u8]> {
if p.is_null() {
set_err_msg(&format!("null {what}"), EINVAL);
return None;
}
let bytes = unsafe { CStr::from_ptr(p) }.to_bytes();
if bytes.len() > FFI_PATH_MAX {
set_err_msg(
&format!("{what} is longer than {FFI_PATH_MAX} bytes"),
ENAMETOOLONG,
);
return None;
}
Some(bytes)
}
unsafe fn cstr_to_name<'a>(p: *const c_char, what: &str) -> Option<&'a str> {
let bytes = unsafe { cstr_to_path(p, what) }?;
match std::str::from_utf8(bytes) {
Ok(s) => Some(s),
Err(_) => {
set_err_msg(
&format!("{what} is not valid UTF-8: {:?}", shown(bytes)),
EINVAL,
);
None
}
}
}
fn shown(path: &[u8]) -> std::borrow::Cow<'_, str> {
String::from_utf8_lossy(path)
}
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()
}
}
}),
)
}
const MAX_SECTOR_SIZE: u32 = 65536;
fn callback_mount_device(
dev: CallbackDevice,
cfg: &fs_ext4_blockdev_cfg_t,
) -> std::result::Result<Arc<dyn BlockDevice>, String> {
let sector = cfg.block_size;
if sector <= 1 {
return Ok(Arc::new(dev));
}
if !sector.is_power_of_two() || sector > MAX_SECTOR_SIZE {
return Err(format!(
"cfg.block_size {sector} is not a power of two in 2..={MAX_SECTOR_SIZE}"
));
}
if !cfg.size_bytes.is_multiple_of(sector as u64) {
return Err(format!(
"cfg.block_size {sector} does not divide cfg.size_bytes {}",
cfg.size_bytes
));
}
let dev = AlignedDevice::new(dev, sector);
let mut sb = [0u8; 1024];
if dev.read_at(1024, &mut sb).is_ok() && u16::from_le_bytes([sb[0x38], sb[0x39]]) == 0xEF53 {
let log = u32::from_le_bytes([sb[0x18], sb[0x19], sb[0x1A], sb[0x1B]]);
if let Some(fs_block) = 1024u32.checked_shl(log).filter(|_| log < 22) {
if sector > fs_block {
return Err(format!(
"cfg.block_size {sector} is larger than the filesystem block size {fs_block}"
));
}
}
}
Ok(Arc::new(dev))
}
#[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,
};
let dev = match callback_mount_device(dev, cfg) {
Ok(dev) => dev,
Err(msg) => {
set_err_msg(&msg, EINVAL);
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, "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 {
mount_fs_core_handle(handle, false)
}
#[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 {
mount_fs_core_handle(handle, true)
}
unsafe fn mount_fs_core_handle(
handle: *mut fs_core::ffi::FsCoreDevice,
lazy: bool,
) -> *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);
let (mounted, what) = if lazy {
(Filesystem::mount_lazy(dev), "mount_lazy via fs_core handle")
} else {
(Filesystem::mount(dev), "mount via fs_core handle")
};
match mounted {
Ok(fs) => Box::into_raw(Box::new(fs_ext4_fs_t { fs })),
Err(e) => {
set_err_from(&e, what);
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,
};
let dev = match callback_mount_device(dev, cfg) {
Ok(dev) => dev,
Err(msg) => {
set_err_msg(&msg, EINVAL);
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, "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,
};
let dev = match callback_mount_device(dev, cfg) {
Ok(dev) => dev,
Err(msg) => {
set_err_msg(&msg, EINVAL);
return std::ptr::null_mut();
}
};
match Filesystem::mount_lazy(dev) {
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 = &mut (*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_flush(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;
}
match (*fs).fs.flush() {
Ok(()) => 0,
Err(e) => {
set_err_from(&e, "flush");
-1
}
}
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_fresh_read(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;
}
match (*fs).fs.fresh_read() {
Ok(()) => 0,
Err(e) => {
set_err_from(&e, "fresh_read");
-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(16);
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
}),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_set_volume_label(
fs: *mut fs_ext4_fs_t,
label: *const c_char,
) -> c_int {
ffi_guard(
-1,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() || label.is_null() {
set_err_msg("null fs or label", EINVAL);
return -1;
}
let bytes = CStr::from_ptr(label).to_bytes();
match (*fs).fs.set_volume_label(bytes) {
Ok(()) => 0,
Err(e) => {
set_err_from(&e, "set_volume_label");
-1
}
}
}),
)
}
fn resolve_path(fs: &Filesystem, path: &[u8]) -> Result<u32> {
let ino = fs.lookup_path_bytes(path)?;
if path.ends_with(b"/") && path != b"/" {
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 Some(path_b) = (unsafe { cstr_to_path(path, "path") }) else {
return -1;
};
let path = shown(path_b);
let attr = &mut *attr;
match resolve_path(fs, path_b).and_then(|ino| stat_into(fs, ino.into(), attr)) {
Ok(()) => 0,
Err(e) => {
set_err_from(&e, &format!("stat {path}"));
-1
}
}
}),
)
}
#[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 Some(path_b) = (unsafe { cstr_to_path(path, "path") }) else {
return std::ptr::null_mut();
};
let path_str = shown(path_b);
match resolve_path(fs_ref, path_b).and_then(|ino| dir_iter(fs_ref, ino.into())) {
Ok(iter) => iter,
Err(e) => {
set_err_from(&e, &format!("dir_open {path_str}"));
std::ptr::null_mut()
}
}
}),
)
}
fn dir_iter(fs: &Filesystem, dir: InodeRef) -> Result<*mut fs_ext4_dir_iter_t> {
let entries = fs
.read_dir_ino(dir)?
.iter()
.map(dir_entry_to_bridge)
.collect();
Ok(Box::into_raw(Box::new(fs_ext4_dir_iter_t {
entries,
position: 0,
current: unsafe { std::mem::zeroed() },
})))
}
fn stat_into(fs: &Filesystem, r: InodeRef, attr: &mut fs_ext4_attr_t) -> Result<()> {
let inode = fs.stat_ino(r)?;
fill_attr(attr, r.ino, &inode);
Ok(())
}
unsafe fn read_into(
fs: &Filesystem,
r: InodeRef,
buf: *mut c_void,
offset: u64,
length: u64,
) -> Result<i64> {
let inode = fs.stat_ino(r)?;
if !inode.is_file() {
return Err(Error::InvalidArgument("not a regular file"));
}
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);
Ok(fs.read_ino(r, offset, out)? as i64)
}
unsafe fn readlink_into(target: &[u8], buf: *mut c_char, bufsize: usize, what: &str) -> c_int {
let needed = target.len() + 1;
if bufsize < needed {
set_err_msg(
&format!(
"readlink {what}: buffer of {bufsize} bytes is too small, \
{needed} needed (target and NUL)"
),
ERANGE,
);
return -1;
}
let out = std::slice::from_raw_parts_mut(buf.cast::<u8>(), needed);
out[..target.len()].copy_from_slice(target);
out[target.len()] = 0;
target.len() as c_int
}
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 Some(path_b) = (unsafe { cstr_to_path(path, "path") }) else {
return -1;
};
let path_str = shown(path_b);
let read = resolve_path(fs_ref, path_b)
.and_then(|ino| read_into(fs_ref, ino.into(), buf, offset, length));
match read {
Ok(n) => n,
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() {
set_err_msg("null fs/path/buf", EINVAL);
return -1;
}
let fs_ref = &(*fs).fs;
let Some(path_b) = (unsafe { cstr_to_path(path, "path") }) else {
return -1;
};
let path_str = shown(path_b);
let target = match resolve_path(fs_ref, path_b).and_then(|ino| fs_ref.read_link(ino)) {
Ok(t) => t,
Err(e) => {
set_err_from(&e, &format!("readlink {path_str}"));
return -1;
}
};
readlink_into(&target, buf, bufsize, &path_str)
}),
)
}
#[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 Some(path_b) = (unsafe { cstr_to_path(path, "path") }) else {
return -1;
};
let path_str = shown(path_b);
let ino = match resolve_path(fs_ref, path_b) {
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 names = match xattr::list_names(fs_ref, &inode, &inode_raw) {
Ok(v) => v,
Err(e) => {
set_err_from(&e, &format!("listxattr {path_str}"));
return -1;
}
};
let required: usize = names.iter().map(|n| n.len() + 1).sum();
if !buf.is_null() && bufsize > 0 {
let out = std::slice::from_raw_parts_mut(buf.cast::<u8>(), bufsize);
let mut pos = 0;
for name in &names {
let name_bytes = 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 Some(path_b) = (unsafe { cstr_to_path(path, "path") }) else {
return -1;
};
let path_str = shown(path_b);
let Some(name_str) = (unsafe { cstr_to_name(name, "name") }) else {
return -1;
};
let ino = match resolve_path(fs_ref, path_b) {
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_resolved(fs_ref, &inode, &inode_raw, 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 Some(path_b) = (unsafe { cstr_to_path(path, "path") }) else {
return -1;
};
let path_str = shown(path_b);
let res = resolve_path(fs_ref, path_b)
.and_then(|ino| fs_ref.apply_truncate_ino(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 Some(path_b) = (unsafe { cstr_to_path(path, "path") }) else {
return -1;
};
let path_str = shown(path_b);
let ino = match resolve_path(fs_ref, path_b) {
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 Some(path_b) = (unsafe { cstr_to_path(path, "path") }) else {
return -1;
};
let path_str = shown(path_b);
match fs_ref.apply_unlink_bytes(path_b) {
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 Some(path_b) = (unsafe { cstr_to_path(path, "path") }) else {
return 0u32;
};
let path_str = shown(path_b);
match fs_ref.apply_create_bytes(path_b, 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 Some(path_b) = (unsafe { cstr_to_path(path, "path") }) else {
return -1i64;
};
let path_str = shown(path_b);
let ino = match resolve_path(fs_ref, path_b) {
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_bytes(path_b, 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;
}
let slice = match write_arg(data, len, offset) {
Ok(s) => s,
Err(e) => {
set_err_from(&e, "pwrite");
return -1;
}
};
let fs_ref = &(*fs).fs;
let Some(path_b) = (unsafe { cstr_to_path(path, "path") }) else {
return -1i64;
};
let path_str = shown(path_b);
match fs_ref.apply_pwrite_bytes(path_b, offset, slice) {
Ok(new_size) => new_size as i64,
Err(e) => {
set_err_from(&e, &format!("pwrite {path_str} @{offset}+{len}"));
-1
}
}
}),
)
}
unsafe fn write_arg<'a>(data: *const c_void, len: u64, offset: u64) -> Result<&'a [u8]> {
if data.is_null() && len > 0 {
return Err(Error::InvalidArgument("null data with non-zero len"));
}
const MAX_PWRITE_LEN: u64 = 1 << 30;
if len > MAX_PWRITE_LEN {
return Err(Error::InvalidArgument("len exceeds the 1 GiB per-call cap"));
}
if offset.checked_add(len).is_none() {
return Err(Error::InvalidArgument("offset+len overflow"));
}
Ok(if len == 0 {
&[]
} else {
std::slice::from_raw_parts(data as *const u8, len as usize)
})
}
#[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 Some(src_b) = (unsafe { cstr_to_path(src, "src") }) else {
return -1;
};
let src_str = shown(src_b);
let Some(dst_b) = (unsafe { cstr_to_path(dst, "dst") }) else {
return -1;
};
let dst_str = shown(dst_b);
match fs_ref.apply_link_bytes(src_b, dst_b) {
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 Some(src_b) = (unsafe { cstr_to_path(src, "src") }) else {
return -1;
};
let src_str = shown(src_b);
let Some(dst_b) = (unsafe { cstr_to_path(dst, "dst") }) else {
return -1;
};
let dst_str = shown(dst_b);
match fs_ref.apply_rename_bytes(src_b, dst_b, 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;
fn rename_flags(flags: c_int) -> Result<bool> {
if flags & !FS_EXT4_RENAME_REPLACE != 0 {
return Err(Error::InvalidArgument("unknown rename flag bits"));
}
Ok(flags & FS_EXT4_RENAME_REPLACE != 0)
}
#[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 replace = match rename_flags(flags) {
Ok(r) => r,
Err(e) => {
set_err_from(&e, "rename2");
return -1;
}
};
let fs_ref = &(*fs).fs;
let Some(src_b) = (unsafe { cstr_to_path(src, "src") }) else {
return -1;
};
let src_str = shown(src_b);
let Some(dst_b) = (unsafe { cstr_to_path(dst, "dst") }) else {
return -1;
};
let dst_str = shown(dst_b);
match fs_ref.apply_rename_bytes(src_b, dst_b, 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 Some(path_b) = (unsafe { cstr_to_path(path, "path") }) else {
return 0u32;
};
let path_str = shown(path_b);
match fs_ref.apply_mkdir_bytes(path_b, 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 Some(path_b) = (unsafe { cstr_to_path(path, "path") }) else {
return -1;
};
let path_str = shown(path_b);
match fs_ref.apply_rmdir_bytes(path_b) {
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 Some(path_b) = (unsafe { cstr_to_path(path, "path") }) else {
return -1;
};
let path_str = shown(path_b);
match fs_ref.apply_chmod_bytes(path_b, 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 Some(path_b) = (unsafe { cstr_to_path(path, "path") }) else {
return -1;
};
let path_str = shown(path_b);
match fs_ref.apply_chown_bytes(path_b, 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 Some(path_b) = (unsafe { cstr_to_path(path, "path") }) else {
return 0u32;
};
let path_str = shown(path_b);
match fs_ref.apply_mknod_bytes(path_b, 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 Some(path_b) = (unsafe { cstr_to_path(path, "path") }) else {
return -1;
};
let path_str = shown(path_b);
match fs_ref.apply_set_flags_bytes(path_b, 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 Some(path_b) = (unsafe { cstr_to_path(path, "path") }) else {
return -1;
};
let path_str = shown(path_b);
match fs_ref.apply_utimens_bytes(path_b, 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 fs_ref = &(*fs).fs;
let Some(target_b) = (unsafe { cstr_to_path(target, "target") }) else {
return 0u32;
};
let target_str = shown(target_b);
let Some(linkpath_b) = (unsafe { cstr_to_path(linkpath, "linkpath") }) else {
return 0u32;
};
let linkpath_str = shown(linkpath_b);
match fs_ref.apply_symlink_bytes(target_b, linkpath_b) {
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 Some(path_b) = (unsafe { cstr_to_path(path, "path") }) else {
return -1;
};
let path_str = shown(path_b);
let Some(name_str) = (unsafe { cstr_to_name(name, "name") }) else {
return -1;
};
match fs_ref.apply_removexattr_bytes(path_b, 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 Some(path_b) = (unsafe { cstr_to_path(path, "path") }) else {
return -1;
};
let path_str = shown(path_b);
let Some(name_str) = (unsafe { cstr_to_name(name, "name") }) else {
return -1;
};
let value_bytes = if value_len == 0 {
&[][..]
} else {
std::slice::from_raw_parts(value as *const u8, value_len)
};
match fs_ref.apply_setxattr_bytes(path_b, name_str, value_bytes) {
Ok(()) => 0,
Err(e) => {
set_err_from(&e, &format!("setxattr {path_str} {name_str}"));
-1
}
}
}),
)
}
pub const FS_EXT4_GEN_ANY: u32 = u32::MAX;
fn inode_ref(ino: u32, generation: u32) -> InodeRef {
if generation == FS_EXT4_GEN_ANY {
InodeRef::any(ino)
} else {
InodeRef::new(ino, generation)
}
}
fn ino_call<T: Copy>(
fs: *mut fs_ext4_fs_t,
fail: T,
what: &dyn Fn() -> String,
body: impl FnOnce(&Filesystem) -> Result<T>,
) -> T {
ffi_guard(
fail,
AssertUnwindSafe(|| {
clear_last_error();
if fs.is_null() {
set_err_msg(&format!("{}: null fs", what()), EINVAL);
return fail;
}
match body(unsafe { &(*fs).fs }) {
Ok(v) => v,
Err(e) => {
set_err_from(&e, &what());
fail
}
}
}),
)
}
unsafe fn name_arg<'a>(name: *const c_char, name_len: usize) -> Result<&'a [u8]> {
if name.is_null() {
return Err(Error::InvalidArgument("null name"));
}
if name_len > FFI_PATH_MAX {
return Err(Error::NameTooLong);
}
Ok(std::slice::from_raw_parts(name.cast::<u8>(), name_len))
}
unsafe fn name_for_msg(name: *const c_char, name_len: usize) -> String {
match name_arg(name, name_len) {
Ok(n) => String::from_utf8_lossy(n).into_owned(),
Err(_) => "<invalid name>".into(),
}
}
fn attr_out(fs: &Filesystem, ino: u32, attr: *mut fs_ext4_attr_t) -> Result<c_int> {
if !attr.is_null() {
stat_into(fs, InodeRef::any(ino), unsafe { &mut *attr })?;
}
Ok(0)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_lookup_at(
fs: *mut fs_ext4_fs_t,
dir_ino: u32,
dir_generation: u32,
name: *const c_char,
name_len: usize,
attr: *mut fs_ext4_attr_t,
) -> c_int {
let what = || format!("lookup_at {dir_ino}/{}", name_for_msg(name, name_len));
ino_call(fs, -1, &what, |fs| {
if attr.is_null() {
return Err(Error::InvalidArgument("null attr"));
}
let ino = fs.lookup_at(
inode_ref(dir_ino, dir_generation),
name_arg(name, name_len)?,
)?;
stat_into(fs, InodeRef::any(ino), &mut *attr)?;
Ok(0)
})
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_stat_ino(
fs: *mut fs_ext4_fs_t,
ino: u32,
generation: u32,
attr: *mut fs_ext4_attr_t,
) -> c_int {
ino_call(fs, -1, &|| format!("stat_ino {ino}"), |fs| {
if attr.is_null() {
return Err(Error::InvalidArgument("null attr"));
}
stat_into(fs, inode_ref(ino, generation), &mut *attr)?;
Ok(0)
})
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_dir_open_ino(
fs: *mut fs_ext4_fs_t,
ino: u32,
generation: u32,
) -> *mut fs_ext4_dir_iter_t {
ino_call(
fs,
std::ptr::null_mut(),
&|| format!("dir_open_ino {ino}"),
|fs| dir_iter(fs, inode_ref(ino, generation)),
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_pread_ino(
fs: *mut fs_ext4_fs_t,
ino: u32,
generation: u32,
buf: *mut c_void,
offset: u64,
length: u64,
) -> i64 {
ino_call(fs, -1, &|| format!("pread_ino {ino}"), |fs| {
if buf.is_null() {
return Err(Error::InvalidArgument("null buf"));
}
read_into(fs, inode_ref(ino, generation), buf, offset, length)
})
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_readlink_ino(
fs: *mut fs_ext4_fs_t,
ino: u32,
generation: u32,
buf: *mut c_char,
bufsize: usize,
) -> c_int {
ino_call(fs, -1, &|| format!("readlink_ino {ino}"), |fs| {
if buf.is_null() {
return Err(Error::InvalidArgument("null buf"));
}
let target = fs.read_link_ino(inode_ref(ino, generation))?;
Ok(readlink_into(
&target,
buf,
bufsize,
&format!("inode {ino}"),
))
})
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_pwrite_ino(
fs: *mut fs_ext4_fs_t,
ino: u32,
generation: u32,
data: *const c_void,
len: u64,
offset: u64,
) -> i64 {
ino_call(
fs,
-1,
&|| format!("pwrite_ino {ino} @{offset}+{len}"),
|fs| {
let data = write_arg(data, len, offset)?;
Ok(fs.apply_pwrite_ino(inode_ref(ino, generation), offset, data)? as i64)
},
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_truncate_ino(
fs: *mut fs_ext4_fs_t,
ino: u32,
generation: u32,
new_size: u64,
) -> c_int {
ino_call(
fs,
-1,
&|| format!("truncate_ino {ino} -> {new_size}"),
|fs| {
fs.apply_truncate_ino(inode_ref(ino, generation), new_size)?;
Ok(0)
},
)
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_chmod_ino(
fs: *mut fs_ext4_fs_t,
ino: u32,
generation: u32,
mode: u16,
) -> c_int {
ino_call(fs, -1, &|| format!("chmod_ino {ino}"), |fs| {
fs.apply_chmod_ino(inode_ref(ino, generation), mode)?;
Ok(0)
})
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_chown_ino(
fs: *mut fs_ext4_fs_t,
ino: u32,
generation: u32,
uid: u32,
gid: u32,
) -> c_int {
ino_call(fs, -1, &|| format!("chown_ino {ino}"), |fs| {
fs.apply_chown_ino(inode_ref(ino, generation), uid, gid)?;
Ok(0)
})
}
#[no_mangle]
#[allow(clippy::too_many_arguments)]
pub unsafe extern "C" fn fs_ext4_utimens_ino(
fs: *mut fs_ext4_fs_t,
ino: u32,
generation: u32,
atime_sec: i64,
atime_nsec: u32,
mtime_sec: i64,
mtime_nsec: u32,
) -> c_int {
ino_call(fs, -1, &|| format!("utimens_ino {ino}"), |fs| {
fs.apply_utimens_ino(
inode_ref(ino, generation),
atime_sec,
atime_nsec,
mtime_sec,
mtime_nsec,
)?;
Ok(0)
})
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_create_at(
fs: *mut fs_ext4_fs_t,
dir_ino: u32,
dir_generation: u32,
name: *const c_char,
name_len: usize,
mode: u16,
attr: *mut fs_ext4_attr_t,
) -> c_int {
let what = || format!("create_at {dir_ino}/{}", name_for_msg(name, name_len));
ino_call(fs, -1, &what, |fs| {
let dir = inode_ref(dir_ino, dir_generation);
let ino = fs.apply_create_at(dir, name_arg(name, name_len)?, mode)?;
attr_out(fs, ino, attr)
})
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_mkdir_at(
fs: *mut fs_ext4_fs_t,
dir_ino: u32,
dir_generation: u32,
name: *const c_char,
name_len: usize,
mode: u16,
attr: *mut fs_ext4_attr_t,
) -> c_int {
let what = || format!("mkdir_at {dir_ino}/{}", name_for_msg(name, name_len));
ino_call(fs, -1, &what, |fs| {
let dir = inode_ref(dir_ino, dir_generation);
let ino = fs.apply_mkdir_at(dir, name_arg(name, name_len)?, mode)?;
attr_out(fs, ino, attr)
})
}
#[no_mangle]
#[allow(clippy::too_many_arguments)]
pub unsafe extern "C" fn fs_ext4_mknod_at(
fs: *mut fs_ext4_fs_t,
dir_ino: u32,
dir_generation: u32,
name: *const c_char,
name_len: usize,
mode: u16,
major: u32,
minor: u32,
attr: *mut fs_ext4_attr_t,
) -> c_int {
let what = || format!("mknod_at {dir_ino}/{}", name_for_msg(name, name_len));
ino_call(fs, -1, &what, |fs| {
let dir = inode_ref(dir_ino, dir_generation);
let ino = fs.apply_mknod_at(dir, name_arg(name, name_len)?, mode, major, minor)?;
attr_out(fs, ino, attr)
})
}
#[no_mangle]
#[allow(clippy::too_many_arguments)]
pub unsafe extern "C" fn fs_ext4_symlink_at(
fs: *mut fs_ext4_fs_t,
dir_ino: u32,
dir_generation: u32,
name: *const c_char,
name_len: usize,
target: *const c_char,
attr: *mut fs_ext4_attr_t,
) -> c_int {
let what = || format!("symlink_at {dir_ino}/{}", name_for_msg(name, name_len));
ino_call(fs, -1, &what, |fs| {
if target.is_null() {
return Err(Error::InvalidArgument("null target"));
}
let target = CStr::from_ptr(target).to_bytes();
if target.len() > FFI_PATH_MAX {
return Err(Error::NameTooLong);
}
let dir = inode_ref(dir_ino, dir_generation);
let ino = fs.apply_symlink_at(dir, name_arg(name, name_len)?, target)?;
attr_out(fs, ino, attr)
})
}
#[no_mangle]
#[allow(clippy::too_many_arguments)]
pub unsafe extern "C" fn fs_ext4_link_at(
fs: *mut fs_ext4_fs_t,
ino: u32,
generation: u32,
dir_ino: u32,
dir_generation: u32,
name: *const c_char,
name_len: usize,
attr: *mut fs_ext4_attr_t,
) -> c_int {
let what = || {
format!(
"link_at {ino} -> {dir_ino}/{}",
name_for_msg(name, name_len)
)
};
ino_call(fs, -1, &what, |fs| {
fs.apply_link_at(
inode_ref(ino, generation),
inode_ref(dir_ino, dir_generation),
name_arg(name, name_len)?,
)?;
attr_out(fs, ino, attr)
})
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_unlink_at(
fs: *mut fs_ext4_fs_t,
dir_ino: u32,
dir_generation: u32,
name: *const c_char,
name_len: usize,
) -> c_int {
let what = || format!("unlink_at {dir_ino}/{}", name_for_msg(name, name_len));
ino_call(fs, -1, &what, |fs| {
fs.apply_unlink_at(
inode_ref(dir_ino, dir_generation),
name_arg(name, name_len)?,
)?;
Ok(0)
})
}
#[no_mangle]
pub unsafe extern "C" fn fs_ext4_rmdir_at(
fs: *mut fs_ext4_fs_t,
dir_ino: u32,
dir_generation: u32,
name: *const c_char,
name_len: usize,
) -> c_int {
let what = || format!("rmdir_at {dir_ino}/{}", name_for_msg(name, name_len));
ino_call(fs, -1, &what, |fs| {
fs.apply_rmdir_at(
inode_ref(dir_ino, dir_generation),
name_arg(name, name_len)?,
)?;
Ok(0)
})
}
#[no_mangle]
#[allow(clippy::too_many_arguments)]
pub unsafe extern "C" fn fs_ext4_rename_at(
fs: *mut fs_ext4_fs_t,
src_dir_ino: u32,
src_dir_generation: u32,
src_name: *const c_char,
src_name_len: usize,
dst_dir_ino: u32,
dst_dir_generation: u32,
dst_name: *const c_char,
dst_name_len: usize,
flags: c_int,
) -> c_int {
let what = || {
format!(
"rename_at {src_dir_ino}/{} -> {dst_dir_ino}/{}",
name_for_msg(src_name, src_name_len),
name_for_msg(dst_name, dst_name_len)
)
};
ino_call(fs, -1, &what, |fs| {
let replace = rename_flags(flags)?;
fs.apply_rename_at(
inode_ref(src_dir_ino, src_dir_generation),
name_arg(src_name, src_name_len)?,
inode_ref(dst_dir_ino, dst_dir_generation),
name_arg(dst_name, dst_name_len)?,
replace,
)?;
Ok(0)
})
}
#[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 {
crate::mkfs::DEFAULT_BLOCK_SIZE
} 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}"))
}
&Anomaly::DirBlockChecksumMismatch {
dir_ino,
logical_block,
htree,
} => (
"dir_block_checksum",
dir_ino,
format!("logical_block={logical_block} htree={}", u8::from(htree)),
),
Anomaly::UnreadableDirectory { ino, reason } => {
("unreadable_directory", *ino, format!("reason={reason}"))
}
}
}
#[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 = &mut (*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
}
}
}),
)
}