use std::{
ffi::{CString, NulError, OsString},
path::PathBuf,
ptr,
};
pub use get_config::*;
pub use get_event_status::*;
pub use inquiry::*;
pub use prevent_allow_medium_removal::*;
pub use read_cd::*;
pub use read_disc_info::*;
pub use read_subchannel::*;
pub use read_toc::*;
pub use set_cd_speed::*;
pub use start_stop_unit::*;
pub use test_unit_ready::*;
mod get_config;
mod get_event_status;
mod inquiry;
mod prevent_allow_medium_removal;
mod read_cd;
mod read_disc_info;
mod read_subchannel;
mod read_toc;
mod set_cd_speed;
mod start_stop_unit;
mod test_unit_ready;
use docsplay::Display;
use num_enum::FromPrimitive;
use thiserror::Error;
use crate::cdio::Cdio;
pub struct Mmc {
cdio: Cdio,
}
impl Mmc {
pub fn new() -> Result<Mmc, MmcNotFoundError> {
Cdio::with_device(None)
.map(|cdio| Self { cdio })
.filter(|mmc| mmc.is_mmc_device().is_ok_and(|is_mmc| is_mmc))
.ok_or(MmcNotFoundError)
}
pub fn with_device(device: PathBuf) -> Result<Mmc, WithDeviceError> {
let device = CString::new(device.into_os_string().into_encoded_bytes()).map_err(|err| {
WithDeviceError {
device: os_string_from_bytes_safe(err.clone().into_vec()).into(),
source: WithDeviceErrorKind::DeviceHasNullChar(err),
}
})?;
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,
});
};
let maybe_mmc = Self { cdio };
if maybe_mmc.is_mmc_device().is_ok_and(|is_mmc| is_mmc) {
return Ok(maybe_mmc);
} else {
return Err(WithDeviceError {
device: os_string_from_bytes_safe(device.into_bytes()).into(),
source: WithDeviceErrorKind::MmcNotSupported,
});
}
fn os_string_from_bytes_safe(bytes: Vec<u8>) -> OsString {
unsafe { OsString::from_encoded_bytes_unchecked(bytes) }
}
}
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;
}
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,
};
unsafe { libcdio_sys::cdio_free(sense_ptr.cast()) };
Some(sense)
}
fn run_command(
&self,
direction: Option<MmcDirection>,
buf: &mut [u8],
cdb: Cdb,
) -> Result<(), MmcError> {
let direction = direction
.map(|d| d as _)
.unwrap_or(libcdio_sys::mmc_direction_s_SCSI_MMC_DATA_NONE);
let cdb = libcdio_sys::mmc_cdb_s { field: cdb };
let ret = unsafe {
libcdio_sys::mmc_run_cmd(
self.cdio.as_ptr(),
DEFAULT_TIMEOUT_MS,
&cdb,
direction,
buf.len()
.try_into()
.expect("failed to cast length of buf passed to Mmc::run_command()"),
buf.as_mut_ptr().cast(),
)
};
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;
}
}
type Cdb = [u8; 12];
#[derive(Debug, Display, Error)]
pub struct WithDeviceError {
pub device: PathBuf,
pub source: WithDeviceErrorKind,
}
#[derive(Debug, Display, Error)]
pub enum WithDeviceErrorKind {
DeviceHasNullChar(NulError),
CouldNotOpenDevice,
MmcNotSupported,
}
#[non_exhaustive]
#[derive(Debug, Display, Error)]
pub struct MmcNotFoundError;
#[non_exhaustive]
#[derive(Debug, Display, Error)]
pub struct MmcOperationError;
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct MmcSenseData {
pub sense_key: SenseKey,
pub asc: u8,
pub ascq: u8,
pub ili: bool,
pub csi: [u8; 4],
pub fruc: u8,
pub sks: [u8; 3],
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 {
NoSense = 0x0,
RecoveredError = 0x1,
NotReady = 0x2,
MediumError = 0x3,
HardwareError = 0x4,
IllegalRequest = 0x5,
UnitAttention = 0x6,
DataProtect = 0x7,
BlankCheck = 0x8,
VendorSpecific = 0x9,
CopyAborted = 0xA,
AbortedCommand = 0xB,
VolumeOverflow = 0xD,
Miscompare = 0xE,
#[num_enum(catch_all)]
Unknown(u8),
}
#[allow(clippy::derivable_impls)] impl Default for SenseKey {
fn default() -> Self {
Self::NoSense
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
enum MmcDirection {
#[default]
Read = libcdio_sys::mmc_direction_s_SCSI_MMC_DATA_READ as _,
#[allow(unused)]
Write = libcdio_sys::mmc_direction_s_SCSI_MMC_DATA_WRITE as _,
}
#[non_exhaustive]
#[derive(Debug, Display, Error)]
pub enum MmcError {
CheckCondition(MmcSenseData),
Os(OsError),
}
#[repr(i32)]
#[non_exhaustive]
#[derive(Debug, Display, Error, FromPrimitive)]
pub enum OsError {
#[num_enum(catch_all)]
Other(i32),
Unsupported = libcdio_sys::driver_return_code_t_DRIVER_OP_UNSUPPORTED,
OperationNotPermitted = libcdio_sys::driver_return_code_t_DRIVER_OP_NOT_PERMITTED,
BadParameter = libcdio_sys::driver_return_code_t_DRIVER_OP_BAD_PARAMETER,
}
#[repr(u8)]
#[derive(Clone, Copy, Debug)]
enum MmcCommand {
#[allow(unused)]
GetConfiguration = 0x46,
ReadCd = 0xBE,
Inquiry = 0x12,
PreventAllowMediumRemoval = 0x1E,
ReadDiscInfo = 0x51,
ReadToc = 0x43,
SetCdSpeed = 0xBB,
StartStopUnit = 0x1B,
TestUnitReady = 0x00,
}
const LEADOUT_TRACK: u8 = 0xAA;
#[cfg(test)]
mod tests {
use tracing::info;
use super::*;
#[test]
#[ignore = "requires a disc drive with mmc"]
fn with_device() {
Mmc::with_device(PathBuf::from("/dev/cdrom")).unwrap();
}
#[test_log::test(test)]
#[ignore = "requires a disc drive with mmc"]
fn sense_data() {
let mmc = Mmc::new().unwrap();
let mut cdb = Cdb::default();
cdb[0] = 0x43;
cdb[2] = 0xFF; mmc.run_command(Some(crate::mmc::MmcDirection::Write), &mut [], cdb)
.unwrap_err();
let sense_data = mmc.sense_data().unwrap();
info!(?sense_data);
}
}