From b075b949a52a2d8b78c420e7fc9e07a6579d7363 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 31 Jul 2026 04:17:18 +0000 Subject: [PATCH 1/3] fix(glob): count escape backslashes at the right offset in detect_glob_syntax detect_glob_syntax searches for special tokens in a slice that advances past escaped occurrences, but counted preceding backslashes at the slice-relative index into the full pattern. After skipping one escaped token, every later token was checked at the wrong offset, so patterns like \*x* were reported as having no glob syntax. bun install then treats such a workspaces entry as a literal path instead of expanding it (WorkspaceMap.rs). Rebase the match index onto the full pattern before counting, and add unit tests covering escaped, unescaped, and escaped-then-unescaped tokens. Adopts #34275. Co-authored-by: Sam Gammon --- src/glob/lib.rs | 43 ++++++++++++++++++++++++-- test/cli/install/bad-workspace.test.ts | 29 ++++++++++++++++- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/glob/lib.rs b/src/glob/lib.rs index 1c6aae4e1723..397f22547a3e 100644 --- a/src/glob/lib.rs +++ b/src/glob/lib.rs @@ -32,8 +32,12 @@ pub fn detect_glob_syntax(potential_pattern: &[u8]) -> bool { while !slice.is_empty() { if let Some(idx) = slice.iter().position(|&b| b == token) { // Check for even number of backslashes preceding the - // token to know that it's not escaped - let mut i = idx; + // token to know that it's not escaped. `idx` is relative to + // `slice`, so rebase it onto `potential_pattern` before + // counting; otherwise, once an escaped token has been + // skipped, the backslashes are counted at the wrong offset + // (e.g. `\*x*` reported no glob syntax). + let mut i = potential_pattern.len() - slice.len() + idx; let mut backslash_count: u16 = 0; while i > 0 && potential_pattern[i - 1] == b'\\' { @@ -53,3 +57,38 @@ pub fn detect_glob_syntax(potential_pattern: &[u8]) -> bool { false } + +#[cfg(test)] +mod tests { + use super::detect_glob_syntax; + + #[test] + fn detects_unescaped_tokens() { + assert!(detect_glob_syntax(b"*.ts")); + assert!(detect_glob_syntax(b"a/{b,c}/d")); + assert!(detect_glob_syntax(b"a[bc]d")); + assert!(detect_glob_syntax(b"a?c")); + assert!(detect_glob_syntax(b"!foo")); + } + + #[test] + fn ignores_escaped_tokens() { + assert!(!detect_glob_syntax(b"a\\*b")); + assert!(!detect_glob_syntax(b"a\\{b\\}c")); + assert!(!detect_glob_syntax(b"plain/path.txt")); + // even backslash count = escaped backslash, unescaped token + assert!(detect_glob_syntax(b"a\\\\*b")); + } + + #[test] + fn detects_unescaped_token_after_escaped_one() { + // Regression: backslashes were counted at a slice-relative offset, + // so any unescaped token after an escaped one went undetected. + assert!(detect_glob_syntax(b"\\*x*")); + assert!(detect_glob_syntax(b"\\{a\\}{b,c}")); + assert!(detect_glob_syntax(b"\\?a?")); + assert!(detect_glob_syntax(b"\\[a\\]b[cd]")); + // ...while all-escaped stays undetected + assert!(!detect_glob_syntax(b"\\*x\\*")); + } +} diff --git a/test/cli/install/bad-workspace.test.ts b/test/cli/install/bad-workspace.test.ts index e2101f0a3fe3..16978036179f 100644 --- a/test/cli/install/bad-workspace.test.ts +++ b/test/cli/install/bad-workspace.test.ts @@ -1,7 +1,8 @@ import { spawnSync } from "bun"; import { beforeEach, expect, setDefaultTimeout, test } from "bun:test"; import { mkdirSync, writeFileSync } from "fs"; -import { bunEnv, bunExe, tempDir, tmpdirSync } from "harness"; +import { bunEnv, bunExe, isWindows, tempDir, tmpdirSync } from "harness"; +import { join } from "path"; let cwd: string; @@ -58,6 +59,32 @@ test("non-string workspaces entry prints the error without literal markup", asyn expect(exitCode).toBe(1); }); +// `*` is not a valid filename character on Windows. +test.skipIf(isWindows)("workspace glob with an escaped token followed by an unescaped one is expanded", async () => { + // detect_glob_syntax counted preceding backslashes at a slice-relative offset into the + // full pattern, so after one escaped token every later token of the same kind was + // checked at the wrong position. `\*x*` was reported as having no glob syntax and + // treated as a literal path instead of being expanded. + using dir = tempDir("workspace-escaped-glob", { + "package.json": JSON.stringify({ name: "root", workspaces: ["\\*x*"] }), + "*xpkg/package.json": JSON.stringify({ name: "xpkg", version: "1.0.0" }), + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "install"], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stderr).not.toContain("Workspace not found"); + const lockfile = await Bun.file(join(String(dir), "bun.lock")).text(); + expect(lockfile).toContain('"*xpkg"'); + expect(lockfile).toContain('"xpkg@workspace:'); + expect({ stdout, stderr, exitCode }).toMatchObject({ exitCode: 0 }); +}); + test("workspace with ./ should not crash", () => { writeFileSync( `${cwd}/package.json`, From 902149838e300acb1151559ba0021238ac785f1e Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 31 Jul 2026 04:59:05 +0000 Subject: [PATCH 2/3] trim comments: keep invariant, move bug history to PR description --- src/glob/lib.rs | 9 ++------- test/cli/install/bad-workspace.test.ts | 6 +----- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/glob/lib.rs b/src/glob/lib.rs index 397f22547a3e..1f640d273cd5 100644 --- a/src/glob/lib.rs +++ b/src/glob/lib.rs @@ -33,10 +33,7 @@ pub fn detect_glob_syntax(potential_pattern: &[u8]) -> bool { if let Some(idx) = slice.iter().position(|&b| b == token) { // Check for even number of backslashes preceding the // token to know that it's not escaped. `idx` is relative to - // `slice`, so rebase it onto `potential_pattern` before - // counting; otherwise, once an escaped token has been - // skipped, the backslashes are counted at the wrong offset - // (e.g. `\*x*` reported no glob syntax). + // `slice`, so rebase it onto `potential_pattern` before counting. let mut i = potential_pattern.len() - slice.len() + idx; let mut backslash_count: u16 = 0; @@ -82,13 +79,11 @@ mod tests { #[test] fn detects_unescaped_token_after_escaped_one() { - // Regression: backslashes were counted at a slice-relative offset, - // so any unescaped token after an escaped one went undetected. + // https://github.com/oven-sh/bun/pull/34275 assert!(detect_glob_syntax(b"\\*x*")); assert!(detect_glob_syntax(b"\\{a\\}{b,c}")); assert!(detect_glob_syntax(b"\\?a?")); assert!(detect_glob_syntax(b"\\[a\\]b[cd]")); - // ...while all-escaped stays undetected assert!(!detect_glob_syntax(b"\\*x\\*")); } } diff --git a/test/cli/install/bad-workspace.test.ts b/test/cli/install/bad-workspace.test.ts index 16978036179f..67999bb10ce6 100644 --- a/test/cli/install/bad-workspace.test.ts +++ b/test/cli/install/bad-workspace.test.ts @@ -59,12 +59,8 @@ test("non-string workspaces entry prints the error without literal markup", asyn expect(exitCode).toBe(1); }); -// `*` is not a valid filename character on Windows. +// https://github.com/oven-sh/bun/pull/34275 — `*` is not a valid filename character on Windows. test.skipIf(isWindows)("workspace glob with an escaped token followed by an unescaped one is expanded", async () => { - // detect_glob_syntax counted preceding backslashes at a slice-relative offset into the - // full pattern, so after one escaped token every later token of the same kind was - // checked at the wrong position. `\*x*` was reported as having no glob syntax and - // treated as a literal path instead of being expanded. using dir = tempDir("workspace-escaped-glob", { "package.json": JSON.stringify({ name: "root", workspaces: ["\\*x*"] }), "*xpkg/package.json": JSON.stringify({ name: "xpkg", version: "1.0.0" }), From 00ae8b9af42ff89acb8b2fbfac55321a6f25433c Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 31 Jul 2026 05:01:50 +0000 Subject: [PATCH 3/3] drop inline comment narration; keep original comment unchanged --- src/glob/lib.rs | 3 +-- test/cli/install/bad-workspace.test.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/glob/lib.rs b/src/glob/lib.rs index 1f640d273cd5..571c85ff13e5 100644 --- a/src/glob/lib.rs +++ b/src/glob/lib.rs @@ -32,8 +32,7 @@ pub fn detect_glob_syntax(potential_pattern: &[u8]) -> bool { while !slice.is_empty() { if let Some(idx) = slice.iter().position(|&b| b == token) { // Check for even number of backslashes preceding the - // token to know that it's not escaped. `idx` is relative to - // `slice`, so rebase it onto `potential_pattern` before counting. + // token to know that it's not escaped let mut i = potential_pattern.len() - slice.len() + idx; let mut backslash_count: u16 = 0; diff --git a/test/cli/install/bad-workspace.test.ts b/test/cli/install/bad-workspace.test.ts index 67999bb10ce6..120dd15bb274 100644 --- a/test/cli/install/bad-workspace.test.ts +++ b/test/cli/install/bad-workspace.test.ts @@ -59,7 +59,7 @@ test("non-string workspaces entry prints the error without literal markup", asyn expect(exitCode).toBe(1); }); -// https://github.com/oven-sh/bun/pull/34275 — `*` is not a valid filename character on Windows. +// https://github.com/oven-sh/bun/pull/34275 (`*` is not a valid filename character on Windows) test.skipIf(isWindows)("workspace glob with an escaped token followed by an unescaped one is expanded", async () => { using dir = tempDir("workspace-escaped-glob", { "package.json": JSON.stringify({ name: "root", workspaces: ["\\*x*"] }),