From f672f663670ec4f16eed9b572ad1a8e87307918e Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Fri, 30 Sep 2022 15:26:38 -0500 Subject: [PATCH 1/7] Use a more reliable implementation for checking disk type on macOS --- src/apple/macos/component/x86.rs | 12 +-- src/apple/macos/disk.rs | 123 +++++++++++++++++++++++++--- src/apple/macos/ffi.rs | 134 +++++++++++++++++++------------ src/apple/macos/utils.rs | 24 ++++++ 4 files changed, 225 insertions(+), 68 deletions(-) diff --git a/src/apple/macos/component/x86.rs b/src/apple/macos/component/x86.rs index 94eee1f88..858ae619e 100644 --- a/src/apple/macos/component/x86.rs +++ b/src/apple/macos/component/x86.rs @@ -3,7 +3,7 @@ use crate::sys::ffi; use crate::ComponentExt; -use libc::{c_char, c_int, c_void, mach_port_t}; +use libc::{c_char, c_int, c_void}; use std::mem; @@ -296,15 +296,15 @@ impl IoService { // code from https://github.com/Chris911/iStats // Not supported on iOS, or in the default macOS pub(crate) fn new_connection() -> Option { - let mut master_port: mach_port_t = 0; let mut iterator: ffi::io_iterator_t = 0; unsafe { - ffi::IOMasterPort(libc::MACH_PORT_NULL, &mut master_port); - let matching_dictionary = ffi::IOServiceMatching(b"AppleSMC\0".as_ptr() as *const i8); - let result = - ffi::IOServiceGetMatchingServices(master_port, matching_dictionary, &mut iterator); + let result = ffi::IOServiceGetMatchingServices( + ffi::kIOMasterPortDefault, + matching_dictionary, + &mut iterator, + ); if result != ffi::KIO_RETURN_SUCCESS { sysinfo_debug!("Error: IOServiceGetMatchingServices() = {}", result); return None; diff --git a/src/apple/macos/disk.rs b/src/apple/macos/disk.rs index 28e49e632..5625158a1 100644 --- a/src/apple/macos/disk.rs +++ b/src/apple/macos/disk.rs @@ -1,6 +1,6 @@ // Take a look at the license at the top of the repository in the LICENSE file. -use crate::sys::macos::utils::CFReleaser; +use crate::sys::macos::utils::{CFReleaser, IOReleaser}; use crate::sys::{ffi, utils}; use crate::utils::to_cpath; use crate::{Disk, DiskType}; @@ -71,17 +71,8 @@ pub(crate) fn get_disks(session: ffi::DASessionRef) -> Vec { get_bool_value(dict.inner(), b"DAMediaRemovable\0").unwrap_or(false); let ejectable = get_bool_value(dict.inner(), b"DAMediaEjectable\0").unwrap_or(false); - // This is very hackish but still better than nothing... - let type_ = if let Some(model) = get_str_value(dict.inner(), b"DADeviceModel\0") { - if model.contains("SSD") { - DiskType::SSD - } else { - // We just assume by default that this is a HDD - DiskType::HDD - } - } else { - DiskType::Unknown(-1) - }; + + let type_ = get_disk_type(&c_disk); new_disk(name, mount_point, type_, removable || ejectable) }) @@ -89,6 +80,114 @@ pub(crate) fn get_disks(session: ffi::DASessionRef) -> Vec { } } +const UNKNOWN_DISK_TYPE: DiskType = DiskType::Unknown(-1); + +fn get_disk_type(disk: &statfs) -> DiskType { + let characteristics_string = CFReleaser::new(unsafe { + cfs::CFStringCreateWithBytesNoCopy( + kCFAllocatorDefault, + ffi::kIOPropertyDeviceCharacteristicsKey.as_ptr(), + ffi::kIOPropertyDeviceCharacteristicsKey.len() as _, + cfs::kCFStringEncodingUTF8, + false as _, + kCFAllocatorNull, + ) + }) + .unwrap(); + + // Removes `/dev/` from the value. + let bsd_name = { + let full = + std::str::from_utf8(unsafe { &*(&disk.f_mntfromname as *const [i8] as *const [u8]) }) + .unwrap(); + full.strip_prefix("/dev/") + .expect("device mount point in unknown format") + }; + + let matching = + unsafe { ffi::IOBSDNameMatching(ffi::kIOMasterPortDefault, 0, bsd_name.as_ptr().cast()) }; + + if matching.is_null() { + return UNKNOWN_DISK_TYPE; + } + + let mut service_iterator: ffi::io_iterator_t = 0; + + if unsafe { + ffi::IOServiceGetMatchingServices( + ffi::kIOMasterPortDefault, + matching.cast(), + &mut service_iterator, + ) + } != libc::KERN_SUCCESS + { + return UNKNOWN_DISK_TYPE; + } + + let service_iterator = IOReleaser::new(service_iterator).unwrap(); + + let mut parent_entry: ffi::io_registry_entry_t = 0; + + loop { + let mut current_service_entry = + match IOReleaser::new(unsafe { ffi::IOIteratorNext(service_iterator.inner()) }) { + Some(entry) => entry, + None => break, // The iterator is empty + }; + + loop { + let result = unsafe { + ffi::IORegistryEntryGetParentEntry( + current_service_entry.inner(), + ffi::kIOServicePlane.as_ptr().cast(), + &mut parent_entry, + ) + }; + if result != libc::KERN_SUCCESS { + break; + } + + current_service_entry = IOReleaser::new(parent_entry).unwrap(); + + // There were no more parents left. + if parent_entry == 0 { + break; + } + + let properties_result = unsafe { + CFReleaser::new(ffi::IORegistryEntryCreateCFProperty( + current_service_entry.inner(), + characteristics_string.inner(), + kCFAllocatorDefault, + 0, + )) + }; + + if let Some(device_properties) = properties_result { + let disk_type = unsafe { + get_str_value(device_properties.inner(), ffi::kIOPropertyMediumTypeKey) + }; + + if let Some(disk_type) = disk_type.and_then(|medium| match medium.as_str() { + _ if medium == ffi::kIOPropertyMediumTypeSolidStateKey => Some(DiskType::SSD), + _ if medium == ffi::kIOPropertyMediumTypeRotationalKey => Some(DiskType::HDD), + _ => None, + }) { + return disk_type; + } else { + // Many external drive vendors do not advertise their device's storage medium. + // + // In these cases, assuming that there were _any_ properties about them registered, we fallback + // to `HDD` when no storage medium is provided by the device instead of `Unknown`. + return DiskType::HDD; + } + } + } + } + + UNKNOWN_DISK_TYPE +} + unsafe fn get_dict_value Option>( dict: CFDictionaryRef, key: &[u8], diff --git a/src/apple/macos/ffi.rs b/src/apple/macos/ffi.rs index c20ca4863..499059772 100644 --- a/src/apple/macos/ffi.rs +++ b/src/apple/macos/ffi.rs @@ -1,63 +1,70 @@ // Take a look at the license at the top of the repository in the LICENSE file. -use core_foundation_sys::base::{CFAllocatorRef, CFRelease}; -use core_foundation_sys::dictionary::CFMutableDictionaryRef; +use core_foundation_sys::base::{mach_port_t, CFAllocatorRef, CFRelease}; +use core_foundation_sys::dictionary::{CFDictionaryRef, CFMutableDictionaryRef}; use core_foundation_sys::string::{CFStringEncoding, CFStringRef}; -use libc::{c_char, c_void}; +use libc::{c_char, c_void, kern_return_t}; -#[cfg(all( - not(feature = "apple-sandbox"), - any(target_arch = "x86", target_arch = "x86_64") -))] -use libc::{mach_port_t, size_t}; +// Note: IOKit is only available on MacOS up until very recent iOS versions: https://developer.apple.com/documentation/iokit pub(crate) use crate::sys::ffi::*; -#[cfg(all( - not(feature = "apple-sandbox"), - any(target_arch = "x86", target_arch = "x86_64") -))] +#[allow(non_camel_case_types)] +pub type io_object_t = mach_port_t; + +#[allow(non_camel_case_types)] +pub type io_iterator_t = io_object_t; +#[allow(non_camel_case_types)] +pub type io_registry_entry_t = io_object_t; +#[allow(non_camel_case_types)] +pub type io_name_t = *const c_char; + +pub type IOOptionBits = u32; + +#[allow(non_upper_case_globals)] +pub const kIOServicePlane: &str = "IOService\0"; +#[allow(non_upper_case_globals)] +pub const kIOPropertyDeviceCharacteristicsKey: &str = "Device Characteristics"; +#[allow(non_upper_case_globals)] +pub const kIOPropertyMediumTypeKey: &[u8] = b"Medium Type\0"; +#[allow(non_upper_case_globals)] +pub const kIOPropertyMediumTypeSolidStateKey: &str = "Solid State"; +#[allow(non_upper_case_globals)] +pub const kIOPropertyMediumTypeRotationalKey: &str = "Rotational"; + +// Note: Obtaining information about disks using IOKIt is allowed inside the default macOS App Sandbox. extern "C" { - // The proc_* PID functions are internal Apple APIs which are not - // allowed in App store releases as Apple blocks any binary using them. - - // IOKit is only available on MacOS: https://developer.apple.com/documentation/iokit, and when not running inside - // of the default macOS sandbox. - pub fn IOMasterPort(a: i32, b: *mut mach_port_t) -> i32; - - pub fn IOServiceMatching(a: *const c_char) -> *mut c_void; - pub fn IOServiceGetMatchingServices( - a: mach_port_t, - b: *mut c_void, - c: *mut io_iterator_t, - ) -> i32; + mainPort: mach_port_t, + matching: CFMutableDictionaryRef, + existing: *mut io_iterator_t, + ) -> kern_return_t; pub fn IOIteratorNext(iterator: io_iterator_t) -> io_object_t; - pub fn IOObjectRelease(obj: io_object_t) -> i32; - - pub fn IOServiceOpen(device: io_object_t, a: u32, t: u32, x: *mut io_connect_t) -> i32; + pub fn IOObjectRelease(obj: io_object_t) -> kern_return_t; - pub fn IOServiceClose(a: io_connect_t) -> i32; - - #[allow(dead_code)] - pub fn IOConnectCallStructMethod( - connection: mach_port_t, - selector: u32, - inputStruct: *const KeyData_t, - inputStructCnt: size_t, - outputStruct: *mut KeyData_t, - outputStructCnt: *mut size_t, - ) -> i32; - // pub fn IORegistryEntryCreateCFProperties( - // entry: io_registry_entry_t, - // properties: *mut CFMutableDictionaryRef, - // allocator: CFAllocatorRef, - // options: IOOptionBits, - // ) -> kern_return_t; - // pub fn IORegistryEntryGetName(entry: io_registry_entry_t, name: *mut c_char) -> kern_return_t; + pub fn IORegistryEntryCreateCFProperty( + entry: io_registry_entry_t, + key: CFStringRef, + allocator: CFAllocatorRef, + options: IOOptionBits, + ) -> CFDictionaryRef; + pub fn IORegistryEntryGetParentEntry( + entry: io_registry_entry_t, + plane: io_name_t, + parent: *mut io_registry_entry_t, + ) -> kern_return_t; + + pub fn IOBSDNameMatching( + mainPort: mach_port_t, + options: u32, + bsdName: *const c_char, + ) -> CFMutableDictionaryRef; + + // This is deprecated as of macOS 12.0, but Rust doesn't have a good way to only use the replacement on 12+. + pub static kIOMasterPortDefault: mach_port_t; } extern "C" { @@ -113,14 +120,41 @@ unsafe impl Sync for SessionWrap {} any(target_arch = "x86", target_arch = "x86_64") ))] mod io_service { - use super::mach_port_t; + use super::{io_object_t, mach_port_t}; + use core_foundation_sys::dictionary::CFMutableDictionaryRef; + use libc::{c_char, kern_return_t, size_t, task_t}; - #[allow(non_camel_case_types)] - pub type io_object_t = mach_port_t; #[allow(non_camel_case_types)] pub type io_connect_t = io_object_t; + #[allow(non_camel_case_types)] - pub type io_iterator_t = io_object_t; + pub type io_service_t = io_object_t; + + #[allow(non_camel_case_types)] + pub type task_port_t = task_t; + + extern "C" { + pub fn IOServiceMatching(a: *const c_char) -> CFMutableDictionaryRef; + + pub fn IOServiceOpen( + device: io_service_t, + owning_task: task_port_t, + type_: u32, + connect: *mut io_connect_t, + ) -> kern_return_t; + + pub fn IOServiceClose(a: io_connect_t) -> kern_return_t; + + #[allow(dead_code)] + pub fn IOConnectCallStructMethod( + connection: mach_port_t, + selector: u32, + inputStruct: *const KeyData_t, + inputStructCnt: size_t, + outputStruct: *mut KeyData_t, + outputStructCnt: *mut size_t, + ) -> kern_return_t; + } #[cfg_attr(feature = "debug", derive(Debug, Eq, Hash, PartialEq))] #[repr(C)] diff --git a/src/apple/macos/utils.rs b/src/apple/macos/utils.rs index 5db8d5345..e4e7ddb42 100644 --- a/src/apple/macos/utils.rs +++ b/src/apple/macos/utils.rs @@ -32,3 +32,27 @@ impl Drop for CFReleaser { unsafe impl Send for CFReleaser {} unsafe impl Sync for CFReleaser {} + +pub(crate) struct IOReleaser(super::ffi::io_object_t); + +impl IOReleaser { + pub(crate) fn new(obj: u32) -> Option { + if obj == 0 { + None + } else { + Some(Self(obj)) + } + } + + pub(crate) fn inner(&self) -> u32 { + self.0 + } +} + +impl Drop for IOReleaser { + fn drop(&mut self) { + if self.0 != 0 { + unsafe { super::ffi::IOObjectRelease(self.0 as _) }; + } + } +} From c445b0d6ef0c259283a125fa7daec7970ea8415f Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Sun, 2 Oct 2022 19:39:56 -0500 Subject: [PATCH 2/7] Replace DiskArbitration.framework with CoreFoundation for obtaining disk info on macOS --- build.rs | 2 - src/apple/ffi.rs | 14 -- src/apple/macos/component/x86.rs | 31 +-- src/apple/macos/disk.rs | 353 ++++++++++++++++++++----------- src/apple/macos/ffi.rs | 69 ++---- src/apple/system.rs | 21 +- 6 files changed, 247 insertions(+), 243 deletions(-) diff --git a/build.rs b/build.rs index 437300934..5b7fd580a 100644 --- a/build.rs +++ b/build.rs @@ -8,8 +8,6 @@ fn main() { if is_apple { if !is_ios { - // DiskArbitration is not available on iOS: https://developer.apple.com/documentation/diskarbitration - println!("cargo:rustc-link-lib=framework=DiskArbitration"); // IOKit is not available on iOS: https://developer.apple.com/documentation/iokit println!("cargo:rustc-link-lib=framework=IOKit"); } diff --git a/src/apple/ffi.rs b/src/apple/ffi.rs index 7a8248537..0bc07b8c2 100644 --- a/src/apple/ffi.rs +++ b/src/apple/ffi.rs @@ -1,22 +1,8 @@ // Take a look at the license at the top of the repository in the LICENSE file. -use libc::c_void; - // Reexport items defined in either macos or ios ffi module. pub use crate::sys::inner::ffi::*; -#[repr(C)] -pub struct __DADisk(c_void); -#[repr(C)] -pub struct __DASession(c_void); - -// #[allow(non_camel_case_types)] -// pub type io_name_t = [u8; 128]; -// #[allow(non_camel_case_types)] -// pub type io_registry_entry_t = io_object_t; - -// pub type IOOptionBits = u32; - #[cfg_attr(feature = "debug", derive(Eq, Hash, PartialEq))] #[derive(Clone)] #[repr(C)] diff --git a/src/apple/macos/component/x86.rs b/src/apple/macos/component/x86.rs index 858ae619e..415f90455 100644 --- a/src/apple/macos/component/x86.rs +++ b/src/apple/macos/component/x86.rs @@ -1,6 +1,6 @@ // Take a look at the license at the top of the repository in the LICENSE file. -use crate::sys::ffi; +use crate::sys::{ffi, macos::utils::IOReleaser}; use crate::ComponentExt; use libc::{c_char, c_int, c_void}; @@ -253,31 +253,6 @@ fn get_temperature(con: ffi::io_connect_t, key: &[i8]) -> Option { } } -#[repr(transparent)] -pub(crate) struct IoObject(ffi::io_object_t); - -impl IoObject { - fn new(obj: ffi::io_object_t) -> Option { - if obj == 0 { - None - } else { - Some(Self(obj)) - } - } - - fn inner(&self) -> ffi::io_object_t { - self.0 - } -} - -impl Drop for IoObject { - fn drop(&mut self) { - unsafe { - ffi::IOObjectRelease(self.0); - } - } -} - pub(crate) struct IoService(ffi::io_connect_t); impl IoService { @@ -309,7 +284,7 @@ impl IoService { sysinfo_debug!("Error: IOServiceGetMatchingServices() = {}", result); return None; } - let iterator = match IoObject::new(iterator) { + let iterator = match IOReleaser::new(iterator) { Some(i) => i, None => { sysinfo_debug!("Error: IOServiceGetMatchingServices() succeeded but returned invalid descriptor"); @@ -317,7 +292,7 @@ impl IoService { } }; - let device = match IoObject::new(ffi::IOIteratorNext(iterator.inner())) { + let device = match IOReleaser::new(ffi::IOIteratorNext(iterator.inner())) { Some(d) => d, None => { sysinfo_debug!("Error: no SMC found"); diff --git a/src/apple/macos/disk.rs b/src/apple/macos/disk.rs index 5625158a1..bfc5c3a15 100644 --- a/src/apple/macos/disk.rs +++ b/src/apple/macos/disk.rs @@ -2,82 +2,127 @@ use crate::sys::macos::utils::{CFReleaser, IOReleaser}; use crate::sys::{ffi, utils}; -use crate::utils::to_cpath; use crate::{Disk, DiskType}; +use core_foundation_sys::array::CFArrayCreate; use core_foundation_sys::base::{kCFAllocatorDefault, kCFAllocatorNull}; use core_foundation_sys::dictionary::{CFDictionaryGetValueIfPresent, CFDictionaryRef}; -use core_foundation_sys::number::{kCFBooleanTrue, CFBooleanRef}; -use core_foundation_sys::string as cfs; +use core_foundation_sys::number::{kCFBooleanTrue, CFBooleanRef, CFNumberGetValue}; +use core_foundation_sys::string::{self as cfs, CFStringRef}; -use libc::{c_char, c_int, c_void, statfs}; +use libc::{c_void, statfs}; -use std::ffi::{OsStr, OsString}; -use std::mem; +use std::ffi::{CStr, OsStr, OsString}; use std::os::unix::ffi::OsStrExt; use std::path::PathBuf; use std::ptr; -fn to_path(mount_path: &[c_char]) -> Option { - let mut tmp = Vec::with_capacity(mount_path.len()); - for &c in mount_path { - if c == 0 { - break; - } - tmp.push(c as u8); - } - if tmp.is_empty() { - None - } else { - let path = OsStr::from_bytes(&tmp); - Some(PathBuf::from(path)) - } -} - -pub(crate) fn get_disks(session: ffi::DASessionRef) -> Vec { - if session.is_null() { - return Vec::new(); - } - unsafe { +pub(crate) fn get_disks() -> Vec { + let raw_disks = unsafe { let count = libc::getfsstat(ptr::null_mut(), 0, libc::MNT_NOWAIT); if count < 1 { return Vec::new(); } - let bufsize = count * mem::size_of::() as c_int; + let bufsize = count * std::mem::size_of::() as libc::c_int; let mut disks = Vec::with_capacity(count as _); let count = libc::getfsstat(disks.as_mut_ptr(), bufsize, libc::MNT_NOWAIT); + if count < 1 { return Vec::new(); } - disks.set_len(count as _); + + disks.set_len(count as usize); + disks - .into_iter() - .filter_map(|c_disk| { - let mount_point = to_path(&c_disk.f_mntonname)?; - let disk = CFReleaser::new(ffi::DADiskCreateFromBSDName( - kCFAllocatorDefault as _, - session, - c_disk.f_mntfromname.as_ptr(), - ))?; - let dict = CFReleaser::new(ffi::DADiskCopyDescription(disk.inner()))?; - // Keeping this around in case one might want the list of the available - // keys in "dict". - // core_foundation_sys::base::CFShow(dict as _); - let name = match get_str_value(dict.inner(), b"DAMediaName\0").map(OsString::from) { - Some(n) => n, - None => return None, - }; - let removable = - get_bool_value(dict.inner(), b"DAMediaRemovable\0").unwrap_or(false); - let ejectable = - get_bool_value(dict.inner(), b"DAMediaEjectable\0").unwrap_or(false); + }; + + // Create a list of properties about the disk that we want to fetch. + let requested_properties = unsafe { + [ + ffi::kCFURLVolumeIsEjectableKey, + ffi::kCFURLVolumeIsRemovableKey, + ffi::kCFURLVolumeTotalCapacityKey, + ffi::kCFURLVolumeAvailableCapacityForImportantUsageKey, + ffi::kCFURLVolumeAvailableCapacityKey, + ffi::kCFURLVolumeNameKey, + ffi::kCFURLVolumeIsBrowsableKey, + ffi::kCFURLVolumeIsLocalKey, + ] + }; + let requested_properties = CFReleaser::new(unsafe { + CFArrayCreate( + ptr::null_mut(), + requested_properties.as_ptr() as *const *const c_void, + requested_properties.len() as isize, + &core_foundation_sys::array::kCFTypeArrayCallBacks, + ) + }) + .unwrap(); + + let mut disks = Vec::with_capacity(raw_disks.len()); + for c_disk in raw_disks { + let volume_url = CFReleaser::new(unsafe { + core_foundation_sys::url::CFURLCreateFromFileSystemRepresentation( + kCFAllocatorDefault, + c_disk.f_mntonname.as_ptr() as *const u8, + c_disk.f_mntonname.len() as isize, + false as u8, + ) + }) + .unwrap(); + + let prop_dict = match CFReleaser::new(unsafe { + ffi::CFURLCopyResourcePropertiesForKeys( + volume_url.inner(), + requested_properties.inner(), + ptr::null_mut(), + ) + }) { + Some(props) => props, + None => continue, + }; + + let browsable = unsafe { + get_bool_value( + prop_dict.inner(), + DictKey::Extern(ffi::kCFURLVolumeIsBrowsableKey), + ) + .unwrap_or_default() + }; + + // Do not return invisible "disks." Most of the time, these are APFS snapshots, hidden + // system volumes, etc. Browsable is defined to be visible in the system's UI like Finder, + // disk utility, system information, etc. + // + // To avoid seemingly duplicating many disks and creating an inaccurate view of the system's resources, + // these are skipped entirely. + if !browsable { + continue; + } + + let local_only = unsafe { + get_bool_value( + prop_dict.inner(), + DictKey::Extern(ffi::kCFURLVolumeIsLocalKey), + ) + .unwrap_or(true) + }; + + // Skip any drive that is not locally attached to the system. + // + // This includes items like SMB mounts, and matches the other platform's behavior. + if !local_only { + continue; + } - let type_ = get_disk_type(&c_disk); + let mount_point = PathBuf::from(OsStr::from_bytes( + unsafe { CStr::from_ptr(c_disk.f_mntonname.as_ptr()) }.to_bytes(), + )); - new_disk(name, mount_point, type_, removable || ejectable) - }) - .collect::>() + disks.extend(new_disk(mount_point, c_disk, &prop_dict)) } + + disks } const UNKNOWN_DISK_TYPE: DiskType = DiskType::Unknown(-1); @@ -96,11 +141,10 @@ fn get_disk_type(disk: &statfs) -> DiskType { .unwrap(); // Removes `/dev/` from the value. - let bsd_name = { - let full = - std::str::from_utf8(unsafe { &*(&disk.f_mntfromname as *const [i8] as *const [u8]) }) - .unwrap(); - full.strip_prefix("/dev/") + let bsd_name = unsafe { + CStr::from_ptr(disk.f_mntfromname.as_ptr()) + .to_bytes() + .strip_prefix(b"/dev/") .expect("device mount point in unknown format") }; @@ -128,13 +172,13 @@ fn get_disk_type(disk: &statfs) -> DiskType { let mut parent_entry: ffi::io_registry_entry_t = 0; - loop { - let mut current_service_entry = - match IOReleaser::new(unsafe { ffi::IOIteratorNext(service_iterator.inner()) }) { - Some(entry) => entry, - None => break, // The iterator is empty - }; - + while let Some(mut current_service_entry) = + IOReleaser::new(unsafe { ffi::IOIteratorNext(service_iterator.inner()) }) + { + // Note: This loop is required in a non-obvious way. Due to device properties existing as a tree + // in IOKit, we may need an arbitrary number of calls to `IORegistryEntryCreateCFProperty` in order to find + // the values we are looking for. The function may return nothing if we aren't deep enough into the registry + // tree, so we need to continue going from child->parent node until its found. loop { let result = unsafe { ffi::IORegistryEntryGetParentEntry( @@ -165,7 +209,10 @@ fn get_disk_type(disk: &statfs) -> DiskType { if let Some(device_properties) = properties_result { let disk_type = unsafe { - get_str_value(device_properties.inner(), ffi::kIOPropertyMediumTypeKey) + get_str_value( + device_properties.inner(), + DictKey::Defined(ffi::kIOPropertyMediumTypeKey), + ) }; if let Some(disk_type) = disk_type.and_then(|medium| match medium.as_str() { @@ -188,30 +235,43 @@ fn get_disk_type(disk: &statfs) -> DiskType { UNKNOWN_DISK_TYPE } +enum DictKey { + Extern(CFStringRef), + Defined(&'static str), +} + unsafe fn get_dict_value Option>( dict: CFDictionaryRef, - key: &[u8], + key: DictKey, callback: F, ) -> Option { - match CFReleaser::new(ffi::CFStringCreateWithCStringNoCopy( - ptr::null_mut(), - key.as_ptr() as *const c_char, - cfs::kCFStringEncodingUTF8, - kCFAllocatorNull as _, - )) { - Some(c_key) => { - let mut value = std::ptr::null(); - if CFDictionaryGetValueIfPresent(dict, c_key.inner() as _, &mut value) != 0 { - callback(value) - } else { - None - } + let _defined; + let key = match key { + DictKey::Extern(val) => val, + DictKey::Defined(val) => { + _defined = CFReleaser::new(cfs::CFStringCreateWithBytesNoCopy( + kCFAllocatorDefault, + val.as_ptr(), + val.len() as isize, + cfs::kCFStringEncodingUTF8, + false as _, + kCFAllocatorNull, + )) + .unwrap(); + + _defined.inner() } - None => None, + }; + + let mut value = std::ptr::null(); + if CFDictionaryGetValueIfPresent(dict, key.cast(), &mut value) != 0 { + callback(value) + } else { + None } } -unsafe fn get_str_value(dict: CFDictionaryRef, key: &[u8]) -> Option { +unsafe fn get_str_value(dict: CFDictionaryRef, key: DictKey) -> Option { get_dict_value(dict, key, |v| { let v = v as cfs::CFStringRef; @@ -240,60 +300,95 @@ unsafe fn get_str_value(dict: CFDictionaryRef, key: &[u8]) -> Option { }) } -unsafe fn get_bool_value(dict: CFDictionaryRef, key: &[u8]) -> Option { +unsafe fn get_bool_value(dict: CFDictionaryRef, key: DictKey) -> Option { get_dict_value(dict, key, |v| Some(v as CFBooleanRef == kCFBooleanTrue)) } +unsafe fn get_int_value(dict: CFDictionaryRef, key: DictKey) -> Option { + get_dict_value(dict, key, |v| { + let mut val: i64 = 0; + if CFNumberGetValue( + v.cast(), + core_foundation_sys::number::kCFNumberSInt64Type, + &mut val as *mut i64 as *mut c_void, + ) { + Some(val) + } else { + None + } + }) +} + fn new_disk( - name: OsString, mount_point: PathBuf, - type_: DiskType, - is_removable: bool, + c_disk: statfs, + disk_props: &CFReleaser, ) -> Option { - let mount_point_cpath = to_cpath(&mount_point); - let mut total_space = 0; - let mut available_space = 0; - let mut file_system = None; - unsafe { - let mut stat: statfs = mem::zeroed(); - if statfs(mount_point_cpath.as_ptr() as *const i8, &mut stat) == 0 { - // APFS is "special" because its a snapshot-based filesystem, and modern - // macOS devices take full advantage of this. - // - // By default, listing volumes with `statfs` can return both the root-level - // "data" partition and any snapshots that exist. However, other than some flags and - // reserved(undocumented) bytes, there is no difference between the OS boot snapshot - // and the "data" partition. - // - // To avoid duplicating the number of disks (and therefore available space, etc), only return - // a disk (which is really a partition with APFS) if it is the root of the filesystem. - let is_root = stat.f_flags & libc::MNT_ROOTFS as u32 == 0; - if !is_root { - return None; - } + let type_ = get_disk_type(&c_disk); - total_space = u64::from(stat.f_bsize).saturating_mul(stat.f_blocks); - available_space = u64::from(stat.f_bsize).saturating_mul(stat.f_bavail); - let mut vec = Vec::with_capacity(stat.f_fstypename.len()); - for x in &stat.f_fstypename { - if *x == 0 { - break; - } - vec.push(*x as u8); - } - file_system = Some(vec); - } - if total_space == 0 { - return None; - } - Some(Disk { - type_, - name, - file_system: file_system.unwrap_or_else(|| b"".to_vec()), - mount_point, - total_space, - available_space, - is_removable, + let name = unsafe { + get_str_value( + disk_props.inner(), + DictKey::Extern(ffi::kCFURLVolumeNameKey), + ) + } + .map(OsString::from) + .unwrap(); + + let is_removable = unsafe { + let ejectable = get_bool_value( + disk_props.inner(), + DictKey::Extern(ffi::kCFURLVolumeIsEjectableKey), + ) + .unwrap(); + + let removable = get_bool_value( + disk_props.inner(), + DictKey::Extern(ffi::kCFURLVolumeIsRemovableKey), + ) + .unwrap(); + + ejectable || removable + }; + + let total_space = unsafe { + get_int_value( + disk_props.inner(), + DictKey::Extern(ffi::kCFURLVolumeTotalCapacityKey), + ) + } + .unwrap() as u64; + + // We prefer `AvailableCapacityForImportantUsage` over `AvailableCapacity` because + // it takes more of the system's properties into account, like the trash, system-managed caches, + // etc. It generally also returns higher values too, because of the above, so its a more accurate + // representation of what the system _could_ still use. + let available_space = unsafe { + get_int_value( + disk_props.inner(), + DictKey::Extern(ffi::kCFURLVolumeAvailableCapacityForImportantUsageKey), + ) + .filter(|bytes| *bytes != 0) + .or_else(|| { + get_int_value( + disk_props.inner(), + DictKey::Extern(ffi::kCFURLVolumeAvailableCapacityKey), + ) }) } + .unwrap() as u64; + + let file_system = IntoIterator::into_iter(c_disk.f_fstypename) + .filter_map(|b| if b != 0 { Some(b as u8) } else { None }) + .collect(); + + Some(Disk { + type_, + name, + file_system, + mount_point, + total_space, + available_space, + is_removable, + }) } diff --git a/src/apple/macos/ffi.rs b/src/apple/macos/ffi.rs index 499059772..514831136 100644 --- a/src/apple/macos/ffi.rs +++ b/src/apple/macos/ffi.rs @@ -1,15 +1,15 @@ // Take a look at the license at the top of the repository in the LICENSE file. -use core_foundation_sys::base::{mach_port_t, CFAllocatorRef, CFRelease}; +use core_foundation_sys::array::CFArrayRef; +use core_foundation_sys::base::{mach_port_t, CFAllocatorRef}; use core_foundation_sys::dictionary::{CFDictionaryRef, CFMutableDictionaryRef}; -use core_foundation_sys::string::{CFStringEncoding, CFStringRef}; +use core_foundation_sys::string::CFStringRef; -use libc::{c_char, c_void, kern_return_t}; +use core_foundation_sys::url::CFURLRef; +use libc::{c_char, kern_return_t}; // Note: IOKit is only available on MacOS up until very recent iOS versions: https://developer.apple.com/documentation/iokit -pub(crate) use crate::sys::ffi::*; - #[allow(non_camel_case_types)] pub type io_object_t = mach_port_t; @@ -27,7 +27,7 @@ pub const kIOServicePlane: &str = "IOService\0"; #[allow(non_upper_case_globals)] pub const kIOPropertyDeviceCharacteristicsKey: &str = "Device Characteristics"; #[allow(non_upper_case_globals)] -pub const kIOPropertyMediumTypeKey: &[u8] = b"Medium Type\0"; +pub const kIOPropertyMediumTypeKey: &str = "Medium Type"; #[allow(non_upper_case_globals)] pub const kIOPropertyMediumTypeSolidStateKey: &str = "Solid State"; #[allow(non_upper_case_globals)] @@ -68,53 +68,22 @@ extern "C" { } extern "C" { - pub fn CFStringCreateWithCStringNoCopy( - alloc: *mut c_void, - cStr: *const c_char, - encoding: CFStringEncoding, - contentsDeallocator: *mut c_void, - ) -> CFStringRef; - - // Disk information functions are non-operational on iOS because of the sandboxing - // restrictions of apps, so they don't can't filesystem information. This results in - // mountedVolumeURLs and similar returning `nil`. Hence, they are MacOS specific here. - - pub fn DASessionCreate(allocator: CFAllocatorRef) -> DASessionRef; - - // pub fn DADiskCreateFromVolumePath( - // allocator: CFAllocatorRef, - // session: DASessionRef, - // path: CFURLRef, - // ) -> DADiskRef; - pub fn DADiskCreateFromBSDName( - allocator: CFAllocatorRef, - session: DASessionRef, - path: *const c_char, - ) -> DADiskRef; - // pub fn DADiskGetBSDName(disk: DADiskRef) -> *const c_char; - - pub fn DADiskCopyDescription(disk: DADiskRef) -> CFMutableDictionaryRef; -} - -pub type DADiskRef = *const __DADisk; -pub type DASessionRef = *const __DASession; - -// We need to wrap `DASessionRef` to be sure `System` remains Send+Sync. -pub struct SessionWrap(pub DASessionRef); + pub fn CFURLCopyResourcePropertiesForKeys( + url: CFURLRef, + keys: CFArrayRef, + error: *mut CFArrayRef, + ) -> CFDictionaryRef; -impl Drop for SessionWrap { - fn drop(&mut self) { - if !self.0.is_null() { - unsafe { - CFRelease(self.0 as _); - } - } - } + pub static kCFURLVolumeIsEjectableKey: CFStringRef; + pub static kCFURLVolumeIsRemovableKey: CFStringRef; + pub static kCFURLVolumeAvailableCapacityKey: CFStringRef; + pub static kCFURLVolumeAvailableCapacityForImportantUsageKey: CFStringRef; + pub static kCFURLVolumeTotalCapacityKey: CFStringRef; + pub static kCFURLVolumeNameKey: CFStringRef; + pub static kCFURLVolumeIsLocalKey: CFStringRef; + pub static kCFURLVolumeIsBrowsableKey: CFStringRef; } -unsafe impl Send for SessionWrap {} -unsafe impl Sync for SessionWrap {} - #[cfg(all( not(feature = "apple-sandbox"), any(target_arch = "x86", target_arch = "x86_64") diff --git a/src/apple/system.rs b/src/apple/system.rs index 9cd74718c..f379e3ad6 100644 --- a/src/apple/system.rs +++ b/src/apple/system.rs @@ -3,19 +3,13 @@ use crate::sys::component::Component; use crate::sys::cpu::*; use crate::sys::disk::*; -#[cfg(target_os = "macos")] -use crate::sys::ffi; use crate::sys::network::Networks; use crate::sys::process::*; -#[cfg(target_os = "macos")] -use core_foundation_sys::base::kCFAllocatorDefault; use crate::{ CpuExt, CpuRefreshKind, LoadAvg, Pid, ProcessRefreshKind, RefreshKind, SystemExt, User, }; -#[cfg(target_os = "macos")] -use crate::sys::macos::utils::CFReleaser; #[cfg(all(target_os = "macos", not(feature = "apple-sandbox")))] use crate::ProcessExt; @@ -99,10 +93,6 @@ pub struct System { port: mach_port_t, users: Vec, boot_time: u64, - // Used to get disk information, to be more specific, it's needed by the - // DADiskCreateFromVolumePath function. Not supported on iOS. - #[cfg(target_os = "macos")] - session: Option>, #[cfg(all(target_os = "macos", not(feature = "apple-sandbox")))] clock_info: Option, got_cpu_frequency: bool, @@ -177,8 +167,6 @@ impl SystemExt for System { port, users: Vec::new(), boot_time: boot_time(), - #[cfg(target_os = "macos")] - session: None, #[cfg(all(target_os = "macos", not(feature = "apple-sandbox")))] clock_info: crate::sys::macos::system::SystemTimeInfo::new(port), got_cpu_frequency: false, @@ -369,14 +357,7 @@ impl SystemExt for System { #[cfg(target_os = "macos")] fn refresh_disks_list(&mut self) { - unsafe { - if self.session.is_none() { - self.session = CFReleaser::new(ffi::DASessionCreate(kCFAllocatorDefault as _)); - } - if let Some(ref session) = self.session { - self.disks = get_disks(session.inner()); - } - } + self.disks = get_disks(); } fn refresh_users_list(&mut self) { From b571367e41695288e04cf6e5552ca1f498125574 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Sun, 2 Oct 2022 19:47:57 -0500 Subject: [PATCH 3/7] Remove build script in favor of link attribute --- Cargo.toml | 1 - build.rs | 17 ----------------- src/apple/ffi.rs | 3 +++ src/apple/macos/ffi.rs | 1 + 4 files changed, 4 insertions(+), 18 deletions(-) delete mode 100644 build.rs diff --git a/Cargo.toml b/Cargo.toml index 0f129341d..8d9bea342 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,6 @@ exclude = ["/test-unknown"] categories = ["filesystem", "os", "api-bindings"] -build = "build.rs" edition = "2018" [dependencies] diff --git a/build.rs b/build.rs deleted file mode 100644 index 5b7fd580a..000000000 --- a/build.rs +++ /dev/null @@ -1,17 +0,0 @@ -fn main() { - let is_apple = std::env::var("TARGET") - .map(|t| t.contains("-apple")) - .unwrap_or(false); - let is_ios = std::env::var("CARGO_CFG_TARGET_OS") - .map(|s| s == "ios") - .unwrap_or(false); - - if is_apple { - if !is_ios { - // IOKit is not available on iOS: https://developer.apple.com/documentation/iokit - println!("cargo:rustc-link-lib=framework=IOKit"); - } - - println!("cargo:rustc-link-lib=framework=Foundation"); - } -} diff --git a/src/apple/ffi.rs b/src/apple/ffi.rs index 0bc07b8c2..75afff930 100644 --- a/src/apple/ffi.rs +++ b/src/apple/ffi.rs @@ -3,6 +3,9 @@ // Reexport items defined in either macos or ios ffi module. pub use crate::sys::inner::ffi::*; +#[link(name = "CoreFoundation", kind = "framework")] +extern "C" {} + #[cfg_attr(feature = "debug", derive(Eq, Hash, PartialEq))] #[derive(Clone)] #[repr(C)] diff --git a/src/apple/macos/ffi.rs b/src/apple/macos/ffi.rs index 514831136..03b0510b5 100644 --- a/src/apple/macos/ffi.rs +++ b/src/apple/macos/ffi.rs @@ -34,6 +34,7 @@ pub const kIOPropertyMediumTypeSolidStateKey: &str = "Solid State"; pub const kIOPropertyMediumTypeRotationalKey: &str = "Rotational"; // Note: Obtaining information about disks using IOKIt is allowed inside the default macOS App Sandbox. +#[link(name = "IOKit", kind = "framework")] extern "C" { pub fn IOServiceGetMatchingServices( mainPort: mach_port_t, From 62103073140891b4c8d38164f73ce830e9b3ce78 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Mon, 3 Oct 2022 22:02:04 -0500 Subject: [PATCH 4/7] Add support for getting disk info on iOS --- src/apple/disk.rs | 323 +++++++++++++++++++++++++++++-- src/apple/ffi.rs | 22 ++- src/apple/macos/component/arm.rs | 2 +- src/apple/macos/disk.rs | 292 ++-------------------------- src/apple/macos/ffi.rs | 19 -- src/apple/macos/utils.rs | 33 ---- src/apple/system.rs | 6 +- src/apple/utils.rs | 33 +++- src/utils.rs | 2 +- 9 files changed, 377 insertions(+), 355 deletions(-) diff --git a/src/apple/disk.rs b/src/apple/disk.rs index 9f0b4a3a3..d57d5fea9 100644 --- a/src/apple/disk.rs +++ b/src/apple/disk.rs @@ -1,15 +1,23 @@ // Take a look at the license at the top of the repository in the LICENSE file. -use crate::utils::to_cpath; +use crate::sys::{ + ffi, + utils::{self, CFReleaser}, +}; use crate::{DiskExt, DiskType}; -#[cfg(target_os = "macos")] -pub(crate) use crate::sys::inner::disk::*; +use core_foundation_sys::array::CFArrayCreate; +use core_foundation_sys::base::kCFAllocatorDefault; +use core_foundation_sys::dictionary::{CFDictionaryGetValueIfPresent, CFDictionaryRef}; +use core_foundation_sys::number::{kCFBooleanTrue, CFBooleanRef, CFNumberGetValue}; +use core_foundation_sys::string::{self as cfs, CFStringRef}; -use libc::statfs; -use std::ffi::{OsStr, OsString}; -use std::mem; +use libc::c_void; + +use std::ffi::{CStr, OsStr, OsString}; +use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; +use std::ptr; #[doc = include_str!("../../md_doc/disk.md")] pub struct Disk { @@ -17,6 +25,7 @@ pub struct Disk { pub(crate) name: OsString, pub(crate) file_system: Vec, pub(crate) mount_point: PathBuf, + volume_url: RetainedCFURL, pub(crate) total_space: u64, pub(crate) available_space: u64, pub(crate) is_removable: bool, @@ -53,14 +62,302 @@ impl DiskExt for Disk { fn refresh(&mut self) -> bool { unsafe { - let mut stat: statfs = mem::zeroed(); - let mount_point_cpath = to_cpath(&self.mount_point); - if statfs(mount_point_cpath.as_ptr() as *const i8, &mut stat) == 0 { - self.available_space = u64::from(stat.f_bsize).saturating_mul(stat.f_bavail); - true - } else { - false + let requested_properties = build_requested_properties(&[ + ffi::kCFURLVolumeAvailableCapacityKey, + ffi::kCFURLVolumeAvailableCapacityForImportantUsageKey, + ]); + match get_disk_properties(&self.volume_url, &requested_properties) { + Some(disk_props) => { + self.available_space = get_available_volume_space(&disk_props); + true + } + None => false, } } } } + +pub(super) unsafe fn get_disks() -> Vec { + let raw_disks = { + let count = libc::getfsstat(ptr::null_mut(), 0, libc::MNT_NOWAIT); + if count < 1 { + return Vec::new(); + } + let bufsize = count * std::mem::size_of::() as libc::c_int; + let mut disks = Vec::with_capacity(count as _); + let count = libc::getfsstat(disks.as_mut_ptr(), bufsize, libc::MNT_NOWAIT); + + if count < 1 { + return Vec::new(); + } + + disks.set_len(count as usize); + + disks + }; + + // Create a list of properties about the disk that we want to fetch. + let requested_properties = build_requested_properties(&[ + ffi::kCFURLVolumeIsEjectableKey, + ffi::kCFURLVolumeIsRemovableKey, + ffi::kCFURLVolumeTotalCapacityKey, + ffi::kCFURLVolumeAvailableCapacityForImportantUsageKey, + ffi::kCFURLVolumeAvailableCapacityKey, + ffi::kCFURLVolumeNameKey, + ffi::kCFURLVolumeIsBrowsableKey, + ffi::kCFURLVolumeIsLocalKey, + ]); + + let mut disks = Vec::with_capacity(raw_disks.len()); + for c_disk in raw_disks { + let volume_url = CFReleaser::new( + core_foundation_sys::url::CFURLCreateFromFileSystemRepresentation( + kCFAllocatorDefault, + c_disk.f_mntonname.as_ptr() as *const _, + c_disk.f_mntonname.len() as _, + false as _, + ), + ) + .unwrap(); + + let prop_dict = match get_disk_properties(&volume_url, &requested_properties) { + Some(props) => props, + None => continue, + }; + + let browsable = get_bool_value( + prop_dict.inner(), + DictKey::Extern(ffi::kCFURLVolumeIsBrowsableKey), + ) + .unwrap_or_default(); + + // Do not return invisible "disks". Most of the time, these are APFS snapshots, hidden + // system volumes, etc. Browsable is defined to be visible in the system's UI like Finder, + // disk utility, system information, etc. + // + // To avoid seemingly duplicating many disks and creating an inaccurate view of the system's resources, + // these are skipped entirely. + if !browsable { + continue; + } + + let local_only = get_bool_value( + prop_dict.inner(), + DictKey::Extern(ffi::kCFURLVolumeIsLocalKey), + ) + .unwrap_or(true); + + // Skip any drive that is not locally attached to the system. + // + // This includes items like SMB mounts, and matches the other platform's behavior. + if !local_only { + continue; + } + + let mount_point = PathBuf::from(OsStr::from_bytes( + CStr::from_ptr(c_disk.f_mntonname.as_ptr()).to_bytes(), + )); + + disks.extend(new_disk(mount_point, volume_url, c_disk, &prop_dict)) + } + + disks +} + +type RetainedCFArray = CFReleaser; +type RetainedCFDictionary = CFReleaser; +type RetainedCFURL = CFReleaser; + +unsafe fn build_requested_properties(properties: &[CFStringRef]) -> RetainedCFArray { + CFReleaser::new(CFArrayCreate( + ptr::null_mut(), + properties.as_ptr() as *const *const c_void, + properties.len() as _, + &core_foundation_sys::array::kCFTypeArrayCallBacks, + )) + .unwrap() +} + +fn get_disk_properties( + volume_url: &RetainedCFURL, + requested_properties: &RetainedCFArray, +) -> Option { + CFReleaser::new(unsafe { + ffi::CFURLCopyResourcePropertiesForKeys( + volume_url.inner(), + requested_properties.inner(), + ptr::null_mut(), + ) + }) +} + +fn get_available_volume_space(disk_props: &RetainedCFDictionary) -> u64 { + // We prefer `AvailableCapacityForImportantUsage` over `AvailableCapacity` because + // it takes more of the system's properties into account, like the trash, system-managed caches, + // etc. It generally also returns higher values too, because of the above, so it's a more accurate + // representation of what the system _could_ still use. + unsafe { + get_int_value( + disk_props.inner(), + DictKey::Extern(ffi::kCFURLVolumeAvailableCapacityForImportantUsageKey), + ) + .filter(|bytes| *bytes != 0) + .or_else(|| { + get_int_value( + disk_props.inner(), + DictKey::Extern(ffi::kCFURLVolumeAvailableCapacityKey), + ) + }) + } + .unwrap() as u64 +} + +pub(super) enum DictKey { + Extern(CFStringRef), + #[cfg(target_os = "macos")] + Defined(&'static str), +} + +unsafe fn get_dict_value Option>( + dict: CFDictionaryRef, + key: DictKey, + callback: F, +) -> Option { + #[cfg(target_os = "macos")] + let _defined; + let key = match key { + DictKey::Extern(val) => val, + #[cfg(target_os = "macos")] + DictKey::Defined(val) => { + _defined = CFReleaser::new(cfs::CFStringCreateWithBytesNoCopy( + kCFAllocatorDefault, + val.as_ptr(), + val.len() as _, + cfs::kCFStringEncodingUTF8, + false as _, + core_foundation_sys::base::kCFAllocatorNull, + )) + .unwrap(); + + _defined.inner() + } + }; + + let mut value = std::ptr::null(); + if CFDictionaryGetValueIfPresent(dict, key.cast(), &mut value) != 0 { + callback(value) + } else { + None + } +} + +pub(super) unsafe fn get_str_value(dict: CFDictionaryRef, key: DictKey) -> Option { + get_dict_value(dict, key, |v| { + let v = v as cfs::CFStringRef; + + let len_utf16 = cfs::CFStringGetLength(v) as usize; + let len_bytes = len_utf16 * 2; // Two bytes per UTF-16 codepoint. + + let v_ptr = cfs::CFStringGetCStringPtr(v, cfs::kCFStringEncodingUTF8); + if v_ptr.is_null() { + // Fallback on CFStringGetString to read the underlying bytes from the CFString. + let mut buf = vec![0; len_bytes]; + let success = cfs::CFStringGetCString( + v, + buf.as_mut_ptr(), + len_bytes as _, + cfs::kCFStringEncodingUTF8, + ); + + if success != 0 { + utils::vec_to_rust(buf) + } else { + None + } + } else { + utils::cstr_to_rust_with_size(v_ptr, Some(len_bytes)) + } + }) +} + +unsafe fn get_bool_value(dict: CFDictionaryRef, key: DictKey) -> Option { + get_dict_value(dict, key, |v| Some(v as CFBooleanRef == kCFBooleanTrue)) +} + +unsafe fn get_int_value(dict: CFDictionaryRef, key: DictKey) -> Option { + get_dict_value(dict, key, |v| { + let mut val: i64 = 0; + if CFNumberGetValue( + v.cast(), + core_foundation_sys::number::kCFNumberSInt64Type, + &mut val as *mut i64 as *mut c_void, + ) { + Some(val) + } else { + None + } + }) +} + +unsafe fn new_disk( + mount_point: PathBuf, + volume_url: RetainedCFURL, + c_disk: libc::statfs, + disk_props: &RetainedCFDictionary, +) -> Option { + // IOKit is not available on any but the most recent (16+) iOS and iPadOS versions. + // Due to this, we can't query the medium type. All iOS devices use flash-based storage + // so we just assume the disk type is an SSD until Rust has a way to conditionally link to + // IOKit in more recent deployment versions. + #[cfg(target_os = "macos")] + let type_ = crate::sys::inner::disk::get_disk_type(&c_disk); + #[cfg(not(target_os = "macos"))] + let type_ = DiskType::SSD; + + // Note: Since we requested these properties from the system, we don't expect + // these property retrievals to fail. + + let name = get_str_value( + disk_props.inner(), + DictKey::Extern(ffi::kCFURLVolumeNameKey), + ) + .map(OsString::from)?; + + let is_removable = { + let ejectable = get_bool_value( + disk_props.inner(), + DictKey::Extern(ffi::kCFURLVolumeIsEjectableKey), + ) + .unwrap_or_default(); + + let removable = get_bool_value( + disk_props.inner(), + DictKey::Extern(ffi::kCFURLVolumeIsRemovableKey), + ) + .unwrap_or_default(); + + ejectable || removable + }; + + let total_space = get_int_value( + disk_props.inner(), + DictKey::Extern(ffi::kCFURLVolumeTotalCapacityKey), + )? as u64; + + let available_space = get_available_volume_space(disk_props); + + let file_system = IntoIterator::into_iter(c_disk.f_fstypename) + .filter_map(|b| if b != 0 { Some(b as u8) } else { None }) + .collect(); + + Some(Disk { + type_, + name, + file_system, + mount_point, + volume_url, + total_space, + available_space, + is_removable, + }) +} diff --git a/src/apple/ffi.rs b/src/apple/ffi.rs index 75afff930..349c504ac 100644 --- a/src/apple/ffi.rs +++ b/src/apple/ffi.rs @@ -1,10 +1,30 @@ // Take a look at the license at the top of the repository in the LICENSE file. +use core_foundation_sys::{ + array::CFArrayRef, dictionary::CFDictionaryRef, error::CFErrorRef, string::CFStringRef, + url::CFURLRef, +}; + // Reexport items defined in either macos or ios ffi module. pub use crate::sys::inner::ffi::*; #[link(name = "CoreFoundation", kind = "framework")] -extern "C" {} +extern "C" { + pub fn CFURLCopyResourcePropertiesForKeys( + url: CFURLRef, + keys: CFArrayRef, + error: *mut CFErrorRef, + ) -> CFDictionaryRef; + + pub static kCFURLVolumeIsEjectableKey: CFStringRef; + pub static kCFURLVolumeIsRemovableKey: CFStringRef; + pub static kCFURLVolumeAvailableCapacityKey: CFStringRef; + pub static kCFURLVolumeAvailableCapacityForImportantUsageKey: CFStringRef; + pub static kCFURLVolumeTotalCapacityKey: CFStringRef; + pub static kCFURLVolumeNameKey: CFStringRef; + pub static kCFURLVolumeIsLocalKey: CFStringRef; + pub static kCFURLVolumeIsBrowsableKey: CFStringRef; +} #[cfg_attr(feature = "debug", derive(Eq, Hash, PartialEq))] #[derive(Clone)] diff --git a/src/apple/macos/component/arm.rs b/src/apple/macos/component/arm.rs index 776522f14..328ffebfa 100644 --- a/src/apple/macos/component/arm.rs +++ b/src/apple/macos/component/arm.rs @@ -15,7 +15,7 @@ use crate::apple::inner::ffi::{ IOHIDServiceClientCopyProperty, __IOHIDEventSystemClient, __IOHIDServiceClient, HID_DEVICE_PROPERTY_PRODUCT, }; -use crate::sys::macos::utils::CFReleaser; +use crate::sys::utils::CFReleaser; use crate::ComponentExt; pub(crate) struct Components { diff --git a/src/apple/macos/disk.rs b/src/apple/macos/disk.rs index bfc5c3a15..1b20c93f8 100644 --- a/src/apple/macos/disk.rs +++ b/src/apple/macos/disk.rs @@ -1,133 +1,21 @@ // Take a look at the license at the top of the repository in the LICENSE file. -use crate::sys::macos::utils::{CFReleaser, IOReleaser}; -use crate::sys::{ffi, utils}; -use crate::{Disk, DiskType}; +use crate::sys::ffi; +use crate::sys::{ + disk::{get_str_value, DictKey}, + macos::utils::IOReleaser, + utils::CFReleaser, +}; +use crate::DiskType; -use core_foundation_sys::array::CFArrayCreate; use core_foundation_sys::base::{kCFAllocatorDefault, kCFAllocatorNull}; -use core_foundation_sys::dictionary::{CFDictionaryGetValueIfPresent, CFDictionaryRef}; -use core_foundation_sys::number::{kCFBooleanTrue, CFBooleanRef, CFNumberGetValue}; -use core_foundation_sys::string::{self as cfs, CFStringRef}; +use core_foundation_sys::string as cfs; -use libc::{c_void, statfs}; - -use std::ffi::{CStr, OsStr, OsString}; -use std::os::unix::ffi::OsStrExt; -use std::path::PathBuf; -use std::ptr; - -pub(crate) fn get_disks() -> Vec { - let raw_disks = unsafe { - let count = libc::getfsstat(ptr::null_mut(), 0, libc::MNT_NOWAIT); - if count < 1 { - return Vec::new(); - } - let bufsize = count * std::mem::size_of::() as libc::c_int; - let mut disks = Vec::with_capacity(count as _); - let count = libc::getfsstat(disks.as_mut_ptr(), bufsize, libc::MNT_NOWAIT); - - if count < 1 { - return Vec::new(); - } - - disks.set_len(count as usize); - - disks - }; - - // Create a list of properties about the disk that we want to fetch. - let requested_properties = unsafe { - [ - ffi::kCFURLVolumeIsEjectableKey, - ffi::kCFURLVolumeIsRemovableKey, - ffi::kCFURLVolumeTotalCapacityKey, - ffi::kCFURLVolumeAvailableCapacityForImportantUsageKey, - ffi::kCFURLVolumeAvailableCapacityKey, - ffi::kCFURLVolumeNameKey, - ffi::kCFURLVolumeIsBrowsableKey, - ffi::kCFURLVolumeIsLocalKey, - ] - }; - let requested_properties = CFReleaser::new(unsafe { - CFArrayCreate( - ptr::null_mut(), - requested_properties.as_ptr() as *const *const c_void, - requested_properties.len() as isize, - &core_foundation_sys::array::kCFTypeArrayCallBacks, - ) - }) - .unwrap(); - - let mut disks = Vec::with_capacity(raw_disks.len()); - for c_disk in raw_disks { - let volume_url = CFReleaser::new(unsafe { - core_foundation_sys::url::CFURLCreateFromFileSystemRepresentation( - kCFAllocatorDefault, - c_disk.f_mntonname.as_ptr() as *const u8, - c_disk.f_mntonname.len() as isize, - false as u8, - ) - }) - .unwrap(); - - let prop_dict = match CFReleaser::new(unsafe { - ffi::CFURLCopyResourcePropertiesForKeys( - volume_url.inner(), - requested_properties.inner(), - ptr::null_mut(), - ) - }) { - Some(props) => props, - None => continue, - }; - - let browsable = unsafe { - get_bool_value( - prop_dict.inner(), - DictKey::Extern(ffi::kCFURLVolumeIsBrowsableKey), - ) - .unwrap_or_default() - }; - - // Do not return invisible "disks." Most of the time, these are APFS snapshots, hidden - // system volumes, etc. Browsable is defined to be visible in the system's UI like Finder, - // disk utility, system information, etc. - // - // To avoid seemingly duplicating many disks and creating an inaccurate view of the system's resources, - // these are skipped entirely. - if !browsable { - continue; - } - - let local_only = unsafe { - get_bool_value( - prop_dict.inner(), - DictKey::Extern(ffi::kCFURLVolumeIsLocalKey), - ) - .unwrap_or(true) - }; - - // Skip any drive that is not locally attached to the system. - // - // This includes items like SMB mounts, and matches the other platform's behavior. - if !local_only { - continue; - } - - let mount_point = PathBuf::from(OsStr::from_bytes( - unsafe { CStr::from_ptr(c_disk.f_mntonname.as_ptr()) }.to_bytes(), - )); - - disks.extend(new_disk(mount_point, c_disk, &prop_dict)) - } - - disks -} +use std::ffi::CStr; const UNKNOWN_DISK_TYPE: DiskType = DiskType::Unknown(-1); -fn get_disk_type(disk: &statfs) -> DiskType { +pub(crate) fn get_disk_type(disk: &libc::statfs) -> DiskType { let characteristics_string = CFReleaser::new(unsafe { cfs::CFStringCreateWithBytesNoCopy( kCFAllocatorDefault, @@ -209,7 +97,7 @@ fn get_disk_type(disk: &statfs) -> DiskType { if let Some(device_properties) = properties_result { let disk_type = unsafe { - get_str_value( + super::disk::get_str_value( device_properties.inner(), DictKey::Defined(ffi::kIOPropertyMediumTypeKey), ) @@ -234,161 +122,3 @@ fn get_disk_type(disk: &statfs) -> DiskType { UNKNOWN_DISK_TYPE } - -enum DictKey { - Extern(CFStringRef), - Defined(&'static str), -} - -unsafe fn get_dict_value Option>( - dict: CFDictionaryRef, - key: DictKey, - callback: F, -) -> Option { - let _defined; - let key = match key { - DictKey::Extern(val) => val, - DictKey::Defined(val) => { - _defined = CFReleaser::new(cfs::CFStringCreateWithBytesNoCopy( - kCFAllocatorDefault, - val.as_ptr(), - val.len() as isize, - cfs::kCFStringEncodingUTF8, - false as _, - kCFAllocatorNull, - )) - .unwrap(); - - _defined.inner() - } - }; - - let mut value = std::ptr::null(); - if CFDictionaryGetValueIfPresent(dict, key.cast(), &mut value) != 0 { - callback(value) - } else { - None - } -} - -unsafe fn get_str_value(dict: CFDictionaryRef, key: DictKey) -> Option { - get_dict_value(dict, key, |v| { - let v = v as cfs::CFStringRef; - - let len_utf16 = cfs::CFStringGetLength(v); - let len_bytes = len_utf16 as usize * 2; // Two bytes per UTF-16 codepoint. - - let v_ptr = cfs::CFStringGetCStringPtr(v, cfs::kCFStringEncodingUTF8); - if v_ptr.is_null() { - // Fallback on CFStringGetString to read the underlying bytes from the CFString. - let mut buf = vec![0; len_bytes]; - let success = cfs::CFStringGetCString( - v, - buf.as_mut_ptr(), - len_bytes as _, - cfs::kCFStringEncodingUTF8, - ); - - if success != 0 { - utils::vec_to_rust(buf) - } else { - None - } - } else { - utils::cstr_to_rust_with_size(v_ptr, Some(len_bytes)) - } - }) -} - -unsafe fn get_bool_value(dict: CFDictionaryRef, key: DictKey) -> Option { - get_dict_value(dict, key, |v| Some(v as CFBooleanRef == kCFBooleanTrue)) -} - -unsafe fn get_int_value(dict: CFDictionaryRef, key: DictKey) -> Option { - get_dict_value(dict, key, |v| { - let mut val: i64 = 0; - if CFNumberGetValue( - v.cast(), - core_foundation_sys::number::kCFNumberSInt64Type, - &mut val as *mut i64 as *mut c_void, - ) { - Some(val) - } else { - None - } - }) -} - -fn new_disk( - mount_point: PathBuf, - c_disk: statfs, - disk_props: &CFReleaser, -) -> Option { - let type_ = get_disk_type(&c_disk); - - let name = unsafe { - get_str_value( - disk_props.inner(), - DictKey::Extern(ffi::kCFURLVolumeNameKey), - ) - } - .map(OsString::from) - .unwrap(); - - let is_removable = unsafe { - let ejectable = get_bool_value( - disk_props.inner(), - DictKey::Extern(ffi::kCFURLVolumeIsEjectableKey), - ) - .unwrap(); - - let removable = get_bool_value( - disk_props.inner(), - DictKey::Extern(ffi::kCFURLVolumeIsRemovableKey), - ) - .unwrap(); - - ejectable || removable - }; - - let total_space = unsafe { - get_int_value( - disk_props.inner(), - DictKey::Extern(ffi::kCFURLVolumeTotalCapacityKey), - ) - } - .unwrap() as u64; - - // We prefer `AvailableCapacityForImportantUsage` over `AvailableCapacity` because - // it takes more of the system's properties into account, like the trash, system-managed caches, - // etc. It generally also returns higher values too, because of the above, so its a more accurate - // representation of what the system _could_ still use. - let available_space = unsafe { - get_int_value( - disk_props.inner(), - DictKey::Extern(ffi::kCFURLVolumeAvailableCapacityForImportantUsageKey), - ) - .filter(|bytes| *bytes != 0) - .or_else(|| { - get_int_value( - disk_props.inner(), - DictKey::Extern(ffi::kCFURLVolumeAvailableCapacityKey), - ) - }) - } - .unwrap() as u64; - - let file_system = IntoIterator::into_iter(c_disk.f_fstypename) - .filter_map(|b| if b != 0 { Some(b as u8) } else { None }) - .collect(); - - Some(Disk { - type_, - name, - file_system, - mount_point, - total_space, - available_space, - is_removable, - }) -} diff --git a/src/apple/macos/ffi.rs b/src/apple/macos/ffi.rs index 03b0510b5..0b9c82cfa 100644 --- a/src/apple/macos/ffi.rs +++ b/src/apple/macos/ffi.rs @@ -1,11 +1,9 @@ // Take a look at the license at the top of the repository in the LICENSE file. -use core_foundation_sys::array::CFArrayRef; use core_foundation_sys::base::{mach_port_t, CFAllocatorRef}; use core_foundation_sys::dictionary::{CFDictionaryRef, CFMutableDictionaryRef}; use core_foundation_sys::string::CFStringRef; -use core_foundation_sys::url::CFURLRef; use libc::{c_char, kern_return_t}; // Note: IOKit is only available on MacOS up until very recent iOS versions: https://developer.apple.com/documentation/iokit @@ -68,23 +66,6 @@ extern "C" { pub static kIOMasterPortDefault: mach_port_t; } -extern "C" { - pub fn CFURLCopyResourcePropertiesForKeys( - url: CFURLRef, - keys: CFArrayRef, - error: *mut CFArrayRef, - ) -> CFDictionaryRef; - - pub static kCFURLVolumeIsEjectableKey: CFStringRef; - pub static kCFURLVolumeIsRemovableKey: CFStringRef; - pub static kCFURLVolumeAvailableCapacityKey: CFStringRef; - pub static kCFURLVolumeAvailableCapacityForImportantUsageKey: CFStringRef; - pub static kCFURLVolumeTotalCapacityKey: CFStringRef; - pub static kCFURLVolumeNameKey: CFStringRef; - pub static kCFURLVolumeIsLocalKey: CFStringRef; - pub static kCFURLVolumeIsBrowsableKey: CFStringRef; -} - #[cfg(all( not(feature = "apple-sandbox"), any(target_arch = "x86", target_arch = "x86_64") diff --git a/src/apple/macos/utils.rs b/src/apple/macos/utils.rs index e4e7ddb42..71baa6f45 100644 --- a/src/apple/macos/utils.rs +++ b/src/apple/macos/utils.rs @@ -1,38 +1,5 @@ // Take a look at the license at the top of the repository in the LICENSE file. -use core_foundation_sys::base::CFRelease; - -// A helper using to auto release the resource got from CoreFoundation. -// More information about the ownership policy for CoreFoundation pelease refer the link below: -// https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFMemoryMgmt/Concepts/Ownership.html#//apple_ref/doc/uid/20001148-CJBEJBHH -#[repr(transparent)] -pub(crate) struct CFReleaser(*const T); - -impl CFReleaser { - pub(crate) fn new(ptr: *const T) -> Option { - if ptr.is_null() { - None - } else { - Some(Self(ptr)) - } - } - - pub(crate) fn inner(&self) -> *const T { - self.0 - } -} - -impl Drop for CFReleaser { - fn drop(&mut self) { - if !self.0.is_null() { - unsafe { CFRelease(self.0 as _) } - } - } -} - -unsafe impl Send for CFReleaser {} -unsafe impl Sync for CFReleaser {} - pub(crate) struct IOReleaser(super::ffi::io_object_t); impl IOReleaser { diff --git a/src/apple/system.rs b/src/apple/system.rs index f379e3ad6..12abbd23b 100644 --- a/src/apple/system.rs +++ b/src/apple/system.rs @@ -352,12 +352,8 @@ impl SystemExt for System { } } - #[cfg(target_os = "ios")] - fn refresh_disks_list(&mut self) {} - - #[cfg(target_os = "macos")] fn refresh_disks_list(&mut self) { - self.disks = get_disks(); + self.disks = unsafe { get_disks() }; } fn refresh_users_list(&mut self) { diff --git a/src/apple/utils.rs b/src/apple/utils.rs index 019295b95..e524f56ca 100644 --- a/src/apple/utils.rs +++ b/src/apple/utils.rs @@ -1,7 +1,39 @@ // Take a look at the license at the top of the repository in the LICENSE file. +use core_foundation_sys::base::CFRelease; use libc::c_char; +// A helper using to auto release the resource got from CoreFoundation. +// More information about the ownership policy for CoreFoundation pelease refer the link below: +// https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFMemoryMgmt/Concepts/Ownership.html#//apple_ref/doc/uid/20001148-CJBEJBHH +#[repr(transparent)] +pub(crate) struct CFReleaser(*const T); + +impl CFReleaser { + pub(crate) fn new(ptr: *const T) -> Option { + if ptr.is_null() { + None + } else { + Some(Self(ptr)) + } + } + + pub(crate) fn inner(&self) -> *const T { + self.0 + } +} + +impl Drop for CFReleaser { + fn drop(&mut self) { + if !self.0.is_null() { + unsafe { CFRelease(self.0 as _) } + } + } +} + +unsafe impl Send for CFReleaser {} +unsafe impl Sync for CFReleaser {} + pub(crate) fn cstr_to_rust(c: *const c_char) -> Option { cstr_to_rust_with_size(c, None) } @@ -28,7 +60,6 @@ pub(crate) fn cstr_to_rust_with_size(c: *const c_char, size: Option) -> O } } -#[cfg(target_os = "macos")] pub(crate) fn vec_to_rust(buf: Vec) -> Option { String::from_utf8( buf.into_iter() diff --git a/src/utils.rs b/src/utils.rs index b5dda0866..a81ad567f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -3,7 +3,7 @@ /* convert a path to a NUL-terminated Vec suitable for use with C functions */ #[cfg(all( not(feature = "unknown-ci"), - any(target_os = "linux", target_os = "android", target_vendor = "apple") + any(target_os = "linux", target_os = "android") ))] pub(crate) fn to_cpath(path: &std::path::Path) -> Vec { use std::{ffi::OsStr, os::unix::ffi::OsStrExt}; From d4b4055e0003edceb34e4d6fae3650b3bd880166 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Tue, 4 Oct 2022 00:44:09 -0500 Subject: [PATCH 5/7] Improve ejectable disk detection on Apple platforms --- src/apple/disk.rs | 19 +++++++++++++++++-- src/apple/ffi.rs | 1 + 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/apple/disk.rs b/src/apple/disk.rs index d57d5fea9..a1e91b7f0 100644 --- a/src/apple/disk.rs +++ b/src/apple/disk.rs @@ -100,6 +100,7 @@ pub(super) unsafe fn get_disks() -> Vec { let requested_properties = build_requested_properties(&[ ffi::kCFURLVolumeIsEjectableKey, ffi::kCFURLVolumeIsRemovableKey, + ffi::kCFURLVolumeIsInternalKey, ffi::kCFURLVolumeTotalCapacityKey, ffi::kCFURLVolumeAvailableCapacityForImportantUsageKey, ffi::kCFURLVolumeAvailableCapacityKey, @@ -209,7 +210,7 @@ fn get_available_volume_space(disk_props: &RetainedCFDictionary) -> u64 { ) }) } - .unwrap() as u64 + .unwrap_or_default() as u64 } pub(super) enum DictKey { @@ -336,7 +337,21 @@ unsafe fn new_disk( ) .unwrap_or_default(); - ejectable || removable + let is_removable = ejectable || removable; + + if is_removable { + is_removable + } else { + // If neither `ejectable` or `removable` return `true`, fallback to checking + // if the disk is attached to the internal system. + let internal = get_bool_value( + disk_props.inner(), + DictKey::Extern(ffi::kCFURLVolumeIsInternalKey), + ) + .unwrap_or_default(); + + !internal + } }; let total_space = get_int_value( diff --git a/src/apple/ffi.rs b/src/apple/ffi.rs index 349c504ac..72822202f 100644 --- a/src/apple/ffi.rs +++ b/src/apple/ffi.rs @@ -23,6 +23,7 @@ extern "C" { pub static kCFURLVolumeTotalCapacityKey: CFStringRef; pub static kCFURLVolumeNameKey: CFStringRef; pub static kCFURLVolumeIsLocalKey: CFStringRef; + pub static kCFURLVolumeIsInternalKey: CFStringRef; pub static kCFURLVolumeIsBrowsableKey: CFStringRef; } From a4e6255685d5321a31285bc9636da0f6d0e31108 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Tue, 4 Oct 2022 21:17:46 -0500 Subject: [PATCH 6/7] Add implementation comments --- src/apple/disk.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/apple/disk.rs b/src/apple/disk.rs index a1e91b7f0..b5595a1b1 100644 --- a/src/apple/disk.rs +++ b/src/apple/disk.rs @@ -126,6 +126,10 @@ pub(super) unsafe fn get_disks() -> Vec { None => continue, }; + // Future note: There is a difference between `kCFURLVolumeIsBrowsableKey` and the + // `kCFURLEnumeratorSkipInvisibles` option of `CFURLEnumeratorOptions`. Specifically, + // the first one considers the writable `Data`(`/System/Volumes/Data`) partition to be + // browsable, while it is classified as "invisible" by CoreFoundation's volume emumerator. let browsable = get_bool_value( prop_dict.inner(), DictKey::Extern(ffi::kCFURLVolumeIsBrowsableKey), From 13c32c94c78caf9e22872b785cd729f5970912fb Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Sat, 22 Oct 2022 20:21:48 -0500 Subject: [PATCH 7/7] Improve object releasers and cleanup usage sites --- src/apple/disk.rs | 47 +++++++++++++++++++++++-------------- src/apple/macos/disk.rs | 50 +++++++++++++++++++++------------------- src/apple/macos/utils.rs | 25 ++++++++++++-------- src/apple/utils.rs | 21 +++++++++-------- 4 files changed, 82 insertions(+), 61 deletions(-) diff --git a/src/apple/disk.rs b/src/apple/disk.rs index b5595a1b1..d866d5bba 100644 --- a/src/apple/disk.rs +++ b/src/apple/disk.rs @@ -62,16 +62,20 @@ impl DiskExt for Disk { fn refresh(&mut self) -> bool { unsafe { - let requested_properties = build_requested_properties(&[ + if let Some(requested_properties) = build_requested_properties(&[ ffi::kCFURLVolumeAvailableCapacityKey, ffi::kCFURLVolumeAvailableCapacityForImportantUsageKey, - ]); - match get_disk_properties(&self.volume_url, &requested_properties) { - Some(disk_props) => { - self.available_space = get_available_volume_space(&disk_props); - true + ]) { + match get_disk_properties(&self.volume_url, &requested_properties) { + Some(disk_props) => { + self.available_space = get_available_volume_space(&disk_props); + true + } + None => false, } - None => false, + } else { + sysinfo_debug!("failed to create volume key list, skipping refresh"); + false } } } @@ -97,7 +101,7 @@ pub(super) unsafe fn get_disks() -> Vec { }; // Create a list of properties about the disk that we want to fetch. - let requested_properties = build_requested_properties(&[ + let requested_properties = match build_requested_properties(&[ ffi::kCFURLVolumeIsEjectableKey, ffi::kCFURLVolumeIsRemovableKey, ffi::kCFURLVolumeIsInternalKey, @@ -107,19 +111,30 @@ pub(super) unsafe fn get_disks() -> Vec { ffi::kCFURLVolumeNameKey, ffi::kCFURLVolumeIsBrowsableKey, ffi::kCFURLVolumeIsLocalKey, - ]); + ]) { + Some(properties) => properties, + None => { + sysinfo_debug!("failed to create volume key list"); + return Vec::new(); + } + }; let mut disks = Vec::with_capacity(raw_disks.len()); for c_disk in raw_disks { - let volume_url = CFReleaser::new( + let volume_url = match CFReleaser::new( core_foundation_sys::url::CFURLCreateFromFileSystemRepresentation( kCFAllocatorDefault, c_disk.f_mntonname.as_ptr() as *const _, c_disk.f_mntonname.len() as _, false as _, ), - ) - .unwrap(); + ) { + Some(url) => url, + None => { + sysinfo_debug!("getfsstat returned incompatible paths"); + continue; + } + }; let prop_dict = match get_disk_properties(&volume_url, &requested_properties) { Some(props) => props, @@ -173,14 +188,13 @@ type RetainedCFArray = CFReleaser; type RetainedCFDictionary = CFReleaser; type RetainedCFURL = CFReleaser; -unsafe fn build_requested_properties(properties: &[CFStringRef]) -> RetainedCFArray { +unsafe fn build_requested_properties(properties: &[CFStringRef]) -> Option { CFReleaser::new(CFArrayCreate( ptr::null_mut(), properties.as_ptr() as *const *const c_void, properties.len() as _, &core_foundation_sys::array::kCFTypeArrayCallBacks, )) - .unwrap() } fn get_disk_properties( @@ -241,8 +255,7 @@ unsafe fn get_dict_value Option>( cfs::kCFStringEncodingUTF8, false as _, core_foundation_sys::base::kCFAllocatorNull, - )) - .unwrap(); + ))?; _defined.inner() } @@ -315,7 +328,7 @@ unsafe fn new_disk( // so we just assume the disk type is an SSD until Rust has a way to conditionally link to // IOKit in more recent deployment versions. #[cfg(target_os = "macos")] - let type_ = crate::sys::inner::disk::get_disk_type(&c_disk); + let type_ = crate::sys::inner::disk::get_disk_type(&c_disk).unwrap_or(DiskType::Unknown(-1)); #[cfg(not(target_os = "macos"))] let type_ = DiskType::SSD; diff --git a/src/apple/macos/disk.rs b/src/apple/macos/disk.rs index 1b20c93f8..3a4372a2f 100644 --- a/src/apple/macos/disk.rs +++ b/src/apple/macos/disk.rs @@ -13,34 +13,36 @@ use core_foundation_sys::string as cfs; use std::ffi::CStr; -const UNKNOWN_DISK_TYPE: DiskType = DiskType::Unknown(-1); - -pub(crate) fn get_disk_type(disk: &libc::statfs) -> DiskType { - let characteristics_string = CFReleaser::new(unsafe { - cfs::CFStringCreateWithBytesNoCopy( +pub(crate) fn get_disk_type(disk: &libc::statfs) -> Option { + let characteristics_string = unsafe { + CFReleaser::new(cfs::CFStringCreateWithBytesNoCopy( kCFAllocatorDefault, ffi::kIOPropertyDeviceCharacteristicsKey.as_ptr(), ffi::kIOPropertyDeviceCharacteristicsKey.len() as _, cfs::kCFStringEncodingUTF8, false as _, kCFAllocatorNull, - ) - }) - .unwrap(); + ))? + }; // Removes `/dev/` from the value. let bsd_name = unsafe { CStr::from_ptr(disk.f_mntfromname.as_ptr()) .to_bytes() .strip_prefix(b"/dev/") - .expect("device mount point in unknown format") + .or_else(|| { + sysinfo_debug!("unknown disk mount path format"); + None + })? }; + // We don't need to wrap this in an auto-releaser because the following call to `IOServiceGetMatchingServices` + // will take ownership of one retain reference. let matching = unsafe { ffi::IOBSDNameMatching(ffi::kIOMasterPortDefault, 0, bsd_name.as_ptr().cast()) }; if matching.is_null() { - return UNKNOWN_DISK_TYPE; + return None; } let mut service_iterator: ffi::io_iterator_t = 0; @@ -53,10 +55,11 @@ pub(crate) fn get_disk_type(disk: &libc::statfs) -> DiskType { ) } != libc::KERN_SUCCESS { - return UNKNOWN_DISK_TYPE; + return None; } - let service_iterator = IOReleaser::new(service_iterator).unwrap(); + // Safety: We checked for success, so there is always a valid iterator, even if its empty. + let service_iterator = unsafe { IOReleaser::new_unchecked(service_iterator) }; let mut parent_entry: ffi::io_registry_entry_t = 0; @@ -68,23 +71,22 @@ pub(crate) fn get_disk_type(disk: &libc::statfs) -> DiskType { // the values we are looking for. The function may return nothing if we aren't deep enough into the registry // tree, so we need to continue going from child->parent node until its found. loop { - let result = unsafe { + if unsafe { ffi::IORegistryEntryGetParentEntry( current_service_entry.inner(), ffi::kIOServicePlane.as_ptr().cast(), &mut parent_entry, ) - }; - if result != libc::KERN_SUCCESS { + } != libc::KERN_SUCCESS + { break; } - current_service_entry = IOReleaser::new(parent_entry).unwrap(); - - // There were no more parents left. - if parent_entry == 0 { - break; - } + current_service_entry = match IOReleaser::new(parent_entry) { + Some(service) => service, + // There were no more parents left + None => break, + }; let properties_result = unsafe { CFReleaser::new(ffi::IORegistryEntryCreateCFProperty( @@ -108,17 +110,17 @@ pub(crate) fn get_disk_type(disk: &libc::statfs) -> DiskType { _ if medium == ffi::kIOPropertyMediumTypeRotationalKey => Some(DiskType::HDD), _ => None, }) { - return disk_type; + return Some(disk_type); } else { // Many external drive vendors do not advertise their device's storage medium. // // In these cases, assuming that there were _any_ properties about them registered, we fallback // to `HDD` when no storage medium is provided by the device instead of `Unknown`. - return DiskType::HDD; + return Some(DiskType::HDD); } } } } - UNKNOWN_DISK_TYPE + None } diff --git a/src/apple/macos/utils.rs b/src/apple/macos/utils.rs index 71baa6f45..ff870db55 100644 --- a/src/apple/macos/utils.rs +++ b/src/apple/macos/utils.rs @@ -1,25 +1,30 @@ // Take a look at the license at the top of the repository in the LICENSE file. -pub(crate) struct IOReleaser(super::ffi::io_object_t); +use std::num::NonZeroU32; + +type IoObject = NonZeroU32; + +pub(crate) struct IOReleaser(IoObject); impl IOReleaser { pub(crate) fn new(obj: u32) -> Option { - if obj == 0 { - None - } else { - Some(Self(obj)) - } + IoObject::new(obj).map(Self) + } + + pub(crate) unsafe fn new_unchecked(obj: u32) -> Self { + // Chance at catching in-development mistakes + debug_assert_ne!(obj, 0); + Self(IoObject::new_unchecked(obj)) } + #[inline] pub(crate) fn inner(&self) -> u32 { - self.0 + self.0.get() } } impl Drop for IOReleaser { fn drop(&mut self) { - if self.0 != 0 { - unsafe { super::ffi::IOObjectRelease(self.0 as _) }; - } + unsafe { super::ffi::IOObjectRelease(self.0.get() as _) }; } } diff --git a/src/apple/utils.rs b/src/apple/utils.rs index e524f56ca..408c02c31 100644 --- a/src/apple/utils.rs +++ b/src/apple/utils.rs @@ -2,35 +2,36 @@ use core_foundation_sys::base::CFRelease; use libc::c_char; +use std::ptr::NonNull; // A helper using to auto release the resource got from CoreFoundation. // More information about the ownership policy for CoreFoundation pelease refer the link below: // https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFMemoryMgmt/Concepts/Ownership.html#//apple_ref/doc/uid/20001148-CJBEJBHH #[repr(transparent)] -pub(crate) struct CFReleaser(*const T); +pub(crate) struct CFReleaser(NonNull); impl CFReleaser { pub(crate) fn new(ptr: *const T) -> Option { - if ptr.is_null() { - None - } else { - Some(Self(ptr)) - } + // This cast is OK because `NonNull` is a transparent wrapper + // over a `*const T`. Additionally, mutability doesn't matter with + // pointers here. + NonNull::new(ptr as *mut T).map(Self) } pub(crate) fn inner(&self) -> *const T { - self.0 + self.0.as_ptr().cast() } } impl Drop for CFReleaser { fn drop(&mut self) { - if !self.0.is_null() { - unsafe { CFRelease(self.0 as _) } - } + unsafe { CFRelease(self.0.as_ptr().cast()) } } } +// Safety: These are safe to implement because we only wrap non-mutable +// CoreFoundation types, which are generally threadsafe unless noted +// otherwise. unsafe impl Send for CFReleaser {} unsafe impl Sync for CFReleaser {}