Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
36 changes: 19 additions & 17 deletions src/shell_parser/braces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,9 @@ pub fn expand(
contains_nested: bool,
) -> Result<(), ExpandError> {
check_brace_group_count(tokens)?;
let mut out_key_counter: u16 = 1;
// Next unclaimed `out` slot; ends at `out.len()` once every slot is
// claimed, so it must hold one more than the largest key.
Comment thread
robobun marked this conversation as resolved.
Resolved
let mut out_key_counter: usize = 1;
if !contains_nested {
let expansions_table = build_expansion_table_alloc(tokens)?;

Expand Down Expand Up @@ -582,8 +584,8 @@ pub fn expand(
unsafe fn expand_nested(
root: *mut ast::Group,
out: &mut [Vec<u8>],
out_key: u16,
out_key_counter: &mut u16,
out_key: usize,
out_key_counter: &mut usize,
start: u32,
) -> Result<(), ExpandError> {
// SAFETY: see fn doc comment —
Expand All @@ -607,7 +609,7 @@ unsafe fn expand_nested(

match &(*root).atoms {
ast::GroupAtoms::Single(ast::Atom::Text(txt)) => {
out[usize::from(out_key)].extend_from_slice(txt.slice());
out[out_key].extend_from_slice(txt.slice());
if !(*root).bubble_up.is_null() {
let bubble_up = (*root).bubble_up;
let next = (*root).bubble_up_next.unwrap();
Expand All @@ -622,7 +624,7 @@ unsafe fn expand_nested(
return Ok(());
}
ast::GroupAtoms::Single(ast::Atom::Expansion(expansion)) => {
let length = out[usize::from(out_key)].len();
let length = out[out_key].len();
let variants = expansion.variants;
let variants_len = variants.len();
for j in 0..variants_len {
Expand All @@ -636,8 +638,8 @@ unsafe fn expand_nested(
// new_key > out_key always (counter is bumped past out_key before
// any recursion into it), and the j==0 recursion only appends to
// out[out_key], so its [..length] prefix is stable.
let (lo, hi) = out.split_at_mut(usize::from(new_key));
hi[0].extend_from_slice(&lo[usize::from(out_key)][..length]);
let (lo, hi) = out.split_at_mut(new_key);
hi[0].extend_from_slice(&lo[out_key][..length]);
*out_key_counter += 1;
new_key
};
Expand Down Expand Up @@ -670,10 +672,10 @@ unsafe fn expand_nested(
let atom: &ast::Atom = &(*many)[i_];
match atom {
ast::Atom::Text(txt) => {
out[usize::from(out_key)].extend_from_slice(txt.slice());
out[out_key].extend_from_slice(txt.slice());
}
ast::Atom::Expansion(expansion) => {
let length = out[usize::from(out_key)].len();
let length = out[out_key].len();
let variants = expansion.variants;
let variants_len = variants.len();
for j in 0..variants_len {
Expand All @@ -684,8 +686,8 @@ unsafe fn expand_nested(
out_key
} else {
let new_key = *out_key_counter;
let (lo, hi) = out.split_at_mut(usize::from(new_key));
hi[0].extend_from_slice(&lo[usize::from(out_key)][..length]);
let (lo, hi) = out.split_at_mut(new_key);
hi[0].extend_from_slice(&lo[out_key][..length]);
*out_key_counter += 1;
new_key
};
Expand Down Expand Up @@ -713,8 +715,8 @@ fn expand_flat(
tokens: &[Token],
expansion_table: &[ExpansionVariant],
out: &mut [Vec<u8>],
out_key: u16,
out_key_counter: &mut u16,
out_key: usize,
out_key_counter: &mut usize,
depth_: u8,
start: usize,
end: usize,
Expand All @@ -728,7 +730,7 @@ fn expand_flat(
for atom in tokens[start..end].iter() {
match atom {
Token::Text(txt) => {
out[usize::from(out_key)].extend_from_slice(txt.slice());
out[out_key].extend_from_slice(txt.slice());
}
Token::Close => {
depth -= 1;
Expand All @@ -741,16 +743,16 @@ fn expand_flat(
[usize::from(expansion_variants.idx)..usize::from(expansion_variants.end)];
let skip_over_idx = variants[variants.len() - 1].end();

let starting_len = out[usize::from(out_key)].len();
let starting_len = out[out_key].len();
for (i, variant) in variants.iter().enumerate() {
let new_key = if i == 0 {
out_key
} else {
let new_key = *out_key_counter;
// new_key > out_key always; the i==0 recursion only appends to
// out[out_key], so its [..starting_len] prefix is stable.
let (lo, hi) = out.split_at_mut(usize::from(new_key));
hi[0].extend_from_slice(&lo[usize::from(out_key)][..starting_len]);
let (lo, hi) = out.split_at_mut(new_key);
hi[0].extend_from_slice(&lo[out_key][..starting_len]);
*out_key_counter += 1;
new_key
};
Expand Down
46 changes: 46 additions & 0 deletions test/js/bun/shell/brace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,52 @@ console.log(JSON.stringify(Bun.$.braces("echo {a,b}")));`,
`);
expect(exitCode).toBe(0);
});

// The expander hands out output slots from a counter that is bumped once per
// slot, so after the last of N slots it holds N. The expansion cap admits
// N = 65536, one more than the old u16 counter could hold, so a word with
// exactly 65536 variants tripped the overflow check in debug builds.
// Each case runs in a subprocess because that check aborts the process. The
// child prints [count, first, last]; the last variant lives in the slot whose
// claim used to overflow.
describe("exactly 65536 variants", () => {
async function expandInChild(script: string) {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", script],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
return { stdout: stdout.trim(), stderr, exitCode };
}

test.concurrent("shell word with 16 flat groups (2^16)", async () => {
const result = await expandInChild(`
const word = Buffer.alloc(16 * "{a,b}".length, "{a,b}").toString();
const words = (await Bun.$\`echo \${{ raw: word }}\`.text()).trimEnd().split(" ");
console.log(JSON.stringify([words.length, words[0], words.at(-1)]));
`);
expect(result).toEqual({
stdout: JSON.stringify([65536, "aaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbb"]),
stderr: "",
exitCode: 0,
});
});

test.concurrent("$.braces() with 8 nested groups (4^8)", async () => {
const result = await expandInChild(`
const word = Buffer.alloc(8 * "{{a,b},{c,d}}".length, "{{a,b},{c,d}}").toString();
const out = Bun.$.braces(word);
console.log(JSON.stringify([out.length, out[0], out.at(-1)]));
`);
expect(result).toEqual({
stdout: JSON.stringify([65536, "aaaaaaaa", "dddddddd"]),
stderr: "",
exitCode: 0,
});
});
});
});

// A `{...}` group with no top-level comma is literal text in bash. The brace
Expand Down
Loading