Skip to main content

libcdio_rs/mmc/
read_cd.rs

1// Copyright (C) 2026 Shiva Kiran Koninty <shiva@skran.xyz>
2//
3// This file is part of libcdio-rs.
4//
5// libcdio-rs is free software: you can redistribute it and/or
6// modify it under the terms of the GNU General Public License as
7// published by the Free Software Foundation, either version 3 of the
8// License, or (at your option) any later version.
9//
10// libcdio-rs is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13// General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with libcdio-rs. If not, see <https://www.gnu.org/licenses/>.
17
18//! Routines based on `READ CD`.
19// See MMC-6 2g, 6.19 READ CD Command
20
21use docsplay::Display;
22use thiserror::Error;
23
24use crate::{
25    Mmc,
26    mmc::{Cdb, MmcCommand, MmcDirection, MmcError},
27};
28
29/// Routines based on `READ CD`.
30impl Mmc {
31    /// Read disc using MMC `READ CD`.
32    ///
33    /// The transfer size is dependent on the opted fields in the read options.
34    /// See [`ReadCdOptions`].
35    pub fn read_cd(
36        &self,
37        options: ReadCdOptions,
38        sector: u32,
39        count: u16,
40        buf: &mut [u8],
41    ) -> Result<(), MmcReadCdError> {
42        let mut cdb = Cdb::default();
43        cdb[0] = MmcCommand::ReadCd as u8;
44        if let Some(SectorOption::Cdda { dap: true }) = options.expected_sector_type {
45            cdb[1] |= 1 << 1;
46        }
47        if let Some(sector_type) = options.expected_sector_type {
48            cdb[1] |= (sector_type.discriminant() & 0b111) << 2;
49        }
50        cdb[2..=5].copy_from_slice(&sector.to_be_bytes());
51        cdb[7..=8].copy_from_slice(&count.to_be_bytes());
52        if options.include_sync {
53            cdb[9] |= 1 << 7;
54        }
55        if options.include_subheader {
56            cdb[9] |= 1 << 6;
57        }
58        if options.include_header {
59            cdb[9] |= 1 << 5;
60        }
61        if options.include_user_data {
62            cdb[9] |= 1 << 4;
63        }
64        if options.include_ecc {
65            cdb[9] |= 1 << 3;
66        }
67        if let Some(c2_error) = options.include_c2_error {
68            cdb[9] |= (c2_error as u8 & 0b11) << 1;
69        }
70        if let Some(subchan_option) = options.include_subchannel {
71            cdb[10] |= subchan_option as u8 & 0b111;
72        }
73
74        self.run_command(Some(MmcDirection::Read), buf, cdb)?;
75
76        Ok(())
77    }
78}
79
80/// Disc read options.
81///
82/// A few options are valid only for certain sector types.
83/// Non-contiguous combinations of fields may not be feasible to read, and will
84/// result in an error (such as include_sync + include_User_data).
85#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
86pub struct ReadCdOptions {
87    /// Restrict read to sector type.
88    pub expected_sector_type: Option<SectorOption>,
89
90    /// Include sync fields (12 bytes per sector).
91    /// Sync fields are not present in CD-DA.
92    pub include_sync: bool,
93
94    /// Include headers (4 bytes per sector).
95    /// Headers are present in all sector types except CD-DA.
96    pub include_header: bool,
97
98    /// Include sub-headers (8 bytes per sector).
99    /// Sub-headers are present only in Mode2 formed sectors.
100    pub include_subheader: bool,
101
102    /// Include user data. Defaults to `true`.
103    /// Size depends on the sector type of the disc.
104    pub include_user_data: bool,
105
106    /// Include ECC/EDC, i.e the fields that follow user data. Present only in
107    /// Mode1 (288 bytes per sector) and Mode2 formed sectors
108    /// (Form1: 280 bytes per sector; Form2: 4 bytes per sector).
109    pub include_ecc: bool,
110
111    /// Include "C2 error bits" (294 to 296 bytes per sector).
112    pub include_c2_error: Option<C2Option>,
113
114    /// Include sub-channel information. (16 to 96 bytes per sector).
115    pub include_subchannel: Option<SubchannelOption>,
116}
117
118impl Default for ReadCdOptions {
119    /// Include only user data.
120    fn default() -> Self {
121        Self {
122            expected_sector_type: None,
123            include_sync: false,
124            include_header: false,
125            include_subheader: false,
126            include_user_data: true,
127            include_ecc: false,
128            include_c2_error: None,
129            include_subchannel: None,
130        }
131    }
132}
133
134/// Sector type to include.
135#[repr(u8)]
136#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
137pub enum SectorOption {
138    /// IEC 908 (CD-DA) with user data of 2352 bytes.
139    Cdda {
140        /// Return data with flaw obsuring mechanisms such as audio data mute
141        /// and interpolate.
142        dap: bool,
143    } = 0b001,
144
145    /// User data of 2048 bytes.
146    #[default]
147    Mode1 = 0b010,
148
149    /// User data of 2336 bytes.
150    Mode2Formless = 0b011,
151
152    /// User data of 2048 bytes.
153    Mode2Form1 = 0b100,
154
155    /// User data of 2324 bytes.
156    Mode2Form2 = 0b101,
157}
158impl SectorOption {
159    fn discriminant(&self) -> u8 {
160        // SAFETY: `repr(u8)` is laid out as `repr(C)` `union` with the `u8`
161        // discriminant as its first field.
162        // Source:
163        // https://doc.rust-lang.org/stable/std/mem/fn.discriminant.html#accessing-the-numeric-value-of-the-discriminant
164        unsafe { *(&raw const *self).cast::<u8>() }
165    }
166}
167
168/// C2 error information to include.
169#[repr(u8)]
170#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
171pub enum C2Option {
172    /// 294 bytes of "C2 error bits".
173    #[default]
174    Raw = 0b01,
175
176    /// 296 bytes consisting of:
177    /// - Block error byte: logical OR of all the 294 bytes of "C2 error bits"
178    /// - A pad byte of zero
179    /// - 294 bytes of "C2 error bits"
180    RawAndBlock = 0b10,
181}
182
183/// Sub-Channel information to include.
184// This corresponds to the 'Sub-channel Selection Field Values'.
185// Variant `0b000`, i.e 'no sub-channel data' can be represented using `None`.
186// Variant `0b001`, i.e 'RawPw' has been marked as legacy since `MMC-6`.
187#[repr(u8)]
188#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
189pub enum SubchannelOption {
190    /// RAW P-W sub-channel data (96 bytes)
191    RawPw = 0b001,
192
193    /// Formatted Q sub-channel data (16 bytes).
194    #[default]
195    Q = 0b010,
196
197    /// Corrected and de-interleaved R-W sub-channel data (96 bytes).
198    Rw = 0b100,
199}
200
201/// error from a `READ CD` command
202#[derive(Debug, Display, Error)]
203pub struct MmcReadCdError {
204    #[from]
205    pub source: MmcError,
206}
207
208#[cfg(test)]
209mod tests {
210    use tracing::info;
211
212    use super::*;
213
214    #[test_log::test(test)]
215    #[ignore]
216    fn read_cd() {
217        let mmc = Mmc::new().unwrap();
218        const SIZE: usize = 23520;
219        let mut buf = vec![0; SIZE];
220        mmc.read_cd(ReadCdOptions::default(), 200, 10, &mut buf)
221            .unwrap();
222        info!(
223            "read buffer is populated about {:?} bytes out of {SIZE} bytes",
224            buf.iter().rposition(|z| *z != 0),
225        );
226        assert!(buf.iter().any(|e| *e != 0));
227    }
228}