diff --git a/README.md b/README.md index 0ce3f18..7a2816e 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,11 @@ To run ignored tests, which require some extra setup: cargo test -- --include-ignored ``` +To run tests which require manual intervention: +```sh +MANUAL_TESTS=1 cargo test -- [TEST_FILTER1 TEST_FILTER2...] --include-ignored +``` + ### Run the executables For example, to run the `iso-info` program: ```sh 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 5c9779a..a54e3f8 100644 --- a/libcdio-rs/src/mmc.rs +++ b/libcdio-rs/src/mmc.rs @@ -25,6 +25,15 @@ use std::{ pub use get_config::*; pub use get_event_status::*; +pub use prevent_allow_medium_removal::*; +pub use read_subchannel::*; +pub use start_stop_unit::*; + +mod get_config; +mod get_event_status; +mod prevent_allow_medium_removal; +mod read_subchannel; +mod start_stop_unit; pub use read_disc_info::*; pub use read_subchannel::*; pub use test_unit_ready::*; @@ -77,7 +86,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) @@ -95,7 +104,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, @@ -365,6 +374,8 @@ pub enum OsError { enum MmcCommand { #[allow(unused)] GetConfiguration = 0x46, + PreventAllowMediumRemoval = 0x1E, + StartStopUnit = 0x1B, TestUnitReady = 0x00, ReadDiscInfo = 0x51, ReadToc = 0x43, diff --git a/libcdio-rs/src/mmc/prevent_allow_medium_removal.rs b/libcdio-rs/src/mmc/prevent_allow_medium_removal.rs new file mode 100644 index 0000000..5371454 --- /dev/null +++ b/libcdio-rs/src/mmc/prevent_allow_medium_removal.rs @@ -0,0 +1,73 @@ +// Copyright (C) 2026 Shiva Kiran Koninty +// +// This file is part of libcdio-rs. +// +// libcdio-rs is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// libcdio-rs is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with libcdio-rs. If not, see . + +//! Routines based on MMC `PREVENT ALLOW MEDIUM REMOVAL`. + +use displaydoc::Display; +use thiserror::Error; + +use crate::{ + Mmc, + mmc::{Cdb, MmcCommand, MmcDirection, MmcError}, +}; + +/// Routines based on MMC `PREVENT ALLOW MEDIUM REMOVAL` +impl Mmc { + /// Allow media removal. + pub fn allow_media_removal(&self) -> Result<(), MmcMediaRemovalError> { + self.prevent_allow_medium_removal(PreventOption::Clear) + } + + /// Prevent media removal. + pub fn prevent_media_removal(&self) -> Result<(), MmcMediaRemovalError> { + self.prevent_allow_medium_removal(PreventOption::Set) + } + + fn prevent_allow_medium_removal( + &self, + prevent: PreventOption, + ) -> Result<(), MmcMediaRemovalError> { + let mut cdb = Cdb::default(); + cdb[0] = MmcCommand::PreventAllowMediumRemoval as u8; + cdb[4] = prevent as u8; + + self.run_command(Some(MmcDirection::Write), &mut [], cdb)?; + + Ok(()) + } +} + +/// error from a `PREVENT ALLOW MEDIUM REMOVAL` command. +#[non_exhaustive] +#[derive(Debug, Display, Error)] +pub struct MmcMediaRemovalError { + #[from] + pub source: MmcError, +} + +/// Operations of the `PREVENT ALLOW MEDIUM REMOVAL` command +#[allow(unused)] +#[repr(u8)] +#[derive(Clone, Copy, Debug)] +enum PreventOption { + Clear = 0b00, + Set = 0b01, + ClearPersistent = 0b10, + SetPersistent = 0b11, +} + +// see tests/media_removal.rs diff --git a/libcdio-rs/src/mmc/start_stop_unit.rs b/libcdio-rs/src/mmc/start_stop_unit.rs new file mode 100644 index 0000000..99bebd9 --- /dev/null +++ b/libcdio-rs/src/mmc/start_stop_unit.rs @@ -0,0 +1,112 @@ +// Copyright (C) 2026 Shiva Kiran Koninty +// +// This file is part of libcdio-rs. +// +// libcdio-rs is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// libcdio-rs is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with libcdio-rs. If not, see . + +//! Routines based on MMC `START STOP UNIT`. + +use displaydoc::Display; +use thiserror::Error; + +use crate::{ + Mmc, + mmc::{Cdb, MmcCommand, MmcDirection, MmcError}, +}; + +/// Routines based on MMC `START STOP UNIT`. +impl Mmc { + /// Eject the media, if permitted. + /// + /// This might require a prior allow eject operation. + pub fn eject(&self) -> Result<(), MmcEjectError> { + self.start_stop_unit(StartStopOperation::EjectDisc)?; + Ok(()) + } + + /// Close the tray. + pub fn close_tray(&self) -> Result<(), MmcCloseTrayError> { + self.start_stop_unit(StartStopOperation::LoadStartDisc)?; + Ok(()) + } + + fn start_stop_unit(&self, operation: StartStopOperation) -> Result<(), MmcStartStopError> { + let mut cdb = Cdb::default(); + + cdb[0] = MmcCommand::StartStopUnit as u8; + cdb[1] = 0; // not using the immediate bit for now + if let StartStopOperation::Jump { layer_number } = operation { + cdb[3] = layer_number & 0b11 + } + cdb[4] = match operation { + StartStopOperation::StartDisc => 0b01, + StartStopOperation::EjectDisc => 0b10, + StartStopOperation::LoadStartDisc | StartStopOperation::Jump { .. } => 0b11, + _ => 0b00, + }; + + self.run_command(Some(MmcDirection::Write), &mut [], cdb)?; + + Ok(()) + } +} + +/// could not eject MMC device +#[derive(Debug, Display, Error)] +pub struct MmcEjectError { + #[from] + pub source: MmcStartStopError, +} + +/// could not close tray of the MMC device +#[derive(Debug, Display, Error)] +pub struct MmcCloseTrayError { + #[from] + pub source: MmcStartStopError, +} + +/// error from a `START STOP UNIT` command +#[derive(Debug, Display, Error)] +pub struct MmcStartStopError { + #[from] + pub source: MmcError, +} + +/// Operations of the `START STOP UNIT` command +#[allow(unused)] +enum StartStopOperation { + StopDisc, + StartDisc, + EjectDisc, + LoadStartDisc, + + /// Change the online format-layer to the specified value for hybrid discs. + /// Only the last two bits will be set. + Jump { + layer_number: u8, + }, + + /// Place the device in the specified power condition + Power(PowerCondition), +} + +/// A power state as defined under MMC `START STOP UNIT` +#[allow(unused)] +enum PowerCondition { + Idle = 0x2, + Standby = 0x3, + Sleep = 0x5, +} + +// see tests/media_removal.rs diff --git a/libcdio-rs/tests/media_removal.rs b/libcdio-rs/tests/media_removal.rs new file mode 100644 index 0000000..9c9db5a --- /dev/null +++ b/libcdio-rs/tests/media_removal.rs @@ -0,0 +1,39 @@ +//! Tests that would remove the drive media +use libcdio_rs::{ + Mmc, + mmc::{MmcCloseTrayError, MmcError, MmcSenseData, MmcStartStopError, SenseKey}, +}; + +#[test] +#[ignore = "requires a drive with mmc"] +fn media_removal() { + if std::env::var("MANUAL_TESTS").is_err() { + return; + } + let mmc = Mmc::new().unwrap(); + + mmc.prevent_media_removal().unwrap(); + mmc.eject().unwrap_err(); // eject should fail + + mmc.allow_media_removal().unwrap(); + mmc.eject().unwrap(); // eject should pass + + // if present, the tray should close + let res = mmc.close_tray(); + assert!(matches!( + res, + Ok(()) + | Err(MmcCloseTrayError { + source: MmcStartStopError { + // Sense data corresponding to MMC error "INVALID FIELD IN CDB" + // is returned if the device does not have a tray + source: MmcError::CheckCondition(MmcSenseData { + sense_key: SenseKey::IllegalRequest, + asc: 0x24, + ascq: 0x00, + .. + },), + } + }) + )); +}