qemu-command-builder 11.0.0-1

Type safe command line builder for qemu
Documentation
use annotate_snippets::renderer::DecorStyle;
use annotate_snippets::{AnnotationKind, Level, Renderer, Snippet};
use proptest_derive::Arbitrary;
use std::fmt::{Display, Formatter};
use std::ops::Deref;
use std::str::FromStr;
use winnow::error::{ContextError, ParseError};
use winnow::prelude::*;

fn shell_quote(s: &str) -> String {
    if !s.is_empty() && s.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.' | '/' | ':' | ',' | '=' | '+')) {
        return s.to_string();
    }

    let escaped = s.replace('\'', r#"'\''"#);
    format!("'{}'", escaped)
}

fn parse_shell_text(s: &str) -> Result<String, String> {
    if s.contains(['\'', '"', '\\']) {
        let parsed = shellish_parse::parse(s, shellish_parse::ParseOptions::new()).map_err(|e| e.to_string())?;
        return Ok(parsed.join(" "));
    }

    Ok(s.to_string())
}

#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Arbitrary)]
pub struct ShellString {
    #[proptest(regex = r#"[^,\n]{0,100}"#)]
    pub s: String,
}

impl ShellString {
    pub fn new(s: impl Into<String>) -> Self {
        Self { s: s.into() }
    }

    pub fn shell_quoted(&self) -> String {
        shell_quote(&self.s)
    }
}

impl Display for ShellString {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.shell_quoted())
    }
}

impl FromStr for ShellString {
    type Err = String;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(ShellString { s: parse_shell_text(s)? })
    }
}

impl From<ShellString> for String {
    fn from(val: ShellString) -> Self {
        val.s.to_string()
    }
}

impl TryFrom<String> for ShellString {
    type Error = String;

    fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
        Ok(ShellString { s: value })
    }
}

impl<'a> From<&'a str> for ShellString {
    fn from(s: &'a str) -> Self {
        ShellString { s: s.to_string() }
    }
}
impl Deref for ShellString {
    type Target = str;

    fn deref(&self) -> &Self::Target {
        &self.s
    }
}

impl AsRef<str> for ShellString {
    fn as_ref(&self) -> &str {
        &self.s
    }
}

#[derive(Debug)]
pub struct ShellStringError {
    message: String,
    span: std::ops::Range<usize>,
    input: String,
}

impl ShellStringError {
    pub(crate) fn new(message: impl Into<String>) -> Self {
        Self {
            message: message.into(),
            span: 0..0,
            input: String::new(),
        }
    }

    pub(crate) fn from_parse(error: ParseError<&str, ContextError>) -> Self {
        let message = error.inner().to_string();
        let input = (*error.input()).to_owned();
        let span = error.char_span();
        Self { message, span, input }
    }
}
impl std::fmt::Display for ShellStringError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let report = &[Level::ERROR
            .primary_title(self.message.as_str())
            .element(Snippet::source(&self.input).annotation(AnnotationKind::Primary.span(self.span.clone()).label("parse failed here")))];

        let renderer = Renderer::styled().decor_style(DecorStyle::Unicode);
        let rendered = renderer.render(report);
        rendered.fmt(f)
    }
}

impl std::error::Error for ShellStringError {}

pub(crate) fn shell_string_until_comma<'a>(input: &mut &'a str) -> ModalResult<&'a str> {
    shell_string_until(input, &[','])
}

fn shell_string_until<'a>(input: &mut &'a str, delimiters: &[char]) -> ModalResult<&'a str> {
    let mut single_quoted = false;
    let mut double_quoted = false;
    let mut escaped = false;

    for (idx, ch) in input.char_indices() {
        if escaped {
            escaped = false;
            continue;
        }

        match ch {
            '\\' if !single_quoted => {
                escaped = true;
            }
            '\'' if !double_quoted => {
                single_quoted = !single_quoted;
            }
            '"' if !single_quoted => {
                double_quoted = !double_quoted;
            }
            _ if !single_quoted && !double_quoted && delimiters.contains(&ch) => {
                if idx == 0 {
                    return Err(winnow::error::ErrMode::Backtrack(ContextError::new()));
                }
                let (head, tail) = input.split_at(idx);
                *input = tail;
                return Ok(head);
            }
            _ => {}
        }
    }

    if escaped || single_quoted || double_quoted {
        return Err(winnow::error::ErrMode::Cut(ContextError::new()));
    }

    if input.is_empty() {
        return Err(winnow::error::ErrMode::Backtrack(ContextError::new()));
    }

    let head = *input;
    *input = "";
    Ok(head)
}