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
5 changes: 4 additions & 1 deletion src/glob/GlobWalker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1863,7 +1863,9 @@ impl<A: Accessor, const SENTINEL: bool> GlobWalker<A, SENTINEL> {
!filepath.is_empty() && filepath[0] == b'.'
}

const SYNTAX_TOKENS: &'static [u8] = b"*[{?!";
// `\` is an escape on POSIX; on Windows it is a path separator and
// only reaches a component slice as a trailing sep, so skip it there.
const SYNTAX_TOKENS: &'static [u8] = if IS_WINDOWS { b"*[{?!" } else { b"*[{?!\\" };

fn check_special_syntax(pattern: &[u8]) -> bool {
strings::index_of_any(pattern, Self::SYNTAX_TOKENS).is_some()
Expand Down Expand Up @@ -1943,6 +1945,7 @@ impl<A: Accessor, const SENTINEL: bool> GlobWalker<A, SENTINEL> {
// because that only applies negation if at the
// beginning of the string.
b'[' | b'{' | b'?' | b'*' => break 'out_of_check_wildcard_filepath,
b'\\' if !IS_WINDOWS => break 'out_of_check_wildcard_filepath,
_ => {}
}
}
Expand Down
47 changes: 46 additions & 1 deletion test/js/bun/glob/scan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import { Glob, GlobScanOptions } from "bun";
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import fg from "fast-glob";
import { bunEnv, bunExe, tempDir, tempDirWithFiles, tmpdirSync } from "harness";
import { bunEnv, bunExe, isWindows, tempDir, tempDirWithFiles, tmpdirSync } from "harness";
import * as fs from "node:fs";
import * as path from "path";
import { createTempDirectoryWithBrokenSymlinks, prepareEntries, tempFixturesDir } from "./util";
Expand Down Expand Up @@ -618,6 +618,51 @@ describe("literal fast path", async () => {
});
});

// On Windows `\` is a path separator, not an escape character.
describe.skipIf(isWindows)("backslash escapes non-special characters", () => {
let cwd = "";
beforeAll(() => {
cwd = tempDirWithFiles("glob-scan-backslash-escape", {
"sp ace.txt": "x",
"d r": { "f.txt": "x" },
"foo.t s": "x",
});
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
afterAll(() => {
fs.rmSync(cwd, { recursive: true, force: true });
});

// `\x` quotes `x` whether or not `x` is special, so scan must agree with match.
test.each([
["literal component", "sp\\ ace.txt", ["sp ace.txt"]],
["*.ext component", "*.t\\ s", ["foo.t s"]],
["directory component", "d\\ r/*.txt", ["d r/f.txt"]],
["under **", "**/sp\\ ace.txt", ["sp ace.txt"]],
["escaped backslash is literal", "sp\\\\ ace.txt", []],
])("%s", async (_, pattern, expected) => {
const glob = new Glob(pattern);
const sync = Array.from(glob.scanSync({ cwd })).sort();
const async = (await Array.fromAsync(glob.scan({ cwd }))).sort();
expect({ sync, async }).toEqual({ sync: expected, async: expected });
for (const name of expected) {
expect(glob.match(name)).toBe(true);
}
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// `\\` is one literal `\`, so the on-disk name without a backslash must not match.
test("escaped backslash requires a literal backslash in the name", () => {
const glob = new Glob("sp\\\\ ace.txt");
expect(glob.match("sp ace.txt")).toBe(false);
expect(glob.match("sp\\ ace.txt")).toBe(true);
});

test("absolute path", async () => {
const glob = new Glob(path.posix.join(cwd, "sp\\ ace.txt"));
const entries = await Array.fromAsync(glob.scan());
expect(entries).toEqual([path.join(cwd, "sp ace.txt")]);
});
});

describe("trailing directory separator", async () => {
test("matches directories absolute", async () => {
const tmpdir = tmpdirSync();
Expand Down
Loading