use std::io;
use std::path::{Path, PathBuf};
use crate::error::{Error, Result};
const MAX_DEPTH: usize = 64;
pub trait FileReader {
fn read_file(&self, path: &Path) -> io::Result<Vec<u8>>;
}
impl<F> FileReader for F
where
F: Fn(&Path) -> io::Result<Vec<u8>>,
{
fn read_file(&self, path: &Path) -> io::Result<Vec<u8>> {
self(path)
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct FsReader;
impl FileReader for FsReader {
fn read_file(&self, path: &Path) -> io::Result<Vec<u8>> {
std::fs::read(path)
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct NoFiles;
impl FileReader for NoFiles {
fn read_file(&self, _path: &Path) -> io::Result<Vec<u8>> {
Err(io::Error::new(
io::ErrorKind::NotFound,
"response files are disabled",
))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Quoting {
Gnu,
Windows,
}
impl Quoting {
#[must_use]
pub fn host_default() -> Self {
if cfg!(windows) {
Self::Windows
} else {
Self::Gnu
}
}
}
#[must_use]
pub fn tokenize(contents: &[u8], quoting: Quoting) -> Vec<Vec<u8>> {
match quoting {
Quoting::Gnu => tokenize_gnu(contents),
Quoting::Windows => tokenize_windows(contents),
}
}
fn is_space(byte: u8) -> bool {
matches!(byte, b' ' | b'\t' | b'\n' | b'\r' | 0x0b | 0x0c)
}
fn tokenize_gnu(contents: &[u8]) -> Vec<Vec<u8>> {
let mut args = Vec::new();
let mut bytes = contents.iter().copied().peekable();
loop {
while bytes.next_if(|&b| is_space(b)).is_some() {}
if bytes.peek().is_none() {
break;
}
let mut arg = Vec::new();
let (mut squote, mut dquote, mut escape) = (false, false, false);
while let Some(&byte) = bytes.peek() {
if is_space(byte) && !squote && !dquote && !escape {
break;
}
bytes.next();
if escape {
escape = false;
arg.push(byte);
} else if byte == b'\\' {
escape = true;
} else if squote {
if byte == b'\'' {
squote = false;
} else {
arg.push(byte);
}
} else if dquote {
if byte == b'"' {
dquote = false;
} else {
arg.push(byte);
}
} else if byte == b'\'' {
squote = true;
} else if byte == b'"' {
dquote = true;
} else {
arg.push(byte);
}
}
args.push(arg);
}
args
}
fn tokenize_windows(contents: &[u8]) -> Vec<Vec<u8>> {
let mut args = Vec::new();
let mut i = 0;
let len = contents.len();
loop {
while i < len && contents.get(i).is_some_and(|&b| is_space(b)) {
i += 1;
}
if i >= len {
break;
}
let mut arg = Vec::new();
let mut quoted = false;
while let Some(&byte) = contents.get(i) {
if is_space(byte) && !quoted {
break;
}
if byte == b'\\' {
let start = i;
while contents.get(i) == Some(&b'\\') {
i += 1;
}
let count = i - start;
if contents.get(i) == Some(&b'"') {
arg.extend(std::iter::repeat_n(b'\\', count / 2));
if count % 2 == 1 {
arg.push(b'"');
i += 1;
}
} else {
arg.extend(std::iter::repeat_n(b'\\', count));
}
continue;
}
if byte == b'"' {
if quoted && contents.get(i + 1) == Some(&b'"') {
arg.push(b'"');
i += 2;
continue;
}
quoted = !quoted;
i += 1;
continue;
}
arg.push(byte);
i += 1;
}
args.push(arg);
}
args
}
pub fn quoting_from_args(args: &[Vec<u8>]) -> Result<Quoting> {
let mut quoting = Quoting::host_default();
let mut iter = args.iter();
while let Some(arg) = iter.next() {
let value = if arg == b"--rsp-quoting" || arg == b"-rsp-quoting" {
match iter.next() {
Some(value) => value.as_slice(),
None => break,
}
} else if let Some(value) = arg
.strip_prefix(b"--rsp-quoting=")
.or_else(|| arg.strip_prefix(b"-rsp-quoting="))
{
value
} else if arg == b"--" {
break;
} else {
continue;
};
quoting = match value {
b"posix" => Quoting::Gnu,
b"windows" => Quoting::Windows,
other => {
return Err(Error::Option(format!(
"invalid response file quoting: {}",
String::from_utf8_lossy(other)
)));
}
};
}
Ok(quoting)
}
pub fn expand(
args: Vec<Vec<u8>>,
reader: &dyn FileReader,
quoting: Quoting,
) -> Result<Vec<Vec<u8>>> {
let mut out = Vec::with_capacity(args.len());
expand_into(args, reader, quoting, 0, &mut out)?;
Ok(out)
}
fn expand_into(
args: Vec<Vec<u8>>,
reader: &dyn FileReader,
quoting: Quoting,
depth: usize,
out: &mut Vec<Vec<u8>>,
) -> Result<()> {
for arg in args {
let Some(name) = arg.strip_prefix(b"@") else {
out.push(arg);
continue;
};
if name.is_empty() {
out.push(arg);
continue;
}
if depth >= MAX_DEPTH {
return Err(Error::Option(format!(
"response files nested too deeply at {}",
String::from_utf8_lossy(&arg)
)));
}
let path = bytes_to_path(name);
let contents = reader
.read_file(&path)
.map_err(|source| Error::io(path.clone(), source))?;
let nested = tokenize(&contents, quoting);
expand_into(nested, reader, quoting, depth + 1, out)?;
}
Ok(())
}
pub(crate) fn bytes_to_path(bytes: &[u8]) -> PathBuf {
PathBuf::from(super::parse::bytes_to_os(bytes.to_vec()))
}
#[cfg(test)]
mod tests {
use super::*;
fn gnu(text: &str) -> Vec<String> {
tokenize(text.as_bytes(), Quoting::Gnu)
.into_iter()
.map(|a| String::from_utf8(a).unwrap())
.collect()
}
fn win(text: &str) -> Vec<String> {
tokenize(text.as_bytes(), Quoting::Windows)
.into_iter()
.map(|a| String::from_utf8(a).unwrap())
.collect()
}
#[test]
fn gnu_quoting_matches_buildargv() {
assert_eq!(gnu(" a\tb\n\nc "), ["a", "b", "c"]);
assert_eq!(gnu("'a b' \"c d\""), ["a b", "c d"]);
assert_eq!(gnu(r"a\ b"), ["a b"]);
assert_eq!(gnu(r"'it\'s'"), ["it's"]);
assert_eq!(gnu(r#""say \"hi\"""#), [r#"say "hi""#]);
assert_eq!(gnu("x'y z'w"), ["xy zw"]);
assert_eq!(gnu("'' \"\""), ["", ""]);
assert_eq!(gnu("trailing\\"), ["trailing"]);
assert_eq!(gnu("'unterminated quote"), ["unterminated quote"]);
assert!(gnu(" \n").is_empty());
}
#[test]
fn windows_quoting_matches_msvc() {
assert_eq!(win(r"C:\dir\file.o b"), [r"C:\dir\file.o", "b"]);
assert_eq!(win(r#""a b" c"#), ["a b", "c"]);
assert_eq!(win(r#"a\"b"#), [r#"a"b"#]);
assert_eq!(win(r#"a\\"b c""#), [r"a\b c"]);
assert_eq!(win(r#""a""b""#), [r#"a"b"#]);
}
#[test]
fn expansion_is_recursive_and_bounded() {
let reader = |path: &Path| -> io::Result<Vec<u8>> {
match path.to_str() {
Some("outer") => Ok(b"-a @inner -d".to_vec()),
Some("inner") => Ok(b"-b 'c c'".to_vec()),
Some("loop") => Ok(b"@loop".to_vec()),
_ => Err(io::Error::from(io::ErrorKind::NotFound)),
}
};
let args = vec![b"@outer".to_vec(), b"x".to_vec()];
let out = expand(args, &reader, Quoting::Gnu).unwrap();
assert_eq!(
out,
[&b"-a"[..], b"-b", b"c c", b"-d", b"x"]
.iter()
.map(|a| a.to_vec())
.collect::<Vec<_>>()
);
assert!(expand(vec![b"@loop".to_vec()], &reader, Quoting::Gnu).is_err());
assert!(matches!(
expand(vec![b"@missing".to_vec()], &reader, Quoting::Gnu),
Err(Error::Io { .. })
));
assert_eq!(
expand(vec![b"@".to_vec()], &reader, Quoting::Gnu).unwrap(),
vec![b"@".to_vec()]
);
}
#[test]
fn rsp_quoting_option_is_found_before_expansion() {
let args = |list: &[&str]| -> Vec<Vec<u8>> {
list.iter().map(|a| a.as_bytes().to_vec()).collect()
};
assert_eq!(
quoting_from_args(&args(&["--rsp-quoting=windows"])).unwrap(),
Quoting::Windows
);
assert_eq!(
quoting_from_args(&args(&["--rsp-quoting", "posix"])).unwrap(),
Quoting::Gnu
);
assert!(quoting_from_args(&args(&["--rsp-quoting=dos"])).is_err());
}
}