Skip to content
Merged
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
70 changes: 70 additions & 0 deletions library/std/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3078,6 +3078,76 @@ pub fn rename<P: AsRef<Path>, Q: AsRef<Path>>(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.
///
/// 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.
///
/// # 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<P: AsRef<Path>, Q: AsRef<Path>>(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.
///
Expand Down
92 changes: 92 additions & 0 deletions library/std/src/fs/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2647,6 +2647,98 @@ 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() {
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();

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]
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(
Expand Down
4 changes: 4 additions & 0 deletions library/std/src/sys/fs/hermit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
4 changes: 4 additions & 0 deletions library/std/src/sys/fs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
4 changes: 4 additions & 0 deletions library/std/src/sys/fs/motor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions library/std/src/sys/fs/solid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions library/std/src/sys/fs/uefi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
60 changes: 59 additions & 1 deletion library/std/src/sys/fs/unix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) });
Comment thread
joshtriplett marked this conversation as resolved.
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)
Comment thread
joshtriplett marked this conversation as resolved.
}

pub fn set_perm(p: &CStr, perm: FilePermissions) -> io::Result<()> {
cvt_r(|| unsafe { libc::chmod(p.as_ptr(), perm.mode) }).map(|_| ())
}
Expand Down Expand Up @@ -2094,6 +2147,11 @@ pub fn canonicalize(path: &CStr) -> io::Result<PathBuf> {
})))
}

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;
Expand Down
4 changes: 4 additions & 0 deletions library/std/src/sys/fs/unsupported.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
}
Expand Down
4 changes: 2 additions & 2 deletions library/std/src/sys/fs/vexos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
25 changes: 19 additions & 6 deletions library/std/src/sys/fs/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1302,8 +1302,8 @@ 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: 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
// the file while ignoring the readonly attribute.
Expand Down Expand Up @@ -1335,10 +1335,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
Expand Down Expand Up @@ -1373,6 +1371,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(())
Expand Down
Loading