]> git.proxmox.com Git - rustc.git/blobdiff - vendor/sysinfo/src/apple/disk.rs
New upstream version 1.67.1+dfsg1
[rustc.git] / vendor / sysinfo / src / apple / disk.rs
index 9f0b4a3a3755bfeb418597354d99e6f0a24a2a30..d866d5bbad83c76b01d64cd62d5fd764ab98d8f1 100644 (file)
@@ -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<u8>,
     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,334 @@ 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
+            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 => false,
+                }
             } else {
+                sysinfo_debug!("failed to create volume key list, skipping refresh");
                 false
             }
         }
     }
 }
+
+pub(super) unsafe fn get_disks() -> Vec<Disk> {
+    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::<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 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 = 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 Vec::new();
+        }
+    };
+
+    let mut disks = Vec::with_capacity(raw_disks.len());
+    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(),
+        ));
+
+        disks.extend(new_disk(mount_point, volume_url, c_disk, &prop_dict))
+    }
+
+    disks
+}
+
+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 {
+            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(DiskType::Unknown(-1));
+    #[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();
+
+        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 = 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,
+    })
+}