Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions library/std/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@
#![feature(str_internals)]
#![feature(sync_unsafe_cell)]
#![feature(temporary_niche_types)]
#![feature(trim_prefix_suffix)]
#![feature(ub_checks)]
#![feature(uint_carryless_mul)]
#![feature(used_with_arg)]
Expand Down
71 changes: 68 additions & 3 deletions library/std/src/sys/fs/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::ffi::{OsStr, OsString, c_void};
use crate::fs::TryLockError;
use crate::io::{self, BorrowedCursor, Error, IoSlice, IoSliceMut, SeekFrom};
use crate::mem::{self, MaybeUninit, offset_of};
use crate::os::windows::ffi::{OsStrExt, OsStringExt};
use crate::os::windows::io::{AsHandle, BorrowedHandle};
use crate::os::windows::prelude::*;
use crate::path::{Path, PathBuf};
Expand All @@ -18,6 +19,9 @@ use crate::sys::time::SystemTime;
use crate::sys::{Align8, AsInner, FromInner, IntoInner, c, cvt};
use crate::{fmt, ptr, slice};

#[cfg(test)]
mod tests;

mod dir;
pub use dir::Dir;
mod remove_dir_all;
Expand Down Expand Up @@ -1591,14 +1595,75 @@ pub fn set_times_nofollow(p: &WCStr, times: FileTimes) -> io::Result<()> {
}

fn get_path(f: impl AsRawHandle) -> io::Result<PathBuf> {
let h = f.as_raw_handle();
// If getting the canonical path fails with ERROR_INVALID_FUNCTION
// then it's likely it failed to resolve the path's drive.
// In that case, use the fallback method to resolve it.
let invalid_function = Some(c::ERROR_INVALID_FUNCTION as i32);
match get_path_canonical(h) {
Err(e) if e.raw_os_error() == invalid_function => get_path_fallback(h).ok_or(e),
result => result,
}
}

fn get_path_canonical(handle: c::HANDLE) -> io::Result<PathBuf> {
fill_utf16_buf(
|buf, sz| unsafe {
c::GetFinalPathNameByHandleW(f.as_raw_handle(), buf, sz, c::VOLUME_NAME_DOS)
},
|buf, sz| unsafe { c::GetFinalPathNameByHandleW(handle, buf, sz, c::VOLUME_NAME_DOS) },
|buf| PathBuf::from(OsString::from_wide(buf)),
)
}

/// Fallback in case `get_path_canonical` fails.

@clarfonthey clarfonthey Aug 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I know it's going to be a pain, but it would be worth adding some of the extra context that you shared on Zulip (+ more if you've got it) on when exactly this might fail. Knowing that it generally is a driver issue for the specific drive (whether it's physical hardware or virtual) is helpful here, and it would also be helpful to know why the normal method can fail while this would still succeed.

View changes since the review

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've now added a brief explanation

///
/// `get_path_canonical` can fail if the Win32 drive name cannot be resolved.
/// This can happen with certain third party drivers that don't integrate
/// with the mount manager.
///
/// Instead we manually do the same job by getting the NT path
/// and then finding the first drive letter that points to a prefix of
/// that path. From there we can construct a Win32 path.
///
/// It's implemented by first getting the NT path, which should always succeed.
/// Then we use [`GetLogicalDrives`] to get a bit array of win32 drive letters
/// from 'A' to 'Z'. If the corresponding bit is set then it means that drive exists.
/// E.g. bit 2 being set means there's a `C:` drive.
///
/// Then for each drive we use [`QueryDosDeviceW`] to see the NT path that drive resolves to.
/// If that path is a prefix to the path we got initially then we treat that as the canonical drive letter.
/// So in the unlikely even two drives point to the same device, the lowest one is considered canonical.
///
/// [`GetLogicalDrives`]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getlogicaldrives
/// [`QueryDosDeviceW`]:
fn get_path_fallback(handle: c::HANDLE) -> Option<PathBuf> {
fill_utf16_buf(
|buf, sz| unsafe { c::GetFinalPathNameByHandleW(handle, buf, sz, c::VOLUME_NAME_NT) },
|nt_path| {
let mut buf = [0_u16; c::MAX_PATH as usize];
for letter in api::get_logical_drives() {
let device_name = [letter as u16, b':' as u16, 0];
// SAFETY: `device_name` is a null terminated u16 string
if let Some(drive_path) = unsafe { api::query_dos_device(&device_name, &mut buf) } {
if let Some(nt_path) = nt_path.strip_prefix(drive_path) {
// Reserve approximately enough space for the drive + path.
let mut path = Vec::with_capacity(r"\\?\C:".len() + nt_path.len());
// Create a verbatim drive root (e.g. \\?\C:)
path.extend_from_slice(&[b'\\', b'\\', b'?', b'\\', letter, b':']);
path.extend(OsString::from_wide(nt_path).into_encoded_bytes());
// SAFETY: All characters are either in the ASCII range (the prefix)
// or else came from an OsString.
unsafe {
return Some(OsString::from_encoded_bytes_unchecked(path).into());
}
}
}
}
None
},
)
.ok()
.flatten()
}

pub fn canonicalize(p: &WCStr) -> io::Result<PathBuf> {
let mut opts = OpenOptions::new();
// No read or write permissions are necessary
Expand Down
21 changes: 21 additions & 0 deletions library/std/src/sys/fs/windows/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use super::{get_path_canonical, get_path_fallback};
use crate::env;
use crate::fs::{File, canonicalize};
use crate::os::windows::io::AsRawHandle;
use crate::test_helpers::tmpdir;

#[test]
/// Test that `get_path_canonical` and `get_path_fallback` return the exact same path.
fn canonicalize_fallback() {
let t = tmpdir();
let fname = t.join("hello.txt");
// This test may break if run in an environment that requires the fallback.
// So skip it if not in CI.
if env::var_os("CI").is_none() && canonicalize(&fname).is_err() {
return;
}
let f = File::create(fname).unwrap();
let canonical = get_path_canonical(f.as_raw_handle()).unwrap();
let fallback = get_path_fallback(f.as_raw_handle()).unwrap();
assert_eq!(canonical, fallback);
}
39 changes: 39 additions & 0 deletions library/std/src/sys/pal/windows/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,3 +364,42 @@ pub macro unicode_str {
)
}
}

/// Returns a list of enabled drive letters.
///
/// This is a wrapper around [`GetLogicalDrives`].
/// Each letter is returned as an ascii byte.
///
/// [`GetLogicalDrives`]: (https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getlogicaldrives)
pub fn get_logical_drives() -> impl Iterator<Item = u8> {
// SAFETY: `GetLogicalDrives` only returns information.
let drives = unsafe { c::GetLogicalDrives() };
(b'A'..=b'Z').filter(move |letter| drives >> (letter - b'A') & 1 == 1)
}

/// Get the NT path a device name points to.
///
/// # Safety
///
/// `device_name` must be null-terminated.
// FIXME: Use a null-terminated wide string type to assert validity, similar to CStr.
// Then this function can be safe.
pub unsafe fn query_dos_device<'a>(
device_name: &[u16],
buffer: &'a mut [u16],
) -> Option<&'a [u16]> {
let device_ptr = device_name.as_ptr();
let buffer_ptr = buffer.as_mut_ptr();
let buffer_len = buffer.len().try_into().ok()?;
// SAFETY: `device_ptr` points to a null-terminated u16 string.
// `buffer_ptr` is writeable up to buffer_len u16s.
let result = unsafe { c::QueryDosDeviceW(device_ptr, buffer_ptr, buffer_len) } as usize;
if result > 0 {
// QueryDosDeviceW returns a list of null-terminated strings where the list itself is also null-terminated
// In the case where you pass a device name (which we always) it only returns one string.
// Therefore to get the string we trim off both the list null termination and the string null termination.
Some(buffer[..result].trim_suffix(&[0, 0]))
} else {
None
}
}
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 @@ -2181,6 +2181,7 @@ GETFINALPATHNAMEBYHANDLE_FLAGS
GetFinalPathNameByHandleW
GetFullPathNameW
GetLastError
GetLogicalDrives
GetModuleFileNameW
GetModuleHandleA
GetModuleHandleExW
Expand Down Expand Up @@ -2354,6 +2355,7 @@ PROFILE_KERNEL
PROFILE_SERVER
PROFILE_USER
PROGRESS_CONTINUE
QueryDosDeviceW
QueryPerformanceCounter
QueryPerformanceFrequency
READ_CONTROL
Expand Down Expand Up @@ -2508,6 +2510,7 @@ UpdateProcThreadAttribute
VOLUME_NAME_DOS
VOLUME_NAME_GUID
VOLUME_NAME_NONE
VOLUME_NAME_NT
WAIT_ABANDONED
WAIT_ABANDONED_0
WAIT_FAILED
Expand Down
3 changes: 3 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 @@ -54,6 +54,7 @@ windows_link::link!("kernel32.dll" "system" fn GetFileType(hfile : HANDLE) -> FI
windows_link::link!("kernel32.dll" "system" fn GetFinalPathNameByHandleW(hfile : HANDLE, lpszfilepath : PWSTR, cchfilepath : u32, dwflags : GETFINALPATHNAMEBYHANDLE_FLAGS) -> u32);
windows_link::link!("kernel32.dll" "system" fn GetFullPathNameW(lpfilename : PCWSTR, nbufferlength : u32, lpbuffer : PWSTR, lpfilepart : *mut PWSTR) -> u32);
windows_link::link!("kernel32.dll" "system" fn GetLastError() -> WIN32_ERROR);
windows_link::link!("kernel32.dll" "system" fn GetLogicalDrives() -> u32);
windows_link::link!("kernel32.dll" "system" fn GetModuleFileNameW(hmodule : HMODULE, lpfilename : PWSTR, nsize : u32) -> u32);
windows_link::link!("kernel32.dll" "system" fn GetModuleHandleA(lpmodulename : PCSTR) -> HMODULE);
windows_link::link!("kernel32.dll" "system" fn GetModuleHandleExW(dwflags : u32, lpmodulename : PCWSTR, phmodule : *mut HMODULE) -> BOOL);
Expand Down Expand Up @@ -83,6 +84,7 @@ windows_link::link!("ntdll.dll" "system" fn NtReadFile(filehandle : HANDLE, even
windows_link::link!("ntdll.dll" "system" fn NtSetInformationFile(filehandle : HANDLE, iostatusblock : *mut IO_STATUS_BLOCK, fileinformation : *const core::ffi::c_void, length : u32, fileinformationclass : FILE_INFORMATION_CLASS) -> NTSTATUS);
windows_link::link!("ntdll.dll" "system" fn NtWriteFile(filehandle : HANDLE, event : HANDLE, apcroutine : PIO_APC_ROUTINE, apccontext : *const core::ffi::c_void, iostatusblock : *mut IO_STATUS_BLOCK, buffer : *const core::ffi::c_void, length : u32, byteoffset : *const i64, key : *const u32) -> NTSTATUS);
windows_link::link!("advapi32.dll" "system" fn OpenProcessToken(processhandle : HANDLE, desiredaccess : TOKEN_ACCESS_MASK, tokenhandle : *mut HANDLE) -> BOOL);
windows_link::link!("kernel32.dll" "system" fn QueryDosDeviceW(lpdevicename : PCWSTR, lptargetpath : PWSTR, ucchmax : u32) -> u32);
windows_link::link!("kernel32.dll" "system" fn QueryPerformanceCounter(lpperformancecount : *mut i64) -> BOOL);
windows_link::link!("kernel32.dll" "system" fn QueryPerformanceFrequency(lpfrequency : *mut i64) -> BOOL);
windows_link::link!("kernel32.dll" "system" fn ReadConsoleW(hconsoleinput : HANDLE, lpbuffer : *mut core::ffi::c_void, nnumberofcharstoread : u32, lpnumberofcharsread : *mut u32, pinputcontrol : *const CONSOLE_READCONSOLE_CONTROL) -> BOOL);
Expand Down Expand Up @@ -3411,6 +3413,7 @@ impl Default for UNICODE_STRING {
pub const VOLUME_NAME_DOS: GETFINALPATHNAMEBYHANDLE_FLAGS = 0u32;
pub const VOLUME_NAME_GUID: GETFINALPATHNAMEBYHANDLE_FLAGS = 1u32;
pub const VOLUME_NAME_NONE: GETFINALPATHNAMEBYHANDLE_FLAGS = 4u32;
pub const VOLUME_NAME_NT: GETFINALPATHNAMEBYHANDLE_FLAGS = 2u32;
pub const WAIT_ABANDONED: WAIT_EVENT = 128u32;
pub const WAIT_ABANDONED_0: WAIT_EVENT = 128u32;
pub type WAIT_EVENT = u32;
Expand Down
2 changes: 1 addition & 1 deletion src/tools/generate-windows-sys/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ fn main() -> Result<(), Box<dyn Error>> {

let mut f = std::fs::File::options().append(true).open("windows_sys.rs")?;
f.write_all(ARM32_SHIM.as_bytes())?;
writeln!(&mut f, "// ignore-tidy-filelength")?;
writeln!(&mut f, "// ignore-tidy-file-filelength")?;

Ok(())
}
Expand Down
Loading