Skip to main content

winnow_args/
response.rs

1//! Response files: `@file` words that stand for the file's contents.
2//!
3//! A linker's command line is often too long for the kernel, so compilers pass
4//! it as `@file`: the file holds the words, separated by whitespace, with
5//! `'…'` and `"…"` quoting and `\` escaping one byte, as GNU's `buildargv`
6//! reads them. A word in a file may itself be `@file`, up to [`MAX_DEPTH`]
7//! files deep.
8//!
9//! [`expand`] reads the files into a [`ResponseFiles`] that owns their bytes,
10//! and returns the whole command line as words that borrow from it and from
11//! the arguments, ready for [`Argv::new`](crate::Argv::new). Only a word that
12//! had quotes or backslashes removed is copied.
13//!
14//! ```
15//! use winnow_args::response::{ResponseFiles, expand};
16//!
17//! let dir = std::env::temp_dir().join("winnow-args-response-doc");
18//! std::fs::create_dir_all(&dir).unwrap();
19//! let file = dir.join("link.rsp");
20//! std::fs::write(&file, "-o 'my out' main.o\n").unwrap();
21//! let at = format!("@{}", file.display());
22//!
23//! let args = ["-shared", at.as_str(), "-lc"];
24//! let mut files = ResponseFiles::default();
25//! let words = expand(&args, &mut files).unwrap();
26//! let words: Vec<&[u8]> = words.iter().map(|w| w.as_ref()).collect();
27//! assert_eq!(words, [&b"-shared"[..], b"-o", b"my out", b"main.o", b"-lc"]);
28//! ```
29//!
30//! A program expands its arguments, then parses the words with
31//! [`Args::parse_words`](crate::Args::parse_words) where it would call `parse()`;
32//! nothing turns `@file` on but that call. `examples/ld.rs` does it.
33//! Off Unix a response path is Unicode: bytes that are not UTF-8 are an
34//! I/O error, not a file named by an empty path.
35
36use std::ffi::OsStr;
37use std::fmt;
38use std::path::{Path, PathBuf};
39
40use winnow::stream::BStr;
41
42/// How many response files deep `@file` may nest: a file named on the command
43/// line is at depth 1. mold's limit.
44pub const MAX_DEPTH: usize = 10;
45
46/// How many response files one command line may read in all: nesting fans
47/// out, and a file that names several others ten deep would never end.
48pub const MAX_FILES: usize = 4096;
49
50/// The storage [`expand`]'s words borrow from: the files' contents and the
51/// words that had to be unquoted.
52#[derive(Default, Debug)]
53pub struct ResponseFiles {
54    files: Vec<Vec<u8>>,
55    owned: Vec<Vec<u8>>,
56}
57
58/// Why a response file could not be read.
59#[derive(Debug)]
60pub struct ResponseError {
61    /// The file, as it was named.
62    pub path: PathBuf,
63    /// What went wrong.
64    pub kind: ResponseErrorKind,
65}
66
67/// What went wrong reading a response file.
68#[derive(Debug)]
69#[non_exhaustive]
70pub enum ResponseErrorKind {
71    /// The file could not be read.
72    Io(std::io::Error),
73    /// A quote was not closed, or the file ends in a lone `\`.
74    PrematureEnd,
75    /// `@file` nested deeper than [`MAX_DEPTH`].
76    TooDeep,
77    /// More than [`MAX_FILES`] response files.
78    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/// A word of the expanded command line, before the storage is borrowed.
103#[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
114/// Replace every `@file` argument with the words of the file, recursively.
115///
116/// Every other argument is kept as it is. Inside a file, a word that starts
117/// with `@` once unquoted names a file too, as in GNU ld and mold.
118pub 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/// A response path where the platform's paths are Unicode.
196#[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
219/// Split a response file into words: ranges of `data` where nothing was
220/// unquoted, copies where something was. `None` on an unclosed quote or a
221/// trailing `\`.
222fn 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}