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    json: Option<String>,
31}
32
33impl AudioDev {
34    pub fn new(driver: impl Into<String>) -> Self {
35        Self {
36            driver: driver.into(),
37            props: Vec::new(),
38            json: None,
39        }
40    }
41    /// Creates an audio backend from QEMU's JSON command-line form.
42    pub fn from_json(json: impl Into<String>) -> Result<Self, String> {
43        let json = json.into();
44        if !json.trim().starts_with('{') || !json.trim().ends_with('}') {
45            return Err("-audiodev JSON must be a JSON object".to_string());
46        }
47        Ok(Self {
48            driver: String::new(),
49            props: Vec::new(),
50            json: Some(json),
51        })
52    }
53
54    pub fn add_prop(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
55        self.props.push(AudioDevProperty {
56            key: key.into(),
57            value: Some(value.into()),
58        });
59        self
60    }
61
62    pub fn add_flag(&mut self, key: impl Into<String>) -> &mut Self {
63        self.props.push(AudioDevProperty { key: key.into(), value: None });
64        self
65    }
66}
67
68impl ToCommand for AudioDev {
69    fn command(&self) -> String {
70        ARG_AUDIODEV.to_string()
71    }
72    fn to_args(&self) -> Vec<String> {
73        if let Some(json) = &self.json {
74            return vec![json.clone()];
75        }
76        let mut args = vec![self.driver.clone()];
77
78        for prop in &self.props {
79            if let Some(value) = &prop.value {
80                args.push(format!("{}={}", prop.key, value));
81            } else {
82                args.push(prop.key.clone());
83            }
84        }
85        vec![args.join(DELIM_COMMA)]
86    }
87}
88
89impl FromStr for AudioDev {
90    type Err = String;
91
92    fn from_str(s: &str) -> Result<Self, Self::Err> {
93        if s.trim().starts_with('{') {
94            return Self::from_json(s);
95        }
96        let mut parts = s.split(DELIM_COMMA);
97        let first = parts.next().ok_or_else(|| "empty -audiodev argument".to_string())?;
98
99        let driver = if let Some(value) = first.strip_prefix("driver=") {
100            value.to_string()
101        } else if !first.contains('=') {
102            first.to_string()
103        } else {
104            return Err(format!("unsupported first -audiodev component: {first}"));
105        };
106
107        let mut props = Vec::new();
108        for part in parts {
109            if let Some((key, value)) = part.split_once('=') {
110                props.push(AudioDevProperty {
111                    key: key.to_string(),
112                    value: Some(value.to_string()),
113                });
114            } else {
115                props.push(AudioDevProperty { key: part.to_string(), value: None });
116            }
117        }
118
119        let has_id = props.iter().any(|prop| prop.key == "id");
120        if !has_id {
121            return Err("-audiodev requires id=".to_string());
122        }
123
124        Ok(Self { driver, props, json: None })
125    }
126}