From 756d18b596da1ef2265360edc0916a04fce8f29a Mon Sep 17 00:00:00 2001 From: Seva Zaikov Date: Sun, 12 Jul 2026 08:46:24 -0700 Subject: [PATCH 1/6] move data_reader module inside a folder --- src/{data_reader.rs => data_reader/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{data_reader.rs => data_reader/mod.rs} (100%) diff --git a/src/data_reader.rs b/src/data_reader/mod.rs similarity index 100% rename from src/data_reader.rs rename to src/data_reader/mod.rs From 1515ede7cb772cb03a27a02b749bc9745b75f944 Mon Sep 17 00:00:00 2001 From: Seva Zaikov Date: Sun, 12 Jul 2026 08:56:40 -0700 Subject: [PATCH 2/6] add all modes to the SectorReadFormat --- src/data_reader/mod.rs | 95 +-------------- src/data_reader/sector_read_format.rs | 159 ++++++++++++++++++++++++++ src/platform/macos/native/read_cd.c | 60 +++++++++- src/platform/macos/read_cd.rs | 14 +++ 4 files changed, 231 insertions(+), 97 deletions(-) create mode 100644 src/data_reader/sector_read_format.rs diff --git a/src/data_reader/mod.rs b/src/data_reader/mod.rs index d828b84..cb3d03c 100644 --- a/src/data_reader/mod.rs +++ b/src/data_reader/mod.rs @@ -1,3 +1,7 @@ +mod sector_read_format; + +pub use sector_read_format::SectorReadFormat; + use crate::retry::RetryConfig; /// Sector format and retry options for track and sector-range reads. @@ -41,73 +45,6 @@ impl Default for ReadOptions { } } -/// Sector format requested through the READ CD (0xBE) command. -/// -/// Controls CDB byte 1 (Expected Sector Type) and byte 9 (Main Channel Selection) -/// to return audio, cooked Mode 1 data, or complete Mode 1 sectors. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SectorReadFormat { - /// Audio: 2352 bytes/sector, raw PCM. - /// CDB byte 1 = 0x00 (any type), byte 9 = 0x10 (user data). - Audio, - /// Mode 1 cooked data: 2048 bytes/sector, containing user data only. - /// CDB byte 1 = 0x08 (Mode 1), byte 9 = 0x10 (user data). - Mode1Cooked, - /// Complete Mode 1 sector: 2352 bytes with sync, header, user data, EDC, and ECC. - /// CDB byte 1 = 0x08 (Mode 1), byte 9 = 0xF8 (all main-channel fields). - Mode1Raw, -} - -impl SectorReadFormat { - /// Bytes per sector for this read format. - pub fn sector_size(&self) -> usize { - match self { - SectorReadFormat::Audio => 2352, - SectorReadFormat::Mode1Cooked => 2048, - SectorReadFormat::Mode1Raw => 2352, - } - } - - /// CDB byte 1: Expected Sector Type field (bits 4-2). - /// - /// Per SCSI MMC, the type value is shifted left by 2: Mode 1 is - /// `010b << 2 = 0x08`. (`0x04` would be `001b << 2`, i.e. CD-DA.) - pub fn cdb_byte1(&self) -> u8 { - match self { - SectorReadFormat::Audio => 0x00, - SectorReadFormat::Mode1Cooked => 0x08, - SectorReadFormat::Mode1Raw => 0x08, - } - } - - /// CDB byte 9: Main Channel Selection bits. - pub fn cdb_byte9(&self) -> u8 { - match self { - SectorReadFormat::Audio => 0x10, - SectorReadFormat::Mode1Cooked => 0x10, - SectorReadFormat::Mode1Raw => 0xF8, - } - } - - /// Maximum sectors per single `READ CD` command. - /// - /// This is the sole chunker for the blocking `read_track` / - /// `read_sector_range` paths, which can hand a whole track (tens of thousands - /// of sectors) straight to the read loop; the streaming API already limits - /// itself via its `TrackStreamOptions` chunk size. 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 - } - } -} - /// Build a READ CD (0xBE) command descriptor block for Linux and Windows. #[cfg(any(target_os = "linux", target_os = "windows", test))] pub(crate) fn build_read_cd_cdb(lba: u32, sectors: u32, format: SectorReadFormat) -> [u8; 12] { @@ -139,30 +76,6 @@ mod tests { assert_eq!(options.retry().max_attempts, 9); } - #[test] - fn cdb_byte1_encodes_expected_sector_type() { - // Expected Sector Type lives in CDB byte 1, bits 4-2 (value << 2). - // Audio leaves it 0 (any type) and relies on byte 9; Mode 1 reads use - // 010b << 2 = 0x08 (0x04 would mean CD-DA). - assert_eq!(SectorReadFormat::Audio.cdb_byte1(), 0x00); - assert_eq!(SectorReadFormat::Mode1Cooked.cdb_byte1(), 0x08); - assert_eq!(SectorReadFormat::Mode1Raw.cdb_byte1(), 0x08); - } - - #[test] - fn cdb_byte9_selects_main_channel() { - assert_eq!(SectorReadFormat::Audio.cdb_byte9(), 0x10); - assert_eq!(SectorReadFormat::Mode1Cooked.cdb_byte9(), 0x10); - assert_eq!(SectorReadFormat::Mode1Raw.cdb_byte9(), 0xF8); - } - - #[test] - fn sector_size_matches_format() { - assert_eq!(SectorReadFormat::Audio.sector_size(), 2352); - assert_eq!(SectorReadFormat::Mode1Cooked.sector_size(), 2048); - assert_eq!(SectorReadFormat::Mode1Raw.sector_size(), 2352); - } - #[test] fn builds_read_cd_cdb() { assert_eq!( diff --git a/src/data_reader/sector_read_format.rs b/src/data_reader/sector_read_format.rs new file mode 100644 index 0000000..33352ff --- /dev/null +++ b/src/data_reader/sector_read_format.rs @@ -0,0 +1,159 @@ +/// Sector format requested through the READ CD (0xBE) command. +/// +/// Each variant selects an expected sector type through CDB byte 1 and the +/// returned main-channel fields through CDB byte 9. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SectorReadFormat { + /// Complete 2352-byte main-channel sector without declaring an expected + /// sector type. + /// + /// This is primarily useful for sector-type detection. Some drives may not + /// support raw reads with an expected type of “Any.” + AnyRaw, + + /// CD-DA audio: 2352 bytes of PCM per sector. + Audio, + + /// Mode 1 user data only: 2048 bytes per sector. + Mode1Cooked, + /// Complete Mode 1 sector: 2352 bytes with sync, header, user data, EDC, + /// and ECC. + Mode1Raw, + + /// Mode 2 formless user data: 2336 bytes per sector. + Mode2FormlessCooked, + /// Complete Mode 2 formless sector: 2352 bytes. + Mode2FormlessRaw, + + /// Mode 2 Form 1 user data: 2048 bytes per sector. + Mode2Form1Cooked, + /// Complete Mode 2 Form 1 sector: 2352 bytes. + Mode2Form1Raw, + + /// Mode 2 Form 2 user area: 2328 bytes per sector. + /// + /// This consists of 2324 bytes of application payload followed by its + /// 4-byte EDC, as exposed by MMC and macOS IOKit. + Mode2Form2Cooked, + /// Complete Mode 2 Form 2 sector: 2352 bytes. + Mode2Form2Raw, +} + +impl SectorReadFormat { + /// Bytes returned per sector for this format. + pub fn sector_size(&self) -> usize { + match self { + Self::AnyRaw + | Self::Audio + | Self::Mode1Raw + | Self::Mode2FormlessRaw + | Self::Mode2Form1Raw + | Self::Mode2Form2Raw => 2352, + Self::Mode1Cooked | Self::Mode2Form1Cooked => 2048, + Self::Mode2FormlessCooked => 2336, + Self::Mode2Form2Cooked => 2328, + } + } + + /// CDB byte 1: Expected Sector Type in bits 4–2. + pub fn cdb_byte1(&self) -> u8 { + match self { + Self::AnyRaw => 0x00, + Self::Audio => 0x04, + Self::Mode1Cooked | Self::Mode1Raw => 0x08, + Self::Mode2FormlessCooked | Self::Mode2FormlessRaw => 0x0C, + Self::Mode2Form1Cooked | Self::Mode2Form1Raw => 0x10, + Self::Mode2Form2Cooked | Self::Mode2Form2Raw => 0x14, + } + } + + /// CDB byte 9: Main Channel Selection. + pub fn cdb_byte9(&self) -> u8 { + match self { + Self::Audio + | Self::Mode1Cooked + | Self::Mode2FormlessCooked + | Self::Mode2Form1Cooked + | Self::Mode2Form2Cooked => 0x10, + Self::AnyRaw + | Self::Mode1Raw + | Self::Mode2FormlessRaw + | Self::Mode2Form1Raw + | Self::Mode2Form2Raw => 0xF8, + } + } + + /// Maximum sectors per single `READ CD` command. + /// + /// Transfers are kept at approximately 64 KiB for compatibility with + /// optical-drive firmware and USB bridges. + pub(crate) fn max_sectors_per_xfer(&self) -> u32 { + (64 * 1024 / self.sector_size() as u32).max(1) + } +} + +#[cfg(test)] +mod tests { + use super::SectorReadFormat; + + #[test] + fn expected_sector_types_are_encoded_in_cdb_byte1() { + assert_eq!(SectorReadFormat::AnyRaw.cdb_byte1(), 0x00); + assert_eq!(SectorReadFormat::Audio.cdb_byte1(), 0x04); + assert_eq!(SectorReadFormat::Mode1Cooked.cdb_byte1(), 0x08); + assert_eq!(SectorReadFormat::Mode1Raw.cdb_byte1(), 0x08); + assert_eq!(SectorReadFormat::Mode2FormlessCooked.cdb_byte1(), 0x0C); + assert_eq!(SectorReadFormat::Mode2FormlessRaw.cdb_byte1(), 0x0C); + assert_eq!(SectorReadFormat::Mode2Form1Cooked.cdb_byte1(), 0x10); + assert_eq!(SectorReadFormat::Mode2Form1Raw.cdb_byte1(), 0x10); + assert_eq!(SectorReadFormat::Mode2Form2Cooked.cdb_byte1(), 0x14); + assert_eq!(SectorReadFormat::Mode2Form2Raw.cdb_byte1(), 0x14); + } + + #[test] + fn main_channel_fields_are_encoded_in_cdb_byte9() { + assert_eq!(SectorReadFormat::AnyRaw.cdb_byte9(), 0xF8); + assert_eq!(SectorReadFormat::Audio.cdb_byte9(), 0x10); + assert_eq!(SectorReadFormat::Mode1Cooked.cdb_byte9(), 0x10); + assert_eq!(SectorReadFormat::Mode1Raw.cdb_byte9(), 0xF8); + assert_eq!(SectorReadFormat::Mode2FormlessCooked.cdb_byte9(), 0x10); + assert_eq!(SectorReadFormat::Mode2FormlessRaw.cdb_byte9(), 0xF8); + assert_eq!(SectorReadFormat::Mode2Form1Cooked.cdb_byte9(), 0x10); + assert_eq!(SectorReadFormat::Mode2Form1Raw.cdb_byte9(), 0xF8); + assert_eq!(SectorReadFormat::Mode2Form2Cooked.cdb_byte9(), 0x10); + assert_eq!(SectorReadFormat::Mode2Form2Raw.cdb_byte9(), 0xF8); + } + + #[test] + fn sector_sizes_match_mmc_layouts() { + assert_eq!(SectorReadFormat::AnyRaw.sector_size(), 2352); + assert_eq!(SectorReadFormat::Audio.sector_size(), 2352); + assert_eq!(SectorReadFormat::Mode1Cooked.sector_size(), 2048); + assert_eq!(SectorReadFormat::Mode1Raw.sector_size(), 2352); + assert_eq!(SectorReadFormat::Mode2FormlessCooked.sector_size(), 2336); + assert_eq!(SectorReadFormat::Mode2FormlessRaw.sector_size(), 2352); + assert_eq!(SectorReadFormat::Mode2Form1Cooked.sector_size(), 2048); + assert_eq!(SectorReadFormat::Mode2Form1Raw.sector_size(), 2352); + assert_eq!(SectorReadFormat::Mode2Form2Cooked.sector_size(), 2328); + assert_eq!(SectorReadFormat::Mode2Form2Raw.sector_size(), 2352); + } + + #[test] + fn transfer_caps_stay_within_64_kib() { + for format in [ + SectorReadFormat::AnyRaw, + SectorReadFormat::Audio, + SectorReadFormat::Mode1Cooked, + SectorReadFormat::Mode1Raw, + SectorReadFormat::Mode2FormlessCooked, + SectorReadFormat::Mode2FormlessRaw, + SectorReadFormat::Mode2Form1Cooked, + SectorReadFormat::Mode2Form1Raw, + SectorReadFormat::Mode2Form2Cooked, + SectorReadFormat::Mode2Form2Raw, + ] { + let bytes = format.max_sectors_per_xfer() as usize * format.sector_size(); + assert!(bytes <= 64 * 1024); + } + } +} diff --git a/src/platform/macos/native/read_cd.c b/src/platform/macos/native/read_cd.c index 29e4b7f..d39cb40 100644 --- a/src/platform/macos/native/read_cd.c +++ b/src/platform/macos/native/read_cd.c @@ -5,13 +5,20 @@ // Rust) means the IOKit constants only ever appear where their header is // imported. // -// 0 = Audio -> user area, CDDA, 2352 B/sector -// 1 = Mode1Cooked -> user area, Mode 1, 2048 B/sector -// 2 = Mode1Raw -> sync+header+user+aux, Mode 1, 2352 B/sector +// 0 = Audio -> user, CDDA, 2352 B/sector +// 1 = Mode1Cooked -> user, Mode 1, 2048 B/sector +// 2 = Mode1Raw -> full Mode 1, 2352 B/sector +// 3 = Mode2FormlessCooked -> user, Mode 2, 2336 B/sector +// 4 = Mode2FormlessRaw -> full Mode 2, 2352 B/sector +// 5 = Mode2Form1Cooked -> user, Mode 2 Form 1, 2048 B/sector +// 6 = Mode2Form1Raw -> full Mode 2 Form 1, 2352 B/sector +// 7 = Mode2Form2Cooked -> user, Mode 2 Form 2, 2328 B/sector +// 8 = Mode2Form2Raw -> full Mode 2 Form 2, 2352 B/sector +// 9 = AnyRaw -> full sector, unknown type, 2352 B/sector static bool sector_layout_for_format(uint32_t format_id, - CDSectorArea *outArea, - CDSectorType *outType, - uint32_t *outSectorSize) { + CDSectorArea *outArea, + CDSectorType *outType, + uint32_t *outSectorSize) { switch (format_id) { case 0: *outArea = kCDSectorAreaUser; @@ -29,6 +36,47 @@ static bool sector_layout_for_format(uint32_t format_id, *outType = kCDSectorTypeMode1; *outSectorSize = 2352; return true; + case 3: + *outArea = kCDSectorAreaUser; + *outType = kCDSectorTypeMode2; + *outSectorSize = 2336; + return true; + case 4: + *outArea = (CDSectorArea)(kCDSectorAreaSync | kCDSectorAreaHeader | + kCDSectorAreaUser); + *outType = kCDSectorTypeMode2; + *outSectorSize = 2352; + return true; + case 5: + *outArea = kCDSectorAreaUser; + *outType = kCDSectorTypeMode2Form1; + *outSectorSize = 2048; + return true; + case 6: + *outArea = (CDSectorArea)(kCDSectorAreaSync | kCDSectorAreaHeader | + kCDSectorAreaSubHeader | kCDSectorAreaUser | + kCDSectorAreaAuxiliary); + *outType = kCDSectorTypeMode2Form1; + *outSectorSize = 2352; + return true; + case 7: + *outArea = kCDSectorAreaUser; + *outType = kCDSectorTypeMode2Form2; + *outSectorSize = 2328; + return true; + case 8: + *outArea = (CDSectorArea)(kCDSectorAreaSync | kCDSectorAreaHeader | + kCDSectorAreaSubHeader | kCDSectorAreaUser); + *outType = kCDSectorTypeMode2Form2; + *outSectorSize = 2352; + return true; + case 9: + *outArea = (CDSectorArea)(kCDSectorAreaSync | kCDSectorAreaHeader | + kCDSectorAreaSubHeader | kCDSectorAreaUser | + kCDSectorAreaAuxiliary); + *outType = kCDSectorTypeUnknown; + *outSectorSize = 2352; + return true; default: return false; } diff --git a/src/platform/macos/read_cd.rs b/src/platform/macos/read_cd.rs index 528f423..7d87e5f 100644 --- a/src/platform/macos/read_cd.rs +++ b/src/platform/macos/read_cd.rs @@ -41,6 +41,13 @@ fn format_id(format: SectorReadFormat) -> u32 { SectorReadFormat::Audio => 0, SectorReadFormat::Mode1Cooked => 1, SectorReadFormat::Mode1Raw => 2, + SectorReadFormat::Mode2FormlessCooked => 3, + SectorReadFormat::Mode2FormlessRaw => 4, + SectorReadFormat::Mode2Form1Cooked => 5, + SectorReadFormat::Mode2Form1Raw => 6, + SectorReadFormat::Mode2Form2Cooked => 7, + SectorReadFormat::Mode2Form2Raw => 8, + SectorReadFormat::AnyRaw => 9, } } @@ -54,5 +61,12 @@ mod tests { assert_eq!(format_id(SectorReadFormat::Audio), 0); assert_eq!(format_id(SectorReadFormat::Mode1Cooked), 1); assert_eq!(format_id(SectorReadFormat::Mode1Raw), 2); + assert_eq!(format_id(SectorReadFormat::Mode2FormlessCooked), 3); + assert_eq!(format_id(SectorReadFormat::Mode2FormlessRaw), 4); + assert_eq!(format_id(SectorReadFormat::Mode2Form1Cooked), 5); + assert_eq!(format_id(SectorReadFormat::Mode2Form1Raw), 6); + assert_eq!(format_id(SectorReadFormat::Mode2Form2Cooked), 7); + assert_eq!(format_id(SectorReadFormat::Mode2Form2Raw), 8); + assert_eq!(format_id(SectorReadFormat::AnyRaw), 9); } } From 2c7dcff326c20081f72adfa7891e625d3d7cb729 Mon Sep 17 00:00:00 2001 From: Seva Zaikov Date: Sun, 12 Jul 2026 09:08:30 -0700 Subject: [PATCH 3/6] add read track information command --- build.rs | 6 + src/data_reader/mod.rs | 1 + src/data_reader/track_information.rs | 129 ++++++++++++++++++ src/errors.rs | 4 +- src/platform/linux/mod.rs | 9 ++ src/platform/linux/track_information.rs | 32 +++++ src/platform/macos/ffi.rs | 7 + src/platform/macos/mod.rs | 9 ++ src/platform/macos/native/shim_common.h | 1 + src/platform/macos/native/track_information.c | 63 +++++++++ src/platform/macos/track_information.rs | 27 ++++ src/platform/windows/mod.rs | 9 ++ src/platform/windows/track_information.rs | 32 +++++ 13 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 src/data_reader/track_information.rs create mode 100644 src/platform/linux/track_information.rs create mode 100644 src/platform/macos/native/track_information.c create mode 100644 src/platform/macos/track_information.rs create mode 100644 src/platform/windows/track_information.rs diff --git a/build.rs b/build.rs index bfb2321..43b900a 100644 --- a/build.rs +++ b/build.rs @@ -1,5 +1,9 @@ #[cfg(target_os = "macos")] fn main() { + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("macos") { + return; + } + println!("cargo:rerun-if-changed=build.rs"); const NATIVE_DIR: &str = "src/platform/macos/native"; @@ -7,6 +11,7 @@ fn main() { println!("cargo:rerun-if-changed={NATIVE_DIR}/device_service.c"); println!("cargo:rerun-if-changed={NATIVE_DIR}/list_drives.c"); println!("cargo:rerun-if-changed={NATIVE_DIR}/toc_reader.c"); + println!("cargo:rerun-if-changed={NATIVE_DIR}/track_information.c"); println!("cargo:rerun-if-changed={NATIVE_DIR}/read_cd.c"); println!("cargo:rustc-link-lib=framework=IOKit"); @@ -15,6 +20,7 @@ fn main() { .file(format!("{NATIVE_DIR}/device_service.c")) .file(format!("{NATIVE_DIR}/list_drives.c")) .file(format!("{NATIVE_DIR}/toc_reader.c")) + .file(format!("{NATIVE_DIR}/track_information.c")) .file(format!("{NATIVE_DIR}/read_cd.c")) .include(NATIVE_DIR) // force C compilation diff --git a/src/data_reader/mod.rs b/src/data_reader/mod.rs index cb3d03c..0131cdf 100644 --- a/src/data_reader/mod.rs +++ b/src/data_reader/mod.rs @@ -1,4 +1,5 @@ mod sector_read_format; +pub(crate) mod track_information; pub use sector_read_format::SectorReadFormat; diff --git a/src/data_reader/track_information.rs b/src/data_reader/track_information.rs new file mode 100644 index 0000000..5be1714 --- /dev/null +++ b/src/data_reader/track_information.rs @@ -0,0 +1,129 @@ +use std::io; + +#[cfg(any(target_os = "linux", target_os = "windows", test))] +pub(crate) const TRACK_INFORMATION_RESPONSE_SIZE: usize = 36; + +/// Track-level metadata returned by MMC READ TRACK INFORMATION (0x52). +#[allow(dead_code)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct TrackInformation { + pub(crate) track_number: u16, + pub(crate) session_number: u16, + pub(crate) track_mode: u8, + pub(crate) data_mode: u8, + pub(crate) start_lba: u32, + pub(crate) track_size: u32, +} + +/// Build READ TRACK INFORMATION with the address interpreted as a track number. +#[cfg(any(target_os = "linux", target_os = "windows", test))] +pub(crate) fn build_read_track_information_cdb( + track_number: u8, + allocation_length: u16, +) -> [u8; 10] { + let mut cdb = [0u8; 10]; + cdb[0] = 0x52; + cdb[1] = 0x01; // Address Type: track number + cdb[5] = track_number; + cdb[7..9].copy_from_slice(&allocation_length.to_be_bytes()); + cdb +} + +/// Parse the standard MMC READ TRACK INFORMATION response. +pub(crate) fn parse_track_information(data: &[u8]) -> io::Result { + const MINIMUM_RESPONSE_SIZE: usize = 32; + + if data.len() < MINIMUM_RESPONSE_SIZE { + return Err(invalid_data("track information response is too short")); + } + + let declared_size = u16::from_be_bytes([data[0], data[1]]) as usize + 2; + if declared_size < MINIMUM_RESPONSE_SIZE || declared_size > data.len() { + return Err(invalid_data("track information response length is invalid")); + } + + let track_number_msb = data.get(32).copied().unwrap_or(0); + let session_number_msb = data.get(33).copied().unwrap_or(0); + + Ok(TrackInformation { + track_number: u16::from_be_bytes([track_number_msb, data[2]]), + session_number: u16::from_be_bytes([session_number_msb, data[3]]), + track_mode: data[5] & 0x0F, + data_mode: data[6] & 0x0F, + start_lba: read_u32(data, 8), + track_size: read_u32(data, 24), + }) +} + +fn read_u32(data: &[u8], offset: usize) -> u32 { + u32::from_be_bytes([ + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3], + ]) +} + +fn invalid_data(message: &'static str) -> io::Error { + io::Error::new(io::ErrorKind::InvalidData, message) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn builds_track_number_request() { + assert_eq!( + build_read_track_information_cdb(16, TRACK_INFORMATION_RESPONSE_SIZE as u16), + [0x52, 0x01, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x24, 0x00] + ); + } + + #[test] + fn parses_track_information() { + let mut data = [0u8; TRACK_INFORMATION_RESPONSE_SIZE]; + data[0..2].copy_from_slice(&34u16.to_be_bytes()); + data[2] = 16; + data[3] = 2; + data[5] = 0xA4; + data[6] = 0xB1; + data[8..12].copy_from_slice(&263_053u32.to_be_bytes()); + data[24..28].copy_from_slice(&64_655u32.to_be_bytes()); + + assert_eq!( + parse_track_information(&data).unwrap(), + TrackInformation { + track_number: 16, + session_number: 2, + track_mode: 4, + data_mode: 1, + start_lba: 263_053, + track_size: 64_655, + } + ); + } + + #[test] + fn parses_mmc6_extended_track_and_session_numbers() { + let mut data = [0u8; TRACK_INFORMATION_RESPONSE_SIZE]; + data[0..2].copy_from_slice(&34u16.to_be_bytes()); + data[2] = 0x34; + data[3] = 0x78; + data[32] = 0x12; + data[33] = 0x56; + + let information = parse_track_information(&data).unwrap(); + assert_eq!(information.track_number, 0x1234); + assert_eq!(information.session_number, 0x5678); + } + + #[test] + fn rejects_short_or_truncated_responses() { + assert!(parse_track_information(&[0; 31]).is_err()); + + let mut truncated = [0u8; 32]; + truncated[0..2].copy_from_slice(&34u16.to_be_bytes()); + assert!(parse_track_information(&truncated).is_err()); + } +} diff --git a/src/errors.rs b/src/errors.rs index 9ef26c1..0e2cf12 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -5,8 +5,10 @@ use std::fmt; pub enum ScsiOp { /// `READ TOC/PMA/ATIP` command (opcode `0x43`) for TOC/session metadata. ReadToc, - /// `READ CD` command (opcode `0xBE`) for CD-DA sector payload (2352 bytes/sector). + /// `READ CD` command (opcode `0xBE`) for audio or data sectors. ReadCd, + /// `READ TRACK INFORMATION` command (opcode `0x52`) for track metadata. + ReadTrackInformation, /// `READ SUB-CHANNEL` command for Q-channel/subcode metadata. ReadSubChannel, } diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs index 65fda4f..00d88d9 100644 --- a/src/platform/linux/mod.rs +++ b/src/platform/linux/mod.rs @@ -2,6 +2,7 @@ mod device; mod read_cd; mod sg_io; mod toc; +mod track_information; pub(crate) use device::{Drive, list_drive_paths}; @@ -12,6 +13,14 @@ impl Drive { toc::read_toc(self) } + #[allow(dead_code)] + pub(crate) fn read_track_information( + &self, + track_number: u8, + ) -> Result { + track_information::read_track_information(self, track_number) + } + pub(crate) fn read_cd_chunk( &self, lba: u32, diff --git a/src/platform/linux/track_information.rs b/src/platform/linux/track_information.rs new file mode 100644 index 0000000..bc5252f --- /dev/null +++ b/src/platform/linux/track_information.rs @@ -0,0 +1,32 @@ +use super::device::Drive; +use super::sg_io::{CommandContext, execute_read}; +use crate::data_reader::track_information::{ + TRACK_INFORMATION_RESPONSE_SIZE, TrackInformation, build_read_track_information_cdb, + parse_track_information, +}; +use crate::{CdReaderError, ScsiOp}; + +const TRACK_INFORMATION_TIMEOUT_MS: u32 = 10_000; + +pub(super) fn read_track_information( + drive: &Drive, + track_number: u8, +) -> Result { + let mut data = vec![0u8; TRACK_INFORMATION_RESPONSE_SIZE]; + let mut cdb = + build_read_track_information_cdb(track_number, TRACK_INFORMATION_RESPONSE_SIZE as u16); + let transferred = execute_read( + drive.fd(), + &mut cdb, + &mut data, + TRACK_INFORMATION_TIMEOUT_MS, + CommandContext { + op: ScsiOp::ReadTrackInformation, + lba: None, + sectors: None, + }, + )?; + data.truncate(transferred); + + parse_track_information(&data).map_err(|error| CdReaderError::Parse(error.to_string())) +} diff --git a/src/platform/macos/ffi.rs b/src/platform/macos/ffi.rs index 8255e79..b4b31bd 100644 --- a/src/platform/macos/ffi.rs +++ b/src/platform/macos/ffi.rs @@ -27,6 +27,13 @@ unsafe extern "C" { out_len: *mut u32, out_err: *mut MacScsiError, ) -> bool; + pub(super) fn cd_read_track_information( + fd: libc::c_int, + track_number: u8, + out_buf: *mut *mut u8, + out_len: *mut u32, + out_err: *mut MacScsiError, + ) -> bool; pub(super) fn read_cd_sectors( fd: libc::c_int, lba: u32, diff --git a/src/platform/macos/mod.rs b/src/platform/macos/mod.rs index 26aedaa..1cea49a 100644 --- a/src/platform/macos/mod.rs +++ b/src/platform/macos/mod.rs @@ -2,6 +2,7 @@ mod device; mod ffi; mod read_cd; mod toc; +mod track_information; pub(crate) use device::{Drive, list_drive_paths}; @@ -12,6 +13,14 @@ impl Drive { toc::read_toc(self) } + #[allow(dead_code)] + pub(crate) fn read_track_information( + &self, + track_number: u8, + ) -> Result { + track_information::read_track_information(self, track_number) + } + pub(crate) fn read_cd_chunk( &self, lba: u32, diff --git a/src/platform/macos/native/shim_common.h b/src/platform/macos/native/shim_common.h index 4f14347..f6abcd6 100644 --- a/src/platform/macos/native/shim_common.h +++ b/src/platform/macos/native/shim_common.h @@ -33,6 +33,7 @@ typedef struct { } CdDriveInfo; bool cd_read_toc(int fd, uint8_t **outBuf, uint32_t *outLen, CdScsiError *outErr); +bool cd_read_track_information(int fd, uint8_t trackNumber, uint8_t **outBuf, uint32_t *outLen, CdScsiError *outErr); bool read_cd_sectors(int fd, uint32_t lba, uint32_t sectors, uint32_t format_id, uint8_t **outBuf, uint32_t *outLen, CdScsiError *outErr); void cd_free(void *p); diff --git a/src/platform/macos/native/track_information.c b/src/platform/macos/native/track_information.c new file mode 100644 index 0000000..35b2f6c --- /dev/null +++ b/src/platform/macos/native/track_information.c @@ -0,0 +1,63 @@ +#include "shim_common.h" + +bool cd_read_track_information(int fd, uint8_t trackNumber, + uint8_t **outBuf, uint32_t *outLen, + CdScsiError *outErr) { + if (!outBuf || !outLen) { + return false; + } + + *outBuf = NULL; + *outLen = 0; + if (outErr) { + memset(outErr, 0, sizeof(CdScsiError)); + } + + const uint16_t rawAlloc = sizeof(CDTrackInfo); + uint8_t *raw = calloc(1, rawAlloc); + if (!raw) { + fprintf(stderr, "[TRACK INFO] oom\n"); + return false; + } + + dk_cd_read_track_info_t request = {0}; + request.address = trackNumber; + request.addressType = kCDTrackInfoAddressTypeTrackNumber; + request.bufferLength = rawAlloc; + request.buffer = raw; + + int ret = ioctl(fd, DKIOCCDREADTRACKINFO, &request); + if (ret < 0) { + fprintf(stderr, "[TRACK INFO] DKIOCCDREADTRACKINFO failed (errno=%d)\n", errno); + free(raw); + return false; + } + + if (request.bufferLength < 32) { + fprintf(stderr, "[TRACK INFO] response is too short\n"); + free(raw); + return false; + } + + uint16_t dataLength = OSSwapBigToHostInt16(((CDTrackInfo *)raw)->dataLength); + uint32_t responseLength = (uint32_t)dataLength + sizeof(uint16_t); + if (responseLength < 32 || responseLength > request.bufferLength) { + fprintf(stderr, "[TRACK INFO] response length is invalid\n"); + free(raw); + return false; + } + + uint8_t *result = malloc(responseLength); + if (!result) { + fprintf(stderr, "[TRACK INFO] oom\n"); + free(raw); + return false; + } + + memcpy(result, raw, responseLength); + free(raw); + + *outBuf = result; + *outLen = responseLength; + return true; +} diff --git a/src/platform/macos/track_information.rs b/src/platform/macos/track_information.rs new file mode 100644 index 0000000..98ad932 --- /dev/null +++ b/src/platform/macos/track_information.rs @@ -0,0 +1,27 @@ +use std::{ptr, slice}; + +use super::device::Drive; +use super::ffi::{MacScsiError, cd_free, cd_read_track_information, map_error}; +use crate::data_reader::track_information::{TrackInformation, parse_track_information}; +use crate::{CdReaderError, ScsiOp}; + +pub(super) fn read_track_information( + drive: &Drive, + track_number: u8, +) -> Result { + let mut buffer: *mut u8 = ptr::null_mut(); + let mut len = 0u32; + let mut error = MacScsiError::default(); + + let success = unsafe { + cd_read_track_information(drive.fd(), track_number, &mut buffer, &mut len, &mut error) + }; + if !success { + return Err(map_error(error, ScsiOp::ReadTrackInformation, None, None)); + } + + let data = unsafe { slice::from_raw_parts(buffer, len as usize) }.to_vec(); + unsafe { cd_free(buffer.cast()) }; + + parse_track_information(&data).map_err(|error| CdReaderError::Parse(error.to_string())) +} diff --git a/src/platform/windows/mod.rs b/src/platform/windows/mod.rs index 6f2609b..ad856d5 100644 --- a/src/platform/windows/mod.rs +++ b/src/platform/windows/mod.rs @@ -2,6 +2,7 @@ mod device; mod read_cd; mod spti; mod toc; +mod track_information; pub(crate) use device::{Drive, list_drive_paths}; @@ -12,6 +13,14 @@ impl Drive { toc::read_toc(self) } + #[allow(dead_code)] + pub(crate) fn read_track_information( + &self, + track_number: u8, + ) -> Result { + track_information::read_track_information(self, track_number) + } + pub(crate) fn read_cd_chunk( &self, lba: u32, diff --git a/src/platform/windows/track_information.rs b/src/platform/windows/track_information.rs new file mode 100644 index 0000000..f2f7762 --- /dev/null +++ b/src/platform/windows/track_information.rs @@ -0,0 +1,32 @@ +use super::device::Drive; +use super::spti::{CommandContext, execute_read}; +use crate::data_reader::track_information::{ + TRACK_INFORMATION_RESPONSE_SIZE, TrackInformation, build_read_track_information_cdb, + parse_track_information, +}; +use crate::{CdReaderError, ScsiOp}; + +const TRACK_INFORMATION_TIMEOUT_SECONDS: u32 = 10; + +pub(super) fn read_track_information( + drive: &Drive, + track_number: u8, +) -> Result { + let mut data = vec![0u8; TRACK_INFORMATION_RESPONSE_SIZE]; + let cdb = + build_read_track_information_cdb(track_number, TRACK_INFORMATION_RESPONSE_SIZE as u16); + let transferred = execute_read( + drive.handle(), + &cdb, + &mut data, + TRACK_INFORMATION_TIMEOUT_SECONDS, + CommandContext { + op: ScsiOp::ReadTrackInformation, + lba: None, + sectors: None, + }, + )?; + data.truncate(transferred); + + parse_track_information(&data).map_err(|error| CdReaderError::Parse(error.to_string())) +} From d2a8b33e94aa9b78257215573cb376312e5483b9 Mon Sep 17 00:00:00 2001 From: Seva Zaikov Date: Sun, 12 Jul 2026 09:15:59 -0700 Subject: [PATCH 4/6] add detect track mode function --- src/data_reader/detect.rs | 80 +++++++++++++++++++++++++++++++++++++ src/data_reader/mod.rs | 1 + src/errors.rs | 23 ++++++++++- src/platform/linux/mod.rs | 1 - src/platform/macos/mod.rs | 1 - src/platform/windows/mod.rs | 1 - 6 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 src/data_reader/detect.rs diff --git a/src/data_reader/detect.rs b/src/data_reader/detect.rs new file mode 100644 index 0000000..b19be37 --- /dev/null +++ b/src/data_reader/detect.rs @@ -0,0 +1,80 @@ +use crate::{CdReader, CdReaderError, SectorReadFormat, Track}; + +impl CdReader { + /// Detect the default read format for a track. + /// + /// Audio tracks are identified directly from the TOC. Data tracks are + /// queried with MMC READ TRACK INFORMATION and mapped to their cooked + /// sector representation. + pub fn detect_track_format(&self, track: &Track) -> Result { + if track.is_audio { + return Ok(SectorReadFormat::Audio); + } + + let information = self.drive.read_track_information(track.number)?; + format_from_data_mode(information.data_mode).ok_or(CdReaderError::CannotDetectTrackFormat { + track_number: track.number, + data_mode: Some(information.data_mode), + }) + } +} + +/// Map the MMC READ TRACK INFORMATION Data Mode field to the default cooked +/// representation for that physical sector layout. +fn format_from_data_mode(data_mode: u8) -> Option { + match data_mode { + 0x00 => Some(SectorReadFormat::Mode1Cooked), + 0x01 => Some(SectorReadFormat::Mode2FormlessCooked), + 0x02 => Some(SectorReadFormat::Mode2Form1Cooked), + 0x03 => Some(SectorReadFormat::Mode2Form2Cooked), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn audio_tracks_do_not_require_drive_detection() { + let reader = CdReader::test_reader(); + let track = Track { + number: 1, + start_lba: 0, + start_msf: (0, 2, 0), + is_audio: true, + }; + + assert_eq!( + reader.detect_track_format(&track).unwrap(), + SectorReadFormat::Audio + ); + } + + #[test] + fn maps_known_mmc_data_modes_to_cooked_formats() { + assert_eq!( + format_from_data_mode(0x00), + Some(SectorReadFormat::Mode1Cooked) + ); + assert_eq!( + format_from_data_mode(0x01), + Some(SectorReadFormat::Mode2FormlessCooked) + ); + assert_eq!( + format_from_data_mode(0x02), + Some(SectorReadFormat::Mode2Form1Cooked) + ); + assert_eq!( + format_from_data_mode(0x03), + Some(SectorReadFormat::Mode2Form2Cooked) + ); + } + + #[test] + fn rejects_unknown_or_reserved_mmc_data_modes() { + for data_mode in 0x04..=0x0F { + assert_eq!(format_from_data_mode(data_mode), None); + } + } +} diff --git a/src/data_reader/mod.rs b/src/data_reader/mod.rs index 0131cdf..c4b57b9 100644 --- a/src/data_reader/mod.rs +++ b/src/data_reader/mod.rs @@ -1,3 +1,4 @@ +mod detect; mod sector_read_format; pub(crate) mod track_information; diff --git a/src/errors.rs b/src/errors.rs index 0e2cf12..ed95d27 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -44,6 +44,13 @@ pub enum CdReaderError { Scsi(ScsiError), /// Parsing failure for command payloads (TOC/CD-TEXT/subchannel parsing). Parse(String), + /// The track's sector format could not be determined. + CannotDetectTrackFormat { + /// Track number from the TOC. + track_number: u8, + /// Unrecognized MMC Data Mode value, when one was reported. + data_mode: Option, + }, /// Drive enumeration completed without finding a usable audio CD. NoUsableDrive, } @@ -58,6 +65,17 @@ impl fmt::Display for CdReaderError { err.op, err.scsi_status, err.lba, err.sectors, err.sense_key, err.asc, err.ascq ), Self::Parse(msg) => write!(f, "parse error: {msg}"), + Self::CannotDetectTrackFormat { + track_number, + data_mode: Some(data_mode), + } => write!( + f, + "could not detect sector format for track {track_number}: unknown MMC data mode 0x{data_mode:02x}" + ), + Self::CannotDetectTrackFormat { + track_number, + data_mode: None, + } => write!(f, "could not detect sector format for track {track_number}"), Self::NoUsableDrive => write!(f, "no usable audio CD drive found"), } } @@ -67,7 +85,10 @@ impl std::error::Error for CdReaderError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Self::Io(error) => Some(error), - Self::Scsi(_) | Self::Parse(_) | Self::NoUsableDrive => None, + Self::Scsi(_) + | Self::Parse(_) + | Self::CannotDetectTrackFormat { .. } + | Self::NoUsableDrive => None, } } } diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs index 00d88d9..25da844 100644 --- a/src/platform/linux/mod.rs +++ b/src/platform/linux/mod.rs @@ -13,7 +13,6 @@ impl Drive { toc::read_toc(self) } - #[allow(dead_code)] pub(crate) fn read_track_information( &self, track_number: u8, diff --git a/src/platform/macos/mod.rs b/src/platform/macos/mod.rs index 1cea49a..a780fc1 100644 --- a/src/platform/macos/mod.rs +++ b/src/platform/macos/mod.rs @@ -13,7 +13,6 @@ impl Drive { toc::read_toc(self) } - #[allow(dead_code)] pub(crate) fn read_track_information( &self, track_number: u8, diff --git a/src/platform/windows/mod.rs b/src/platform/windows/mod.rs index ad856d5..2efbe9f 100644 --- a/src/platform/windows/mod.rs +++ b/src/platform/windows/mod.rs @@ -13,7 +13,6 @@ impl Drive { toc::read_toc(self) } - #[allow(dead_code)] pub(crate) fn read_track_information( &self, track_number: u8, From c5a77c85a8d81c32649cc2d5cb6f1e51650eac4b Mon Sep 17 00:00:00 2001 From: Seva Zaikov Date: Sun, 12 Jul 2026 09:39:18 -0700 Subject: [PATCH 5/6] fix Mode detection issue --- examples/detect_track_formats.rs | 14 ++++++++++++ src/data_reader/detect.rs | 37 ++++++++++++++------------------ src/errors.rs | 4 ++-- 3 files changed, 32 insertions(+), 23 deletions(-) create mode 100644 examples/detect_track_formats.rs diff --git a/examples/detect_track_formats.rs b/examples/detect_track_formats.rs new file mode 100644 index 0000000..3082a11 --- /dev/null +++ b/examples/detect_track_formats.rs @@ -0,0 +1,14 @@ +/// Detects the default read format for every track on the inserted disc. +use cd_da_reader::CdReader; + +fn main() -> Result<(), Box> { + let reader = CdReader::open_default()?; + let toc = reader.read_toc()?; + + for track in &toc.tracks { + let format = reader.detect_track_format(track)?; + println!("Track #{:>2}: {format:?}", track.number); + } + + Ok(()) +} diff --git a/src/data_reader/detect.rs b/src/data_reader/detect.rs index b19be37..86142f7 100644 --- a/src/data_reader/detect.rs +++ b/src/data_reader/detect.rs @@ -19,14 +19,15 @@ impl CdReader { } } -/// Map the MMC READ TRACK INFORMATION Data Mode field to the default cooked -/// representation for that physical sector layout. +/// Map the MMC READ TRACK INFORMATION Data Mode field to an unambiguous +/// default read format. +/// +/// MMC reports `0x01` for Mode 1 and `0x02` for Mode 2. Track Information does +/// not identify the per-sector Mode 2 form, so Mode 2 requires raw inspection +/// before a read format can be selected. fn format_from_data_mode(data_mode: u8) -> Option { match data_mode { - 0x00 => Some(SectorReadFormat::Mode1Cooked), - 0x01 => Some(SectorReadFormat::Mode2FormlessCooked), - 0x02 => Some(SectorReadFormat::Mode2Form1Cooked), - 0x03 => Some(SectorReadFormat::Mode2Form2Cooked), + 0x01 => Some(SectorReadFormat::Mode1Cooked), _ => None, } } @@ -52,28 +53,22 @@ mod tests { } #[test] - fn maps_known_mmc_data_modes_to_cooked_formats() { - assert_eq!( - format_from_data_mode(0x00), - Some(SectorReadFormat::Mode1Cooked) - ); + fn maps_mode1_to_its_cooked_format() { assert_eq!( format_from_data_mode(0x01), - Some(SectorReadFormat::Mode2FormlessCooked) - ); - assert_eq!( - format_from_data_mode(0x02), - Some(SectorReadFormat::Mode2Form1Cooked) - ); - assert_eq!( - format_from_data_mode(0x03), - Some(SectorReadFormat::Mode2Form2Cooked) + Some(SectorReadFormat::Mode1Cooked) ); } + #[test] + fn mode2_requires_raw_sector_inspection() { + assert_eq!(format_from_data_mode(0x02), None); + } + #[test] fn rejects_unknown_or_reserved_mmc_data_modes() { - for data_mode in 0x04..=0x0F { + assert_eq!(format_from_data_mode(0x00), None); + for data_mode in 0x03..=0x0F { assert_eq!(format_from_data_mode(data_mode), None); } } diff --git a/src/errors.rs b/src/errors.rs index ed95d27..3cd6fe9 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -48,7 +48,7 @@ pub enum CdReaderError { CannotDetectTrackFormat { /// Track number from the TOC. track_number: u8, - /// Unrecognized MMC Data Mode value, when one was reported. + /// MMC Data Mode value that was reported, when available. data_mode: Option, }, /// Drive enumeration completed without finding a usable audio CD. @@ -70,7 +70,7 @@ impl fmt::Display for CdReaderError { data_mode: Some(data_mode), } => write!( f, - "could not detect sector format for track {track_number}: unknown MMC data mode 0x{data_mode:02x}" + "could not detect sector format for track {track_number} from MMC data mode 0x{data_mode:02x}" ), Self::CannotDetectTrackFormat { track_number, From 74a70daf64741dadff7018202a5db2cd4dfcbdd4 Mon Sep 17 00:00:00 2001 From: Seva Zaikov Date: Sun, 12 Jul 2026 10:20:19 -0700 Subject: [PATCH 6/6] expose only Mode2Raw for now --- src/data_reader/detect.rs | 12 ++-- src/data_reader/sector_read_format.rs | 100 +++++--------------------- src/platform/macos/native/read_cd.c | 48 ++----------- src/platform/macos/read_cd.rs | 16 +---- 4 files changed, 33 insertions(+), 143 deletions(-) diff --git a/src/data_reader/detect.rs b/src/data_reader/detect.rs index 86142f7..37aed17 100644 --- a/src/data_reader/detect.rs +++ b/src/data_reader/detect.rs @@ -23,11 +23,12 @@ impl CdReader { /// default read format. /// /// MMC reports `0x01` for Mode 1 and `0x02` for Mode 2. Track Information does -/// not identify the per-sector Mode 2 form, so Mode 2 requires raw inspection -/// before a read format can be selected. +/// not identify the per-sector Mode 2 form, so Mode 2 is exposed as complete +/// raw sectors. fn format_from_data_mode(data_mode: u8) -> Option { match data_mode { 0x01 => Some(SectorReadFormat::Mode1Cooked), + 0x02 => Some(SectorReadFormat::Mode2Raw), _ => None, } } @@ -61,8 +62,11 @@ mod tests { } #[test] - fn mode2_requires_raw_sector_inspection() { - assert_eq!(format_from_data_mode(0x02), None); + fn maps_mode2_to_raw_sectors() { + assert_eq!( + format_from_data_mode(0x02), + Some(SectorReadFormat::Mode2Raw) + ); } #[test] diff --git a/src/data_reader/sector_read_format.rs b/src/data_reader/sector_read_format.rs index 33352ff..b3f308f 100644 --- a/src/data_reader/sector_read_format.rs +++ b/src/data_reader/sector_read_format.rs @@ -1,85 +1,47 @@ /// Sector format requested through the READ CD (0xBE) command. -/// -/// Each variant selects an expected sector type through CDB byte 1 and the -/// returned main-channel fields through CDB byte 9. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SectorReadFormat { - /// Complete 2352-byte main-channel sector without declaring an expected - /// sector type. - /// - /// This is primarily useful for sector-type detection. Some drives may not - /// support raw reads with an expected type of “Any.” - AnyRaw, - /// CD-DA audio: 2352 bytes of PCM per sector. Audio, - /// Mode 1 user data only: 2048 bytes per sector. Mode1Cooked, /// Complete Mode 1 sector: 2352 bytes with sync, header, user data, EDC, /// and ECC. Mode1Raw, - - /// Mode 2 formless user data: 2336 bytes per sector. - Mode2FormlessCooked, - /// Complete Mode 2 formless sector: 2352 bytes. - Mode2FormlessRaw, - - /// Mode 2 Form 1 user data: 2048 bytes per sector. - Mode2Form1Cooked, - /// Complete Mode 2 Form 1 sector: 2352 bytes. - Mode2Form1Raw, - - /// Mode 2 Form 2 user area: 2328 bytes per sector. + /// Complete Mode 2 sector: 2352 bytes. /// - /// This consists of 2324 bytes of application payload followed by its - /// 4-byte EDC, as exposed by MMC and macOS IOKit. - Mode2Form2Cooked, - /// Complete Mode 2 Form 2 sector: 2352 bytes. - Mode2Form2Raw, + /// Mode 2 forms are a per-sector property. Consumers that need the + /// application payload must inspect each sector's XA subheader. + Mode2Raw, } impl SectorReadFormat { /// Bytes returned per sector for this format. pub fn sector_size(&self) -> usize { match self { - Self::AnyRaw - | Self::Audio - | Self::Mode1Raw - | Self::Mode2FormlessRaw - | Self::Mode2Form1Raw - | Self::Mode2Form2Raw => 2352, - Self::Mode1Cooked | Self::Mode2Form1Cooked => 2048, - Self::Mode2FormlessCooked => 2336, - Self::Mode2Form2Cooked => 2328, + Self::Audio | Self::Mode1Raw | Self::Mode2Raw => 2352, + Self::Mode1Cooked => 2048, } } /// CDB byte 1: Expected Sector Type in bits 4–2. - pub fn cdb_byte1(&self) -> u8 { + #[cfg(any(target_os = "linux", target_os = "windows", test))] + pub(crate) fn cdb_byte1(&self) -> u8 { match self { - Self::AnyRaw => 0x00, Self::Audio => 0x04, Self::Mode1Cooked | Self::Mode1Raw => 0x08, - Self::Mode2FormlessCooked | Self::Mode2FormlessRaw => 0x0C, - Self::Mode2Form1Cooked | Self::Mode2Form1Raw => 0x10, - Self::Mode2Form2Cooked | Self::Mode2Form2Raw => 0x14, + // Mode 2 forms can be interleaved, so let the drive determine the + // actual sector type while returning the complete sector. + Self::Mode2Raw => 0x00, } } /// CDB byte 9: Main Channel Selection. - pub fn cdb_byte9(&self) -> u8 { + #[cfg(any(target_os = "linux", target_os = "windows", test))] + pub(crate) fn cdb_byte9(&self) -> u8 { match self { - Self::Audio - | Self::Mode1Cooked - | Self::Mode2FormlessCooked - | Self::Mode2Form1Cooked - | Self::Mode2Form2Cooked => 0x10, - Self::AnyRaw - | Self::Mode1Raw - | Self::Mode2FormlessRaw - | Self::Mode2Form1Raw - | Self::Mode2Form2Raw => 0xF8, + Self::Audio | Self::Mode1Cooked => 0x10, + Self::Mode1Raw | Self::Mode2Raw => 0xF8, } } @@ -98,59 +60,35 @@ mod tests { #[test] fn expected_sector_types_are_encoded_in_cdb_byte1() { - assert_eq!(SectorReadFormat::AnyRaw.cdb_byte1(), 0x00); assert_eq!(SectorReadFormat::Audio.cdb_byte1(), 0x04); assert_eq!(SectorReadFormat::Mode1Cooked.cdb_byte1(), 0x08); assert_eq!(SectorReadFormat::Mode1Raw.cdb_byte1(), 0x08); - assert_eq!(SectorReadFormat::Mode2FormlessCooked.cdb_byte1(), 0x0C); - assert_eq!(SectorReadFormat::Mode2FormlessRaw.cdb_byte1(), 0x0C); - assert_eq!(SectorReadFormat::Mode2Form1Cooked.cdb_byte1(), 0x10); - assert_eq!(SectorReadFormat::Mode2Form1Raw.cdb_byte1(), 0x10); - assert_eq!(SectorReadFormat::Mode2Form2Cooked.cdb_byte1(), 0x14); - assert_eq!(SectorReadFormat::Mode2Form2Raw.cdb_byte1(), 0x14); + assert_eq!(SectorReadFormat::Mode2Raw.cdb_byte1(), 0x00); } #[test] fn main_channel_fields_are_encoded_in_cdb_byte9() { - assert_eq!(SectorReadFormat::AnyRaw.cdb_byte9(), 0xF8); assert_eq!(SectorReadFormat::Audio.cdb_byte9(), 0x10); assert_eq!(SectorReadFormat::Mode1Cooked.cdb_byte9(), 0x10); assert_eq!(SectorReadFormat::Mode1Raw.cdb_byte9(), 0xF8); - assert_eq!(SectorReadFormat::Mode2FormlessCooked.cdb_byte9(), 0x10); - assert_eq!(SectorReadFormat::Mode2FormlessRaw.cdb_byte9(), 0xF8); - assert_eq!(SectorReadFormat::Mode2Form1Cooked.cdb_byte9(), 0x10); - assert_eq!(SectorReadFormat::Mode2Form1Raw.cdb_byte9(), 0xF8); - assert_eq!(SectorReadFormat::Mode2Form2Cooked.cdb_byte9(), 0x10); - assert_eq!(SectorReadFormat::Mode2Form2Raw.cdb_byte9(), 0xF8); + assert_eq!(SectorReadFormat::Mode2Raw.cdb_byte9(), 0xF8); } #[test] fn sector_sizes_match_mmc_layouts() { - assert_eq!(SectorReadFormat::AnyRaw.sector_size(), 2352); assert_eq!(SectorReadFormat::Audio.sector_size(), 2352); assert_eq!(SectorReadFormat::Mode1Cooked.sector_size(), 2048); assert_eq!(SectorReadFormat::Mode1Raw.sector_size(), 2352); - assert_eq!(SectorReadFormat::Mode2FormlessCooked.sector_size(), 2336); - assert_eq!(SectorReadFormat::Mode2FormlessRaw.sector_size(), 2352); - assert_eq!(SectorReadFormat::Mode2Form1Cooked.sector_size(), 2048); - assert_eq!(SectorReadFormat::Mode2Form1Raw.sector_size(), 2352); - assert_eq!(SectorReadFormat::Mode2Form2Cooked.sector_size(), 2328); - assert_eq!(SectorReadFormat::Mode2Form2Raw.sector_size(), 2352); + assert_eq!(SectorReadFormat::Mode2Raw.sector_size(), 2352); } #[test] fn transfer_caps_stay_within_64_kib() { for format in [ - SectorReadFormat::AnyRaw, SectorReadFormat::Audio, SectorReadFormat::Mode1Cooked, SectorReadFormat::Mode1Raw, - SectorReadFormat::Mode2FormlessCooked, - SectorReadFormat::Mode2FormlessRaw, - SectorReadFormat::Mode2Form1Cooked, - SectorReadFormat::Mode2Form1Raw, - SectorReadFormat::Mode2Form2Cooked, - SectorReadFormat::Mode2Form2Raw, + SectorReadFormat::Mode2Raw, ] { let bytes = format.max_sectors_per_xfer() as usize * format.sector_size(); assert!(bytes <= 64 * 1024); diff --git a/src/platform/macos/native/read_cd.c b/src/platform/macos/native/read_cd.c index d39cb40..815aef9 100644 --- a/src/platform/macos/native/read_cd.c +++ b/src/platform/macos/native/read_cd.c @@ -5,16 +5,10 @@ // Rust) means the IOKit constants only ever appear where their header is // imported. // -// 0 = Audio -> user, CDDA, 2352 B/sector -// 1 = Mode1Cooked -> user, Mode 1, 2048 B/sector -// 2 = Mode1Raw -> full Mode 1, 2352 B/sector -// 3 = Mode2FormlessCooked -> user, Mode 2, 2336 B/sector -// 4 = Mode2FormlessRaw -> full Mode 2, 2352 B/sector -// 5 = Mode2Form1Cooked -> user, Mode 2 Form 1, 2048 B/sector -// 6 = Mode2Form1Raw -> full Mode 2 Form 1, 2352 B/sector -// 7 = Mode2Form2Cooked -> user, Mode 2 Form 2, 2328 B/sector -// 8 = Mode2Form2Raw -> full Mode 2 Form 2, 2352 B/sector -// 9 = AnyRaw -> full sector, unknown type, 2352 B/sector +// 0 = Audio -> user, CDDA, 2352 B/sector +// 1 = Mode1Cooked -> user, Mode 1, 2048 B/sector +// 2 = Mode1Raw -> full Mode 1, 2352 B/sector +// 3 = Mode2Raw -> full sector, unknown type, 2352 B/sector static bool sector_layout_for_format(uint32_t format_id, CDSectorArea *outArea, CDSectorType *outType, @@ -37,40 +31,6 @@ static bool sector_layout_for_format(uint32_t format_id, *outSectorSize = 2352; return true; case 3: - *outArea = kCDSectorAreaUser; - *outType = kCDSectorTypeMode2; - *outSectorSize = 2336; - return true; - case 4: - *outArea = (CDSectorArea)(kCDSectorAreaSync | kCDSectorAreaHeader | - kCDSectorAreaUser); - *outType = kCDSectorTypeMode2; - *outSectorSize = 2352; - return true; - case 5: - *outArea = kCDSectorAreaUser; - *outType = kCDSectorTypeMode2Form1; - *outSectorSize = 2048; - return true; - case 6: - *outArea = (CDSectorArea)(kCDSectorAreaSync | kCDSectorAreaHeader | - kCDSectorAreaSubHeader | kCDSectorAreaUser | - kCDSectorAreaAuxiliary); - *outType = kCDSectorTypeMode2Form1; - *outSectorSize = 2352; - return true; - case 7: - *outArea = kCDSectorAreaUser; - *outType = kCDSectorTypeMode2Form2; - *outSectorSize = 2328; - return true; - case 8: - *outArea = (CDSectorArea)(kCDSectorAreaSync | kCDSectorAreaHeader | - kCDSectorAreaSubHeader | kCDSectorAreaUser); - *outType = kCDSectorTypeMode2Form2; - *outSectorSize = 2352; - return true; - case 9: *outArea = (CDSectorArea)(kCDSectorAreaSync | kCDSectorAreaHeader | kCDSectorAreaSubHeader | kCDSectorAreaUser | kCDSectorAreaAuxiliary); diff --git a/src/platform/macos/read_cd.rs b/src/platform/macos/read_cd.rs index 7d87e5f..18a2bec 100644 --- a/src/platform/macos/read_cd.rs +++ b/src/platform/macos/read_cd.rs @@ -41,13 +41,7 @@ fn format_id(format: SectorReadFormat) -> u32 { SectorReadFormat::Audio => 0, SectorReadFormat::Mode1Cooked => 1, SectorReadFormat::Mode1Raw => 2, - SectorReadFormat::Mode2FormlessCooked => 3, - SectorReadFormat::Mode2FormlessRaw => 4, - SectorReadFormat::Mode2Form1Cooked => 5, - SectorReadFormat::Mode2Form1Raw => 6, - SectorReadFormat::Mode2Form2Cooked => 7, - SectorReadFormat::Mode2Form2Raw => 8, - SectorReadFormat::AnyRaw => 9, + SectorReadFormat::Mode2Raw => 3, } } @@ -61,12 +55,6 @@ mod tests { assert_eq!(format_id(SectorReadFormat::Audio), 0); assert_eq!(format_id(SectorReadFormat::Mode1Cooked), 1); assert_eq!(format_id(SectorReadFormat::Mode1Raw), 2); - assert_eq!(format_id(SectorReadFormat::Mode2FormlessCooked), 3); - assert_eq!(format_id(SectorReadFormat::Mode2FormlessRaw), 4); - assert_eq!(format_id(SectorReadFormat::Mode2Form1Cooked), 5); - assert_eq!(format_id(SectorReadFormat::Mode2Form1Raw), 6); - assert_eq!(format_id(SectorReadFormat::Mode2Form2Cooked), 7); - assert_eq!(format_id(SectorReadFormat::Mode2Form2Raw), 8); - assert_eq!(format_id(SectorReadFormat::AnyRaw), 9); + assert_eq!(format_id(SectorReadFormat::Mode2Raw), 3); } }