Skip to content

Commit 5fd2335

Browse files
authored
Merge pull request #593 from LHMQ878/fix/protected-patterns-windows-separator
fix: normalize a single backslash so protectedFilePatterns work on Windows
2 parents 1d1f5c5 + 5f8f33b commit 5fd2335

2 files changed

Lines changed: 109 additions & 1 deletion

File tree

lib/protected-patterns.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
function normalizePath(input: string): string {
2-
return input.replaceAll("\\\\", "/")
2+
// A single backslash. In source, "\\" is the one-character string; the
3+
// previous "\\\\" was a *two*-character string, so it only ever matched a
4+
// doubled separator -- which a real Windows path does not contain. The
5+
// normalisation was therefore a no-op on the only platform that needs it.
6+
return input.replaceAll("\\", "/")
37
}
48

59
function escapeRegExpChar(ch: string): string {

tests/protected-patterns.test.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import assert from "node:assert/strict"
2+
import test from "node:test"
3+
import {
4+
getFilePathsFromParameters,
5+
isFilePathProtected,
6+
isToolNameProtected,
7+
matchesGlob,
8+
} from "../lib/protected-patterns"
9+
10+
// A single backslash, built from its char code so the intent survives any later
11+
// reformatting of this file. The bug being pinned here was precisely an escaping
12+
// mistake in a string literal, so the tests avoid writing one by hand.
13+
const BS = String.fromCharCode(92)
14+
const winPath = (...segments: string[]) => segments.join(BS)
15+
16+
test("matchesGlob treats a Windows separator as a path separator", () => {
17+
// `protectedFilePatterns` is documented with forward slashes ("**/*.config.ts"),
18+
// but on Windows the tool parameters carry backslashes. Both sides are
19+
// normalised, so the same pattern must match either spelling of the same file.
20+
const path = winPath("C:", "repo", "src", "config", "secrets.ts")
21+
22+
assert.equal(matchesGlob(path, "**/secrets.ts"), true)
23+
assert.equal(matchesGlob(path, "**/config/*.ts"), true)
24+
assert.equal(matchesGlob(path, "C:/repo/src/**"), true)
25+
assert.equal(matchesGlob(path, "**/*.ts"), true)
26+
})
27+
28+
test("matchesGlob accepts a pattern written with Windows separators", () => {
29+
// Normalisation applies to the pattern too, so a user who copies a path out of
30+
// Explorer and uses it as a pattern gets the same result as the documented form.
31+
const pattern = winPath("**", "config", "*.ts")
32+
33+
assert.equal(matchesGlob("C:/repo/src/config/secrets.ts", pattern), true)
34+
assert.equal(matchesGlob(winPath("C:", "repo", "src", "config", "secrets.ts"), pattern), true)
35+
})
36+
37+
test("a single-segment wildcard still does not cross a Windows separator", () => {
38+
// `*` is defined as "[^/]*". Normalisation must convert separators rather than
39+
// erase them, or `*` would silently start matching across directories.
40+
assert.equal(matchesGlob(winPath("src", "config", "secrets.ts"), "src/*"), false)
41+
assert.equal(matchesGlob(winPath("src", "config", "secrets.ts"), "src/*/*.ts"), true)
42+
assert.equal(matchesGlob(winPath("src", "secrets.ts"), "src/*"), true)
43+
})
44+
45+
test("isFilePathProtected protects a Windows path the user configured", () => {
46+
// The end-to-end shape: a `read` tool call on Windows, checked against the
47+
// documented pattern style. This is the assertion that failed before the fix --
48+
// the file was protected on POSIX and unprotected on Windows.
49+
const patterns = ["**/secrets.ts", "**/.env"]
50+
51+
for (const path of [
52+
"C:/repo/src/config/secrets.ts",
53+
winPath("C:", "repo", "src", "config", "secrets.ts"),
54+
]) {
55+
const paths = getFilePathsFromParameters("read", { filePath: path })
56+
assert.equal(isFilePathProtected(paths, patterns), true, `not protected: ${path}`)
57+
}
58+
})
59+
60+
test("isFilePathProtected still returns false for a genuinely unmatched path", () => {
61+
// The fix must widen matching only for separators, not for anything else.
62+
const paths = getFilePathsFromParameters("read", {
63+
filePath: winPath("C:", "repo", "src", "main.ts"),
64+
})
65+
66+
assert.equal(isFilePathProtected(paths, ["**/secrets.ts"]), false)
67+
assert.equal(isFilePathProtected(paths, []), false)
68+
assert.equal(isFilePathProtected([], ["**/*.ts"]), false)
69+
})
70+
71+
test("multiedit and apply_patch paths are protected on Windows too", () => {
72+
// These two tools carry paths in shapes of their own, so they need their own
73+
// coverage: a nested `edits` array and paths embedded in patch text.
74+
const patterns = ["**/secrets.ts"]
75+
76+
const multiedit = getFilePathsFromParameters("multiedit", {
77+
filePath: winPath("src", "main.ts"),
78+
edits: [{ filePath: winPath("src", "config", "secrets.ts") }],
79+
})
80+
assert.equal(isFilePathProtected(multiedit, patterns), true)
81+
82+
const patch = getFilePathsFromParameters("apply_patch", {
83+
patchText: `*** Update File: ${winPath("src", "config", "secrets.ts")}\n@@\n-a\n+b\n`,
84+
})
85+
assert.equal(isFilePathProtected(patch, patterns), true)
86+
})
87+
88+
test("isToolNameProtected is unaffected by separator normalisation", () => {
89+
// Tool names contain no separators; this pins that the shared helper does not
90+
// change their behaviour.
91+
assert.equal(isToolNameProtected("bash", ["bash"]), true)
92+
assert.equal(isToolNameProtected("bash", ["ba*"]), true)
93+
assert.equal(isToolNameProtected("bash", ["read"]), false)
94+
assert.equal(isToolNameProtected("bash", []), false)
95+
assert.equal(isToolNameProtected("", ["bash"]), false)
96+
})
97+
98+
test("matchesGlob rejects an empty pattern and handles regex metacharacters", () => {
99+
// Pattern text is interpolated into a RegExp, so characters that mean something
100+
// there must be matched literally.
101+
assert.equal(matchesGlob("a/b.ts", ""), false)
102+
assert.equal(matchesGlob("src/a+b(1).ts", "src/a+b(1).ts"), true)
103+
assert.equal(matchesGlob("src/axb1.ts", "src/a+b(1).ts"), false)
104+
})

0 commit comments

Comments
 (0)