qemu-command-builder 11.0.0-1

Type safe command line builder for qemu
Documentation
use std::path::PathBuf;
use std::str::FromStr;

use bon::Builder;
use proptest_derive::Arbitrary;

use crate::common::OnOff;
use crate::parsers::{ARG_INCOMING, DELIM_COMMA};
use crate::to_command::{ToArg, ToCommand};

#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
pub struct Tcp {
    host: Option<String>,
    port: u16,
    to: Option<u16>,
    ipv4: Option<OnOff>,
    ipv6: Option<OnOff>,
}

#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
pub struct Rdma {
    host: String,
    port: u16,
    ipv4: Option<OnOff>,
    ipv6: Option<OnOff>,
}

#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
pub struct File {
    filename: PathBuf,
    offset: Option<String>,
}

#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Arbitrary)]
pub enum Incoming {
    Tcp(Tcp),
    Rdma(Rdma),
    Unix(PathBuf),
    Fd(String),
    File(File),
    Exec(String),
    Channel(String),
    Defer,
}

impl ToCommand for Incoming {
    fn command(&self) -> String {
        ARG_INCOMING.to_string()
    }
    fn to_args(&self) -> Vec<String> {
        match self {
            Incoming::Tcp(tcp) => {
                let mut args = vec![];
                if let Some(host) = &tcp.host {
                    args.push(format!("tcp:{}:{}", host, tcp.port));
                } else {
                    args.push(format!("tcp::{}", tcp.port));
                }
                if let Some(to) = &tcp.to {
                    args.push(format!("to={}", to));
                }
                if let Some(ipv4) = &tcp.ipv4 {
                    args.push(format!("ipv4={}", ipv4.to_arg()));
                }
                if let Some(ipv6) = &tcp.ipv6 {
                    args.push(format!("ipv6={}", ipv6.to_arg()));
                }
                vec![args.join(DELIM_COMMA)]
            }
            Incoming::Rdma(rdma) => {
                let mut args = vec![];
                args.push(format!("rdma:{}:{}", rdma.host, rdma.port));
                if let Some(ipv4) = &rdma.ipv4 {
                    args.push(format!("ipv4={}", ipv4.to_arg()));
                }
                if let Some(ipv6) = &rdma.ipv6 {
                    args.push(format!("ipv6={}", ipv6.to_arg()));
                }
                vec![args.join(DELIM_COMMA)]
            }
            Incoming::Unix(unix) => {
                vec![format!("unix:{}", unix.display())]
            }
            Incoming::Fd(fd) => {
                vec![format!("fd:{}", fd)]
            }
            Incoming::File(file) => {
                let mut args = vec![format!("file:{}", file.filename.display())];
                if let Some(offset) = &file.offset {
                    args.push(format!("offset={}", offset));
                }
                vec![args.join(DELIM_COMMA)]
            }
            Incoming::Exec(exec) => {
                vec![format!("exec:{}", exec)]
            }
            Incoming::Channel(chrono) => {
                vec![format!("channel:{}", chrono)]
            }
            Incoming::Defer => {
                vec!["defer".to_string()]
            }
        }
    }
}

impl FromStr for Incoming {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s == "defer" {
            return Ok(Self::Defer);
        }
        if let Some(rest) = s.strip_prefix("tcp:") {
            let mut parts = rest.split(DELIM_COMMA);
            let endpoint = parts.next().ok_or_else(|| "invalid incoming tcp endpoint".to_string())?;
            let (host, port) = parse_optional_host_port(endpoint)?;
            let mut to = None;
            let mut ipv4 = None;
            let mut ipv6 = None;
            for part in parts {
                let (key, value) = part.split_once('=').ok_or_else(|| format!("invalid incoming tcp option: {part}"))?;
                match key {
                    "to" => to = Some(value.parse::<u16>().map_err(|e| e.to_string())?),
                    "ipv4" => ipv4 = Some(value.parse::<OnOff>().map_err(|_| format!("invalid ipv4 value: {value}"))?),
                    "ipv6" => ipv6 = Some(value.parse::<OnOff>().map_err(|_| format!("invalid ipv6 value: {value}"))?),
                    other => return Err(format!("unsupported incoming tcp option: {other}")),
                }
            }
            return Ok(Self::Tcp(Tcp { host, port, to, ipv4, ipv6 }));
        }
        if let Some(rest) = s.strip_prefix("rdma:") {
            let mut parts = rest.split(DELIM_COMMA);
            let endpoint = parts.next().ok_or_else(|| "invalid incoming rdma endpoint".to_string())?;
            let (host, port) = endpoint.rsplit_once(':').ok_or_else(|| format!("invalid rdma endpoint: {endpoint}"))?;
            let mut ipv4 = None;
            let mut ipv6 = None;
            for part in parts {
                let (key, value) = part.split_once('=').ok_or_else(|| format!("invalid incoming rdma option: {part}"))?;
                match key {
                    "ipv4" => ipv4 = Some(value.parse::<OnOff>().map_err(|_| format!("invalid ipv4 value: {value}"))?),
                    "ipv6" => ipv6 = Some(value.parse::<OnOff>().map_err(|_| format!("invalid ipv6 value: {value}"))?),
                    other => return Err(format!("unsupported incoming rdma option: {other}")),
                }
            }
            return Ok(Self::Rdma(Rdma {
                host: host.to_string(),
                port: port.parse::<u16>().map_err(|e| e.to_string())?,
                ipv4,
                ipv6,
            }));
        }
        if let Some(path) = s.strip_prefix("unix:") {
            return Ok(Self::Unix(PathBuf::from(path)));
        }
        if let Some(fd) = s.strip_prefix("fd:") {
            return Ok(Self::Fd(fd.to_string()));
        }
        if let Some(rest) = s.strip_prefix("file:") {
            let mut parts = rest.split(DELIM_COMMA);
            let filename = PathBuf::from(parts.next().ok_or_else(|| "invalid incoming file endpoint".to_string())?);
            let mut offset = None;
            for part in parts {
                let (key, value) = part.split_once('=').ok_or_else(|| format!("invalid incoming file option: {part}"))?;
                match key {
                    "offset" => offset = Some(value.to_string()),
                    other => return Err(format!("unsupported incoming file option: {other}")),
                }
            }
            return Ok(Self::File(File { filename, offset }));
        }
        if let Some(exec) = s.strip_prefix("exec:") {
            return Ok(Self::Exec(exec.to_string()));
        }
        if s.starts_with('{') || s.contains("addr.") || s.contains(',') {
            return Ok(Self::Channel(s.to_string()));
        }
        Ok(Self::Channel(s.to_string()))
    }
}

fn parse_optional_host_port(value: &str) -> Result<(Option<String>, u16), String> {
    let (host, port) = value.rsplit_once(':').ok_or_else(|| format!("invalid host:port endpoint: {value}"))?;
    let host = if host.is_empty() { None } else { Some(host.to_string()) };
    Ok((host, port.parse::<u16>().map_err(|e| e.to_string())?))
}