qemu_command_builder/
shell_string.rs1use 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}