diff --git a/crates/script/src/prelude.js b/crates/script/src/prelude.js index 092ce6cb8..39d7437ed 100644 --- a/crates/script/src/prelude.js +++ b/crates/script/src/prelude.js @@ -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; diff --git a/crates/script/src/runtime.rs b/crates/script/src/runtime.rs index 1e1ae7c1c..1c76bd1f1 100644 --- a/crates/script/src/runtime.rs +++ b/crates/script/src/runtime.rs @@ -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 = 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] diff --git a/crates/script/src/tests.rs b/crates/script/src/tests.rs index b5b1c36e7..2583bd9cd 100644 --- a/crates/script/src/tests.rs +++ b/crates/script/src/tests.rs @@ -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")); }