From f405ff03e48d9ead3249936236c5e3abfe199870 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Thu, 13 Aug 2026 11:34:58 +0800 Subject: [PATCH] feat(fspy-nostd): add Unix file lifecycle operations Co-authored-by: GPT-5 Codex --- crates/fspy_nostd/src/fs/linux.rs | 18 +++++++++- crates/fspy_nostd/src/fs/mac.rs | 17 +++++++++- crates/fspy_nostd/src/fs/mod.rs | 11 +++++- crates/fspy_nostd/src/lib.rs | 2 +- crates/fspy_shm/src/unix.rs | 56 ++++++++++++++++--------------- 5 files changed, 73 insertions(+), 31 deletions(-) diff --git a/crates/fspy_nostd/src/fs/linux.rs b/crates/fspy_nostd/src/fs/linux.rs index fb94da7ba..a1a68e758 100644 --- a/crates/fspy_nostd/src/fs/linux.rs +++ b/crates/fspy_nostd/src/fs/linux.rs @@ -4,7 +4,7 @@ use rustix::fd::FromRawFd as _; use crate::{ AsRawFd as _, BorrowedFd, CStr, Error, Fat, OwnedFd, Result, Thin, - fs::{Mode, OFlags}, + fs::{AtFlags, Mode, OFlags}, }; // Linux UAPI `PATH_MAX`. @@ -40,6 +40,22 @@ pub(super) fn openat( Ok(unsafe { OwnedFd::from_raw_fd(fd) }) } +#[expect(clippy::needless_pass_by_value, reason = "CStr is a borrowed value type")] +pub(super) fn unlinkat(dirfd: BorrowedFd<'_>, path: CStr<'_, R>, flags: AtFlags) -> Result<()> { + // SAFETY: `dirfd` remains borrowed and `path` is NUL-terminated for the + // syscall. + unsafe { + syscalls::syscall3( + syscalls::Sysno::unlinkat, + syscall_fd(dirfd)?, + path.as_ptr().addr(), + usize::try_from(flags.bits()).map_err(|_| Error::INVAL)?, + ) + } + .map_err(|errno| Error::from_raw_os_error(errno.into_raw()))?; + Ok(()) +} + /// Reads the target of `path` relative to `dirfd` into `buf`. /// /// The returned bytes borrow the initialized prefix of `buf`. As with the diff --git a/crates/fspy_nostd/src/fs/mac.rs b/crates/fspy_nostd/src/fs/mac.rs index e35b5fba8..bd31513fa 100644 --- a/crates/fspy_nostd/src/fs/mac.rs +++ b/crates/fspy_nostd/src/fs/mac.rs @@ -2,7 +2,7 @@ use core::{mem::MaybeUninit, slice}; use rustix::{ fd::{AsFd as _, AsRawFd as _, FromRawFd as _, OwnedFd}, - fs::{Mode, OFlags}, + fs::{AtFlags, Mode, OFlags}, }; use crate::{BorrowedFd, CStr, CWD, Error, Fat, Result, Thin}; @@ -37,6 +37,21 @@ pub(super) fn openat( Ok(unsafe { OwnedFd::from_raw_fd(fd) }) } +#[expect(clippy::needless_pass_by_value, reason = "CStr is a borrowed value type")] +pub(super) fn unlinkat(dirfd: BorrowedFd<'_>, path: CStr<'_, R>, flags: AtFlags) -> Result<()> { + // SAFETY: `dirfd` remains borrowed and `path` is NUL-terminated for the + // call. + let result = unsafe { + libc::unlinkat(dirfd.as_raw_fd(), path.as_ptr().cast(), flags.bits().cast_signed()) + }; + if result == -1 { + // SAFETY: libSystem stored this call's error before returning -1. + Err(Error::from_raw_os_error(unsafe { *libc::__error() })) + } else { + Ok(()) + } +} + /// Gets the path associated with `fd`. /// /// `F_GETPATH` writes a NUL-terminated path into its `MAXPATHLEN` buffer but diff --git a/crates/fspy_nostd/src/fs/mod.rs b/crates/fspy_nostd/src/fs/mod.rs index 84c1211b4..3720e13bd 100644 --- a/crates/fspy_nostd/src/fs/mod.rs +++ b/crates/fspy_nostd/src/fs/mod.rs @@ -2,7 +2,7 @@ use core::mem::MaybeUninit; -pub use rustix::fs::{Mode, OFlags, fstat}; +pub use rustix::fs::{AtFlags, Mode, OFlags, fstat, ftruncate}; use crate::{BorrowedFd, CStr, Fat, OwnedFd, Result}; @@ -37,6 +37,15 @@ pub fn openat( imp::openat(dirfd, path, flags, mode) } +/// Removes `path` relative to `dirfd`. +/// +/// # Errors +/// +/// Returns the error reported by `unlinkat`. +pub fn unlinkat(dirfd: BorrowedFd<'_>, path: CStr<'_, R>, flags: AtFlags) -> Result<()> { + imp::unlinkat(dirfd, path, flags) +} + /// Writes the absolute pathname of the current working directory into `buf`. /// /// The returned C string borrows `buf`, starts at the same address as `buf`, diff --git a/crates/fspy_nostd/src/lib.rs b/crates/fspy_nostd/src/lib.rs index 545ad8fc4..779889d51 100644 --- a/crates/fspy_nostd/src/lib.rs +++ b/crates/fspy_nostd/src/lib.rs @@ -65,7 +65,7 @@ macro_rules! wide_cstr { #[cfg(unix)] pub use rustix::{ - fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd}, + fd::{AsRawFd, BorrowedFd, OwnedFd}, fs::CWD, io::Errno as Error, }; diff --git a/crates/fspy_shm/src/unix.rs b/crates/fspy_shm/src/unix.rs index 5fbfbf57c..4090ae69b 100644 --- a/crates/fspy_shm/src/unix.rs +++ b/crates/fspy_shm/src/unix.rs @@ -4,10 +4,9 @@ use std::{ env::temp_dir, ffi::OsStr, - fs::{self, File, OpenOptions}, io, num::NonZeroUsize, - os::unix::{ffi::OsStrExt as _, fs::OpenOptionsExt as _, io::IntoRawFd as _}, + os::unix::ffi::OsStrExt as _, path::PathBuf, ptr::{self, NonNull}, }; @@ -75,19 +74,20 @@ pub fn create(size: usize) -> io::Result<(ShmKeeper, ShmHandle)> { let path = std::path::absolute(temp_dir())? .join(format!("{BACKING_PREFIX}{}.shm", Uuid::new_v4().simple())); - let file = OpenOptions::new() - .read(true) - .write(true) - .create_new(true) + let file = open_file( + path.as_os_str(), + fspy_nostd::fs::OFlags::RDWR + | fspy_nostd::fs::OFlags::CREATE + | fspy_nostd::fs::OFlags::EXCL + | fspy_nostd::fs::OFlags::CLOEXEC, // Only the creating user may open the mapping. - .mode(0o600) - .open(&path)?; + fspy_nostd::fs::Mode::RUSR | fspy_nostd::fs::Mode::WUSR, + )?; // The keeper exists from here on, so every error path below cleans up. let keeper = ShmKeeper { path }; // Every byte reads as zero because the file is all holes. - file.set_len(size_u64)?; - let file = into_nostd_fd(file); + fspy_nostd::fs::ftruncate(&file, size_u64).map_err(error_to_io)?; Ok((keeper, ShmHandle { file, size })) } @@ -102,7 +102,11 @@ pub fn create(size: usize) -> io::Result<(ShmKeeper, ShmHandle)> { /// Returns an error if the shared memory is unavailable, which is the common /// case once its keeper has been dropped. pub fn open(id: &OsStr) -> io::Result { - let file = open_file(id)?; + let file = open_file( + id, + fspy_nostd::fs::OFlags::RDWR | fspy_nostd::fs::OFlags::CLOEXEC, + fspy_nostd::fs::Mode::empty(), + )?; // If another process shrinks the file before `map`, mapping fails. If it // resizes afterwards, nothing here touches the mapped pages. A concurrent // resize cannot make a mapping access invalid memory. @@ -113,16 +117,21 @@ pub fn open(id: &OsStr) -> io::Result { Ok(ShmHandle { file, size }) } -fn open_file(path: &OsStr) -> io::Result { +fn open_file( + path: &OsStr, + flags: fspy_nostd::fs::OFlags, + mode: fspy_nostd::fs::Mode, +) -> io::Result { let mut buf = [0_u8; fspy_nostd::fs::PATH_MAX]; let path = copy_path(path, &mut buf)?; - fspy_nostd::fs::openat( - fspy_nostd::CWD, - path, - fspy_nostd::fs::OFlags::RDWR | fspy_nostd::fs::OFlags::CLOEXEC, - fspy_nostd::fs::Mode::empty(), - ) - .map_err(error_to_io) + fspy_nostd::fs::openat(fspy_nostd::CWD, path, flags, mode).map_err(error_to_io) +} + +fn remove_file(path: &OsStr) -> io::Result<()> { + let mut buf = [0_u8; fspy_nostd::fs::PATH_MAX]; + let path = copy_path(path, &mut buf)?; + fspy_nostd::fs::unlinkat(fspy_nostd::CWD, path, fspy_nostd::fs::AtFlags::empty()) + .map_err(error_to_io) } fn copy_path<'buf>( @@ -150,16 +159,9 @@ fn error_to_io(error: fspy_nostd::Error) -> io::Error { io::Error::from_raw_os_error(error.raw_os_error()) } -fn into_nostd_fd(file: File) -> fspy_nostd::OwnedFd { - let fd = file.into_raw_fd(); - // SAFETY: ownership of `file`'s descriptor transfers without closing or - // duplicating it. - unsafe { fspy_nostd::FromRawFd::from_raw_fd(fd) } -} - impl Drop for ShmKeeper { fn drop(&mut self) { - let _ = fs::remove_file(&self.path); + let _ = remove_file(self.path.as_os_str()); } }