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#[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#[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#[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#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Default, Builder, Arbitrary)]
78pub struct Spice {
79 port: Option<u16>,
81
82 addr: Option<String>,
84
85 ipv4: Option<OnOff>,
87 ipv6: Option<OnOff>,
88 unix: Option<OnOff>,
89
90 password_secret: Option<String>,
93
94 sasl: Option<OnOff>,
107
108 disable_ticketing: Option<OnOff>,
110
111 disable_copy_paste: Option<OnOff>,
113
114 disable_agent_file_xfer: Option<OnOff>,
117
118 tls_port: Option<u16>,
120
121 x509_dir: Option<PathBuf>,
124
125 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 tls_ciphers: Option<String>,
134
135 tls_channel: Option<Channel>,
142 plaintext_channel: Option<Channel>,
143
144 image_compression: Option<ImageCompression>,
146
147 jpeg_wan_compression: Option<AutoNeverAlways>,
150 zlib_glz_wan_compression: Option<AutoNeverAlways>,
151
152 streaming_video: Option<OffAllFilter>,
154
155 agent_mouse: Option<OnOffDefaultOn>,
157
158 playback_compression: Option<OnOffDefaultOn>,
161
162 seamless_migration: Option<OnOffDefaultOff>,
164
165 gl: Option<OnOffDefaultOn>,
167
168 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}