Skip to main content

libcdio_rs/
drive.rs

1// Copyright (C) 2026 Shiva Kiran Koninty <shiva@skran.xyz>
2//
3// This file is part of libcdio-rs.
4//
5// libcdio-rs is free software: you can redistribute it and/or
6// modify it under the terms of the GNU General Public License as
7// published by the Free Software Foundation, either version 3 of the
8// License, or (at your option) any later version.
9//
10// libcdio-rs is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13// General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with libcdio-rs. If not, see <https://www.gnu.org/licenses/>.
17
18//! Routines related to CD/DVD drives.
19
20use std::{
21    error::Error,
22    ffi::{CStr, CString, OsString},
23    fmt,
24    mem::MaybeUninit,
25    path::{Path, PathBuf},
26};
27
28use bitflags::bitflags;
29use libcdio_sys::cdio_hwinfo_t;
30use thiserror::Error;
31
32use crate::{
33    cdio::{CDIO_INIT_LOCK, Cdio},
34    logging,
35};
36
37/// An interface to a disc drive.
38pub struct Drive {
39    pub(crate) cdio: Cdio,
40}
41
42impl Drive {
43    /// Returns a list of connected drives.
44    pub fn drives() -> Vec<PathBuf> {
45        logging::init_logger();
46
47        // SAFETY: This method internally initializes an instance of CdIo_t,
48        // which is not thread safe. Hold CDIO_INIT_LOCK to uphold thread
49        // safety.
50        let _lock = CDIO_INIT_LOCK.lock().unwrap();
51        let drive_list =
52            unsafe { libcdio_sys::cdio_get_devices(libcdio_sys::driver_id_t_DRIVER_DEVICE) };
53        if drive_list.is_null() {
54            return vec![];
55        }
56
57        let mut drives = Vec::new();
58        let mut ptr = drive_list;
59
60        // SAFETY: Null checked
61        while !ptr.is_null()
62            && let drive = unsafe { *ptr }
63            && !drive.is_null()
64        {
65            // SAFETY: `drive` represents a system path, making it a valid `OsString`
66            drives.push(PathBuf::from(unsafe {
67                OsString::from_encoded_bytes_unchecked(CStr::from_ptr(drive).to_bytes().to_vec())
68            }));
69            ptr = unsafe { ptr.offset(1) };
70        }
71
72        // SAFETY: drive_list has been copied into drives
73        unsafe {
74            libcdio_sys::cdio_free_device_list(drive_list);
75        }
76
77        drives
78    }
79
80    /// Opens a default connected drive.
81    pub fn new() -> Result<Self, DriveNotFoundError> {
82        Cdio::with_device(None)
83            .ok_or(DriveNotFoundError)
84            .map(|cdio| Self { cdio })
85    }
86
87    /// Opens drive at given path.
88    ///
89    /// See [`Self::drives()`] for a list of connected drives.
90    pub fn with_drive(drive: PathBuf) -> Result<Self, DriveOpenError> {
91        let drive = CString::new(drive.into_os_string().into_encoded_bytes())
92            .map_err(|err| DriveOpenError::new(err.clone().into_vec(), err.into()))?;
93        let cdio = Cdio::with_device(Some(&drive)).ok_or_else(|| {
94            DriveOpenError::new(drive.into_bytes(), "cdio_open_am() returned NULL".into())
95        })?;
96
97        Ok(Self { cdio })
98    }
99
100    /// Returns hardware identifiers of the drive such as Model, Vendor and Revision.
101    pub fn hardware_identifiers(&self) -> Result<HardwareIdentifiers, DriveOperationError> {
102        let mut hwinfo: MaybeUninit<cdio_hwinfo_t> = MaybeUninit::uninit();
103        let ret = unsafe { libcdio_sys::cdio_get_hwinfo(self.cdio.as_ptr(), hwinfo.as_mut_ptr()) };
104        if !ret {
105            return Err(DriveOperationError);
106        }
107
108        // SAFETY: cdio_get_hwinfo() returned true, therefore hwinfo should be initialized
109        let hwinfo = unsafe { hwinfo.assume_init() };
110
111        // SAFETY: The strings are null terminated
112        unsafe {
113            let model = CStr::from_ptr(hwinfo.psz_model.as_ptr());
114            let vendor = CStr::from_ptr(hwinfo.psz_vendor.as_ptr());
115            let revision = CStr::from_ptr(hwinfo.psz_revision.as_ptr());
116
117            Ok(HardwareIdentifiers {
118                model: model.to_string_lossy().trim_end().to_string(),
119                vendor: vendor.to_string_lossy().trim_end().to_string(),
120                revision: revision.to_string_lossy().trim_end().to_string(),
121            })
122        }
123    }
124
125    /// Returns drive capabilities.
126    pub fn capabilities(&self) -> Result<DriveCapabilities, DriveOperationError> {
127        let mut read = 0;
128        let mut write = 0;
129        let mut misc = 0;
130        unsafe {
131            libcdio_sys::cdio_get_drive_cap(self.cdio.as_ptr(), &mut read, &mut write, &mut misc);
132        }
133
134        (|| {
135            Some(DriveCapabilities {
136                read: ReadCapabilities::from_bits(read)?,
137                write: WriteCapabilities::from_bits(write)?,
138                misc: MiscCapabilities::from_bits(misc)?,
139            })
140        })()
141        .ok_or(DriveOperationError)
142    }
143}
144
145#[non_exhaustive]
146#[derive(Debug, Error)]
147#[error("could not find any drives")]
148pub struct DriveNotFoundError;
149
150#[derive(Debug, Error)]
151#[error(transparent)]
152pub struct DriveOpenError(Box<OpenErrRepr>);
153
154#[derive(Debug, Error)]
155#[error("could not open drive at `{path}`")]
156struct OpenErrRepr {
157    path: PathBuf,
158    source: Box<dyn Error + Send + Sync>,
159}
160
161impl DriveOpenError {
162    /// Returns the system path of the drive.
163    pub fn path(&self) -> &Path {
164        &self.0.path
165    }
166
167    fn new(path_bytes: Vec<u8>, source: Box<dyn Error + Send + Sync>) -> Self {
168        Self(Box::new(OpenErrRepr {
169            // SAFETY: path_bytes originate from a PathBuf
170            path: unsafe { OsString::from_encoded_bytes_unchecked(path_bytes) }.into(),
171            source,
172        }))
173    }
174}
175
176#[non_exhaustive]
177#[derive(Debug, Error)]
178#[error("could not perform operation on the drive")]
179pub struct DriveOperationError;
180
181/// Hardware identifiers such as model, vendor and revision.
182#[derive(Clone, Debug, PartialEq, Eq)]
183pub struct HardwareIdentifiers {
184    pub model: String,
185    pub vendor: String,
186    pub revision: String,
187}
188
189/// Drive capabilities.
190#[derive(Clone, Copy, Debug)]
191pub struct DriveCapabilities {
192    pub read: ReadCapabilities,
193    pub write: WriteCapabilities,
194    pub misc: MiscCapabilities,
195}
196// the C enum discriminants are explicit, positive and fit a u32, making these casts safe
197bitflags! {
198    /// Miscellaneous capabilities of the drive.
199    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
200    pub struct MiscCapabilities: u32 {
201        /// Can close tray
202        const CloseTray = libcdio_sys::cdio_drive_cap_misc_t_CDIO_DRIVE_CAP_MISC_CLOSE_TRAY as _;
203        /// Can eject
204        const Eject = libcdio_sys::cdio_drive_cap_misc_t_CDIO_DRIVE_CAP_MISC_EJECT as _;
205        /// Can disable manual eject
206        const Lock = libcdio_sys::cdio_drive_cap_misc_t_CDIO_DRIVE_CAP_MISC_LOCK as _;
207        /// Can set drive speed
208        const SelectSpeed = libcdio_sys::cdio_drive_cap_misc_t_CDIO_DRIVE_CAP_MISC_SELECT_SPEED as _;
209        /// Can select juke-box disc
210        const SelectDisc = libcdio_sys::cdio_drive_cap_misc_t_CDIO_DRIVE_CAP_MISC_SELECT_DISC as _;
211        /// Can read multiple sessions
212        const MultiSession = libcdio_sys::cdio_drive_cap_misc_t_CDIO_DRIVE_CAP_MISC_MULTI_SESSION as _;
213        /// Can detect if media changed
214        const MediaChanged = libcdio_sys::cdio_drive_cap_misc_t_CDIO_DRIVE_CAP_MISC_MEDIA_CHANGED as _;
215        /// Can hard reset device
216        const Reset = libcdio_sys::cdio_drive_cap_misc_t_CDIO_DRIVE_CAP_MISC_RESET as _;
217    }
218}
219// the C enum discriminants are explicit, positive and fit a u32, making these casts safe
220bitflags! {
221    /// Read capabilities of the drive.
222    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
223    pub struct ReadCapabilities: u32 {
224        /// Can play audio
225        const Audio = libcdio_sys::cdio_drive_cap_read_t_CDIO_DRIVE_CAP_READ_AUDIO as _;
226        /// Can read CD-DA
227        const CdDa = libcdio_sys::cdio_drive_cap_read_t_CDIO_DRIVE_CAP_READ_CD_DA as _;
228        /// Can read CD+G
229        const CdPlusG = libcdio_sys::cdio_drive_cap_read_t_CDIO_DRIVE_CAP_READ_CD_G as _;
230        /// Can read CD-R
231        const CdR = libcdio_sys::cdio_drive_cap_read_t_CDIO_DRIVE_CAP_READ_CD_R as _;
232        /// Can read CD-RW
233        const CdRw = libcdio_sys::cdio_drive_cap_read_t_CDIO_DRIVE_CAP_READ_CD_RW as _;
234        /// Can read DVD-R
235        const DvdR = libcdio_sys::cdio_drive_cap_read_t_CDIO_DRIVE_CAP_READ_DVD_R as _;
236        /// Can read DVD+R
237        const DvdPlusR = libcdio_sys::cdio_drive_cap_read_t_CDIO_DRIVE_CAP_READ_DVD_PR as _;
238        /// Can read DVD-RAM
239        const DvdRam = libcdio_sys::cdio_drive_cap_read_t_CDIO_DRIVE_CAP_READ_DVD_RAM as _;
240        /// Can read DVD-ROM
241        const DvdRom = libcdio_sys::cdio_drive_cap_read_t_CDIO_DRIVE_CAP_READ_DVD_ROM as _;
242        /// Can read DVD-RW
243        const DvdRw = libcdio_sys::cdio_drive_cap_read_t_CDIO_DRIVE_CAP_READ_DVD_RW as _;
244        /// Can read DVD+RW
245        const DvdPlusRw = libcdio_sys::cdio_drive_cap_read_t_CDIO_DRIVE_CAP_READ_DVD_RPW as _;
246        /// Can read C2 errors
247        const C2Errors = libcdio_sys::cdio_drive_cap_read_t_CDIO_DRIVE_CAP_READ_C2_ERRS as _;
248        /// Can read Mode 2 Form 1 (VCD)
249        const Mode2Form1 = libcdio_sys::cdio_drive_cap_read_t_CDIO_DRIVE_CAP_READ_MODE2_FORM1 as _;
250        /// Can read Mode 2 Form 2 (VCD)
251        const Mode2Form2 = libcdio_sys::cdio_drive_cap_read_t_CDIO_DRIVE_CAP_READ_MODE2_FORM2 as _;
252        /// Can read MCN
253        const Mcn = libcdio_sys::cdio_drive_cap_read_t_CDIO_DRIVE_CAP_READ_MCN as _;
254        /// Can read ISRC
255        const Isrc = libcdio_sys::cdio_drive_cap_read_t_CDIO_DRIVE_CAP_READ_ISRC as _;
256    }
257}
258// the C enum discriminants are explicit, positive and fit a u32, making these casts safe
259bitflags! {
260    /// Write capabilities of the drive.
261    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
262    pub struct WriteCapabilities: u32 {
263        /// Can write CD-R
264        const CdR = libcdio_sys::cdio_drive_cap_write_t_CDIO_DRIVE_CAP_WRITE_CD_R as _;
265        /// Can write CD-RW
266        const CdRw = libcdio_sys::cdio_drive_cap_write_t_CDIO_DRIVE_CAP_WRITE_CD_RW as _;
267        /// Can write DVD-R
268        const DvdR = libcdio_sys::cdio_drive_cap_write_t_CDIO_DRIVE_CAP_WRITE_DVD_R as _;
269        /// Can write DVD+R
270        const DvdPlusR = libcdio_sys::cdio_drive_cap_write_t_CDIO_DRIVE_CAP_WRITE_DVD_PR as _;
271        /// Can write DVD-RAM
272        const DvdRam = libcdio_sys::cdio_drive_cap_write_t_CDIO_DRIVE_CAP_WRITE_DVD_RAM as _;
273        /// Can write DVD-RW
274        const DvdRw = libcdio_sys::cdio_drive_cap_write_t_CDIO_DRIVE_CAP_WRITE_DVD_RW as _;
275        /// Can write DVD+RW
276        const DvdPlusRw = libcdio_sys::cdio_drive_cap_write_t_CDIO_DRIVE_CAP_WRITE_DVD_RPW as _;
277        /// Can write MRW (Mount Rainier)
278        const Mrw = libcdio_sys::cdio_drive_cap_write_t_CDIO_DRIVE_CAP_WRITE_MT_RAINIER as _;
279        /// Can write using Burn proof
280        const BurnProof = libcdio_sys::cdio_drive_cap_write_t_CDIO_DRIVE_CAP_WRITE_BURN_PROOF as _;
281    }
282}
283
284impl fmt::Display for MiscCapabilities {
285    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
286        match *self {
287            MiscCapabilities::CloseTray => write!(f, "Close Tray"),
288            MiscCapabilities::Eject => write!(f, "Eject"),
289            MiscCapabilities::Lock => write!(f, "Lock"),
290            MiscCapabilities::SelectSpeed => write!(f, "Select Speed"),
291            MiscCapabilities::SelectDisc => write!(f, "Select Disc"),
292            MiscCapabilities::MultiSession => write!(f, "Multi Session"),
293            MiscCapabilities::MediaChanged => write!(f, "Media Change Detection"),
294            MiscCapabilities::Reset => write!(f, "Hard Reset"),
295            _ => write!(f, "Unknown"),
296        }
297    }
298}
299impl fmt::Display for ReadCapabilities {
300    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301        match *self {
302            ReadCapabilities::Audio => write!(f, "Play Audio"),
303            ReadCapabilities::CdDa => write!(f, "CD-DA"),
304            ReadCapabilities::CdPlusG => write!(f, "CD+G"),
305            ReadCapabilities::CdR => write!(f, "CD-R"),
306            ReadCapabilities::CdRw => write!(f, "CD-RW"),
307            ReadCapabilities::DvdR => write!(f, "DVD-R"),
308            ReadCapabilities::DvdPlusR => write!(f, "DVD+R"),
309            ReadCapabilities::DvdRam => write!(f, "DVD-RAM"),
310            ReadCapabilities::DvdRom => write!(f, "DVD-ROM"),
311            ReadCapabilities::DvdRw => write!(f, "DVD-RW"),
312            ReadCapabilities::DvdPlusRw => write!(f, "DVD+RW"),
313            ReadCapabilities::C2Errors => write!(f, "C2 Errors"),
314            ReadCapabilities::Mode2Form1 => write!(f, "Mode 2 Form 1 (VCD)"),
315            ReadCapabilities::Mode2Form2 => write!(f, "Mode 2 Form 2 (VCD)"),
316            ReadCapabilities::Mcn => write!(f, "MCN"),
317            ReadCapabilities::Isrc => write!(f, "ISRC"),
318            _ => write!(f, "Unknown"),
319        }
320    }
321}
322impl fmt::Display for WriteCapabilities {
323    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
324        match *self {
325            WriteCapabilities::CdR => write!(f, "CD-R"),
326            WriteCapabilities::CdRw => write!(f, "CD-RW"),
327            WriteCapabilities::DvdR => write!(f, "DVD-R"),
328            WriteCapabilities::DvdPlusR => write!(f, "DVD+R"),
329            WriteCapabilities::DvdRam => write!(f, "DVD-RAM"),
330            WriteCapabilities::DvdRw => write!(f, "DVD-RW"),
331            WriteCapabilities::DvdPlusRw => write!(f, "DVD+RW"),
332            WriteCapabilities::Mrw => write!(f, "MRW"),
333            WriteCapabilities::BurnProof => write!(f, "Burn Proof"),
334            _ => write!(f, "Unknown"),
335        }
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342    #[test]
343    #[ignore = "requires a disc drive"]
344    fn with_drive() {
345        Drive::with_drive(PathBuf::from("/dev/cdrom")).unwrap();
346    }
347
348    #[test]
349    #[ignore = "requires a disc drive"]
350    fn drives() {
351        assert!(!Drive::drives().is_empty());
352    }
353
354    #[test]
355    #[ignore = "requires a disc drive"]
356    fn hardware_identifiers() {
357        Drive::new().unwrap().hardware_identifiers().unwrap();
358    }
359
360    #[test]
361    #[ignore = "requires a disc drive"]
362    fn capabilities() {
363        Drive::new().unwrap().capabilities().unwrap();
364    }
365}