Skip to main content

qemu_command_builder/args/
audiodev.rs

1use crate::parsers::ARG_AUDIODEV;
2use std::str::FromStr;
3
4use bon::Builder;
5use proptest_derive::Arbitrary;
6
7use crate::parsers::DELIM_COMMA;
8use crate::to_command::ToCommand;
9
10/// A generic `prop` or `prop=value` entry for `-audiodev`.
11#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Arbitrary)]
12pub struct AudioDevProperty {
13    /// The property name.
14    pub key: String,
15    /// The optional property value.
16    pub value: Option<String>,
17}
18
19/// A QEMU `-audiodev [driver=]driver,id=id[,prop[=value][,...]]` definition.
20///
21/// This type intentionally preserves arbitrary property keys instead of
22/// validating backend-specific options, so it can round-trip the generic
23/// `-audiodev` surface described by QEMU.
24#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Default, Builder, Arbitrary)]
25pub struct AudioDev {
26    /// The backend driver name, such as `alsa`, `pa`, `spice`, or `wav`.
27    driver: String,
28    /// Generic backend/global properties in canonical output order.
29    props: Vec<AudioDevProperty>,
30}
31
32impl AudioDev {
33    pub fn new(driver: impl Into<String>) -> Self {
34        Self {
35            driver: driver.into(),
36            props: Vec::new(),
37        }
38    }
39
40    pub fn add_prop(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
41        self.props.push(AudioDevProperty {
42            key: key.into(),
43            value: Some(value.into()),
44        });
45        self
46    }
47
48    pub fn add_flag(&mut self, key: impl Into<String>) -> &mut Self {
49        self.props.push(AudioDevProperty { key: key.into(), value: None });
50        self
51    }
52}
53
54impl ToCommand for AudioDev {
55    fn command(&self) -> String {
56        ARG_AUDIODEV.to_string()
57    }
58    fn to_args(&self) -> Vec<String> {
59        let mut args = vec![self.driver.clone()];
60
61        let mut props = self.props.clone();
62        props.sort_by(|a, b| a.key.cmp(&b.key).then_with(|| a.value.cmp(&b.value)));
63
64        for prop in &props {
65            if let Some(value) = &prop.value {
66                args.push(format!("{}={}", prop.key, value));
67            } else {
68                args.push(prop.key.clone());
69            }
70        }
71        vec![args.join(DELIM_COMMA)]
72    }
73}
74
75impl FromStr for AudioDev {
76    type Err = String;
77
78    fn from_str(s: &str) -> Result<Self, Self::Err> {
79        let mut parts = s.split(DELIM_COMMA);
80        let first = parts.next().ok_or_else(|| "empty -audiodev argument".to_string())?;
81
82        let driver = if let Some(value) = first.strip_prefix("driver=") {
83            value.to_string()
84        } else if !first.contains('=') {
85            first.to_string()
86        } else {
87            return Err(format!("unsupported first -audiodev component: {first}"));
88        };
89
90        let mut props = Vec::new();
91        for part in parts {
92            if let Some((key, value)) = part.split_once('=') {
93                props.push(AudioDevProperty {
94                    key: key.to_string(),
95                    value: Some(value.to_string()),
96                });
97            } else {
98                props.push(AudioDevProperty { key: part.to_string(), value: None });
99            }
100        }
101
102        let has_id = props.iter().any(|prop| prop.key == "id");
103        if !has_id {
104            return Err("-audiodev requires id=".to_string());
105        }
106
107        Ok(Self { driver, props })
108    }
109}