diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 937d77c9..7f809029 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -13,7 +13,22 @@ name = "agentfs" path = "src/main.rs" [features] -default = ["sandbox"] +default = ["sandbox", "abi-7-39"] +abi-7-19 = [] +abi-7-20 = ["abi-7-19"] +abi-7-21 = ["abi-7-20"] +abi-7-22 = ["abi-7-21"] +abi-7-23 = ["abi-7-22"] +abi-7-24 = ["abi-7-23"] +abi-7-25 = ["abi-7-24"] +abi-7-26 = ["abi-7-25"] +abi-7-27 = ["abi-7-26"] +abi-7-28 = ["abi-7-27"] +abi-7-29 = ["abi-7-28"] +abi-7-30 = ["abi-7-29"] +abi-7-31 = ["abi-7-30"] +abi-7-36 = ["abi-7-31"] +abi-7-39 = ["abi-7-36"] sandbox = [ "dep:agentfs-sandbox", "dep:reverie", diff --git a/cli/src/fuse.rs b/cli/src/fuse.rs index 03080e6a..59ea32e0 100644 --- a/cli/src/fuse.rs +++ b/cli/src/fuse.rs @@ -4,8 +4,8 @@ use crate::fuser::{ FUSE_WRITEBACK_CACHE, }, FileAttr, FileType, Filesystem, KernelConfig, MountOption, ReplyAttr, ReplyCreate, ReplyData, - ReplyDirectory, ReplyDirectoryPlus, ReplyEmpty, ReplyEntry, ReplyOpen, ReplyStatfs, ReplyWrite, - Request, + ReplyDirectory, ReplyDirectoryPlus, ReplyEmpty, ReplyEntry, ReplyOpen, ReplyStatfs, ReplyStatx, + ReplyWrite, Request, }; use agentfs_sdk::error::Error as SdkError; use agentfs_sdk::{BoxedFile, FileSystem, Stats}; @@ -163,6 +163,35 @@ impl Filesystem for AgentFSFuse { } } + /// Retrieves extended file attributes (statx) for a given inode. + /// + /// This is similar to getattr but provides more detailed information including + /// birth time and file attributes. Available since FUSE ABI 7.39. + fn statx( + &mut self, + _req: &Request, + ino: u64, + _fh: Option, + _flags: u32, + mask: u32, + reply: ReplyStatx, + ) { + tracing::debug!("FUSE::statx: ino={}, mask={:#x}", ino, mask); + let Some(path) = self.get_path(ino) else { + reply.error(libc::ENOENT); + return; + }; + + let fs = self.fs.clone(); + let result = self.runtime.block_on(async move { fs.lstat(&path).await }); + + match result { + Ok(Some(stats)) => reply.statx(&TTL, &fillattr(&stats, self.uid, self.gid), mask), + Ok(None) => reply.error(libc::ENOENT), + Err(e) => reply.error(error_to_errno(&e)), + } + } + /// Reads the target of a symbolic link. /// /// Returns the path that the symlink points to. This is called by operations diff --git a/cli/src/fuser/ll/fuse_abi.rs b/cli/src/fuser/ll/fuse_abi.rs index dd46ed63..583d2a37 100644 --- a/cli/src/fuser/ll/fuse_abi.rs +++ b/cli/src/fuser/ll/fuse_abi.rs @@ -220,6 +220,38 @@ pub mod consts { // The read buffer is required to be at least 8k, but may be much larger pub const FUSE_MIN_READ_BUFFER: usize = 8192; + + // statx mask flags (FUSE ABI 7.39+) + #[cfg(feature = "abi-7-39")] + pub const STATX_TYPE: u32 = 0x00000001; + #[cfg(feature = "abi-7-39")] + pub const STATX_MODE: u32 = 0x00000002; + #[cfg(feature = "abi-7-39")] + pub const STATX_NLINK: u32 = 0x00000004; + #[cfg(feature = "abi-7-39")] + pub const STATX_UID: u32 = 0x00000008; + #[cfg(feature = "abi-7-39")] + pub const STATX_GID: u32 = 0x00000010; + #[cfg(feature = "abi-7-39")] + pub const STATX_ATIME: u32 = 0x00000020; + #[cfg(feature = "abi-7-39")] + pub const STATX_MTIME: u32 = 0x00000040; + #[cfg(feature = "abi-7-39")] + pub const STATX_CTIME: u32 = 0x00000080; + #[cfg(feature = "abi-7-39")] + pub const STATX_INO: u32 = 0x00000100; + #[cfg(feature = "abi-7-39")] + pub const STATX_SIZE: u32 = 0x00000200; + #[cfg(feature = "abi-7-39")] + pub const STATX_BLOCKS: u32 = 0x00000400; + #[cfg(feature = "abi-7-39")] + pub const STATX_BASIC_STATS: u32 = 0x000007ff; + #[cfg(feature = "abi-7-39")] + pub const STATX_BTIME: u32 = 0x00000800; + #[cfg(feature = "abi-7-39")] + pub const STATX_MNT_ID: u32 = 0x00001000; + #[cfg(feature = "abi-7-39")] + pub const STATX_ALL: u32 = 0x00000fff; } /// Invalid opcode error. @@ -280,6 +312,8 @@ pub enum fuse_opcode { FUSE_LSEEK = 46, #[cfg(feature = "abi-7-28")] FUSE_COPY_FILE_RANGE = 47, + #[cfg(feature = "abi-7-39")] + FUSE_STATX = 52, #[cfg(target_os = "macos")] FUSE_SETVOLNAME = 61, @@ -346,6 +380,8 @@ impl TryFrom for fuse_opcode { 46 => Ok(fuse_opcode::FUSE_LSEEK), #[cfg(feature = "abi-7-28")] 47 => Ok(fuse_opcode::FUSE_COPY_FILE_RANGE), + #[cfg(feature = "abi-7-39")] + 52 => Ok(fuse_opcode::FUSE_STATX), #[cfg(target_os = "macos")] 61 => Ok(fuse_opcode::FUSE_SETVOLNAME), @@ -999,3 +1035,65 @@ pub struct fuse_copy_file_range_in { pub len: u64, pub flags: u64, } + +/// Statx input structure (FUSE ABI 7.39+) +#[cfg(feature = "abi-7-39")] +#[repr(C)] +#[derive(Debug, FromBytes, KnownLayout, Immutable)] +pub struct fuse_statx_in { + pub getattr_flags: u32, + pub reserved: u32, + pub fh: u64, + pub sx_flags: u32, + pub sx_mask: u32, +} + +/// Statx time structure - bit-for-bit compatible with statx(2) ABI +#[cfg(feature = "abi-7-39")] +#[repr(C)] +#[derive(Debug, Clone, Copy, IntoBytes, FromBytes, KnownLayout, Immutable)] +pub struct fuse_sx_time { + pub tv_sec: i64, + pub tv_nsec: u32, + pub __reserved: i32, +} + +/// Statx structure - bit-for-bit compatible with statx(2) ABI +#[cfg(feature = "abi-7-39")] +#[repr(C)] +#[derive(Debug, Clone, Copy, IntoBytes, FromBytes, KnownLayout, Immutable)] +pub struct fuse_statx { + pub mask: u32, + pub blksize: u32, + pub attributes: u64, + pub nlink: u32, + pub uid: u32, + pub gid: u32, + pub mode: u16, + pub __spare0: [u16; 1], + pub ino: u64, + pub size: u64, + pub blocks: u64, + pub attributes_mask: u64, + pub atime: fuse_sx_time, + pub btime: fuse_sx_time, + pub ctime: fuse_sx_time, + pub mtime: fuse_sx_time, + pub rdev_major: u32, + pub rdev_minor: u32, + pub dev_major: u32, + pub dev_minor: u32, + pub __spare2: [u64; 14], +} + +/// Statx output structure (FUSE ABI 7.39+) +#[cfg(feature = "abi-7-39")] +#[repr(C)] +#[derive(Debug, IntoBytes, KnownLayout, Immutable)] +pub struct fuse_statx_out { + pub attr_valid: u64, + pub attr_valid_nsec: u32, + pub flags: u32, + pub spare: [u64; 2], + pub stat: fuse_statx, +} diff --git a/cli/src/fuser/ll/reply.rs b/cli/src/fuser/ll/reply.rs index 405df6d0..ac0d0804 100644 --- a/cli/src/fuser/ll/reply.rs +++ b/cli/src/fuser/ll/reply.rs @@ -262,6 +262,19 @@ impl<'a> Response<'a> { Self::from_struct(&r) } + /// Create a statx response (FUSE ABI 7.39+) + #[cfg(feature = "abi-7-39")] + pub(crate) fn new_statx(ttl: &Duration, statx: &StatxAttr) -> Self { + let r = abi::fuse_statx_out { + attr_valid: ttl.as_secs(), + attr_valid_nsec: ttl.subsec_nanos(), + flags: 0, + spare: [0; 2], + stat: statx.stat, + }; + Self::from_struct(&r) + } + pub(crate) fn from_struct(data: &T) -> Self { Self::Data(SmallVec::from_slice(data.as_bytes())) } @@ -348,6 +361,82 @@ impl From for Attr { } } +/// Statx attributes wrapper (FUSE ABI 7.39+) +#[cfg(feature = "abi-7-39")] +#[derive(Debug, Clone, Copy)] +pub struct StatxAttr { + pub(crate) stat: abi::fuse_statx, +} + +#[cfg(feature = "abi-7-39")] +impl StatxAttr { + /// Create a new StatxAttr from FileAttr + pub fn from_file_attr(attr: &super::super::FileAttr, mask: u32) -> Self { + use abi::consts::*; + + let (atime_secs, atime_nanos) = time_from_system_time(&attr.atime); + let (mtime_secs, mtime_nanos) = time_from_system_time(&attr.mtime); + let (ctime_secs, ctime_nanos) = time_from_system_time(&attr.ctime); + let (btime_secs, btime_nanos) = time_from_system_time(&attr.crtime); + + // Build the mask of which fields we're returning + let returned_mask = STATX_TYPE + | STATX_MODE + | STATX_NLINK + | STATX_UID + | STATX_GID + | STATX_ATIME + | STATX_MTIME + | STATX_CTIME + | STATX_INO + | STATX_SIZE + | STATX_BLOCKS + | STATX_BTIME; + + Self { + stat: abi::fuse_statx { + mask: mask & returned_mask, + blksize: attr.blksize, + attributes: 0, + nlink: attr.nlink, + uid: attr.uid, + gid: attr.gid, + mode: mode_from_kind_and_perm(attr.kind, attr.perm) as u16, + __spare0: [0; 1], + ino: attr.ino, + size: attr.size, + blocks: attr.blocks, + attributes_mask: 0, + atime: abi::fuse_sx_time { + tv_sec: atime_secs, + tv_nsec: atime_nanos, + __reserved: 0, + }, + btime: abi::fuse_sx_time { + tv_sec: btime_secs, + tv_nsec: btime_nanos, + __reserved: 0, + }, + ctime: abi::fuse_sx_time { + tv_sec: ctime_secs, + tv_nsec: ctime_nanos, + __reserved: 0, + }, + mtime: abi::fuse_sx_time { + tv_sec: mtime_secs, + tv_nsec: mtime_nanos, + __reserved: 0, + }, + rdev_major: (attr.rdev >> 8) & 0xfff, + rdev_minor: attr.rdev & 0xff, + dev_major: 0, + dev_minor: 0, + __spare2: [0; 14], + }, + } + } +} + #[derive(Debug)] /// A generic data buffer pub(crate) struct EntListBuf { diff --git a/cli/src/fuser/ll/request.rs b/cli/src/fuser/ll/request.rs index 519d10b5..c32e1d37 100644 --- a/cli/src/fuser/ll/request.rs +++ b/cli/src/fuser/ll/request.rs @@ -1597,6 +1597,38 @@ mod op { } impl_request!(CuseInit<'a>); + /// Get extended file attributes (statx). + /// Available since FUSE ABI 7.39. + #[cfg(feature = "abi-7-39")] + #[derive(Debug)] + pub struct Statx<'a> { + header: &'a fuse_in_header, + arg: &'a fuse_statx_in, + } + #[cfg(feature = "abi-7-39")] + impl_request!(Statx<'a>); + #[cfg(feature = "abi-7-39")] + impl Statx<'_> { + /// Returns the file handle if FUSE_GETATTR_FH flag is set + pub fn file_handle(&self) -> Option { + if self.arg.getattr_flags & FUSE_GETATTR_FH != 0 { + Some(FileHandle(self.arg.fh)) + } else { + None + } + } + + /// Returns the statx flags (AT_* flags passed to statx syscall) + pub fn flags(&self) -> u32 { + self.arg.sx_flags + } + + /// Returns the statx mask (STATX_* mask of requested fields) + pub fn mask(&self) -> u32 { + self.arg.sx_mask + } + } + fn system_time_from_time(secs: i64, nsecs: u32) -> SystemTime { if secs >= 0 { SystemTime::UNIX_EPOCH + Duration::new(secs as u64, nsecs) @@ -1815,6 +1847,11 @@ mod op { header, arg: data.fetch()?, }), + #[cfg(feature = "abi-7-39")] + fuse_opcode::FUSE_STATX => Operation::Statx(Statx { + header, + arg: data.fetch()?, + }), #[cfg(target_os = "macos")] fuse_opcode::FUSE_SETVOLNAME => Operation::SetVolName(SetVolName { @@ -1898,6 +1935,8 @@ pub enum Operation<'a> { Lseek(Lseek<'a>), #[cfg(feature = "abi-7-28")] CopyFileRange(CopyFileRange<'a>), + #[cfg(feature = "abi-7-39")] + Statx(Statx<'a>), #[cfg(target_os = "macos")] SetVolName(SetVolName<'a>), @@ -2084,6 +2123,14 @@ impl fmt::Display for Operation<'_> { x.dest(), x.len() ), + #[cfg(feature = "abi-7-39")] + Operation::Statx(x) => write!( + f, + "STATX fh {:?}, flags {:#x}, mask {:#x}", + x.file_handle(), + x.flags(), + x.mask() + ), #[cfg(target_os = "macos")] Operation::SetVolName(x) => write!(f, "SETVOLNAME name {:?}", x.name()), diff --git a/cli/src/fuser/mod.rs b/cli/src/fuser/mod.rs index a4b1432b..fda9b0cd 100644 --- a/cli/src/fuser/mod.rs +++ b/cli/src/fuser/mod.rs @@ -31,6 +31,8 @@ pub use ll::TimeOrNow; pub use mnt::mount_options::MountOption; pub use notify::{Notifier, PollHandle}; pub use reply::ReplyPoll; +#[cfg(feature = "abi-7-39")] +pub use reply::ReplyStatx; pub use reply::ReplyXattr; pub use reply::{Reply, ReplyAttr, ReplyData, ReplyEmpty, ReplyEntry, ReplyOpen}; pub use reply::{ @@ -763,6 +765,25 @@ pub trait Filesystem { ); reply.error(ENOSYS); } + + /// Get extended file attributes (statx). + /// Available since FUSE ABI 7.39. This provides more detailed file information + /// than getattr, including birth time and file attributes. + #[cfg(feature = "abi-7-39")] + fn statx( + &mut self, + _req: &Request<'_>, + ino: u64, + fh: Option, + flags: u32, + mask: u32, + reply: ReplyStatx, + ) { + warn!( + "[Not Implemented] statx(ino: {ino:#x?}, fh: {fh:?}, flags: {flags:#x}, mask: {mask:#x})" + ); + reply.error(ENOSYS); + } } /// Mount the given filesystem to the given mountpoint. This function will diff --git a/cli/src/fuser/reply.rs b/cli/src/fuser/reply.rs index bb067d8b..df481b44 100644 --- a/cli/src/fuser/reply.rs +++ b/cli/src/fuser/reply.rs @@ -217,6 +217,40 @@ impl ReplyAttr { } } +/// +/// Statx Reply (FUSE ABI 7.39+) +/// +#[cfg(feature = "abi-7-39")] +#[derive(Debug)] +pub struct ReplyStatx { + reply: ReplyRaw, +} + +#[cfg(feature = "abi-7-39")] +impl Reply for ReplyStatx { + fn new(unique: u64, sender: S) -> ReplyStatx { + ReplyStatx { + reply: Reply::new(unique, sender), + } + } +} + +#[cfg(feature = "abi-7-39")] +impl ReplyStatx { + /// Reply to a request with the given statx attributes. + /// The mask parameter indicates which fields were requested. + pub fn statx(self, ttl: &Duration, attr: &FileAttr, mask: u32) { + let statx_attr = ll::reply::StatxAttr::from_file_attr(attr, mask); + self.reply + .send_ll(&ll::Response::new_statx(ttl, &statx_attr)); + } + + /// Reply to a request with the given error code + pub fn error(self, err: c_int) { + self.reply.error(err); + } +} + /// /// XTimes Reply /// diff --git a/cli/src/fuser/request.rs b/cli/src/fuser/request.rs index a0a4bca0..7acb7cc5 100644 --- a/cli/src/fuser/request.rs +++ b/cli/src/fuser/request.rs @@ -577,6 +577,17 @@ impl<'a> Request<'a> { self.reply(), ); } + #[cfg(feature = "abi-7-39")] + ll::Operation::Statx(x) => { + se.filesystem.statx( + self, + self.request.nodeid().into(), + x.file_handle().map(std::convert::Into::into), + x.flags(), + x.mask(), + self.reply(), + ); + } #[cfg(target_os = "macos")] ll::Operation::SetVolName(x) => { se.filesystem.setvolname(self, x.name(), self.reply());