Skip to main content

cd_da_reader/data_reader/
sector_read_format.rs

1/// Selects the type and layout of sectors returned when reading from an optical drive.
2///
3/// This is the crate's platform-independent representation of the sector type and
4/// main-channel fields requested by the MMC `READ CD` command (`0xBE`) or the
5/// equivalent platform API.
6///
7/// [`ReadOptions`](crate::ReadOptions) defaults to [`Audio`](Self::Audio), so
8/// callers reading audio tracks normally do not need to select a format. For a
9/// data track, call
10/// [`CdReader::detect_track_format`](crate::CdReader::detect_track_format) and
11/// pass the result to [`ReadOptions::with_format`](crate::ReadOptions::with_format). Detection
12/// chooses [`Mode1Cooked`](Self::Mode1Cooked) for Mode 1 tracks and
13/// [`Mode2Raw`](Self::Mode2Raw) for Mode 2 tracks.
14///
15/// Selecting a format does not convert the sectors. It tells the drive what
16/// sector type and fields to return, so the selection must match the track being
17/// read. A mismatched format may be rejected by the library or the drive.
18///
19/// The `Raw` variants return the complete 2,352-byte main-channel sector. They
20/// do not include subchannel data or C2 error information. Use
21/// [`sector_size`](Self::sector_size) to obtain the number of bytes returned per
22/// sector for any variant.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum SectorReadFormat {
25    /// CD-DA audio as 2,352 bytes of headerless PCM per sector.
26    ///
27    /// The samples are signed 16-bit little-endian stereo at 44.1 kHz. Each
28    /// sector contains 588 stereo sample frames and represents 1/75 second of
29    /// audio. The returned bytes can be passed directly to [`create_wav`](crate::create_wav).
30    Audio,
31
32    /// The 2,048-byte user-data field from a Mode 1 sector.
33    ///
34    /// The drive omits the sync pattern, sector header, Error Detection Code
35    /// (EDC), reserved bytes, and Error Correction Code (ECC). This is usually
36    /// the preferred representation for reading filesystems; concatenating the
37    /// cooked sectors of a typical ISO 9660 track produces a directly usable
38    /// disc image.
39    Mode1Cooked,
40
41    /// A complete 2,352-byte Mode 1 main-channel sector.
42    ///
43    /// This includes the 12-byte sync pattern, 4-byte header, 2,048-byte user
44    /// data field, EDC, reserved bytes, and ECC. Use this when preserving or
45    /// inspecting the original sector framing. For normal filesystem access,
46    /// [`Mode1Cooked`](Self::Mode1Cooked) is usually more convenient.
47    Mode1Raw,
48
49    /// A complete 2,352-byte Mode 2 main-channel sector.
50    ///
51    /// This is the only Mode 2 representation provided by the crate. Mode 2 XA
52    /// tracks can mix Form 1 and Form 2 sectors within the same track: Form 1
53    /// carries 2,048 bytes of user data with stronger error correction, while
54    /// Form 2 carries 2,324 bytes of user data.
55    ///
56    /// The form is recorded in each sector's XA subheader. The crate does not
57    /// expose a cooked Mode 2 reader or a public XA payload parser, so callers
58    /// must inspect each sector and extract the appropriate payload themselves.
59    Mode2Raw,
60}
61
62impl SectorReadFormat {
63    pub(crate) fn is_audio(&self) -> bool {
64        matches!(self, Self::Audio)
65    }
66
67    /// Bytes returned per sector for this format.
68    pub fn sector_size(&self) -> usize {
69        match self {
70            Self::Audio | Self::Mode1Raw | Self::Mode2Raw => 2352,
71            Self::Mode1Cooked => 2048,
72        }
73    }
74
75    /// CDB byte 1: Expected Sector Type in bits 4–2.
76    #[cfg(any(target_os = "linux", target_os = "windows", test))]
77    pub(crate) fn cdb_byte1(&self) -> u8 {
78        match self {
79            Self::Audio => 0x04,
80            Self::Mode1Cooked | Self::Mode1Raw => 0x08,
81            // Mode 2 forms can be interleaved, so let the drive determine the
82            // actual sector type while returning the complete sector.
83            Self::Mode2Raw => 0x00,
84        }
85    }
86
87    /// CDB byte 9: Main Channel Selection.
88    #[cfg(any(target_os = "linux", target_os = "windows", test))]
89    pub(crate) fn cdb_byte9(&self) -> u8 {
90        match self {
91            Self::Audio | Self::Mode1Cooked => 0x10,
92            Self::Mode1Raw | Self::Mode2Raw => 0xF8,
93        }
94    }
95
96    /// Maximum sectors per single `READ CD` command.
97    ///
98    /// Transfers are kept at approximately 64 KiB for compatibility with
99    /// optical-drive firmware and USB bridges.
100    pub(crate) fn max_sectors_per_xfer(&self) -> u32 {
101        (64 * 1024 / self.sector_size() as u32).max(1)
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::SectorReadFormat;
108
109    #[test]
110    fn expected_sector_types_are_encoded_in_cdb_byte1() {
111        assert_eq!(SectorReadFormat::Audio.cdb_byte1(), 0x04);
112        assert_eq!(SectorReadFormat::Mode1Cooked.cdb_byte1(), 0x08);
113        assert_eq!(SectorReadFormat::Mode1Raw.cdb_byte1(), 0x08);
114        assert_eq!(SectorReadFormat::Mode2Raw.cdb_byte1(), 0x00);
115    }
116
117    #[test]
118    fn main_channel_fields_are_encoded_in_cdb_byte9() {
119        assert_eq!(SectorReadFormat::Audio.cdb_byte9(), 0x10);
120        assert_eq!(SectorReadFormat::Mode1Cooked.cdb_byte9(), 0x10);
121        assert_eq!(SectorReadFormat::Mode1Raw.cdb_byte9(), 0xF8);
122        assert_eq!(SectorReadFormat::Mode2Raw.cdb_byte9(), 0xF8);
123    }
124
125    #[test]
126    fn sector_sizes_match_mmc_layouts() {
127        assert_eq!(SectorReadFormat::Audio.sector_size(), 2352);
128        assert_eq!(SectorReadFormat::Mode1Cooked.sector_size(), 2048);
129        assert_eq!(SectorReadFormat::Mode1Raw.sector_size(), 2352);
130        assert_eq!(SectorReadFormat::Mode2Raw.sector_size(), 2352);
131    }
132
133    #[test]
134    fn transfer_caps_stay_within_64_kib() {
135        for format in [
136            SectorReadFormat::Audio,
137            SectorReadFormat::Mode1Cooked,
138            SectorReadFormat::Mode1Raw,
139            SectorReadFormat::Mode2Raw,
140        ] {
141            let bytes = format.max_sectors_per_xfer() as usize * format.sector_size();
142            assert!(bytes <= 64 * 1024);
143        }
144    }
145}