address review: check_nesting scans template interpolations, file masks keep wildcard length

This commit is contained in:
Dylan M. Taylor
2026-11-03 22:58:29 +00:00
parent dab17b1e9c
commit e7dd6288e5
3 changed files with 62 additions and 5 deletions
+4 -2
View File
@@ -296,10 +296,12 @@ function Folder(path) {
// A case-insensitive `*` / `?` file mask match in time bounded by mask × name length (a
// regular expression of `.*` runs backtracks combinatorially on overlapping wildcards).
function __globMatch(mask, name) {
var p = mask.toLowerCase(), s = name.toLowerCase();
// Wildcards run over the original name (lowercasing it can change its length: "İ" becomes
// two code units); literal characters compare case-insensitively one at a time.
var p = mask, s = name;
var i = 0, j = 0, star = -1, mark = 0;
while (j < s.length) {
if (i < p.length && (p[i] === "?" || p[i] === s[j])) { i++; j++; }
if (i < p.length && (p[i] === "?" || p[i] === s[j] || p[i].toLowerCase() === s[j].toLowerCase())) { i++; j++; }
else if (i < p.length && p[i] === "*") { star = i++; mark = j; }
else if (star !== -1) { i = star + 1; j = ++mark; }
else return false;
+55 -3
View File
@@ -768,19 +768,61 @@ fn run_expanded(session: &mut Session, req: &ScriptRequest) -> Outcome {
/// 8 MiB thread such as the Script Console's, so deeper scripts are rejected up front.
pub const MAX_NESTING: usize = 96;
/// An error when `code` nests brackets deeper than [`MAX_NESTING`] (strings, template literals
/// and comments are skipped).
/// Skip template-literal text from `*i` (just past a backtick or an interpolation's `}`). Returns
/// true when it stopped past a `${` (code follows), false at the closing backtick or the end.
fn skip_template_text(b: &[u8], i: &mut usize) -> bool {
while let Some(&d) = b.get(*i) {
match d {
b'\\' => *i += 2,
b'`' => {
*i += 1;
return false;
}
b'$' if b.get(*i + 1) == Some(&b'{') => {
*i += 2;
return true;
}
_ => *i += 1,
}
}
false
}
/// An error when `code` nests brackets deeper than [`MAX_NESTING`] (strings, template literal
/// text and comments are skipped; the code inside `${…}` interpolations is checked).
pub fn check_nesting(code: &str) -> Result<(), String> {
let b = code.as_bytes();
let (mut i, mut depth, mut max) = (0usize, 0usize, 0usize);
// The bracket depth of each open `${`, to tell its closing `}` from an inner one.
let mut interpolations: Vec<usize> = vec![];
while let Some(&c) = b.get(i) {
match c {
b'`' => {
i += 1;
if skip_template_text(b, &mut i) {
depth += 1;
max = max.max(depth);
interpolations.push(depth);
}
continue;
}
b'}' if interpolations.last() == Some(&depth) => {
interpolations.pop();
depth -= 1;
i += 1;
if skip_template_text(b, &mut i) {
depth += 1;
max = max.max(depth);
interpolations.push(depth);
}
continue;
}
b'(' | b'[' | b'{' => {
depth += 1;
max = max.max(depth);
}
b')' | b']' | b'}' => depth = depth.saturating_sub(1),
b'"' | b'\'' | b'`' => {
b'"' | b'\'' => {
// Skip to the closing quote (escapes skip a byte).
i += 1;
while let Some(&d) = b.get(i) {
@@ -879,6 +921,16 @@ mod nesting_tests {
assert!(check_nesting(&("(".repeat(MAX_NESTING) + &")".repeat(MAX_NESTING))).is_ok());
}
#[test]
fn template_interpolations_are_checked_but_template_text_is_not() {
let deep = |n: usize| format!("`a${{{}1{}}}b`", "(".repeat(n), ")".repeat(n));
assert!(check_nesting(&deep(5000)).is_err());
assert!(check_nesting(&deep(10)).is_ok());
// Brackets in the literal text, and a nested template, are fine; depth carries across `}`.
assert!(check_nesting(&format!("`{} ${{ `x${{1+(2)}}y` }} {}`", "(".repeat(500), "{".repeat(500))).is_ok());
assert!(check_nesting(&format!("`${{1}}${{{}2{}}}`", "[".repeat(MAX_NESTING), "]".repeat(MAX_NESTING))).is_err());
}
/// A canonical drive path and a lexical one compare once the `\\?\` prefix is gone; a UNC
/// share and a Unix path are left as they are.
#[test]
+3
View File
@@ -1934,4 +1934,7 @@ fn file_masks_match_without_backtracking() {
"[__globMatch('*.JSX', 'a.jsx'), __globMatch('a?c*', 'abcdef'), __globMatch('*a*b', 'xaxb'), __globMatch('*.png', 'a.jsx'), __globMatch('*a*a*a*a*a*a*a*a*a*a*a*b', new Array(60).join('a'))].join()",
);
assert_eq!(r.result.as_str(), Some("true,true,true,false,false"));
// `?` still matches one character that lowercases to two.
let r = ok(&mut s, "[__globMatch('?.txt', '\\u0130.txt'), __globMatch('\\u0130*', '\\u0130x')].join()");
assert_eq!(r.result.as_str(), Some("true,true"));
}