Adjust path processing

This commit is contained in:
Owen
2026-10-12 15:10:48 -04:00
parent 5e93678122
commit f82b209e69
2 changed files with 59 additions and 13 deletions
+35 -13
View File
@@ -1,16 +1,37 @@
const segmentRegexCache = new Map<string, RegExp>();
// Matches a single segment against a glob where `*` matches any run of
// characters and `?` matches exactly one. Greedy two-pointer matching that
// only ever backtracks to the most recent `*`, so it runs in
// O(pattern * text) worst case. Compiling to a `.*` regex instead
// backtracks catastrophically on patterns like `*a*a*a*a*b`.
function matchSegmentGlob(pattern: string, text: string): boolean {
let p = 0;
let t = 0;
let starP = -1;
let starT = 0;
function getSegmentRegex(patternPart: string): RegExp {
let regex = segmentRegexCache.get(patternPart);
if (!regex) {
const regexPattern = patternPart
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
.replace(/\*/g, ".*")
.replace(/\?/g, ".");
regex = new RegExp(`^${regexPattern}$`);
segmentRegexCache.set(patternPart, regex);
while (t < text.length) {
if (p < pattern.length && pattern[p] === "*") {
starP = p++;
starT = t;
} else if (
p < pattern.length &&
(pattern[p] === "?" || pattern[p] === text[t])
) {
p++;
t++;
} else if (starP !== -1) {
// let the last `*` swallow one more character and retry
p = starP + 1;
t = ++starT;
} else {
return false;
}
}
return regex;
while (p < pattern.length && pattern[p] === "*") {
p++;
}
return p === pattern.length;
}
// Decodes percent-encoding (so an encoded slash like `%2F` is treated as a
@@ -79,8 +100,9 @@ export function isPathAllowed(pattern: string, path: string): boolean {
// match zero segments, or consume one path segment
dp[i][j] = dp[i + 1][j] || dp[i][j + 1];
} else if (currentPatternPart.includes("*")) {
const regex = getSegmentRegex(currentPatternPart);
dp[i][j] = regex.test(pathParts[j]) && dp[i + 1][j + 1];
dp[i][j] =
dp[i + 1][j + 1] &&
matchSegmentGlob(currentPatternPart, pathParts[j]);
} else {
dp[i][j] =
currentPatternPart === pathParts[j] && dp[i + 1][j + 1];
@@ -383,6 +383,30 @@ function runSpecialCharacterTests() {
"Deeply nested paths should still match"
);
assertEquals(
isPathAllowed("*a*b", "xaxyb"),
true,
"Multiple wildcards within a segment should match"
);
assertEquals(
isPathAllowed("*a*b", "xaxyba"),
false,
"In-segment wildcard pattern must still be anchored at the end"
);
// a `.*`-style regex backtracks catastrophically here and never returns
const start = Date.now();
isPathAllowed("/" + "*a".repeat(10) + "b", "/" + "a".repeat(5000));
isPathAllowed(
"/" + Array(10).fill("*a*a*a*").join("/") + "/nevermatch",
"/" + Array(25).fill("a".repeat(200)).join("/")
);
assertEquals(
Date.now() - start < 1000,
true,
"In-segment wildcards should not backtrack exponentially"
);
console.log("All special character tests passed!");
}