diff --git a/src/data_reader.rs b/src/data_reader.rs new file mode 100644 index 0000000..f2435c1 --- /dev/null +++ b/src/data_reader.rs @@ -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). + pub fn cdb_byte1(&self) -> u8 { + match self { + SectorReadMode::Audio => 0x00, + SectorReadMode::DataCooked => 0x04, + 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 + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 3f9ddd3..549b299 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; @@ -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, 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, 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, diff --git a/src/linux.rs b/src/linux.rs index 0ced759..255c3ab 100644 --- a/src/linux.rs +++ b/src/linux.rs @@ -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}; @@ -205,78 +203,32 @@ pub fn read_sectors_with_retry( sectors: u32, cfg: &RetryConfig, ) -> std::result::Result, 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, 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::::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 = 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, 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, 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; @@ -284,9 +236,7 @@ fn read_cd_audio_chunk(lba: u32, this_sectors: u32) -> std::result::Result> 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, @@ -346,11 +296,3 @@ fn parse_sense(sense: &[u8], sb_len_wr: u8) -> (Option, Option, Option u32 { - if current > 8 { - 8.max(min_chunk) - } else { - min_chunk - } -} diff --git a/src/mac/audio_reader.c b/src/mac/audio_reader.c index 92ab9f3..35bbaef 100644 --- a/src/mac/audio_reader.c +++ b/src/mac/audio_reader.c @@ -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; + *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, §orArea, §orType, §orSize)) { + 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; @@ -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; diff --git a/src/mac/device_service.c b/src/mac/device_service.c index 700f9f9..a5e7fa3 100644 --- a/src/mac/device_service.c +++ b/src/mac/device_service.c @@ -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; } diff --git a/src/mac/shim_common.h b/src/mac/shim_common.h index 81bd712..f6c1cd0 100644 --- a/src/mac/shim_common.h +++ b/src/mac/shim_common.h @@ -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); diff --git a/src/macos.rs b/src/macos.rs index d66e6d9..6c4ec31 100644 --- a/src/macos.rs +++ b/src/macos.rs @@ -1,8 +1,8 @@ use std::ffi::{CStr, CString}; use std::io; use std::{ptr, slice}; -use std::{thread::sleep, time::Duration}; +use crate::data_reader::SectorReadMode; use crate::parse_toc::parse_toc; use crate::utils::get_track_bounds; use crate::{CdReaderError, DriveInfo, RetryConfig, ScsiError, ScsiOp, Toc}; @@ -31,9 +31,10 @@ struct MacDriveInfo { #[link(name = "macos_cd_shim", kind = "static")] unsafe extern "C" { fn cd_read_toc(out_buf: *mut *mut u8, out_len: *mut u32, out_err: *mut MacScsiError) -> bool; - fn read_cd_audio( + fn read_cd_sectors( lba: u32, sectors: u32, + mode_id: u32, out_buf: *mut *mut u8, out_len: *mut u32, out_err: *mut MacScsiError, @@ -133,70 +134,41 @@ pub fn read_sectors_with_retry( sectors: u32, cfg: &RetryConfig, ) -> Result, CdReaderError> { - const SECTOR_BYTES: usize = 2352; - const MAX_SECTORS_PER_XFER: u32 = 27; - - let mut out = Vec::::with_capacity((sectors as usize) * SECTOR_BYTES); - let mut remaining = sectors; - let mut lba = start_lba; - let attempts_total = cfg.max_attempts.max(1); - let min_chunk = cfg.min_sectors_per_read.max(1); - - while remaining > 0 { - let mut chunk_sectors = remaining.min(MAX_SECTORS_PER_XFER); - let mut backoff_ms = cfg.initial_backoff_ms; - let mut last_err: Option = 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); - } - } + read_sectors_with_mode(start_lba, sectors, &SectorReadMode::Audio, cfg) +} - Ok(out) +pub fn read_sectors_with_mode( + start_lba: u32, + sectors: u32, + mode: &SectorReadMode, + cfg: &RetryConfig, +) -> Result, CdReaderError> { + 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, sectors: u32) -> Result, CdReaderError> { +fn read_cd_chunk(lba: u32, sectors: u32, mode: &SectorReadMode) -> Result, CdReaderError> { let mut buf: *mut u8 = ptr::null_mut(); let mut len: u32 = 0; let mut err: MacScsiError = Default::default(); - let ok = unsafe { read_cd_audio(lba, sectors, &mut buf, &mut len, &mut err) }; + + // Discriminant understood by `read_cd_sectors`, which maps it to the + // matching macOS CD sector area/type for DKIOCCDREAD. + let mode_id: u32 = match mode { + SectorReadMode::Audio => 0, + SectorReadMode::DataCooked => 1, + SectorReadMode::DataRaw => 2, + }; + + let ok = unsafe { read_cd_sectors(lba, sectors, mode_id, &mut buf, &mut len, &mut err) }; if !ok { return Err(map_mac_error(err, ScsiOp::ReadCd, Some(lba), Some(sectors))); } let data = unsafe { slice::from_raw_parts(buf, len as usize) }; - - // `.to_vec()` will copy the data, so we can free it safely after let result = data.to_vec(); - unsafe { cd_free(buf as *mut _) }; Ok(result) @@ -222,11 +194,3 @@ fn map_mac_error( CdReaderError::Io(std::io::Error::other("macOS CD command failed")) } - -fn next_chunk_size(current: u32, min_chunk: u32) -> u32 { - if current > 8 { - 8.max(min_chunk) - } else { - min_chunk - } -} diff --git a/src/read_loop.rs b/src/read_loop.rs new file mode 100644 index 0000000..5df91ac --- /dev/null +++ b/src/read_loop.rs @@ -0,0 +1,88 @@ +//! Shared retry + chunking loop for `READ CD` reads. +//! +//! Every platform issues the same logical read: split a sector range into +//! drive-safe chunks, read each chunk with capped exponential backoff and +//! adaptive chunk-size reduction, and concatenate the results. Only the +//! single-command read itself (SG_IO on Linux, SPTI on Windows, IOKit on +//! macOS) is platform-specific, so it is injected as a closure. + +use std::thread::sleep; +use std::time::Duration; + +use crate::data_reader::SectorReadMode; +use crate::{CdReaderError, RetryConfig}; + +/// Read `sectors` sectors starting at `start_lba` in the given `mode`. +/// +/// `read_chunk(lba, sectors)` performs one platform-specific `READ CD` +/// command and returns the raw bytes for that chunk. The loop owns chunk +/// sizing, retries, and backoff so platform code only implements the +/// single-command read. +pub(crate) fn read_sectors_chunked( + start_lba: u32, + sectors: u32, + mode: &SectorReadMode, + cfg: &RetryConfig, + mut read_chunk: F, +) -> Result, CdReaderError> +where + F: FnMut(u32, u32) -> Result, CdReaderError>, +{ + let total_bytes = (sectors as usize) * mode.sector_size(); + let max_sectors_per_xfer = mode.max_sectors_per_xfer(); + let mut out = Vec::::with_capacity(total_bytes); + + let mut remaining = sectors; + let mut lba = start_lba; + let attempts_total = cfg.max_attempts.max(1); + let min_chunk = cfg.min_sectors_per_read.max(1); + + while remaining > 0 { + let mut chunk_sectors = remaining.min(max_sectors_per_xfer); + let mut backoff_ms = cfg.initial_backoff_ms; + let mut last_err: Option = None; + + for attempt in 1..=attempts_total { + match read_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) +} + +/// Shrink the chunk size after a failed read to improve the odds of success, +/// stepping large reads down toward `min_chunk` (for example `27 -> 8 -> 1`). +fn next_chunk_size(current: u32, min_chunk: u32) -> u32 { + if current > 8 { + 8.max(min_chunk) + } else { + min_chunk + } +} diff --git a/src/windows.rs b/src/windows.rs index 4f14699..c952459 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -10,6 +10,7 @@ use windows_sys::Win32::Storage::IscsiDisc::{ }; use windows_sys::Win32::System::IO::DeviceIoControl; +use crate::data_reader::SectorReadMode; use crate::{CdReaderError, RetryConfig, ScsiError, ScsiOp, Toc, parse_toc, windows_read_track}; use std::mem; @@ -174,13 +175,22 @@ pub fn read_sectors_with_retry( start_lba: u32, sectors: u32, cfg: &RetryConfig, +) -> Result, CdReaderError> { + read_sectors_with_mode(start_lba, sectors, &SectorReadMode::Audio, cfg) +} + +pub fn read_sectors_with_mode( + start_lba: u32, + sectors: u32, + mode: &SectorReadMode, + cfg: &RetryConfig, ) -> Result, CdReaderError> { let handle = unsafe { DRIVE_HANDLE .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "Drive not opened")) .map_err(CdReaderError::Io)? }; - windows_read_track::read_audio_range_with_retry(handle, start_lba, sectors, cfg) + windows_read_track::read_range_with_retry(handle, start_lba, sectors, mode, cfg) } fn parse_sense(sense: &[u8], sense_len: u8) -> (Option, Option, Option) { diff --git a/src/windows_read_track.rs b/src/windows_read_track.rs index 6c9102d..b517fa8 100644 --- a/src/windows_read_track.rs +++ b/src/windows_read_track.rs @@ -1,8 +1,5 @@ -use std::cmp::min; use std::mem; use std::ptr; -use std::thread::sleep; -use std::time::Duration; use windows_sys::Win32::Foundation::HANDLE; use windows_sys::Win32::Storage::IscsiDisc::{ @@ -10,77 +7,30 @@ use windows_sys::Win32::Storage::IscsiDisc::{ }; use windows_sys::Win32::System::IO::DeviceIoControl; +use crate::data_reader::SectorReadMode; use crate::windows::SptdWithSense; use crate::{CdReaderError, RetryConfig, ScsiError, ScsiOp}; -// --- READ CD (0xBE): read an arbitrary LBA range as CD-DA (2352 bytes/sector) --- -pub fn read_audio_range_with_retry( +pub fn read_range_with_retry( handle: HANDLE, start_lba: u32, sectors: u32, + mode: &SectorReadMode, cfg: &RetryConfig, ) -> Result, 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::::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 = None; - - for attempt in 1..=attempts_total { - match read_cd_audio_chunk(handle, 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(handle, lba, chunk_sectors, mode) + }) } -fn read_cd_audio_chunk( +fn read_cd_chunk( handle: HANDLE, lba: u32, this_sectors: u32, + mode: &SectorReadMode, ) -> Result, CdReaderError> { - const SECTOR_BYTES: usize = 2352; - let mut chunk = vec![0u8; (this_sectors as usize) * SECTOR_BYTES]; + let sector_size = mode.sector_size(); + let mut chunk = vec![0u8; (this_sectors as usize) * sector_size]; let mut wrapper: SptdWithSense = unsafe { mem::zeroed() }; let sptd = &mut wrapper.sptd; @@ -97,6 +47,7 @@ fn read_cd_audio_chunk( let cdb = &mut sptd.Cdb; cdb.fill(0); cdb[0] = 0xBE; + 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; @@ -104,7 +55,7 @@ fn read_cd_audio_chunk( 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[9] = mode.cdb_byte9(); let mut bytes = 0u32; let ok = unsafe { @@ -146,11 +97,3 @@ fn parse_sense(sense: &[u8], sense_len: u8) -> (Option, Option, Option u32 { - if current > 8 { - 8.max(min_chunk) - } else { - min_chunk - } -}