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/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.rs b/src/data_reader.rs deleted file mode 100644 index d828b84..0000000 --- a/src/data_reader.rs +++ /dev/null @@ -1,175 +0,0 @@ -use crate::retry::RetryConfig; - -/// Sector format and retry options for track and sector-range reads. -/// -/// The defaults read audio sectors using the default retry policy. Use the -/// builder methods to override only the options you need. -#[derive(Debug, Clone)] -pub struct ReadOptions { - format: SectorReadFormat, - retry: RetryConfig, -} - -impl ReadOptions { - /// Select the sector format requested from the drive. - pub fn with_format(mut self, format: SectorReadFormat) -> Self { - self.format = format; - self - } - - /// Set the retry policy applied to each read command. - pub fn with_retry(mut self, retry: RetryConfig) -> Self { - self.retry = retry; - self - } - - pub(crate) fn format(&self) -> SectorReadFormat { - self.format - } - - pub(crate) fn retry(&self) -> &RetryConfig { - &self.retry - } -} - -impl Default for ReadOptions { - fn default() -> Self { - Self { - format: SectorReadFormat::Audio, - retry: RetryConfig::default(), - } - } -} - -/// 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] { - let mut cdb = [0u8; 12]; - cdb[0] = 0xBE; - cdb[1] = format.cdb_byte1(); - cdb[2..6].copy_from_slice(&lba.to_be_bytes()); - cdb[6] = ((sectors >> 16) & 0xFF) as u8; - cdb[7] = ((sectors >> 8) & 0xFF) as u8; - cdb[8] = (sectors & 0xFF) as u8; - cdb[9] = format.cdb_byte9(); - cdb -} - -#[cfg(test)] -mod tests { - use super::{ReadOptions, SectorReadFormat, build_read_cd_cdb}; - - #[test] - fn read_options_builders_override_individual_defaults() { - assert_eq!(ReadOptions::default().format(), SectorReadFormat::Audio); - - let retry = crate::RetryConfig::default().with_max_attempts(9); - let options = ReadOptions::default() - .with_format(SectorReadFormat::Mode1Raw) - .with_retry(retry); - - assert_eq!(options.format(), SectorReadFormat::Mode1Raw); - 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!( - build_read_cd_cdb(0x1234_5678, 0x0000_ABCD, SectorReadFormat::Mode1Raw), - [ - 0xBE, 0x08, 0x12, 0x34, 0x56, 0x78, 0x00, 0xAB, 0xCD, 0xF8, 0x00, 0x00, - ] - ); - } -} diff --git a/src/data_reader/detect.rs b/src/data_reader/detect.rs new file mode 100644 index 0000000..37aed17 --- /dev/null +++ b/src/data_reader/detect.rs @@ -0,0 +1,79 @@ +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 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 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, + } +} + +#[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_mode1_to_its_cooked_format() { + assert_eq!( + format_from_data_mode(0x01), + Some(SectorReadFormat::Mode1Cooked) + ); + } + + #[test] + fn maps_mode2_to_raw_sectors() { + assert_eq!( + format_from_data_mode(0x02), + Some(SectorReadFormat::Mode2Raw) + ); + } + + #[test] + fn rejects_unknown_or_reserved_mmc_data_modes() { + 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/data_reader/mod.rs b/src/data_reader/mod.rs new file mode 100644 index 0000000..c4b57b9 --- /dev/null +++ b/src/data_reader/mod.rs @@ -0,0 +1,90 @@ +mod detect; +mod sector_read_format; +pub(crate) mod track_information; + +pub use sector_read_format::SectorReadFormat; + +use crate::retry::RetryConfig; + +/// Sector format and retry options for track and sector-range reads. +/// +/// The defaults read audio sectors using the default retry policy. Use the +/// builder methods to override only the options you need. +#[derive(Debug, Clone)] +pub struct ReadOptions { + format: SectorReadFormat, + retry: RetryConfig, +} + +impl ReadOptions { + /// Select the sector format requested from the drive. + pub fn with_format(mut self, format: SectorReadFormat) -> Self { + self.format = format; + self + } + + /// Set the retry policy applied to each read command. + pub fn with_retry(mut self, retry: RetryConfig) -> Self { + self.retry = retry; + self + } + + pub(crate) fn format(&self) -> SectorReadFormat { + self.format + } + + pub(crate) fn retry(&self) -> &RetryConfig { + &self.retry + } +} + +impl Default for ReadOptions { + fn default() -> Self { + Self { + format: SectorReadFormat::Audio, + retry: RetryConfig::default(), + } + } +} + +/// 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] { + let mut cdb = [0u8; 12]; + cdb[0] = 0xBE; + cdb[1] = format.cdb_byte1(); + cdb[2..6].copy_from_slice(&lba.to_be_bytes()); + cdb[6] = ((sectors >> 16) & 0xFF) as u8; + cdb[7] = ((sectors >> 8) & 0xFF) as u8; + cdb[8] = (sectors & 0xFF) as u8; + cdb[9] = format.cdb_byte9(); + cdb +} + +#[cfg(test)] +mod tests { + use super::{ReadOptions, SectorReadFormat, build_read_cd_cdb}; + + #[test] + fn read_options_builders_override_individual_defaults() { + assert_eq!(ReadOptions::default().format(), SectorReadFormat::Audio); + + let retry = crate::RetryConfig::default().with_max_attempts(9); + let options = ReadOptions::default() + .with_format(SectorReadFormat::Mode1Raw) + .with_retry(retry); + + assert_eq!(options.format(), SectorReadFormat::Mode1Raw); + assert_eq!(options.retry().max_attempts, 9); + } + + #[test] + fn builds_read_cd_cdb() { + assert_eq!( + build_read_cd_cdb(0x1234_5678, 0x0000_ABCD, SectorReadFormat::Mode1Raw), + [ + 0xBE, 0x08, 0x12, 0x34, 0x56, 0x78, 0x00, 0xAB, 0xCD, 0xF8, 0x00, 0x00, + ] + ); + } +} diff --git a/src/data_reader/sector_read_format.rs b/src/data_reader/sector_read_format.rs new file mode 100644 index 0000000..b3f308f --- /dev/null +++ b/src/data_reader/sector_read_format.rs @@ -0,0 +1,97 @@ +/// Sector format requested through the READ CD (0xBE) command. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SectorReadFormat { + /// 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, + /// Complete Mode 2 sector: 2352 bytes. + /// + /// 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::Audio | Self::Mode1Raw | Self::Mode2Raw => 2352, + Self::Mode1Cooked => 2048, + } + } + + /// CDB byte 1: Expected Sector Type in bits 4–2. + #[cfg(any(target_os = "linux", target_os = "windows", test))] + pub(crate) fn cdb_byte1(&self) -> u8 { + match self { + Self::Audio => 0x04, + Self::Mode1Cooked | Self::Mode1Raw => 0x08, + // 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. + #[cfg(any(target_os = "linux", target_os = "windows", test))] + pub(crate) fn cdb_byte9(&self) -> u8 { + match self { + Self::Audio | Self::Mode1Cooked => 0x10, + Self::Mode1Raw | Self::Mode2Raw => 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::Audio.cdb_byte1(), 0x04); + assert_eq!(SectorReadFormat::Mode1Cooked.cdb_byte1(), 0x08); + assert_eq!(SectorReadFormat::Mode1Raw.cdb_byte1(), 0x08); + assert_eq!(SectorReadFormat::Mode2Raw.cdb_byte1(), 0x00); + } + + #[test] + fn main_channel_fields_are_encoded_in_cdb_byte9() { + assert_eq!(SectorReadFormat::Audio.cdb_byte9(), 0x10); + assert_eq!(SectorReadFormat::Mode1Cooked.cdb_byte9(), 0x10); + assert_eq!(SectorReadFormat::Mode1Raw.cdb_byte9(), 0xF8); + assert_eq!(SectorReadFormat::Mode2Raw.cdb_byte9(), 0xF8); + } + + #[test] + fn sector_sizes_match_mmc_layouts() { + assert_eq!(SectorReadFormat::Audio.sector_size(), 2352); + assert_eq!(SectorReadFormat::Mode1Cooked.sector_size(), 2048); + assert_eq!(SectorReadFormat::Mode1Raw.sector_size(), 2352); + assert_eq!(SectorReadFormat::Mode2Raw.sector_size(), 2352); + } + + #[test] + fn transfer_caps_stay_within_64_kib() { + for format in [ + SectorReadFormat::Audio, + SectorReadFormat::Mode1Cooked, + SectorReadFormat::Mode1Raw, + SectorReadFormat::Mode2Raw, + ] { + let bytes = format.max_sectors_per_xfer() as usize * format.sector_size(); + assert!(bytes <= 64 * 1024); + } + } +} 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..3cd6fe9 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, } @@ -42,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, + /// MMC Data Mode value that was reported, when available. + data_mode: Option, + }, /// Drive enumeration completed without finding a usable audio CD. NoUsableDrive, } @@ -56,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} from 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"), } } @@ -65,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 65fda4f..25da844 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,13 @@ impl Drive { toc::read_toc(self) } + 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..a780fc1 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,13 @@ impl Drive { toc::read_toc(self) } + 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/read_cd.c b/src/platform/macos/native/read_cd.c index 29e4b7f..815aef9 100644 --- a/src/platform/macos/native/read_cd.c +++ b/src/platform/macos/native/read_cd.c @@ -5,13 +5,14 @@ // 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 = Mode2Raw -> 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 +30,13 @@ static bool sector_layout_for_format(uint32_t format_id, *outType = kCDSectorTypeMode1; *outSectorSize = 2352; return true; + case 3: + *outArea = (CDSectorArea)(kCDSectorAreaSync | kCDSectorAreaHeader | + kCDSectorAreaSubHeader | kCDSectorAreaUser | + kCDSectorAreaAuxiliary); + *outType = kCDSectorTypeUnknown; + *outSectorSize = 2352; + return true; default: return false; } 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/read_cd.rs b/src/platform/macos/read_cd.rs index 528f423..18a2bec 100644 --- a/src/platform/macos/read_cd.rs +++ b/src/platform/macos/read_cd.rs @@ -41,6 +41,7 @@ fn format_id(format: SectorReadFormat) -> u32 { SectorReadFormat::Audio => 0, SectorReadFormat::Mode1Cooked => 1, SectorReadFormat::Mode1Raw => 2, + SectorReadFormat::Mode2Raw => 3, } } @@ -54,5 +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::Mode2Raw), 3); } } 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..2efbe9f 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,13 @@ impl Drive { toc::read_toc(self) } + 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())) +}