use std::{
ffi::OsStr,
fs::{self, File},
io::{Read, Write},
path::{Path, PathBuf},
process::ExitStatus,
};
use tempfile::NamedTempFile;
use crate::{constants::RESPONSE_FILE_ARGUMENT_THRESHOLD, error::Error};
use super::execute_command_for_status;
const MAX_RESPONSE_DEPTH: usize = 128;
const MAX_RESPONSE_BYTES: u64 = 64 * 1024 * 1024;
const MAX_RESPONSE_ARGUMENTS: usize = 1_000_000;
pub(crate) fn expand_response_files(args: &[String]) -> Result<Vec<String>, Error> {
let mut expansion = ResponseExpansion {
active: Vec::new(),
bytes_left: MAX_RESPONSE_BYTES,
arguments_left: MAX_RESPONSE_ARGUMENTS,
args: Vec::new(),
};
for arg in args {
expansion.append(arg.clone())?;
}
Ok(expansion.args)
}
struct ResponseExpansion {
active: Vec<PathBuf>,
bytes_left: u64,
arguments_left: usize,
args: Vec<String>,
}
impl ResponseExpansion {
fn append(&mut self, arg: String) -> Result<(), Error> {
self.arguments_left = self.arguments_left.checked_sub(1).ok_or_else(|| {
Error::InvalidArguments("response file expansion exceeds argument limit".into())
})?;
let Some(filename) = arg.strip_prefix('@') else {
self.args.push(arg);
return Ok(());
};
let invalid = |reason: String| {
Error::InvalidArguments(format!("response file {filename:?}: {reason}"))
};
if self.active.len() >= MAX_RESPONSE_DEPTH {
return Err(invalid("expansion exceeds depth limit".into()));
}
let path = match Path::new(filename).canonicalize() {
Ok(path) => path,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
self.args.push(arg);
return Ok(());
}
Err(e) => return Err(invalid(e.to_string())),
};
if self.active.contains(&path) {
return Err(invalid("expansion cycle".into()));
}
let metadata = fs::metadata(&path).map_err(|e| invalid(e.to_string()))?;
if !metadata.is_file() {
return Err(invalid("expected a regular file".into()));
}
if metadata.len() > self.bytes_left {
return Err(invalid("expansion exceeds size limit".into()));
}
let mut contents = String::new();
File::open(&path)
.map_err(|e| invalid(e.to_string()))?
.take(self.bytes_left + 1)
.read_to_string(&mut contents)
.map_err(|e| invalid(format!("cannot read UTF-8 text: {e}")))?;
if contents.len() as u64 > self.bytes_left {
return Err(invalid("expansion exceeds size limit".into()));
}
self.bytes_left -= contents.len() as u64;
if contents.contains('\0') {
return Err(invalid("NUL bytes are not supported".into()));
}
self.active.push(path);
let contents = contents.strip_prefix('\u{feff}').unwrap_or(&contents);
let mut chars = contents.chars();
let mut quote = None;
let mut token = String::new();
while let Some(ch) = chars.next() {
match ch {
'\\' => token.push(chars.next().unwrap_or('\\')),
'\'' | '"' if quote.is_none() => quote = Some(ch),
ch if quote == Some(ch) => quote = None,
' ' | '\t' | '\r' | '\n' if quote.is_none() => {
if !token.is_empty() {
self.append(std::mem::take(&mut token))?;
}
}
_ => token.push(ch),
}
}
if !token.is_empty() {
self.append(token)?;
}
self.active.pop();
Ok(())
}
}
const CHARACTERS_NEEDING_ESCAPE: &[u8] = b"\\\"' \t\n\r";
fn escape_response_file_argument(argument: impl AsRef<OsStr>) -> Vec<u8> {
let argument = argument.as_ref().as_encoded_bytes();
let mut escaped = Vec::with_capacity(argument.len());
for &byte in argument {
if CHARACTERS_NEEDING_ESCAPE.contains(&byte) {
escaped.push(b'\\');
}
escaped.push(byte);
}
escaped
}
fn needs_response_file(argument_count: usize) -> bool {
argument_count > RESPONSE_FILE_ARGUMENT_THRESHOLD
}
pub(crate) fn execute_llvm_tool<P, S>(program_filepath: P, args: &[S]) -> Result<ExitStatus, Error>
where
P: AsRef<Path>,
S: AsRef<OsStr>,
{
let argument_bytes = args.iter().fold(0usize, |total, arg| {
total.saturating_add(arg.as_ref().as_encoded_bytes().len() + 1)
});
if !needs_response_file(args.len()) && argument_bytes <= 32 * 1024 {
return execute_command_for_status(program_filepath, args);
}
let mut response_files = Vec::new();
let mut invocation_args = Vec::new();
for (index, run) in args.split(|arg| arg.as_ref().is_empty()).enumerate() {
if index > 0 {
invocation_args.push(OsStr::new("").to_os_string());
}
if run.is_empty() {
continue;
}
let mut response_file = NamedTempFile::new().map_err(Error::Io)?;
for arg in run {
response_file.write_all(&escape_response_file_argument(arg.as_ref()))?;
response_file.write_all(b"\n")?;
}
response_file.flush().map_err(Error::Io)?;
tracing::debug!(
"Passing {} arguments to {:?} through the response file {:?}",
run.len(),
program_filepath.as_ref(),
response_file.path()
);
let mut argument = OsStr::new("@").to_os_string();
argument.push(response_file.path());
invocation_args.push(argument);
response_files.push(response_file);
}
execute_command_for_status(program_filepath, &invocation_args)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn escaping_leaves_an_ordinary_path_alone() {
assert_eq!(
escape_response_file_argument("/tmp/build/foo.bc"),
b"/tmp/build/foo.bc"
);
}
#[test]
fn escaping_covers_separators_and_quoting() {
assert_eq!(
escape_response_file_argument("/tmp/a dir/x.bc"),
br"/tmp/a\ dir/x.bc"
);
assert_eq!(
escape_response_file_argument("/tmp/it's/x.bc"),
br"/tmp/it\'s/x.bc"
);
assert_eq!(
escape_response_file_argument("/tmp/say\"hi\"/x.bc"),
br#"/tmp/say\"hi\"/x.bc"#
);
assert_eq!(
escape_response_file_argument(r"C:\tmp\x.bc"),
br"C:\\tmp\\x.bc"
);
assert_eq!(
escape_response_file_argument("/tmp/tab\there/x.bc"),
b"/tmp/tab\\\there/x.bc"
);
}
#[test]
fn the_threshold_is_the_last_count_passed_directly() {
assert!(!needs_response_file(RESPONSE_FILE_ARGUMENT_THRESHOLD - 1));
assert!(!needs_response_file(RESPONSE_FILE_ARGUMENT_THRESHOLD));
assert!(needs_response_file(RESPONSE_FILE_ARGUMENT_THRESHOLD + 1));
}
}