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