use std::ffi::OsString;
use bstr::{BStr, BString};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Outcome {
pub env: Vec<(String, OsString)>,
pub command: OsString,
pub args: Vec<OsString>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Error {
MissingClosingQuote,
MissingEscapedByte,
MissingCommand,
UnrepresentableOsString,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Error::MissingClosingQuote => "missing closing quote",
Error::MissingEscapedByte => "missing byte after escape",
Error::MissingCommand => "missing command",
Error::UnrepresentableOsString => {
"command, argument, or environment value cannot be represented as an OS string"
}
})
}
}
impl std::error::Error for Error {}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Quote {
Single,
Double,
}
struct Word {
value: BString,
assignment_separator: Option<usize>,
}
pub fn command_line(input: &BStr) -> Result<Outcome, Error> {
let mut words = parse_words(input)?;
let assignment_count = words
.iter()
.take_while(|word| word.assignment_separator.is_some())
.count();
let mut args = words.split_off(assignment_count).into_iter().map(|word| word.value);
let command = into_os_string(args.next().ok_or(Error::MissingCommand)?)?;
let env = words
.into_iter()
.map(|word| {
let separator = word.assignment_separator.expect("only recognized assignments remain");
Ok((
String::from_utf8(word.value[..separator].to_owned())
.expect("shell assignment names contain only ASCII bytes"),
into_os_string(word.value[separator + 1..].to_owned().into())?,
))
})
.collect::<Result<_, Error>>()?;
Ok(Outcome {
env,
command,
args: args.map(into_os_string).collect::<Result<_, _>>()?,
})
}
pub(crate) fn arguments(input: &BStr) -> Result<Vec<OsString>, Error> {
parse_words(input)?
.into_iter()
.map(|word| into_os_string(word.value))
.collect()
}
fn parse_words(input: &BStr) -> Result<Vec<Word>, Error> {
let mut words = Vec::new();
let mut value = BString::default();
let mut assignment_possible = true;
let mut assignment_separator = None;
let mut word_started = false;
let mut quote = None;
let mut bytes = input.iter().copied();
while let Some(byte) = bytes.next() {
match quote {
Some(Quote::Single) => {
if byte == b'\'' {
quote = None;
} else {
value.push(byte);
}
}
Some(Quote::Double) => match byte {
b'"' => quote = None,
b'\\' => match bytes.next() {
Some(b'\n') => {}
Some(next @ (b'$' | b'`' | b'"' | b'\\')) => value.push(next),
Some(next) => {
value.push(b'\\');
value.push(next);
}
None => return Err(Error::MissingClosingQuote),
},
_ => value.push(byte),
},
None => match byte {
b' ' | b'\t' | b'\n' => {
if word_started {
words.push(Word {
value: std::mem::take(&mut value),
assignment_separator,
});
(assignment_possible, assignment_separator, word_started) = (true, None, false);
}
}
b'#' if !word_started => {
bytes.by_ref().find(|byte| *byte == b'\n');
}
b'\'' => (assignment_possible, quote, word_started) = (false, Some(Quote::Single), true),
b'"' => (assignment_possible, quote, word_started) = (false, Some(Quote::Double), true),
b'\\' => match bytes.next() {
Some(b'\n') => {}
Some(next) => {
(assignment_possible, word_started) = (false, true);
value.push(next);
}
None => return Err(Error::MissingEscapedByte),
},
_ => {
word_started = true;
push_unquoted(&mut value, &mut assignment_possible, &mut assignment_separator, byte);
}
},
}
}
if quote.is_some() {
return Err(Error::MissingClosingQuote);
}
if word_started {
words.push(Word {
value,
assignment_separator,
});
}
Ok(words)
}
fn push_unquoted(
value: &mut BString,
assignment_possible: &mut bool,
assignment_separator: &mut Option<usize>,
byte: u8,
) {
if assignment_separator.is_none() && *assignment_possible {
if value.is_empty() {
*assignment_possible = byte == b'_' || byte.is_ascii_alphabetic();
} else if byte == b'=' {
*assignment_separator = Some(value.len());
} else if byte != b'_' && !byte.is_ascii_alphanumeric() {
*assignment_possible = false;
}
}
value.push(byte);
}
fn into_os_string(value: BString) -> Result<OsString, Error> {
gix_path::try_from_bstring(value)
.map(std::path::PathBuf::into_os_string)
.map_err(|_| Error::UnrepresentableOsString)
}