Skip to content

Commit 1e414f1

Browse files
committed
Win: Use FILE_RENAME_FLAG_POSIX_SEMANTICS for std::fs::rename if available
Windows 10 1601 introduced `FileRenameInfoEx` as well as `FILE_RENAME_FLAG_POSIX_SEMANTICS`, allowing for atomic renaming. If it isn't supported, we fall back to `FileRenameInfo`. This commit also replicates `MoveFileExW`'s behavior of checking whether the source file is a mount point and moving the mount point instead of resolving the target path.
1 parent ed04567 commit 1e414f1

File tree

4 files changed

+168
-4
lines changed

4 files changed

+168
-4
lines changed

library/std/src/fs.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2171,12 +2171,14 @@ pub fn symlink_metadata<P: AsRef<Path>>(path: P) -> io::Result<Metadata> {
21712171
/// # Platform-specific behavior
21722172
///
21732173
/// This function currently corresponds to the `rename` function on Unix
2174-
/// and the `MoveFileEx` function with the `MOVEFILE_REPLACE_EXISTING` flag on Windows.
2174+
/// and the `SetFileInformationByHandle` function on Windows.
21752175
///
21762176
/// Because of this, the behavior when both `from` and `to` exist differs. On
21772177
/// Unix, if `from` is a directory, `to` must also be an (empty) directory. If
2178-
/// `from` is not a directory, `to` must also be not a directory. In contrast,
2179-
/// on Windows, `from` can be anything, but `to` must *not* be a directory.
2178+
/// `from` is not a directory, `to` must also be not a directory. The behavior
2179+
/// on Windows is the same on Windows 10 1607 and higher if `FileRenameInfoEx`
2180+
/// is supported by the filesystem; otherwise, `from` can be anything, but
2181+
/// `to` must *not* be a directory.
21802182
///
21812183
/// Note that, this [may change in the future][changes].
21822184
///

library/std/src/sys/pal/windows/c/bindings.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2295,6 +2295,7 @@ Windows.Win32.Storage.FileSystem.FILE_NAME_OPENED
22952295
Windows.Win32.Storage.FileSystem.FILE_READ_ATTRIBUTES
22962296
Windows.Win32.Storage.FileSystem.FILE_READ_DATA
22972297
Windows.Win32.Storage.FileSystem.FILE_READ_EA
2298+
Windows.Win32.Storage.FileSystem.FILE_RENAME_INFO
22982299
Windows.Win32.Storage.FileSystem.FILE_SHARE_DELETE
22992300
Windows.Win32.Storage.FileSystem.FILE_SHARE_MODE
23002301
Windows.Win32.Storage.FileSystem.FILE_SHARE_NONE
@@ -2597,5 +2598,7 @@ Windows.Win32.System.Threading.WaitForMultipleObjects
25972598
Windows.Win32.System.Threading.WaitForSingleObject
25982599
Windows.Win32.System.Threading.WakeAllConditionVariable
25992600
Windows.Win32.System.Threading.WakeConditionVariable
2601+
Windows.Win32.System.WindowsProgramming.FILE_RENAME_FLAG_POSIX_SEMANTICS
2602+
Windows.Win32.System.WindowsProgramming.FILE_RENAME_FLAG_REPLACE_IF_EXISTS
26002603
Windows.Win32.System.WindowsProgramming.PROGRESS_CONTINUE
26012604
Windows.Win32.UI.Shell.GetUserProfileDirectoryW

library/std/src/sys/pal/windows/c/windows_sys.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2470,6 +2470,22 @@ pub const FILE_RANDOM_ACCESS: NTCREATEFILE_CREATE_OPTIONS = 2048u32;
24702470
pub const FILE_READ_ATTRIBUTES: FILE_ACCESS_RIGHTS = 128u32;
24712471
pub const FILE_READ_DATA: FILE_ACCESS_RIGHTS = 1u32;
24722472
pub const FILE_READ_EA: FILE_ACCESS_RIGHTS = 8u32;
2473+
pub const FILE_RENAME_FLAG_POSIX_SEMANTICS: u32 = 2u32;
2474+
pub const FILE_RENAME_FLAG_REPLACE_IF_EXISTS: u32 = 1u32;
2475+
#[repr(C)]
2476+
#[derive(Clone, Copy)]
2477+
pub struct FILE_RENAME_INFO {
2478+
pub Anonymous: FILE_RENAME_INFO_0,
2479+
pub RootDirectory: HANDLE,
2480+
pub FileNameLength: u32,
2481+
pub FileName: [u16; 1],
2482+
}
2483+
#[repr(C)]
2484+
#[derive(Clone, Copy)]
2485+
pub union FILE_RENAME_INFO_0 {
2486+
pub ReplaceIfExists: BOOLEAN,
2487+
pub Flags: u32,
2488+
}
24732489
pub const FILE_RESERVE_OPFILTER: NTCREATEFILE_CREATE_OPTIONS = 1048576u32;
24742490
pub const FILE_SEQUENTIAL_ONLY: NTCREATEFILE_CREATE_OPTIONS = 4u32;
24752491
pub const FILE_SESSION_AWARE: NTCREATEFILE_CREATE_OPTIONS = 262144u32;

library/std/src/sys/pal/windows/fs.rs

Lines changed: 144 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use super::api::{self, WinError};
22
use super::{IoResult, to_u16s};
3+
use crate::alloc::{alloc, handle_alloc_error};
34
use crate::borrow::Cow;
45
use crate::ffi::{OsStr, OsString, c_void};
56
use crate::io::{self, BorrowedCursor, Error, IoSlice, IoSliceMut, SeekFrom};
@@ -1095,7 +1096,149 @@ pub fn unlink(p: &Path) -> io::Result<()> {
10951096
pub fn rename(old: &Path, new: &Path) -> io::Result<()> {
10961097
let old = maybe_verbatim(old)?;
10971098
let new = maybe_verbatim(new)?;
1098-
cvt(unsafe { c::MoveFileExW(old.as_ptr(), new.as_ptr(), c::MOVEFILE_REPLACE_EXISTING) })?;
1099+
1100+
let new_len_without_nul_in_bytes = (new.len() - 1).try_into().unwrap();
1101+
1102+
let struct_size = mem::size_of::<c::FILE_RENAME_INFO>() - mem::size_of::<u16>()
1103+
+ new.len() * mem::size_of::<u16>();
1104+
1105+
let struct_size: u32 = struct_size.try_into().unwrap();
1106+
1107+
let create_file = |extra_access, extra_flags| {
1108+
let handle = unsafe {
1109+
HandleOrInvalid::from_raw_handle(c::CreateFileW(
1110+
old.as_ptr(),
1111+
c::SYNCHRONIZE | c::DELETE | extra_access,
1112+
c::FILE_SHARE_READ | c::FILE_SHARE_WRITE | c::FILE_SHARE_DELETE,
1113+
ptr::null(),
1114+
c::OPEN_EXISTING,
1115+
c::FILE_ATTRIBUTE_NORMAL | c::FILE_FLAG_BACKUP_SEMANTICS | extra_flags,
1116+
ptr::null_mut(),
1117+
))
1118+
};
1119+
1120+
OwnedHandle::try_from(handle).map_err(|_| io::Error::last_os_error())
1121+
};
1122+
1123+
// The following code replicates `MoveFileEx`'s behavior as reverse-engineered from its disassembly.
1124+
// If `old` refers to a mount point, we move it instead of the target.
1125+
let handle = match create_file(c::FILE_READ_ATTRIBUTES, c::FILE_FLAG_OPEN_REPARSE_POINT) {
1126+
Ok(handle) => {
1127+
let mut file_attribute_tag_info: MaybeUninit<c::FILE_ATTRIBUTE_TAG_INFO> =
1128+
MaybeUninit::uninit();
1129+
1130+
let result = unsafe {
1131+
cvt(c::GetFileInformationByHandleEx(
1132+
handle.as_raw_handle(),
1133+
c::FileAttributeTagInfo,
1134+
file_attribute_tag_info.as_mut_ptr().cast(),
1135+
mem::size_of::<c::FILE_ATTRIBUTE_TAG_INFO>().try_into().unwrap(),
1136+
))
1137+
};
1138+
1139+
if let Err(err) = result {
1140+
if err.raw_os_error() == Some(c::ERROR_INVALID_PARAMETER as _)
1141+
|| err.raw_os_error() == Some(c::ERROR_INVALID_FUNCTION as _)
1142+
{
1143+
// `GetFileInformationByHandleEx` documents that not all underlying drivers support all file information classes.
1144+
// Since we know we passed the correct arguments, this means the underlying driver didn't understand our request;
1145+
// `MoveFileEx` proceeds by reopening the file without inhibiting reparse point behavior.
1146+
None
1147+
} else {
1148+
Some(Err(err))
1149+
}
1150+
} else {
1151+
// SAFETY: The struct has been initialized by GetFileInformationByHandleEx
1152+
let file_attribute_tag_info = unsafe { file_attribute_tag_info.assume_init() };
1153+
1154+
if file_attribute_tag_info.FileAttributes & c::FILE_ATTRIBUTE_REPARSE_POINT != 0
1155+
&& file_attribute_tag_info.ReparseTag != c::IO_REPARSE_TAG_MOUNT_POINT
1156+
{
1157+
// The file is not a mount point: Reopen the file without inhibiting reparse point behavior.
1158+
None
1159+
} else {
1160+
// The file is a mount point: Don't reopen the file so that the mount point gets renamed.
1161+
Some(Ok(handle))
1162+
}
1163+
}
1164+
}
1165+
// The underlying driver may not support `FILE_FLAG_OPEN_REPARSE_POINT`: Retry without it.
1166+
Err(err) if err.raw_os_error() == Some(c::ERROR_INVALID_PARAMETER as _) => None,
1167+
Err(err) => Some(Err(err)),
1168+
}
1169+
.unwrap_or_else(|| create_file(0, 0))?;
1170+
1171+
// The last field of FILE_RENAME_INFO, the file name, is unsized.
1172+
// Therefore we need to subtract the size of one wide char.
1173+
let layout = core::alloc::Layout::from_size_align(
1174+
struct_size as _,
1175+
mem::align_of::<c::FILE_RENAME_INFO>(),
1176+
)
1177+
.unwrap();
1178+
1179+
let file_rename_info = unsafe { alloc(layout) } as *mut c::FILE_RENAME_INFO;
1180+
1181+
if file_rename_info.is_null() {
1182+
handle_alloc_error(layout);
1183+
}
1184+
1185+
// SAFETY: file_rename_info is a non-null pointer pointing to memory allocated by the global allocator.
1186+
let mut file_rename_info = unsafe { Box::from_raw(file_rename_info) };
1187+
1188+
// SAFETY: We have allocated enough memory for a full FILE_RENAME_INFO struct and a filename.
1189+
unsafe {
1190+
(&raw mut (*file_rename_info).Anonymous).write(c::FILE_RENAME_INFO_0 {
1191+
// Don't bother with FileRenameInfo on Windows 7 since it doesn't exist.
1192+
#[cfg(not(target_vendor = "win7"))]
1193+
Flags: c::FILE_RENAME_FLAG_REPLACE_IF_EXISTS | c::FILE_RENAME_FLAG_POSIX_SEMANTICS,
1194+
#[cfg(target_vendor = "win7")]
1195+
ReplaceIfExists: 1,
1196+
});
1197+
1198+
(&raw mut (*file_rename_info).RootDirectory).write(ptr::null_mut());
1199+
(&raw mut (*file_rename_info).FileNameLength).write(new_len_without_nul_in_bytes);
1200+
1201+
new.as_ptr()
1202+
.copy_to_nonoverlapping((&raw mut (*file_rename_info).FileName) as *mut u16, new.len());
1203+
}
1204+
1205+
#[cfg(not(target_vendor = "win7"))]
1206+
const FileInformationClass: c::FILE_INFO_BY_HANDLE_CLASS = c::FileRenameInfoEx;
1207+
#[cfg(target_vendor = "win7")]
1208+
const FileInformationClass: c::FILE_INFO_BY_HANDLE_CLASS = c::FileRenameInfo;
1209+
1210+
// We don't use `set_file_information_by_handle` here as `FILE_RENAME_INFO` is used for both `FileRenameInfo` and `FileRenameInfoEx`.
1211+
let result = unsafe {
1212+
cvt(c::SetFileInformationByHandle(
1213+
handle.as_raw_handle(),
1214+
FileInformationClass,
1215+
(&raw const *file_rename_info).cast::<c_void>(),
1216+
struct_size,
1217+
))
1218+
};
1219+
1220+
#[cfg(not(target_vendor = "win7"))]
1221+
if let Err(err) = result {
1222+
if err.raw_os_error() == Some(c::ERROR_INVALID_PARAMETER as _) {
1223+
// FileRenameInfoEx and FILE_RENAME_FLAG_POSIX_SEMANTICS were added in Windows 10 1607; retry with FileRenameInfo.
1224+
file_rename_info.Anonymous.ReplaceIfExists = 1;
1225+
1226+
cvt(unsafe {
1227+
c::SetFileInformationByHandle(
1228+
handle.as_raw_handle(),
1229+
c::FileRenameInfo,
1230+
(&raw const *file_rename_info).cast::<c_void>(),
1231+
struct_size,
1232+
)
1233+
})?;
1234+
} else {
1235+
return Err(err);
1236+
}
1237+
}
1238+
1239+
#[cfg(target_vendor = "win7")]
1240+
result?;
1241+
10991242
Ok(())
11001243
}
11011244

0 commit comments

Comments
 (0)