use std::{
ffi::{CStr, CString, OsStr, c_char, c_int, c_void},
os::unix::ffi::OsStrExt,
path::Path,
sync::Arc,
time::SystemTime,
};
use tracing::warn;
use crate::{
core::ContentAddressedMount,
error::{MountError, Result},
shell::{Attrs, DIR_UNIX_MODE, Entry, NodeId, NodeKind, PlatformShell},
};
pub mod c_abi;
use c_abi::{
HeddleEnumerateEmit, HeddleFSKitSessionHandle, heddle_fskit_is_available,
heddle_fskit_session_free, heddle_fskit_session_mount, heddle_fskit_session_new,
heddle_fskit_session_unmount,
};
pub struct FSKitShell {
handle: HeddleFSKitSessionHandle,
}
unsafe impl Send for FSKitShell {}
unsafe impl Sync for FSKitShell {}
impl FSKitShell {
pub fn new(mount: ContentAddressedMount) -> Self {
Self::from_shell(Arc::new(mount))
}
pub fn from_shell(shell: Arc<dyn PlatformShell + Send + Sync>) -> Self {
let boxed: Box<Arc<dyn PlatformShell + Send + Sync>> = Box::new(shell);
let user_data = Box::into_raw(boxed) as *mut c_void;
let handle = unsafe {
heddle_fskit_session_new(
user_data,
Some(trampoline_lookup),
Some(trampoline_getattr),
Some(trampoline_read),
Some(trampoline_write),
Some(trampoline_enumerate),
Some(trampoline_flush),
Some(trampoline_drop),
)
};
if handle.is_null() {
unsafe {
drop(Box::from_raw(
user_data as *mut Arc<dyn PlatformShell + Send + Sync>,
))
};
panic!("heddle_fskit_session_new returned null");
}
Self { handle }
}
pub fn is_runtime_available() -> bool {
unsafe { heddle_fskit_is_available() == 1 }
}
pub fn mount_background(self, mountpoint: impl AsRef<Path>) -> Result<FSKitSession> {
let path = mountpoint.as_ref();
let c_path = CString::new(path.as_os_str().as_bytes())
.map_err(|e| MountError::Stale(format!("mountpoint contains NUL: {e}")))?;
let handle = self.handle;
std::mem::forget(self);
let rc = unsafe { heddle_fskit_session_mount(handle, c_path.as_ptr()) };
if rc != 0 {
unsafe { heddle_fskit_session_free(handle) };
return Err(MountError::Store(objects::error::HeddleError::Io(
std::io::Error::from_raw_os_error(rc),
)));
}
Ok(FSKitSession {
handle: Some(handle),
})
}
}
impl Drop for FSKitShell {
fn drop(&mut self) {
if !self.handle.is_null() {
unsafe { heddle_fskit_session_free(self.handle) };
}
}
}
pub struct FSKitSession {
handle: Option<HeddleFSKitSessionHandle>,
}
unsafe impl Send for FSKitSession {}
unsafe impl Sync for FSKitSession {}
impl FSKitSession {
pub fn unmount(mut self) -> Result<()> {
let Some(handle) = self.handle.take() else {
return Ok(());
};
let rc = unsafe { heddle_fskit_session_unmount(handle) };
let unmount_err = if rc != 0 {
Some(MountError::Store(objects::error::HeddleError::Io(
std::io::Error::from_raw_os_error(rc),
)))
} else {
None
};
unsafe { heddle_fskit_session_free(handle) };
match unmount_err {
Some(err) => Err(err),
None => Ok(()),
}
}
}
impl Drop for FSKitSession {
fn drop(&mut self) {
let Some(handle) = self.handle.take() else {
return;
};
let rc = unsafe { heddle_fskit_session_unmount(handle) };
if rc != 0 {
warn!(rc, "fskit session unmount returned non-zero on drop");
}
unsafe { heddle_fskit_session_free(handle) };
}
}
#[inline]
unsafe fn shell_ref<'a>(
user_data: *mut c_void,
) -> Option<&'a Arc<dyn PlatformShell + Send + Sync>> {
if user_data.is_null() {
return None;
}
Some(unsafe { &*(user_data as *const Arc<dyn PlatformShell + Send + Sync>) })
}
fn errno_from(err: MountError) -> c_int {
err.to_errno()
}
unsafe extern "C" fn trampoline_lookup(
user_data: *mut c_void,
parent_inode: u64,
name_utf8: *const c_char,
out_child_inode: *mut u64,
out_unix_mode: *mut u32,
out_size: *mut u64,
) -> c_int {
unsafe {
let Some(shell) = shell_ref(user_data) else {
return libc::EINVAL;
};
if name_utf8.is_null() {
return libc::EINVAL;
}
let cstr = CStr::from_ptr(name_utf8);
let name: &OsStr = OsStr::from_bytes(cstr.to_bytes());
match shell.lookup(NodeId(parent_inode), name) {
Ok(Some(entry)) => {
write_out(out_child_inode, entry.node.0);
write_out(out_unix_mode, entry.unix_mode);
write_out(out_size, entry.size);
0
}
Ok(None) => libc::ENOENT,
Err(err) => errno_from(err),
}
}
}
unsafe extern "C" fn trampoline_getattr(
user_data: *mut c_void,
inode: u64,
out_unix_mode: *mut u32,
out_size: *mut u64,
out_nlink: *mut u32,
) -> c_int {
unsafe {
let Some(shell) = shell_ref(user_data) else {
return libc::EINVAL;
};
match shell.attrs(NodeId(inode)) {
Ok(attrs) => {
write_out(out_unix_mode, attrs.unix_mode);
write_out(out_size, attrs.size);
write_out(out_nlink, attrs.nlink);
0
}
Err(err) => errno_from(err),
}
}
}
unsafe extern "C" fn trampoline_read(
user_data: *mut c_void,
inode: u64,
offset: u64,
buffer: *mut u8,
buffer_capacity: u64,
out_bytes_read: *mut u64,
) -> c_int {
unsafe {
let Some(shell) = shell_ref(user_data) else {
return libc::EINVAL;
};
if buffer.is_null() {
return libc::EINVAL;
}
let cap = buffer_capacity as usize;
let buf = std::slice::from_raw_parts_mut(buffer, cap);
match shell.read(NodeId(inode), offset, buf) {
Ok(n) => {
write_out(out_bytes_read, n as u64);
0
}
Err(err) => errno_from(err),
}
}
}
unsafe extern "C" fn trampoline_write(
user_data: *mut c_void,
inode: u64,
offset: u64,
data: *const u8,
data_len: u64,
out_bytes_written: *mut u64,
) -> c_int {
unsafe {
let Some(shell) = shell_ref(user_data) else {
return libc::EINVAL;
};
if data.is_null() && data_len > 0 {
return libc::EINVAL;
}
let slice = if data_len == 0 {
&[][..]
} else {
std::slice::from_raw_parts(data, data_len as usize)
};
match shell.write(NodeId(inode), offset, slice) {
Ok(n) => {
write_out(out_bytes_written, n as u64);
0
}
Err(err) => errno_from(err),
}
}
}
unsafe extern "C" fn trampoline_enumerate(
user_data: *mut c_void,
dir_inode: u64,
emit_user_data: *mut c_void,
emit: HeddleEnumerateEmit,
) -> c_int {
unsafe {
let Some(shell) = shell_ref(user_data) else {
return libc::EINVAL;
};
let Some(emit) = emit else {
return libc::EINVAL;
};
let entries: Vec<Entry> = match shell.enumerate(NodeId(dir_inode)) {
Ok(e) => e,
Err(err) => return errno_from(err),
};
for entry in entries {
let bytes = entry.name.as_os_str().as_bytes();
let Ok(c_name) = CString::new(bytes) else {
warn!(?entry.name, "fskit enumerate: skipping entry with embedded NUL");
continue;
};
let rc = emit(
emit_user_data,
entry.node.0,
c_name.as_ptr(),
entry.unix_mode,
entry.size,
);
if rc != 0 {
break;
}
}
0
}
}
unsafe extern "C" fn trampoline_flush(user_data: *mut c_void, inode: u64) -> c_int {
unsafe {
let Some(shell) = shell_ref(user_data) else {
return libc::EINVAL;
};
match shell.flush(NodeId(inode)) {
Ok(()) => 0,
Err(err) => errno_from(err),
}
}
}
unsafe extern "C" fn trampoline_drop(user_data: *mut c_void) {
if user_data.is_null() {
return;
}
unsafe {
drop(Box::from_raw(
user_data as *mut Arc<dyn PlatformShell + Send + Sync>,
));
}
}
#[inline]
fn write_out<T: Copy>(ptr: *mut T, value: T) {
if !ptr.is_null() {
unsafe { ptr.write(value) };
}
}
#[allow(dead_code)]
fn _attrs_in_scope_check() -> Option<(Attrs, NodeKind, SystemTime, u32)> {
let _ = DIR_UNIX_MODE;
None
}
#[cfg(test)]
mod tests {
use std::{
sync::atomic::{AtomicUsize, Ordering},
time::UNIX_EPOCH,
};
use super::*;
struct CountingShell {
drops: Arc<AtomicUsize>,
}
impl Drop for CountingShell {
fn drop(&mut self) {
self.drops.fetch_add(1, Ordering::SeqCst);
}
}
impl PlatformShell for CountingShell {
fn lookup(&self, _parent: NodeId, _name: &OsStr) -> Result<Option<Entry>> {
Ok(None)
}
fn read(&self, _node: NodeId, _offset: u64, _buf: &mut [u8]) -> Result<usize> {
Ok(0)
}
fn write(&self, _node: NodeId, _offset: u64, _data: &[u8]) -> Result<usize> {
Err(MountError::ReadOnly)
}
fn enumerate(&self, _dir: NodeId) -> Result<Vec<Entry>> {
Ok(vec![])
}
fn attrs(&self, node: NodeId) -> Result<Attrs> {
Ok(Attrs {
node,
kind: NodeKind::Directory,
size: 0,
unix_mode: DIR_UNIX_MODE,
nlink: 2,
mtime: UNIX_EPOCH,
})
}
fn invalidate(&self, _node: NodeId) -> Result<()> {
Ok(())
}
}
#[cfg(target_os = "macos")]
#[test]
#[ignore = "requires the fskit feature; opt-in via --features fskit"]
fn fskit_session_lifecycle_drops_inner_shell() {
let drops = Arc::new(AtomicUsize::new(0));
let shell = Arc::new(CountingShell {
drops: Arc::clone(&drops),
});
let fskit = FSKitShell::from_shell(shell);
assert_eq!(drops.load(Ordering::SeqCst), 0);
drop(fskit);
assert_eq!(drops.load(Ordering::SeqCst), 1);
}
#[cfg(target_os = "macos")]
#[test]
#[ignore = "requires the fskit feature; opt-in via --features fskit"]
fn fskit_runtime_availability_is_callable() {
let _ = FSKitShell::is_runtime_available();
}
#[cfg(target_os = "macos")]
#[test]
#[ignore = "requires the fskit feature; opt-in via --features fskit"]
fn fskit_bridging_header_declares_every_c_abi_symbol() {
const EXPECTED_SYMBOLS: &[&str] = &[
"HeddleFSKitSessionHandle",
"HeddleLookupCallback",
"HeddleGetattrCallback",
"HeddleReadCallback",
"HeddleWriteCallback",
"HeddleEnumerateEmit",
"HeddleEnumerateCallback",
"HeddleFlushCallback",
"HeddleDropCallback",
"heddle_fskit_session_new",
"heddle_fskit_session_mount",
"heddle_fskit_session_unmount",
"heddle_fskit_session_free",
"heddle_fskit_is_available",
];
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("swift")
.join("HeddleFSKit")
.join("HeddleFSKit-Bridging.h");
let header = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
assert!(
header.contains("GENERATED FROM `crates/mount/src/fskit/c_abi.rs`"),
"bridging header missing the cbindgen 'GENERATED' banner; \
rerun `cargo build -p mount --features fskit`"
);
for sym in EXPECTED_SYMBOLS {
assert!(
header.contains(sym),
"bridging header missing `{sym}` — has the C ABI in \
src/fskit/c_abi.rs drifted? Re-run `cargo build -p \
mount --features fskit` to regenerate."
);
}
}
}