diff --git a/src/uu/df/src/filesystem.rs b/src/uu/df/src/filesystem.rs index 3ee55e4f5f1..9b544007a10 100644 --- a/src/uu/df/src/filesystem.rs +++ b/src/uu/df/src/filesystem.rs @@ -8,7 +8,9 @@ //! filesystem mounted at a particular directory. It also includes //! information on amount of space available and amount of space used. // spell-checker:ignore canonicalized -use std::{ffi::OsString, path::Path}; +use std::ffi::OsString; +#[cfg(unix)] +use std::path::Path; use uucore::fsext::{FsUsage, MountInfo}; @@ -59,6 +61,7 @@ pub(crate) enum FsError { /// /// * [`Path::canonicalize`] /// * [`MountInfo::mount_dir`] +#[cfg(unix)] fn mount_info_from_path

( mounts: &[MountInfo], path: P, @@ -128,6 +131,7 @@ impl Filesystem { /// * [`Path::canonicalize`] /// * [`MountInfo::mount_dir`] /// + #[cfg(unix)] pub(crate) fn from_path

(mounts: &[MountInfo], path: P) -> Result where P: AsRef, @@ -141,7 +145,7 @@ impl Filesystem { } } -#[cfg(test)] +#[cfg(all(test, unix))] mod tests { mod mount_info_from_path { diff --git a/src/uu/df/src/platform/windows.rs b/src/uu/df/src/platform/windows.rs index 400d37ba8c2..5f694721498 100644 --- a/src/uu/df/src/platform/windows.rs +++ b/src/uu/df/src/platform/windows.rs @@ -3,7 +3,9 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -//! Windows backend of `df`: volume usage probes and the `-i` notice. +// spell-checker:ignore SUBST + +//! Windows backend of `df`: volume usage probes, path resolution and the `-i` notice. use std::ffi::OsString; use std::path::Path; @@ -21,13 +23,7 @@ pub(crate) fn sync() {} /// Usage of the filesystem at `mount_info`, `None` if it cannot be queried. pub(crate) fn fs_usage(mount_info: &MountInfo) -> Option { - let stat_path = if mount_info.mount_dir.is_empty() { - // On windows, we expect the volume id - mount_info.dev_id.as_ref() - } else { - mount_info.mount_dir.as_os_str() - }; - FsUsage::new(Path::new(stat_path)).ok() + FsUsage::new(Path::new(&mount_info.mount_dir)).ok() } /// Find and create the filesystem from the given mount. @@ -39,7 +35,9 @@ pub(crate) fn filesystem_from_mount( Filesystem::new(mount.clone(), file).ok_or(FsError::MountMissing) } -/// Find and create the filesystem that contains `path` through the mount table. +/// Find and create the filesystem that contains `path`: the mount with the +/// longest directory prefixing it, or, when the mount table does not list it +/// (UNC paths, unreadable table), the volume root the OS reports for it. pub(crate) fn filesystem_for_path

( mounts: &[MountInfo], _use_fallback: bool, @@ -48,7 +46,23 @@ pub(crate) fn filesystem_for_path

( where P: AsRef, { - Filesystem::from_path(mounts, path) + let path = path.as_ref(); + let file = path.as_os_str().to_owned(); + // Not `canonicalize`: it resolves SUBST drives and junctions away and + // yields `\\?\` prefixes that never match a mount directory. + let absolute = std::path::absolute(path).map_err(|_| FsError::InvalidPath)?; + absolute.metadata().map_err(|_| FsError::InvalidPath)?; + let longest = mounts + .iter() + .filter(|m| absolute.starts_with(&m.mount_dir)) + .max_by_key(|m| m.mount_dir.len()); + let mount_info = if let Some(mount_info) = longest { + mount_info.clone() + } else { + let root = uucore::fs::volume_path_name(&absolute).map_err(|_| FsError::MountMissing)?; + MountInfo::from_mount_dir(root.into_os_string()) + }; + Filesystem::new(mount_info, Some(file)).ok_or(FsError::MountMissing) } /// `-i` is not supported: say so and stop successfully. diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 88a6ef9a2ba..05b9b0b156e 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -119,7 +119,9 @@ windows-sys = { workspace = true, optional = true, default-features = false, fea "Win32_Storage_FileSystem", "Win32_Foundation", "Win32_Globalization", + "Win32_NetworkManagement_WNet", "Win32_System_Console", + "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_JobObjects", @@ -144,7 +146,7 @@ entries = ["libc", "rustix/fs", "rustix/process"] extendedbigdecimal = ["bigdecimal", "num-traits"] fast-inc = [] fs = ["dunce", "libc", "rustix/fs", "windows-sys"] -fsext = ["libc", "windows-sys", "bstr"] +fsext = ["libc", "windows-sys", "bstr", "wide"] fsxattr = ["xattr", "itertools", "libc"] hardware = [] lines = [] diff --git a/src/uucore/src/lib/features/fsext.rs b/src/uucore/src/lib/features/fsext/mod.rs similarity index 82% rename from src/uucore/src/lib/features/fsext.rs rename to src/uucore/src/lib/features/fsext/mod.rs index 79d34644f76..b676936b130 100644 --- a/src/uucore/src/lib/features/fsext.rs +++ b/src/uucore/src/lib/features/fsext/mod.rs @@ -7,16 +7,15 @@ // spell-checker:ignore DATETIME getmntinfo subsecond (fs) cifs smbfs +#[cfg(windows)] +mod windows; + #[cfg(any(target_os = "linux", target_os = "android", target_os = "cygwin"))] const LINUX_MTAB: &str = "/etc/mtab"; #[cfg(any(target_os = "linux", target_os = "android", target_os = "cygwin"))] 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( target_vendor = "apple", @@ -25,39 +24,11 @@ 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(unix)] 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}, - Storage::FileSystem::{ - FindFirstVolumeW, FindNextVolumeW, FindVolumeClose, GetDiskFreeSpaceW, GetDriveTypeW, - GetVolumeInformationW, GetVolumePathNamesForVolumeNameW, QueryDosDeviceW, - }, - 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 core::ffi::CStr; @@ -67,12 +38,10 @@ use libc::{ }; #[cfg(unix)] use std::ffi::CString; -#[cfg(not(target_os = "wasi"))] +#[cfg(unix)] use std::io::Error as IOError; #[cfg(unix)] use std::mem; -#[cfg(windows)] -use std::path::Path; use std::time::SystemTime; #[cfg(unix)] use std::time::UNIX_EPOCH; @@ -187,7 +156,7 @@ pub fn metadata_get_time(md: &Metadata, md_time: MetadataTimeField) -> Option 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` - 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 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(), - mount_option: String::new(), - remote, - dummy: false, - }) - } } #[cfg(any( @@ -431,8 +327,7 @@ use crate::error::UResult; target_vendor = "apple", target_os = "freebsd", target_os = "netbsd", - target_os = "openbsd", - windows + target_os = "openbsd" ))] use crate::error::USimpleError; #[cfg(any(target_os = "linux", target_os = "android", target_os = "cygwin"))] @@ -443,8 +338,7 @@ use std::io::{BufRead, BufReader}; target_vendor = "apple", target_os = "freebsd", target_os = "netbsd", - target_os = "openbsd", - windows + target_os = "openbsd" ))] use std::ptr; #[cfg(any( @@ -493,44 +387,7 @@ pub fn read_fs_list() -> UResult> { } #[cfg(windows)] { - let mut volume_name_buf = [0u16; MAX_PATH]; - // 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) }; - if INVALID_HANDLE_VALUE == find_handle { - let os_err = IOError::last_os_error(); - let msg = format!("FindFirstVolumeW failed: {os_err}"); - return Err(USimpleError::new(EXIT_ERR, msg)); - } - 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) { - mounts.push(m); - } - if 0 == unsafe { - FindNextVolumeW( - find_handle, - volume_name_buf.as_mut_ptr(), - volume_name_buf.len() as u32, - ) - } { - let err = IOError::last_os_error(); - if err.raw_os_error() != Some(ERROR_NO_MORE_FILES as i32) { - let msg = format!("FindNextVolumeW failed: {err}"); - return Err(USimpleError::new(EXIT_ERR, msg)); - } - break; - } - } - unsafe { - FindVolumeClose(find_handle); - } - Ok(mounts) + windows::read_fs_list() } #[cfg(any( target_os = "aix", @@ -615,61 +472,6 @@ 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, - ); - } - - 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, - // Total number of free blocks. - bfree: number_of_free_clusters 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, - // Total number of file nodes (inodes) on the file system. - files: 0, // Not available on windows - // Total number of free file nodes (inodes). - ffree: 0, // Meaningless on Windows - }) - } } #[cfg(unix)] diff --git a/src/uucore/src/lib/features/fsext/windows.rs b/src/uucore/src/lib/features/fsext/windows.rs new file mode 100644 index 00000000000..dd3eb15b077 --- /dev/null +++ b/src/uucore/src/lib/features/fsext/windows.rs @@ -0,0 +1,316 @@ +// 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. + +// spell-checker:ignore WNet FAILCRITICALERRORS SUBST + +//! Windows backend of `fsext`: volume enumeration and usage probes. + +use std::ffi::OsString; +use std::io; +use std::path::Path; + +use super::{FsUsage, MountInfo}; +use crate::error::UResult; + +/// Every mount path of every volume, then the drive letters that are not +/// volume mount points (mapped network drives, SUBST drives). +pub(super) fn read_fs_list() -> UResult> { + let _quiet = sys::ErrorMode::fail_critical_errors(); + let mut mounts = Vec::new(); + for volume in sys::volumes()? { + for mount_dir in sys::volume_mount_paths(&volume).unwrap_or_default() { + mounts.push(MountInfo::from_mount_dir(mount_dir)); + } + } + for drive in sys::logical_drives() { + if !mounts.iter().any(|m| m.mount_dir == drive) { + mounts.push(MountInfo::from_mount_dir(drive)); + } + } + Ok(mounts) +} + +impl MountInfo { + /// The filesystem mounted at `mount_dir` (`C:\`, `C:\mount\`, `\\server\share\`). + pub fn from_mount_dir(mount_dir: OsString) -> Self { + let remote = sys::is_remote_drive(&mount_dir); + let dev_name = remote + .then(|| sys::remote_name(&mount_dir)) + .flatten() + .unwrap_or_else(|| mount_dir.to_string_lossy().into_owned()); + let (dev_id, fs_type) = match sys::volume_information(&mount_dir) { + Ok(info) => (info.serial.to_string(), info.fs_type), + Err(_) => (dev_name.clone(), String::new()), + }; + Self { + dev_id, + dev_name, + fs_type, + mount_root: OsString::new(), + mount_dir, + mount_option: String::new(), + remote, + dummy: false, + } + } +} + +impl FsUsage { + /// Usage of the volume mounted at `root`; Windows reports no inode counts. + pub fn new(root: &Path) -> io::Result { + let _quiet = sys::ErrorMode::fail_critical_errors(); + let root = root.as_os_str(); + let space = sys::disk_space(root)?; + let blocksize = sys::cluster_size(root).unwrap_or(1).max(1); + Ok(Self { + blocksize, + blocks: space.total / blocksize, + bfree: space.free / blocksize, + bavail: space.available / blocksize, + bavail_top_bit_set: false, + files: 0, + ffree: 0, + }) + } +} + +/// Safe wrappers around the Win32 calls; every `unsafe` lives here. +mod sys { + use std::ffi::{OsStr, OsString}; + use std::io; + use std::os::windows::ffi::OsStringExt; + use std::ptr; + + use crate::wide::{FromWide, ToWide}; + use windows_sys::Win32::Foundation::{ + ERROR_MORE_DATA, ERROR_NO_MORE_FILES, HANDLE, INVALID_HANDLE_VALUE, MAX_PATH, NO_ERROR, + }; + use windows_sys::Win32::NetworkManagement::WNet::WNetGetConnectionW; + use windows_sys::Win32::Storage::FileSystem::{ + FindFirstVolumeW, FindNextVolumeW, FindVolumeClose, GetDiskFreeSpaceExW, GetDiskFreeSpaceW, + GetDriveTypeW, GetLogicalDrives, GetVolumeInformationW, GetVolumePathNamesForVolumeNameW, + }; + use windows_sys::Win32::System::Diagnostics::Debug::{ + SEM_FAILCRITICALERRORS, SetThreadErrorMode, + }; + use windows_sys::Win32::System::WindowsProgramming::DRIVE_REMOTE; + use windows_sys::core::BOOL; + + const BUF_LEN: usize = MAX_PATH as usize + 1; + + fn cvt(result: BOOL) -> io::Result<()> { + if result == 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } + } + + /// Keeps the "no disk in drive" dialog away while probing drives without + /// media; the previous mode is restored on drop. + pub struct ErrorMode(u32); + + impl ErrorMode { + pub fn fail_critical_errors() -> Self { + let mut previous = 0; + // SAFETY: `previous` is a valid out-pointer. + unsafe { SetThreadErrorMode(SEM_FAILCRITICALERRORS, &raw mut previous) }; + Self(previous) + } + } + + impl Drop for ErrorMode { + fn drop(&mut self) { + // SAFETY: a null out-pointer is allowed. + unsafe { SetThreadErrorMode(self.0, ptr::null_mut()) }; + } + } + + struct FindVolume(HANDLE); + + impl Drop for FindVolume { + fn drop(&mut self) { + // SAFETY: the handle came from `FindFirstVolumeW` and is closed once. + unsafe { FindVolumeClose(self.0) }; + } + } + + /// The `\\?\Volume{...}\` name of every volume. + pub fn volumes() -> io::Result> { + let mut name = [0u16; BUF_LEN]; + // SAFETY: `name` is a valid buffer of `name.len()` units. + let handle = unsafe { FindFirstVolumeW(name.as_mut_ptr(), name.len() as u32) }; + if handle == INVALID_HANDLE_VALUE { + return Err(io::Error::last_os_error()); + } + let handle = FindVolume(handle); + let mut volumes = vec![String::from_wide_null(&name)]; + loop { + // SAFETY: `handle` is open; `name` is a valid buffer of `name.len()` units. + if unsafe { FindNextVolumeW(handle.0, name.as_mut_ptr(), name.len() as u32) } == 0 { + let err = io::Error::last_os_error(); + return if err.raw_os_error() == Some(ERROR_NO_MORE_FILES as i32) { + Ok(volumes) + } else { + Err(err) + }; + } + volumes.push(String::from_wide_null(&name)); + } + } + + /// The drive letters and mounted folders `volume` is reachable at, each + /// with a trailing `\`. + pub fn volume_mount_paths(volume: &str) -> io::Result> { + let volume = volume.to_wide_null(); + let mut paths = vec![0u16; BUF_LEN]; + loop { + let mut len = 0; + // SAFETY: `volume` is NUL-terminated; `paths` is a valid buffer of + // `paths.len()` units and `len` a valid out-pointer. + let ok = unsafe { + GetVolumePathNamesForVolumeNameW( + volume.as_ptr(), + paths.as_mut_ptr(), + paths.len() as u32, + &raw mut len, + ) + }; + if ok != 0 { + return Ok(paths[..len as usize] + .split(|&c| c == 0) + .filter(|p| !p.is_empty()) + .map(OsString::from_wide) + .collect()); + } + let err = io::Error::last_os_error(); + if err.raw_os_error() != Some(ERROR_MORE_DATA as i32) { + return Err(err); + } + paths.resize(len as usize, 0); + } + } + + /// The root of every drive letter in use, `A:\` to `Z:\`. + pub fn logical_drives() -> impl Iterator { + // SAFETY: no preconditions. + let mask = unsafe { GetLogicalDrives() }; + (b'A'..=b'Z') + .filter(move |letter| mask & (1 << (letter - b'A')) != 0) + .map(|letter| OsString::from(format!("{}:\\", letter as char))) + } + + pub struct VolumeInformation { + pub fs_type: String, + pub serial: u32, + } + + pub fn volume_information(root: &OsStr) -> io::Result { + let root = root.to_wide_null(); + let mut serial = 0; + let mut fs_type = [0u16; BUF_LEN]; + // SAFETY: `root` is NUL-terminated; `serial` and `fs_type` are valid + // out-buffers and the remaining out-pointers may be null. + cvt(unsafe { + GetVolumeInformationW( + root.as_ptr(), + ptr::null_mut(), + 0, + &raw mut serial, + ptr::null_mut(), + ptr::null_mut(), + fs_type.as_mut_ptr(), + fs_type.len() as u32, + ) + })?; + Ok(VolumeInformation { + fs_type: String::from_wide_null(&fs_type), + serial, + }) + } + + pub fn is_remote_drive(root: &OsStr) -> bool { + let root = root.to_wide_null(); + // SAFETY: `root` is NUL-terminated. + unsafe { GetDriveTypeW(root.as_ptr()) == DRIVE_REMOTE } + } + + /// The UNC name a drive letter is mapped to, `\\server\share`. + pub fn remote_name(root: &OsStr) -> Option { + // `X:` only; the API rejects the trailing separator. + let local: Vec = root.to_wide().into_iter().take(2).chain([0]).collect(); + let mut remote = [0u16; BUF_LEN]; + let mut len = remote.len() as u32; + // SAFETY: `local` is NUL-terminated; `remote` is a valid buffer of `len` units. + let status = + unsafe { WNetGetConnectionW(local.as_ptr(), remote.as_mut_ptr(), &raw mut len) }; + (status == NO_ERROR).then(|| String::from_wide_null(&remote)) + } + + pub struct DiskSpace { + pub total: u64, + pub free: u64, + pub available: u64, + } + + /// Byte counts of the volume at `root`; `available` honours quotas. + pub fn disk_space(root: &OsStr) -> io::Result { + let root = root.to_wide_null(); + let mut space = DiskSpace { + total: 0, + free: 0, + available: 0, + }; + // SAFETY: `root` is NUL-terminated; the three out-pointers are valid. + cvt(unsafe { + GetDiskFreeSpaceExW( + root.as_ptr(), + &raw mut space.available, + &raw mut space.total, + &raw mut space.free, + ) + })?; + Ok(space) + } + + /// Bytes per allocation unit of the volume at `root`. + pub fn cluster_size(root: &OsStr) -> io::Result { + let root = root.to_wide_null(); + let mut sectors_per_cluster = 0; + let mut bytes_per_sector = 0; + // SAFETY: `root` is NUL-terminated; the two out-pointers are valid and + // the remaining ones may be null. + cvt(unsafe { + GetDiskFreeSpaceW( + root.as_ptr(), + &raw mut sectors_per_cluster, + &raw mut bytes_per_sector, + ptr::null_mut(), + ptr::null_mut(), + ) + })?; + Ok(u64::from(sectors_per_cluster) * u64::from(bytes_per_sector)) + } +} + +#[cfg(test)] +mod tests { + use super::read_fs_list; + use std::ffi::OsString; + + #[test] + fn test_read_fs_list_has_system_drive() { + let system_drive = OsString::from(std::env::var("SystemDrive").unwrap() + "\\"); + let mounts = read_fs_list().unwrap(); + assert!( + mounts + .iter() + .all(|m| m.mount_dir.to_string_lossy().ends_with('\\')) + ); + let system = mounts.iter().find(|m| m.mount_dir == system_drive).unwrap(); + assert!(!system.fs_type.is_empty()); + assert_eq!(system.dev_name, system_drive.to_string_lossy()); + } +}