Skip to content
63 changes: 63 additions & 0 deletions src/data_reader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/// Sector read mode for the READ CD (0xBE) command.
///
/// Controls CDB byte 1 (Expected Sector Type) and byte 9 (Main Channel Selection)
/// to read different sector formats from the same READ CD command.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SectorReadMode {
/// Audio: 2352 bytes/sector, raw PCM.
/// CDB byte 1 = 0x00 (any type), byte 9 = 0x10 (user data).
Audio,
/// Data cooked: 2048 bytes/sector, user data only (no sync/header/EDC/ECC).
/// CDB byte 1 = 0x04 (Mode 1), byte 9 = 0x10 (user data).
DataCooked,
/// Data raw: 2352 bytes/sector with sync + header + user data + EDC/ECC.
/// CDB byte 1 = 0x04 (Mode 1), byte 9 = 0xF8 (sync + header + user data + EDC/ECC).
DataRaw,
}

impl SectorReadMode {
/// Bytes per sector for this read mode.
pub fn sector_size(&self) -> usize {
match self {
SectorReadMode::Audio => 2352,
SectorReadMode::DataCooked => 2048,
SectorReadMode::DataRaw => 2352,
}
}

/// CDB byte 1: Expected Sector Type field (bits 5-2).
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small correction: They live in bits 4-2

pub fn cdb_byte1(&self) -> u8 {
match self {
SectorReadMode::Audio => 0x00,
SectorReadMode::DataCooked => 0x04,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I found the spec: ref, and in the reference they say:

Image

So the mode 1 should be 010b << 2 = 01000b or 0x08. Unless I am missing something?

SectorReadMode::DataRaw => 0x04,
}
}

/// CDB byte 9: Main Channel Selection bits.
pub fn cdb_byte9(&self) -> u8 {
match self {
SectorReadMode::Audio => 0x10,
SectorReadMode::DataCooked => 0x10,
SectorReadMode::DataRaw => 0xF8,
}
}

/// Maximum sectors per single `READ CD` command.
///
/// This is the sole chunker for the blocking `read_track` /
/// `read_data_sectors` paths, which hand a whole track (tens of thousands
/// of sectors) straight to the read loop; the streaming API already limits
/// itself via `TrackStreamConfig::sectors_per_chunk`. The cap is not about
/// OS pass-through limits (modern SG_IO/SPTI handle far larger transfers)
/// but about optical-drive firmware and USB-bridge reliability: large
/// multi-sector `READ CD` requests are flaky across the zoo of drives. The
/// values keep each transfer around 64 KiB, matching the conventional
/// ~27-sector chunk used by cdparanoia/libcdio.
pub(crate) fn max_sectors_per_xfer(&self) -> u32 {
match self.sector_size() {
2048 => 32, // 32 * 2048 = 64 KiB
_ => 27, // 27 * 2352 ≈ 62 KiB
}
}
}
46 changes: 46 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,14 @@ mod macos;
#[cfg(target_os = "windows")]
mod windows;

pub mod data_reader;
mod discovery;
mod errors;
mod read_loop;
mod retry;
mod stream;
mod utils;
pub use data_reader::SectorReadMode;
pub use discovery::DriveInfo;
pub use errors::{CdReaderError, ScsiError, ScsiOp};
pub use retry::RetryConfig;
Expand Down Expand Up @@ -321,6 +324,49 @@ impl CdReader {
}
}

/// Read sectors in a specific mode (audio, data cooked, or data raw).
///
/// This uses the READ CD (0xBE) SCSI command with configurable sector type
/// and main channel flags, supporting audio, cooked data (2048 B/sector),
/// and raw data (2352 B/sector) reads.
pub fn read_data_sectors(
&self,
lba: u32,
count: u32,
mode: SectorReadMode,
cfg: &RetryConfig,
) -> Result<Vec<u8>, CdReaderError> {
self.read_sectors_with_mode(lba, count, &mode, cfg)
}

pub(crate) fn read_sectors_with_mode(
&self,
start_lba: u32,
sectors: u32,
mode: &SectorReadMode,
cfg: &RetryConfig,
) -> Result<Vec<u8>, CdReaderError> {
#[cfg(target_os = "windows")]
{
windows::read_sectors_with_mode(start_lba, sectors, mode, cfg)
}

#[cfg(target_os = "macos")]
{
macos::read_sectors_with_mode(start_lba, sectors, mode, cfg)
}

#[cfg(target_os = "linux")]
{
linux::read_sectors_with_mode(start_lba, sectors, mode, cfg)
}

#[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
{
compile_error!("Unsupported platform")
}
}

pub(crate) fn read_sectors_with_retry(
&self,
start_lba: u32,
Expand Down
90 changes: 16 additions & 74 deletions src/linux.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
use libc::{O_NONBLOCK, O_RDWR, c_uchar, c_void};
use std::cmp::min;
use std::ffi::CString;
use std::fs::File;
use std::io::Error;
use std::os::fd::{AsRawFd, FromRawFd};
use std::path::Path;
use std::thread::sleep;
use std::time::Duration;

use crate::Toc;
use crate::data_reader::SectorReadMode;
use crate::parse_toc::parse_toc;
use crate::utils::get_track_bounds;
use crate::{CdReaderError, RetryConfig, ScsiError, ScsiOp};
Expand Down Expand Up @@ -205,88 +203,40 @@ pub fn read_sectors_with_retry(
sectors: u32,
cfg: &RetryConfig,
) -> std::result::Result<Vec<u8>, CdReaderError> {
read_cd_audio_range(start_lba, sectors, cfg)
read_sectors_with_mode(start_lba, sectors, &SectorReadMode::Audio, cfg)
}

// --- READ CD (0xBE): read an arbitrary LBA range as CD-DA (2352 bytes/sector) ---
fn read_cd_audio_range(
pub fn read_sectors_with_mode(
start_lba: u32,
sectors: u32,
mode: &SectorReadMode,
cfg: &RetryConfig,
) -> std::result::Result<Vec<u8>, CdReaderError> {
// SCSI-2 defines reading data in 2352 bytes chunks
const SECTOR_BYTES: usize = 2352;

// read ~64 KBs per request
const MAX_SECTORS_PER_XFER: u32 = 27; // 27 * 2352 = 63,504 bytes

let total_bytes = (sectors as usize) * SECTOR_BYTES;
// allocate the entire necessary size from the beginning to avoid memory realloc
let mut out = Vec::<u8>::with_capacity(total_bytes);

let mut remaining = sectors;
let mut lba = start_lba;
let attempts_total = cfg.max_attempts.max(1);

while remaining > 0 {
let mut chunk_sectors = min(remaining, MAX_SECTORS_PER_XFER);
let min_chunk = cfg.min_sectors_per_read.max(1);
let mut backoff_ms = cfg.initial_backoff_ms;
let mut last_err: Option<CdReaderError> = None;

for attempt in 1..=attempts_total {
match read_cd_audio_chunk(lba, chunk_sectors) {
Ok(chunk) => {
out.extend_from_slice(&chunk);
lba += chunk_sectors;
remaining -= chunk_sectors;
last_err = None;
break;
}
Err(err) => {
last_err = Some(err);
if attempt == attempts_total {
break;
}
if cfg.reduce_chunk_on_retry && chunk_sectors > min_chunk {
chunk_sectors = next_chunk_size(chunk_sectors, min_chunk);
}
if backoff_ms > 0 {
sleep(Duration::from_millis(backoff_ms));
}
if cfg.max_backoff_ms > 0 {
backoff_ms = (backoff_ms.saturating_mul(2)).min(cfg.max_backoff_ms);
}
}
}
}
if let Some(err) = last_err {
return Err(err);
}
}

Ok(out)
crate::read_loop::read_sectors_chunked(start_lba, sectors, mode, cfg, |lba, chunk_sectors| {
read_cd_chunk(lba, chunk_sectors, mode)
})
}

fn read_cd_audio_chunk(lba: u32, this_sectors: u32) -> std::result::Result<Vec<u8>, CdReaderError> {
const SECTOR_BYTES: usize = 2352;
let mut chunk = vec![0u8; (this_sectors as usize) * SECTOR_BYTES];
fn read_cd_chunk(
lba: u32,
this_sectors: u32,
mode: &SectorReadMode,
) -> std::result::Result<Vec<u8>, CdReaderError> {
let sector_size = mode.sector_size();
let mut chunk = vec![0u8; (this_sectors as usize) * sector_size];
let mut sense = vec![0u8; 64];

// CDB: READ CD (0xBE), LBA addressing
let mut cdb = [0u8; 12];
cdb.fill(0);
cdb[0] = 0xBE; // READ CD
cdb[1] = mode.cdb_byte1();
cdb[2] = ((lba >> 24) & 0xFF) as u8;
cdb[3] = ((lba >> 16) & 0xFF) as u8;
cdb[4] = ((lba >> 8) & 0xFF) as u8;
cdb[5] = (lba & 0xFF) as u8;
cdb[6] = ((this_sectors >> 16) & 0xFF) as u8;
cdb[7] = ((this_sectors >> 8) & 0xFF) as u8;
cdb[8] = (this_sectors & 0xFF) as u8;
cdb[9] = 0x10;
cdb[10] = 0x00;
cdb[11] = 0x00;
cdb[9] = mode.cdb_byte9();

let mut hdr = SgIoHeader {
interface_id: 'S' as i32,
Expand Down Expand Up @@ -346,11 +296,3 @@ fn parse_sense(sense: &[u8], sb_len_wr: u8) -> (Option<u8>, Option<u8>, Option<u
let ascq = Some(sense[13]);
(sense_key, asc, ascq)
}

fn next_chunk_size(current: u32, min_chunk: u32) -> u32 {
if current > 8 {
8.max(min_chunk)
} else {
min_chunk
}
}
53 changes: 47 additions & 6 deletions src/mac/audio_reader.c
Original file line number Diff line number Diff line change
@@ -1,19 +1,60 @@
#include "shim_common.h"

bool read_cd_audio(uint32_t lba, uint32_t sectors, uint8_t **outBuf, uint32_t *outLen, CdScsiError *outErr) {
// Map our SectorReadMode discriminant to the macOS CD sector area/type and the
// resulting bytes-per-sector. Keeping the mapping here (rather than in Rust)
// means the IOKit constants only ever appear where their header is imported.
//
// 0 = Audio -> user area, CDDA, 2352 B/sector
// 1 = DataCooked -> user area, Mode 1, 2048 B/sector
// 2 = DataRaw -> sync+header+user+aux, Mode 1, 2352 B/sector
static bool sector_layout_for_mode(uint32_t mode_id,
CDSectorArea *outArea,
CDSectorType *outType,
uint32_t *outSectorSize) {
switch (mode_id) {
case 0:
*outArea = kCDSectorAreaUser;
*outType = kCDSectorTypeCDDA;
*outSectorSize = 2352;
return true;
case 1:
*outArea = kCDSectorAreaUser;
*outType = kCDSectorTypeMode1;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can see in the docs: ref -- this value is 0x02. If we shift it, we'll get 0x02 << 2 == 0x08. So from my understanding Mode 1 should have value 0x08 in the Rust version as well

*outSectorSize = 2048;
return true;
case 2:
*outArea = (CDSectorArea)(kCDSectorAreaSync | kCDSectorAreaHeader |
kCDSectorAreaUser | kCDSectorAreaAuxiliary);
*outType = kCDSectorTypeMode1;
*outSectorSize = 2352;
return true;
default:
return false;
}
}

bool read_cd_sectors(uint32_t lba, uint32_t sectors, uint32_t mode_id,
uint8_t **outBuf, uint32_t *outLen, CdScsiError *outErr) {
*outBuf = NULL;
*outLen = 0;
if (outErr) {
memset(outErr, 0, sizeof(CdScsiError));
}

const uint32_t SECTOR_SZ = 2352;
CDSectorArea sectorArea;
CDSectorType sectorType;
uint32_t sectorSize;
if (!sector_layout_for_mode(mode_id, &sectorArea, &sectorType, &sectorSize)) {
fprintf(stderr, "[READ] unknown sector mode %u\n", mode_id);
goto fail;
}

if (sectors == 0) {
fprintf(stderr, "[READ] sectors == 0\n");
goto fail;
}

uint64_t totalBytes64 = (uint64_t)SECTOR_SZ * (uint64_t)sectors;
uint64_t totalBytes64 = (uint64_t)sectorSize * (uint64_t)sectors;
if (totalBytes64 > UINT32_MAX) {
fprintf(stderr, "[READ] requested size too large\n");
goto fail;
Expand All @@ -34,9 +75,9 @@ bool read_cd_audio(uint32_t lba, uint32_t sectors, uint8_t **outBuf, uint32_t *o
}

dk_cd_read_t read = {0};
read.offset = (uint64_t)lba * (uint64_t)SECTOR_SZ;
read.sectorArea = kCDSectorAreaUser;
read.sectorType = kCDSectorTypeCDDA;
read.offset = (uint64_t)lba * (uint64_t)sectorSize;
read.sectorArea = sectorArea;
read.sectorType = sectorType;
read.bufferLength = totalBytes;
read.buffer = dst;

Expand Down
15 changes: 12 additions & 3 deletions src/mac/device_service.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,25 @@ int open_cd_raw_device(void) {
return open(path, O_RDONLY | O_NONBLOCK);
}

// Reading the TOC and sector data both go through the read-only
// DKIOCCDREAD/DKIOCCDREADTOC ioctls on the raw BSD device, so opening a
// "session" is just remembering which device to open. This avoids the
// SCSITaskDeviceInterface path, which requires exclusive access and forces
// the volume to unmount. We validate the name by opening the raw device once.
Boolean open_dev_session(const char *bsdName) {
if (!bsdName || bsdName[0] == '\0') {
return false;
}

if (globalBsdName[0] != '\0') {
return strcmp(globalBsdName, bsdName) == 0;
snprintf(globalBsdName, sizeof(globalBsdName), "%s", bsdName);

int fd = open_cd_raw_device();
if (fd < 0) {
globalBsdName[0] = '\0';
return false;
}
close(fd);

snprintf(globalBsdName, sizeof(globalBsdName), "%s", bsdName);
return true;
}

Expand Down
2 changes: 1 addition & 1 deletion src/mac/shim_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ typedef struct {
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);
bool read_cd_sectors(uint32_t lba, uint32_t sectors, uint32_t mode_id, uint8_t **outBuf, uint32_t *outLen, CdScsiError *outErr);
void cd_free(void *p);

bool list_cd_drives(CdDriveInfo **outDrives, uint32_t *outCount);
Expand Down
Loading
Loading