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    /// We load the disc and issue a TOC command, which is supported only on media CDs
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    /// This method does not work on macOS due to the fact for reliable confirmation
20    /// whether the drive has an Audio CD we need to mount it and later release; macOS
21    /// can assign a different drive name afterwards, so reading that name is unreliable.
22    /// Instead, use open_drive(drive) or open_default(), which acquires the exclusivity
23    #[cfg(target_os = "macos")]
24    pub fn list_drives() -> Result<Vec<DriveInfo>, CdReaderError> {
25        Err(CdReaderError::Io(std::io::Error::other(
26            "list_drives is not reliable on macOS due to remount/re-enumeration; use open_default instead or open a disk directly using CDReader::open",
27        )))
28    }
29
30    /// Enumerate candidate optical drives and probe whether they currently have an audio CD.
31    ///
32    /// On Windows, we try to read type of every drive from A to Z. On Linux, we read
33    /// /sys/class/block directory and check every entry starting with "sr"
34    #[cfg(not(target_os = "macos"))]
35    pub fn list_drives() -> Result<Vec<DriveInfo>, CdReaderError> {
36        let mut paths = {
37            #[cfg(target_os = "windows")]
38            {
39                crate::windows::list_drive_paths().map_err(CdReaderError::Io)?
40            }
41
42            #[cfg(target_os = "linux")]
43            {
44                crate::linux::list_drive_paths().map_err(CdReaderError::Io)?
45            }
46
47            #[cfg(not(any(target_os = "windows", target_os = "linux")))]
48            {
49                compile_error!("Unsupported platform")
50            }
51        };
52
53        paths.sort();
54        paths.dedup();
55
56        let mut drives = Vec::with_capacity(paths.len());
57        for path in paths {
58            let has_audio_cd = match Self::open(&path) {
59                Ok(reader) => match reader.read_toc() {
60                    Ok(toc) => toc.tracks.iter().any(|track| track.is_audio),
61                    Err(_) => false,
62                },
63                Err(_) => false,
64            };
65
66            drives.push(DriveInfo {
67                display_name: Some(path.clone()),
68                path,
69                has_audio_cd,
70            });
71        }
72
73        Ok(drives)
74    }
75
76    /// Open the first discovered drive that currently has an audio CD.
77    ///
78    /// On macOS, we open each drive returned from `diskutil list`, and
79    /// evaluate each disk. Once we are able to open it and read correct TOC,
80    /// we return it back with already acquired exclusivity.
81    #[cfg(target_os = "macos")]
82    pub fn open_default() -> Result<Self, CdReaderError> {
83        let mut paths = crate::macos::list_drive_paths().map_err(CdReaderError::Io)?;
84        paths.sort();
85        paths.dedup();
86
87        for path in paths {
88            let Ok(reader) = Self::open(&path) else {
89                continue;
90            };
91            let Ok(toc) = reader.read_toc() else {
92                continue;
93            };
94            if toc.tracks.iter().any(|track| track.is_audio) {
95                return Ok(reader);
96            }
97        }
98
99        Err(CdReaderError::Io(std::io::Error::new(
100            std::io::ErrorKind::NotFound,
101            "no usable audio CD drive found",
102        )))
103    }
104
105    /// Open the first discovered drive that currently has an audio CD.
106    ///
107    /// On Windows and Linux, we get the first device from the list and
108    /// try to open it, returning an error if it fails.
109    #[cfg(not(target_os = "macos"))]
110    pub fn open_default() -> Result<Self, CdReaderError> {
111        let drives = Self::list_drives()?;
112        let chosen = pick_default_drive_path(&drives).ok_or_else(|| {
113            CdReaderError::Io(std::io::Error::new(
114                std::io::ErrorKind::NotFound,
115                "no usable audio CD drive found",
116            ))
117        })?;
118
119        Self::open(chosen).map_err(CdReaderError::Io)
120    }
121}
122
123#[cfg(any(test, not(target_os = "macos")))]
124fn pick_default_drive_path(drives: &[DriveInfo]) -> Option<&str> {
125    drives
126        .iter()
127        .find(|drive| drive.has_audio_cd)
128        .map(|drive| drive.path.as_str())
129}
130
131#[cfg(test)]
132mod tests {
133    use super::{DriveInfo, pick_default_drive_path};
134
135    #[test]
136    fn chooses_first_audio_drive() {
137        let drives = vec![
138            DriveInfo {
139                path: "disk10".to_string(),
140                display_name: None,
141                has_audio_cd: false,
142            },
143            DriveInfo {
144                path: "disk11".to_string(),
145                display_name: None,
146                has_audio_cd: true,
147            },
148            DriveInfo {
149                path: "disk12".to_string(),
150                display_name: None,
151                has_audio_cd: true,
152            },
153        ];
154
155        assert_eq!(pick_default_drive_path(&drives), Some("disk11"));
156    }
157
158    #[test]
159    fn returns_none_when_no_audio_drive() {
160        let drives = vec![DriveInfo {
161            path: "/dev/sr0".to_string(),
162            display_name: None,
163            has_audio_cd: false,
164        }];
165
166        assert_eq!(pick_default_drive_path(&drives), None);
167    }
168}