use crate::common::{AutoNeverAlways, OnOff, OnOffDefaultOff, OnOffDefaultOn};
use crate::parsers::ARG_SPICE;
use crate::parsers::DELIM_COMMA;
use crate::to_command::ToArg;
use crate::to_command::ToCommand;
use bon::Builder;
use proptest_derive::Arbitrary;
use std::path::PathBuf;
use std::str::FromStr;
#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Arbitrary)]
pub enum Channel {
Main,
Display,
Cursor,
Inputs,
Record,
Playback,
}
impl ToArg for Channel {
fn to_arg(&self) -> &str {
match self {
Channel::Main => "main",
Channel::Display => "display",
Channel::Cursor => "cursor",
Channel::Inputs => "inputs",
Channel::Record => "record",
Channel::Playback => "playback",
}
}
}
#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Default, Arbitrary)]
pub enum ImageCompression {
AutoGlz,
#[default]
AutoLz,
Quic,
Glz,
Lz,
Off,
}
impl ToArg for ImageCompression {
fn to_arg(&self) -> &str {
match self {
ImageCompression::AutoGlz => "auto_glz",
ImageCompression::AutoLz => "auto_lz",
ImageCompression::Quic => "quic",
ImageCompression::Glz => "glz",
ImageCompression::Lz => "lz",
ImageCompression::Off => "off",
}
}
}
#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Default, Arbitrary)]
pub enum OffAllFilter {
#[default]
Off,
All,
Filter,
}
impl ToArg for OffAllFilter {
fn to_arg(&self) -> &str {
match self {
OffAllFilter::Off => "off",
OffAllFilter::All => "all",
OffAllFilter::Filter => "filter",
}
}
}
#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Default, Builder, Arbitrary)]
pub struct Spice {
port: Option<u16>,
addr: Option<String>,
ipv4: Option<OnOff>,
ipv6: Option<OnOff>,
unix: Option<OnOff>,
password_secret: Option<String>,
sasl: Option<OnOff>,
disable_ticketing: Option<OnOff>,
disable_copy_paste: Option<OnOff>,
disable_agent_file_xfer: Option<OnOff>,
tls_port: Option<u16>,
x509_dir: Option<PathBuf>,
x509_key_file: Option<PathBuf>,
x509_key_password: Option<PathBuf>,
x509_cert_file: Option<PathBuf>,
x509_cacert_file: Option<PathBuf>,
x509_dh_key_file: Option<PathBuf>,
tls_ciphers: Option<String>,
tls_channel: Option<Channel>,
plaintext_channel: Option<Channel>,
image_compression: Option<ImageCompression>,
jpeg_wan_compression: Option<AutoNeverAlways>,
zlib_glz_wan_compression: Option<AutoNeverAlways>,
streaming_video: Option<OffAllFilter>,
agent_mouse: Option<OnOffDefaultOn>,
playback_compression: Option<OnOffDefaultOn>,
seamless_migration: Option<OnOffDefaultOff>,
gl: Option<OnOffDefaultOn>,
rendernode: Option<PathBuf>,
}
impl ToCommand for Spice {
fn command(&self) -> String {
ARG_SPICE.to_string()
}
fn to_args(&self) -> Vec<String> {
let mut args = vec![];
if let Some(port) = &self.port {
args.push(format!("port={}", port));
}
if let Some(addr) = &self.addr {
args.push(format!("addr={}", addr));
}
if let Some(ipv4) = &self.ipv4 {
args.push(format!("ipv4={}", ipv4.to_arg()));
}
if let Some(ipv6) = &self.ipv6 {
args.push(format!("ipv6={}", ipv6.to_arg()));
}
if let Some(unix) = &self.unix {
args.push(format!("unix={}", unix.to_arg()));
}
if let Some(password_secret) = &self.password_secret {
args.push(format!("password-secret={}", password_secret));
}
if let Some(sasl) = &self.sasl {
args.push(format!("sasl={}", sasl.to_arg()));
}
if let Some(disable_ticketing) = &self.disable_ticketing {
args.push(format!("disable-ticketing={}", disable_ticketing.to_arg()));
}
if let Some(disable_copy_paste) = &self.disable_copy_paste {
args.push(format!("disable-copy-paste={}", disable_copy_paste.to_arg()));
}
if let Some(disable_agent_file_xfer) = &self.disable_agent_file_xfer {
args.push(format!("disable-agent-file-xfer={}", disable_agent_file_xfer.to_arg()));
}
if let Some(tls_port) = &self.tls_port {
args.push(format!("tls-port={}", tls_port));
}
if let Some(x509_dir) = &self.x509_dir {
args.push(format!("x509-dir={}", x509_dir.display()));
}
if let Some(x509_key_file) = &self.x509_key_file {
args.push(format!("x509-key-file={}", x509_key_file.display()));
}
if let Some(x509_key_password) = &self.x509_key_password {
args.push(format!("x509-key-password={}", x509_key_password.display()));
}
if let Some(x509_cert_file) = &self.x509_cert_file {
args.push(format!("x509-cert-file={}", x509_cert_file.display()));
}
if let Some(x509_cacert_file) = &self.x509_cacert_file {
args.push(format!("x509-cacert-file={}", x509_cacert_file.display()));
}
if let Some(x509_dh_key_file) = &self.x509_dh_key_file {
args.push(format!("x509-dh-key-file={}", x509_dh_key_file.display()));
}
if let Some(tls_ciphers) = &self.tls_ciphers {
args.push(format!("tls-ciphers={}", tls_ciphers));
}
if let Some(tls_channel) = &self.tls_channel {
args.push(format!("tls-channel={}", tls_channel.to_arg()));
}
if let Some(plaintext_channel) = &self.plaintext_channel {
args.push(format!("plaintext-channel={}", plaintext_channel.to_arg()));
}
if let Some(image_compression) = &self.image_compression {
args.push(format!("image-compression={}", image_compression.to_arg()));
}
if let Some(jpeg_wan_compression) = &self.jpeg_wan_compression {
args.push(format!("jpeg-wan-compression={}", jpeg_wan_compression.to_arg()));
}
if let Some(zlib_glz_wan_compression) = &self.zlib_glz_wan_compression {
args.push(format!("zlib-glz-wan-compression={}", zlib_glz_wan_compression.to_arg()));
}
if let Some(streaming_video) = &self.streaming_video {
args.push(format!("streaming-video={}", streaming_video.to_arg()));
}
if let Some(agent_mouse) = &self.agent_mouse {
args.push(format!("agent-mouse={}", agent_mouse.to_arg()));
}
if let Some(playback_compression) = &self.playback_compression {
args.push(format!("playback-compression={}", playback_compression.to_arg()));
}
if let Some(seamless_migration) = &self.seamless_migration {
args.push(format!("seamless-migration={}", seamless_migration.to_arg()));
}
if let Some(gl) = &self.gl {
args.push(format!("gl={}", gl.to_arg()));
}
if let Some(rendernode) = &self.rendernode {
args.push(format!("rendernode={}", rendernode.display()));
}
vec![args.join(DELIM_COMMA)]
}
}
impl FromStr for Spice {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut value = Self::default();
for part in s.split(DELIM_COMMA).filter(|part| !part.is_empty()) {
let (key, raw) = part.split_once('=').ok_or_else(|| format!("invalid -spice option: {part}"))?;
match key {
"port" => value.port = Some(raw.parse::<u16>().map_err(|e| e.to_string())?),
"addr" => value.addr = Some(raw.to_string()),
"ipv4" => value.ipv4 = Some(raw.parse::<OnOff>().map_err(|_| format!("invalid ipv4 value: {raw}"))?),
"ipv6" => value.ipv6 = Some(raw.parse::<OnOff>().map_err(|_| format!("invalid ipv6 value: {raw}"))?),
"unix" => value.unix = Some(raw.parse::<OnOff>().map_err(|_| format!("invalid unix value: {raw}"))?),
"password-secret" => value.password_secret = Some(raw.to_string()),
"sasl" => value.sasl = Some(raw.parse::<OnOff>().map_err(|_| format!("invalid sasl value: {raw}"))?),
"disable-ticketing" => value.disable_ticketing = Some(raw.parse::<OnOff>().map_err(|_| format!("invalid disable-ticketing value: {raw}"))?),
"disable-copy-paste" => value.disable_copy_paste = Some(raw.parse::<OnOff>().map_err(|_| format!("invalid disable-copy-paste value: {raw}"))?),
"disable-agent-file-xfer" => value.disable_agent_file_xfer = Some(raw.parse::<OnOff>().map_err(|_| format!("invalid disable-agent-file-xfer value: {raw}"))?),
"tls-port" => value.tls_port = Some(raw.parse::<u16>().map_err(|e| e.to_string())?),
"x509-dir" => value.x509_dir = Some(PathBuf::from(raw)),
"x509-key-file" => value.x509_key_file = Some(PathBuf::from(raw)),
"x509-key-password" => value.x509_key_password = Some(PathBuf::from(raw)),
"x509-cert-file" => value.x509_cert_file = Some(PathBuf::from(raw)),
"x509-cacert-file" => value.x509_cacert_file = Some(PathBuf::from(raw)),
"x509-dh-key-file" => value.x509_dh_key_file = Some(PathBuf::from(raw)),
"tls-ciphers" => value.tls_ciphers = Some(raw.to_string()),
"tls-channel" => value.tls_channel = Some(parse_channel(raw)?),
"plaintext-channel" => value.plaintext_channel = Some(parse_channel(raw)?),
"image-compression" => value.image_compression = Some(parse_image_compression(raw)?),
"jpeg-wan-compression" => value.jpeg_wan_compression = Some(parse_auto_never_always(raw)?),
"zlib-glz-wan-compression" => value.zlib_glz_wan_compression = Some(parse_auto_never_always(raw)?),
"streaming-video" => value.streaming_video = Some(parse_off_all_filter(raw)?),
"agent-mouse" => value.agent_mouse = Some(raw.parse::<OnOffDefaultOn>().map_err(|_| format!("invalid agent-mouse value: {raw}"))?),
"playback-compression" => value.playback_compression = Some(raw.parse::<OnOffDefaultOn>().map_err(|_| format!("invalid playback-compression value: {raw}"))?),
"seamless-migration" => value.seamless_migration = Some(raw.parse::<OnOffDefaultOff>().map_err(|_| format!("invalid seamless-migration value: {raw}"))?),
"gl" => value.gl = Some(raw.parse::<OnOffDefaultOn>().map_err(|_| format!("invalid gl value: {raw}"))?),
"rendernode" => value.rendernode = Some(PathBuf::from(raw)),
other => return Err(format!("unsupported -spice option: {other}")),
}
}
Ok(value)
}
}
fn parse_channel(value: &str) -> Result<Channel, String> {
match value {
"main" => Ok(Channel::Main),
"display" => Ok(Channel::Display),
"cursor" => Ok(Channel::Cursor),
"inputs" => Ok(Channel::Inputs),
"record" => Ok(Channel::Record),
"playback" => Ok(Channel::Playback),
_ => Err(format!("invalid spice channel: {value}")),
}
}
fn parse_image_compression(value: &str) -> Result<ImageCompression, String> {
match value {
"auto_glz" => Ok(ImageCompression::AutoGlz),
"auto_lz" => Ok(ImageCompression::AutoLz),
"quic" => Ok(ImageCompression::Quic),
"glz" => Ok(ImageCompression::Glz),
"lz" => Ok(ImageCompression::Lz),
"off" => Ok(ImageCompression::Off),
_ => Err(format!("invalid image-compression value: {value}")),
}
}
fn parse_off_all_filter(value: &str) -> Result<OffAllFilter, String> {
match value {
"off" => Ok(OffAllFilter::Off),
"all" => Ok(OffAllFilter::All),
"filter" => Ok(OffAllFilter::Filter),
_ => Err(format!("invalid streaming-video value: {value}")),
}
}
fn parse_auto_never_always(value: &str) -> Result<AutoNeverAlways, String> {
match value {
"auto" => Ok(AutoNeverAlways::Auto),
"never" => Ok(AutoNeverAlways::Never),
"always" => Ok(AutoNeverAlways::Always),
_ => Err(format!("invalid auto/never/always value: {value}")),
}
}