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(§or.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}