1use std::ffi::OsString;
2
3use bstr::{BStr, BString};
4use gix_error::{ExnResult, Result, 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::<ExnResult<_, Error>>()?,
96 })
97}
98
99pub(crate) fn arguments(input: &BStr) -> Result<Vec<OsString>> {
100 parse_words(input)
101 .or_error()?
102 .into_iter()
103 .map(|word| into_os_string(word.value).or_error())
104 .collect()
105}
106
107fn parse_words(input: &BStr) -> std::result::Result<Vec<Word>, Error> {
108 let mut words = Vec::new();
109 let mut value = BString::default();
110 let mut assignment_possible = true;
111 let mut assignment_separator = None;
112 let mut word_started = false;
113 let mut quote = None;
114 let mut bytes = input.iter().copied();
115
116 while let Some(byte) = bytes.next() {
117 match quote {
118 Some(Quote::Single) => {
119 if byte == b'\'' {
120 quote = None;
121 } else {
122 value.push(byte);
123 }
124 }
125 Some(Quote::Double) => match byte {
126 b'"' => quote = None,
127 b'\\' => match bytes.next() {
128 Some(b'\n') => {}
129 Some(next @ (b'$' | b'`' | b'"' | b'\\')) => value.push(next),
130 Some(next) => {
131 value.push(b'\\');
132 value.push(next);
133 }
134 None => return Err(Error::MissingClosingQuote),
135 },
136 _ => value.push(byte),
137 },
138 None => match byte {
139 b' ' | b'\t' | b'\n' => {
140 if word_started {
141 words.push(Word {
142 value: std::mem::take(&mut value),
143 assignment_separator,
144 });
145 (assignment_possible, assignment_separator, word_started) = (true, None, false);
146 }
147 }
148 b'#' if !word_started => {
149 bytes.by_ref().find(|byte| *byte == b'\n');
150 }
151 b'\'' => (assignment_possible, quote, word_started) = (false, Some(Quote::Single), true),
152 b'"' => (assignment_possible, quote, word_started) = (false, Some(Quote::Double), true),
153 b'\\' => match bytes.next() {
154 Some(b'\n') => {}
155 Some(next) => {
156 (assignment_possible, word_started) = (false, true);
157 value.push(next);
158 }
159 None => return Err(Error::MissingEscapedByte),
160 },
161 _ => {
162 word_started = true;
163 push_unquoted(&mut value, &mut assignment_possible, &mut assignment_separator, byte);
164 }
165 },
166 }
167 }
168 if quote.is_some() {
169 return Err(Error::MissingClosingQuote);
170 }
171 if word_started {
172 words.push(Word {
173 value,
174 assignment_separator,
175 });
176 }
177 Ok(words)
178}
179
180fn push_unquoted(
183 value: &mut BString,
184 assignment_possible: &mut bool,
185 assignment_separator: &mut Option<usize>,
186 byte: u8,
187) {
188 if assignment_separator.is_none() && *assignment_possible {
189 if value.is_empty() {
190 *assignment_possible = byte == b'_' || byte.is_ascii_alphabetic();
191 } else if byte == b'=' {
192 *assignment_separator = Some(value.len());
193 } else if byte != b'_' && !byte.is_ascii_alphanumeric() {
194 *assignment_possible = false;
195 }
196 }
197 value.push(byte);
198}
199
200fn into_os_string(value: BString) -> ExnResult<OsString, Error> {
201 gix_path::from_bstring(value)
202 .map(std::path::PathBuf::into_os_string)
203 .or_raise_typed(|| Error::UnrepresentableOsString)
204}