diff --git a/library/std/src/lib.rs b/library/std/src/lib.rs index 92eccc27ee05d..5fb512eb3ff9d 100644 --- a/library/std/src/lib.rs +++ b/library/std/src/lib.rs @@ -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)] diff --git a/library/std/src/sys/fs/windows.rs b/library/std/src/sys/fs/windows.rs index c99524375113a..cb43fe6a7e3fc 100644 --- a/library/std/src/sys/fs/windows.rs +++ b/library/std/src/sys/fs/windows.rs @@ -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}; @@ -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; @@ -1591,14 +1595,75 @@ pub fn set_times_nofollow(p: &WCStr, times: FileTimes) -> io::Result<()> { } fn get_path(f: impl AsRawHandle) -> io::Result { + 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 { 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. +/// +/// `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 { + 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 { let mut opts = OpenOptions::new(); // No read or write permissions are necessary diff --git a/library/std/src/sys/fs/windows/tests.rs b/library/std/src/sys/fs/windows/tests.rs new file mode 100644 index 0000000000000..b53dfddecf627 --- /dev/null +++ b/library/std/src/sys/fs/windows/tests.rs @@ -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); +} diff --git a/library/std/src/sys/pal/windows/api.rs b/library/std/src/sys/pal/windows/api.rs index 25a6c2d7d8eda..c3494bf9aa4e6 100644 --- a/library/std/src/sys/pal/windows/api.rs +++ b/library/std/src/sys/pal/windows/api.rs @@ -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 { + // 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 + } +} diff --git a/library/std/src/sys/pal/windows/c/bindings.txt b/library/std/src/sys/pal/windows/c/bindings.txt index a0b2126af9a58..c4c4ac8c7d06e 100644 --- a/library/std/src/sys/pal/windows/c/bindings.txt +++ b/library/std/src/sys/pal/windows/c/bindings.txt @@ -2181,6 +2181,7 @@ GETFINALPATHNAMEBYHANDLE_FLAGS GetFinalPathNameByHandleW GetFullPathNameW GetLastError +GetLogicalDrives GetModuleFileNameW GetModuleHandleA GetModuleHandleExW @@ -2354,6 +2355,7 @@ PROFILE_KERNEL PROFILE_SERVER PROFILE_USER PROGRESS_CONTINUE +QueryDosDeviceW QueryPerformanceCounter QueryPerformanceFrequency READ_CONTROL @@ -2508,6 +2510,7 @@ UpdateProcThreadAttribute VOLUME_NAME_DOS VOLUME_NAME_GUID VOLUME_NAME_NONE +VOLUME_NAME_NT WAIT_ABANDONED WAIT_ABANDONED_0 WAIT_FAILED diff --git a/library/std/src/sys/pal/windows/c/windows_sys.rs b/library/std/src/sys/pal/windows/c/windows_sys.rs index 9c6f593e1e108..c3c5e193e41f1 100644 --- a/library/std/src/sys/pal/windows/c/windows_sys.rs +++ b/library/std/src/sys/pal/windows/c/windows_sys.rs @@ -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); @@ -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); @@ -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; diff --git a/src/tools/generate-windows-sys/src/main.rs b/src/tools/generate-windows-sys/src/main.rs index 9b1d62f14bb7b..e51340af9a95c 100644 --- a/src/tools/generate-windows-sys/src/main.rs +++ b/src/tools/generate-windows-sys/src/main.rs @@ -33,7 +33,7 @@ fn main() -> Result<(), Box> { 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(()) }