1use 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
57pub struct Mmc {
59 cdio: Cdio,
60}
61
62impl Mmc {
63 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 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 unsafe { OsString::from_encoded_bytes_unchecked(bytes) }
105 }
106 }
107
108 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 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 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 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#[derive(Debug, Display, Error)]
174pub struct WithDeviceError {
175 pub device: PathBuf,
176 pub source: WithDeviceErrorKind,
177}
178#[derive(Debug, Display, Error)]
180pub enum WithDeviceErrorKind {
181 DeviceHasNullChar(NulError),
183 CouldNotOpenDevice,
185 MmcNotSupported,
187}
188
189#[non_exhaustive]
191#[derive(Debug, Display, Error)]
192pub struct MmcNotFoundError;
193
194#[non_exhaustive]
196#[derive(Debug, Display, Error)]
197pub struct MmcOperationError;
198
199#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
204pub struct MmcSenseData {
205 pub sense_key: SenseKey,
207
208 pub asc: u8,
211
212 pub ascq: u8,
215
216 pub ili: bool,
218
219 pub csi: [u8; 4],
222
223 pub fruc: u8,
225
226 pub sks: [u8; 3],
228
229 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 NoSense = 0x0,
254
255 RecoveredError = 0x1,
257
258 NotReady = 0x2,
260
261 MediumError = 0x3,
263
264 HardwareError = 0x4,
266
267 IllegalRequest = 0x5,
269
270 UnitAttention = 0x6,
273
274 DataProtect = 0x7,
277
278 BlankCheck = 0x8,
281
282 VendorSpecific = 0x9,
284
285 CopyAborted = 0xA,
288
289 AbortedCommand = 0xB,
291
292 VolumeOverflow = 0xD,
294
295 Miscompare = 0xE,
297
298 #[num_enum(catch_all)]
300 Unknown(u8),
301}
302
303#[allow(clippy::derivable_impls)] impl Default for SenseKey {
305 fn default() -> Self {
306 Self::NoSense
307 }
308}
309
310#[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#[non_exhaustive]
323#[derive(Debug, Display, Error)]
324pub enum MmcError {
325 CheckCondition(MmcSenseData),
327
328 Os(OsError),
330}
331
332#[repr(i32)]
334#[non_exhaustive]
335#[derive(Debug, Display, Error, FromPrimitive)]
336pub enum OsError {
337 #[num_enum(catch_all)]
339 Other(i32),
340 Unsupported = libcdio_sys::driver_return_code_t_DRIVER_OP_UNSUPPORTED,
342 OperationNotPermitted = libcdio_sys::driver_return_code_t_DRIVER_OP_NOT_PERMITTED,
344 BadParameter = libcdio_sys::driver_return_code_t_DRIVER_OP_BAD_PARAMETER,
346}
347
348#[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; #[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 let mut cdb = Cdb::default();
384 cdb[0] = 0x43;
385 cdb[2] = 0xFF; 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}