Skip to main content

qemu_command_builder/args/
device.rs

1use crate::parsers::ARG_DEVICE;
2use crate::parsers::DELIM_COMMA;
3use crate::shell_string::ShellString;
4use crate::to_command::ToCommand;
5use bon::Builder;
6use proptest_derive::Arbitrary;
7use std::collections::BTreeMap;
8use std::str::FromStr;
9
10/// A generic `-device` property rendered after the device driver name.
11#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Arbitrary)]
12pub struct DeviceProperty {
13    key: ShellString,
14    value: Option<ShellString>,
15}
16
17impl DeviceProperty {
18    /// Creates a `key=value` property.
19    pub fn with_value(key: impl AsRef<str>, value: impl AsRef<str>) -> Self {
20        Self {
21            key: ShellString::from(key.as_ref()),
22            value: Some(ShellString::from(value.as_ref())),
23        }
24    }
25
26    /// Creates a bare `key` property.
27    pub fn flag(key: impl AsRef<str>) -> Self {
28        Self {
29            key: ShellString::from(key.as_ref()),
30            value: None,
31        }
32    }
33}
34
35/// Add a QEMU device driver with optional `prop` or `prop=value` settings.
36///
37/// Valid properties depend on the device driver. Use `-device help` or
38/// `-device driver,help` in QEMU for the device-specific property list.
39#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
40pub struct Device {
41    device: ShellString,
42    #[builder(default)]
43    properties: BTreeMap<ShellString, Option<ShellString>>,
44    json: Option<String>,
45}
46
47impl Device {
48    /// Creates a new device argument for the given QEMU device driver.
49    pub fn new<S: AsRef<str>>(device: S) -> Self {
50        Device {
51            device: ShellString::from(device.as_ref()),
52            properties: Default::default(),
53            json: None,
54        }
55    }
56
57    /// Creates the QEMU 11.1 ARM SMMUv3 device. Properties such as `ril`,
58    /// `ats`, `oas`, `ssidsize`, and `cmdqv` accept QEMU's `auto` value via
59    /// [`Device::add_prop`].
60    pub fn arm_smmuv3() -> Self {
61        Self::new("arm-smmuv3")
62    }
63    /// Creates a device from QEMU's JSON command-line form.
64    pub fn from_json(json: impl Into<String>) -> Result<Self, String> {
65        let json = json.into();
66        if !json.trim().starts_with('{') || !json.trim().ends_with('}') {
67            return Err("-device JSON must be a JSON object".to_string());
68        }
69        Ok(Self {
70            device: ShellString::from(""),
71            properties: BTreeMap::new(),
72            json: Some(json),
73        })
74    }
75
76    /// Adds a `key=value` property to the device.
77    pub fn add_prop<K: AsRef<str>, V: AsRef<str>>(&mut self, key: K, value: V) -> &mut Self {
78        self.properties.insert(ShellString::from(key.as_ref()), Some(ShellString::from(value.as_ref())));
79        self
80    }
81
82    /// Adds a bare `key` property to the device.
83    pub fn add_flag<K: AsRef<str>>(&mut self, key: K) -> &mut Self {
84        self.properties.insert(ShellString::from(key.as_ref()), None);
85        self
86    }
87}
88
89impl ToCommand for Device {
90    fn command(&self) -> String {
91        ARG_DEVICE.to_string()
92    }
93
94    fn to_args(&self) -> Vec<String> {
95        if let Some(json) = &self.json {
96            return vec![json.clone()];
97        }
98        let mut args = vec![self.device.as_ref().to_string()];
99
100        for (prop_key, prop_value) in &self.properties {
101            match prop_value {
102                Some(value) => args.push(format!("{}={}", prop_key.as_ref(), value.as_ref())),
103                None => args.push(prop_key.as_ref().to_string()),
104            }
105        }
106
107        vec![args.join(DELIM_COMMA)]
108    }
109}
110
111impl FromStr for Device {
112    type Err = String;
113
114    fn from_str(s: &str) -> Result<Self, Self::Err> {
115        if s.trim().starts_with('{') {
116            return Self::from_json(s);
117        }
118        let mut parts = s.split(DELIM_COMMA);
119        let device = parts.next().ok_or_else(|| "empty device argument".to_string())?;
120        if device.is_empty() {
121            return Err("missing device driver".to_string());
122        }
123
124        let mut properties = BTreeMap::new();
125        for part in parts {
126            match part.split_once('=') {
127                Some((key, value)) => {
128                    properties.insert(ShellString::from(key), Some(ShellString::from_str(value)?));
129                }
130                None => {
131                    properties.insert(ShellString::from(part), None);
132                }
133            }
134        }
135
136        Ok(Device {
137            device: ShellString::from(device),
138            properties,
139            json: None,
140        })
141    }
142}