Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 39 additions & 4 deletions src/data_reader/detect.rs
Original file line number Diff line number Diff line change
@@ -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<SectorReadFormat, CdReaderError> {
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),
})
Expand All @@ -33,10 +40,26 @@ fn format_from_data_mode(data_mode: u8) -> Option<SectorReadFormat> {
}
}

fn format_from_raw_sector(sector: &[u8]) -> Result<SectorReadFormat, RawSectorParseError> {
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();
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/data_reader/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod detect;
mod raw_sector;
mod sector_read_format;
pub(crate) mod track_information;

Expand Down
214 changes: 214 additions & 0 deletions src/data_reader/raw_sector.rs
Original file line number Diff line number Diff line change
@@ -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<usize> = 16..20;
const XA_SUBHEADER_SECOND: std::ops::Range<usize> = 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<XaForm>,
}

#[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<RawSectorInfo, RawSectorParseError> {
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<XaForm> {
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(&sector(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(&sector).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(&sector).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(&sector(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(&sector).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(&sector(0)),
Err(RawSectorParseError::UnsupportedMode(0))
);
assert_eq!(
parse_raw_sector(&sector(3)),
Err(RawSectorParseError::UnsupportedMode(3))
);
}
}
Loading