Coverage Report

Created: 2026-03-14 06:41

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/adhd/audio_processor/src/cdcfg.rs
Line
Count
Source
1
// Copyright 2024 The ChromiumOS Authors
2
// Use of this source code is governed by a BSD-style license that can be
3
// found in the LICENSE file.
4
5
use std::cell::RefCell;
6
use std::collections::HashMap;
7
use std::collections::HashSet;
8
use std::path::Path;
9
use std::path::PathBuf;
10
11
use anyhow::bail;
12
use anyhow::Context;
13
14
use crate::config::Processor;
15
use crate::proto::cdcfg::dlc_plugin;
16
use crate::proto::cdcfg::dlc_plugin::Dlc_id_oneof;
17
use crate::proto::cdcfg::plugin;
18
19
pub trait ResolverContext {
20
    fn get_wav_dump_root(&self) -> Option<&Path>;
21
    fn get_dlc_root_path(&self, dlc_id: &str) -> anyhow::Result<PathBuf>;
22
    fn get_duplicate_channel_0(&self) -> Option<usize>;
23
}
24
25
#[derive(Default)]
26
pub struct NaiveResolverContext {
27
    wav_dump_root: Option<PathBuf>,
28
    duplicate_channel_0: Option<usize>,
29
}
30
31
impl ResolverContext for NaiveResolverContext {
32
0
    fn get_wav_dump_root(&self) -> Option<&Path> {
33
0
        self.wav_dump_root.as_deref()
34
0
    }
35
36
0
    fn get_dlc_root_path(&self, dlc_id: &str) -> anyhow::Result<PathBuf> {
37
0
        Ok(Path::new("/run/imageloader")
38
0
            .join(dlc_id)
39
0
            .join("package/root"))
40
0
    }
41
42
0
    fn get_duplicate_channel_0(&self) -> Option<usize> {
43
0
        self.duplicate_channel_0
44
0
    }
45
}
46
47
#[derive(Default)]
48
struct DlcIdCollector {
49
    dlcs: RefCell<HashSet<String>>,
50
}
51
52
impl ResolverContext for DlcIdCollector {
53
0
    fn get_wav_dump_root(&self) -> Option<&Path> {
54
0
        None
55
0
    }
56
57
0
    fn get_dlc_root_path(&self, dlc_id: &str) -> anyhow::Result<PathBuf> {
58
0
        self.dlcs.borrow_mut().insert(dlc_id.to_string());
59
        // DlcIdCollector is a stub ResolverContext used only to figure out
60
        // the required DLCs, we don't have to return the real DLC path.
61
0
        Ok(PathBuf::from("fake-dlc-path"))
62
0
    }
63
64
0
    fn get_duplicate_channel_0(&self) -> Option<usize> {
65
0
        None
66
0
    }
67
}
68
69
enum ConstOrVar<'a> {
70
    Const(&'a str),
71
    Var(&'a str),
72
}
73
74
impl<'a> From<&'a Dlc_id_oneof> for ConstOrVar<'a> {
75
0
    fn from(value: &'a Dlc_id_oneof) -> Self {
76
0
        match value {
77
0
            Dlc_id_oneof::DlcId(s) => ConstOrVar::Const(s),
78
0
            Dlc_id_oneof::DlcIdVar(s) => ConstOrVar::Var(s),
79
        }
80
0
    }
81
}
82
83
impl<'a> From<&'a dlc_plugin::Path_oneof> for ConstOrVar<'a> {
84
0
    fn from(value: &'a dlc_plugin::Path_oneof) -> Self {
85
0
        match value {
86
0
            dlc_plugin::Path_oneof::Path(s) => ConstOrVar::Const(s),
87
0
            dlc_plugin::Path_oneof::PathVar(s) => ConstOrVar::Var(s),
88
        }
89
0
    }
90
}
91
92
impl<'a> From<&'a dlc_plugin::Constructor_oneof> for ConstOrVar<'a> {
93
0
    fn from(value: &'a dlc_plugin::Constructor_oneof) -> Self {
94
0
        match value {
95
0
            dlc_plugin::Constructor_oneof::Constructor(s) => ConstOrVar::Const(s),
96
0
            dlc_plugin::Constructor_oneof::ConstructorVar(s) => ConstOrVar::Var(s),
97
        }
98
0
    }
99
}
100
101
impl<'a> From<&'a plugin::Path_oneof> for ConstOrVar<'a> {
102
0
    fn from(value: &'a plugin::Path_oneof) -> Self {
103
0
        match value {
104
0
            plugin::Path_oneof::Path(s) => ConstOrVar::Const(s),
105
0
            plugin::Path_oneof::PathVar(s) => ConstOrVar::Var(s),
106
        }
107
0
    }
108
}
109
110
impl<'a> From<&'a plugin::Constructor_oneof> for ConstOrVar<'a> {
111
0
    fn from(value: &'a plugin::Constructor_oneof) -> Self {
112
0
        match value {
113
0
            plugin::Constructor_oneof::Constructor(s) => ConstOrVar::Const(s),
114
0
            plugin::Constructor_oneof::ConstructorVar(s) => ConstOrVar::Var(s),
115
        }
116
0
    }
117
}
118
119
0
fn get_const_or_var_str<'a>(
120
0
    vars: &'a HashMap<String, String>,
121
0
    const_or_var: ConstOrVar<'a>,
122
0
) -> anyhow::Result<&'a str> {
123
0
    match const_or_var {
124
0
        ConstOrVar::Const(s) => Ok(s),
125
0
        ConstOrVar::Var(s) => Ok(vars
126
0
            .get(s)
127
0
            .with_context(|| format!("var {s:?} not found"))?),
128
    }
129
0
}
130
131
0
fn resolve(
132
0
    context: &dyn ResolverContext,
133
0
    vars: &HashMap<String, String>,
134
0
    proto: &crate::proto::cdcfg::Processor,
135
0
) -> anyhow::Result<crate::config::Processor> {
136
0
    let Some(processor) = &proto.processor_oneof else {
137
0
        bail!("processor_oneof is empty")
138
    };
139
    use crate::proto::cdcfg::processor::Processor_oneof::*;
140
0
    Ok(match processor {
141
0
        MaybeWavDump(maybe_wav_dump) => match context.get_wav_dump_root() {
142
0
            Some(wav_dump_root) => Processor::WavSink {
143
0
                path: wav_dump_root.join(maybe_wav_dump.filename.clone()),
144
0
            },
145
0
            None => Processor::Nothing,
146
        },
147
0
        Plugin(plugin) => Processor::Plugin {
148
0
            path: Path::new(get_const_or_var_str(
149
0
                vars,
150
0
                plugin
151
0
                    .path_oneof
152
0
                    .as_ref()
153
0
                    .context("missing plugin path")?
154
0
                    .into(),
155
0
            )?)
156
0
            .into(),
157
0
            constructor: get_const_or_var_str(
158
0
                vars,
159
0
                plugin
160
0
                    .constructor_oneof
161
0
                    .as_ref()
162
0
                    .context("missing constructor")?
163
0
                    .into(),
164
0
            )?
165
0
            .to_string(),
166
        },
167
0
        DlcPlugin(dlc_plugin) => Processor::Plugin {
168
0
            path: context
169
0
                .get_dlc_root_path(get_const_or_var_str(
170
0
                    vars,
171
0
                    dlc_plugin
172
0
                        .dlc_id_oneof
173
0
                        .as_ref()
174
0
                        .context("missing dlc_id")?
175
0
                        .into(),
176
0
                )?)
177
0
                .context("context.dlc_root_path")?
178
0
                .join(get_const_or_var_str(
179
0
                    vars,
180
0
                    dlc_plugin
181
0
                        .path_oneof
182
0
                        .as_ref()
183
0
                        .context("missing path")?
184
0
                        .into(),
185
0
                )?),
186
0
            constructor: get_const_or_var_str(
187
0
                vars,
188
0
                dlc_plugin
189
0
                    .constructor_oneof
190
0
                    .as_ref()
191
0
                    .context("missing constructor")?
192
0
                    .into(),
193
0
            )?
194
0
            .to_string(),
195
        },
196
0
        WrapChunk(wrap_chunk) => Processor::WrapChunk {
197
0
            inner: Box::new(resolve(context, vars, &wrap_chunk.inner).context("wrap_chunk inner")?),
198
0
            inner_block_size: wrap_chunk
199
0
                .inner_block_size
200
0
                .try_into()
201
0
                .context("wrap_chunk inner_block_size")?,
202
            disallow_hoisting: false,
203
        },
204
0
        Resample(resample) => Processor::Resample {
205
0
            output_frame_rate: resample
206
0
                .output_frame_rate
207
0
                .try_into()
208
0
                .context("resample output_frame_rate")?,
209
        },
210
0
        Pipeline(pipeline) => Processor::Pipeline {
211
0
            processors: pipeline
212
0
                .processors
213
0
                .iter()
214
0
                .enumerate()
215
0
                .map(|(i, processor)| -> anyhow::Result<Processor> {
216
0
                    resolve(context, vars, processor)
217
0
                        .with_context(|| format!("pipeline processor {i}"))
218
0
                })
219
0
                .collect::<Result<Vec<_>, _>>()?,
220
        },
221
0
        ShuffleChannels(shuffle_channels) => Processor::ShuffleChannels {
222
0
            channel_indexes: shuffle_channels
223
0
                .channel_indexes
224
0
                .iter()
225
0
                .cloned()
226
0
                .map(usize::try_from)
227
0
                .collect::<Result<Vec<_>, _>>()
228
0
                .context("shuffle_channels channel_indexes")?,
229
        },
230
0
        MaybeDuplicateChannel0(_) => match context.get_duplicate_channel_0() {
231
0
            Some(count) => Processor::ShuffleChannels {
232
0
                channel_indexes: vec![0; count],
233
0
            },
234
0
            None => Processor::Nothing,
235
        },
236
0
        CheckFormat(check_format) => Processor::CheckFormat {
237
0
            channels: positive_or_none(check_format.channels),
238
0
            block_size: positive_or_none(check_format.block_size),
239
0
            frame_rate: positive_or_none(check_format.frame_rate),
240
0
        },
241
0
        Peer(peer) => Processor::Peer {
242
0
            processor: Box::new(resolve(context, vars, &peer.processor).context("peer.processor")?),
243
        },
244
    })
245
0
}
246
247
0
fn positive_or_none(val: i32) -> Option<usize> {
248
0
    match val.try_into() {
249
0
        Ok(0) => None,
250
0
        Ok(val) => Some(val),
251
0
        Err(_) => None,
252
    }
253
0
}
254
255
0
fn resolve_str(
256
0
    context: &dyn ResolverContext,
257
0
    vars: &HashMap<String, String>,
258
0
    s: &str,
259
0
) -> anyhow::Result<Processor> {
260
0
    let cfg: crate::proto::cdcfg::Processor =
261
0
        protobuf::text_format::parse_from_str(s).context("protobuf text_format error")?;
262
0
    resolve(context, vars, &cfg)
263
0
}
264
265
0
pub fn parse(
266
0
    context: &dyn ResolverContext,
267
0
    vars: &HashMap<String, String>,
268
0
    path: &Path,
269
0
) -> anyhow::Result<Processor> {
270
0
    let s = std::fs::read_to_string(path)
271
0
        .with_context(|| format!("read_to_string {}", path.display()))?;
272
0
    resolve_str(context, vars, &s)
273
0
}
274
275
/// Get all DLC IDs specified in a given processing config.
276
0
pub fn get_required_dlcs(
277
0
    vars: &HashMap<String, String>,
278
0
    path: &Path,
279
0
) -> anyhow::Result<HashSet<String>> {
280
0
    let resolver = DlcIdCollector::default();
281
0
    parse(&resolver, vars, path)?;
282
0
    Ok(resolver.dlcs.take())
283
0
}
284
285
#[cfg(test)]
286
mod tests {
287
    use std::collections::HashMap;
288
    use std::collections::HashSet;
289
    use std::path::PathBuf;
290
291
    use super::resolve_str;
292
    use super::NaiveResolverContext;
293
    use super::ResolverContext;
294
    use crate::cdcfg::DlcIdCollector;
295
    use crate::config::Processor;
296
297
    fn assert_resolve_error(
298
        context: &dyn ResolverContext,
299
        vars: &HashMap<String, String>,
300
        s: &str,
301
        err_substring: &str,
302
    ) {
303
        let err = resolve_str(context, vars, s).unwrap_err();
304
        assert!(
305
            err.to_string().contains(err_substring),
306
            "{:?} does not contain {err_substring}",
307
            err.to_string()
308
        );
309
    }
310
311
    #[test]
312
    fn invalid_text_format() {
313
        let context: NaiveResolverContext = Default::default();
314
        assert_resolve_error(
315
            &context,
316
            &HashMap::new(),
317
            "abcd",
318
            "protobuf text_format error",
319
        );
320
    }
321
322
    #[test]
323
    fn passthrough() {
324
        let context: NaiveResolverContext = Default::default();
325
        let processor = resolve_str(
326
            &context,
327
            &HashMap::new(),
328
            r#"pipeline {
329
  processors {
330
    wrap_chunk {
331
      inner_block_size: 10
332
      inner { resample { output_frame_rate: 24000 } }
333
    }
334
  }
335
  processors {
336
    plugin {
337
      path: "foo"
338
      constructor: "bar"
339
    }
340
  }
341
  processors {
342
    shuffle_channels {
343
        channel_indexes: 0
344
        channel_indexes: 1
345
    }
346
  }
347
  processors {
348
    check_format {
349
      channels: 2
350
      block_size: 0
351
      frame_rate: -1
352
    }
353
  }
354
  processors {
355
    peer {
356
      processor {
357
        resample { output_frame_rate: 48000 }
358
      }
359
    }
360
  }
361
}"#,
362
        )
363
        .unwrap();
364
        assert_eq!(
365
            processor,
366
            Processor::Pipeline {
367
                processors: vec![
368
                    Processor::WrapChunk {
369
                        inner: Box::new(Processor::Resample {
370
                            output_frame_rate: 24000
371
                        }),
372
                        inner_block_size: 10,
373
                        disallow_hoisting: false,
374
                    },
375
                    Processor::Plugin {
376
                        path: PathBuf::from("foo"),
377
                        constructor: "bar".into(),
378
                    },
379
                    Processor::ShuffleChannels {
380
                        channel_indexes: vec![0, 1]
381
                    },
382
                    Processor::CheckFormat {
383
                        channels: Some(2),
384
                        block_size: None,
385
                        frame_rate: None
386
                    },
387
                    Processor::Peer {
388
                        processor: Box::new(Processor::Resample {
389
                            output_frame_rate: 48000
390
                        })
391
                    }
392
                ]
393
            }
394
        );
395
    }
396
397
    #[test]
398
    fn error_stack() {
399
        let context: NaiveResolverContext = Default::default();
400
        let err = resolve_str(
401
            &context,
402
            &HashMap::new(),
403
            r#"pipeline {
404
  processors {
405
    shuffle_channels {
406
      channel_indexes: 0
407
      channel_indexes: 1
408
    }
409
  }
410
  processors {
411
    wrap_chunk {
412
      inner_block_size: 10
413
      inner {}
414
    }
415
  }
416
}"#,
417
        )
418
        .unwrap_err();
419
420
        assert_eq!(
421
            format!("{err:#}"),
422
            "pipeline processor 1: wrap_chunk inner: processor_oneof is empty"
423
        );
424
    }
425
426
    #[test]
427
    fn wav_dump() {
428
        let spec = r#"maybe_wav_dump { filename: "a.wav" }"#;
429
430
        let context: NaiveResolverContext = Default::default();
431
        let processor = resolve_str(&context, &HashMap::new(), spec).unwrap();
432
        assert_eq!(processor, Processor::Nothing);
433
434
        let context = NaiveResolverContext {
435
            wav_dump_root: Some(PathBuf::from("/tmp")),
436
            ..Default::default()
437
        };
438
        let processor = resolve_str(&context, &HashMap::new(), spec).unwrap();
439
        assert_eq!(
440
            processor,
441
            Processor::WavSink {
442
                path: PathBuf::from("/tmp/a.wav")
443
            }
444
        );
445
    }
446
447
    #[test]
448
    fn dlc() {
449
        let context: NaiveResolverContext = Default::default();
450
        let spec = r#"
451
pipeline {
452
  processors {
453
    dlc_plugin { dlc_id: "nc-ap-dlc" path: "libdenoiser.so" constructor: "plugin_processor_create" }
454
  }
455
  processors {
456
    dlc_plugin { dlc_id: "second-dlc" path: "libsecond.so" constructor: "plugin_processor_create2" }
457
  }
458
}"#;
459
        let processor = resolve_str(&context, &HashMap::new(), spec).unwrap();
460
        assert_eq!(
461
            processor,
462
            Processor::Pipeline {
463
                processors: vec![
464
                    Processor::Plugin {
465
                        path: PathBuf::from(
466
                            "/run/imageloader/nc-ap-dlc/package/root/libdenoiser.so"
467
                        ),
468
                        constructor: "plugin_processor_create".into(),
469
                    },
470
                    Processor::Plugin {
471
                        path: PathBuf::from(
472
                            "/run/imageloader/second-dlc/package/root/libsecond.so"
473
                        ),
474
                        constructor: "plugin_processor_create2".into(),
475
                    }
476
                ]
477
            }
478
        );
479
480
        let context = DlcIdCollector::default();
481
        resolve_str(&context, &HashMap::new(), spec).unwrap();
482
        assert_eq!(
483
            context.dlcs.borrow().clone(),
484
            HashSet::from(["nc-ap-dlc".to_string(), "second-dlc".to_string()])
485
        );
486
    }
487
488
    #[test]
489
    fn dlc_missing_fields() {
490
        let context: NaiveResolverContext = Default::default();
491
        assert_resolve_error(
492
            &context,
493
            &HashMap::new(),
494
            r#"dlc_plugin { path: "libdenoiser.so" constructor: "plugin_processor_create" }"#,
495
            "missing dlc_id",
496
        );
497
        assert_resolve_error(
498
            &context,
499
            &HashMap::new(),
500
            r#"dlc_plugin { dlc_id: "nc-ap-dlc" constructor: "plugin_processor_create" }"#,
501
            "missing path",
502
        );
503
        assert_resolve_error(
504
            &context,
505
            &HashMap::new(),
506
            r#"dlc_plugin { dlc_id: "nc-ap-dlc" path: "libdenoiser.so" }"#,
507
            "missing constructor",
508
        );
509
    }
510
511
    #[test]
512
    fn dlc_var() {
513
        let context: NaiveResolverContext = Default::default();
514
        let vars = HashMap::from_iter(
515
            [("a", "aaa"), ("b", "bbb"), ("c", "ccc")]
516
                .iter()
517
                .map(|(k, v)| (k.to_string(), v.to_string())),
518
        );
519
        let spec = r#"dlc_plugin { dlc_id_var: "a" path_var: "b" constructor_var: "c" }"#;
520
        let processor = resolve_str(&context, &vars, spec).unwrap();
521
        assert_eq!(
522
            processor,
523
            Processor::Plugin {
524
                path: PathBuf::from("/run/imageloader/aaa/package/root/bbb"),
525
                constructor: "ccc".into(),
526
            }
527
        );
528
    }
529
530
    #[test]
531
    fn dlc_var_missing() {
532
        let context: NaiveResolverContext = Default::default();
533
        assert_resolve_error(
534
            &context,
535
            &HashMap::new(),
536
            r#"dlc_plugin { dlc_id_var: "xyz" path: "libdenoiser.so" constructor: "plugin_processor_create" }"#,
537
            r#"var "xyz" not found"#,
538
        );
539
    }
540
541
    #[test]
542
    fn plugin_var() {
543
        let context: NaiveResolverContext = Default::default();
544
        let vars = HashMap::from_iter(
545
            [("a", "aaa"), ("b", "/path/to/bbb"), ("c", "ccc")]
546
                .iter()
547
                .map(|(k, v)| (k.to_string(), v.to_string())),
548
        );
549
        let spec = r#"plugin { path_var: "b" constructor_var: "c" }"#;
550
        let processor = resolve_str(&context, &vars, spec).unwrap();
551
        assert_eq!(
552
            processor,
553
            Processor::Plugin {
554
                path: PathBuf::from("/path/to/bbb"),
555
                constructor: "ccc".into(),
556
            }
557
        );
558
    }
559
560
    #[test]
561
    fn plugin_var_missing() {
562
        let context: NaiveResolverContext = Default::default();
563
        assert_resolve_error(
564
            &context,
565
            &HashMap::new(),
566
            r#"plugin { path_var: "xyz"constructor: "plugin_processor_create" }"#,
567
            r#"var "xyz" not found"#,
568
        );
569
    }
570
571
    #[test]
572
    fn duplicate_channel_0() {
573
        let spec = "maybe_duplicate_channel_0 {}";
574
575
        let context: NaiveResolverContext = Default::default();
576
        assert_eq!(
577
            resolve_str(&context, &HashMap::new(), spec).unwrap(),
578
            Processor::Nothing
579
        );
580
581
        let context = NaiveResolverContext {
582
            duplicate_channel_0: Some(2),
583
            ..Default::default()
584
        };
585
        assert_eq!(
586
            resolve_str(&context, &HashMap::new(), spec).unwrap(),
587
            Processor::ShuffleChannels {
588
                channel_indexes: vec![0; 2]
589
            }
590
        );
591
    }
592
}