1use crate::{Error, ErrorContext, ErrorGroup, NativeStatus, Operation, Result};
3
4pub fn collect<I, T>(
8 ids: Result<impl IntoIterator<Item = I>>,
9 mut read: impl FnMut(I) -> Result<Option<T>>,
10) -> Result<Vec<T>> {
11 let mut out = Vec::new();
12 for id in ids? {
13 if let Some(entry) = read(id)? {
14 out.push(entry);
15 }
16 }
17 Ok(out)
18}
19
20pub fn device_identity(
23 name: Result<String>,
24 native_id: impl FnOnce() -> Result<String>,
25) -> Option<(String, String)> {
26 if let Ok(name) = name {
27 if !name.is_empty() {
28 return Some((name.clone(), name));
29 }
30 }
31 native_id()
32 .ok()
33 .filter(|id| !id.is_empty())
34 .map(|id| (id, String::new()))
35}
36
37pub fn device_identity_matches(
39 requested_id: &str,
40 name: Result<String>,
41 native_id: impl FnOnce() -> Result<String>,
42) -> bool {
43 if name.is_ok_and(|name| !name.is_empty() && name == requested_id) {
44 return true;
45 }
46 native_id().is_ok_and(|id| !id.is_empty() && id == requested_id)
47}
48
49pub fn selected_output_device<'a>(
51 devices: &'a [crate::DeviceInfo],
52 requested_id: Option<&str>,
53) -> Result<&'a crate::DeviceInfo> {
54 let device = devices
55 .iter()
56 .find(|device| {
57 device.is_loopback && requested_id.map_or(device.is_default, |id| device.id == id)
58 })
59 .ok_or(Error::DeviceNotFound)?;
60 require_device_format(if device.format_available {
61 Ok((device.sample_rate, device.channels))
62 } else {
63 Err(Error::Backend(
64 "selected output device format is unavailable".into(),
65 ))
66 })?;
67 Ok(device)
68}
69
70pub fn device_format(format: Result<(u32, u16)>) -> (u32, u16, bool) {
72 match format {
73 Ok((rate, channels)) if rate > 0 && channels > 0 => (rate, channels, true),
74 _ => (0, 0, false),
75 }
76}
77
78pub fn require_device_format(format: Result<(u32, u16)>) -> Result<(u32, u16)> {
80 let format = format.and_then(|format| {
81 if format.0 == 0 || format.1 == 0 {
82 Err(Error::UnsupportedFormat(
83 "native device format contains a zero rate or channel count".into(),
84 ))
85 } else {
86 Ok(format)
87 }
88 });
89 format.map_err(|cause| Error::Multiple(ErrorGroup::new(
90 Error::Backend("device capture format could not be read; reconnect the device and retry discovery/open".into())
91 .with_context(ErrorContext::new(Operation::Start)),
92 cause,
93 Vec::new(),
94 )))
95}
96
97pub fn optional<T>(value: Result<Option<T>>) -> Option<T> {
99 value.ok().flatten()
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum ProcessFailure {
105 Vanished,
107 UnknownMetadata,
109}
110
111pub fn windows_process_failure(bits: u32) -> ProcessFailure {
114 if bits == 0x8007_0057 {
115 ProcessFailure::Vanished
116 } else {
117 ProcessFailure::UnknownMetadata
118 }
119}
120
121pub fn macos_process_failure(code: i32) -> ProcessFailure {
124 if matches!(code, 0x216f_626a | 3 | 2) {
125 ProcessFailure::Vanished
126 } else {
127 ProcessFailure::UnknownMetadata
128 }
129}
130
131pub fn windows_error_failure(error: &Error) -> ProcessFailure {
133 match error {
134 Error::Context { source, context } => match context.native_status() {
135 Some(NativeStatus::HResult { bits, .. }) => windows_process_failure(bits),
136 _ => windows_error_failure(source),
137 },
138 Error::Diagnostic { source, .. } => windows_error_failure(source),
139 Error::Multiple(group) => windows_error_failure(group.primary()),
140 _ => ProcessFailure::UnknownMetadata,
141 }
142}
143
144pub fn process_metadata<T>(
147 value: Result<Option<T>>,
148 classify: impl FnOnce(&Error) -> ProcessFailure,
149) -> Option<Option<T>> {
150 match value {
151 Ok(value) => Some(value),
152 Err(error) if classify(&error) == ProcessFailure::Vanished => None,
153 Err(_) => Some(None),
154 }
155}
156
157pub fn macos_error_failure(error: &Error) -> ProcessFailure {
159 match error {
160 Error::Context { source, context } => match context.native_status() {
161 Some(NativeStatus::OsStatus { value, .. }) => macos_process_failure(value),
162 _ => macos_error_failure(source),
163 },
164 Error::Diagnostic { source, .. } => macos_error_failure(source),
165 Error::Multiple(group) => macos_error_failure(group.primary()),
166 _ => ProcessFailure::UnknownMetadata,
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 fn fault(code: i32) -> Error {
174 Error::Backend("fixture failed; retry".into()).with_context(
175 ErrorContext::new(Operation::Enumerate).with_native_status(NativeStatus::OsStatus {
176 call: "fixture",
177 value: code,
178 }),
179 )
180 }
181 #[test]
184 fn unreadable_device_name_keeps_selectable_native_identity() {
185 assert_eq!(
186 device_identity(Err(fault(-50)), || Ok("native-id".into())),
187 Some(("native-id".into(), String::new()))
188 );
189 assert_eq!(
190 device_identity(Ok(String::new()), || Ok("native-id".into())),
191 Some(("native-id".into(), String::new()))
192 );
193 assert_eq!(device_identity(Err(fault(-50)), || Err(fault(-1))), None);
194 assert_eq!(
195 device_identity(Ok("name".into()), || panic!("unnecessary native query")),
196 Some(("name".into(), "name".into()))
197 );
198 }
199
200 #[test]
201 fn native_identity_remains_selectable_after_display_name_recovers() {
202 assert!(device_identity_matches(
203 "native-id",
204 Ok("recovered name".into()),
205 || Ok("native-id".into())
206 ));
207 assert!(device_identity_matches(
208 "recovered name",
209 Ok("recovered name".into()),
210 || panic!("unnecessary native query")
211 ));
212 assert!(!device_identity_matches(
213 "missing",
214 Err(fault(-50)),
215 || Err(fault(-1))
216 ));
217 }
218
219 #[test]
220 fn default_output_open_rejects_unavailable_format_and_keeps_cause() {
221 let device = crate::DeviceInfo {
222 id: "fixture".into(),
223 name: "fixture".into(),
224 source_kind: crate::SourceKind::SystemLoopback,
225 sample_rate: 0,
226 channels: 0,
227 format_available: false,
228 is_loopback: true,
229 is_default: true,
230 };
231 let error = selected_output_device(std::slice::from_ref(&device), None).unwrap_err();
232 assert!(error.to_string().contains("could not be read"));
233 assert_eq!(error.kind(), crate::ErrorKind::Backend);
234 assert!(matches!(error, Error::Multiple(_)));
235 assert!(selected_output_device(std::slice::from_ref(&device), Some("fixture")).is_err());
236 let available = crate::DeviceInfo {
237 sample_rate: 44_100,
238 channels: 6,
239 format_available: true,
240 ..device
241 };
242 assert_eq!(
243 selected_output_device(&[available], None)
244 .map(|device| (device.sample_rate, device.channels)),
245 Ok((44_100, 6))
246 );
247 assert_eq!(
248 selected_output_device(&[], None),
249 Err(Error::DeviceNotFound)
250 );
251 }
252
253 #[test]
254 fn optional_default_failure_keeps_inventory_without_a_default() {
255 let default = optional::<u32>(Err(fault(-50)));
256 let entries = collect(Ok([0, 1, 2]), |id| Ok(Some((id, default == Some(id))))).unwrap();
257 assert_eq!(entries, [(0, false), (1, false), (2, false)]);
258 }
259
260 #[test]
261 fn per_entry_format_failure_keeps_neighbors_and_uses_zero_sentinel() {
262 let entries = collect(Ok([0, 1, 2]), |id| {
263 Ok(Some(device_format(if id == 1 {
264 Err(fault(-50))
265 } else {
266 Ok((44_100, 2))
267 })))
268 })
269 .unwrap();
270 assert_eq!(
271 entries,
272 [(44_100, 2, true), (0, 0, false), (44_100, 2, true)]
273 );
274 assert_eq!(device_format(Ok((0, 2))), (0, 0, false));
275 let error = require_device_format(Err(fault(-50))).unwrap_err();
276 assert_eq!(error.kind(), crate::ErrorKind::Backend);
277 assert!(error.to_string().contains("could not be read"));
278 assert!(error.to_string().contains("retry"));
279 let Error::Multiple(group) = error else {
280 panic!("missing typed cause")
281 };
282 assert_eq!(group.secondary().next(), Some(&fault(-50)));
283 }
284 #[test]
285 fn optional_names_do_not_fail_the_inventory() {
286 let entries = collect(Ok([0, 1, 2]), |id| {
287 Ok(Some(optional(if id == 1 {
288 Err(fault(-50))
289 } else {
290 Ok(Some("name"))
291 })))
292 })
293 .unwrap();
294 assert_eq!(entries, [Some("name"), None, Some("name")]);
295 }
296 #[test]
297 fn collection_and_count_faults_fail_the_whole_call_with_cause() {
298 for code in [-50, -1] {
299 assert_eq!(
300 collect::<u32, u32>(Err::<Vec<u32>, _>(fault(code)), |id| Ok(Some(id))),
301 Err(fault(code))
302 );
303 }
304 }
305 #[test]
306 fn native_process_fault_classes_keep_unknown_names_and_omit_vanished() {
307 let windows = collect(Ok([0x8007_0057, 0x8007_0005, 0x8007_001f]), |bits| {
308 let error = Error::Backend("fixture failed; retry".into()).with_context(
309 ErrorContext::new(Operation::Enumerate).with_native_status(NativeStatus::HResult {
310 call: "OpenProcess",
311 bits,
312 }),
313 );
314 Ok(process_metadata(
315 Err::<Option<String>, _>(error),
316 windows_error_failure,
317 ))
318 })
319 .unwrap();
320 assert_eq!(windows, [None, None]);
321 let mac = collect(Ok([0x216f_626a, 3, 2, 13, 1, -50]), |code| {
322 Ok(process_metadata(
323 Err::<Option<String>, _>(fault(code)),
324 macos_error_failure,
325 ))
326 })
327 .unwrap();
328 assert_eq!(mac, [None, None, None]);
329 }
330}