Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand All @@ -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")
Expand Down
2 changes: 0 additions & 2 deletions examples/list_drives.rs
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error>> {
Expand Down
10 changes: 3 additions & 7 deletions src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<DriveInfo>, 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.
Expand Down
146 changes: 146 additions & 0 deletions src/mac/list_drives.c
Original file line number Diff line number Diff line change
@@ -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;
}
9 changes: 9 additions & 0 deletions src/mac/shim_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#import <IOKit/IOCFPlugIn.h>
#import <IOKit/IOBSD.h>
#import <IOKit/storage/IOCDMedia.h>
#import <IOKit/storage/IOCDTypes.h>
#import <IOKit/scsi/SCSITaskLib.h>
#import <IOKit/scsi/IOSCSIMultimediaCommandsDevice.h>
#include <DiskArbitration/DiskArbitration.h>
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down
54 changes: 52 additions & 2 deletions src/macos.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -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);
Expand All @@ -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();
}
Expand Down Expand Up @@ -70,6 +80,46 @@ pub fn list_drive_paths() -> io::Result<Vec<String>> {
Ok(paths)
}

pub fn list_drives() -> io::Result<Vec<DriveInfo>> {
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()) };
Expand Down
Loading