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