From fe11e0d38f144fea52d730605c3bab3776d646ac Mon Sep 17 00:00:00 2001 From: Seva Zaikov Date: Sun, 3 May 2026 04:34:22 -0700 Subject: [PATCH] enable list_drives() method on macOS --- build.rs | 2 + examples/list_drives.rs | 2 - src/discovery.rs | 10 +-- src/mac/list_drives.c | 146 ++++++++++++++++++++++++++++++++++++++++ src/mac/shim_common.h | 9 +++ src/macos.rs | 54 ++++++++++++++- 6 files changed, 212 insertions(+), 11 deletions(-) create mode 100644 src/mac/list_drives.c diff --git a/build.rs b/build.rs index bb29751..bb9d949 100644 --- a/build.rs +++ b/build.rs @@ -4,6 +4,7 @@ fn main() { println!("cargo:rerun-if-changed=src/mac/shim_common.h"); println!("cargo:rerun-if-changed=src/mac/da_guard.c"); println!("cargo:rerun-if-changed=src/mac/device_service.c"); + println!("cargo:rerun-if-changed=src/mac/list_drives.c"); println!("cargo:rerun-if-changed=src/mac/toc_reader.c"); println!("cargo:rerun-if-changed=src/mac/audio_reader.c"); @@ -13,6 +14,7 @@ fn main() { cc::Build::new() .file("src/mac/da_guard.c") .file("src/mac/device_service.c") + .file("src/mac/list_drives.c") .file("src/mac/toc_reader.c") .file("src/mac/audio_reader.c") .include("src/mac") diff --git a/examples/list_drives.rs b/examples/list_drives.rs index eaf1919..5c8dcb3 100644 --- a/examples/list_drives.rs +++ b/examples/list_drives.rs @@ -1,6 +1,4 @@ /// Lists all optical drives detected on the system and whether they contain an audio CD. -/// -/// Note: this does not work on macOS — use `open_default` or `open` with a known path instead. use cd_da_reader::CdReader; fn main() -> Result<(), Box> { diff --git a/src/discovery.rs b/src/discovery.rs index 321298f..9b70038 100644 --- a/src/discovery.rs +++ b/src/discovery.rs @@ -16,15 +16,11 @@ pub struct DriveInfo { impl CdReader { /// Enumerate candidate optical drives and probe whether they currently have an audio CD. /// - /// This method does not work on macOS due to the fact for reliable confirmation - /// whether the drive has an Audio CD we need to mount it and later release; macOS - /// can assign a different drive name afterwards, so reading that name is unreliable. - /// Instead, use open_drive(drive) or open_default(), which acquires the exclusivity + /// On macOS, this uses the `IOCDMedia` objects published in the I/O Registry and + /// inspects their TOC property without claiming exclusive access. #[cfg(target_os = "macos")] pub fn list_drives() -> Result, CdReaderError> { - Err(CdReaderError::Io(std::io::Error::other( - "list_drives is not reliable on macOS due to remount/re-enumeration; use open_default instead or open a disk directly using CDReader::open", - ))) + crate::macos::list_drives().map_err(CdReaderError::Io) } /// Enumerate candidate optical drives and probe whether they currently have an audio CD. diff --git a/src/mac/list_drives.c b/src/mac/list_drives.c new file mode 100644 index 0000000..42d49e5 --- /dev/null +++ b/src/mac/list_drives.c @@ -0,0 +1,146 @@ +#include "shim_common.h" + +static bool copy_bsd_name(io_service_t media, char *outName, size_t outNameLen) { + if (!outName || outNameLen == 0) { + return false; + } + + CFTypeRef bsd = IORegistryEntryCreateCFProperty( + media, + CFSTR(kIOBSDNameKey), + kCFAllocatorDefault, + 0 + ); + if (!bsd) { + return false; + } + + bool ok = false; + if (CFGetTypeID(bsd) == CFStringGetTypeID()) { + ok = CFStringGetCString((CFStringRef)bsd, outName, outNameLen, kCFStringEncodingUTF8); + } + + CFRelease(bsd); + return ok; +} + +static bool toc_has_audio_track(CFDataRef tocData) { + if (!tocData) { + return false; + } + + CFIndex len = CFDataGetLength(tocData); + if (len < (CFIndex)sizeof(CDTOC)) { + return false; + } + + CDTOC *toc = (CDTOC *)CFDataGetBytePtr(tocData); + uint16_t tocLength = OSSwapBigToHostInt16(toc->length); + size_t tocSize = (size_t)tocLength + sizeof(toc->length); + if (tocSize > (size_t)len) { + return false; + } + + UInt32 count = CDTOCGetDescriptorCount(toc); + for (UInt32 i = 0; i < count; i++) { + CDTOCDescriptor *desc = &toc->descriptors[i]; + if (desc->adr != 1) { + continue; + } + + // Format 0x02 includes A0/A1/A2 metadata descriptors; real tracks are 1..99. + if (desc->point < 1 || desc->point > 99) { + continue; + } + + if ((desc->control & 0x04) == 0) { + return true; + } + } + + return false; +} + +static bool inspect_toc(io_service_t media, uint8_t *hasToc, uint8_t *hasAudio) { + if (hasToc) { + *hasToc = 0; + } + if (hasAudio) { + *hasAudio = 0; + } + + CFTypeRef toc = IORegistryEntryCreateCFProperty( + media, + CFSTR(kIOCDMediaTOCKey), + kCFAllocatorDefault, + 0 + ); + if (!toc) { + return true; + } + + if (CFGetTypeID(toc) == CFDataGetTypeID()) { + if (hasToc) { + *hasToc = 1; + } + if (hasAudio && toc_has_audio_track((CFDataRef)toc)) { + *hasAudio = 1; + } + } + + CFRelease(toc); + return true; +} + +bool list_cd_drives(CdDriveInfo **outDrives, uint32_t *outCount) { + if (!outDrives || !outCount) { + return false; + } + + *outDrives = NULL; + *outCount = 0; + + CFMutableDictionaryRef match = IOServiceMatching(kIOCDMediaClass); + if (!match) { + return false; + } + + io_iterator_t it = IO_OBJECT_NULL; + kern_return_t kr = IOServiceGetMatchingServices(kIOMainPortDefault, match, &it); + if (kr != KERN_SUCCESS) { + return false; + } + + CdDriveInfo *drives = NULL; + uint32_t count = 0; + io_service_t media; + + while ((media = IOIteratorNext(it))) { + CdDriveInfo info; + memset(&info, 0, sizeof(info)); + + if (copy_bsd_name(media, info.bsd_name, sizeof(info.bsd_name))) { + inspect_toc(media, &info.has_toc, &info.has_audio); + + CdDriveInfo *next = realloc(drives, (count + 1) * sizeof(CdDriveInfo)); + if (!next) { + IOObjectRelease(media); + free(drives); + IOObjectRelease(it); + return false; + } + + drives = next; + drives[count] = info; + count++; + } + + IOObjectRelease(media); + } + + IOObjectRelease(it); + + *outDrives = drives; + *outCount = count; + return true; +} diff --git a/src/mac/shim_common.h b/src/mac/shim_common.h index 09217cc..afbda25 100644 --- a/src/mac/shim_common.h +++ b/src/mac/shim_common.h @@ -6,6 +6,7 @@ #import #import #import +#import #import #import #include @@ -33,6 +34,12 @@ typedef struct { uint32_t task_status; } CdScsiError; +typedef struct { + char bsd_name[64]; + uint8_t has_toc; + uint8_t has_audio; +} CdDriveInfo; + extern DASessionRef g_session; extern DAGuardCtx g_guard; extern io_service_t globalDevSvc; @@ -47,6 +54,8 @@ bool cd_read_toc(uint8_t **outBuf, uint32_t *outLen, CdScsiError *outErr); bool read_cd_audio(uint32_t lba, uint32_t sectors, uint8_t **outBuf, uint32_t *outLen, CdScsiError *outErr); void cd_free(void *p); +bool list_cd_drives(CdDriveInfo **outDrives, uint32_t *outCount); + Boolean get_dev_svc(const char *bsdName); void reset_dev_scv(void); Boolean open_dev_session(const char *bsdName); diff --git a/src/macos.rs b/src/macos.rs index e89d165..7e886e0 100644 --- a/src/macos.rs +++ b/src/macos.rs @@ -1,10 +1,11 @@ -use std::{ffi::CString, ptr, slice}; +use std::ffi::{CStr, CString}; use std::{io, process::Command}; +use std::{ptr, slice}; use std::{thread::sleep, time::Duration}; use crate::parse_toc::parse_toc; use crate::utils::get_track_bounds; -use crate::{CdReaderError, RetryConfig, ScsiError, ScsiOp, Toc}; +use crate::{CdReaderError, DriveInfo, RetryConfig, ScsiError, ScsiOp, Toc}; #[repr(C)] #[derive(Debug, Default, Clone, Copy)] @@ -19,6 +20,14 @@ struct MacScsiError { task_status: u32, } +#[repr(C)] +#[derive(Debug, Clone, Copy)] +struct MacDriveInfo { + bsd_name: [libc::c_char; 64], + has_toc: u8, + has_audio: u8, +} + #[link(name = "macos_cd_shim", kind = "static")] unsafe extern "C" { fn start_da_guard(bsd_name: *const libc::c_char); @@ -32,6 +41,7 @@ unsafe extern "C" { out_err: *mut MacScsiError, ) -> bool; fn cd_free(p: *mut libc::c_void); + fn list_cd_drives(out_drives: *mut *mut MacDriveInfo, out_count: *mut u32) -> bool; fn open_dev_session(bsd_name: *const libc::c_char) -> bool; fn close_dev_session(); } @@ -70,6 +80,46 @@ pub fn list_drive_paths() -> io::Result> { Ok(paths) } +pub fn list_drives() -> io::Result> { + let mut raw_drives: *mut MacDriveInfo = ptr::null_mut(); + let mut count: u32 = 0; + + let ok = unsafe { list_cd_drives(&mut raw_drives, &mut count) }; + if !ok { + return Err(io::Error::other("could not enumerate CD drives")); + } + + let drives = if raw_drives.is_null() || count == 0 { + Vec::new() + } else { + let raw = unsafe { slice::from_raw_parts(raw_drives, count as usize) }; + let mut drives = Vec::with_capacity(raw.len()); + + for drive in raw { + let path = unsafe { CStr::from_ptr(drive.bsd_name.as_ptr()) } + .to_string_lossy() + .into_owned(); + if path.is_empty() { + continue; + } + + drives.push(DriveInfo { + display_name: Some(path.clone()), + path, + has_audio_cd: drive.has_audio != 0, + }); + } + + drives.sort_by(|a, b| a.path.cmp(&b.path)); + drives.dedup_by(|a, b| a.path == b.path); + drives + }; + + unsafe { cd_free(raw_drives as *mut _) }; + + Ok(drives) +} + pub fn open_drive(path: &str) -> std::io::Result<()> { let bsd = CString::new(path).unwrap(); unsafe { start_da_guard(bsd.as_ptr()) };