Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions libcdio-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
21 changes: 14 additions & 7 deletions libcdio-rs/src/cdio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self> {
/// Initialize a hardware Cdio resource with read-write access.
pub(crate) fn with_device(device: Option<&CStr>) -> Option<Self> {
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 {
Expand Down
7 changes: 4 additions & 3 deletions libcdio-rs/src/drive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf> {
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![];
}
Expand Down Expand Up @@ -72,7 +73,7 @@ impl Drive {
/// # Errors
/// If there are no drives connected, or the drive could not be opened.
pub fn new() -> Result<Self, DriveNotFoundError> {
Cdio::new(None, Cdio::DEVICE_DRIVER)
Cdio::with_device(None)
.ok_or(DriveNotFoundError)
.map(|cdio| Self { cdio })
}
Expand All @@ -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,
})?;
Expand Down
192 changes: 183 additions & 9 deletions libcdio-rs/src/mmc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use std::{
ffi::{CString, NulError, OsString},
path::PathBuf,
ptr,
};

pub use get_config::*;
Expand All @@ -30,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,
Expand Down Expand Up @@ -70,7 +71,7 @@ impl Mmc {
/// # Errors
/// If an MMC capable device could not be found.
pub fn new() -> Result<Mmc, MmcNotFoundError> {
Cdio::new(None, Cdio::DEVICE_DRIVER)
Cdio::with_device(None)
.map(|cdio| Self { cdio })
.filter(|mmc| mmc.level().is_ok())
.ok_or(MmcNotFoundError)
Expand All @@ -88,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,
Expand Down Expand Up @@ -120,12 +121,37 @@ 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<MmcSenseData> {
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<MmcDirection>,
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);
Expand All @@ -140,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;
}
Expand Down Expand Up @@ -178,6 +208,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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Additional Sense Code indicates further information related
/// Additional Sense Code (ASC) indicates further information related

/// to the exception reported by `sense_key`.
pub asc: u8,

/// Additional Sense Code Qualifier indicates detailed information related

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Additional Sense Code Qualifier indicates detailed information related
/// Additional Sense Code Qualifier (ASCQ) 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

@rocky rocky Jul 28, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In https://www.13thmonkey.org/documentation/SCSI/x3_304_1997.pdf I see do not see "Additional Sense Bytes" mentioned, although I do see "additional sense code" mentioned. Is this the same thing?

More generally, some of these fields like asc and ascq correspond very closely to the names used in many versions of a standard (including the one we are focusing on): ASC and ASCQ. But something like ili or Incorrect length indicator I am having a hard time finding matching terms in a specification.

How do we make it clear in the comments which terms are exact and which terms are our terminology for a concept that might not be spelled out with a particular term in the specification? And for the exact terms, where would I go in a spec to understand that an ASCQ is u8 (or even just 8 bits since that's probably all the standard is going to indicate)?

/// 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)]
Expand All @@ -188,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]
Expand All @@ -204,8 +353,18 @@ 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;

use super::*;

#[test]
Expand All @@ -218,4 +377,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);
}
}
Loading