Skip to main content

libcdio_rs/
mmc.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//! SCSI MMC (MultiMedia Commands) routines.
19//! Refer to `README.md` for the reference manuals of SPC and MMC used.
20
21use std::{
22    ffi::{CString, NulError, OsString},
23    path::PathBuf,
24    ptr,
25};
26
27pub use get_config::*;
28pub use get_event_status::*;
29pub use inquiry::*;
30pub use prevent_allow_medium_removal::*;
31pub use read_cd::*;
32pub use read_disc_info::*;
33pub use read_subchannel::*;
34pub use read_toc::*;
35pub use set_cd_speed::*;
36pub use start_stop_unit::*;
37pub use test_unit_ready::*;
38
39mod get_config;
40mod get_event_status;
41mod inquiry;
42mod prevent_allow_medium_removal;
43mod read_cd;
44mod read_disc_info;
45mod read_subchannel;
46mod read_toc;
47mod set_cd_speed;
48mod start_stop_unit;
49mod test_unit_ready;
50
51use docsplay::Display;
52use num_enum::FromPrimitive;
53use thiserror::Error;
54
55use crate::cdio::Cdio;
56
57/// An interface for SCSI MMC commands.
58pub struct Mmc {
59    cdio: Cdio,
60}
61
62impl Mmc {
63    /// Use a default device.
64    ///
65    /// # Errors
66    /// If an MMC capable device could not be found.
67    pub fn new() -> Result<Mmc, MmcNotFoundError> {
68        Cdio::with_device(None)
69            .map(|cdio| Self { cdio })
70            .filter(|mmc| mmc.is_mmc_device().is_ok_and(|is_mmc| is_mmc))
71            .ok_or(MmcNotFoundError)
72    }
73
74    /// Use the provided device.
75    ///
76    /// # Errors
77    /// If there are no devices with MMC connected, or the device could not be
78    /// opened.
79    pub fn with_device(device: PathBuf) -> Result<Mmc, WithDeviceError> {
80        let device = CString::new(device.into_os_string().into_encoded_bytes()).map_err(|err| {
81            WithDeviceError {
82                device: os_string_from_bytes_safe(err.clone().into_vec()).into(),
83                source: WithDeviceErrorKind::DeviceHasNullChar(err),
84            }
85        })?;
86        let Some(cdio) = Cdio::with_device(Some(&device)) else {
87            return Err(WithDeviceError {
88                device: os_string_from_bytes_safe(device.into_bytes()).into(),
89                source: WithDeviceErrorKind::CouldNotOpenDevice,
90            });
91        };
92        let maybe_mmc = Self { cdio };
93        if maybe_mmc.is_mmc_device().is_ok_and(|is_mmc| is_mmc) {
94            return Ok(maybe_mmc);
95        } else {
96            return Err(WithDeviceError {
97                device: os_string_from_bytes_safe(device.into_bytes()).into(),
98                source: WithDeviceErrorKind::MmcNotSupported,
99            });
100        }
101
102        fn os_string_from_bytes_safe(bytes: Vec<u8>) -> OsString {
103            // SAFETY: the bytes originate from an OsString
104            unsafe { OsString::from_encoded_bytes_unchecked(bytes) }
105        }
106    }
107
108    /// Returns the current sense data from the device.
109    pub fn sense_data(&self) -> Option<MmcSenseData> {
110        let mut sense_ptr = ptr::null_mut();
111        let ret = unsafe { libcdio_sys::mmc_last_cmd_sense(self.cdio.as_ptr(), &mut sense_ptr) };
112        if ret <= 0 || sense_ptr.is_null() {
113            return None;
114        }
115        // SAFETY: Null check done.
116        let sense = unsafe { *sense_ptr };
117        let sense = MmcSenseData {
118            sense_key: SenseKey::from(sense.sense_key()),
119            asc: sense.asc,
120            ascq: sense.ascq,
121            ili: sense.ili() != 0,
122            csi: sense.command_info,
123            fruc: sense.fruc,
124            sks: sense.sks,
125            asb: sense.asb,
126        };
127        // SAFETY: The contents have been copied.
128        unsafe { libcdio_sys::cdio_free(sense_ptr.cast()) };
129
130        Some(sense)
131    }
132
133    fn run_command(
134        &self,
135        direction: Option<MmcDirection>,
136        buf: &mut [u8],
137        cdb: Cdb,
138    ) -> Result<(), MmcError> {
139        // the cast is safe as MmcDirection's discriminants are
140        // small (< i8::MAX) and non-negative
141        let direction = direction
142            .map(|d| d as _)
143            .unwrap_or(libcdio_sys::mmc_direction_s_SCSI_MMC_DATA_NONE);
144        let cdb = libcdio_sys::mmc_cdb_s { field: cdb };
145        let ret = unsafe {
146            libcdio_sys::mmc_run_cmd(
147                self.cdio.as_ptr(),
148                DEFAULT_TIMEOUT_MS,
149                &cdb,
150                direction,
151                buf.len()
152                    .try_into()
153                    .expect("failed to cast length of buf passed to Mmc::run_command()"),
154                buf.as_mut_ptr().cast(),
155            )
156        };
157        return if ret >= 0 {
158            Ok(())
159        } else if ret == -1
160            && let Some(sense_data) = self.sense_data()
161        {
162            Err(MmcError::CheckCondition(sense_data))
163        } else {
164            Err(MmcError::Os(OsError::from(ret)))
165        };
166
167        const DEFAULT_TIMEOUT_MS: u32 = 6000;
168    }
169}
170type Cdb = [u8; 12];
171
172/// error opening MMC device at `{device}`
173#[derive(Debug, Display, Error)]
174pub struct WithDeviceError {
175    pub device: PathBuf,
176    pub source: WithDeviceErrorKind,
177}
178/// Error kind of [`WithDeviceError`]
179#[derive(Debug, Display, Error)]
180pub enum WithDeviceErrorKind {
181    /// device path contains null character
182    DeviceHasNullChar(NulError),
183    /// could not open device
184    CouldNotOpenDevice,
185    /// device does not support MMC
186    MmcNotSupported,
187}
188
189/// could not find any devices that support MMC
190#[non_exhaustive]
191#[derive(Debug, Display, Error)]
192pub struct MmcNotFoundError;
193
194/// could not perform operation on the MMC device
195#[non_exhaustive]
196#[derive(Debug, Display, Error)]
197pub struct MmcOperationError;
198
199/// Error and status information returned by an MMC device.
200///
201/// Source:
202/// SPC-3 > General Concepts > Sense data > Fixed format sense data.
203#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
204pub struct MmcSenseData {
205    /// Sense Key (SK) represents generic information describing an exception.
206    pub sense_key: SenseKey,
207
208    /// Additional Sense Code (ASC) indicates further information related
209    /// to the exception reported by `sense_key`.
210    pub asc: u8,
211
212    /// Additional Sense Code Qualifier (ASCQ) indicates detailed information related
213    /// to the `additional_sense_code`.
214    pub ascq: u8,
215
216    /// Incorrect Length Indicator.
217    pub ili: bool,
218
219    /// Command Specific Information indicates info that depends on the command
220    /// on which the exception occured.
221    pub csi: [u8; 4],
222
223    /// Field Replaceable Unit Code identifies a component that has failed.
224    pub fruc: u8,
225
226    /// Sense Key Specific indicates additional information about the exception.
227    pub sks: [u8; 3],
228
229    /// Additional Sense Bytes may contain vendor specific data that further
230    /// define the exception.
231    pub asb: [u8; 46],
232}
233
234impl Default for MmcSenseData {
235    fn default() -> Self {
236        Self {
237            sense_key: Default::default(),
238            asc: Default::default(),
239            ascq: Default::default(),
240            ili: Default::default(),
241            csi: Default::default(),
242            fruc: Default::default(),
243            sks: Default::default(),
244            asb: [0; _],
245        }
246    }
247}
248
249#[repr(u8)]
250#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, FromPrimitive)]
251pub enum SenseKey {
252    /// No sense condition.
253    NoSense = 0x0,
254
255    /// The command completed successfully, but some recovery action was taken.
256    RecoveredError = 0x1,
257
258    /// The logical unit is not ready to receive the command.
259    NotReady = 0x2,
260
261    /// The medium (disk/tape) is defective or the data is unreadable.
262    MediumError = 0x3,
263
264    /// A non-recoverable hardware failure occurred.
265    HardwareError = 0x4,
266
267    /// An invalid field in the CDB or an unsupported command was sent.
268    IllegalRequest = 0x5,
269
270    /// The device has a condition that needs the host's attention
271    /// (e.g., medium changed).
272    UnitAttention = 0x6,
273
274    /// A command that reads or writes the medium was attempted on a protected
275    /// block.
276    DataProtect = 0x7,
277
278    /// A write-once or sequential-access device encountered blank medium or
279    /// format-defined end-of-data indication while reading or writing.
280    BlankCheck = 0x8,
281
282    /// Vendor specific conditions.
283    VendorSpecific = 0x9,
284
285    /// An `EXTENDED COPY` command was aborted due to an error condition on
286    /// either the source or destination device.
287    CopyAborted = 0xA,
288
289    /// The device server aborted the command.
290    AbortedCommand = 0xB,
291
292    /// A buffered SCSI device has reached end-of-partition.
293    VolumeOverflow = 0xD,
294
295    /// The source data did not match the data read from the medium.
296    Miscompare = 0xE,
297
298    /// Unknown sense key.
299    #[num_enum(catch_all)]
300    Unknown(u8),
301}
302
303#[allow(clippy::derivable_impls)] // `num_enum` doesn't work with `#[derive(Default)]`
304impl Default for SenseKey {
305    fn default() -> Self {
306        Self::NoSense
307    }
308}
309
310/// Direction of MMC data transfer
311// The casts are safe since the C enums have implicit discriminants,
312// which should be small and non-negative.
313#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
314enum MmcDirection {
315    #[default]
316    Read = libcdio_sys::mmc_direction_s_SCSI_MMC_DATA_READ as _,
317    #[allow(unused)]
318    Write = libcdio_sys::mmc_direction_s_SCSI_MMC_DATA_WRITE as _,
319}
320
321/// error performing MMC command
322#[non_exhaustive]
323#[derive(Debug, Display, Error)]
324pub enum MmcError {
325    /// terminated with `CHECK CONDITION`, sense_key: {0.sense_key:?}, asc: 0x{0.asc:x}, ascq: 0x{0.ascq:x}
326    CheckCondition(MmcSenseData),
327
328    /// operating system error
329    Os(OsError),
330}
331
332/// operating system error
333#[repr(i32)]
334#[non_exhaustive]
335#[derive(Debug, Display, Error, FromPrimitive)]
336pub enum OsError {
337    /// other error: {0}
338    #[num_enum(catch_all)]
339    Other(i32),
340    /// unsupported operation
341    Unsupported = libcdio_sys::driver_return_code_t_DRIVER_OP_UNSUPPORTED,
342    /// operation not permitted
343    OperationNotPermitted = libcdio_sys::driver_return_code_t_DRIVER_OP_NOT_PERMITTED,
344    /// bad parameter
345    BadParameter = libcdio_sys::driver_return_code_t_DRIVER_OP_BAD_PARAMETER,
346}
347
348/// Implemented MMC commands and their operation codes.
349#[repr(u8)]
350#[derive(Clone, Copy, Debug)]
351enum MmcCommand {
352    #[allow(unused)]
353    GetConfiguration = 0x46,
354    ReadCd = 0xBE,
355    Inquiry = 0x12,
356    PreventAllowMediumRemoval = 0x1E,
357    ReadDiscInfo = 0x51,
358    ReadToc = 0x43,
359    SetCdSpeed = 0xBB,
360    StartStopUnit = 0x1B,
361    TestUnitReady = 0x00,
362}
363
364const LEADOUT_TRACK: u8 = 0xAA; // Indicates the end of the disc.
365
366#[cfg(test)]
367mod tests {
368    use tracing::info;
369
370    use super::*;
371
372    #[test]
373    #[ignore = "requires a disc drive with mmc"]
374    fn with_device() {
375        Mmc::with_device(PathBuf::from("/dev/cdrom")).unwrap();
376    }
377
378    #[test_log::test(test)]
379    #[ignore = "requires a disc drive with mmc"]
380    fn sense_data() {
381        let mmc = Mmc::new().unwrap();
382        // perform an invalid `READ TOC`
383        let mut cdb = Cdb::default();
384        cdb[0] = 0x43;
385        cdb[2] = 0xFF; // invalid value
386        mmc.run_command(Some(crate::mmc::MmcDirection::Write), &mut [], cdb)
387            .unwrap_err();
388
389        let sense_data = mmc.sense_data().unwrap();
390        info!(?sense_data);
391    }
392}