diff --git a/src/data_reader/detect.rs b/src/data_reader/detect.rs index 37aed17..3a02610 100644 --- a/src/data_reader/detect.rs +++ b/src/data_reader/detect.rs @@ -1,18 +1,25 @@ -use crate::{CdReader, CdReaderError, SectorReadFormat, Track}; +use super::raw_sector::{RawSectorMode, RawSectorParseError, parse_raw_sector}; +use crate::{CdReader, CdReaderError, ReadOptions, 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. + /// queried with MMC READ TRACK INFORMATION. If its Data Mode is + /// inconclusive, one raw sector is inspected as a fallback. 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 { + if let Some(format) = format_from_data_mode(information.data_mode) { + return Ok(format); + } + + let options = ReadOptions::default().with_format(SectorReadFormat::Mode2Raw); + let raw_sector = self.read_sector_range(track.start_lba, 1, &options)?; + format_from_raw_sector(&raw_sector).map_err(|_| CdReaderError::CannotDetectTrackFormat { track_number: track.number, data_mode: Some(information.data_mode), }) @@ -33,10 +40,26 @@ fn format_from_data_mode(data_mode: u8) -> Option { } } +fn format_from_raw_sector(sector: &[u8]) -> Result { + match parse_raw_sector(sector)?.mode { + RawSectorMode::Mode1 => Ok(SectorReadFormat::Mode1Cooked), + RawSectorMode::Mode2 => Ok(SectorReadFormat::Mode2Raw), + } +} + #[cfg(test)] mod tests { use super::*; + fn raw_sector(mode: u8) -> [u8; 2352] { + let mut sector = [0u8; 2352]; + sector[0] = 0x00; + sector[1..11].fill(0xFF); + sector[11] = 0x00; + sector[15] = mode; + sector + } + #[test] fn audio_tracks_do_not_require_drive_detection() { let reader = CdReader::test_reader(); @@ -69,6 +92,18 @@ mod tests { ); } + #[test] + fn maps_raw_sector_modes_to_supported_formats() { + assert_eq!( + format_from_raw_sector(&raw_sector(1)).unwrap(), + SectorReadFormat::Mode1Cooked + ); + assert_eq!( + format_from_raw_sector(&raw_sector(2)).unwrap(), + SectorReadFormat::Mode2Raw + ); + } + #[test] fn rejects_unknown_or_reserved_mmc_data_modes() { assert_eq!(format_from_data_mode(0x00), None); diff --git a/src/data_reader/mod.rs b/src/data_reader/mod.rs index c4b57b9..ec2ff10 100644 --- a/src/data_reader/mod.rs +++ b/src/data_reader/mod.rs @@ -1,4 +1,5 @@ mod detect; +mod raw_sector; mod sector_read_format; pub(crate) mod track_information; diff --git a/src/data_reader/raw_sector.rs b/src/data_reader/raw_sector.rs new file mode 100644 index 0000000..9d8a21f --- /dev/null +++ b/src/data_reader/raw_sector.rs @@ -0,0 +1,214 @@ +#![cfg_attr(not(test), allow(dead_code))] + +use std::fmt; + +const RAW_SECTOR_SIZE: usize = 2352; +const MODE_OFFSET: usize = 15; +const XA_SUBHEADER_FIRST: std::ops::Range = 16..20; +const XA_SUBHEADER_SECOND: std::ops::Range = 20..24; +const XA_SUBMODE_OFFSET: usize = 18; +const XA_FORM2_BIT: u8 = 0x20; +const XA_CONTENT_TYPE_MASK: u8 = 0x0E; +const SYNC_PATTERN: [u8; 12] = [ + 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, +]; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum RawSectorMode { + Mode1, + Mode2, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum XaForm { + Form1, + Form2, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct RawSectorInfo { + pub(super) mode: RawSectorMode, + /// XA form observed in this sector when both subheader copies agree. + pub(super) xa_form: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum RawSectorParseError { + InvalidLength { actual: usize }, + InvalidSync, + UnsupportedMode(u8), +} + +impl fmt::Display for RawSectorParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidLength { actual } => { + write!( + f, + "raw sector has {actual} bytes, expected {RAW_SECTOR_SIZE}" + ) + } + Self::InvalidSync => write!(f, "raw sector has an invalid sync pattern"), + Self::UnsupportedMode(mode) => { + write!(f, "raw sector has unsupported mode 0x{mode:02x}") + } + } + } +} + +impl std::error::Error for RawSectorParseError {} + +/// Parse one complete 2352-byte data sector. +pub(super) fn parse_raw_sector(sector: &[u8]) -> Result { + if sector.len() != RAW_SECTOR_SIZE { + return Err(RawSectorParseError::InvalidLength { + actual: sector.len(), + }); + } + + if sector[..SYNC_PATTERN.len()] != SYNC_PATTERN { + return Err(RawSectorParseError::InvalidSync); + } + + match sector[MODE_OFFSET] { + 0x01 => Ok(RawSectorInfo { + mode: RawSectorMode::Mode1, + xa_form: None, + }), + 0x02 => Ok(RawSectorInfo { + mode: RawSectorMode::Mode2, + xa_form: parse_xa_form(sector), + }), + mode => Err(RawSectorParseError::UnsupportedMode(mode)), + } +} + +fn parse_xa_form(sector: &[u8]) -> Option { + if sector[XA_SUBHEADER_FIRST] != sector[XA_SUBHEADER_SECOND] { + return None; + } + + let submode = sector[XA_SUBMODE_OFFSET]; + if submode & XA_CONTENT_TYPE_MASK == 0 { + return None; + } + + if submode & XA_FORM2_BIT == 0 { + Some(XaForm::Form1) + } else { + Some(XaForm::Form2) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sector(mode: u8) -> [u8; RAW_SECTOR_SIZE] { + let mut sector = [0u8; RAW_SECTOR_SIZE]; + sector[..SYNC_PATTERN.len()].copy_from_slice(&SYNC_PATTERN); + sector[MODE_OFFSET] = mode; + sector + } + + fn set_xa_subheader(sector: &mut [u8], submode: u8) { + let subheader = [1, 2, submode, 4]; + sector[XA_SUBHEADER_FIRST].copy_from_slice(&subheader); + sector[XA_SUBHEADER_SECOND].copy_from_slice(&subheader); + } + + #[test] + fn parses_mode1() { + assert_eq!( + parse_raw_sector(§or(1)).unwrap(), + RawSectorInfo { + mode: RawSectorMode::Mode1, + xa_form: None, + } + ); + } + + #[test] + fn parses_mode2_form1() { + let mut sector = sector(2); + set_xa_subheader(&mut sector, 0x08); + + assert_eq!( + parse_raw_sector(§or).unwrap(), + RawSectorInfo { + mode: RawSectorMode::Mode2, + xa_form: Some(XaForm::Form1), + } + ); + } + + #[test] + fn parses_mode2_form2() { + let mut sector = sector(2); + set_xa_subheader(&mut sector, 0x28); + + assert_eq!( + parse_raw_sector(§or).unwrap(), + RawSectorInfo { + mode: RawSectorMode::Mode2, + xa_form: Some(XaForm::Form2), + } + ); + } + + #[test] + fn leaves_xa_form_unknown_without_a_valid_xa_content_type() { + assert_eq!( + parse_raw_sector(§or(2)).unwrap(), + RawSectorInfo { + mode: RawSectorMode::Mode2, + xa_form: None, + } + ); + } + + #[test] + fn leaves_xa_form_unknown_when_subheaders_disagree() { + let mut sector = sector(2); + sector[XA_SUBHEADER_FIRST].copy_from_slice(&[1, 2, 0x08, 4]); + sector[XA_SUBHEADER_SECOND].copy_from_slice(&[1, 2, 0x28, 4]); + + assert_eq!( + parse_raw_sector(§or).unwrap(), + RawSectorInfo { + mode: RawSectorMode::Mode2, + xa_form: None, + } + ); + } + + #[test] + fn rejects_invalid_length() { + assert_eq!( + parse_raw_sector(&[0; RAW_SECTOR_SIZE - 1]), + Err(RawSectorParseError::InvalidLength { + actual: RAW_SECTOR_SIZE - 1, + }) + ); + } + + #[test] + fn rejects_invalid_sync() { + assert_eq!( + parse_raw_sector(&[0; RAW_SECTOR_SIZE]), + Err(RawSectorParseError::InvalidSync) + ); + } + + #[test] + fn rejects_unsupported_mode() { + assert_eq!( + parse_raw_sector(§or(0)), + Err(RawSectorParseError::UnsupportedMode(0)) + ); + assert_eq!( + parse_raw_sector(§or(3)), + Err(RawSectorParseError::UnsupportedMode(3)) + ); + } +}