Skip to main content

libcdio_rs/mmc/
read_toc.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 MMC `READ TOC/PMA/ATIP`.
19
20use displaydoc::Display;
21use num_enum::{TryFromPrimitive, TryFromPrimitiveError};
22use thiserror::Error;
23use tracing::debug;
24use winnow::{
25    Parser,
26    binary::{
27        be_u16, be_u32,
28        bits::{bits, take as bits_take},
29        u8,
30    },
31    error::{ContextError, StrContext},
32    token::take,
33};
34
35use crate::{
36    Mmc,
37    mmc::{Cdb, LEADOUT_TRACK, MmcCommand, MmcDirection, MmcError},
38};
39
40/// Routines based on MMC `READ TOC/PMA/ATIP`.
41impl Mmc {
42    /// Get the CD-TEXT from the RW sub-channel of the media.
43    pub fn cd_text(&self) -> Result<Vec<u8>, MmcTocError> {
44        let mut data = self.read_toc(ResponseFormat::Cdtext)?;
45        let input = &mut data.as_slice();
46        parse_header(input)?;
47
48        let cdtext_len = input.len();
49        // modify `data` itself to the result instead of using the header parsed
50        // `input` to avoid an allocation
51        data.drain(0..HEADER_SIZE);
52        data.truncate(cdtext_len);
53
54        return Ok(data);
55
56        const HEADER_SIZE: usize = 4;
57    }
58
59    /// Get the CD type of the media.
60    pub fn cd_type(&self) -> Result<CdType, MmcTocError> {
61        let data = self.read_toc(ResponseFormat::FullToc {
62            session_number: u8::default(),
63        })?;
64        let input = &mut data.as_slice();
65        parse_header(input)?;
66
67        let (
68            _session_num,
69            (_adr, _control),
70            _tno,
71            _point,
72            _min,
73            _sec,
74            _frame,
75            _zero,
76            _pmin,
77            psec,
78            _pframe,
79        ) = (
80            u8, // session
81            bits((
82                bits_take::<_, u8, _, ContextError>(ADR_BITS),
83                bits_take::<_, u8, _, _>(CONTROL_BITS),
84            )),
85            u8, // tno
86            u8.verify(|point| *point == POINT),
87            u8, // min
88            u8, // sec
89            u8, // frame
90            u8.verify(|zero| *zero == 0),
91            u8, // pmin
92            u8, // psec
93            u8, // pframe
94        )
95            .context(StrContext::Label("raw toc descriptor"))
96            .parse_next(input)?;
97
98        return CdType::try_from(psec).map_err(MmcTocError::from);
99
100        const ADR_BITS: usize = 4;
101        const CONTROL_BITS: usize = 4;
102        const POINT: u8 = 0xA0; // with this value, PSEC will have disc type
103    }
104
105    /// Get the last Logical Sector Number (LSN) of the disc
106    pub fn last_sector(&self) -> Result<u32, MmcTocError> {
107        let data = self.read_toc(ResponseFormat::Toc {
108            address_format: AddressFormat::Lba,
109            track_number: LEADOUT_TRACK,
110        })?;
111        let input = &mut data.as_slice();
112        parse_header(input)?;
113
114        let (_, (_adr, _control), _track_num, _, track_start_address) = (
115            u8, // reserved
116            bits((
117                bits_take::<_, u8, _, ContextError>(ADR_BITS), // adr
118                bits_take::<_, u8, _, _>(CONTROL_BITS),        // control
119            )),
120            u8.verify(|track_num| *track_num == LEADOUT_TRACK),
121            u8,     // reserved
122            be_u32, // track start address
123        )
124            .context(StrContext::Label("formatted toc descriptor"))
125            .parse_next(input)?;
126
127        // This address represents the lead-out (ending) of the disc, reading
128        // from the "track_start_address" returns an MMC out of range error,
129        // thus the last (readable) sector is one less than this address.
130        return Ok(track_start_address.saturating_sub(1));
131
132        const ADR_BITS: usize = 4;
133        const CONTROL_BITS: usize = 4;
134    }
135
136    fn read_toc(&self, format: ResponseFormat) -> Result<Vec<u8>, MmcTocError> {
137        let mut buf = vec![0; DEFAULT_BUFFER_SIZE];
138        let mut cdb = Cdb::default();
139
140        cdb[0] = MmcCommand::ReadToc as u8;
141        if let ResponseFormat::Toc { address_format, .. }
142        | ResponseFormat::SessionInfo { address_format } = format
143            && let AddressFormat::Time = address_format
144        {
145            cdb[1] |= 1 << TIME_BITPOS;
146        }
147        cdb[2] = format.discriminant();
148        if let ResponseFormat::Toc {
149            track_number: num, ..
150        }
151        | ResponseFormat::FullToc {
152            session_number: num,
153        } = format
154        {
155            cdb[6] = num;
156        }
157        cdb[7..9].copy_from_slice(&(buf.len() as u16).to_be_bytes());
158
159        self.run_command(Some(MmcDirection::Read), buf.as_mut_slice(), cdb)?;
160
161        let descriptor_length = buf[0..DTORLEN_SIZE]
162            .try_into()
163            .map(u16::from_be_bytes)
164            .expect("buffer length is greater than two bytes");
165        let data_size = usize::from(descriptor_length) + HEADER_SIZE; // extra 4 for the header
166        if data_size > buf.len() {
167            buf.resize(data_size, u8::default());
168            cdb[7..9].copy_from_slice(&(buf.len() as u16).to_be_bytes());
169            self.run_command(Some(MmcDirection::Read), buf.as_mut_slice(), cdb)?;
170        }
171
172        return Ok(buf);
173
174        const DEFAULT_BUFFER_SIZE: usize = 64;
175        const TIME_BITPOS: usize = 1;
176        const DTORLEN_SIZE: usize = 2;
177        const HEADER_SIZE: usize = 4;
178    }
179}
180
181/// Parse the header and return the third and fourth byte fields
182fn parse_header(input: &mut &[u8]) -> Result<(u8, u8), MmcTocError> {
183    debug!(?input, len = input.len(), "parse_header()");
184
185    let descriptors_length = be_u16::<_, ContextError>
186        .context(StrContext::Label("READ TOC/PMA/ATIP header"))
187        .parse_next(input)?;
188    let (first_byte, second_byte, descriptors) =
189        (u8::<_, ContextError>, u8, take(descriptors_length))
190            .context(StrContext::Label("READ TOC/PMA/ATIP header"))
191            .parse_next(input)?;
192    *input = descriptors;
193
194    Ok((first_byte, second_byte))
195}
196
197/// error from a `READ TOC/PMA/ATIP` command
198#[non_exhaustive]
199#[derive(Debug, Display, Error)]
200pub enum MmcTocError {
201    /// operating system returned an error
202    Os(#[from] MmcError),
203
204    /// invalid response from mmc command: {0}
205    InvalidResponse(String),
206}
207impl From<ContextError> for MmcTocError {
208    fn from(err: ContextError) -> Self {
209        Self::InvalidResponse(err.to_string())
210    }
211}
212impl<T: TryFromPrimitive> From<TryFromPrimitiveError<T>> for MmcTocError {
213    fn from(err: TryFromPrimitiveError<T>) -> Self {
214        Self::InvalidResponse(err.to_string())
215    }
216}
217
218#[allow(unused)]
219#[derive(Clone, Copy, Debug)]
220enum AddressFormat {
221    Lba,
222    Time,
223}
224
225#[repr(u8)]
226#[allow(unused)]
227#[derive(Clone, Copy, Debug)]
228enum ResponseFormat {
229    Toc {
230        address_format: AddressFormat,
231        track_number: u8,
232    } = 0b0000,
233    SessionInfo {
234        address_format: AddressFormat,
235    } = 0b0001,
236    FullToc {
237        session_number: u8,
238    } = 0b0010,
239    Pma = 0b0011,
240    Atip = 0b0100,
241    Cdtext = 0b0101,
242}
243impl ResponseFormat {
244    fn discriminant(&self) -> u8 {
245        // SAFETY: Because `Self` is marked `repr(u8)`, its layout is a `repr(C)` `union`
246        // between `repr(C)` structs, each of which has the `u8` discriminant as its first
247        // field, so we can read the discriminant without offsetting the pointer.
248        // Source:
249        // https://doc.rust-lang.org/stable/std/mem/fn.discriminant.html#accessing-the-numeric-value-of-the-discriminant
250        unsafe { *(&raw const *self).cast::<u8>() }
251    }
252}
253
254/// The type of a CD disc
255#[repr(u8)]
256#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, TryFromPrimitive)]
257pub enum CdType {
258    /// CD-DA or CD Data with first track in Mode 1
259    #[default]
260    CddaOrCdData = 0x00,
261
262    /// CD-I
263    Cdi = 0x10,
264
265    /// CD-ROM XA disc with first track in Mode 2
266    CdromXa = 0x20,
267}
268
269#[cfg(test)]
270mod tests {
271    use tracing::info;
272
273    use super::*;
274
275    #[test_log::test(test)]
276    #[ignore = "requires a disc drive with mmc"]
277    fn cd_text() {
278        let cd_text = Mmc::new().unwrap().cd_text().unwrap();
279        info!(?cd_text, len = cd_text.len());
280        assert!(!cd_text.is_empty());
281    }
282
283    #[test_log::test(test)]
284    #[ignore = "requires a disc drive with mmc"]
285    fn cd_type() {
286        let cd_type = Mmc::new().unwrap().cd_type().unwrap();
287        info!(?cd_type);
288    }
289
290    #[test_log::test(test)]
291    #[ignore = "requires a disc drive with mmc"]
292    fn last_sector() {
293        let last_sector = Mmc::new().unwrap().last_sector().unwrap();
294        info!(?last_sector);
295    }
296}