Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 41 additions & 7 deletions brush-core/src/interp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1968,22 +1968,56 @@ fn setup_process_substitution(
Ok((candidate_fd_num, target_file))
}

/// Largest payload we will write directly into a freshly created pipe without
/// a helper thread.
///
/// Nothing is reading from the other end of the pipe yet, so an inline
/// `write_all` can only complete if the whole payload fits in the pipe's
/// buffer. 4 KiB is the smallest default buffer across the platforms we
/// target (it is the Windows default; Linux and macOS are larger), so staying
/// at or below it is safe everywhere.
const MAX_INLINE_PIPE_WRITE: usize = 4096;

/// Creates a pipe whose read end is preloaded with `contents`, for use in
/// backing here-documents and here-strings.
fn setup_open_file_with_contents(contents: &str) -> Result<OpenFile, error::Error> {
let (reader, mut writer) = std::io::pipe()?;

let bytes = contents.as_bytes();

// On Linux we can try to grow the pipe's buffer to fit the entire payload, which
// lets us write it inline and hand back a pipe that's already fully populated.
// This is best-effort: the request fails for payloads beyond
// /proc/sys/fs/pipe-max-size (1 MiB by default for unprivileged processes) and for
// a zero-length payload. Any failure just means we fall back to the helper thread
// below, so we deliberately ignore the result instead of failing the redirection.
#[cfg(any(target_os = "linux", target_os = "android"))]
{
let resized_to_fit = {
use std::os::fd::AsFd as _;

let len = i32::try_from(bytes.len())
.map_err(|_err| error::Error::from(error::ErrorKind::TooMuchData))?;
nix::fcntl::fcntl(reader.as_fd(), nix::fcntl::FcntlArg::F_SETPIPE_SZ(len))?;
}
i32::try_from(bytes.len()).is_ok_and(|len| {
nix::fcntl::fcntl(reader.as_fd(), nix::fcntl::FcntlArg::F_SETPIPE_SZ(len)).is_ok()
})
};
#[cfg(not(any(target_os = "linux", target_os = "android")))]
let resized_to_fit = false;

writer.write_all(bytes)?;
drop(writer);
if resized_to_fit || bytes.len() <= MAX_INLINE_PIPE_WRITE {
writer.write_all(bytes)?;
drop(writer);
} else {
// The payload is too large to be absorbed by the pipe's buffer, so writing it
// here would block forever: the reader is the command we haven't spawned yet.
// Hand the write off to a helper thread instead. It exits once the payload is
// consumed, or earlier with a broken-pipe error if the read end is dropped
// first (e.g. a command that never reads its stdin).
let contents = contents.to_owned();
std::thread::Builder::new()
.name(String::from("brush-pipe-writer"))
.spawn(move || {
let _ = writer.write_all(contents.as_bytes());
})?;
}

Ok(reader.into())
}
26 changes: 26 additions & 0 deletions brush-shell/tests/cases/compat/here.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -281,3 +281,29 @@ cases:
EOF
)
echo $var

- name: "Here doc larger than pipe buffer"
stdin: |
payload=
i=0
while [ $i -lt 500 ]; do
payload="${payload}0123456789012345678901234567890123456789"
i=$((i + 1))
done
out=$(cat <<EOF
$payload
EOF
)
echo "${#out}"

- name: "Here string larger than pipe buffer"
stdin: |
shopt -ou posix
payload=
i=0
while [ $i -lt 500 ]; do
payload="${payload}0123456789012345678901234567890123456789"
i=$((i + 1))
done
out=$(cat <<<"$payload")
echo "${#out}"
Loading