Expand response files in mold-common for both linkers

The ELF and Mach-O linkers each had a copy of the same response file
reader. They now share mold-common's expand_response_files. Its macho
flag keeps the Mach-O linker's one difference: an argument starting
with @rpath, @loader_path or @executable_path is a dylib path, not a
response file.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Rui UeyamaandClaude Opus 5.5 committed 2026-10-09 22:10:53 +09:00
1 parent ad4102263d
commit eb21f960fd
6 files changed
+125 -207

No files matched your search

+1
View File
@@ -22,6 +22,7 @@ pub mod path;
pub mod perf;
mod prefetch;
pub mod record;
pub mod response_file;
pub mod siphash;
pub mod subprocess;
pub mod tar;
+121
View File
@@ -0,0 +1,121 @@
//! Response files. If a command line argument is in the form of
//! `@path/to/some/file` (i.e. it starts with an at sign), the linker reads
//! the given file and interprets its contents as a list of command line
//! arguments. A file containing command line arguments is called a
//! "response file".
//!
//! A response file is often used to pass a very large number of arguments
//! to the linker without exceeding the kernel's command line length limit.
use std::borrow::Cow;
use std::ffi::{OsStr, OsString};
use std::path::Path;
use bstr::ByteVec;
use crate::bytes::{display, is_space, os_str};
use crate::fatal;
use crate::mapped_file::MappedFile;
/// Replaces each "@path/to/some/text/file" argument with the arguments
/// the file holds. For the Mach-O linker (`macho`), an argument starting
/// with "@rpath", "@loader_path" or "@executable_path" is a dylib path,
/// not a response file, wherever it is, even an option's argument.
pub fn expand_response_files(argv: Vec<OsString>, macho: bool) -> Vec<Cow<'static, OsStr>> {
let mut args = Vec::new();
for arg in argv {
if let Some(path) = response_file(arg.as_encoded_bytes(), macho) {
args.extend(read_response_file(path, 1, macho));
} else {
args.push(Cow::Owned(arg));
}
}
args
}
/// The response file an argument names, if it names one (see
/// expand_response_files).
fn response_file(arg: &[u8], macho: bool) -> Option<&Path> {
const DYLIB_PATHS: [&[u8]; 3] = [b"@rpath", b"@loader_path", b"@executable_path"];
if macho && DYLIB_PATHS.iter().any(|prefix| arg.starts_with(prefix)) {
return None;
}
arg.strip_prefix(b"@").map(|path| Path::new(os_str(path)))
}
/// Opens a response file, tokenizes its contents, and returns the tokens.
fn read_response_file(path: &Path, depth: usize, macho: bool) -> Vec<Cow<'static, OsStr>> {
if depth > 10 {
fatal!("{}: response file nesting too deep", path.display());
}
let mf = MappedFile::must_open(path);
mf.set_dependency(false);
let data = mf.data();
// Arguments are passed on as C strings, e.g. to the LTO plugin, so they
// must not contain a NUL byte. Arguments given by the OS never do.
if data.contains(&0) {
fatal!("{}: response file contains a NUL byte", path.display());
}
let mut expanded = Vec::new();
let mut i = 0;
while i < data.len() {
if is_space(data[i]) {
i += 1;
continue;
}
// Plain tokens can borrow the mapping, which lives for the complete
// link. Copy only when removing quotes or backslashes.
let start = i;
while i < data.len() && !is_space(data[i]) && !matches!(data[i], b'\\' | b'\'' | b'"') {
i += 1;
}
let mut tok = Cow::Borrowed(&data[start..i]);
let mut quote = None;
while i < data.len() {
let c = data[i];
if c == b'\\' {
if i + 1 == data.len() {
fatal!("{}: premature end of input", path.display());
}
tok.to_mut().push(data[i + 1]);
i += 2;
} else if let Some(q) = quote {
if c == q {
quote = None;
} else {
tok.to_mut().push(c);
}
i += 1;
} else if c == b'\'' || c == b'"' {
quote = Some(c);
i += 1;
} else if is_space(c) {
break;
} else {
tok.to_mut().push(c);
i += 1;
}
}
if quote.is_some() {
fatal!("{}: premature end of input", path.display());
}
if let Some(nested) = response_file(&tok, macho) {
expanded.extend(read_response_file(nested, depth + 1, macho));
} else {
expanded.push(match tok {
Cow::Borrowed(bytes) => Cow::Borrowed(os_str(bytes)),
Cow::Owned(bytes) => Cow::Owned(
bytes
.into_os_string()
.unwrap_or_else(|e| fatal!("invalid OS string: {}", display(e.as_bytes()))),
),
});
}
}
expanded
}
+1 -100
View File
@@ -7,7 +7,7 @@ use std::io::{IsTerminal, Write};
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use bstr::{ByteSlice, ByteVec};
use bstr::ByteSlice;
use mold_common::bits::align_down;
use mold_common::bytes::{display, is_space, os_str};
use mold_common::error::strerror;
@@ -739,105 +739,6 @@ pub struct TargetTraits {
pub page_size: u64,
}
// If a command line argument is in the form of `@path/to/some/file` (i.e.
// it starts with an at sign), the linker reads the given file and
// interprets its contents as a list of command line arguments. A file
// containing command line arguments is called a "response file".
//
// A response file is often used to pass a very large number of arguments
// to the linker without exceeding the kernel's command line length limit.
//
// This function opens a given file, tokenizes its contents, and returns a
// list of tokens.
fn read_response_file(path: &Path, depth: usize) -> Vec<Cow<'static, OsStr>> {
if depth > 10 {
fatal!("{}: response file nesting too deep", path.display());
}
let mf = MappedFile::must_open(path);
mf.set_dependency(false);
let data = mf.data();
// Arguments are passed on as C strings, e.g. to the LTO plugin, so they
// must not contain a NUL byte. Arguments given by the OS never do.
if data.contains(&0) {
fatal!("{}: response file contains a NUL byte", path.display());
}
let mut expanded = Vec::new();
let mut i = 0;
while i < data.len() {
if is_space(data[i]) {
i += 1;
continue;
}
// Plain tokens can borrow the mapping, which lives for the complete
// link. Copy only when removing quotes or backslashes.
let start = i;
while i < data.len() && !is_space(data[i]) && !matches!(data[i], b'\\' | b'\'' | b'"') {
i += 1;
}
let mut tok = Cow::Borrowed(&data[start..i]);
let mut quote = None;
while i < data.len() {
let c = data[i];
if c == b'\\' {
if i + 1 == data.len() {
fatal!("{}: premature end of input", path.display());
}
tok.to_mut().push(data[i + 1]);
i += 2;
} else if let Some(q) = quote {
if c == q {
quote = None;
} else {
tok.to_mut().push(c);
}
i += 1;
} else if c == b'\'' || c == b'"' {
quote = Some(c);
i += 1;
} else if is_space(c) {
break;
} else {
tok.to_mut().push(c);
i += 1;
}
}
if quote.is_some() {
fatal!("{}: premature end of input", path.display());
}
if let Some(nested) = tok.strip_prefix(b"@") {
expanded.extend(read_response_file(Path::new(os_str(nested)), depth + 1));
} else {
expanded.push(match tok {
Cow::Borrowed(bytes) => Cow::Borrowed(os_str(bytes)),
Cow::Owned(bytes) => Cow::Owned(
bytes
.into_os_string()
.unwrap_or_else(|e| fatal!("invalid OS string: {}", display(e.as_bytes()))),
),
});
}
}
expanded
}
// Replace "@path/to/some/text/file" with its file contents.
pub fn expand_response_files(argv: Vec<OsString>) -> Vec<Cow<'static, OsStr>> {
let mut args = Vec::new();
for arg in argv {
if let Some(path) = arg.as_encoded_bytes().strip_prefix(b"@") {
args.extend(read_response_file(Path::new(os_str(path)), 1));
} else {
args.push(Cow::Owned(arg));
}
}
args
}
// This function matches a command line argument against an option
// name and, on success, returns the remainder of the argument. For
// example, matching "--foo=bar" against "foo" yields "=bar", and
+1 -1
View File
@@ -55,7 +55,7 @@ pub fn main(
let orig_cwd = std::env::current_dir().ok();
// Expand response files
let cmdline: Arc<[_]> = cmdline::expand_response_files(argv).into();
let cmdline: Arc<[_]> = mold_common::response_file::expand_response_files(argv, false).into();
// Parse with an enabled target's defaults; if the target turns out to
// be different, start over with the right one.
-105
View File
@@ -19,7 +19,6 @@ use mold_common::{fatal, warn};
use crate::arch::Target;
use crate::context::Context;
use crate::macho::*;
use crate::mapped_file::MappedFile;
use crate::reader::under_root;
/// The Apple ld64 version whose command line this linker implements,
@@ -1334,110 +1333,6 @@ fn sectcreate_name(kind: &str, name: &[u8]) -> Vec<u8> {
cut
}
/// The response file an argument names, if it names one: an argument
/// starting with '@', but a dylib path starting "@rpath", "@loader_path"
/// or "@executable_path", wherever it is, even an option's argument.
fn response_file(arg: &[u8]) -> Option<&Path> {
const DYLIB_PATHS: [&[u8]; 3] = [b"@rpath", b"@loader_path", b"@executable_path"];
if DYLIB_PATHS.iter().any(|prefix| arg.starts_with(prefix)) {
return None;
}
arg.strip_prefix(b"@").map(|path| Path::new(os_str(path)))
}
// If a command line argument is in the form of `@path/to/some/file`
// (i.e. it starts with an atsign), the linker reads the given file and
// interprets its contents as a list of command line arguments. A file
// containing command line arguments is called a "response file".
//
// A response file is often used to pass a very large number of arguments
// to the linker without exceeding the kernel's command line length limit.
//
// This function opens a given file, tokenizes its contents, and returns a
// list of tokens.
fn read_response_file(path: &Path, depth: usize) -> Vec<Cow<'static, OsStr>> {
if depth > 10 {
fatal!("{}: response file nesting too deep", path.display());
}
let data = MappedFile::must_open(path).data();
// Arguments are passed on as C strings, e.g. to the LTO plugin, so they
// must not contain a NUL byte. Arguments given by the OS never do.
if data.contains(&0) {
fatal!("{}: response file contains a NUL byte", path.display());
}
let mut expanded = Vec::new();
let mut i = 0;
while i < data.len() {
if is_space(data[i]) {
i += 1;
continue;
}
// Plain tokens can borrow the mapping, which lives for the complete
// link. Copy only when removing quotes or backslashes.
let start = i;
while i < data.len() && !is_space(data[i]) && !matches!(data[i], b'\\' | b'\'' | b'"') {
i += 1;
}
let mut tok = Cow::Borrowed(&data[start..i]);
let mut quote = None;
while i < data.len() {
let c = data[i];
if c == b'\\' {
if i + 1 == data.len() {
fatal!("{}: premature end of input", path.display());
}
tok.to_mut().push(data[i + 1]);
i += 2;
} else if let Some(q) = quote {
if c == q {
quote = None;
} else {
tok.to_mut().push(c);
}
i += 1;
} else if c == b'\'' || c == b'"' {
quote = Some(c);
i += 1;
} else if is_space(c) {
break;
} else {
tok.to_mut().push(c);
i += 1;
}
}
if quote.is_some() {
fatal!("{}: premature end of input", path.display());
}
if let Some(nested) = response_file(&tok) {
expanded.extend(read_response_file(nested, depth + 1));
} else {
expanded.push(match tok {
Cow::Borrowed(bytes) => Cow::Borrowed(os_str(bytes)),
Cow::Owned(bytes) => Cow::Owned(os_str(&bytes).to_owned()),
});
}
}
expanded
}
// Replace "@path/to/some/text/file" with its file contents.
pub fn expand_response_files(argv: Vec<OsString>) -> Vec<Cow<'static, OsStr>> {
let mut args = Vec::new();
for arg in argv {
if let Some(path) = response_file(arg.as_encoded_bytes()) {
args.extend(read_response_file(path, 1));
} else {
args.push(Cow::Owned(arg));
}
}
args
}
/// Reads a -filelist file: one input path per line, in whatever bytes
/// the file system uses, optionally under a directory given after a
/// comma in the option's argument. Returns the file's path and the
+1 -1
View File
@@ -37,7 +37,7 @@ pub fn main(
initial_target: &str,
link_for_target: impl Fn(&str, Cmdline) -> LinkResult,
) -> i32 {
let cmdline: Cmdline = cmdline::expand_response_files(argv).into();
let cmdline: Cmdline = mold_common::response_file::expand_response_files(argv, true).into();
// Parse with an enabled target's defaults; if the target turns out to
// be different, start over with the right one.