Skip to main content

qemu_command_builder/
shell_string.rs

1use annotate_snippets::renderer::DecorStyle;
2use annotate_snippets::{AnnotationKind, Level, Renderer, Snippet};
3use proptest_derive::Arbitrary;
4use std::fmt::{Display, Formatter};
5use std::ops::Deref;
6use std::str::FromStr;
7use winnow::error::{ContextError, ParseError};
8use winnow::prelude::*;
9
10fn shell_quote(s: &str) -> String {
11    if !s.is_empty() && s.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.' | '/' | ':' | ',' | '=' | '+')) {
12        return s.to_string();
13    }
14
15    let escaped = s.replace('\'', r#"'\''"#);
16    format!("'{}'", escaped)
17}
18
19fn parse_shell_text(s: &str) -> Result<String, String> {
20    if s.contains(['\'', '"', '\\']) {
21        let parsed = shellish_parse::parse(s, shellish_parse::ParseOptions::new()).map_err(|e| e.to_string())?;
22        return Ok(parsed.join(" "));
23    }
24
25    Ok(s.to_string())
26}
27
28#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Arbitrary)]
29pub struct ShellString {
30    #[proptest(regex = r#"[^,\n]{0,100}"#)]
31    pub s: String,
32}
33
34impl ShellString {
35    pub fn new(s: impl Into<String>) -> Self {
36        Self { s: s.into() }
37    }
38
39    pub fn shell_quoted(&self) -> String {
40        shell_quote(&self.s)
41    }
42}
43
44impl Display for ShellString {
45    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
46        write!(f, "{}", self.shell_quoted())
47    }
48}
49
50impl FromStr for ShellString {
51    type Err = String;
52    fn from_str(s: &str) -> Result<Self, Self::Err> {
53        Ok(ShellString { s: parse_shell_text(s)? })
54    }
55}
56
57impl From<ShellString> for String {
58    fn from(val: ShellString) -> Self {
59        val.s.to_string()
60    }
61}
62
63impl TryFrom<String> for ShellString {
64    type Error = String;
65
66    fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
67        Ok(ShellString { s: value })
68    }
69}
70
71impl<'a> From<&'a str> for ShellString {
72    fn from(s: &'a str) -> Self {
73        ShellString { s: s.to_string() }
74    }
75}
76impl Deref for ShellString {
77    type Target = str;
78
79    fn deref(&self) -> &Self::Target {
80        &self.s
81    }
82}
83
84impl AsRef<str> for ShellString {
85    fn as_ref(&self) -> &str {
86        &self.s
87    }
88}
89
90#[derive(Debug)]
91pub struct ShellStringError {
92    message: String,
93    span: std::ops::Range<usize>,
94    input: String,
95}
96
97impl ShellStringError {
98    pub(crate) fn new(message: impl Into<String>) -> Self {
99        Self {
100            message: message.into(),
101            span: 0..0,
102            input: String::new(),
103        }
104    }
105
106    pub(crate) fn from_parse(error: ParseError<&str, ContextError>) -> Self {
107        let message = error.inner().to_string();
108        let input = (*error.input()).to_owned();
109        let span = error.char_span();
110        Self { message, span, input }
111    }
112}
113impl std::fmt::Display for ShellStringError {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        let report = &[Level::ERROR
116            .primary_title(self.message.as_str())
117            .element(Snippet::source(&self.input).annotation(AnnotationKind::Primary.span(self.span.clone()).label("parse failed here")))];
118
119        let renderer = Renderer::styled().decor_style(DecorStyle::Unicode);
120        let rendered = renderer.render(report);
121        rendered.fmt(f)
122    }
123}
124
125impl std::error::Error for ShellStringError {}
126
127pub(crate) fn shell_string_until_comma<'a>(input: &mut &'a str) -> ModalResult<&'a str> {
128    shell_string_until(input, &[','])
129}
130
131fn shell_string_until<'a>(input: &mut &'a str, delimiters: &[char]) -> ModalResult<&'a str> {
132    let mut single_quoted = false;
133    let mut double_quoted = false;
134    let mut escaped = false;
135
136    for (idx, ch) in input.char_indices() {
137        if escaped {
138            escaped = false;
139            continue;
140        }
141
142        match ch {
143            '\\' if !single_quoted => {
144                escaped = true;
145            }
146            '\'' if !double_quoted => {
147                single_quoted = !single_quoted;
148            }
149            '"' if !single_quoted => {
150                double_quoted = !double_quoted;
151            }
152            _ if !single_quoted && !double_quoted && delimiters.contains(&ch) => {
153                if idx == 0 {
154                    return Err(winnow::error::ErrMode::Backtrack(ContextError::new()));
155                }
156                let (head, tail) = input.split_at(idx);
157                *input = tail;
158                return Ok(head);
159            }
160            _ => {}
161        }
162    }
163
164    if escaped || single_quoted || double_quoted {
165        return Err(winnow::error::ErrMode::Cut(ContextError::new()));
166    }
167
168    if input.is_empty() {
169        return Err(winnow::error::ErrMode::Backtrack(ContextError::new()));
170    }
171
172    let head = *input;
173    *input = "";
174    Ok(head)
175}