From d70cda431a258c00bcf3a4485ded3cbcac231be8 Mon Sep 17 00:00:00 2001 From: Seva Zaikov Date: Sun, 3 May 2026 05:43:12 -0700 Subject: [PATCH] read track and toc data on macos with no exclusivity --- README.md | 11 +- build.rs | 3 - src/discovery.rs | 38 +++---- src/lib.rs | 25 ++--- src/mac/audio_reader.c | 130 +++++------------------- src/mac/da_guard.c | 84 ---------------- src/mac/device_service.c | 211 ++++----------------------------------- src/mac/shim_common.h | 27 ++--- src/mac/toc_reader.c | 188 +++++++++++++++++++--------------- src/macos.rs | 43 +------- src/stream.rs | 3 +- 11 files changed, 185 insertions(+), 578 deletions(-) delete mode 100644 src/mac/da_guard.c diff --git a/README.md b/README.md index 608ba5a..50cf031 100644 --- a/README.md +++ b/README.md @@ -31,20 +31,13 @@ use cd_da_reader::{CdReader}; let drives = CdReader::list_drives()?; ``` -This will give you a vector of drives, and the struct will have `has_audio_cd` field for audio CDs. Unfortunately, this does not work on macOS due to how CD drive handles are treated. When we execute any command to a CD drive (which we need to check whether the CD is audio or not), we need to claim exclusivity, which will cause it to unmount. If we release the handle, it will cause it to remount, and that will do 2 things: +This will give you a vector of drives, and the struct will have `has_audio_cd` field for audio CDs. -1. call the default application for an audio CD (probably Apple Music) -2. that app will claim exclusivity, so we won't be able to get it back for some time - -Because of that, on macOS you should either provide the name by yourself, or get the default drive: +If you already know the drive name, you can open it directly: ```rust use cd_da_reader::{CdReader}; -// get the default drive, which should be what you want -let reader = CdReader::open_default()?; - -// or read the disk directly let reader = CdReader::open("disk14")?; ``` diff --git a/build.rs b/build.rs index bb9d949..eabe876 100644 --- a/build.rs +++ b/build.rs @@ -2,7 +2,6 @@ fn main() { println!("cargo:rerun-if-changed=build.rs"); 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"); @@ -10,9 +9,7 @@ fn main() { println!("cargo:rustc-link-lib=framework=IOKit"); println!("cargo:rustc-link-lib=framework=CoreFoundation"); - println!("cargo:rustc-link-lib=framework=DiskArbitration"); 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") diff --git a/src/discovery.rs b/src/discovery.rs index 9b70038..8e922f8 100644 --- a/src/discovery.rs +++ b/src/discovery.rs @@ -9,7 +9,7 @@ pub struct DriveInfo { pub path: String, /// Just the device name, without the full path for the OS pub display_name: Option, - /// We load the disc and issue a TOC command, which is supported only on media CDs + /// Whether the current disc appears to contain at least one audio track. pub has_audio_cd: bool, } @@ -71,31 +71,23 @@ impl CdReader { /// Open the first discovered drive that currently has an audio CD. /// - /// On macOS, we open each drive returned from `diskutil list`, and - /// evaluate each disk. Once we are able to open it and read correct TOC, - /// we return it back with already acquired exclusivity. + /// On macOS, we use the passive `IOCDMedia` discovery path and then open + /// the matching BSD device name without claiming exclusive access. #[cfg(target_os = "macos")] pub fn open_default() -> Result { - let mut paths = crate::macos::list_drive_paths().map_err(CdReaderError::Io)?; - paths.sort(); - paths.dedup(); - - for path in paths { - let Ok(reader) = Self::open(&path) else { - continue; - }; - let Ok(toc) = reader.read_toc() else { - continue; - }; - if toc.tracks.iter().any(|track| track.is_audio) { - return Ok(reader); - } - } + let drives = Self::list_drives()?; + let chosen = drives + .iter() + .find(|drive| drive.has_audio_cd) + .map(|drive| drive.path.as_str()) + .ok_or_else(|| { + CdReaderError::Io(std::io::Error::new( + std::io::ErrorKind::NotFound, + "no usable audio CD drive found", + )) + })?; - Err(CdReaderError::Io(std::io::Error::new( - std::io::ErrorKind::NotFound, - "no usable audio CD drive found", - ))) + Self::open(chosen).map_err(CdReaderError::Io) } /// Open the first discovered drive that currently has an audio CD. diff --git a/src/lib.rs b/src/lib.rs index 4c7ce10..df63101 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,9 +3,9 @@ //! This library provides cross-platform audio CD reading capabilities //! (tested on Windows, macOS and Linux). It was written to enable CD ripping, //! but you can also implement a live audio CD player with its help. -//! The library works by issuing direct SCSI commands and abstracts both -//! access to the CD drive and reading the actual data from it, so you don't -//! deal with the hardware directly. +//! The library works through platform CD-drive APIs on macOS and issuing direct +//! SCSI commands on Windows and Linux and abstracts both access to the CD drive +//! and reading the actual data from it, so you don't deal with the hardware directly. //! //! All operations happen in this order: //! @@ -42,11 +42,6 @@ //! # Ok::<(), Box>(()) //! ``` //! -//! > **macOS note:** querying drives requires claiming exclusive access, which -//! > unmounts the disc. Releasing it triggers a remount that hands control to -//! > the default app (usually Apple Music). Use `open_default` or `open` with a -//! > known path instead of `list_drives` on macOS. -//! //! ## Reading ToC //! //! Each audio CD carries a Table of Contents with the block address of every @@ -203,12 +198,8 @@ pub struct Toc { /// directly, it implements `Drop` trait, so that the CD drive handle is properly closed. /// /// Please note that you should not read multiple CDs at the same time, and preferably do -/// not use it in multiple threads. CD drives are a physical thing and they really want to -/// have exclusive access, because of that currently only sequential access is supported. -/// -/// This is especially true on macOS, where releasing exclusive lock on the audio CD will -/// cause it to remount, and the default application (very likely Apple Music) will get -/// the exclusive access and it will be challenging to implement a reliable waiting strategy. +/// not use it in multiple threads. CD drives are physical devices, so currently only +/// sequential access is properly tested and supported. pub struct CdReader {} impl CdReader { @@ -217,10 +208,8 @@ impl CdReader { /// It is crucial to call this function and not to create the Reader /// by yourself, as each OS needs its own way of handling the drive access. /// - /// You don't need to close the drive, it will be handled automatically - /// when the `CdReader` is dropped. On macOS, that will cause the CD drive - /// to be remounted, and the default application (like Apple Music) will - /// be called. + /// You don't need to close the drive; it will be handled automatically + /// when the `CdReader` is dropped. /// /// # Arguments /// diff --git a/src/mac/audio_reader.c b/src/mac/audio_reader.c index 2cfae9b..92ab9f3 100644 --- a/src/mac/audio_reader.c +++ b/src/mac/audio_reader.c @@ -1,42 +1,5 @@ #include "shim_common.h" -static uint8_t task_status_to_scsi_status(SCSITaskStatus status) { - switch (status) { - case kSCSITaskStatus_GOOD: return 0x00; - case kSCSITaskStatus_CHECK_CONDITION: return 0x02; - case kSCSITaskStatus_BUSY: return 0x08; - case kSCSITaskStatus_RESERVATION_CONFLICT: return 0x18; - case kSCSITaskStatus_TASK_SET_FULL: return 0x28; - case kSCSITaskStatus_ACA_ACTIVE: return 0x30; - default: return 0xFF; - } -} - -static void fill_scsi_error(CdScsiError *outErr, kern_return_t ex, SCSITaskStatus status, SCSI_Sense_Data *sense) { - if (!outErr) return; - - outErr->has_scsi_error = 1; - outErr->exec_error = (uint32_t)ex; - outErr->task_status = (uint32_t)status; - outErr->scsi_status = task_status_to_scsi_status(status); - - const uint8_t *sense_bytes = (const uint8_t *)sense; - bool has_sense = false; - for (size_t i = 0; i < sizeof(SCSI_Sense_Data); i++) { - if (sense_bytes[i] != 0) { - has_sense = true; - break; - } - } - - outErr->has_sense = has_sense ? 1 : 0; - if (has_sense && sizeof(SCSI_Sense_Data) >= 14) { - outErr->sense_key = sense_bytes[2] & 0x0F; - outErr->asc = sense_bytes[12]; - outErr->ascq = sense_bytes[13]; - } -} - bool read_cd_audio(uint32_t lba, uint32_t sectors, uint8_t **outBuf, uint32_t *outLen, CdScsiError *outErr) { *outBuf = NULL; *outLen = 0; @@ -44,12 +7,6 @@ bool read_cd_audio(uint32_t lba, uint32_t sectors, uint8_t **outBuf, uint32_t *o memset(outErr, 0, sizeof(CdScsiError)); } - SCSITaskDeviceInterface **dev = globalDev; - if (!dev) { - fprintf(stderr, "[READ] Device session is not open\n"); - goto fail; - } - const uint32_t SECTOR_SZ = 2352; if (sectors == 0) { fprintf(stderr, "[READ] sectors == 0\n"); @@ -69,76 +26,37 @@ bool read_cd_audio(uint32_t lba, uint32_t sectors, uint8_t **outBuf, uint32_t *o goto fail; } - const uint32_t MAX_SECTORS_PER_CMD = 27; - - uint32_t remaining = sectors; - uint32_t curLBA = lba; - uint32_t written = 0; - - while (remaining > 0) { - uint32_t xfer = (remaining > MAX_SECTORS_PER_CMD) ? MAX_SECTORS_PER_CMD : remaining; - uint32_t bytes = xfer * SECTOR_SZ; - - // READ CD (0xBE) 12-byte CDB for CD-DA. - uint8_t cdb[12] = {0}; - cdb[0] = 0xBE; - cdb[1] = 0x00; - cdb[2] = (uint8_t)((curLBA >> 24) & 0xFF); - cdb[3] = (uint8_t)((curLBA >> 16) & 0xFF); - cdb[4] = (uint8_t)((curLBA >> 8) & 0xFF); - cdb[5] = (uint8_t)((curLBA >> 0) & 0xFF); - cdb[6] = (uint8_t)((xfer >> 16) & 0xFF); - cdb[7] = (uint8_t)((xfer >> 8) & 0xFF); - cdb[8] = (uint8_t)((xfer >> 0) & 0xFF); - cdb[9] = 0x10; // USER DATA only (2352 bytes/sector) - cdb[10] = 0x00; - cdb[11] = 0x00; - - SCSITaskInterface **task = (*dev)->CreateSCSITask(dev); - if (!task) { - fprintf(stderr, "[READ] CreateSCSITask failed\n"); - free(dst); - goto fail; - } - - IOVirtualRange vr = {0}; - vr.address = (IOVirtualAddress)(dst + written); - vr.length = bytes; - - if ((*task)->SetCommandDescriptorBlock(task, cdb, sizeof(cdb)) != kIOReturnSuccess) { - fprintf(stderr, "[READ] SetCommandDescriptorBlock failed\n"); - (*task)->Release(task); - free(dst); - goto fail; - } + int fd = open_cd_raw_device(); + if (fd < 0) { + fprintf(stderr, "[READ] open raw CD device failed (errno=%d)\n", errno); + free(dst); + goto fail; + } - // dir=2 means from device in SCSITaskLib. - if ((*task)->SetScatterGatherEntries(task, &vr, 1, bytes, 2) != kIOReturnSuccess) { - fprintf(stderr, "[READ] SetScatterGatherEntries failed\n"); - (*task)->Release(task); - free(dst); - goto fail; - } + dk_cd_read_t read = {0}; + read.offset = (uint64_t)lba * (uint64_t)SECTOR_SZ; + read.sectorArea = kCDSectorAreaUser; + read.sectorType = kCDSectorTypeCDDA; + read.bufferLength = totalBytes; + read.buffer = dst; - SCSI_Sense_Data sense = {0}; - SCSITaskStatus status = kSCSITaskStatus_No_Status; - kern_return_t ex = (*task)->ExecuteTaskSync(task, &sense, &status, NULL); - (*task)->Release(task); + int ret = ioctl(fd, DKIOCCDREAD, &read); + close(fd); - if (ex != kIOReturnSuccess || status != kSCSITaskStatus_GOOD) { - fill_scsi_error(outErr, ex, status, &sense); - fprintf(stderr, "[READ] ExecuteTaskSync failed (ex=0x%x, status=%u)\n", ex, status); - free(dst); - goto fail; - } + if (ret < 0) { + fprintf(stderr, "[READ] DKIOCCDREAD failed (errno=%d)\n", errno); + free(dst); + goto fail; + } - written += bytes; - curLBA += xfer; - remaining -= xfer; + if (read.bufferLength != totalBytes) { + fprintf(stderr, "[READ] short read: requested=%u actual=%u\n", totalBytes, read.bufferLength); + free(dst); + goto fail; } *outBuf = dst; - *outLen = written; + *outLen = totalBytes; return true; diff --git a/src/mac/da_guard.c b/src/mac/da_guard.c deleted file mode 100644 index 52afbbf..0000000 --- a/src/mac/da_guard.c +++ /dev/null @@ -1,84 +0,0 @@ -#include "shim_common.h" - -DASessionRef g_session = NULL; -DAGuardCtx g_guard = {0}; - -static Boolean disk_matches(DADiskRef disk, const char *bsdName) { - CFDictionaryRef desc = DADiskCopyDescription(disk); - if (!desc) return false; - - CFStringRef bsd = CFDictionaryGetValue(desc, kDADiskDescriptionMediaBSDNameKey); - char name[256] = {0}; - Boolean match = (bsd && CFStringGetCString(bsd, name, sizeof(name), kCFStringEncodingUTF8) - && strcmp(name, bsdName) == 0); - CFRelease(desc); - return match; -} - -// Mount-approval callback: veto mounts for our target disk while active. -static DADissenterRef mount_approval_cb(DADiskRef disk, void *context) { - DAGuardCtx *ctx = (DAGuardCtx *)context; - if (disk_matches(disk, ctx->bsdName)) { - return DADissenterCreate(kCFAllocatorDefault, kDAReturnNotPermitted, CFSTR("reserved by app")); - } - return NULL; -} - -// Unmount completion: signal our waiter. -static void unmount_cb(DADiskRef disk, DADissenterRef dissenter, void *context) { - DAGuardCtx *ctx = (DAGuardCtx *)context; - (void)disk; - (void)dissenter; - dispatch_semaphore_signal(ctx->sem); -} - -void start_da_guard(const char *bsdName) { - g_session = DASessionCreate(kCFAllocatorDefault); - if (!g_session) return; - - DASessionScheduleWithRunLoop(g_session, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); - - if (g_guard.bsdName) { - free((void *)g_guard.bsdName); - g_guard.bsdName = NULL; - } - g_guard.bsdName = strdup(bsdName); - g_guard.sem = dispatch_semaphore_create(0); - - // Veto remounts while we run. - DARegisterDiskMountApprovalCallback(g_session, NULL, mount_approval_cb, &g_guard); - - // Kick one unmount so the device is no longer busy. - char path[64]; - snprintf(path, sizeof(path), "/dev/%s", bsdName); - DADiskRef d = DADiskCreateFromBSDName(kCFAllocatorDefault, g_session, path); - if (!d) return; - - DADiskUnmount(d, kDADiskUnmountOptionDefault, unmount_cb, &g_guard); - - // Wait for unmount while pumping the run loop. - dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC); - while (dispatch_semaphore_wait(g_guard.sem, DISPATCH_TIME_NOW) != 0) { - CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, true); - if (dispatch_time(DISPATCH_TIME_NOW, 0) > timeout) { - printf("Unmount timeout!\n"); - break; - } - } - - CFRelease(d); -} - -void stop_da_guard(void) { - if (!g_session) return; - - DAUnregisterCallback(g_session, mount_approval_cb, &g_guard); - DASessionUnscheduleFromRunLoop(g_session, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); - CFRelease(g_session); - g_session = NULL; - - if (g_guard.bsdName) { - free((void *)g_guard.bsdName); - g_guard.bsdName = NULL; - } -} diff --git a/src/mac/device_service.c b/src/mac/device_service.c index 9f18610..700f9f9 100644 --- a/src/mac/device_service.c +++ b/src/mac/device_service.c @@ -1,211 +1,40 @@ #include "shim_common.h" -io_service_t globalDevSvc = IO_OBJECT_NULL; -IOCFPlugInInterface **globalPlugin = NULL; -MMCDeviceInterface **globalMmc = NULL; -SCSITaskDeviceInterface **globalDev = NULL; +char globalBsdName[64] = {0}; -static io_service_t find_media(const char *bsdName) { - io_iterator_t it = IO_OBJECT_NULL; - io_service_t svc = IO_OBJECT_NULL; - - CFMutableDictionaryRef match = IOBSDNameMatching(kIOMainPortDefault, 0, bsdName); - if (!match) { - printf("[ERROR] Failed at IOBSDNameMatching for %s\n", bsdName); - return IO_OBJECT_NULL; - } - - kern_return_t kr = IOServiceGetMatchingServices(kIOMainPortDefault, match, &it); - if (kr != KERN_SUCCESS) { - printf("[ERROR] Failed at IOServiceGetMatchingServices for %s (error: 0x%x)\n", bsdName, kr); - return IO_OBJECT_NULL; - } - - io_service_t cur; - while ((cur = IOIteratorNext(it))) { - if (IOObjectConformsTo(cur, kIOCDMediaClass)) { - svc = cur; - IOObjectRetain(svc); - IOObjectRelease(cur); - break; - } - - io_service_t node = cur; - IOObjectRetain(node); - bool found_cd_media = false; - - for (int parent_depth = 0; node && parent_depth < 10; parent_depth++) { - if (IOObjectConformsTo(node, kIOCDMediaClass)) { - svc = node; - IOObjectRetain(svc); - found_cd_media = true; - break; - } - - io_iterator_t pit = IO_OBJECT_NULL; - if (IORegistryEntryGetParentIterator(node, kIOServicePlane, &pit) != KERN_SUCCESS) { - break; - } - - io_service_t parent = IOIteratorNext(pit); - IOObjectRelease(pit); - IOObjectRelease(node); - node = parent; - } - - if (node) { - IOObjectRelease(node); - } - IOObjectRelease(cur); - - if (found_cd_media) { - break; - } - } - - IOObjectRelease(it); - return svc; -} - -static bool service_has_uc(io_service_t svc, CFUUIDRef userClientType) { - CFDictionaryRef d = IORegistryEntryCreateCFProperty( - svc, CFSTR("IOCFPlugInTypes"), kCFAllocatorDefault, 0); - if (!d) return false; - - CFStringRef want = CFUUIDCreateString(kCFAllocatorDefault, userClientType); - bool ok = CFDictionaryContainsKey(d, want); - CFRelease(want); - CFRelease(d); - return ok; -} - -// Climb until we find a node that lists the desired user client. -static io_service_t ascend_to_uc(io_service_t start, CFUUIDRef userClientType) { - io_service_t node = start; - IOObjectRetain(node); - - for (int depth = 0; node && depth < 32; depth++) { - if (service_has_uc(node, userClientType)) return node; - - io_registry_entry_t parent = MACH_PORT_NULL; - if (IORegistryEntryGetParentEntry(node, kIOServicePlane, &parent) != KERN_SUCCESS) { - break; - } - - IOObjectRelease(node); - node = (io_service_t)parent; - } - - if (node) IOObjectRelease(node); - return IO_OBJECT_NULL; -} - -Boolean get_dev_svc(const char *bsdName) { - // Do not allow grabbing another drive while one is open. - if (globalDevSvc) { - return false; +int open_cd_raw_device(void) { + if (globalBsdName[0] == '\0') { + errno = ENODEV; + return -1; } - io_service_t media = find_media(bsdName); - if (!media) { - fprintf(stderr, "[TOC] Could not find media for bsd\n"); - return false; - } - io_service_t devSvc = ascend_to_uc(media, kIOMMCDeviceUserClientTypeID); - IOObjectRelease(media); - - if (!devSvc) { - fprintf(stderr, "[TOC] Could not find mmc device for bsd\n"); - return false; + char path[96]; + if (strncmp(globalBsdName, "/dev/rdisk", 10) == 0) { + snprintf(path, sizeof(path), "%s", globalBsdName); + } else if (strncmp(globalBsdName, "/dev/disk", 9) == 0) { + snprintf(path, sizeof(path), "/dev/r%s", globalBsdName + 5); + } else if (strncmp(globalBsdName, "rdisk", 5) == 0) { + snprintf(path, sizeof(path), "/dev/%s", globalBsdName); + } else { + snprintf(path, sizeof(path), "/dev/r%s", globalBsdName); } - globalDevSvc = devSvc; - return true; -} - -void reset_dev_scv(void) { - if (globalDevSvc) { - IOObjectRelease(globalDevSvc); - } - globalDevSvc = IO_OBJECT_NULL; + return open(path, O_RDONLY | O_NONBLOCK); } Boolean open_dev_session(const char *bsdName) { - if (globalDev) { - return true; - } - - if (!globalDevSvc && !get_dev_svc(bsdName)) { - return false; - } - - SInt32 score = 0; - IOCFPlugInInterface **plugin = NULL; - kern_return_t kret = IOCreatePlugInInterfaceForService( - globalDevSvc, - kIOMMCDeviceUserClientTypeID, - kIOCFPlugInInterfaceID, - &plugin, - &score - ); - if (kret != kIOReturnSuccess || !plugin) { - fprintf(stderr, "[OPEN] IOCreatePlugInInterfaceForService failed: 0x%x\n", kret); + if (!bsdName || bsdName[0] == '\0') { return false; } - MMCDeviceInterface **mmc = NULL; - HRESULT hr = (*plugin)->QueryInterface( - plugin, - CFUUIDGetUUIDBytes(kIOMMCDeviceInterfaceID), - (LPVOID)&mmc - ); - if (hr != S_OK || !mmc) { - fprintf(stderr, "[OPEN] QueryInterface(kIOMMCDeviceInterfaceID) failed (hr=0x%lx)\n", (long)hr); - IODestroyPlugInInterface(plugin); - return false; - } - - SCSITaskDeviceInterface **dev = (*mmc)->GetSCSITaskDeviceInterface(mmc); - if (!dev) { - fprintf(stderr, "[OPEN] GetSCSITaskDeviceInterface failed\n"); - (*mmc)->Release(mmc); - IODestroyPlugInInterface(plugin); - return false; - } - - kret = (*dev)->ObtainExclusiveAccess(dev); - if (kret != kIOReturnSuccess) { - if (kret == kIOReturnBusy) { - fprintf(stderr, "[OPEN] Busy on obtaining exclusive access\n"); - } else { - fprintf(stderr, "[OPEN] ObtainExclusiveAccess error: 0x%x\n", kret); - } - (*mmc)->Release(mmc); - IODestroyPlugInInterface(plugin); - return false; + if (globalBsdName[0] != '\0') { + return strcmp(globalBsdName, bsdName) == 0; } - globalPlugin = plugin; - globalMmc = mmc; - globalDev = dev; + snprintf(globalBsdName, sizeof(globalBsdName), "%s", bsdName); return true; } void close_dev_session(void) { - if (globalDev) { - (*globalDev)->ReleaseExclusiveAccess(globalDev); - globalDev = NULL; - } - - if (globalMmc) { - (*globalMmc)->Release(globalMmc); - globalMmc = NULL; - } - - if (globalPlugin) { - IODestroyPlugInInterface(globalPlugin); - globalPlugin = NULL; - } - - reset_dev_scv(); + globalBsdName[0] = '\0'; } diff --git a/src/mac/shim_common.h b/src/mac/shim_common.h index afbda25..81bd712 100644 --- a/src/mac/shim_common.h +++ b/src/mac/shim_common.h @@ -3,25 +3,19 @@ #import #import -#import #import +#import #import #import -#import -#import -#include -#include #include +#include +#include #include #include #include #include - -typedef struct { - const char *bsdName; - dispatch_semaphore_t sem; -} DAGuardCtx; +#include typedef struct { uint8_t has_scsi_error; @@ -40,15 +34,7 @@ typedef struct { uint8_t has_audio; } CdDriveInfo; -extern DASessionRef g_session; -extern DAGuardCtx g_guard; -extern io_service_t globalDevSvc; -extern IOCFPlugInInterface **globalPlugin; -extern MMCDeviceInterface **globalMmc; -extern SCSITaskDeviceInterface **globalDev; - -void start_da_guard(const char *bsdName); -void stop_da_guard(void); +extern char globalBsdName[64]; 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); @@ -56,8 +42,7 @@ 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); +int open_cd_raw_device(void); Boolean open_dev_session(const char *bsdName); void close_dev_session(void); diff --git a/src/mac/toc_reader.c b/src/mac/toc_reader.c index 38c82eb..e864672 100644 --- a/src/mac/toc_reader.c +++ b/src/mac/toc_reader.c @@ -1,111 +1,139 @@ #include "shim_common.h" -static uint8_t task_status_to_scsi_status(SCSITaskStatus status) { - switch (status) { - case kSCSITaskStatus_GOOD: return 0x00; - case kSCSITaskStatus_CHECK_CONDITION: return 0x02; - case kSCSITaskStatus_BUSY: return 0x08; - case kSCSITaskStatus_RESERVATION_CONFLICT: return 0x18; - case kSCSITaskStatus_TASK_SET_FULL: return 0x28; - case kSCSITaskStatus_ACA_ACTIVE: return 0x30; - default: return 0xFF; +static Boolean read_toc(uint8_t **outBuf, uint32_t *outLen, CdScsiError *outErr) { + *outBuf = NULL; + *outLen = 0; + if (outErr) { + memset(outErr, 0, sizeof(CdScsiError)); } -} -static void fill_scsi_error(CdScsiError *outErr, kern_return_t ex, SCSITaskStatus status, SCSI_Sense_Data *sense) { - if (!outErr) return; + int fd = open_cd_raw_device(); + if (fd < 0) { + fprintf(stderr, "[TOC] open raw CD device failed (errno=%d)\n", errno); + goto fail; + } - outErr->has_scsi_error = 1; - outErr->exec_error = (uint32_t)ex; - outErr->task_status = (uint32_t)status; - outErr->scsi_status = task_status_to_scsi_status(status); + const uint16_t rawAlloc = 4096; + uint8_t *raw = malloc(rawAlloc); + if (!raw) { + close(fd); + fprintf(stderr, "[TOC] oom\n"); + goto fail; + } - const uint8_t *sense_bytes = (const uint8_t *)sense; - bool has_sense = false; - for (size_t i = 0; i < sizeof(SCSI_Sense_Data); i++) { - if (sense_bytes[i] != 0) { - has_sense = true; - break; - } + dk_cd_read_toc_t request = {0}; + request.format = kCDTOCFormatTOC; + request.formatAsTime = 1; + request.address.session = 0; + request.bufferLength = rawAlloc; + request.buffer = raw; + + int ret = ioctl(fd, DKIOCCDREADTOC, &request); + close(fd); + + if (ret < 0) { + fprintf(stderr, "[TOC] DKIOCCDREADTOC failed (errno=%d)\n", errno); + free(raw); + goto fail; } - outErr->has_sense = has_sense ? 1 : 0; - if (has_sense && sizeof(SCSI_Sense_Data) >= 14) { - outErr->sense_key = sense_bytes[2] & 0x0F; - outErr->asc = sense_bytes[12]; - outErr->ascq = sense_bytes[13]; + if (request.bufferLength < sizeof(CDTOC)) { + fprintf(stderr, "[TOC] returned TOC is too short\n"); + free(raw); + goto fail; } -} -static Boolean read_toc(uint8_t **outBuf, uint32_t *outLen, CdScsiError *outErr) { - *outBuf = NULL; - *outLen = 0; - if (outErr) { - memset(outErr, 0, sizeof(CdScsiError)); + CDTOC *toc = (CDTOC *)raw; + uint16_t tocLength = OSSwapBigToHostInt16(toc->length); + size_t tocSize = (size_t)tocLength + sizeof(toc->length); + if (tocSize > request.bufferLength) { + fprintf(stderr, "[TOC] returned TOC length is invalid\n"); + free(raw); + goto fail; } - SCSITaskDeviceInterface **dev = globalDev; - SCSITaskInterface **task = NULL; + CDTOCDescriptor *tracks[100] = {0}; + CDTOCDescriptor *leadout = NULL; + uint8_t firstTrack = 0; + uint8_t lastTrack = 0; + uint32_t trackCount = 0; + + UInt32 descriptorCount = CDTOCGetDescriptorCount(toc); + for (UInt32 i = 0; i < descriptorCount; i++) { + CDTOCDescriptor *desc = &toc->descriptors[i]; + if (desc->adr != 1) { + continue; + } - if (!dev) { - fprintf(stderr, "[TOC] Device session is not open\n"); - goto fail; + if (desc->point >= 1 && desc->point <= 99) { + if (!tracks[desc->point]) { + trackCount++; + } + tracks[desc->point] = desc; + if (firstTrack == 0 || desc->point < firstTrack) { + firstTrack = desc->point; + } + if (desc->point > lastTrack) { + lastTrack = desc->point; + } + } else if (desc->point == 0xA2) { + leadout = desc; + } } - task = (*dev)->CreateSCSITask(dev); - if (!task) { - fprintf(stderr, "[TOC] CreateSCSITask failed\n"); + if (trackCount == 0 || !leadout) { + fprintf(stderr, "[TOC] did not find track descriptors and leadout\n"); + free(raw); goto fail; } - const uint32_t alloc = 2048; - uint8_t cdb[10] = {0}; - cdb[0] = 0x43; // READ TOC/PMA/ATIP - cdb[1] = 0x00; // LBA format - cdb[2] = 0x00; // Format 0x00: TOC - cdb[6] = 0x00; // Starting track 0 = first track/session - cdb[7] = (alloc >> 8) & 0xFF; - cdb[8] = alloc & 0xFF; - - IOVirtualRange vr = {.address = 0, .length = 0}; - uint8_t *buf = malloc(alloc); + uint32_t outputDescriptors = trackCount + 1; + uint16_t outputTocLength = (uint16_t)(2 + outputDescriptors * 8); + uint32_t outputLen = (uint32_t)outputTocLength + sizeof(uint16_t); + uint8_t *buf = malloc(outputLen); if (!buf) { - fprintf(stderr, "oom\n"); - goto fail_task; + free(raw); + fprintf(stderr, "[TOC] oom\n"); + goto fail; } - vr.address = (IOVirtualAddress)buf; - vr.length = alloc; + memset(buf, 0, outputLen); + + buf[0] = (uint8_t)((outputTocLength >> 8) & 0xFF); + buf[1] = (uint8_t)(outputTocLength & 0xFF); + buf[2] = firstTrack; + buf[3] = lastTrack; + + uint32_t offset = 4; + for (uint8_t track = firstTrack; track <= lastTrack; track++) { + CDTOCDescriptor *desc = tracks[track]; + if (!desc) { + continue; + } - if ((*task)->SetCommandDescriptorBlock(task, cdb, sizeof(cdb)) != kIOReturnSuccess) { - fprintf(stderr, "SetCommandDescriptorBlock failed\n"); - goto fail_buf; + uint32_t lba = CDConvertMSFToLBA(desc->p); + buf[offset + 1] = (uint8_t)((desc->adr << 4) | desc->control); + buf[offset + 2] = track; + buf[offset + 4] = (uint8_t)((lba >> 24) & 0xFF); + buf[offset + 5] = (uint8_t)((lba >> 16) & 0xFF); + buf[offset + 6] = (uint8_t)((lba >> 8) & 0xFF); + buf[offset + 7] = (uint8_t)(lba & 0xFF); + offset += 8; } - // 0 = no data, 1 = to device, 2 = from device - if ((*task)->SetScatterGatherEntries(task, &vr, 1, alloc, 2) != kIOReturnSuccess) { - fprintf(stderr, "SetScatterGatherEntries failed\n"); - goto fail_buf; - } + uint32_t leadoutLba = CDConvertMSFToLBA(leadout->p); + buf[offset + 1] = (uint8_t)((leadout->adr << 4) | leadout->control); + buf[offset + 2] = 0xAA; + buf[offset + 4] = (uint8_t)((leadoutLba >> 24) & 0xFF); + buf[offset + 5] = (uint8_t)((leadoutLba >> 16) & 0xFF); + buf[offset + 6] = (uint8_t)((leadoutLba >> 8) & 0xFF); + buf[offset + 7] = (uint8_t)(leadoutLba & 0xFF); - SCSI_Sense_Data sense = {0}; - SCSITaskStatus status = kSCSITaskStatus_No_Status; - kern_return_t ex = (*task)->ExecuteTaskSync(task, &sense, &status, NULL); - if (ex != kIOReturnSuccess || status != kSCSITaskStatus_GOOD) { - fill_scsi_error(outErr, ex, status, &sense); - fprintf(stderr, "ExecuteTaskSync failed (status=%u)\n", status); - goto fail_buf; - } + free(raw); *outBuf = buf; - *outLen = alloc; - - (*task)->Release(task); + *outLen = outputLen; return true; -fail_buf: - free(buf); -fail_task: - if (task) (*task)->Release(task); fail: return false; } diff --git a/src/macos.rs b/src/macos.rs index 7e886e0..d66e6d9 100644 --- a/src/macos.rs +++ b/src/macos.rs @@ -1,5 +1,5 @@ use std::ffi::{CStr, CString}; -use std::{io, process::Command}; +use std::io; use std::{ptr, slice}; use std::{thread::sleep, time::Duration}; @@ -30,8 +30,6 @@ struct MacDriveInfo { #[link(name = "macos_cd_shim", kind = "static")] unsafe extern "C" { - fn start_da_guard(bsd_name: *const libc::c_char); - fn stop_da_guard(); fn cd_read_toc(out_buf: *mut *mut u8, out_len: *mut u32, out_err: *mut MacScsiError) -> bool; fn read_cd_audio( lba: u32, @@ -46,40 +44,6 @@ unsafe extern "C" { fn close_dev_session(); } -pub fn list_drive_paths() -> io::Result> { - let output = Command::new("diskutil").arg("list").output()?; - if !output.status.success() { - return Err(io::Error::other("diskutil list failed")); - } - - let mut paths = Vec::new(); - let mut current_disk: Option = None; - let stdout = String::from_utf8_lossy(&output.stdout); - for raw_line in stdout.lines() { - let line = raw_line.trim(); - - if let Some(rest) = line.strip_prefix("/dev/") { - let disk = rest.split_whitespace().next().unwrap_or_default(); - current_disk = if disk.starts_with("disk") { - Some(disk.to_string()) - } else { - None - }; - continue; - } - - if line.contains("CD_partition_scheme") - && let Some(disk) = current_disk.as_ref() - { - paths.push(disk.clone()); - } - } - - paths.sort(); - paths.dedup(); - Ok(paths) -} - pub fn list_drives() -> io::Result> { let mut raw_drives: *mut MacDriveInfo = ptr::null_mut(); let mut count: u32 = 0; @@ -122,11 +86,9 @@ pub fn list_drives() -> io::Result> { pub fn open_drive(path: &str) -> std::io::Result<()> { let bsd = CString::new(path).unwrap(); - unsafe { start_da_guard(bsd.as_ptr()) }; let result = unsafe { open_dev_session(bsd.as_ptr()) }; if !result { - unsafe { stop_da_guard() }; return Err(std::io::Error::other("could not get device")); } @@ -135,7 +97,6 @@ pub fn open_drive(path: &str) -> std::io::Result<()> { pub fn close_drive() { unsafe { close_dev_session() }; - unsafe { stop_da_guard() }; } pub fn read_toc() -> Result { @@ -259,7 +220,7 @@ fn map_mac_error( }); } - CdReaderError::Io(std::io::Error::other("macOS SCSI command failed")) + CdReaderError::Io(std::io::Error::other("macOS CD command failed")) } fn next_chunk_size(current: u32, min_chunk: u32) -> u32 { diff --git a/src/stream.rs b/src/stream.rs index d3fcca4..b03fd6c 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -132,8 +132,7 @@ impl<'a> TrackStream<'a> { impl CdReader { /// Open a streaming reader for a specific track in the provided TOC. /// It is important to create track streams through this method so the - /// lifetime for the drive exclusive access is managed through a single - /// CDReader instance. + /// drive session is managed through a single CDReader instance. /// /// Use `TrackStream::next_chunk` to pull sector-aligned PCM chunks. pub fn open_track_stream<'a>(