Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
33 changes: 31 additions & 2 deletions cli/src/fuse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<u64>,
_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
Expand Down
98 changes: 98 additions & 0 deletions cli/src/fuser/ll/fuse_abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -346,6 +380,8 @@ impl TryFrom<u32> 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),
Expand Down Expand Up @@ -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,
}
89 changes: 89 additions & 0 deletions cli/src/fuser/ll/reply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: IntoBytes + Immutable + ?Sized>(data: &T) -> Self {
Self::Data(SmallVec::from_slice(data.as_bytes()))
}
Expand Down Expand Up @@ -348,6 +361,82 @@ impl From<super::super::FileAttr> 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 {
Expand Down
47 changes: 47 additions & 0 deletions cli/src/fuser/ll/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<FileHandle> {
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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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>),
Expand Down Expand Up @@ -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()),
Expand Down
Loading