blob: e5025390dd6556e1de72e42705efd3ea1bb3e1e8 [file] [log] [blame]
// Take a look at the license at the top of the repository in the LICENSE file.
use crate::sys::{
ffi,
utils::{self, CFReleaser},
};
use crate::{Disk, DiskKind};
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::c_void;
use std::ffi::{CStr, OsStr, OsString};
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::path::{Path, PathBuf};
use std::ptr;
pub(crate) struct DiskInner {
pub(crate) type_: DiskKind,
pub(crate) name: OsString,
pub(crate) file_system: OsString,
pub(crate) mount_point: PathBuf,
volume_url: RetainedCFURL,
pub(crate) total_space: u64,
pub(crate) available_space: u64,
pub(crate) 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.mount_point
}
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 {
unsafe {
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
}
None => {
sysinfo_debug!("Failed to get disk properties");
false
}
}
} else {
sysinfo_debug!("failed to create volume key list, skipping refresh");
false
}
}
}
}
impl crate::DisksInner {
pub(crate) fn new() -> Self {
Self {
disks: Vec::with_capacity(2),
}
}
pub(crate) fn refresh_list(&mut self) {
unsafe {
get_list(&mut self.disks);
}
}
pub(crate) fn list(&self) -> &[Disk] {
&self.disks
}
pub(crate) fn list_mut(&mut self) -> &mut [Disk] {
&mut self.disks
}
}
unsafe fn get_list(container: &mut Vec<Disk>) {
container.clear();
let raw_disks = {
let count = libc::getfsstat(ptr::null_mut(), 0, libc::MNT_NOWAIT);
if count < 1 {
return;
}
let bufsize = count * std::mem::size_of::<libc::statfs>() 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;
}
disks.set_len(count as usize);
disks
};
// Create a list of properties about the disk that we want to fetch.
let requested_properties = match build_requested_properties(&[
ffi::kCFURLVolumeIsEjectableKey,
ffi::kCFURLVolumeIsRemovableKey,
ffi::kCFURLVolumeIsInternalKey,
ffi::kCFURLVolumeTotalCapacityKey,
ffi::kCFURLVolumeAvailableCapacityForImportantUsageKey,
ffi::kCFURLVolumeAvailableCapacityKey,
ffi::kCFURLVolumeNameKey,
ffi::kCFURLVolumeIsBrowsableKey,
ffi::kCFURLVolumeIsLocalKey,
]) {
Some(properties) => properties,
None => {
sysinfo_debug!("failed to create volume key list");
return;
}
};
for c_disk in raw_disks {
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 _,
),
) {
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,
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),
)
.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(),
));
if let Some(disk) = new_disk(mount_point, volume_url, c_disk, &prop_dict) {
container.push(disk);
}
}
}
type RetainedCFArray = CFReleaser<core_foundation_sys::array::__CFArray>;
type RetainedCFDictionary = CFReleaser<core_foundation_sys::dictionary::__CFDictionary>;
type RetainedCFURL = CFReleaser<core_foundation_sys::url::__CFURL>;
unsafe fn build_requested_properties(properties: &[CFStringRef]) -> Option<RetainedCFArray> {
CFReleaser::new(CFArrayCreate(
ptr::null_mut(),
properties.as_ptr() as *const *const c_void,
properties.len() as _,
&core_foundation_sys::array::kCFTypeArrayCallBacks,
))
}
fn get_disk_properties(
volume_url: &RetainedCFURL,
requested_properties: &RetainedCFArray,
) -> Option<RetainedCFDictionary> {
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_or_default() as u64
}
pub(super) enum DictKey {
Extern(CFStringRef),
#[cfg(target_os = "macos")]
Defined(&'static str),
}
unsafe fn get_dict_value<T, F: FnOnce(*const c_void) -> Option<T>>(
dict: CFDictionaryRef,
key: DictKey,
callback: F,
) -> Option<T> {
#[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,
))?;
_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<String> {
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 {
crate::unix::utils::cstr_to_rust_with_size(v_ptr, Some(len_bytes))
}
})
}
unsafe fn get_bool_value(dict: CFDictionaryRef, key: DictKey) -> Option<bool> {
get_dict_value(dict, key, |v| Some(v as CFBooleanRef == kCFBooleanTrue))
}
unsafe fn get_int_value(dict: CFDictionaryRef, key: DictKey) -> Option<i64> {
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<Disk> {
// 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).unwrap_or(DiskKind::Unknown(-1));
#[cfg(not(target_os = "macos"))]
let type_ = DiskKind::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();
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(
disk_props.inner(),
DictKey::Extern(ffi::kCFURLVolumeTotalCapacityKey),
)? as u64;
let available_space = get_available_volume_space(disk_props);
let file_system = {
let len = c_disk
.f_fstypename
.iter()
.position(|&b| b == 0)
.unwrap_or(c_disk.f_fstypename.len());
OsString::from_vec(
c_disk.f_fstypename[..len]
.iter()
.map(|c| *c as u8)
.collect(),
)
};
Some(Disk {
inner: DiskInner {
type_,
name,
file_system,
mount_point,
volume_url,
total_space,
available_space,
is_removable,
},
})
}