#![allow(
unsafe_code,
reason = "IOKit/CoreFoundation FFI is inherently unsafe; isolated to collect_media + read_media, each call annotated with SAFETY"
)]
use std::ffi::CString;
use super::{Error, Partition, PhysicalDisk};
use core_foundation::base::{CFType, TCFType};
use core_foundation::boolean::CFBoolean;
use core_foundation::dictionary::CFDictionary;
use core_foundation::number::CFNumber;
use core_foundation::string::CFString;
use core_foundation_sys::base::kCFAllocatorDefault;
use core_foundation_sys::dictionary::CFMutableDictionaryRef;
use io_kit_sys::types::{io_object_t, io_registry_entry_t};
use io_kit_sys::{
kIOMasterPortDefault, IOIteratorNext, IOObjectCopyClass, IOObjectRelease,
IORegistryEntryCreateCFProperties, IOServiceGetMatchingServices, IOServiceMatching,
};
const KERN_SUCCESS: i32 = 0;
#[derive(Debug, Clone, PartialEq, Eq)]
struct RawMedia {
bsd: String,
size: u64,
block: u32,
base: u64,
whole: bool,
removable: bool,
writable: bool,
content: String,
class: String,
}
pub(super) fn enumerate() -> Result<Vec<PhysicalDisk>, Error> {
Ok(assemble(&collect_media()?))
}
fn assemble(media: &[RawMedia]) -> Vec<PhysicalDisk> {
let mut disks: Vec<PhysicalDisk> = media
.iter()
.filter(|m| m.whole)
.map(|m| PhysicalDisk {
device_path: format!("/dev/{}", m.bsd),
name: m.bsd.clone(),
size_bytes: m.size,
logical_sector_size: m.block,
physical_sector_size: m.block,
model: None,
serial: None,
removable: m.removable,
read_only: !m.writable,
synthesized: m.class.starts_with("AppleAPFS"),
partitions: Vec::new(),
})
.collect();
for m in media.iter().filter(|m| !m.whole) {
let parent = whole_disk_of(&m.bsd);
if let Some(d) = disks.iter_mut().find(|d| d.name == parent) {
d.partitions.push(Partition {
device_path: format!("/dev/{}", m.bsd),
name: m.bsd.clone(),
start_offset: m.base,
size_bytes: m.size,
partition_type: (!m.content.is_empty()).then(|| m.content.clone()),
mount_point: None,
filesystem: None,
label: None,
});
}
}
for d in &mut disks {
d.partitions.sort_by_key(|p| p.start_offset);
}
disks.sort_by_key(|d| disk_index(&d.name));
disks
}
fn whole_disk_of(bsd: &str) -> String {
let rest = bsd.strip_prefix("disk").unwrap_or(bsd);
let num: String = rest.chars().take_while(char::is_ascii_digit).collect();
format!("disk{num}")
}
fn disk_index(name: &str) -> u64 {
name.strip_prefix("disk")
.and_then(|n| n.parse().ok())
.unwrap_or(u64::MAX)
}
fn collect_media() -> Result<Vec<RawMedia>, Error> {
let class = CString::new("IOMedia").expect("static string has no NUL");
let mut out = Vec::new();
unsafe {
let matching = IOServiceMatching(class.as_ptr());
if matching.is_null() {
return Err(Error::Os("IOServiceMatching(IOMedia) returned null".into()));
}
let mut iter: io_object_t = 0;
let kr = IOServiceGetMatchingServices(kIOMasterPortDefault, matching.cast(), &raw mut iter);
if kr != KERN_SUCCESS {
return Err(Error::Os(format!(
"IOServiceGetMatchingServices failed (kern_return {kr})"
)));
}
loop {
let entry = IOIteratorNext(iter);
if entry == 0 {
break;
}
if let Some(m) = read_media(entry) {
out.push(m);
}
IOObjectRelease(entry);
}
IOObjectRelease(iter);
}
Ok(out)
}
unsafe fn read_media(entry: io_registry_entry_t) -> Option<RawMedia> {
let mut props: CFMutableDictionaryRef = core::ptr::null_mut();
let kr = IORegistryEntryCreateCFProperties(entry, &raw mut props, kCFAllocatorDefault, 0);
if kr != KERN_SUCCESS || props.is_null() {
return None;
}
let dict: CFDictionary<CFString, CFType> =
CFDictionary::wrap_under_create_rule(props.cast_const());
let bsd = dict_string(&dict, "BSD Name")?;
let class_ref = IOObjectCopyClass(entry);
let class = if class_ref.is_null() {
String::new()
} else {
CFString::wrap_under_create_rule(class_ref).to_string()
};
Some(RawMedia {
bsd,
size: dict_i64(&dict, "Size").unwrap_or(0).max(0) as u64,
block: dict_i64(&dict, "Preferred Block Size")
.unwrap_or(512)
.max(1) as u32,
base: dict_i64(&dict, "Base").unwrap_or(0).max(0) as u64,
whole: dict_bool(&dict, "Whole").unwrap_or(false),
removable: dict_bool(&dict, "Removable").unwrap_or(false),
writable: dict_bool(&dict, "Writable").unwrap_or(true),
content: dict_string(&dict, "Content").unwrap_or_default(),
class,
})
}
fn dict_i64(d: &CFDictionary<CFString, CFType>, key: &str) -> Option<i64> {
d.find(CFString::new(key))
.and_then(|v| v.downcast::<CFNumber>())
.and_then(|n| n.to_i64())
}
fn dict_bool(d: &CFDictionary<CFString, CFType>, key: &str) -> Option<bool> {
d.find(CFString::new(key))
.and_then(|v| v.downcast::<CFBoolean>())
.map(bool::from)
}
fn dict_string(d: &CFDictionary<CFString, CFType>, key: &str) -> Option<String> {
d.find(CFString::new(key))
.and_then(|v| v.downcast::<CFString>())
.map(|s| s.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
fn whole(bsd: &str, size: u64, removable: bool, class: &str) -> RawMedia {
RawMedia {
bsd: bsd.into(),
size,
block: 512,
base: 0,
whole: true,
removable,
writable: true,
content: String::new(),
class: class.into(),
}
}
fn part(bsd: &str, base: u64, size: u64, content: &str) -> RawMedia {
RawMedia {
bsd: bsd.into(),
size,
block: 512,
base,
whole: false,
removable: false,
writable: true,
content: content.into(),
class: "IOMedia".into(),
}
}
#[test]
fn assemble_groups_partitions_under_whole_disks() {
let media = vec![
part("disk0s2", 600_000_000, 3_990_000_000_000, "Apple_APFS"),
whole("disk5", 8_000_000_000_000, true, "IOMedia"),
part("disk0s1", 20480, 524_300_000, "Apple_APFS_ISC"),
whole("disk0", 4_000_000_000_000, false, "IOMedia"),
part("disk5s1", 20480, 8_000_000_000_000, "Microsoft Basic Data"),
part(
"disk0s3",
3_990_600_000_000,
5_400_000_000,
"Apple_APFS_Recovery",
),
whole("disk4", 2_000_000_000_000, true, "IOMedia"),
];
let disks = assemble(&media);
assert_eq!(
disks.iter().map(|d| d.name.as_str()).collect::<Vec<_>>(),
["disk0", "disk4", "disk5"]
);
let disk0 = &disks[0];
assert_eq!(disk0.partitions.len(), 3);
assert_eq!(
disk0
.partitions
.iter()
.map(|p| p.name.as_str())
.collect::<Vec<_>>(),
["disk0s1", "disk0s2", "disk0s3"]
);
assert_eq!(
disk0.partitions[0].partition_type.as_deref(),
Some("Apple_APFS_ISC")
);
assert_eq!(disk0.device_path, "/dev/disk0");
assert!(!disk0.removable);
assert!(disks[1].partitions.is_empty());
assert!(disks[1].removable);
assert_eq!(disks[2].partitions.len(), 1);
}
#[test]
fn assemble_flags_synthesized_apfs_disks() {
let media = vec![
whole("disk0", 4_000_000_000_000, false, "IOMedia"),
whole("disk3", 4_000_000_000_000, false, "AppleAPFSMedia"),
];
let disks = assemble(&media);
assert!(!disks[0].synthesized, "physical disk0");
assert!(disks[1].synthesized, "synthesized disk3");
}
#[test]
fn assemble_read_only_from_writable_flag() {
let mut m = whole("disk9", 1_000_000, false, "IOMedia");
m.writable = false;
let disks = assemble(&[m]);
assert!(disks[0].read_only);
}
#[test]
fn whole_disk_of_strips_partition_suffix() {
assert_eq!(whole_disk_of("disk0s2"), "disk0");
assert_eq!(whole_disk_of("disk3s3s1"), "disk3");
assert_eq!(whole_disk_of("disk10s1"), "disk10");
assert_eq!(whole_disk_of("disk7"), "disk7");
}
#[test]
fn empty_content_becomes_none_partition_type() {
let disks = assemble(&[
whole("disk0", 100, false, "IOMedia"),
part("disk0s1", 0, 100, ""),
]);
assert_eq!(disks[0].partitions[0].partition_type, None);
}
}