Skip to main content

qemu_command_builder/args/
spice.rs

1use crate::common::{AutoNeverAlways, OnOff, OnOffDefaultOff, OnOffDefaultOn};
2use crate::parsers::ARG_SPICE;
3use crate::parsers::DELIM_COMMA;
4use crate::to_command::ToArg;
5use crate::to_command::ToCommand;
6use bon::Builder;
7use proptest_derive::Arbitrary;
8use std::path::PathBuf;
9use std::str::FromStr;
10
11/// A SPICE channel name used by `tls-channel=` and `plaintext-channel=`.
12#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Arbitrary)]
13pub enum Channel {
14    Main,
15    Display,
16    Cursor,
17    Inputs,
18    Record,
19    Playback,
20}
21
22impl ToArg for Channel {
23    fn to_arg(&self) -> &str {
24        match self {
25            Channel::Main => "main",
26            Channel::Display => "display",
27            Channel::Cursor => "cursor",
28            Channel::Inputs => "inputs",
29            Channel::Record => "record",
30            Channel::Playback => "playback",
31        }
32    }
33}
34/// SPICE image compression mode for `image-compression=`.
35#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Default, Arbitrary)]
36pub enum ImageCompression {
37    AutoGlz,
38    #[default]
39    AutoLz,
40    Quic,
41    Glz,
42    Lz,
43    Off,
44}
45
46impl ToArg for ImageCompression {
47    fn to_arg(&self) -> &str {
48        match self {
49            ImageCompression::AutoGlz => "auto_glz",
50            ImageCompression::AutoLz => "auto_lz",
51            ImageCompression::Quic => "quic",
52            ImageCompression::Glz => "glz",
53            ImageCompression::Lz => "lz",
54            ImageCompression::Off => "off",
55        }
56    }
57}
58/// Ternary SPICE policy used by `streaming-video=`.
59#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Default, Arbitrary)]
60pub enum OffAllFilter {
61    #[default]
62    Off,
63    All,
64    Filter,
65}
66
67impl ToArg for OffAllFilter {
68    fn to_arg(&self) -> &str {
69        match self {
70            OffAllFilter::Off => "off",
71            OffAllFilter::All => "all",
72            OffAllFilter::Filter => "filter",
73        }
74    }
75}
76/// Enable the spice remote desktop protocol.
77#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Default, Builder, Arbitrary)]
78pub struct Spice {
79    /// Set the TCP port spice is listening on for plaintext channels.
80    port: Option<u16>,
81
82    /// Set the IP address spice is listening on. Default is any address.
83    addr: Option<String>,
84
85    /// Force using the specified IP version.
86    ipv4: Option<OnOff>,
87    ipv6: Option<OnOff>,
88    unix: Option<OnOff>,
89
90    /// Set the ID of the ``secret`` object containing the password
91    /// you need to authenticate.
92    password_secret: Option<String>,
93
94    /// Require that the client use SASL to authenticate with the spice.
95    /// The exact choice of authentication method used is controlled
96    /// from the system / user's SASL configuration file for the 'qemu'
97    /// service. This is typically found in /etc/sasl2/qemu.conf. If
98    /// running QEMU as an unprivileged user, an environment variable
99    /// SASL\_CONF\_PATH can be used to make it search alternate
100    /// locations for the service config. While some SASL auth methods
101    /// can also provide data encryption (eg GSSAPI), it is recommended
102    /// that SASL always be combined with the 'tls' and 'x509' settings
103    /// to enable use of SSL and server certificates. This ensures a
104    /// data encryption preventing compromise of authentication
105    /// credentials.
106    sasl: Option<OnOff>,
107
108    /// Allow client connects without authentication.
109    disable_ticketing: Option<OnOff>,
110
111    /// Disable copy paste between the client and the guest.
112    disable_copy_paste: Option<OnOff>,
113
114    /// Disable spice-vdagent based file-xfer between the client and the
115    /// guest.
116    disable_agent_file_xfer: Option<OnOff>,
117
118    /// Set the TCP port spice is listening on for encrypted channels.
119    tls_port: Option<u16>,
120
121    /// Set the x509 file directory. Expects same filenames as -vnc
122    /// $display,x509=$dir
123    x509_dir: Option<PathBuf>,
124
125    /// The x509 file names can also be configured individually.
126    x509_key_file: Option<PathBuf>,
127    x509_key_password: Option<PathBuf>,
128    x509_cert_file: Option<PathBuf>,
129    x509_cacert_file: Option<PathBuf>,
130    x509_dh_key_file: Option<PathBuf>,
131
132    /// Specify which ciphers to use.
133    tls_ciphers: Option<String>,
134
135    /// Force specific channel to be used with or without TLS
136    /// encryption. The options can be specified multiple times to
137    /// configure multiple channels. The special name "default" can be
138    /// used to set the default mode. For channels which are not
139    /// explicitly forced into one mode the spice client is allowed to
140    /// pick tls/plaintext as he pleases.
141    tls_channel: Option<Channel>,
142    plaintext_channel: Option<Channel>,
143
144    /// Configure image compression (lossless). Default is auto\_glz.
145    image_compression: Option<ImageCompression>,
146
147    /// Configure wan image compression (lossy for slow links). Default
148    /// is auto.
149    jpeg_wan_compression: Option<AutoNeverAlways>,
150    zlib_glz_wan_compression: Option<AutoNeverAlways>,
151
152    /// Configure video stream detection. Default is off.
153    streaming_video: Option<OffAllFilter>,
154
155    /// Enable/disable passing mouse events via vdagent. Default is on.
156    agent_mouse: Option<OnOffDefaultOn>,
157
158    /// Enable/disable audio stream compression (using celt 0.5.1).
159    /// Default is on.
160    playback_compression: Option<OnOffDefaultOn>,
161
162    /// Enable/disable spice seamless migration. Default is off.
163    seamless_migration: Option<OnOffDefaultOff>,
164
165    /// Enable/disable OpenGL context. Default is off.
166    gl: Option<OnOffDefaultOn>,
167
168    /// DRM render node for OpenGL rendering. If not specified, it will
169    /// pick the first available. (Since 2.9)
170    rendernode: Option<PathBuf>,
171}
172
173impl ToCommand for Spice {
174    fn command(&self) -> String {
175        ARG_SPICE.to_string()
176    }
177    fn to_args(&self) -> Vec<String> {
178        let mut args = vec![];
179        if let Some(port) = &self.port {
180            args.push(format!("port={}", port));
181        }
182        if let Some(addr) = &self.addr {
183            args.push(format!("addr={}", addr));
184        }
185        if let Some(ipv4) = &self.ipv4 {
186            args.push(format!("ipv4={}", ipv4.to_arg()));
187        }
188        if let Some(ipv6) = &self.ipv6 {
189            args.push(format!("ipv6={}", ipv6.to_arg()));
190        }
191        if let Some(unix) = &self.unix {
192            args.push(format!("unix={}", unix.to_arg()));
193        }
194        if let Some(password_secret) = &self.password_secret {
195            args.push(format!("password-secret={}", password_secret));
196        }
197        if let Some(sasl) = &self.sasl {
198            args.push(format!("sasl={}", sasl.to_arg()));
199        }
200        if let Some(disable_ticketing) = &self.disable_ticketing {
201            args.push(format!("disable-ticketing={}", disable_ticketing.to_arg()));
202        }
203        if let Some(disable_copy_paste) = &self.disable_copy_paste {
204            args.push(format!("disable-copy-paste={}", disable_copy_paste.to_arg()));
205        }
206        if let Some(disable_agent_file_xfer) = &self.disable_agent_file_xfer {
207            args.push(format!("disable-agent-file-xfer={}", disable_agent_file_xfer.to_arg()));
208        }
209        if let Some(tls_port) = &self.tls_port {
210            args.push(format!("tls-port={}", tls_port));
211        }
212        if let Some(x509_dir) = &self.x509_dir {
213            args.push(format!("x509-dir={}", x509_dir.display()));
214        }
215        if let Some(x509_key_file) = &self.x509_key_file {
216            args.push(format!("x509-key-file={}", x509_key_file.display()));
217        }
218        if let Some(x509_key_password) = &self.x509_key_password {
219            args.push(format!("x509-key-password={}", x509_key_password.display()));
220        }
221        if let Some(x509_cert_file) = &self.x509_cert_file {
222            args.push(format!("x509-cert-file={}", x509_cert_file.display()));
223        }
224        if let Some(x509_cacert_file) = &self.x509_cacert_file {
225            args.push(format!("x509-cacert-file={}", x509_cacert_file.display()));
226        }
227        if let Some(x509_dh_key_file) = &self.x509_dh_key_file {
228            args.push(format!("x509-dh-key-file={}", x509_dh_key_file.display()));
229        }
230        if let Some(tls_ciphers) = &self.tls_ciphers {
231            args.push(format!("tls-ciphers={}", tls_ciphers));
232        }
233        if let Some(tls_channel) = &self.tls_channel {
234            args.push(format!("tls-channel={}", tls_channel.to_arg()));
235        }
236        if let Some(plaintext_channel) = &self.plaintext_channel {
237            args.push(format!("plaintext-channel={}", plaintext_channel.to_arg()));
238        }
239        if let Some(image_compression) = &self.image_compression {
240            args.push(format!("image-compression={}", image_compression.to_arg()));
241        }
242        if let Some(jpeg_wan_compression) = &self.jpeg_wan_compression {
243            args.push(format!("jpeg-wan-compression={}", jpeg_wan_compression.to_arg()));
244        }
245        if let Some(zlib_glz_wan_compression) = &self.zlib_glz_wan_compression {
246            args.push(format!("zlib-glz-wan-compression={}", zlib_glz_wan_compression.to_arg()));
247        }
248        if let Some(streaming_video) = &self.streaming_video {
249            args.push(format!("streaming-video={}", streaming_video.to_arg()));
250        }
251        if let Some(agent_mouse) = &self.agent_mouse {
252            args.push(format!("agent-mouse={}", agent_mouse.to_arg()));
253        }
254        if let Some(playback_compression) = &self.playback_compression {
255            args.push(format!("playback-compression={}", playback_compression.to_arg()));
256        }
257        if let Some(seamless_migration) = &self.seamless_migration {
258            args.push(format!("seamless-migration={}", seamless_migration.to_arg()));
259        }
260        if let Some(gl) = &self.gl {
261            args.push(format!("gl={}", gl.to_arg()));
262        }
263        if let Some(rendernode) = &self.rendernode {
264            args.push(format!("rendernode={}", rendernode.display()));
265        }
266
267        vec![args.join(DELIM_COMMA)]
268    }
269}
270
271impl FromStr for Spice {
272    type Err = String;
273
274    fn from_str(s: &str) -> Result<Self, Self::Err> {
275        let mut value = Self::default();
276        for part in s.split(DELIM_COMMA).filter(|part| !part.is_empty()) {
277            let (key, raw) = part.split_once('=').ok_or_else(|| format!("invalid -spice option: {part}"))?;
278            match key {
279                "port" => value.port = Some(raw.parse::<u16>().map_err(|e| e.to_string())?),
280                "addr" => value.addr = Some(raw.to_string()),
281                "ipv4" => value.ipv4 = Some(raw.parse::<OnOff>().map_err(|_| format!("invalid ipv4 value: {raw}"))?),
282                "ipv6" => value.ipv6 = Some(raw.parse::<OnOff>().map_err(|_| format!("invalid ipv6 value: {raw}"))?),
283                "unix" => value.unix = Some(raw.parse::<OnOff>().map_err(|_| format!("invalid unix value: {raw}"))?),
284                "password-secret" => value.password_secret = Some(raw.to_string()),
285                "sasl" => value.sasl = Some(raw.parse::<OnOff>().map_err(|_| format!("invalid sasl value: {raw}"))?),
286                "disable-ticketing" => value.disable_ticketing = Some(raw.parse::<OnOff>().map_err(|_| format!("invalid disable-ticketing value: {raw}"))?),
287                "disable-copy-paste" => value.disable_copy_paste = Some(raw.parse::<OnOff>().map_err(|_| format!("invalid disable-copy-paste value: {raw}"))?),
288                "disable-agent-file-xfer" => value.disable_agent_file_xfer = Some(raw.parse::<OnOff>().map_err(|_| format!("invalid disable-agent-file-xfer value: {raw}"))?),
289                "tls-port" => value.tls_port = Some(raw.parse::<u16>().map_err(|e| e.to_string())?),
290                "x509-dir" => value.x509_dir = Some(PathBuf::from(raw)),
291                "x509-key-file" => value.x509_key_file = Some(PathBuf::from(raw)),
292                "x509-key-password" => value.x509_key_password = Some(PathBuf::from(raw)),
293                "x509-cert-file" => value.x509_cert_file = Some(PathBuf::from(raw)),
294                "x509-cacert-file" => value.x509_cacert_file = Some(PathBuf::from(raw)),
295                "x509-dh-key-file" => value.x509_dh_key_file = Some(PathBuf::from(raw)),
296                "tls-ciphers" => value.tls_ciphers = Some(raw.to_string()),
297                "tls-channel" => value.tls_channel = Some(parse_channel(raw)?),
298                "plaintext-channel" => value.plaintext_channel = Some(parse_channel(raw)?),
299                "image-compression" => value.image_compression = Some(parse_image_compression(raw)?),
300                "jpeg-wan-compression" => value.jpeg_wan_compression = Some(parse_auto_never_always(raw)?),
301                "zlib-glz-wan-compression" => value.zlib_glz_wan_compression = Some(parse_auto_never_always(raw)?),
302                "streaming-video" => value.streaming_video = Some(parse_off_all_filter(raw)?),
303                "agent-mouse" => value.agent_mouse = Some(raw.parse::<OnOffDefaultOn>().map_err(|_| format!("invalid agent-mouse value: {raw}"))?),
304                "playback-compression" => value.playback_compression = Some(raw.parse::<OnOffDefaultOn>().map_err(|_| format!("invalid playback-compression value: {raw}"))?),
305                "seamless-migration" => value.seamless_migration = Some(raw.parse::<OnOffDefaultOff>().map_err(|_| format!("invalid seamless-migration value: {raw}"))?),
306                "gl" => value.gl = Some(raw.parse::<OnOffDefaultOn>().map_err(|_| format!("invalid gl value: {raw}"))?),
307                "rendernode" => value.rendernode = Some(PathBuf::from(raw)),
308                other => return Err(format!("unsupported -spice option: {other}")),
309            }
310        }
311        Ok(value)
312    }
313}
314
315fn parse_channel(value: &str) -> Result<Channel, String> {
316    match value {
317        "main" => Ok(Channel::Main),
318        "display" => Ok(Channel::Display),
319        "cursor" => Ok(Channel::Cursor),
320        "inputs" => Ok(Channel::Inputs),
321        "record" => Ok(Channel::Record),
322        "playback" => Ok(Channel::Playback),
323        _ => Err(format!("invalid spice channel: {value}")),
324    }
325}
326
327fn parse_image_compression(value: &str) -> Result<ImageCompression, String> {
328    match value {
329        "auto_glz" => Ok(ImageCompression::AutoGlz),
330        "auto_lz" => Ok(ImageCompression::AutoLz),
331        "quic" => Ok(ImageCompression::Quic),
332        "glz" => Ok(ImageCompression::Glz),
333        "lz" => Ok(ImageCompression::Lz),
334        "off" => Ok(ImageCompression::Off),
335        _ => Err(format!("invalid image-compression value: {value}")),
336    }
337}
338
339fn parse_off_all_filter(value: &str) -> Result<OffAllFilter, String> {
340    match value {
341        "off" => Ok(OffAllFilter::Off),
342        "all" => Ok(OffAllFilter::All),
343        "filter" => Ok(OffAllFilter::Filter),
344        _ => Err(format!("invalid streaming-video value: {value}")),
345    }
346}
347
348fn parse_auto_never_always(value: &str) -> Result<AutoNeverAlways, String> {
349    match value {
350        "auto" => Ok(AutoNeverAlways::Auto),
351        "never" => Ok(AutoNeverAlways::Never),
352        "always" => Ok(AutoNeverAlways::Always),
353        _ => Err(format!("invalid auto/never/always value: {value}")),
354    }
355}