1use std::ffi::OsStr;
37use std::fmt;
38use std::path::{Path, PathBuf};
39
40use winnow::stream::BStr;
41
42pub const MAX_DEPTH: usize = 10;
45
46pub const MAX_FILES: usize = 4096;
49
50#[derive(Default, Debug)]
53pub struct ResponseFiles {
54 files: Vec<Vec<u8>>,
55 owned: Vec<Vec<u8>>,
56}
57
58#[derive(Debug)]
60pub struct ResponseError {
61 pub path: PathBuf,
63 pub kind: ResponseErrorKind,
65}
66
67#[derive(Debug)]
69#[non_exhaustive]
70pub enum ResponseErrorKind {
71 Io(std::io::Error),
73 PrematureEnd,
75 TooDeep,
77 TooMany,
79}
80
81impl fmt::Display for ResponseError {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 let path = self.path.display();
84 match &self.kind {
85 ResponseErrorKind::Io(error) => write!(f, "{path}: {error}"),
86 ResponseErrorKind::PrematureEnd => write!(f, "{path}: premature end of input"),
87 ResponseErrorKind::TooDeep => write!(f, "{path}: response file nesting too deep"),
88 ResponseErrorKind::TooMany => write!(f, "{path}: too many response files"),
89 }
90 }
91}
92
93impl std::error::Error for ResponseError {
94 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
95 match &self.kind {
96 ResponseErrorKind::Io(error) => Some(error),
97 _ => None,
98 }
99 }
100}
101
102#[derive(Clone, Copy)]
104enum Word {
105 Arg(usize),
106 File {
107 file: usize,
108 start: usize,
109 end: usize,
110 },
111 Owned(usize),
112}
113
114pub fn expand<'a, S: AsRef<OsStr>>(
119 args: &'a [S],
120 files: &'a mut ResponseFiles,
121) -> Result<Vec<&'a BStr>, ResponseError> {
122 let mut words = Vec::with_capacity(args.len());
123 for (index, arg) in args.iter().enumerate() {
124 match arg.as_ref().as_encoded_bytes().strip_prefix(b"@") {
125 Some(path) => read(files, path_of(path)?, 1, &mut words)?,
126 None => words.push(Word::Arg(index)),
127 }
128 }
129 let files: &'a ResponseFiles = files;
130 Ok(words
131 .into_iter()
132 .map(|word| {
133 BStr::new(match word {
134 Word::Arg(index) => args[index].as_ref().as_encoded_bytes(),
135 Word::File { file, start, end } => &files.files[file][start..end],
136 Word::Owned(index) => &files.owned[index],
137 })
138 })
139 .collect())
140}
141
142fn read(
143 files: &mut ResponseFiles,
144 path: &Path,
145 depth: usize,
146 words: &mut Vec<Word>,
147) -> Result<(), ResponseError> {
148 let error = |kind| ResponseError {
149 path: path.to_owned(),
150 kind,
151 };
152 if depth > MAX_DEPTH {
153 return Err(error(ResponseErrorKind::TooDeep));
154 }
155 if files.files.len() >= MAX_FILES {
156 return Err(error(ResponseErrorKind::TooMany));
157 }
158 let data = std::fs::read(path).map_err(|e| error(ResponseErrorKind::Io(e)))?;
159 let file = files.files.len();
160 let tokens = tokenize(&data).ok_or_else(|| error(ResponseErrorKind::PrematureEnd))?;
161 files.files.push(data);
162
163 for token in tokens {
164 let bytes = match &token {
165 Token::Range(start, end) => &files.files[file][*start..*end],
166 Token::Owned(bytes) => bytes,
167 };
168 if let Some(nested) = bytes.strip_prefix(b"@") {
169 let nested = path_of(nested)?.to_owned();
170 read(files, &nested, depth + 1, words)?;
171 continue;
172 }
173 words.push(match token {
174 Token::Range(start, end) => Word::File { file, start, end },
175 Token::Owned(bytes) => {
176 files.owned.push(bytes);
177 Word::Owned(files.owned.len() - 1)
178 }
179 });
180 }
181 Ok(())
182}
183
184#[cfg(unix)]
185fn path_of(bytes: &[u8]) -> Result<&Path, ResponseError> {
186 use std::os::unix::ffi::OsStrExt;
187 Ok(Path::new(OsStr::from_bytes(bytes)))
188}
189
190#[cfg(not(unix))]
191fn path_of(bytes: &[u8]) -> Result<&Path, ResponseError> {
192 unicode_path(bytes)
193}
194
195#[cfg(any(test, not(unix)))]
197fn unicode_path(bytes: &[u8]) -> Result<&Path, ResponseError> {
198 match std::str::from_utf8(bytes) {
199 Ok(text) => Ok(Path::new(text)),
200 Err(error) => Err(ResponseError {
201 path: PathBuf::from(String::from_utf8_lossy(bytes).into_owned()),
202 kind: ResponseErrorKind::Io(std::io::Error::new(
203 std::io::ErrorKind::InvalidData,
204 error,
205 )),
206 }),
207 }
208}
209
210enum Token {
211 Range(usize, usize),
212 Owned(Vec<u8>),
213}
214
215fn is_space(c: u8) -> bool {
216 matches!(c, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')
217}
218
219fn tokenize(data: &[u8]) -> Option<Vec<Token>> {
223 let mut tokens = Vec::new();
224 let mut i = 0;
225 while i < data.len() {
226 if is_space(data[i]) {
227 i += 1;
228 continue;
229 }
230 let start = i;
231 while i < data.len() && !is_space(data[i]) && !matches!(data[i], b'\\' | b'\'' | b'"') {
232 i += 1;
233 }
234 if i == data.len() || is_space(data[i]) {
235 tokens.push(Token::Range(start, i));
236 continue;
237 }
238
239 let mut word = data[start..i].to_vec();
240 let mut quote = None;
241 while i < data.len() {
242 let c = data[i];
243 if c == b'\\' {
244 word.push(*data.get(i + 1)?);
245 i += 2;
246 } else if let Some(q) = quote {
247 if c == q {
248 quote = None;
249 } else {
250 word.push(c);
251 }
252 i += 1;
253 } else if c == b'\'' || c == b'"' {
254 quote = Some(c);
255 i += 1;
256 } else if is_space(c) {
257 break;
258 } else {
259 word.push(c);
260 i += 1;
261 }
262 }
263 if quote.is_some() {
264 return None;
265 }
266 tokens.push(Token::Owned(word));
267 }
268 Some(tokens)
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 fn words(data: &str) -> Option<Vec<String>> {
276 let tokens = tokenize(data.as_bytes())?;
277 Some(
278 tokens
279 .into_iter()
280 .map(|token| match token {
281 Token::Range(start, end) => data[start..end].to_owned(),
282 Token::Owned(bytes) => String::from_utf8(bytes).unwrap(),
283 })
284 .collect(),
285 )
286 }
287
288 #[test]
289 fn splits_on_whitespace() {
290 assert_eq!(words(" a\tb\n\nc \r\n").unwrap(), ["a", "b", "c"]);
291 assert!(words("").unwrap().is_empty());
292 }
293
294 #[test]
295 fn quotes_and_backslashes() {
296 assert_eq!(
297 words(r#"'a b' "c d" e\ f g'h i'j "it's" 'say "hi"' '' \\"#).unwrap(),
298 ["a b", "c d", "e f", "gh ij", "it's", "say \"hi\"", "", "\\"]
299 );
300 }
301
302 #[test]
303 fn plain_words_borrow() {
304 let tokens = tokenize(b"plain 'quoted'").unwrap();
305 assert!(matches!(tokens[0], Token::Range(0, 5)));
306 assert!(matches!(tokens[1], Token::Owned(_)));
307 }
308
309 #[test]
310 fn premature_end() {
311 assert!(words("'open").is_none());
312 assert!(words("trailing\\").is_none());
313 }
314
315 #[test]
316 fn a_non_utf8_path_is_not_an_empty_name() {
317 let error = unicode_path(b"\xff").unwrap_err();
318 assert!(matches!(error.kind, ResponseErrorKind::Io(_)));
319 assert!(!error.path.as_os_str().is_empty());
320 }
321}