From 8cafe4c32c7163465602948f0c31e97c7e8a40bf Mon Sep 17 00:00:00 2001 From: Valentyn Kit Date: Sun, 30 Aug 2026 20:38:11 +0300 Subject: [PATCH 1/5] std: add `fs::rename_noreplace` Unlike `fs::rename`, the destination is never overwritten, returning `ErrorKind::AlreadyExists` if `to` already exists. Platform-specific APIs used: `renameat2` with `RENAME_NOREPLACE` flag on `Linux` and `Android`, `renamex_np` with `RENAME_EXCL` on `Apple`, and `MoveFileExW` without the `MOVEFILE_REPLACE_EXISTING` flag on `Windows`. On other Unix platforms, and on Linux, Android and Apple when the kernel or the filesystem doesn't support it, `link` followed by `unlink` is used instead. Platforms with neither return `Unsupported`. --- library/std/src/fs.rs | 67 +++++++++++++++++++++++++++ library/std/src/sys/fs/hermit.rs | 4 ++ library/std/src/sys/fs/mod.rs | 4 ++ library/std/src/sys/fs/motor.rs | 4 ++ library/std/src/sys/fs/solid.rs | 4 ++ library/std/src/sys/fs/uefi.rs | 4 ++ library/std/src/sys/fs/unix.rs | 60 +++++++++++++++++++++++- library/std/src/sys/fs/unsupported.rs | 4 ++ library/std/src/sys/fs/vexos.rs | 4 +- library/std/src/sys/fs/windows.rs | 30 +++++++++--- 10 files changed, 176 insertions(+), 9 deletions(-) diff --git a/library/std/src/fs.rs b/library/std/src/fs.rs index 65b8ed634bc05..f040b35013c7e 100644 --- a/library/std/src/fs.rs +++ b/library/std/src/fs.rs @@ -3078,6 +3078,73 @@ pub fn rename, Q: AsRef>(from: P, to: Q) -> io::Result<()> fs_imp::rename(from.as_ref(), to.as_ref()) } +/// Renames a file or directory to a new name, returning [`AlreadyExists`] if `to` already +/// exists. +/// +/// Unlike [`fs::rename`], the destination is never overwritten. The check happens as part of the +/// same operation, which avoids check-then-rename races, but does not guarantee crash safety. +/// +/// This will not work if the new name is on a different mount point. +/// +/// # Platform-specific behavior +/// +/// This function currently corresponds to: +/// +/// * [renameat2] with `RENAME_NOREPLACE` on Linux and Android +/// * `renamex_np` with `RENAME_EXCL` on Apple platforms +/// * [MoveFileExW] without the `MOVEFILE_REPLACE_EXISTING` flag, with a fallback to +/// `SetFileInformationByHandle`, on Windows +/// * `link` followed by `unlink` on other Unix platforms and on WASI +/// * Otherwise, this function returns [`Unsupported`] +/// +/// The `link` and `unlink` fallback is also used at runtime on Linux, Android and Apple platforms +/// when the kernel or the filesystem does not support the flag, so which of these applies can +/// differ between filesystems on the same machine. +/// +/// Note that, this [may change in the future][changes]. +/// +/// [changes]: io#platform-specific-behavior +/// [renameat2]: https://man7.org/linux/man-pages/man2/renameat2.2.html +/// [MoveFileExW]: https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-movefileexw +/// [`AlreadyExists`]: crate::io::ErrorKind::AlreadyExists +/// [`Unsupported`]: crate::io::ErrorKind::Unsupported +/// [`fs::rename`]: crate::fs::rename +/// +/// # Errors +/// +/// This function will return an error in the following situations, but is not +/// limited to just these cases: +/// +/// * `from` does not exist. +/// * The user lacks permissions to view contents. +/// * `from` and `to` are on separate filesystems. +/// * `to` already exists. +/// * The platform does not support this operation. +/// +/// # Examples +/// +/// ```no_run +/// #![feature(rename_noreplace)] +/// use std::fs; +/// use std::io::ErrorKind; +/// +/// fn main() -> std::io::Result<()> { +/// // Renames a.txt to b.txt, b.txt does not exist yet. +/// fs::rename_noreplace("a.txt", "b.txt")?; +/// +/// // b.txt exists now, so this fails instead of silently overwriting it. +/// let err = fs::rename_noreplace("c.txt", "b.txt").unwrap_err(); +/// assert_eq!(err.kind(), ErrorKind::AlreadyExists); +/// Ok(()) +/// } +/// ``` +#[doc(alias = "renameat2", alias = "renamex_np", alias = "MoveFileEx", alias = "RENAME_NOREPLACE")] +#[unstable(feature = "rename_noreplace", issue = "161427")] +#[cfg_attr(not(test), rustc_diagnostic_item = "fs_rename_noreplace")] +pub fn rename_noreplace, Q: AsRef>(from: P, to: Q) -> io::Result<()> { + fs_imp::rename_noreplace(from.as_ref(), to.as_ref()) +} + /// Copies the contents of one file to another. This function will also /// copy the permission bits of the original file to the destination file. /// diff --git a/library/std/src/sys/fs/hermit.rs b/library/std/src/sys/fs/hermit.rs index 33feee4fc1c97..9751b0a3a0ca7 100644 --- a/library/std/src/sys/fs/hermit.rs +++ b/library/std/src/sys/fs/hermit.rs @@ -594,6 +594,10 @@ pub fn rename(_old: &Path, _new: &Path) -> io::Result<()> { unsupported() } +pub fn rename_noreplace(_old: &Path, _new: &Path) -> io::Result<()> { + unsupported() +} + pub fn set_perm(_p: &Path, _perm: FilePermissions) -> io::Result<()> { unsupported() } diff --git a/library/std/src/sys/fs/mod.rs b/library/std/src/sys/fs/mod.rs index b2666eb2a3da9..059ba6c046076 100644 --- a/library/std/src/sys/fs/mod.rs +++ b/library/std/src/sys/fs/mod.rs @@ -78,6 +78,10 @@ pub fn rename(old: &Path, new: &Path) -> io::Result<()> { with_native_path(old, &|old| with_native_path(new, &|new| imp::rename(old, new))) } +pub fn rename_noreplace(old: &Path, new: &Path) -> io::Result<()> { + with_native_path(old, &|old| with_native_path(new, &|new| imp::rename_noreplace(old, new))) +} + pub fn remove_dir(path: &Path) -> io::Result<()> { with_native_path(path, &imp::rmdir) } diff --git a/library/std/src/sys/fs/motor.rs b/library/std/src/sys/fs/motor.rs index 938d1537790a1..d4eb30f518fe0 100644 --- a/library/std/src/sys/fs/motor.rs +++ b/library/std/src/sys/fs/motor.rs @@ -324,6 +324,10 @@ pub fn rename(old: &Path, new: &Path) -> io::Result<()> { moto_rt::fs::rename(old, new).map_err(map_motor_error) } +pub fn rename_noreplace(_old: &Path, _new: &Path) -> io::Result<()> { + unsupported() +} + pub fn rmdir(path: &Path) -> io::Result<()> { let path = path.to_str().ok_or(io::Error::from(io::ErrorKind::InvalidFilename))?; moto_rt::fs::rmdir(path).map_err(map_motor_error) diff --git a/library/std/src/sys/fs/solid.rs b/library/std/src/sys/fs/solid.rs index bd963b1d2f038..b1d2acdc2b79c 100644 --- a/library/std/src/sys/fs/solid.rs +++ b/library/std/src/sys/fs/solid.rs @@ -530,6 +530,10 @@ pub fn rename(old: &Path, new: &Path) -> io::Result<()> { Ok(()) } +pub fn rename_noreplace(_old: &Path, _new: &Path) -> io::Result<()> { + unsupported() +} + pub fn set_perm(p: &Path, perm: FilePermissions) -> io::Result<()> { // Solid does not support symlinks set_perm_nofollow(p, perm) diff --git a/library/std/src/sys/fs/uefi.rs b/library/std/src/sys/fs/uefi.rs index 08473e245cc8e..2d14a92a6b8c9 100644 --- a/library/std/src/sys/fs/uefi.rs +++ b/library/std/src/sys/fs/uefi.rs @@ -474,6 +474,10 @@ pub fn rename(old: &Path, new: &Path) -> io::Result<()> { f.set_file_info(new_info) } +pub fn rename_noreplace(_old: &Path, _new: &Path) -> io::Result<()> { + unsupported() +} + pub fn set_perm(p: &Path, perm: FilePermissions) -> io::Result<()> { // UEFI does not support symlinks set_perm_nofollow(p, perm) diff --git a/library/std/src/sys/fs/unix.rs b/library/std/src/sys/fs/unix.rs index 46d5d33d23ac2..d5e8db4e9030a 100644 --- a/library/std/src/sys/fs/unix.rs +++ b/library/std/src/sys/fs/unix.rs @@ -60,7 +60,7 @@ use crate::sys::fd::FileDesc; pub use crate::sys::fs::common::exists; use crate::sys::helpers::run_path_with_cstr; use crate::sys::time::SystemTime; -#[cfg(all(target_os = "linux", target_env = "gnu"))] +#[cfg(any(target_os = "linux", target_os = "android"))] use crate::sys::weak::syscall; #[cfg(target_os = "android")] use crate::sys::weak::weak; @@ -1885,6 +1885,59 @@ pub fn rename(old: &CStr, new: &CStr) -> io::Result<()> { cvt(unsafe { libc::rename(old.as_ptr(), new.as_ptr()) }).map(|_| ()) } +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn rename_noreplace(old: &CStr, new: &CStr) -> io::Result<()> { + syscall!( + fn renameat2( + olddirfd: c_int, + oldpath: *const libc::c_char, + newdirfd: c_int, + newpath: *const libc::c_char, + flags: libc::c_uint, + ) -> c_int; + ); + + // `RENAME_NOREPLACE` is `c_uint` on linux and `c_int` on Android, so casting is needed here. + let flags = libc::RENAME_NOREPLACE as libc::c_uint; + let res = cvt(unsafe { + renameat2(libc::AT_FDCWD, old.as_ptr(), libc::AT_FDCWD, new.as_ptr(), flags) + }); + + match res { + Ok(_) => Ok(()), + Err(err) => match err.raw_os_error() { + // The kernel predates `renameat2`. + Some(libc::ENOSYS) => link_then_unlink(old, new), + // The filesystem does not support `RENAME_NOREPLACE`. + // `EINVAL` also covers misuse, so report it unchanged unless the fallback found `new`. + Some(libc::EINVAL) => match link_then_unlink(old, new) { + Err(fallback) if fallback.raw_os_error() != Some(libc::EEXIST) => Err(err), + res => res, + }, + _ => Err(err), + }, + } +} + +#[cfg(target_vendor = "apple")] +pub fn rename_noreplace(old: &CStr, new: &CStr) -> io::Result<()> { + let res = cvt(unsafe { libc::renamex_np(old.as_ptr(), new.as_ptr(), libc::RENAME_EXCL) }); + match res { + Ok(_) => Ok(()), + Err(err) => match err.raw_os_error() { + // The filesystem does not support `RENAME_EXCL`. + // Unlike Linux, Darwin keeps `EINVAL` for misuse only. + Some(libc::ENOTSUP) => link_then_unlink(old, new), + _ => Err(err), + }, + } +} + +#[cfg(not(any(target_os = "linux", target_os = "android", target_vendor = "apple")))] +pub fn rename_noreplace(old: &CStr, new: &CStr) -> io::Result<()> { + link_then_unlink(old, new) +} + pub fn set_perm(p: &CStr, perm: FilePermissions) -> io::Result<()> { cvt_r(|| unsafe { libc::chmod(p.as_ptr(), perm.mode) }).map(|_| ()) } @@ -2094,6 +2147,11 @@ pub fn canonicalize(path: &CStr) -> io::Result { }))) } +fn link_then_unlink(old: &CStr, new: &CStr) -> io::Result<()> { + link(old, new)?; + unlink(old) +} + fn open_from(from: &Path) -> io::Result<(crate::fs::File, crate::fs::Metadata)> { use crate::fs::File; use crate::sys::fs::common::NOT_FILE_ERROR; diff --git a/library/std/src/sys/fs/unsupported.rs b/library/std/src/sys/fs/unsupported.rs index 512af27401dfe..b7aa0871a8088 100644 --- a/library/std/src/sys/fs/unsupported.rs +++ b/library/std/src/sys/fs/unsupported.rs @@ -309,6 +309,10 @@ pub fn rename(_old: &Path, _new: &Path) -> io::Result<()> { unsupported() } +pub fn rename_noreplace(_old: &Path, _new: &Path) -> io::Result<()> { + unsupported() +} + pub fn set_perm(_p: &Path, perm: FilePermissions) -> io::Result<()> { match perm.0 {} } diff --git a/library/std/src/sys/fs/vexos.rs b/library/std/src/sys/fs/vexos.rs index 1357787c44c77..d9fc8fd26e8b2 100644 --- a/library/std/src/sys/fs/vexos.rs +++ b/library/std/src/sys/fs/vexos.rs @@ -12,8 +12,8 @@ use crate::sys::{unsupported, unsupported_err}; #[path = "unsupported.rs"] mod unsupported_fs; pub use unsupported_fs::{ - Dir, DirBuilder, FileTimes, canonicalize, link, readlink, remove_dir_all, rename, rmdir, - symlink, unlink, + Dir, DirBuilder, FileTimes, canonicalize, link, readlink, remove_dir_all, rename, + rename_noreplace, rmdir, symlink, unlink, }; /// VEXos file descriptor. diff --git a/library/std/src/sys/fs/windows.rs b/library/std/src/sys/fs/windows.rs index 399c0749bc21e..479d3655db369 100644 --- a/library/std/src/sys/fs/windows.rs +++ b/library/std/src/sys/fs/windows.rs @@ -1302,8 +1302,13 @@ pub fn unlink(path: &WCStr) -> io::Result<()> { } } -pub fn rename(old: &WCStr, new: &WCStr) -> io::Result<()> { - if unsafe { c::MoveFileExW(old.as_ptr(), new.as_ptr(), c::MOVEFILE_REPLACE_EXISTING) } == 0 { +fn rename_inner( + old: &WCStr, + new: &WCStr, + move_flags: c::MOVE_FILE_FLAGS, + rename_flags: u32, +) -> io::Result<()> { + if unsafe { c::MoveFileExW(old.as_ptr(), new.as_ptr(), move_flags) } == 0 { let err = api::get_last_error(); // if `MoveFileExW` fails with ERROR_ACCESS_DENIED then try to move // the file while ignoring the readonly attribute. @@ -1335,10 +1340,8 @@ pub fn rename(old: &WCStr, new: &WCStr) -> io::Result<()> { return Err(io::ErrorKind::OutOfMemory.into()); } - (&raw mut (*file_rename_info).Anonymous).write(c::FILE_RENAME_INFO_0 { - Flags: c::FILE_RENAME_FLAG_REPLACE_IF_EXISTS - | c::FILE_RENAME_FLAG_POSIX_SEMANTICS, - }); + (&raw mut (*file_rename_info).Anonymous) + .write(c::FILE_RENAME_INFO_0 { Flags: rename_flags }); (&raw mut (*file_rename_info).RootDirectory).write(ptr::null_mut()); // Don't include the NULL in the size @@ -1373,6 +1376,21 @@ pub fn rename(old: &WCStr, new: &WCStr) -> io::Result<()> { Ok(()) } +pub fn rename(old: &WCStr, new: &WCStr) -> io::Result<()> { + rename_inner( + old, + new, + c::MOVEFILE_REPLACE_EXISTING, + c::FILE_RENAME_FLAG_REPLACE_IF_EXISTS | c::FILE_RENAME_FLAG_POSIX_SEMANTICS, + ) +} + +pub fn rename_noreplace(old: &WCStr, new: &WCStr) -> io::Result<()> { + // Without `MOVEFILE_REPLACE_EXISTING`, `MoveFileExW` fails if the destination + // exists. + rename_inner(old, new, 0, c::FILE_RENAME_FLAG_POSIX_SEMANTICS) +} + pub fn rmdir(p: &WCStr) -> io::Result<()> { cvt(unsafe { c::RemoveDirectoryW(p.as_ptr()) })?; Ok(()) From f09e0ef7eabceadb32c2e9a0a9ec95a40380cece Mon Sep 17 00:00:00 2001 From: Valentyn Kit Date: Sun, 30 Aug 2026 21:54:55 +0300 Subject: [PATCH 2/5] std: add tests for `fs::rename_noreplace` --- library/std/src/fs/tests.rs | 84 +++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/library/std/src/fs/tests.rs b/library/std/src/fs/tests.rs index 250109f277dab..98d8708b5ad86 100644 --- a/library/std/src/fs/tests.rs +++ b/library/std/src/fs/tests.rs @@ -2647,6 +2647,90 @@ fn test_rename_symlink() { assert_eq!(fs::read_link(&dest).unwrap().as_path(), not_exist); } +#[test] +fn test_rename_noreplace() { + let tmpdir = tmpdir(); + let source_path = tmpdir.join("source_file.txt"); + let target_path = tmpdir.join("target_file.txt"); + + fs::write(&source_path, b"source hello world").unwrap(); + + fs::rename_noreplace(&source_path, &target_path).unwrap(); + assert!(!source_path.exists()); + assert_eq!(fs::read(&target_path).unwrap(), b"source hello world"); +} + +#[test] +fn test_rename_noreplace_existing_file() { + let tmpdir = tmpdir(); + let source_path = tmpdir.join("source_file.txt"); + let target_path = tmpdir.join("target_file.txt"); + + fs::write(&source_path, b"source hello world").unwrap(); + fs::write(&target_path, b"target hello world").unwrap(); + + let err = fs::rename_noreplace(&source_path, &target_path).unwrap_err(); + assert_eq!(err.kind(), ErrorKind::AlreadyExists); + // Make sure the failed rename left both files untouched. + assert_eq!(fs::read(&source_path).unwrap(), b"source hello world"); + assert_eq!(fs::read(&target_path).unwrap(), b"target hello world"); +} + +#[test] +fn test_rename_noreplace_directory_to_empty_directory() { + // Renaming a directory over an existing empty directory should fail; + // plain `rename` would succeed here. + let tmpdir = tmpdir(); + let source_path = tmpdir.join("source_directory"); + let target_path = tmpdir.join("target_directory"); + + fs::create_dir(&source_path).unwrap(); + fs::create_dir(&target_path).unwrap(); + + let err = fs::rename_noreplace(&source_path, &target_path).unwrap_err(); + assert_matches!( + err.kind(), + // A native no-replace rename returns `AlreadyExists`. The `link`/`unlink` + // fallback cannot rename directories and returns `PermissionDenied`. + ErrorKind::AlreadyExists | ErrorKind::PermissionDenied, + "Expected AlreadyExists or PermissionDenied error, got {err}" + ); +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple", windows))] +fn test_rename_noreplace_directory() { + // The `link`/`unlink` fallback cannot move directories, so only platforms + // with a native no-replace rename are tested. + let tmpdir = tmpdir(); + let source_path = tmpdir.join("source_directory"); + let target_path = tmpdir.join("target_directory"); + + fs::create_dir(&source_path).unwrap(); + fs::write(source_path.join("file.txt"), b"hello world").unwrap(); + + fs::rename_noreplace(&source_path, &target_path).unwrap(); + assert!(!source_path.exists()); + assert_eq!(fs::read(target_path.join("file.txt")).unwrap(), b"hello world"); +} + +#[test] +fn test_rename_noreplace_symlink() { + let tmpdir = tmpdir(); + if !got_symlink_permission(&tmpdir) { + return; + }; + + let original = tmpdir.join("original"); + let dest = tmpdir.join("dest"); + let not_exist = Path::new("does not exist"); + + symlink_file(not_exist, &original).unwrap(); + fs::rename_noreplace(&original, &dest).unwrap(); + // Make sure that renaming `original` to `dest` preserves the symlink. + assert_eq!(fs::read_link(&dest).unwrap().as_path(), not_exist); +} + #[test] #[cfg(windows)] #[cfg_attr( From d0d58b4a64468599732faa682006113a386e041c Mon Sep 17 00:00:00 2001 From: Valentyn Kit Date: Sun, 27 Sep 2026 21:42:02 +0300 Subject: [PATCH 3/5] std: clarify rename_noreplace crash guarantees Co-authored-by: Josh Triplett --- library/std/src/fs.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/library/std/src/fs.rs b/library/std/src/fs.rs index f040b35013c7e..7c833f76f3929 100644 --- a/library/std/src/fs.rs +++ b/library/std/src/fs.rs @@ -3082,7 +3082,10 @@ pub fn rename, Q: AsRef>(from: P, to: Q) -> io::Result<()> /// exists. /// /// Unlike [`fs::rename`], the destination is never overwritten. The check happens as part of the -/// same operation, which avoids check-then-rename races, but does not guarantee crash safety. +/// same operation, which avoids check-then-rename races. +/// +/// If the system crashes in the middle, it may be possible to end up with both copies of the file +/// existing, but it should not be possible to end up with zero copies, or with a half-written file. /// /// This will not work if the new name is on a different mount point. /// From 7b7ed95383a3f402319717c72d751c80d35d300f Mon Sep 17 00:00:00 2001 From: Valentyn Kit Date: Sun, 27 Sep 2026 22:46:35 +0300 Subject: [PATCH 4/5] std: take plain u32 move flags on Windows windows-bindgen no longer generates the `MOVE_FILE_FLAGS` alias, so `u32` is used instead. --- library/std/src/sys/fs/windows.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/library/std/src/sys/fs/windows.rs b/library/std/src/sys/fs/windows.rs index 479d3655db369..af909bc403590 100644 --- a/library/std/src/sys/fs/windows.rs +++ b/library/std/src/sys/fs/windows.rs @@ -1302,12 +1302,7 @@ pub fn unlink(path: &WCStr) -> io::Result<()> { } } -fn rename_inner( - old: &WCStr, - new: &WCStr, - move_flags: c::MOVE_FILE_FLAGS, - rename_flags: u32, -) -> io::Result<()> { +fn rename_inner(old: &WCStr, new: &WCStr, move_flags: u32, rename_flags: u32) -> io::Result<()> { if unsafe { c::MoveFileExW(old.as_ptr(), new.as_ptr(), move_flags) } == 0 { let err = api::get_last_error(); // if `MoveFileExW` fails with ERROR_ACCESS_DENIED then try to move From 3fe740638c59366c701fb4b2fefbbdc966e283cc Mon Sep 17 00:00:00 2001 From: Valentyn Kit Date: Sun, 4 Oct 2026 18:24:27 +0300 Subject: [PATCH 5/5] std: fix failing `test_rename_noreplace_directory` `test_rename_noreplace_directory()` fails on kernels before Linux 3.15, because they lack `renameat2`, so std implementation falls back to `link`/`unlink`, which couldn't move directories, so expectations of the test were wrong. Test now accepts `PermissionDenied` on Unix, in fallback case. --- library/std/src/fs/tests.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/library/std/src/fs/tests.rs b/library/std/src/fs/tests.rs index 98d8708b5ad86..0f8d876e3c00a 100644 --- a/library/std/src/fs/tests.rs +++ b/library/std/src/fs/tests.rs @@ -2700,8 +2700,6 @@ fn test_rename_noreplace_directory_to_empty_directory() { #[test] #[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple", windows))] fn test_rename_noreplace_directory() { - // The `link`/`unlink` fallback cannot move directories, so only platforms - // with a native no-replace rename are tested. let tmpdir = tmpdir(); let source_path = tmpdir.join("source_directory"); let target_path = tmpdir.join("target_directory"); @@ -2709,9 +2707,19 @@ fn test_rename_noreplace_directory() { fs::create_dir(&source_path).unwrap(); fs::write(source_path.join("file.txt"), b"hello world").unwrap(); - fs::rename_noreplace(&source_path, &target_path).unwrap(); - assert!(!source_path.exists()); - assert_eq!(fs::read(target_path.join("file.txt")).unwrap(), b"hello world"); + match fs::rename_noreplace(&source_path, &target_path) { + Ok(()) => { + assert!(!source_path.exists()); + assert_eq!(fs::read(target_path.join("file.txt")).unwrap(), b"hello world"); + } + // Without a native no-replace rename (Linux before 3.15), Unix + // platforms fall back to `link`/`unlink`, and it rejects directories. + Err(err) if cfg!(unix) && err.kind() == ErrorKind::PermissionDenied => { + assert!(source_path.exists()); + assert!(!target_path.exists()); + } + Err(err) => panic!("unexpected error: {err:?}"), + } } #[test]