Skip to content
Open
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
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@ jobs:
steps:
- uses: actions/checkout@v6

# Free ~25 GB from the hosted runner. Across 6 cargo-test stages with
# different feature combos, target/ grows past the default 14 GB free.
- name: Free disk space
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \
/opt/hostedtoolcache/CodeQL /usr/local/share/boost \
/usr/local/share/powershell /usr/local/.ghcup || true
sudo docker image prune --all --force || true
df -h /

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

Expand Down
208 changes: 25 additions & 183 deletions crates/bashkit/src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2630,204 +2630,44 @@ impl<'a> Parser<'a> {
Ok(word)
}
Some(tokens::Token::ProcessSubIn) | Some(tokens::Token::ProcessSubOut) => {
// Process substitution <(cmd) or >(cmd)
// Process substitution <(cmd) or >(cmd).
//
// ISSUE #1333: slice the body directly from the original source
// instead of re-serializing tokens. Re-serialization lost
// per-segment quote boundaries for tokens like QuotedGlobWord
// (e.g. `./"$var"*.ext`), which caused the inner parser to see
// the glob `*` as quoted and suppress pathname expansion.
let is_input = matches!(self.current_token, Some(tokens::Token::ProcessSubIn));
self.advance();

// Parse commands until we hit a closing paren
let mut cmd_str = String::new();
let mut depth = 1;
let start_offset = match &self.current_token {
Some(_) => self.current_span.start.offset,
None => {
return Err(Error::parse(
"unexpected end of input in process substitution".to_string(),
));
}
};
let end_offset;
let mut depth: u32 = 1;
loop {
match &self.current_token {
Some(tokens::Token::LeftParen) => {
Some(tokens::Token::LeftParen)
| Some(tokens::Token::ProcessSubIn)
| Some(tokens::Token::ProcessSubOut) => {
depth += 1;
cmd_str.push('(');
self.advance();
}
Some(tokens::Token::RightParen) => {
depth -= 1;
if depth == 0 {
end_offset = self.current_span.start.offset;
self.advance();
break;
}
cmd_str.push(')');
self.advance();
}
Some(tokens::Token::Word(w)) => {
if !cmd_str.is_empty() {
cmd_str.push(' ');
}
cmd_str.push_str(w);
self.advance();
}
Some(tokens::Token::QuotedWord(w))
| Some(tokens::Token::QuotedGlobWord(w)) => {
if !cmd_str.is_empty() {
cmd_str.push(' ');
}
cmd_str.push('"');
cmd_str.push_str(w);
cmd_str.push('"');
self.advance();
}
Some(tokens::Token::LiteralWord(w)) => {
if !cmd_str.is_empty() {
cmd_str.push(' ');
}
cmd_str.push('\'');
cmd_str.push_str(w);
cmd_str.push('\'');
self.advance();
}
Some(tokens::Token::Pipe) => {
cmd_str.push_str(" | ");
self.advance();
}
Some(tokens::Token::Semicolon) => {
cmd_str.push_str("; ");
self.advance();
}
Some(tokens::Token::And) => {
cmd_str.push_str(" && ");
self.advance();
}
Some(tokens::Token::Or) => {
cmd_str.push_str(" || ");
self.advance();
}
Some(tokens::Token::Background) => {
cmd_str.push_str(" & ");
self.advance();
}
Some(tokens::Token::RedirectOut) => {
cmd_str.push_str(" > ");
self.advance();
}
Some(tokens::Token::RedirectAppend) => {
cmd_str.push_str(" >> ");
self.advance();
}
Some(tokens::Token::RedirectIn) => {
cmd_str.push_str(" < ");
self.advance();
}
Some(tokens::Token::HereString) => {
cmd_str.push_str(" <<< ");
self.advance();
}
Some(tokens::Token::DupOutput) => {
cmd_str.push_str(" >&");
self.advance();
}
Some(tokens::Token::RedirectFd(fd)) => {
cmd_str.push_str(&format!(" {}> ", fd));
self.advance();
}
Some(tokens::Token::LeftBrace) => {
if !cmd_str.is_empty() {
cmd_str.push(' ');
}
cmd_str.push('{');
self.advance();
}
Some(tokens::Token::RightBrace) => {
cmd_str.push_str(" }");
self.advance();
}
Some(tokens::Token::Newline) => {
cmd_str.push('\n');
self.advance();
}
Some(tokens::Token::DoubleLeftBracket) => {
if !cmd_str.is_empty() {
cmd_str.push(' ');
}
cmd_str.push_str("[[");
self.advance();
}
Some(tokens::Token::DoubleRightBracket) => {
cmd_str.push_str(" ]]");
self.advance();
}
Some(tokens::Token::DoubleLeftParen) => {
if !cmd_str.is_empty() {
cmd_str.push(' ');
}
cmd_str.push_str("((");
self.advance();
}
Some(tokens::Token::DoubleRightParen) => {
cmd_str.push_str("))");
self.advance();
}
Some(tokens::Token::DoubleSemicolon) => {
cmd_str.push_str(";;");
self.advance();
}
Some(tokens::Token::SemiAmp) => {
cmd_str.push_str(";&");
self.advance();
}
Some(tokens::Token::DoubleSemiAmp) => {
cmd_str.push_str(";;&");
self.advance();
}
Some(tokens::Token::Assignment) => {
cmd_str.push('=');
self.advance();
}
Some(tokens::Token::RedirectBoth) => {
cmd_str.push_str(" &>");
self.advance();
}
Some(tokens::Token::Clobber) => {
cmd_str.push_str(" >|");
self.advance();
}
Some(tokens::Token::DupInput) => {
cmd_str.push_str(" <&");
self.advance();
}
Some(tokens::Token::HereDoc) => {
cmd_str.push_str(" <<");
self.advance();
}
Some(tokens::Token::HereDocStrip) => {
cmd_str.push_str(" <<-");
self.advance();
}
Some(tokens::Token::RedirectFdAppend(fd)) => {
cmd_str.push_str(&format!(" {}>>", fd));
self.advance();
}
Some(tokens::Token::DupFd(src, dst)) => {
cmd_str.push_str(&format!(" {}>& {}", src, dst));
self.advance();
}
Some(tokens::Token::DupFdIn(src, dst)) => {
cmd_str.push_str(&format!(" {}<& {}", src, dst));
self.advance();
}
Some(tokens::Token::DupFdClose(fd)) => {
cmd_str.push_str(&format!(" {}<&-", fd));
self.advance();
}
Some(tokens::Token::RedirectFdIn(fd)) => {
cmd_str.push_str(&format!(" {}< ", fd));
self.advance();
}
Some(tokens::Token::ProcessSubIn) => {
cmd_str.push_str(" <(");
depth += 1;
self.advance();
}
Some(tokens::Token::ProcessSubOut) => {
cmd_str.push_str(" >(");
depth += 1;
self.advance();
}
Some(tokens::Token::Error(e)) => {
// Propagate lexer errors
let msg = e.clone();
self.advance();
return Err(Error::parse(format!(
Expand All @@ -2840,14 +2680,16 @@ impl<'a> Parser<'a> {
"unexpected end of input in process substitution".to_string(),
));
}
#[allow(unreachable_patterns)]
_ => {
// Safety net for future Token variants
self.advance();
}
}
}

let cmd_str = self
.source_slice(start_offset, end_offset)
.unwrap_or_default();

// THREAT[TM-DOS-021]: Propagate parent parser limits to child parser
// to prevent depth limit bypass via nested process substitution.
// Child inherits remaining depth budget and fuel from parent.
Expand Down
28 changes: 28 additions & 0 deletions crates/bashkit/tests/spec_cases/bash/procsub.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,31 @@ a
b
c
### end

### process_subst_glob_adjacent_quoted_var
### bash_diff: requires /dev/fd/ and VFS-specific filesystem
# Issue #1333: glob `*` adjacent to quoted variable inside <(...) must expand
mkdir -p /tmp/psubglob
cd /tmp/psubglob
: > tag_foo.tmp.html
: > tag_bar.tmp.html
p="tag_"
while IFS= read -r i; do echo "got: $i"; done < <(ls ./"$p"*.tmp.html | sort)
### expect
got: ./tag_bar.tmp.html
got: ./tag_foo.tmp.html
### end

### process_subst_glob_cat
### bash_diff: requires /dev/fd/ and VFS-specific filesystem
# Issue #1333: `cat < <(...)` variant
mkdir -p /tmp/psubglob2
cd /tmp/psubglob2
: > tag_a.tmp.html
: > tag_b.tmp.html
p="tag_"
cat < <(ls ./"$p"*.tmp.html | sort)
### expect
./tag_a.tmp.html
./tag_b.tmp.html
### end
Loading