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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
15 changes: 13 additions & 2 deletions libcdio-rs/src/mmc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down Expand Up @@ -77,7 +86,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 @@ -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,
Expand Down Expand Up @@ -365,6 +374,8 @@ pub enum OsError {
enum MmcCommand {
#[allow(unused)]
GetConfiguration = 0x46,
PreventAllowMediumRemoval = 0x1E,
StartStopUnit = 0x1B,
TestUnitReady = 0x00,
ReadDiscInfo = 0x51,
ReadToc = 0x43,
Expand Down
73 changes: 73 additions & 0 deletions libcdio-rs/src/mmc/prevent_allow_medium_removal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (C) 2026 Shiva Kiran Koninty <shiva@skran.xyz>
//
// 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 <https://www.gnu.org/licenses/>.

//! 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
112 changes: 112 additions & 0 deletions libcdio-rs/src/mmc/start_stop_unit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright (C) 2026 Shiva Kiran Koninty <shiva@skran.xyz>
//
// 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 <https://www.gnu.org/licenses/>.

//! 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
39 changes: 39 additions & 0 deletions libcdio-rs/tests/media_removal.rs
Original file line number Diff line number Diff line change
@@ -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,
..
},),
}
})
));
}