Skip to content

Commit 2fc2a33

Browse files
committed
fix(workspace): use native anchored Windows rename
1 parent 4a33d28 commit 2fc2a33

1 file changed

Lines changed: 87 additions & 21 deletions

File tree

crates/localityd/src/windows_workspace_fs.rs

Lines changed: 87 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,24 @@ use std::ptr;
1111
use windows_sys::Wdk::Foundation::OBJECT_ATTRIBUTES;
1212
use windows_sys::Wdk::Storage::FileSystem::{
1313
FILE_CREATE, FILE_DIRECTORY_FILE, FILE_NON_DIRECTORY_FILE, FILE_OPEN, FILE_OPEN_IF,
14-
FILE_OPEN_REPARSE_POINT, FILE_SYNCHRONOUS_IO_NONALERT, NtCreateFile,
14+
FILE_OPEN_REPARSE_POINT, FILE_RENAME_INFORMATION, FILE_SYNCHRONOUS_IO_NONALERT,
15+
FileRenameInformation, NtCreateFile, NtSetInformationFile,
1516
};
1617
use windows_sys::Win32::Foundation::{
17-
HANDLE, OBJ_CASE_INSENSITIVE, RtlNtStatusToDosError, STATUS_OBJECT_NAME_COLLISION,
18-
STATUS_OBJECT_NAME_NOT_FOUND, UNICODE_STRING,
18+
HANDLE, NTSTATUS, OBJ_CASE_INSENSITIVE, RtlNtStatusToDosError, STATUS_NO_SUCH_FILE,
19+
STATUS_OBJECT_NAME_COLLISION, STATUS_OBJECT_NAME_EXISTS, STATUS_OBJECT_NAME_NOT_FOUND,
20+
STATUS_OBJECT_PATH_NOT_FOUND, UNICODE_STRING,
1921
};
2022
use windows_sys::Win32::Storage::FileSystem::{
2123
DELETE, FILE_ADD_FILE, FILE_ADD_SUBDIRECTORY, FILE_ATTRIBUTE_NORMAL, FILE_ATTRIBUTE_READONLY,
2224
FILE_ATTRIBUTE_REPARSE_POINT, FILE_ATTRIBUTE_TAG_INFO, FILE_BASIC_INFO,
2325
FILE_DISPOSITION_FLAG_DELETE, FILE_DISPOSITION_FLAG_IGNORE_READONLY_ATTRIBUTE,
2426
FILE_DISPOSITION_FLAG_POSIX_SEMANTICS, FILE_DISPOSITION_INFO_EX, FILE_ID_INFO,
25-
FILE_LIST_DIRECTORY, FILE_READ_ATTRIBUTES, FILE_READ_DATA, FILE_RENAME_INFO, FILE_SHARE_DELETE,
26-
FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_TRAVERSE, FILE_WRITE_ATTRIBUTES, FILE_WRITE_DATA,
27-
FileAttributeTagInfo, FileBasicInfo, FileDispositionInfoEx, FileIdInfo, FileRenameInfo,
28-
FlushFileBuffers, GetFileInformationByHandleEx, LOCKFILE_EXCLUSIVE_LOCK, LockFileEx,
29-
SYNCHRONIZE, SetFileInformationByHandle,
27+
FILE_LIST_DIRECTORY, FILE_READ_ATTRIBUTES, FILE_READ_DATA, FILE_SHARE_DELETE, FILE_SHARE_READ,
28+
FILE_SHARE_WRITE, FILE_TRAVERSE, FILE_WRITE_ATTRIBUTES, FILE_WRITE_DATA, FileAttributeTagInfo,
29+
FileBasicInfo, FileDispositionInfoEx, FileIdInfo, FlushFileBuffers,
30+
GetFileInformationByHandleEx, LOCKFILE_EXCLUSIVE_LOCK, LockFileEx, SYNCHRONIZE,
31+
SetFileInformationByHandle,
3032
};
3133
use windows_sys::Win32::System::IO::{IO_STATUS_BLOCK, OVERLAPPED};
3234

@@ -380,24 +382,29 @@ impl WindowsDirectory {
380382
.and_then(|length| u32::try_from(length).ok())
381383
.ok_or_else(|| io::Error::other("workspace destination name is too long"))?;
382384
let total = rename_information_buffer_size(name.len())?;
385+
let total_u32 = u32::try_from(total)
386+
.map_err(|_| io::Error::other("workspace rename buffer is too large"))?;
383387
let words = total.div_ceil(size_of::<usize>());
384388
let mut storage = vec![0_usize; words];
385-
let info = storage.as_mut_ptr().cast::<FILE_RENAME_INFO>();
389+
let info = storage.as_mut_ptr().cast::<FILE_RENAME_INFORMATION>();
386390
// SAFETY: `storage` is suitably aligned and large enough for the fixed
387-
// header plus the exact UTF-16 name payload.
391+
// header plus the exact UTF-16 name payload. Both source and parent
392+
// handles remain owned for the synchronous native rename call.
388393
unsafe {
389394
(*info).Anonymous.ReplaceIfExists = false;
390395
(*info).RootDirectory = raw_handle(&destination_parent.handle);
391396
(*info).FileNameLength = byte_len;
392397
ptr::copy_nonoverlapping(name.as_ptr(), (*info).FileName.as_mut_ptr(), name.len());
393-
if SetFileInformationByHandle(
398+
let mut status_block: IO_STATUS_BLOCK = zeroed();
399+
let status = NtSetInformationFile(
394400
raw_handle(&self.handle),
395-
FileRenameInfo,
401+
&mut status_block,
396402
info.cast(),
397-
total as u32,
398-
) == 0
399-
{
400-
return Err(io::Error::last_os_error());
403+
total_u32,
404+
FileRenameInformation,
405+
);
406+
if status < 0 || status == STATUS_OBJECT_NAME_EXISTS {
407+
return Err(rename_status_error(status));
401408
}
402409
}
403410
Ok(())
@@ -411,14 +418,28 @@ fn rename_information_buffer_size(name_units: usize) -> io::Result<usize> {
411418
let trailing_bytes = trailing_units
412419
.checked_mul(size_of::<u16>())
413420
.ok_or_else(|| io::Error::other("workspace rename buffer overflow"))?;
414-
// `FILE_RENAME_INFO` already contains one UTF-16 code unit plus tail
421+
// `FILE_RENAME_INFORMATION` already contains one UTF-16 code unit plus tail
415422
// padding. Starting at `FileName` omits that padding and Windows rejects
416423
// the otherwise valid rename buffer with ERROR_INVALID_PARAMETER.
417-
size_of::<FILE_RENAME_INFO>()
424+
size_of::<FILE_RENAME_INFORMATION>()
418425
.checked_add(trailing_bytes)
419426
.ok_or_else(|| io::Error::other("workspace rename buffer overflow"))
420427
}
421428

429+
fn rename_status_error(status: NTSTATUS) -> io::Error {
430+
let kind = match status {
431+
STATUS_OBJECT_NAME_COLLISION | STATUS_OBJECT_NAME_EXISTS => io::ErrorKind::AlreadyExists,
432+
STATUS_OBJECT_NAME_NOT_FOUND | STATUS_OBJECT_PATH_NOT_FOUND | STATUS_NO_SUCH_FILE => {
433+
io::ErrorKind::NotFound
434+
}
435+
_ => io::ErrorKind::Other,
436+
};
437+
io::Error::new(
438+
kind,
439+
io::Error::from_raw_os_error(unsafe { RtlNtStatusToDosError(status) } as i32),
440+
)
441+
}
442+
422443
pub(crate) fn lock_file_exclusive(file: &File) -> io::Result<()> {
423444
let mut overlapped = OVERLAPPED::default();
424445
// SAFETY: the file handle and overlapped structure are valid for this
@@ -718,16 +739,28 @@ mod lock_tests {
718739
fn rename_buffer_includes_the_struct_tail_and_alignment() {
719740
assert_eq!(
720741
rename_information_buffer_size(1).expect("one-unit rename buffer"),
721-
size_of::<FILE_RENAME_INFO>()
742+
size_of::<FILE_RENAME_INFORMATION>()
722743
);
723744
assert_eq!(
724745
rename_information_buffer_size(8).expect("eight-unit rename buffer"),
725-
size_of::<FILE_RENAME_INFO>() + 7 * size_of::<u16>()
746+
size_of::<FILE_RENAME_INFORMATION>() + 7 * size_of::<u16>()
726747
);
727748
assert!(rename_information_buffer_size(0).is_err());
728749
assert!(
729750
rename_information_buffer_size(8).expect("aligned rename buffer")
730-
> std::mem::offset_of!(FILE_RENAME_INFO, FileName) + 8 * size_of::<u16>()
751+
> std::mem::offset_of!(FILE_RENAME_INFORMATION, FileName) + 8 * size_of::<u16>()
752+
);
753+
assert_eq!(
754+
rename_status_error(STATUS_OBJECT_NAME_COLLISION).kind(),
755+
io::ErrorKind::AlreadyExists
756+
);
757+
assert_eq!(
758+
rename_status_error(STATUS_OBJECT_NAME_NOT_FOUND).kind(),
759+
io::ErrorKind::NotFound
760+
);
761+
assert_eq!(
762+
rename_status_error(STATUS_OBJECT_PATH_NOT_FOUND).kind(),
763+
io::ErrorKind::NotFound
731764
);
732765
}
733766

@@ -753,6 +786,39 @@ mod lock_tests {
753786
expected
754787
);
755788

789+
let collision_source = parent
790+
.create_directory(OsStr::new("collision-source"))
791+
.expect("create collision source");
792+
let collision_source_identity = collision_source.identity().expect("collision source id");
793+
let collision_destination = parent
794+
.create_directory(OsStr::new("collision-destination"))
795+
.expect("create collision destination");
796+
let collision_destination_identity = collision_destination
797+
.identity()
798+
.expect("collision destination id");
799+
let error = collision_source
800+
.rename_no_replace(&parent, OsStr::new("collision-destination"))
801+
.expect_err("native relative rename must not replace an existing destination");
802+
assert_eq!(error.kind(), io::ErrorKind::AlreadyExists);
803+
assert_eq!(
804+
parent
805+
.open_directory_read_only(OsStr::new("collision-source"))
806+
.expect("collision source remains")
807+
.identity()
808+
.expect("remaining collision source id"),
809+
collision_source_identity
810+
);
811+
assert_eq!(
812+
parent
813+
.open_directory_read_only(OsStr::new("collision-destination"))
814+
.expect("collision destination remains")
815+
.identity()
816+
.expect("remaining collision destination id"),
817+
collision_destination_identity
818+
);
819+
820+
drop(collision_destination);
821+
drop(collision_source);
756822
drop(source);
757823
drop(parent);
758824
std::fs::remove_dir_all(path).expect("remove rename test directory");

0 commit comments

Comments
 (0)