blob: 440743b3de39df7a4ac1e0ea3a21ab8b83a19609 [file] [log] [blame]
// Take a look at the license at the top of the repository in the LICENSE file.
use crate::{Disk, DiskKind};
use std::ffi::{c_void, OsStr, OsString};
use std::mem::size_of;
use std::os::windows::ffi::OsStringExt;
use std::path::Path;
use windows::core::{Error, HRESULT, PCWSTR};
use windows::Win32::Foundation::{CloseHandle, HANDLE, MAX_PATH};
use windows::Win32::Storage::FileSystem::{
CreateFileW, FindFirstVolumeW, FindNextVolumeW, FindVolumeClose, GetDiskFreeSpaceExW,
GetDriveTypeW, GetVolumeInformationW, GetVolumePathNamesForVolumeNameW, FILE_ACCESS_RIGHTS,
FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
};
use windows::Win32::System::Ioctl::{
PropertyStandardQuery, StorageDeviceSeekPenaltyProperty, DEVICE_SEEK_PENALTY_DESCRIPTOR,
IOCTL_STORAGE_QUERY_PROPERTY, STORAGE_PROPERTY_QUERY,
};
use windows::Win32::System::WindowsProgramming::{DRIVE_FIXED, DRIVE_REMOVABLE};
use windows::Win32::System::IO::DeviceIoControl;
/// Creates a copy of the first zero-terminated wide string in `buf`.
/// The copy includes the zero terminator.
fn from_zero_terminated(buf: &[u16]) -> Vec<u16> {
let end = buf.iter().position(|&x| x == 0).unwrap_or(buf.len());
buf[..=end].to_vec()
}
// Realistically, volume names are probably not longer than 44 characters,
// but the example in the Microsoft documentation uses MAX_PATH as well.
// https://learn.microsoft.com/en-us/windows/win32/fileio/displaying-volume-paths
const VOLUME_NAME_SIZE: usize = MAX_PATH as usize + 1;
const ERROR_NO_MORE_FILES: HRESULT = windows::Win32::Foundation::ERROR_NO_MORE_FILES.to_hresult();
const ERROR_MORE_DATA: HRESULT = windows::Win32::Foundation::ERROR_MORE_DATA.to_hresult();
/// Returns a list of zero-terminated wide strings containing volume GUID paths.
/// Volume GUID paths have the form `\\?\{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}\`.
///
/// Rather confusingly, the Win32 API _also_ calls these "volume names".
pub(crate) fn get_volume_guid_paths() -> Vec<Vec<u16>> {
let mut volume_names = Vec::new();
unsafe {
let mut buf = Box::new([0u16; VOLUME_NAME_SIZE]);
let Ok(handle) = FindFirstVolumeW(&mut buf[..]) else {
sysinfo_debug!(
"Error: FindFirstVolumeW() = {:?}",
Error::from_win32().code()
);
return Vec::new();
};
volume_names.push(from_zero_terminated(&buf[..]));
loop {
if FindNextVolumeW(handle, &mut buf[..]).is_err() {
if Error::from_win32().code() != ERROR_NO_MORE_FILES {
sysinfo_debug!("Error: FindNextVolumeW = {}", Error::from_win32().code());
}
break;
}
volume_names.push(from_zero_terminated(&buf[..]));
}
if FindVolumeClose(handle).is_err() {
sysinfo_debug!("Error: FindVolumeClose = {:?}", Error::from_win32().code());
};
}
volume_names
}
/// Given a volume GUID path (`\\?\{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}\`), returns all
/// volume paths (drive letters and mount paths) associated with it
/// as zero terminated wide strings.
///
/// # Safety
/// `volume_name` must contain a zero-terminated wide string.
pub(crate) unsafe fn get_volume_path_names_for_volume_name(
volume_guid_path: &[u16],
) -> Vec<Vec<u16>> {
let volume_guid_path = PCWSTR::from_raw(volume_guid_path.as_ptr());
// Initial buffer size is just a guess. There is no direct connection between MAX_PATH
// the output of GetVolumePathNamesForVolumeNameW.
let mut path_names_buf = vec![0u16; MAX_PATH as usize];
let mut path_names_output_size = 0u32;
for _ in 0..10 {
let volume_path_names = GetVolumePathNamesForVolumeNameW(
volume_guid_path,
Some(path_names_buf.as_mut_slice()),
&mut path_names_output_size,
);
let code = volume_path_names.map_err(|_| Error::from_win32().code());
match code {
Ok(()) => break,
Err(ERROR_MORE_DATA) => {
// We need a bigger buffer. path_names_output_size contains the required buffer size.
path_names_buf = vec![0u16; path_names_output_size as usize];
continue;
}
Err(_e) => {
sysinfo_debug!("Error: GetVolumePathNamesForVolumeNameW() = {}", _e);
return Vec::new();
}
}
}
// path_names_buf contains multiple zero terminated wide strings.
// An additional zero terminates the list.
let mut path_names = Vec::new();
let mut buf = &path_names_buf[..];
while !buf.is_empty() && buf[0] != 0 {
let path = from_zero_terminated(buf);
buf = &buf[path.len()..];
path_names.push(path);
}
path_names
}
pub(crate) struct DiskInner {
type_: DiskKind,
name: OsString,
file_system: OsString,
mount_point: Vec<u16>,
s_mount_point: OsString,
total_space: u64,
available_space: u64,
is_removable: bool,
}
impl DiskInner {
pub(crate) fn kind(&self) -> DiskKind {
self.type_
}
pub(crate) fn name(&self) -> &OsStr {
&self.name
}
pub(crate) fn file_system(&self) -> &OsStr {
&self.file_system
}
pub(crate) fn mount_point(&self) -> &Path {
self.s_mount_point.as_ref()
}
pub(crate) fn total_space(&self) -> u64 {
self.total_space
}
pub(crate) fn available_space(&self) -> u64 {
self.available_space
}
pub(crate) fn is_removable(&self) -> bool {
self.is_removable
}
pub(crate) fn refresh(&mut self) -> bool {
if self.total_space != 0 {
unsafe {
let mut tmp = 0;
let lpdirectoryname = PCWSTR::from_raw(self.mount_point.as_ptr());
if GetDiskFreeSpaceExW(lpdirectoryname, None, None, Some(&mut tmp)).is_ok() {
self.available_space = tmp;
return true;
}
}
}
false
}
}
pub(crate) struct DisksInner {
pub(crate) disks: Vec<Disk>,
}
impl DisksInner {
pub(crate) fn new() -> Self {
Self {
disks: Vec::with_capacity(2),
}
}
pub(crate) fn from_vec(disks: Vec<Disk>) -> Self {
Self { disks }
}
pub(crate) fn into_vec(self) -> Vec<Disk> {
self.disks
}
pub(crate) fn refresh_list(&mut self) {
unsafe {
self.disks = get_list();
}
}
pub(crate) fn list(&self) -> &[Disk] {
&self.disks
}
pub(crate) fn list_mut(&mut self) -> &mut [Disk] {
&mut self.disks
}
}
struct HandleWrapper(HANDLE);
impl HandleWrapper {
unsafe fn new(drive_name: &[u16], open_rights: FILE_ACCESS_RIGHTS) -> Option<Self> {
let lpfilename = PCWSTR::from_raw(drive_name.as_ptr());
let handle = CreateFileW(
lpfilename,
open_rights.0,
FILE_SHARE_READ | FILE_SHARE_WRITE,
None,
OPEN_EXISTING,
Default::default(),
HANDLE::default(),
)
.ok()?;
Some(Self(handle))
}
}
impl Drop for HandleWrapper {
fn drop(&mut self) {
let _err = unsafe { CloseHandle(self.0) };
}
}
unsafe fn get_drive_size(mount_point: &[u16]) -> Option<(u64, u64)> {
let mut total_size = 0;
let mut available_space = 0;
let lpdirectoryname = PCWSTR::from_raw(mount_point.as_ptr());
if GetDiskFreeSpaceExW(
lpdirectoryname,
None,
Some(&mut total_size),
Some(&mut available_space),
)
.is_ok()
{
Some((total_size, available_space))
} else {
None
}
}
pub(crate) unsafe fn get_list() -> Vec<Disk> {
#[cfg(feature = "multithread")]
use rayon::iter::ParallelIterator;
crate::utils::into_iter(get_volume_guid_paths())
.flat_map(|volume_name| {
let raw_volume_name = PCWSTR::from_raw(volume_name.as_ptr());
let drive_type = GetDriveTypeW(raw_volume_name);
let is_removable = drive_type == DRIVE_REMOVABLE;
if drive_type != DRIVE_FIXED && drive_type != DRIVE_REMOVABLE {
return Vec::new();
}
let mut name = [0u16; MAX_PATH as usize + 1];
let mut file_system = [0u16; 32];
let volume_info_res = GetVolumeInformationW(
raw_volume_name,
Some(&mut name),
None,
None,
None,
Some(&mut file_system),
)
.is_ok();
if !volume_info_res {
sysinfo_debug!(
"Error: GetVolumeInformationW = {:?}",
Error::from_win32().code()
);
return Vec::new();
}
let mount_paths = get_volume_path_names_for_volume_name(&volume_name[..]);
if mount_paths.is_empty() {
return Vec::new();
}
// The device path is the volume name without the trailing backslash.
let device_path = volume_name[..(volume_name.len() - 2)]
.iter()
.copied()
.chain([0])
.collect::<Vec<_>>();
let Some(handle) = HandleWrapper::new(&device_path[..], Default::default()) else {
return Vec::new();
};
let Some((total_space, available_space)) = get_drive_size(&mount_paths[0][..]) else {
return Vec::new();
};
if total_space == 0 {
sysinfo_debug!("total_space == 0");
return Vec::new();
}
let spq_trim = STORAGE_PROPERTY_QUERY {
PropertyId: StorageDeviceSeekPenaltyProperty,
QueryType: PropertyStandardQuery,
AdditionalParameters: [0],
};
let mut result: DEVICE_SEEK_PENALTY_DESCRIPTOR = std::mem::zeroed();
let mut dw_size = 0;
let device_io_control = DeviceIoControl(
handle.0,
IOCTL_STORAGE_QUERY_PROPERTY,
Some(&spq_trim as *const STORAGE_PROPERTY_QUERY as *const c_void),
size_of::<STORAGE_PROPERTY_QUERY>() as u32,
Some(&mut result as *mut DEVICE_SEEK_PENALTY_DESCRIPTOR as *mut c_void),
size_of::<DEVICE_SEEK_PENALTY_DESCRIPTOR>() as u32,
Some(&mut dw_size),
None,
)
.is_ok();
let type_ = if !device_io_control
|| dw_size != size_of::<DEVICE_SEEK_PENALTY_DESCRIPTOR>() as u32
{
DiskKind::Unknown(-1)
} else {
let is_hdd = result.IncursSeekPenalty.as_bool();
if is_hdd {
DiskKind::HDD
} else {
DiskKind::SSD
}
};
let name = os_string_from_zero_terminated(&name);
let file_system = os_string_from_zero_terminated(&file_system);
mount_paths
.into_iter()
.map(move |mount_path| Disk {
inner: DiskInner {
type_,
name: name.clone(),
file_system: file_system.clone(),
s_mount_point: OsString::from_wide(&mount_path[..mount_path.len() - 1]),
mount_point: mount_path,
total_space,
available_space,
is_removable,
},
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
}
fn os_string_from_zero_terminated(name: &[u16]) -> OsString {
let len = name.iter().position(|&x| x == 0).unwrap_or(name.len());
OsString::from_wide(&name[..len])
}