Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rollup of 5 pull requests #134631

Merged
merged 14 commits into from
Dec 22, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
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.
  • Loading branch information
Fulgen301 committed Sep 30, 2024
commit 1e414f1ffff517902979ebde83c8a3be97cc3822
8 changes: 5 additions & 3 deletions library/std/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2171,12 +2171,14 @@ pub fn symlink_metadata<P: AsRef<Path>>(path: P) -> io::Result<Metadata> {
/// # Platform-specific behavior
///
/// This function currently corresponds to the `rename` function on Unix
/// and the `MoveFileEx` function with the `MOVEFILE_REPLACE_EXISTING` flag on Windows.
/// and the `SetFileInformationByHandle` function on Windows.
///
/// Because of this, the behavior when both `from` and `to` exist differs. On
/// Unix, if `from` is a directory, `to` must also be an (empty) directory. If
/// `from` is not a directory, `to` must also be not a directory. In contrast,
/// on Windows, `from` can be anything, but `to` must *not* be a directory.
/// `from` is not a directory, `to` must also be not a directory. The behavior
/// on Windows is the same on Windows 10 1607 and higher if `FileRenameInfoEx`
/// is supported by the filesystem; otherwise, `from` can be anything, but
/// `to` must *not* be a directory.
///
/// Note that, this [may change in the future][changes].
///
Expand Down
3 changes: 3 additions & 0 deletions library/std/src/sys/pal/windows/c/bindings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2295,6 +2295,7 @@ Windows.Win32.Storage.FileSystem.FILE_NAME_OPENED
Windows.Win32.Storage.FileSystem.FILE_READ_ATTRIBUTES
Windows.Win32.Storage.FileSystem.FILE_READ_DATA
Windows.Win32.Storage.FileSystem.FILE_READ_EA
Windows.Win32.Storage.FileSystem.FILE_RENAME_INFO
Windows.Win32.Storage.FileSystem.FILE_SHARE_DELETE
Windows.Win32.Storage.FileSystem.FILE_SHARE_MODE
Windows.Win32.Storage.FileSystem.FILE_SHARE_NONE
Expand Down Expand Up @@ -2597,5 +2598,7 @@ Windows.Win32.System.Threading.WaitForMultipleObjects
Windows.Win32.System.Threading.WaitForSingleObject
Windows.Win32.System.Threading.WakeAllConditionVariable
Windows.Win32.System.Threading.WakeConditionVariable
Windows.Win32.System.WindowsProgramming.FILE_RENAME_FLAG_POSIX_SEMANTICS
Windows.Win32.System.WindowsProgramming.FILE_RENAME_FLAG_REPLACE_IF_EXISTS
Windows.Win32.System.WindowsProgramming.PROGRESS_CONTINUE
Windows.Win32.UI.Shell.GetUserProfileDirectoryW
16 changes: 16 additions & 0 deletions library/std/src/sys/pal/windows/c/windows_sys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2470,6 +2470,22 @@ pub const FILE_RANDOM_ACCESS: NTCREATEFILE_CREATE_OPTIONS = 2048u32;
pub const FILE_READ_ATTRIBUTES: FILE_ACCESS_RIGHTS = 128u32;
pub const FILE_READ_DATA: FILE_ACCESS_RIGHTS = 1u32;
pub const FILE_READ_EA: FILE_ACCESS_RIGHTS = 8u32;
pub const FILE_RENAME_FLAG_POSIX_SEMANTICS: u32 = 2u32;
pub const FILE_RENAME_FLAG_REPLACE_IF_EXISTS: u32 = 1u32;
#[repr(C)]
#[derive(Clone, Copy)]
pub struct FILE_RENAME_INFO {
pub Anonymous: FILE_RENAME_INFO_0,
pub RootDirectory: HANDLE,
pub FileNameLength: u32,
pub FileName: [u16; 1],
}
#[repr(C)]
#[derive(Clone, Copy)]
pub union FILE_RENAME_INFO_0 {
pub ReplaceIfExists: BOOLEAN,
pub Flags: u32,
}
pub const FILE_RESERVE_OPFILTER: NTCREATEFILE_CREATE_OPTIONS = 1048576u32;
pub const FILE_SEQUENTIAL_ONLY: NTCREATEFILE_CREATE_OPTIONS = 4u32;
pub const FILE_SESSION_AWARE: NTCREATEFILE_CREATE_OPTIONS = 262144u32;
Expand Down
145 changes: 144 additions & 1 deletion library/std/src/sys/pal/windows/fs.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::api::{self, WinError};
use super::{IoResult, to_u16s};
use crate::alloc::{alloc, handle_alloc_error};
use crate::borrow::Cow;
use crate::ffi::{OsStr, OsString, c_void};
use crate::io::{self, BorrowedCursor, Error, IoSlice, IoSliceMut, SeekFrom};
Expand Down Expand Up @@ -1095,7 +1096,149 @@ pub fn unlink(p: &Path) -> io::Result<()> {
pub fn rename(old: &Path, new: &Path) -> io::Result<()> {
let old = maybe_verbatim(old)?;
let new = maybe_verbatim(new)?;
cvt(unsafe { c::MoveFileExW(old.as_ptr(), new.as_ptr(), c::MOVEFILE_REPLACE_EXISTING) })?;

let new_len_without_nul_in_bytes = (new.len() - 1).try_into().unwrap();

let struct_size = mem::size_of::<c::FILE_RENAME_INFO>() - mem::size_of::<u16>()
+ new.len() * mem::size_of::<u16>();

let struct_size: u32 = struct_size.try_into().unwrap();

let create_file = |extra_access, extra_flags| {
let handle = unsafe {
HandleOrInvalid::from_raw_handle(c::CreateFileW(
old.as_ptr(),
c::SYNCHRONIZE | c::DELETE | extra_access,
c::FILE_SHARE_READ | c::FILE_SHARE_WRITE | c::FILE_SHARE_DELETE,
ptr::null(),
c::OPEN_EXISTING,
c::FILE_ATTRIBUTE_NORMAL | c::FILE_FLAG_BACKUP_SEMANTICS | extra_flags,
ptr::null_mut(),
))
};

OwnedHandle::try_from(handle).map_err(|_| io::Error::last_os_error())
};

// The following code replicates `MoveFileEx`'s behavior as reverse-engineered from its disassembly.
// If `old` refers to a mount point, we move it instead of the target.
let handle = match create_file(c::FILE_READ_ATTRIBUTES, c::FILE_FLAG_OPEN_REPARSE_POINT) {
Ok(handle) => {
let mut file_attribute_tag_info: MaybeUninit<c::FILE_ATTRIBUTE_TAG_INFO> =
MaybeUninit::uninit();

let result = unsafe {
cvt(c::GetFileInformationByHandleEx(
handle.as_raw_handle(),
c::FileAttributeTagInfo,
file_attribute_tag_info.as_mut_ptr().cast(),
mem::size_of::<c::FILE_ATTRIBUTE_TAG_INFO>().try_into().unwrap(),
))
};

if let Err(err) = result {
if err.raw_os_error() == Some(c::ERROR_INVALID_PARAMETER as _)
|| err.raw_os_error() == Some(c::ERROR_INVALID_FUNCTION as _)
{
// `GetFileInformationByHandleEx` documents that not all underlying drivers support all file information classes.
// Since we know we passed the correct arguments, this means the underlying driver didn't understand our request;
// `MoveFileEx` proceeds by reopening the file without inhibiting reparse point behavior.
None
} else {
Some(Err(err))
}
} else {
// SAFETY: The struct has been initialized by GetFileInformationByHandleEx
let file_attribute_tag_info = unsafe { file_attribute_tag_info.assume_init() };

if file_attribute_tag_info.FileAttributes & c::FILE_ATTRIBUTE_REPARSE_POINT != 0
&& file_attribute_tag_info.ReparseTag != c::IO_REPARSE_TAG_MOUNT_POINT
{
// The file is not a mount point: Reopen the file without inhibiting reparse point behavior.
None
} else {
// The file is a mount point: Don't reopen the file so that the mount point gets renamed.
Some(Ok(handle))
}
}
}
// The underlying driver may not support `FILE_FLAG_OPEN_REPARSE_POINT`: Retry without it.
Err(err) if err.raw_os_error() == Some(c::ERROR_INVALID_PARAMETER as _) => None,
Err(err) => Some(Err(err)),
}
.unwrap_or_else(|| create_file(0, 0))?;

// The last field of FILE_RENAME_INFO, the file name, is unsized.
// Therefore we need to subtract the size of one wide char.
let layout = core::alloc::Layout::from_size_align(
struct_size as _,
mem::align_of::<c::FILE_RENAME_INFO>(),
)
.unwrap();

let file_rename_info = unsafe { alloc(layout) } as *mut c::FILE_RENAME_INFO;

if file_rename_info.is_null() {
handle_alloc_error(layout);
}

// SAFETY: file_rename_info is a non-null pointer pointing to memory allocated by the global allocator.
let mut file_rename_info = unsafe { Box::from_raw(file_rename_info) };

// SAFETY: We have allocated enough memory for a full FILE_RENAME_INFO struct and a filename.
unsafe {
(&raw mut (*file_rename_info).Anonymous).write(c::FILE_RENAME_INFO_0 {
// Don't bother with FileRenameInfo on Windows 7 since it doesn't exist.
#[cfg(not(target_vendor = "win7"))]
Flags: c::FILE_RENAME_FLAG_REPLACE_IF_EXISTS | c::FILE_RENAME_FLAG_POSIX_SEMANTICS,
#[cfg(target_vendor = "win7")]
ReplaceIfExists: 1,
});

(&raw mut (*file_rename_info).RootDirectory).write(ptr::null_mut());
(&raw mut (*file_rename_info).FileNameLength).write(new_len_without_nul_in_bytes);

new.as_ptr()
.copy_to_nonoverlapping((&raw mut (*file_rename_info).FileName) as *mut u16, new.len());
}

#[cfg(not(target_vendor = "win7"))]
const FileInformationClass: c::FILE_INFO_BY_HANDLE_CLASS = c::FileRenameInfoEx;
#[cfg(target_vendor = "win7")]
const FileInformationClass: c::FILE_INFO_BY_HANDLE_CLASS = c::FileRenameInfo;

// We don't use `set_file_information_by_handle` here as `FILE_RENAME_INFO` is used for both `FileRenameInfo` and `FileRenameInfoEx`.
let result = unsafe {
cvt(c::SetFileInformationByHandle(
handle.as_raw_handle(),
FileInformationClass,
(&raw const *file_rename_info).cast::<c_void>(),
struct_size,
))
};

#[cfg(not(target_vendor = "win7"))]
if let Err(err) = result {
if err.raw_os_error() == Some(c::ERROR_INVALID_PARAMETER as _) {
// FileRenameInfoEx and FILE_RENAME_FLAG_POSIX_SEMANTICS were added in Windows 10 1607; retry with FileRenameInfo.
file_rename_info.Anonymous.ReplaceIfExists = 1;

cvt(unsafe {
c::SetFileInformationByHandle(
handle.as_raw_handle(),
c::FileRenameInfo,
(&raw const *file_rename_info).cast::<c_void>(),
struct_size,
)
})?;
} else {
return Err(err);
}
}

#[cfg(target_vendor = "win7")]
result?;

Ok(())
}

Expand Down