use std::{
ffi::OsStr,
panic::AssertUnwindSafe,
path::Path,
sync::Arc,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use fuser::{
BackgroundSession, BsdFileFlags, Config, Errno, FileAttr, FileHandle, FileType, Filesystem,
FopenFlags, Generation, INodeNo, InitFlags, KernelConfig, LockOwner, MountOption, OpenFlags,
RenameFlags, ReplyAttr, ReplyCreate, ReplyData, ReplyDirectory, ReplyEmpty, ReplyEntry,
ReplyOpen, ReplyWrite, Request, Session, TimeOrNow, WriteFlags,
};
use objects::object::FileMode;
use tracing::{debug, warn};
use crate::{
core::ContentAddressedMount,
error::Result,
shell::{AttrUpdate, Attrs, Entry, NodeId, NodeKind, PlatformShell, RenameOptions},
};
const TTL: Duration = Duration::from_secs(1);
const GENERATION: Generation = Generation(0);
pub struct FuseShell {
inner: Arc<ContentAddressedMount>,
}
impl FuseShell {
pub fn new(mount: ContentAddressedMount) -> Self {
Self {
inner: Arc::new(mount),
}
}
fn inner_mtime(&self) -> std::time::SystemTime {
self.inner
.attrs(NodeId::ROOT)
.map(|a| a.mtime)
.unwrap_or(UNIX_EPOCH)
}
pub fn mount_handle(&self) -> Arc<ContentAddressedMount> {
Arc::clone(&self.inner)
}
pub fn mount(self, mountpoint: impl AsRef<Path>) -> Result<()> {
let config = default_config();
fuser::mount2(self, mountpoint.as_ref(), &config)
.map_err(|e| crate::error::MountError::Store(objects::error::HeddleError::Io(e)))?;
Ok(())
}
pub fn mount_background(self, mountpoint: impl AsRef<Path>) -> Result<BackgroundSession> {
let config = default_config();
let session = Session::new(self, mountpoint.as_ref(), &config)
.map_err(|e| crate::error::MountError::Store(objects::error::HeddleError::Io(e)))?;
session
.spawn()
.map_err(|e| crate::error::MountError::Store(objects::error::HeddleError::Io(e)))
}
}
fn default_config() -> Config {
let mut config = Config::default();
config.mount_options = vec![MountOption::FSName("heddle-mount".into())];
config
}
fn process_uid() -> u32 {
unsafe { libc::getuid() }
}
fn process_gid() -> u32 {
unsafe { libc::getgid() }
}
fn file_mode_from_unix(mode: u32) -> FileMode {
if (mode & 0o111) != 0 {
FileMode::Executable
} else {
FileMode::Normal
}
}
fn file_type_for_kind(kind: NodeKind) -> FileType {
match kind {
NodeKind::Directory => FileType::Directory,
NodeKind::File => FileType::RegularFile,
NodeKind::Symlink => FileType::Symlink,
}
}
fn make_file_attr(
node: NodeId,
size: u64,
mtime: std::time::SystemTime,
kind: NodeKind,
unix_mode: u32,
nlink: u32,
) -> FileAttr {
FileAttr {
ino: INodeNo(node.0),
size,
blocks: size.div_ceil(512),
atime: mtime,
mtime,
ctime: mtime,
crtime: mtime,
kind: file_type_for_kind(kind),
perm: (unix_mode & 0o7777) as u16,
nlink,
uid: process_uid(),
gid: process_gid(),
rdev: 0,
blksize: 4096,
flags: 0,
}
}
fn file_attr_from(attrs: Attrs) -> FileAttr {
make_file_attr(
attrs.node,
attrs.size,
attrs.mtime,
attrs.kind,
attrs.unix_mode,
attrs.nlink,
)
}
fn entry_attr_from(entry: &Entry, mtime: std::time::SystemTime) -> FileAttr {
make_file_attr(
entry.node,
entry.size,
mtime,
entry.kind,
entry.unix_mode,
1,
)
}
fn errno_from_mount_error(err: crate::error::MountError) -> Errno {
Errno::from_i32(err.to_errno())
}
fn guard_call<T>(label: &'static str, f: impl FnOnce() -> Result<T>) -> Result<T> {
match std::panic::catch_unwind(AssertUnwindSafe(f)) {
Ok(result) => result,
Err(payload) => {
let msg = crate::error::panic_payload_str(&payload);
tracing::error!(callback = label, %msg, "FUSE callback panicked; returning EIO");
Err(crate::error::MountError::Store(
objects::error::HeddleError::Io(std::io::Error::other(format!(
"panic in FUSE {label}: {msg}"
))),
))
}
}
}
impl Filesystem for FuseShell {
fn init(&mut self, _req: &Request, config: &mut KernelConfig) -> std::io::Result<()> {
if let Err(unsupported) = config.add_capabilities(InitFlags::FUSE_DIRECT_IO_ALLOW_MMAP) {
debug!(
?unsupported,
"kernel does not support FUSE_DIRECT_IO_ALLOW_MMAP (requires 5.16+); \
shared mmap on mounted files will fail with ENODEV"
);
}
Ok(())
}
fn open(&self, _req: &Request, ino: INodeNo, _flags: OpenFlags, reply: ReplyOpen) {
if let Err(err) = guard_call("open", || self.inner.on_open(NodeId(ino.0))) {
warn!(
?err,
"on_open bookkeeping failed; orphan cleanup may misfire"
);
}
reply.opened(FileHandle(0), FopenFlags::FOPEN_DIRECT_IO);
}
fn lookup(&self, _req: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEntry) {
let result = guard_call("lookup", || {
let entry = self.inner.lookup(NodeId(parent.0), name)?;
match entry {
None => Ok(None),
Some(entry) => {
let mtime = self
.inner
.attrs(entry.node)
.map(|a| a.mtime)
.unwrap_or(UNIX_EPOCH);
Ok(Some(entry_attr_from(&entry, mtime)))
}
}
});
match result {
Ok(Some(attr)) => reply.entry(&TTL, &attr, GENERATION),
Ok(None) => reply.error(Errno::ENOENT),
Err(err) => reply.error(errno_from_mount_error(err)),
}
}
fn getattr(&self, _req: &Request, ino: INodeNo, _fh: Option<FileHandle>, reply: ReplyAttr) {
match guard_call("getattr", || self.inner.attrs(NodeId(ino.0))) {
Ok(attrs) => reply.attr(&TTL, &file_attr_from(attrs)),
Err(err) => reply.error(errno_from_mount_error(err)),
}
}
fn read(
&self,
_req: &Request,
ino: INodeNo,
_fh: FileHandle,
offset: u64,
size: u32,
_flags: OpenFlags,
_lock_owner: Option<LockOwner>,
reply: ReplyData,
) {
let result = guard_call("read", || {
let mut buf = vec![0u8; size as usize];
let n = self.inner.read(NodeId(ino.0), offset, &mut buf)?;
buf.truncate(n);
Ok(buf)
});
match result {
Ok(buf) => reply.data(&buf),
Err(err) => reply.error(errno_from_mount_error(err)),
}
}
fn readdir(
&self,
_req: &Request,
ino: INodeNo,
_fh: FileHandle,
offset: u64,
mut reply: ReplyDirectory,
) {
let prepared = guard_call("readdir", || {
let entries = self.inner.enumerate(NodeId(ino.0))?;
let mut all: Vec<(u64, FileType, std::ffi::OsString)> =
Vec::with_capacity(entries.len() + 2);
all.push((ino.0, FileType::Directory, ".".into()));
all.push((ino.0, FileType::Directory, "..".into()));
for entry in entries {
all.push((entry.node.0, file_type_for_kind(entry.kind), entry.name));
}
Ok(all)
});
let all = match prepared {
Ok(v) => v,
Err(err) => {
reply.error(errno_from_mount_error(err));
return;
}
};
for (i, (child_ino, kind, name)) in all.into_iter().enumerate().skip(offset as usize) {
let next_offset = (i + 1) as u64;
if reply.add(INodeNo(child_ino), next_offset, kind, &name) {
break;
}
}
reply.ok();
}
fn write(
&self,
_req: &Request,
ino: INodeNo,
_fh: FileHandle,
offset: u64,
data: &[u8],
_write_flags: WriteFlags,
_flags: OpenFlags,
_lock_owner: Option<LockOwner>,
reply: ReplyWrite,
) {
match guard_call("write", || self.inner.write(NodeId(ino.0), offset, data)) {
Ok(n) => reply.written(n as u32),
Err(err) => reply.error(errno_from_mount_error(err)),
}
}
fn flush(
&self,
_req: &Request,
ino: INodeNo,
_fh: FileHandle,
_lock_owner: LockOwner,
reply: ReplyEmpty,
) {
match guard_call("flush", || self.inner.flush(NodeId(ino.0))) {
Ok(()) => reply.ok(),
Err(err) => reply.error(errno_from_mount_error(err)),
}
}
fn release(
&self,
_req: &Request,
ino: INodeNo,
_fh: FileHandle,
_flags: OpenFlags,
_lock_owner: Option<LockOwner>,
_flush: bool,
reply: ReplyEmpty,
) {
match guard_call("release", || self.inner.release(NodeId(ino.0))) {
Ok(()) => reply.ok(),
Err(err) => reply.error(errno_from_mount_error(err)),
}
}
fn create(
&self,
_req: &Request,
parent: INodeNo,
name: &OsStr,
mode: u32,
_umask: u32,
flags: i32,
reply: ReplyCreate,
) {
let exclusive = (flags & libc::O_EXCL) != 0;
let file_mode = file_mode_from_unix(mode);
let result = guard_call("create", || {
self.inner
.create_file(NodeId(parent.0), name, file_mode, exclusive)
});
match result {
Ok(entry) => {
if let Err(err) = guard_call("create", || self.inner.on_open(entry.node)) {
warn!(
?err,
"on_open bookkeeping failed; orphan cleanup may misfire"
);
}
let attr = match guard_call("create", || self.inner.attrs(entry.node)) {
Ok(attrs) => file_attr_from(attrs),
Err(err) => {
reply.error(errno_from_mount_error(err));
return;
}
};
reply.created(
&TTL,
&attr,
GENERATION,
FileHandle(0),
FopenFlags::FOPEN_DIRECT_IO,
);
}
Err(err) => reply.error(errno_from_mount_error(err)),
}
}
fn mkdir(
&self,
_req: &Request,
parent: INodeNo,
name: &OsStr,
_mode: u32,
_umask: u32,
reply: ReplyEntry,
) {
let result = guard_call("mkdir", || self.inner.make_dir(NodeId(parent.0), name));
match result {
Ok(entry) => {
let attr = entry_attr_from(&entry, self.inner_mtime());
reply.entry(&TTL, &attr, GENERATION);
}
Err(err) => reply.error(errno_from_mount_error(err)),
}
}
fn mknod(
&self,
_req: &Request,
parent: INodeNo,
name: &OsStr,
mode: u32,
_umask: u32,
_rdev: u32,
reply: ReplyEntry,
) {
let kind = mode & libc::S_IFMT;
if kind != libc::S_IFREG && kind != 0 {
reply.error(Errno::from_i32(libc::EPERM));
return;
}
let file_mode = file_mode_from_unix(mode);
let result = guard_call("mknod", || {
self.inner
.create_file(NodeId(parent.0), name, file_mode, true)
});
match result {
Ok(entry) => {
let attr = entry_attr_from(&entry, self.inner_mtime());
reply.entry(&TTL, &attr, GENERATION);
}
Err(err) => reply.error(errno_from_mount_error(err)),
}
}
fn unlink(&self, _req: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEmpty) {
match guard_call("unlink", || self.inner.unlink_entry(NodeId(parent.0), name)) {
Ok(()) => reply.ok(),
Err(err) => reply.error(errno_from_mount_error(err)),
}
}
fn rmdir(&self, _req: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEmpty) {
match guard_call("rmdir", || self.inner.rmdir_entry(NodeId(parent.0), name)) {
Ok(()) => reply.ok(),
Err(err) => reply.error(errno_from_mount_error(err)),
}
}
fn rename(
&self,
_req: &Request,
parent: INodeNo,
name: &OsStr,
newparent: INodeNo,
newname: &OsStr,
flags: RenameFlags,
reply: ReplyEmpty,
) {
if flags.contains(RenameFlags::RENAME_EXCHANGE)
|| flags.contains(RenameFlags::RENAME_WHITEOUT)
{
reply.error(Errno::from_i32(libc::EINVAL));
return;
}
let no_replace = flags.contains(RenameFlags::RENAME_NOREPLACE);
let options = RenameOptions { no_replace };
match guard_call("rename", || {
self.inner.rename_entry_with_options(
NodeId(parent.0),
name,
NodeId(newparent.0),
newname,
options,
)
}) {
Ok(()) => reply.ok(),
Err(err) => reply.error(errno_from_mount_error(err)),
}
}
#[allow(clippy::too_many_arguments)]
fn setattr(
&self,
_req: &Request,
ino: INodeNo,
mode: Option<u32>,
uid: Option<u32>,
gid: Option<u32>,
size: Option<u64>,
_atime: Option<TimeOrNow>,
mtime: Option<TimeOrNow>,
_ctime: Option<SystemTime>,
_fh: Option<FileHandle>,
_crtime: Option<SystemTime>,
_chgtime: Option<SystemTime>,
_bkuptime: Option<SystemTime>,
_flags: Option<BsdFileFlags>,
reply: ReplyAttr,
) {
let mtime_sec = mtime.and_then(|t| match t {
TimeOrNow::SpecificTime(st) => st
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.ok(),
TimeOrNow::Now => SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.ok(),
});
let update = AttrUpdate {
mode,
uid,
gid,
size,
mtime_sec,
};
match guard_call("setattr", || self.inner.set_attrs(NodeId(ino.0), update)) {
Ok(attrs) => reply.attr(&TTL, &file_attr_from(attrs)),
Err(err) => reply.error(errno_from_mount_error(err)),
}
}
fn symlink(
&self,
_req: &Request,
parent: INodeNo,
link_name: &OsStr,
target: &Path,
reply: ReplyEntry,
) {
let result = guard_call("symlink", || {
self.inner
.create_symlink(NodeId(parent.0), link_name, target)
});
match result {
Ok(entry) => {
let attr = entry_attr_from(&entry, self.inner_mtime());
reply.entry(&TTL, &attr, GENERATION);
}
Err(err) => reply.error(errno_from_mount_error(err)),
}
}
fn readlink(&self, _req: &Request, ino: INodeNo, reply: ReplyData) {
use std::os::unix::ffi::OsStrExt;
match guard_call("readlink", || self.inner.read_link(NodeId(ino.0))) {
Ok(target) => reply.data(target.as_os_str().as_bytes()),
Err(err) => reply.error(errno_from_mount_error(err)),
}
}
fn link(
&self,
_req: &Request,
_ino: INodeNo,
_newparent: INodeNo,
_newname: &OsStr,
reply: ReplyEntry,
) {
reply.error(Errno::from_i32(libc::EPERM));
}
fn destroy(&mut self) {
warn!(
thread = %self.inner.thread(),
"fuse mount destroyed"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::mocks::PanicShell;
#[test]
fn guard_call_translates_panic_to_eio() {
let result: Result<()> = guard_call("test", || {
panic!("simulated mutex poison");
});
let err = result.expect_err("expected guard_call to return Err on panic");
assert_eq!(
err.to_errno(),
libc::EIO,
"panic must translate to EIO, got errno {} ({err})",
err.to_errno()
);
}
#[test]
fn guard_call_passes_through_ok() {
let result: Result<i32> = guard_call("test", || Ok(42));
assert_eq!(result.expect("ok"), 42);
}
#[test]
fn panic_shell_dispatch_yields_eio() {
let shell = Arc::new(PanicShell) as Arc<dyn PlatformShell + Send + Sync>;
let result: Result<usize> = guard_call("read", || shell.read(NodeId(1), 0, &mut [0u8; 4]));
let err = result.expect_err("expected PanicShell to panic");
assert_eq!(err.to_errno(), libc::EIO);
}
}