1use std::ffi::OsString;
2
3use bstr::{BStr, BString};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct Outcome {
8 pub env: Vec<(String, OsString)>,
10 pub command: OsString,
12 pub args: Vec<OsString>,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum Error {
19 MissingClosingQuote,
21 MissingEscapedByte,
23 MissingCommand,
25 UnrepresentableOsString,
27}
28
29impl std::fmt::Display for Error {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 f.write_str(match self {
32 Error::MissingClosingQuote => "missing closing quote",
33 Error::MissingEscapedByte => "missing byte after escape",
34 Error::MissingCommand => "missing command",
35 Error::UnrepresentableOsString => {
36 "command, argument, or environment value cannot be represented as an OS string"
37 }
38 })
39 }
40}
41
42impl std::error::Error for Error {}
43
44#[derive(Clone, Copy, PartialEq, Eq)]
45enum Quote {
46 Single,
47 Double,
48}
49
50struct Word {
52 value: BString,
53 assignment_separator: Option<usize>,
55}
56
57pub fn command_line(input: &BStr) -> Result<Outcome, Error> {
65 let mut words = parse_words(input)?;
66 let assignment_count = words
67 .iter()
68 .take_while(|word| word.assignment_separator.is_some())
69 .count();
70 let mut args = words.split_off(assignment_count).into_iter().map(|word| word.value);
71 let command = into_os_string(args.next().ok_or(Error::MissingCommand)?)?;
72 let env = words
73 .into_iter()
74 .map(|word| {
75 let separator = word.assignment_separator.expect("only recognized assignments remain");
76 Ok((
77 String::from_utf8(word.value[..separator].to_owned())
78 .expect("shell assignment names contain only ASCII bytes"),
79 into_os_string(word.value[separator + 1..].to_owned().into())?,
80 ))
81 })
82 .collect::<Result<_, Error>>()?;
83 Ok(Outcome {
84 env,
85 command,
86 args: args.map(into_os_string).collect::<Result<_, _>>()?,
87 })
88}
89
90pub(crate) fn arguments(input: &BStr) -> Result<Vec<OsString>, Error> {
91 parse_words(input)?
92 .into_iter()
93 .map(|word| into_os_string(word.value))
94 .collect()
95}
96
97fn parse_words(input: &BStr) -> Result<Vec<Word>, Error> {
98 let mut words = Vec::new();
99 let mut value = BString::default();
100 let mut assignment_possible = true;
101 let mut assignment_separator = None;
102 let mut word_started = false;
103 let mut quote = None;
104 let mut bytes = input.iter().copied();
105
106 while let Some(byte) = bytes.next() {
107 match quote {
108 Some(Quote::Single) => {
109 if byte == b'\'' {
110 quote = None;
111 } else {
112 value.push(byte);
113 }
114 }
115 Some(Quote::Double) => match byte {
116 b'"' => quote = None,
117 b'\\' => match bytes.next() {
118 Some(b'\n') => {}
119 Some(next @ (b'$' | b'`' | b'"' | b'\\')) => value.push(next),
120 Some(next) => {
121 value.push(b'\\');
122 value.push(next);
123 }
124 None => return Err(Error::MissingClosingQuote),
125 },
126 _ => value.push(byte),
127 },
128 None => match byte {
129 b' ' | b'\t' | b'\n' => {
130 if word_started {
131 words.push(Word {
132 value: std::mem::take(&mut value),
133 assignment_separator,
134 });
135 (assignment_possible, assignment_separator, word_started) = (true, None, false);
136 }
137 }
138 b'#' if !word_started => {
139 bytes.by_ref().find(|byte| *byte == b'\n');
140 }
141 b'\'' => (assignment_possible, quote, word_started) = (false, Some(Quote::Single), true),
142 b'"' => (assignment_possible, quote, word_started) = (false, Some(Quote::Double), true),
143 b'\\' => match bytes.next() {
144 Some(b'\n') => {}
145 Some(next) => {
146 (assignment_possible, word_started) = (false, true);
147 value.push(next);
148 }
149 None => return Err(Error::MissingEscapedByte),
150 },
151 _ => {
152 word_started = true;
153 push_unquoted(&mut value, &mut assignment_possible, &mut assignment_separator, byte);
154 }
155 },
156 }
157 }
158 if quote.is_some() {
159 return Err(Error::MissingClosingQuote);
160 }
161 if word_started {
162 words.push(Word {
163 value,
164 assignment_separator,
165 });
166 }
167 Ok(words)
168}
169
170fn push_unquoted(
173 value: &mut BString,
174 assignment_possible: &mut bool,
175 assignment_separator: &mut Option<usize>,
176 byte: u8,
177) {
178 if assignment_separator.is_none() && *assignment_possible {
179 if value.is_empty() {
180 *assignment_possible = byte == b'_' || byte.is_ascii_alphabetic();
181 } else if byte == b'=' {
182 *assignment_separator = Some(value.len());
183 } else if byte != b'_' && !byte.is_ascii_alphanumeric() {
184 *assignment_possible = false;
185 }
186 }
187 value.push(byte);
188}
189
190fn into_os_string(value: BString) -> Result<OsString, Error> {
191 gix_path::try_from_bstring(value)
192 .map(std::path::PathBuf::into_os_string)
193 .map_err(|_| Error::UnrepresentableOsString)
194}