From 278dcdcdda9c18c07ffc64afb0b1a1509750e771 Mon Sep 17 00:00:00 2001 From: Shiva Kiran Koninty Date: Mon, 20 Jul 2026 12:57:39 +0530 Subject: [PATCH 1/5] lib/mmc: Add method to get sense data --- libcdio-rs/src/mmc.rs | 151 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/libcdio-rs/src/mmc.rs b/libcdio-rs/src/mmc.rs index a949270..e1d4472 100644 --- a/libcdio-rs/src/mmc.rs +++ b/libcdio-rs/src/mmc.rs @@ -20,6 +20,7 @@ use std::{ ffi::{CString, NulError, OsString}, path::PathBuf, + ptr, }; pub use get_config::*; @@ -120,6 +121,31 @@ impl Mmc { .expect("mmc_get_drive_mmc_cap should return a valid mmc_level_t")) } + /// Returns the current sense data from the device. + pub fn sense_data(&self) -> Option { + let mut sense_ptr = ptr::null_mut(); + let ret = unsafe { libcdio_sys::mmc_last_cmd_sense(self.cdio.as_ptr(), &mut sense_ptr) }; + if ret <= 0 || sense_ptr.is_null() { + return None; + } + // SAFETY: Null check done. + let sense = unsafe { *sense_ptr }; + let sense = MmcSenseData { + sense_key: SenseKey::from(sense.sense_key()), + asc: sense.asc, + ascq: sense.ascq, + ili: sense.ili() != 0, + csi: sense.command_info, + fruc: sense.fruc, + sks: sense.sks, + asb: sense.asb, + }; + // SAFETY: The contents have been copied. + unsafe { libcdio_sys::cdio_free(sense_ptr.cast()) }; + + Some(sense) + } + fn run_command( &self, direction: Option, @@ -178,6 +204,114 @@ pub struct MmcNotFoundError; #[derive(Debug, Display, Error)] pub struct MmcOperationError; +/// Error and status information returned by an MMC device +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct MmcSenseData { + /// Generic information describing an exception. + pub sense_key: SenseKey, + + /// Additional Sense Code indicates further information related + /// to the exception reported by `sense_key`. + pub asc: u8, + + /// Additional Sense Code Qualifier indicates detailed information related + /// to the `additional_sense_code`. + pub ascq: u8, + + /// Incorrect Length Indicator. + pub ili: bool, + + /// Command Specific Information indicates info that depends on the command + /// on which the exception occured. + pub csi: [u8; 4], + + /// Field Replaceable Unit Code identifies a component that has failed. + pub fruc: u8, + + /// Sense Key Specific indicates additional information about the exception. + pub sks: [u8; 3], + + /// Additional Sense Bytes may contain vendor specific data that further + /// define the exception. + pub asb: [u8; 46], +} + +impl Default for MmcSenseData { + fn default() -> Self { + Self { + sense_key: Default::default(), + asc: Default::default(), + ascq: Default::default(), + ili: Default::default(), + csi: Default::default(), + fruc: Default::default(), + sks: Default::default(), + asb: [0; _], + } + } +} + +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, FromPrimitive)] +pub enum SenseKey { + /// No sense condition. + NoSense = 0x0, + + /// The command completed successfully, but some recovery action was taken. + RecoveredError = 0x1, + + /// The logical unit is not ready to receive the command. + NotReady = 0x2, + + /// The medium (disk/tape) is defective or the data is unreadable. + MediumError = 0x3, + + /// A non-recoverable hardware failure occurred. + HardwareError = 0x4, + + /// An invalid field in the CDB or an unsupported command was sent. + IllegalRequest = 0x5, + + /// The device has a condition that needs the host's attention + /// (e.g., medium changed). + UnitAttention = 0x6, + + /// A command that reads or writes the medium was attempted on a protected + /// block. + DataProtect = 0x7, + + /// A write-once or sequential-access device encountered blank medium or + /// format-defined end-of-data indication while reading or writing. + BlankCheck = 0x8, + + /// Vendor specific conditions. + VendorSpecific = 0x9, + + /// An `EXTENDED COPY` command was aborted due to an error condition on + /// either the source or destination device. + CopyAborted = 0xA, + + /// The device server aborted the command. + AbortedCommand = 0xB, + + /// A buffered SCSI device has reached end-of-partition. + VolumeOverflow = 0xD, + + /// The source data did not match the data read from the medium. + Miscompare = 0xE, + + /// Unknown sense key. + #[num_enum(catch_all)] + Unknown(u8), +} + +#[allow(clippy::derivable_impls)] // `num_enum` doesn't work with `#[derive(Default)]` +impl Default for SenseKey { + fn default() -> Self { + Self::NoSense + } +} + /// Direction of MMC data transfer #[repr(u32)] #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, IntoPrimitive)] @@ -206,6 +340,8 @@ pub enum OsError { #[cfg(test)] mod tests { + use tracing::info; + use super::*; #[test] @@ -218,4 +354,19 @@ mod tests { fn level() { Mmc::new().unwrap().level().unwrap(); } + + #[test_log::test(test)] + #[ignore = "requires a disc drive with mmc"] + fn sense_data() { + let mmc = Mmc::new().unwrap(); + // perform an invalid `READ TOC` + let mut cdb = Cdb::default(); + cdb[0] = 0x43; + cdb[2] = 0xFF; // invalid value + mmc.run_command(Some(crate::mmc::MmcDirection::Write), &mut [], cdb) + .unwrap_err(); + + let sense_data = mmc.sense_data().unwrap(); + info!(?sense_data); + } } From b5c3aac982bc2739913ae01f6bf4dc447b6a96ca Mon Sep 17 00:00:00 2001 From: Shiva Kiran Koninty Date: Tue, 21 Jul 2026 09:49:39 +0530 Subject: [PATCH 2/5] lib: Add dependency `docsplay` --- Cargo.lock | 21 +++++++++++++++++++++ libcdio-rs/Cargo.toml | 1 + 2 files changed, 22 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index e33dd47..875ba26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -268,6 +268,26 @@ dependencies = [ "syn", ] +[[package]] +name = "docsplay" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8547ea80db62c5bb9d7796fcce5e6e07d1136bdc1a02269095061e806758fab4" +dependencies = [ + "docsplay-macros", +] + +[[package]] +name = "docsplay-macros" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11772ed3eb3db124d826f3abeadf5a791a557f62c19b123e3f07288158a71fdd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "either" version = "1.16.0" @@ -446,6 +466,7 @@ version = "0.1.0" dependencies = [ "bitflags", "displaydoc", + "docsplay", "file-mode", "libcdio-sys", "num_enum", diff --git a/libcdio-rs/Cargo.toml b/libcdio-rs/Cargo.toml index 5e4ef7e..87ad3bb 100644 --- a/libcdio-rs/Cargo.toml +++ b/libcdio-rs/Cargo.toml @@ -17,6 +17,7 @@ udf = ["libcdio-sys/udf", "dep:file-mode", "dep:time"] [dependencies] bitflags = "2.11.1" displaydoc = "0.2.6" +docsplay = "0.1.3" file-mode = { version = "0.1.2", optional = true } libcdio-sys.workspace = true num_enum = { version = "0.7.6", features = ["complex-expressions"] } From f1ea0f7c8a698e63c425b526c5cf014f1ea36c65 Mon Sep 17 00:00:00 2001 From: Shiva Kiran Koninty Date: Tue, 28 Jul 2026 17:24:27 +0530 Subject: [PATCH 3/5] lib/mmc: Introduce sense data based `MmcError` Only `OsError` was used to represent MMC errors. Introduce `MmcError`, which also covers MMC errors by including sense data. --- libcdio-rs/src/mmc.rs | 29 +++++++++++++++++++------- libcdio-rs/src/mmc/get_event_status.rs | 8 +++---- libcdio-rs/src/mmc/read_subchannel.rs | 10 ++++----- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/libcdio-rs/src/mmc.rs b/libcdio-rs/src/mmc.rs index e1d4472..fd49d76 100644 --- a/libcdio-rs/src/mmc.rs +++ b/libcdio-rs/src/mmc.rs @@ -31,7 +31,7 @@ mod get_config; mod get_event_status; mod read_subchannel; -use displaydoc::Display; +use docsplay::Display; use libcdio_sys::{ cdio_mmc_direction_t, cdio_mmc_level_t_CDIO_MMC_LEVEL_1, cdio_mmc_level_t_CDIO_MMC_LEVEL_2, cdio_mmc_level_t_CDIO_MMC_LEVEL_3, cdio_mmc_level_t_CDIO_MMC_LEVEL_NONE, @@ -151,7 +151,7 @@ impl Mmc { direction: Option, buf: &mut [u8], cdb: Cdb, - ) -> Result<(), OsError> { + ) -> Result<(), MmcError> { let direction = direction .map(cdio_mmc_direction_t::from) .unwrap_or(libcdio_sys::mmc_direction_s_SCSI_MMC_DATA_NONE); @@ -166,11 +166,15 @@ impl Mmc { buf.as_mut_ptr().cast(), ) }; - if ret < 0 { - return Err(OsError::from(ret)); - } - - return Ok(()); + return if ret >= 0 { + Ok(()) + } else if ret == -1 + && let Some(sense_data) = self.sense_data() + { + Err(MmcError::CheckCondition(sense_data)) + } else { + Err(MmcError::Os(OsError::from(ret))) + }; const DEFAULT_TIMEOUT_MS: u32 = 6000; } @@ -322,6 +326,17 @@ enum MmcDirection { Write = libcdio_sys::mmc_direction_s_SCSI_MMC_DATA_WRITE, } +/// error performing MMC command +#[non_exhaustive] +#[derive(Debug, Display, Error)] +pub enum MmcError { + /// terminated with `CHECK CONDITION`, sense_key: {0.sense_key:?}, asc: 0x{0.asc:x}, ascq: 0x{0.ascq:x} + CheckCondition(MmcSenseData), + + /// operating system error + Os(OsError), +} + /// operating system error #[repr(i32)] #[non_exhaustive] diff --git a/libcdio-rs/src/mmc/get_event_status.rs b/libcdio-rs/src/mmc/get_event_status.rs index 57bfb42..18ab539 100644 --- a/libcdio-rs/src/mmc/get_event_status.rs +++ b/libcdio-rs/src/mmc/get_event_status.rs @@ -38,7 +38,7 @@ use winnow::{ use crate::{ Mmc, - mmc::{Cdb, MmcDirection, OsError}, + mmc::{Cdb, MmcDirection, MmcError}, }; /// Routines based on MMC `GET EVENT STATUS NOTIFICATION`. @@ -56,7 +56,7 @@ impl Mmc { &self, mode: EventMode, class: EventClass, - ) -> Result { + ) -> Result { let mut data = EventData::default(); let mut cdb = Cdb::default(); cdb[0] = OPCODE; @@ -256,8 +256,8 @@ bitflags! { #[non_exhaustive] #[derive(Debug, Display, Error)] pub enum MmcStatusError { - /// operating system returned an error - Os(#[from] OsError), + /// error performing MMC command + Cmd(#[from] MmcError), /// invalid response from mmc command: {0} InvalidResponse(String), diff --git a/libcdio-rs/src/mmc/read_subchannel.rs b/libcdio-rs/src/mmc/read_subchannel.rs index 51f9809..67f2b16 100644 --- a/libcdio-rs/src/mmc/read_subchannel.rs +++ b/libcdio-rs/src/mmc/read_subchannel.rs @@ -34,7 +34,7 @@ use winnow::{ use crate::{ Mmc, - mmc::{Cdb, MmcDirection, OsError}, + mmc::{Cdb, MmcDirection, MmcError}, }; /// Routines based on MMC `READ SUB-CHANNEL`. @@ -176,7 +176,7 @@ impl Mmc { &self, address_format: AddressFormat, param: SubchannelParameter, - ) -> Result { + ) -> Result { let mut data = SubchannelData::default(); let mut cdb = Cdb::default(); cdb[0] = OPCODE; @@ -220,7 +220,7 @@ pub enum MmcAudioStatusError { NotSupported, /// operating system returned an error: {0} - Os(#[from] OsError), + Cmd(#[from] MmcError), /// invalid response from command InvalidResponse(String), @@ -228,7 +228,7 @@ pub enum MmcAudioStatusError { impl From for MmcAudioStatusError { fn from(value: MmcSubchannelError) -> Self { match value { - MmcSubchannelError::Os(os_error) => Self::Os(os_error), + MmcSubchannelError::Cmd(error) => Self::Cmd(error), MmcSubchannelError::InvalidResponse(error) => Self::InvalidResponse(error), } } @@ -280,7 +280,7 @@ fn parse_header(input: &mut &[u8]) -> Result, MmcSubchann #[derive(Debug, Display, Error)] pub enum MmcSubchannelError { /// operating system returned an error - Os(#[from] OsError), + Cmd(#[from] MmcError), /// invalid response from mmc command: {0} InvalidResponse(String), From 550fa363d04a90e76ab0b6f07a838be8df4409ff Mon Sep 17 00:00:00 2001 From: Shiva Kiran Koninty Date: Tue, 28 Jul 2026 17:40:29 +0530 Subject: [PATCH 4/5] lib/mmc: Add enum `MmcCommand` --- libcdio-rs/src/mmc.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/libcdio-rs/src/mmc.rs b/libcdio-rs/src/mmc.rs index fd49d76..36e09d5 100644 --- a/libcdio-rs/src/mmc.rs +++ b/libcdio-rs/src/mmc.rs @@ -353,6 +353,14 @@ pub enum OsError { BadParameter = libcdio_sys::driver_return_code_t_DRIVER_OP_BAD_PARAMETER, } +/// Implemented MMC commands and their operation codes. +#[allow(unused)] +#[repr(u8)] +#[derive(Clone, Copy, Debug)] +enum MmcCommand { + GetConfiguration = 0x46, +} + #[cfg(test)] mod tests { use tracing::info; From 1022cc9d8452074da62cb9b0614826566c993daa Mon Sep 17 00:00:00 2001 From: Shiva Kiran Koninty Date: Thu, 16 Jul 2026 11:12:38 +0530 Subject: [PATCH 5/5] lib/cdio: Replace `Cdio::new()` with `Cdio::with_device()` There are two major types of drivers in Cdio. - OS - Images Thus, rather than have a generic `new()` method with multiple (unused) initialization options, have two methods `with_device()` and `with_image()`. This commit introduces `with_device()`. --- libcdio-rs/src/cdio.rs | 21 ++++++++++++++------- libcdio-rs/src/drive.rs | 7 ++++--- libcdio-rs/src/mmc.rs | 4 ++-- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/libcdio-rs/src/cdio.rs b/libcdio-rs/src/cdio.rs index a46e447..cd3645b 100644 --- a/libcdio-rs/src/cdio.rs +++ b/libcdio-rs/src/cdio.rs @@ -32,22 +32,29 @@ pub(crate) struct Cdio { } impl Cdio { - /// Create a new Cdio object with the given parameters. - pub(crate) fn new(device: Option<&CStr>, driver: driver_id_t) -> Option { + /// Initialize a hardware Cdio resource with read-write access. + pub(crate) fn with_device(device: Option<&CStr>) -> Option { let source = device.map(|s| s.as_ptr()).unwrap_or(ptr::null()); - NonNull::new(Self::open(source, driver)).map(|cdio| Self { cdio }) + NonNull::new(Self::open(true, source, driver_id_t_DRIVER_DEVICE)).map(|cdio| Self { cdio }) } - fn open(source: *const c_char, driver: driver_id_t) -> *mut CdIo_t { + fn open(allow_writes: bool, source: *const c_char, driver: driver_id_t) -> *mut CdIo_t { logging::init_logger(); + let access_mode = if allow_writes { + RW_ACCESS_MODE.as_ptr() + } else { + ptr::null() + }; // SAFETY: This invokes cdio_init(), which mutates a static variable. // CDIO_LAST_DRIVER_LOCK is held to prevent data races. let _lock = CDIO_LAST_DRIVER_LOCK.lock().unwrap(); - unsafe { libcdio_sys::cdio_open(source, driver) } - } + return unsafe { libcdio_sys::cdio_open_am(source, driver, access_mode) }; - pub(crate) const DEVICE_DRIVER: driver_id_t = driver_id_t_DRIVER_DEVICE; + /// Although prefixed "MMC", this does imply read-write for all + /// operations + static RW_ACCESS_MODE: &CStr = c"MMC_RDWR"; + } } impl Deref for Cdio { diff --git a/libcdio-rs/src/drive.rs b/libcdio-rs/src/drive.rs index cea98df..ab88260 100644 --- a/libcdio-rs/src/drive.rs +++ b/libcdio-rs/src/drive.rs @@ -40,7 +40,8 @@ impl Drive { /// Get a list of connected drives. /// The values could be used with [`Self::with_drive()`]. pub fn drives() -> Vec { - let drive_list = unsafe { libcdio_sys::cdio_get_devices(Cdio::DEVICE_DRIVER) }; + let drive_list = + unsafe { libcdio_sys::cdio_get_devices(libcdio_sys::driver_id_t_DRIVER_DEVICE) }; if drive_list.is_null() { return vec![]; } @@ -72,7 +73,7 @@ impl Drive { /// # Errors /// If there are no drives connected, or the drive could not be opened. pub fn new() -> Result { - Cdio::new(None, Cdio::DEVICE_DRIVER) + Cdio::with_device(None) .ok_or(DriveNotFoundError) .map(|cdio| Self { cdio }) } @@ -91,7 +92,7 @@ impl Drive { source: WithDriveErrorKind::DriveHasNullChar(err), } })?; - let cdio = Cdio::new(Some(&drive), Cdio::DEVICE_DRIVER).ok_or_else(|| WithDriveError { + let cdio = Cdio::with_device(Some(&drive)).ok_or_else(|| WithDriveError { drive: os_string_from_bytes_safe(drive.into_bytes()).into(), source: WithDriveErrorKind::CouldNotOpenAsDrive, })?; diff --git a/libcdio-rs/src/mmc.rs b/libcdio-rs/src/mmc.rs index 36e09d5..bdfadef 100644 --- a/libcdio-rs/src/mmc.rs +++ b/libcdio-rs/src/mmc.rs @@ -71,7 +71,7 @@ impl Mmc { /// # Errors /// If an MMC capable device could not be found. pub fn new() -> Result { - Cdio::new(None, Cdio::DEVICE_DRIVER) + Cdio::with_device(None) .map(|cdio| Self { cdio }) .filter(|mmc| mmc.level().is_ok()) .ok_or(MmcNotFoundError) @@ -89,7 +89,7 @@ impl Mmc { source: WithDeviceErrorKind::DeviceHasNullChar(err), } })?; - let Some(cdio) = Cdio::new(Some(&device), Cdio::DEVICE_DRIVER) else { + let Some(cdio) = Cdio::with_device(Some(&device)) else { return Err(WithDeviceError { device: os_string_from_bytes_safe(device.into_bytes()).into(), source: WithDeviceErrorKind::CouldNotOpenDevice,