Skip to content
Closed
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
43 changes: 41 additions & 2 deletions src/glob/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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'\\' {
Expand All @@ -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\\*"));
}
}
29 changes: 28 additions & 1 deletion test/cli/install/bad-workspace.test.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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`,
Expand Down
Loading