Skip to main content

cd_da_reader/
discovery.rs

1use crate::{CdReader, CdReaderError};
2
3/// Information about all found drives. This info is not tested extensively, and in
4/// general it is encouraged to provide a disk drive directly.
5#[derive(Debug, Clone)]
6pub struct DriveInfo {
7    /// Path to the drive, which can be something like 'disk6' on macOS,
8    /// '\\.\E:' on Windows, and '/dev/sr0' on Linux
9    pub path: String,
10    /// Just the device name, without the full path for the OS
11    pub display_name: Option<String>,
12    /// Whether the current disc appears to contain at least one audio track.
13    pub has_audio_cd: bool,
14}
15
16impl CdReader {
17    /// Enumerate candidate optical drives and probe whether they currently have an audio CD.
18    ///
19    /// On macOS, this uses the `IOCDMedia` objects published in the I/O Registry and
20    /// inspects their TOC property without claiming exclusive access.
21    #[cfg(target_os = "macos")]
22    pub fn list_drives() -> Result<Vec<DriveInfo>, CdReaderError> {
23        crate::macos::list_drives().map_err(CdReaderError::Io)
24    }
25
26    /// Enumerate candidate optical drives and probe whether they currently have an audio CD.
27    ///
28    /// On Windows, we try to read type of every drive from A to Z. On Linux, we read
29    /// /sys/class/block directory and check every entry starting with "sr"
30    #[cfg(not(target_os = "macos"))]
31    pub fn list_drives() -> Result<Vec<DriveInfo>, CdReaderError> {
32        let mut paths = {
33            #[cfg(target_os = "windows")]
34            {
35                crate::windows::list_drive_paths().map_err(CdReaderError::Io)?
36            }
37
38            #[cfg(target_os = "linux")]
39            {
40                crate::linux::list_drive_paths().map_err(CdReaderError::Io)?
41            }
42
43            #[cfg(not(any(target_os = "windows", target_os = "linux")))]
44            {
45                compile_error!("Unsupported platform")
46            }
47        };
48
49        paths.sort();
50        paths.dedup();
51
52        let mut drives = Vec::with_capacity(paths.len());
53        for path in paths {
54            let has_audio_cd = match Self::open(&path) {
55                Ok(reader) => match reader.read_toc() {
56                    Ok(toc) => toc.tracks.iter().any(|track| track.is_audio),
57                    Err(_) => false,
58                },
59                Err(_) => false,
60            };
61
62            drives.push(DriveInfo {
63                display_name: Some(path.clone()),
64                path,
65                has_audio_cd,
66            });
67        }
68
69        Ok(drives)
70    }
71
72    /// Open the first discovered drive that currently has an audio CD.
73    ///
74    /// On macOS, we use the passive `IOCDMedia` discovery path and then open
75    /// the matching BSD device name without claiming exclusive access.
76    #[cfg(target_os = "macos")]
77    pub fn open_default() -> Result<Self, CdReaderError> {
78        let drives = Self::list_drives()?;
79        let chosen = drives
80            .iter()
81            .find(|drive| drive.has_audio_cd)
82            .map(|drive| drive.path.as_str())
83            .ok_or_else(|| {
84                CdReaderError::Io(std::io::Error::new(
85                    std::io::ErrorKind::NotFound,
86                    "no usable audio CD drive found",
87                ))
88            })?;
89
90        Self::open(chosen).map_err(CdReaderError::Io)
91    }
92
93    /// Open the first discovered drive that currently has an audio CD.
94    ///
95    /// On Windows and Linux, we get the first device from the list and
96    /// try to open it, returning an error if it fails.
97    #[cfg(not(target_os = "macos"))]
98    pub fn open_default() -> Result<Self, CdReaderError> {
99        let drives = Self::list_drives()?;
100        let chosen = pick_default_drive_path(&drives).ok_or_else(|| {
101            CdReaderError::Io(std::io::Error::new(
102                std::io::ErrorKind::NotFound,
103                "no usable audio CD drive found",
104            ))
105        })?;
106
107        Self::open(chosen).map_err(CdReaderError::Io)
108    }
109}
110
111#[cfg(any(test, not(target_os = "macos")))]
112fn pick_default_drive_path(drives: &[DriveInfo]) -> Option<&str> {
113    drives
114        .iter()
115        .find(|drive| drive.has_audio_cd)
116        .map(|drive| drive.path.as_str())
117}
118
119#[cfg(test)]
120mod tests {
121    use super::{DriveInfo, pick_default_drive_path};
122
123    #[test]
124    fn chooses_first_audio_drive() {
125        let drives = vec![
126            DriveInfo {
127                path: "disk10".to_string(),
128                display_name: None,
129                has_audio_cd: false,
130            },
131            DriveInfo {
132                path: "disk11".to_string(),
133                display_name: None,
134                has_audio_cd: true,
135            },
136            DriveInfo {
137                path: "disk12".to_string(),
138                display_name: None,
139                has_audio_cd: true,
140            },
141        ];
142
143        assert_eq!(pick_default_drive_path(&drives), Some("disk11"));
144    }
145
146    #[test]
147    fn returns_none_when_no_audio_drive() {
148        let drives = vec![DriveInfo {
149            path: "/dev/sr0".to_string(),
150            display_name: None,
151            has_audio_cd: false,
152        }];
153
154        assert_eq!(pick_default_drive_path(&drives), None);
155    }
156}