1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-27 19:17:43 +00:00

Merge pull request #6040 from hamflx/main

Make mv command fallback to copy only if the src and dst are on different device
This commit is contained in:
Sylvestre Ledru 2025-02-17 08:47:06 +01:00 committed by GitHub
commit 58c336d5c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 142 additions and 1 deletions

View file

@ -136,6 +136,7 @@ vmsplice
# * vars/libc # * vars/libc
COMFOLLOW COMFOLLOW
EXDEV
FILENO FILENO
FTSENT FTSENT
HOSTSIZE HOSTSIZE

2
Cargo.lock generated
View file

@ -3009,8 +3009,10 @@ dependencies = [
"clap", "clap",
"fs_extra", "fs_extra",
"indicatif", "indicatif",
"libc",
"thiserror 2.0.11", "thiserror 2.0.11",
"uucore", "uucore",
"windows-sys 0.59.0",
] ]
[[package]] [[package]]

View file

@ -28,6 +28,16 @@ uucore = { workspace = true, features = [
] } ] }
thiserror = { workspace = true } thiserror = { workspace = true }
[target.'cfg(windows)'.dependencies]
windows-sys = { workspace = true, features = [
"Win32_Foundation",
"Win32_Security",
"Win32_Storage_FileSystem",
] }
[target.'cfg(unix)'.dependencies]
libc = { workspace = true }
[[bin]] [[bin]]
name = "mv" name = "mv"
path = "src/main.rs" path = "src/main.rs"

View file

@ -657,7 +657,22 @@ fn rename_with_fallback(
to: &Path, to: &Path,
multi_progress: Option<&MultiProgress>, multi_progress: Option<&MultiProgress>,
) -> io::Result<()> { ) -> io::Result<()> {
if fs::rename(from, to).is_err() { if let Err(err) = fs::rename(from, to) {
#[cfg(windows)]
const EXDEV: i32 = windows_sys::Win32::Foundation::ERROR_NOT_SAME_DEVICE as _;
#[cfg(unix)]
const EXDEV: i32 = libc::EXDEV as _;
// We will only copy if:
// 1. Files are on different devices (EXDEV error)
// 2. On Windows, if the target file exists and source file is opened by another process
// (MoveFileExW fails with "Access Denied" even if the source file has FILE_SHARE_DELETE permission)
let should_fallback = matches!(err.raw_os_error(), Some(EXDEV))
|| (from.is_file() && can_delete_file(from).unwrap_or(false));
if !should_fallback {
return Err(err);
}
// Get metadata without following symlinks // Get metadata without following symlinks
let metadata = from.symlink_metadata()?; let metadata = from.symlink_metadata()?;
let file_type = metadata.file_type(); let file_type = metadata.file_type();
@ -792,3 +807,55 @@ fn is_empty_dir(path: &Path) -> bool {
Err(_e) => false, Err(_e) => false,
} }
} }
/// Checks if a file can be deleted by attempting to open it with delete permissions.
#[cfg(windows)]
fn can_delete_file(path: &Path) -> Result<bool, io::Error> {
use std::{
os::windows::ffi::OsStrExt as _,
ptr::{null, null_mut},
};
use windows_sys::Win32::{
Foundation::{CloseHandle, INVALID_HANDLE_VALUE},
Storage::FileSystem::{
CreateFileW, DELETE, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_DELETE, FILE_SHARE_READ,
FILE_SHARE_WRITE, OPEN_EXISTING,
},
};
let wide_path = path
.as_os_str()
.encode_wide()
.chain([0])
.collect::<Vec<u16>>();
let handle = unsafe {
CreateFileW(
wide_path.as_ptr(),
DELETE,
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
null(),
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
null_mut(),
)
};
if handle == INVALID_HANDLE_VALUE {
return Err(io::Error::last_os_error());
}
unsafe { CloseHandle(handle) };
Ok(true)
}
#[cfg(not(windows))]
fn can_delete_file(_: &Path) -> Result<bool, io::Error> {
// On non-Windows platforms, always return false to indicate that we don't need
// to try the copy+delete fallback. This is because on Unix-like systems,
// rename() failing with errors other than EXDEV means the operation cannot
// succeed even with a copy+delete approach (e.g. permission errors).
Ok(false)
}

View file

@ -49,6 +49,22 @@ fn test_mv_rename_file() {
assert!(at.file_exists(file2)); assert!(at.file_exists(file2));
} }
#[test]
fn test_mv_with_source_file_opened_and_target_file_exists() {
let (at, mut ucmd) = at_and_ucmd!();
let src = "source_file_opened";
let dst = "target_file_exists";
let f = at.make_file(src);
at.touch(dst);
ucmd.arg(src).arg(dst).succeeds().no_stderr().no_stdout();
drop(f);
}
#[test] #[test]
fn test_mv_move_file_into_dir() { fn test_mv_move_file_into_dir() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
@ -1670,6 +1686,51 @@ fn test_acl() {
assert!(compare_xattrs(&file, &file_target)); assert!(compare_xattrs(&file, &file_target));
} }
#[test]
#[cfg(windows)]
fn test_move_should_not_fallback_to_copy() {
use std::os::windows::fs::OpenOptionsExt;
let (at, mut ucmd) = at_and_ucmd!();
let locked_file = "a_file_is_locked";
let locked_file_path = at.plus(locked_file);
let file = std::fs::OpenOptions::new()
.create(true)
.write(true)
.share_mode(
uucore::windows_sys::Win32::Storage::FileSystem::FILE_SHARE_READ
| uucore::windows_sys::Win32::Storage::FileSystem::FILE_SHARE_WRITE,
)
.open(locked_file_path);
let target_file = "target_file";
ucmd.arg(locked_file).arg(target_file).fails();
assert!(at.file_exists(locked_file));
assert!(!at.file_exists(target_file));
drop(file);
}
#[test]
#[cfg(unix)]
fn test_move_should_not_fallback_to_copy() {
let (at, mut ucmd) = at_and_ucmd!();
let readonly_dir = "readonly_dir";
let locked_file = "readonly_dir/a_file_is_locked";
at.mkdir(readonly_dir);
at.touch(locked_file);
at.set_mode(readonly_dir, 0o555);
let target_file = "target_file";
ucmd.arg(locked_file).arg(target_file).fails();
assert!(at.file_exists(locked_file));
assert!(!at.file_exists(target_file));
}
// Todo: // Todo:
// $ at.touch a b // $ at.touch a b