diff --git a/src/uucore/src/lib/features.rs b/src/uucore/src/lib/features.rs index 24057b8d39f..d11274f6f08 100644 --- a/src/uucore/src/lib/features.rs +++ b/src/uucore/src/lib/features.rs @@ -32,6 +32,8 @@ pub mod fsext; pub mod i18n; #[cfg(feature = "lines")] pub mod lines; +#[cfg(all(windows, feature = "fsext"))] +pub mod nt; #[cfg(any( feature = "parser", feature = "parser-num", diff --git a/src/uucore/src/lib/features/fsext.rs b/src/uucore/src/lib/features/fsext.rs index 0ede0f2875f..a8b07018880 100644 --- a/src/uucore/src/lib/features/fsext.rs +++ b/src/uucore/src/lib/features/fsext.rs @@ -14,8 +14,6 @@ const LINUX_MOUNTINFO: &str = "/proc/self/mountinfo"; #[cfg(all(unix, not(any(target_os = "aix", target_os = "redox"))))] static MOUNT_OPT_BIND: &str = "bind"; #[cfg(windows)] -const MAX_PATH: usize = 266; -#[cfg(windows)] static EXIT_ERR: i32 = 1; #[cfg(any( @@ -25,40 +23,19 @@ static EXIT_ERR: i32 = 1; target_os = "openbsd" ))] use crate::os_str_from_bytes; -#[cfg(windows)] -use crate::show_warning; -#[cfg(not(target_os = "wasi"))] +#[cfg(not(any(windows, target_os = "wasi")))] use std::ffi::OsStr; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; #[cfg(windows)] -use std::os::windows::ffi::OsStrExt; -#[cfg(windows)] use windows_sys::Win32::{ - Foundation::{ERROR_NO_MORE_FILES, INVALID_HANDLE_VALUE}, + Foundation::{ERROR_NO_MORE_FILES, INVALID_HANDLE_VALUE, MAX_PATH}, Storage::FileSystem::{ - FindFirstVolumeW, FindNextVolumeW, FindVolumeClose, GetDiskFreeSpaceW, GetDriveTypeW, - GetVolumeInformationW, GetVolumePathNamesForVolumeNameW, QueryDosDeviceW, + FindFirstVolumeW, FindNextVolumeW, FindVolumeClose, GetVolumePathNamesForVolumeNameW, }, - System::WindowsProgramming::DRIVE_REMOTE, }; -#[cfg(windows)] -#[allow(non_snake_case)] -fn LPWSTR2String(buf: &[u16]) -> String { - let len = buf.iter().position(|&n| n == 0).unwrap(); - String::from_utf16(&buf[..len]).unwrap() -} - -#[cfg(windows)] -fn to_nul_terminated_wide_string(s: impl AsRef) -> Vec { - s.as_ref() - .encode_wide() - .chain(Some(0)) - .collect::>() -} - #[cfg(unix)] use libc::{ S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK, mode_t, strerror, @@ -270,72 +247,78 @@ impl MountInfo { } #[cfg(windows)] - fn new(mut volume_name: String) -> Option { - let mut dev_name_buf = [0u16; MAX_PATH]; - volume_name.pop(); - unsafe { - QueryDosDeviceW( - OsStr::new(&volume_name) - .encode_wide() - .chain(Some(0)) - .skip(4) - .collect::>() - .as_ptr(), - dev_name_buf.as_mut_ptr(), - dev_name_buf.len() as u32, - ) - }; - volume_name.push('\\'); - let dev_name = LPWSTR2String(&dev_name_buf); - - let mut mount_root_buf = [0u16; MAX_PATH]; - let success = unsafe { - let volume_name = to_nul_terminated_wide_string(&volume_name); - GetVolumePathNamesForVolumeNameW( - volume_name.as_ptr(), - mount_root_buf.as_mut_ptr(), - mount_root_buf.len() as u32, - ptr::null_mut(), - ) - }; - if 0 == success { - // TODO: support the case when `GetLastError()` returns `ERROR_MORE_DATA` + fn new(volume_name_buf: &[u16]) -> Option { + use super::nt; + let nul = volume_name_buf.iter().position(|&c| c == 0)?; + let volume_name = String::from_utf16_lossy(&volume_name_buf[..nul]); + if !volume_name.starts_with("\\\\?\\") || !volume_name.ends_with('\\') { return None; } - // TODO: This should probably call `OsString::from_wide`, but unclear if - // terminating zeros need to be striped first. - let mount_root = LPWSTR2String(&mount_root_buf); - - let mut fs_type_buf = [0u16; MAX_PATH]; - let success = unsafe { - let mount_root = to_nul_terminated_wide_string(&mount_root); - GetVolumeInformationW( - mount_root.as_ptr(), - ptr::null_mut(), - 0, - ptr::null_mut(), - ptr::null_mut(), - ptr::null_mut(), - fs_type_buf.as_mut_ptr(), - fs_type_buf.len() as u32, + + let handle = nt::open_file( + Path::new(&volume_name), + nt::SYNCHRONIZE, + nt::FILE_SYNCHRONOUS_IO_NONALERT | nt::FILE_DIRECTORY_FILE, + ) + .ok()?; + + let fs_type = unsafe { + nt::query_volume_information::( + &handle, + nt::FileFsAttributeInformation, ) + } + .map(|info| { + let len = info.file_system_name_length as usize / size_of::(); + String::from_utf16_lossy(&info.file_system_name[..len]) + }) + .unwrap_or_default(); + + let remote = unsafe { + nt::query_volume_information::( + &handle, + nt::FileFsDeviceInformation, + ) + } + .is_ok_and(|info| info.characteristics & nt::FILE_REMOTE_DEVICE != 0); + + let mount_dir: String = unsafe { + // TODO: Once we're on Rust 1.93 we could use MaybeUninit + // here and extract the range with assume_init_ref() below. + let mut buf = [0u16; MAX_PATH as usize]; + let mut len = 0u32; + if 0 == GetVolumePathNamesForVolumeNameW( + volume_name_buf.as_ptr(), + buf.as_mut_ptr(), + MAX_PATH, + &raw mut len, + ) { + return None; + } + + // The buffer contains a double-null-terminated list, + // which this turns into a comma separated string. + buf[..len as usize] + .split(|&c| c == 0) + .filter(|s| !s.is_empty()) + .fold(String::new(), |mut acc, str| { + if !acc.is_empty() { + acc.push_str(", "); + } + acc.extend( + char::decode_utf16(str.iter().copied()) + .map(|r| r.unwrap_or(char::REPLACEMENT_CHARACTER)), + ); + acc + }) }; - let fs_type = if 0 == success { - None - } else { - Some(LPWSTR2String(&fs_type_buf)) - }; - let remote = DRIVE_REMOTE - == unsafe { - let mount_root = to_nul_terminated_wide_string(&mount_root); - GetDriveTypeW(mount_root.as_ptr()) - }; + Some(Self { - dev_id: volume_name, - dev_name, - fs_type: fs_type.unwrap_or_default(), - mount_root: mount_root.into(), // TODO: We should figure out how to keep an OsString here. - mount_dir: OsString::new(), + dev_id: volume_name.clone(), + dev_name: volume_name, + fs_type, + mount_root: OsString::new(), + mount_dir: mount_dir.into(), mount_option: String::new(), remote, dummy: false, @@ -445,7 +428,6 @@ use std::io::{BufRead, BufReader}; #[cfg(any( target_vendor = "apple", target_os = "freebsd", - target_os = "windows", target_os = "netbsd", target_os = "openbsd" ))] @@ -496,7 +478,7 @@ pub fn read_fs_list() -> UResult> { } #[cfg(windows)] { - let mut volume_name_buf = [0u16; MAX_PATH]; + let mut volume_name_buf = [0u16; MAX_PATH as usize]; // As recommended in the MS documentation, retrieve the first volume before the others let find_handle = unsafe { FindFirstVolumeW(volume_name_buf.as_mut_ptr(), volume_name_buf.len() as u32) }; @@ -507,12 +489,7 @@ pub fn read_fs_list() -> UResult> { } let mut mounts = Vec::::new(); loop { - let volume_name = LPWSTR2String(&volume_name_buf); - if !volume_name.starts_with("\\\\?\\") || !volume_name.ends_with('\\') { - show_warning!("A bad path was skipped: {volume_name}"); - continue; - } - if let Some(m) = MountInfo::new(volume_name) { + if let Some(m) = MountInfo::new(&volume_name_buf) { mounts.push(m); } if 0 == unsafe { @@ -524,6 +501,7 @@ pub fn read_fs_list() -> UResult> { } { let err = IOError::last_os_error(); if err.raw_os_error() != Some(ERROR_NO_MORE_FILES as i32) { + unsafe { FindVolumeClose(find_handle) }; let msg = format!("FindNextVolumeW failed: {err}"); return Err(USimpleError::new(EXIT_ERR, msg)); } @@ -624,53 +602,33 @@ impl FsUsage { } #[cfg(windows)] pub fn new(path: &Path) -> UResult { - let mut root_path = [0u16; MAX_PATH]; - let success = unsafe { - let path = to_nul_terminated_wide_string(path); - GetVolumePathNamesForVolumeNameW( - //path_utf8.as_ptr(), - path.as_ptr(), - root_path.as_mut_ptr(), - root_path.len() as u32, - ptr::null_mut(), - ) - }; - if 0 == success { - let msg = format!( - "GetVolumePathNamesForVolumeNameW failed: {}", - IOError::last_os_error() - ); - return Err(USimpleError::new(EXIT_ERR, msg)); - } - - let mut sectors_per_cluster = 0; - let mut bytes_per_sector = 0; - let mut number_of_free_clusters = 0; - let mut total_number_of_clusters = 0; - - unsafe { - let path = to_nul_terminated_wide_string(path); - GetDiskFreeSpaceW( - path.as_ptr(), - &raw mut sectors_per_cluster, - &raw mut bytes_per_sector, - &raw mut number_of_free_clusters, - &raw mut total_number_of_clusters, - ); - } + use super::nt; + + let handle = nt::open_file( + path, + nt::SYNCHRONIZE, + nt::FILE_SYNCHRONOUS_IO_NONALERT + | nt::FILE_DIRECTORY_FILE + | nt::FILE_OPEN_FOR_FREE_SPACE_QUERY, + )?; + + let info: nt::FILE_FS_FULL_SIZE_INFORMATION = + unsafe { nt::query_volume_information(&handle, nt::FileFsFullSizeInformation)? }; + let bytes_per_cluster = + info.sectors_per_allocation_unit as u64 * info.bytes_per_sector as u64; + let avail = info.caller_available_allocation_units as u64; - let bytes_per_cluster = sectors_per_cluster as u64 * bytes_per_sector as u64; Ok(Self { // f_bsize File system block size. blocksize: bytes_per_cluster, // f_blocks - Total number of blocks on the file system, in units of f_frsize. // frsize = Fundamental file system block size (fragment size). - blocks: total_number_of_clusters as u64, + blocks: info.total_allocation_units as u64, // Total number of free blocks. - bfree: number_of_free_clusters as u64, + bfree: info.actual_available_allocation_units as u64, // Total number of free blocks available to non-privileged processes. - bavail: 0, - bavail_top_bit_set: ((bytes_per_sector as u64) & (1u64.rotate_right(1))) != 0, + bavail: avail, + bavail_top_bit_set: (avail & (1u64.rotate_right(1))) != 0, // Total number of file nodes (inodes) on the file system. files: 0, // Not available on windows // Total number of free file nodes (inodes). diff --git a/src/uucore/src/lib/features/nt.rs b/src/uucore/src/lib/features/nt.rs new file mode 100644 index 00000000000..768daaf4172 --- /dev/null +++ b/src/uucore/src/lib/features/nt.rs @@ -0,0 +1,224 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +//! Windows NT API helpers with RAII wrappers. + +use std::os::windows::ffi::OsStrExt; +use std::path::Path; +use std::ptr; +use std::{io::Error, mem::MaybeUninit}; + +use crate::error::{UResult, USimpleError}; + +use windows_sys::Win32::Foundation::NTSTATUS; + +pub const SYNCHRONIZE: u32 = 0x00100000; +pub const FILE_SHARE_READ: u32 = 0x00000001; +pub const FILE_SHARE_WRITE: u32 = 0x00000002; +pub const FILE_SHARE_DELETE: u32 = 0x00000004; +pub const FILE_DIRECTORY_FILE: u32 = 0x00000001; +pub const FILE_SYNCHRONOUS_IO_NONALERT: u32 = 0x00000020; +pub const FILE_OPEN_FOR_FREE_SPACE_QUERY: u32 = 0x00800000; + +const OBJ_CASE_INSENSITIVE: u32 = 0x00000040; + +pub const FILE_REMOTE_DEVICE: u32 = 0x00000010; + +#[allow(non_upper_case_globals)] +pub const FileFsDeviceInformation: u32 = 4; +#[allow(non_upper_case_globals)] +pub const FileFsAttributeInformation: u32 = 5; +#[allow(non_upper_case_globals)] +pub const FileFsFullSizeInformation: u32 = 7; + +#[repr(C)] +pub struct FILE_FS_DEVICE_INFORMATION { + pub device_type: u32, + pub characteristics: u32, +} + +#[repr(C)] +pub struct FILE_FS_ATTRIBUTE_INFORMATION { + pub file_system_attributes: u32, + pub maximum_component_name_length: i32, + pub file_system_name_length: u32, + pub file_system_name: [u16; 128], +} + +#[repr(C)] +pub struct FILE_FS_FULL_SIZE_INFORMATION { + pub total_allocation_units: i64, + pub caller_available_allocation_units: i64, + pub actual_available_allocation_units: i64, + pub sectors_per_allocation_unit: u32, + pub bytes_per_sector: u32, +} + +#[repr(C)] +struct UNICODE_STRING { + length: u16, + maximum_length: u16, + buffer: *mut u16, +} + +#[repr(C)] +struct OBJECT_ATTRIBUTES { + length: u32, + root_directory: *mut std::ffi::c_void, + object_name: *const UNICODE_STRING, + attributes: u32, + security_descriptor: *mut std::ffi::c_void, + security_quality_of_service: *mut std::ffi::c_void, +} + +#[repr(C)] +struct IO_STATUS_BLOCK { + status: NTSTATUS, + information: usize, +} + +unsafe extern "system" { + fn NtOpenFile( + file_handle: *mut *mut std::ffi::c_void, + desired_access: u32, + object_attributes: *const OBJECT_ATTRIBUTES, + io_status_block: *mut IO_STATUS_BLOCK, + share_access: u32, + open_options: u32, + ) -> NTSTATUS; + + fn NtClose(handle: *mut std::ffi::c_void) -> NTSTATUS; + + fn NtQueryVolumeInformationFile( + file_handle: *mut std::ffi::c_void, + io_status_block: *mut IO_STATUS_BLOCK, + fs_information: *mut std::ffi::c_void, + length: u32, + fs_information_class: u32, + ) -> NTSTATUS; + + fn RtlDosPathNameToNtPathName_U( + dos_file_name: *const u16, + nt_file_name: *mut UNICODE_STRING, + file_part: *mut *mut u16, + reserved: *mut std::ffi::c_void, + ) -> u8; + + fn RtlFreeUnicodeString(unicode_string: *mut UNICODE_STRING); +} + +#[repr(transparent)] +pub struct NtHandle(*mut std::ffi::c_void); + +impl Drop for NtHandle { + fn drop(&mut self) { + if !self.0.is_null() { + unsafe { NtClose(self.0) }; + } + } +} + +#[repr(transparent)] +struct UnicodeString(UNICODE_STRING); + +impl UnicodeString { + fn empty() -> Self { + Self(UNICODE_STRING { + length: 0, + maximum_length: 0, + buffer: ptr::null_mut(), + }) + } +} + +impl Drop for UnicodeString { + fn drop(&mut self) { + if !self.0.buffer.is_null() { + unsafe { RtlFreeUnicodeString(&raw mut self.0) }; + } + } +} + +/// Opens a file or directory via `NtOpenFile`. +/// +/// The file is opened with full share access (`READ | WRITE | DELETE`). +pub fn open_file(path: &Path, desired_access: u32, open_options: u32) -> UResult { + let wide: Vec = path.as_os_str().encode_wide().chain(Some(0)).collect(); + let mut nt_path = UnicodeString::empty(); + if unsafe { + RtlDosPathNameToNtPathName_U( + wide.as_ptr(), + &raw mut nt_path.0, + ptr::null_mut(), + ptr::null_mut(), + ) + } == 0 + { + return Err(USimpleError::new( + 1, + format!( + "RtlDosPathNameToNtPathName_U failed: {}", + Error::last_os_error() + ), + )); + } + + let attr = OBJECT_ATTRIBUTES { + length: size_of::() as u32, + root_directory: ptr::null_mut(), + object_name: &nt_path.0, + attributes: OBJ_CASE_INSENSITIVE, + security_descriptor: ptr::null_mut(), + security_quality_of_service: ptr::null_mut(), + }; + let mut handle = ptr::null_mut(); + let mut iosb = MaybeUninit::::uninit(); + let status = unsafe { + NtOpenFile( + &raw mut handle, + desired_access, + &attr, + iosb.as_mut_ptr(), + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + open_options, + ) + }; + if status < 0 { + return Err(USimpleError::new( + 1, + format!("NtOpenFile failed: 0x{:08X}", status as u32), + )); + } + Ok(NtHandle(handle)) +} + +/// Queries volume information for the file associated with the given handle. +/// +/// # Safety +/// +/// `T` must be the correct struct for the given `information_class`. +pub unsafe fn query_volume_information(handle: &NtHandle, information_class: u32) -> UResult { + let mut info = MaybeUninit::::uninit(); + let mut iosb = MaybeUninit::::uninit(); + let status = unsafe { + NtQueryVolumeInformationFile( + handle.0, + iosb.as_mut_ptr(), + info.as_mut_ptr().cast(), + size_of::() as u32, + information_class, + ) + }; + if status < 0 { + return Err(USimpleError::new( + 1, + format!( + "NtQueryVolumeInformationFile failed: 0x{:08X}", + status as u32 + ), + )); + } + Ok(unsafe { info.assume_init() }) +}