From 41b82b5c67fac46d1edfb1e087c4a9a2d93617fb Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Fri, 21 Feb 2025 09:20:07 -0800 Subject: [PATCH] Add support for `renameat_with` on Apple platforms Apple platforms (both iOS and macOS) added a `renameat2` equivalent in 10.12 (and the iOS equivalent). This patch uses that syscall to implement `renameat2`, and subsequently `renameat_with`. Unlike Linux, the `RenameFlags::WHITEOUT` flag isn't supported. At the moment, any programs trying to use that flag will fail to compile on Apple operating systems. --- src/backend/libc/fs/syscalls.rs | 45 ++++++++++++++++++++++++++++++- src/backend/libc/fs/types.rs | 19 +++++++++++++ src/fs/at.rs | 5 ++-- tests/fs/renameat.rs | 48 +++++++++++++-------------------- 4 files changed, 85 insertions(+), 32 deletions(-) diff --git a/src/backend/libc/fs/syscalls.rs b/src/backend/libc/fs/syscalls.rs index 74108a7bb..9f5315074 100644 --- a/src/backend/libc/fs/syscalls.rs +++ b/src/backend/libc/fs/syscalls.rs @@ -25,6 +25,8 @@ use crate::fs::FallocateFlags; use crate::fs::FlockOperation; #[cfg(any(linux_kernel, target_os = "freebsd"))] use crate::fs::MemfdFlags; +#[cfg(any(linux_kernel, apple))] +use crate::fs::RenameFlags; #[cfg(any(linux_kernel, target_os = "freebsd", target_os = "fuchsia"))] use crate::fs::SealFlags; #[cfg(not(any( @@ -79,7 +81,7 @@ use {crate::fs::Advice, core::num::NonZeroU64}; use {crate::fs::XattrFlags, core::mem::size_of, core::ptr::null_mut}; #[cfg(linux_kernel)] use { - crate::fs::{RenameFlags, ResolveFlags, Statx, StatxFlags, CWD}, + crate::fs::{ResolveFlags, Statx, StatxFlags, CWD}, core::ptr::null, }; @@ -563,6 +565,47 @@ pub(crate) fn renameat2( } } +#[cfg(apple)] +pub(crate) fn renameat2( + old_dirfd: BorrowedFd<'_>, + old_path: &CStr, + new_dirfd: BorrowedFd<'_>, + new_path: &CStr, + flags: RenameFlags, +) -> io::Result<()> { + unsafe { + // macOS < 10.12 lacks `renameatx_np`. + weak! { + fn renameatx_np( + c::c_int, + *const ffi::c_char, + c::c_int, + *const ffi::c_char, + c::c_uint + ) -> c::c_int + } + // If we have `renameatx_np`, use it. + if let Some(libc_renameatx_np) = renameatx_np.get() { + return ret(libc_renameatx_np( + borrowed_fd(old_dirfd), + c_str(old_path), + borrowed_fd(new_dirfd), + c_str(new_path), + flags.bits(), + )); + } + // Otherwise, see if we can use rename. There's no point in trying `renamex_np` + // because it was added in the same macOS release as `renameatx_np`. + if !flags.is_empty() + || borrowed_fd(old_dirfd) != c::AT_FDCWD + || borrowed_fd(new_dirfd) != c::AT_FDCWD + { + return Err(io::Errno::NOSYS); + } + ret(c::rename(c_str(old_path), c_str(new_path))) + } +} + pub(crate) fn symlink(old_path: &CStr, new_path: &CStr) -> io::Result<()> { unsafe { ret(c::symlink(c_str(old_path), c_str(new_path))) } } diff --git a/src/backend/libc/fs/types.rs b/src/backend/libc/fs/types.rs index 7ab9f64fa..730eb896b 100644 --- a/src/backend/libc/fs/types.rs +++ b/src/backend/libc/fs/types.rs @@ -475,6 +475,25 @@ bitflags! { } } +#[cfg(apple)] +bitflags! { + /// `RENAME_*` constants for use with [`renameat_with`]. + /// + /// [`renameat_with`]: crate::fs::renameat_with + #[repr(transparent)] + #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] + pub struct RenameFlags: ffi::c_uint { + /// `RENAME_SWAP` + const EXCHANGE = bitcast!(c::RENAME_SWAP); + + /// `RENAME_EXCL` + const NOREPLACE = bitcast!(c::RENAME_EXCL); + + /// + const _ = !0; + } +} + /// `S_IF*` constants for use with [`mknodat`] and [`Stat`]'s `st_mode` field. /// /// [`mknodat`]: crate::fs::mknodat diff --git a/src/fs/at.rs b/src/fs/at.rs index ef03761a0..140f821a0 100644 --- a/src/fs/at.rs +++ b/src/fs/at.rs @@ -14,7 +14,7 @@ use crate::fs::Access; use crate::fs::AtFlags; #[cfg(apple)] use crate::fs::CloneFlags; -#[cfg(linux_kernel)] +#[cfg(any(linux_kernel, apple))] use crate::fs::RenameFlags; #[cfg(not(target_os = "espidf"))] use crate::fs::Stat; @@ -277,9 +277,10 @@ pub fn renameat( /// - [Linux] /// /// [Linux]: https://man7.org/linux/man-pages/man2/renameat2.2.html -#[cfg(linux_kernel)] +#[cfg(any(apple, linux_kernel))] #[inline] #[doc(alias = "renameat2")] +#[doc(alias = "renameatx_np")] pub fn renameat_with( old_dirfd: PFd, old_path: P, diff --git a/tests/fs/renameat.rs b/tests/fs/renameat.rs index c71620cab..398612855 100644 --- a/tests/fs/renameat.rs +++ b/tests/fs/renameat.rs @@ -4,9 +4,17 @@ fn same(a: &Stat, b: &Stat) -> bool { a.st_ino == b.st_ino && a.st_dev == b.st_dev } +#[cfg(test)] +use rustix::fs::OFlags; + +#[cfg(all(test, linux_kernel))] +const DIR_OPEN_FLAGS: OFlags = OFlags::RDONLY.union(OFlags::PATH); +#[cfg(all(test, apple))] +const DIR_OPEN_FLAGS: OFlags = OFlags::RDONLY; + #[test] fn test_rename() { - use rustix::fs::{access, open, rename, stat, Access, Mode, OFlags}; + use rustix::fs::{access, open, rename, stat, Access, Mode}; let tmp = tempfile::tempdir().unwrap(); @@ -29,19 +37,13 @@ fn test_rename() { access(tmp.path().join("bar"), Access::EXISTS).unwrap(); } -#[cfg(linux_kernel)] +#[cfg(any(linux_kernel, apple))] #[test] fn test_renameat() { - use rustix::fs::{accessat, openat, renameat, statat, Access, AtFlags, Mode, OFlags, CWD}; + use rustix::fs::{accessat, openat, renameat, statat, Access, AtFlags, Mode, CWD}; let tmp = tempfile::tempdir().unwrap(); - let dir = openat( - CWD, - tmp.path(), - OFlags::RDONLY | OFlags::PATH, - Mode::empty(), - ) - .unwrap(); + let dir = openat(CWD, tmp.path(), DIR_OPEN_FLAGS, Mode::empty()).unwrap(); let _ = openat(&dir, "file", OFlags::CREATE | OFlags::WRONLY, Mode::empty()).unwrap(); let before = statat(&dir, "file", AtFlags::empty()).unwrap(); @@ -59,19 +61,13 @@ fn test_renameat() { /// Like `test_renameat` but the file already exists, so `renameat` /// overwrites it. -#[cfg(linux_kernel)] +#[cfg(any(linux_kernel, apple))] #[test] fn test_renameat_overwrite() { - use rustix::fs::{openat, renameat, statat, AtFlags, Mode, OFlags, CWD}; + use rustix::fs::{openat, renameat, statat, AtFlags, Mode, CWD}; let tmp = tempfile::tempdir().unwrap(); - let dir = openat( - CWD, - tmp.path(), - OFlags::RDONLY | OFlags::PATH, - Mode::empty(), - ) - .unwrap(); + let dir = openat(CWD, tmp.path(), DIR_OPEN_FLAGS, Mode::empty()).unwrap(); let _ = openat(&dir, "file", OFlags::CREATE | OFlags::WRONLY, Mode::empty()).unwrap(); let _ = openat(&dir, "bar", OFlags::CREATE | OFlags::WRONLY, Mode::empty()).unwrap(); @@ -81,19 +77,13 @@ fn test_renameat_overwrite() { assert!(same(&before, &renamed)); } -#[cfg(linux_kernel)] +#[cfg(any(linux_kernel, apple))] #[test] fn test_renameat_with() { - use rustix::fs::{openat, renameat_with, statat, AtFlags, Mode, OFlags, RenameFlags, CWD}; + use rustix::fs::{openat, renameat_with, statat, AtFlags, Mode, RenameFlags, CWD}; let tmp = tempfile::tempdir().unwrap(); - let dir = openat( - CWD, - tmp.path(), - OFlags::RDONLY | OFlags::PATH, - Mode::empty(), - ) - .unwrap(); + let dir = openat(CWD, tmp.path(), DIR_OPEN_FLAGS, Mode::empty()).unwrap(); let _ = openat(&dir, "file", OFlags::CREATE | OFlags::WRONLY, Mode::empty()).unwrap(); let before = statat(&dir, "file", AtFlags::empty()).unwrap(); @@ -115,7 +105,7 @@ fn test_renameat_with() { ) .unwrap(); - #[cfg(all(target_os = "linux", target_env = "gnu"))] + #[cfg(any(apple, all(target_os = "linux", target_env = "gnu")))] { let green = statat(&dir, "green", AtFlags::empty()).unwrap();