Skip to content

Commit 51df98d

Browse files
authored
Rollup merge of rust-lang#131072 - Fulgen301:windows-rename-posix-semantics, r=ChrisDenton
Win: Use POSIX rename semantics for `std::fs::rename` if available Windows 10 1601 introduced `FileRenameInfoEx` as well as `FILE_RENAME_FLAG_POSIX_SEMANTICS`, allowing for atomic renaming and renaming if the target file is has already been opened with `FILE_SHARE_DELETE`, in which case the file gets renamed on disk while the open file handle still refers to the old file, just like in POSIX. This resolves rust-lang#123985, where atomic renaming proved difficult to impossible due to race conditions. If `FileRenameInfoEx` isn't available due to missing support from the underlying filesystem or missing OS support, the renaming is retried with `FileRenameInfo`, which matches the behavior of `MoveFileEx`. This PR also manually replicates parts of `MoveFileEx`'s internal logic, as reverse-engineered from the disassembly: If the source file is a reparse point and said reparse point is a mount point, the mount point itself gets renamed; otherwise the reparse point is resolved and the result renamed. Notes: - Currently, the `win7` target doesn't bother with `FileRenameInfoEx` at all; it's probably desirable to remove that special casing and try `FileRenameInfoEx` anyway if it doesn't exist, in case the binary is run on newer OS versions. Fixes rust-lang#123985
2 parents 4c40c89 + bfadeeb commit 51df98d

File tree

5 files changed

+199
-4
lines changed

5 files changed

+199
-4
lines changed

library/std/src/fs.rs

+5-3
Original file line numberDiff line numberDiff line change
@@ -2397,12 +2397,14 @@ pub fn symlink_metadata<P: AsRef<Path>>(path: P) -> io::Result<Metadata> {
23972397
/// # Platform-specific behavior
23982398
///
23992399
/// This function currently corresponds to the `rename` function on Unix
2400-
/// and the `MoveFileEx` function with the `MOVEFILE_REPLACE_EXISTING` flag on Windows.
2400+
/// and the `SetFileInformationByHandle` function on Windows.
24012401
///
24022402
/// Because of this, the behavior when both `from` and `to` exist differs. On
24032403
/// Unix, if `from` is a directory, `to` must also be an (empty) directory. If
2404-
/// `from` is not a directory, `to` must also be not a directory. In contrast,
2405-
/// on Windows, `from` can be anything, but `to` must *not* be a directory.
2404+
/// `from` is not a directory, `to` must also be not a directory. The behavior
2405+
/// on Windows is the same on Windows 10 1607 and higher if `FileRenameInfoEx`
2406+
/// is supported by the filesystem; otherwise, `from` can be anything, but
2407+
/// `to` must *not* be a directory.
24062408
///
24072409
/// Note that, this [may change in the future][changes].
24082410
///

library/std/src/fs/tests.rs

+41
Original file line numberDiff line numberDiff line change
@@ -1912,3 +1912,44 @@ fn test_hidden_file_truncation() {
19121912
let metadata = file.metadata().unwrap();
19131913
assert_eq!(metadata.len(), 0);
19141914
}
1915+
1916+
#[cfg(windows)]
1917+
#[test]
1918+
fn test_rename_file_over_open_file() {
1919+
// Make sure that std::fs::rename works if the target file is already opened with FILE_SHARE_DELETE. See #123985.
1920+
let tmpdir = tmpdir();
1921+
1922+
// Create source with test data to read.
1923+
let source_path = tmpdir.join("source_file.txt");
1924+
fs::write(&source_path, b"source hello world").unwrap();
1925+
1926+
// Create target file with test data to read;
1927+
let target_path = tmpdir.join("target_file.txt");
1928+
fs::write(&target_path, b"target hello world").unwrap();
1929+
1930+
// Open target file
1931+
let target_file = fs::File::open(&target_path).unwrap();
1932+
1933+
// Rename source
1934+
fs::rename(source_path, &target_path).unwrap();
1935+
1936+
core::mem::drop(target_file);
1937+
assert_eq!(fs::read(target_path).unwrap(), b"source hello world");
1938+
}
1939+
1940+
#[test]
1941+
#[cfg(windows)]
1942+
fn test_rename_directory_to_non_empty_directory() {
1943+
// Renaming a directory over a non-empty existing directory should fail on Windows.
1944+
let tmpdir: TempDir = tmpdir();
1945+
1946+
let source_path = tmpdir.join("source_directory");
1947+
let target_path = tmpdir.join("target_directory");
1948+
1949+
fs::create_dir(&source_path).unwrap();
1950+
fs::create_dir(&target_path).unwrap();
1951+
1952+
fs::write(target_path.join("target_file.txt"), b"target hello world").unwrap();
1953+
1954+
error!(fs::rename(source_path, target_path), 145); // ERROR_DIR_NOT_EMPTY
1955+
}

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

+3
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
@@ -2603,5 +2604,7 @@ Windows.Win32.System.Threading.WaitForMultipleObjects
26032604
Windows.Win32.System.Threading.WaitForSingleObject
26042605
Windows.Win32.System.Threading.WakeAllConditionVariable
26052606
Windows.Win32.System.Threading.WakeConditionVariable
2607+
Windows.Win32.System.WindowsProgramming.FILE_RENAME_FLAG_POSIX_SEMANTICS
2608+
Windows.Win32.System.WindowsProgramming.FILE_RENAME_FLAG_REPLACE_IF_EXISTS
26062609
Windows.Win32.System.WindowsProgramming.PROGRESS_CONTINUE
26072610
Windows.Win32.UI.Shell.GetUserProfileDirectoryW

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

+16
Original file line numberDiff line numberDiff line change
@@ -2472,6 +2472,22 @@ pub const FILE_RANDOM_ACCESS: NTCREATEFILE_CREATE_OPTIONS = 2048u32;
24722472
pub const FILE_READ_ATTRIBUTES: FILE_ACCESS_RIGHTS = 128u32;
24732473
pub const FILE_READ_DATA: FILE_ACCESS_RIGHTS = 1u32;
24742474
pub const FILE_READ_EA: FILE_ACCESS_RIGHTS = 8u32;
2475+
pub const FILE_RENAME_FLAG_POSIX_SEMANTICS: u32 = 2u32;
2476+
pub const FILE_RENAME_FLAG_REPLACE_IF_EXISTS: u32 = 1u32;
2477+
#[repr(C)]
2478+
#[derive(Clone, Copy)]
2479+
pub struct FILE_RENAME_INFO {
2480+
pub Anonymous: FILE_RENAME_INFO_0,
2481+
pub RootDirectory: HANDLE,
2482+
pub FileNameLength: u32,
2483+
pub FileName: [u16; 1],
2484+
}
2485+
#[repr(C)]
2486+
#[derive(Clone, Copy)]
2487+
pub union FILE_RENAME_INFO_0 {
2488+
pub ReplaceIfExists: BOOLEAN,
2489+
pub Flags: u32,
2490+
}
24752491
pub const FILE_RESERVE_OPFILTER: NTCREATEFILE_CREATE_OPTIONS = 1048576u32;
24762492
pub const FILE_SEQUENTIAL_ONLY: NTCREATEFILE_CREATE_OPTIONS = 4u32;
24772493
pub const FILE_SESSION_AWARE: NTCREATEFILE_CREATE_OPTIONS = 262144u32;

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

+134-1
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};
@@ -1223,7 +1224,139 @@ pub fn unlink(p: &Path) -> io::Result<()> {
12231224
pub fn rename(old: &Path, new: &Path) -> io::Result<()> {
12241225
let old = maybe_verbatim(old)?;
12251226
let new = maybe_verbatim(new)?;
1226-
cvt(unsafe { c::MoveFileExW(old.as_ptr(), new.as_ptr(), c::MOVEFILE_REPLACE_EXISTING) })?;
1227+
1228+
let new_len_without_nul_in_bytes = (new.len() - 1).try_into().unwrap();
1229+
1230+
// The last field of FILE_RENAME_INFO, the file name, is unsized,
1231+
// and FILE_RENAME_INFO has two padding bytes.
1232+
// Therefore we need to make sure to not allocate less than
1233+
// size_of::<c::FILE_RENAME_INFO>() bytes, which would be the case with
1234+
// 0 or 1 character paths + a null byte.
1235+
let struct_size = mem::size_of::<c::FILE_RENAME_INFO>()
1236+
.max(mem::offset_of!(c::FILE_RENAME_INFO, FileName) + new.len() * mem::size_of::<u16>());
1237+
1238+
let struct_size: u32 = struct_size.try_into().unwrap();
1239+
1240+
let create_file = |extra_access, extra_flags| {
1241+
let handle = unsafe {
1242+
HandleOrInvalid::from_raw_handle(c::CreateFileW(
1243+
old.as_ptr(),
1244+
c::SYNCHRONIZE | c::DELETE | extra_access,
1245+
c::FILE_SHARE_READ | c::FILE_SHARE_WRITE | c::FILE_SHARE_DELETE,
1246+
ptr::null(),
1247+
c::OPEN_EXISTING,
1248+
c::FILE_ATTRIBUTE_NORMAL | c::FILE_FLAG_BACKUP_SEMANTICS | extra_flags,
1249+
ptr::null_mut(),
1250+
))
1251+
};
1252+
1253+
OwnedHandle::try_from(handle).map_err(|_| io::Error::last_os_error())
1254+
};
1255+
1256+
// The following code replicates `MoveFileEx`'s behavior as reverse-engineered from its disassembly.
1257+
// If `old` refers to a mount point, we move it instead of the target.
1258+
let handle = match create_file(c::FILE_READ_ATTRIBUTES, c::FILE_FLAG_OPEN_REPARSE_POINT) {
1259+
Ok(handle) => {
1260+
let mut file_attribute_tag_info: MaybeUninit<c::FILE_ATTRIBUTE_TAG_INFO> =
1261+
MaybeUninit::uninit();
1262+
1263+
let result = unsafe {
1264+
cvt(c::GetFileInformationByHandleEx(
1265+
handle.as_raw_handle(),
1266+
c::FileAttributeTagInfo,
1267+
file_attribute_tag_info.as_mut_ptr().cast(),
1268+
mem::size_of::<c::FILE_ATTRIBUTE_TAG_INFO>().try_into().unwrap(),
1269+
))
1270+
};
1271+
1272+
if let Err(err) = result {
1273+
if err.raw_os_error() == Some(c::ERROR_INVALID_PARAMETER as _)
1274+
|| err.raw_os_error() == Some(c::ERROR_INVALID_FUNCTION as _)
1275+
{
1276+
// `GetFileInformationByHandleEx` documents that not all underlying drivers support all file information classes.
1277+
// Since we know we passed the correct arguments, this means the underlying driver didn't understand our request;
1278+
// `MoveFileEx` proceeds by reopening the file without inhibiting reparse point behavior.
1279+
None
1280+
} else {
1281+
Some(Err(err))
1282+
}
1283+
} else {
1284+
// SAFETY: The struct has been initialized by GetFileInformationByHandleEx
1285+
let file_attribute_tag_info = unsafe { file_attribute_tag_info.assume_init() };
1286+
1287+
if file_attribute_tag_info.FileAttributes & c::FILE_ATTRIBUTE_REPARSE_POINT != 0
1288+
&& file_attribute_tag_info.ReparseTag != c::IO_REPARSE_TAG_MOUNT_POINT
1289+
{
1290+
// The file is not a mount point: Reopen the file without inhibiting reparse point behavior.
1291+
None
1292+
} else {
1293+
// The file is a mount point: Don't reopen the file so that the mount point gets renamed.
1294+
Some(Ok(handle))
1295+
}
1296+
}
1297+
}
1298+
// The underlying driver may not support `FILE_FLAG_OPEN_REPARSE_POINT`: Retry without it.
1299+
Err(err) if err.raw_os_error() == Some(c::ERROR_INVALID_PARAMETER as _) => None,
1300+
Err(err) => Some(Err(err)),
1301+
}
1302+
.unwrap_or_else(|| create_file(0, 0))?;
1303+
1304+
let layout = core::alloc::Layout::from_size_align(
1305+
struct_size as _,
1306+
mem::align_of::<c::FILE_RENAME_INFO>(),
1307+
)
1308+
.unwrap();
1309+
1310+
let file_rename_info = unsafe { alloc(layout) } as *mut c::FILE_RENAME_INFO;
1311+
1312+
if file_rename_info.is_null() {
1313+
handle_alloc_error(layout);
1314+
}
1315+
1316+
// SAFETY: file_rename_info is a non-null pointer pointing to memory allocated by the global allocator.
1317+
let mut file_rename_info = unsafe { Box::from_raw(file_rename_info) };
1318+
1319+
// SAFETY: We have allocated enough memory for a full FILE_RENAME_INFO struct and a filename.
1320+
unsafe {
1321+
(&raw mut (*file_rename_info).Anonymous).write(c::FILE_RENAME_INFO_0 {
1322+
Flags: c::FILE_RENAME_FLAG_REPLACE_IF_EXISTS | c::FILE_RENAME_FLAG_POSIX_SEMANTICS,
1323+
});
1324+
1325+
(&raw mut (*file_rename_info).RootDirectory).write(ptr::null_mut());
1326+
(&raw mut (*file_rename_info).FileNameLength).write(new_len_without_nul_in_bytes);
1327+
1328+
new.as_ptr()
1329+
.copy_to_nonoverlapping((&raw mut (*file_rename_info).FileName) as *mut u16, new.len());
1330+
}
1331+
1332+
// We don't use `set_file_information_by_handle` here as `FILE_RENAME_INFO` is used for both `FileRenameInfo` and `FileRenameInfoEx`.
1333+
let result = unsafe {
1334+
cvt(c::SetFileInformationByHandle(
1335+
handle.as_raw_handle(),
1336+
c::FileRenameInfoEx,
1337+
(&raw const *file_rename_info).cast::<c_void>(),
1338+
struct_size,
1339+
))
1340+
};
1341+
1342+
if let Err(err) = result {
1343+
if err.raw_os_error() == Some(c::ERROR_INVALID_PARAMETER as _) {
1344+
// FileRenameInfoEx and FILE_RENAME_FLAG_POSIX_SEMANTICS were added in Windows 10 1607; retry with FileRenameInfo.
1345+
file_rename_info.Anonymous.ReplaceIfExists = 1;
1346+
1347+
cvt(unsafe {
1348+
c::SetFileInformationByHandle(
1349+
handle.as_raw_handle(),
1350+
c::FileRenameInfo,
1351+
(&raw const *file_rename_info).cast::<c_void>(),
1352+
struct_size,
1353+
)
1354+
})?;
1355+
} else {
1356+
return Err(err);
1357+
}
1358+
}
1359+
12271360
Ok(())
12281361
}
12291362

0 commit comments

Comments
 (0)