mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-07 12:13:22 +00:00
address review: file masks fold case as /i does; the nesting check skips regular expression literals
This commit is contained in:
@@ -293,15 +293,25 @@ function Folder(path) {
|
||||
this.__path = path === undefined ? "" : __str(path);
|
||||
}
|
||||
|
||||
// ECMAScript's legacy case canonicalisation (what a `/i` regular expression compares with): the
|
||||
// upper case form when it is one code unit, and not an ASCII letter for a non-ASCII one.
|
||||
function __canon(ch) {
|
||||
var u = ch.toUpperCase();
|
||||
if (u.length !== 1) return ch;
|
||||
if (ch.charCodeAt(0) >= 128 && u.charCodeAt(0) < 128) return ch;
|
||||
return u;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// 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.
|
||||
// two code units); literal characters compare one at a time, case-insensitively as `/i` does
|
||||
// ("ς" and "σ" and "Σ" match each other).
|
||||
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] || p[i].toLowerCase() === s[j].toLowerCase())) { i++; j++; }
|
||||
if (i < p.length && (p[i] === "?" || p[i] === s[j] || __canon(p[i]) === __canon(s[j]))) { 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;
|
||||
|
||||
@@ -788,11 +788,51 @@ fn skip_template_text(b: &[u8], i: &mut usize) -> bool {
|
||||
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).
|
||||
/// Whether a `/` after the last significant byte `prev` (at `at` in `b`) starts a regular
|
||||
/// expression literal rather than dividing: after an operator, an opening bracket, a separator,
|
||||
/// a keyword that takes an expression, or at the start. After a name, a number, a string or a
|
||||
/// closing bracket it divides (a regular expression after `)` is the one case this misreads).
|
||||
fn regex_may_start(b: &[u8], prev: u8, at: usize) -> bool {
|
||||
match prev {
|
||||
0 => true,
|
||||
b'(' | b',' | b'=' | b':' | b'[' | b'!' | b'&' | b'|' | b'?' | b'{' | b';' | b'+' | b'-' | b'*' | b'%' | b'<' | b'>' | b'~' | b'^' => true,
|
||||
c if c.is_ascii_alphanumeric() || c == b'_' || c == b'$' => {
|
||||
let start = (0..=at).rev().find(|&k| !b.get(k).is_some_and(|d| d.is_ascii_alphanumeric() || *d == b'_' || *d == b'$')).map_or(0, |k| k + 1);
|
||||
let word = b.get(start..=at).unwrap_or_default();
|
||||
[&b"return"[..], b"typeof", b"instanceof", b"in", b"of", b"new", b"delete", b"void", b"throw", b"case", b"do", b"else", b"yield", b"await"]
|
||||
.contains(&word)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Skip a regular expression literal that starts at `b[*i]` (a `/`), to its closing `/` (flags
|
||||
/// follow as ordinary letters). A `/` inside a class `[…]` or after a backslash does not end it,
|
||||
/// and neither a quote nor a backtick inside it starts a string; a line break ends it.
|
||||
fn skip_regex(b: &[u8], i: &mut usize) {
|
||||
*i += 1;
|
||||
let mut class = false;
|
||||
while let Some(&d) = b.get(*i) {
|
||||
match d {
|
||||
b'\\' => *i += 1,
|
||||
b'[' => class = true,
|
||||
b']' => class = false,
|
||||
b'/' if !class => return,
|
||||
b'\n' => return,
|
||||
_ => {}
|
||||
}
|
||||
*i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// An error when `code` nests brackets deeper than [`MAX_NESTING`] (strings, regular expression
|
||||
/// literals, 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 last significant byte (not space or comment), and where it is.
|
||||
let (mut prev, mut prev_at) = (0u8, 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) {
|
||||
@@ -803,6 +843,9 @@ pub fn check_nesting(code: &str) -> Result<(), String> {
|
||||
depth += 1;
|
||||
max = max.max(depth);
|
||||
interpolations.push(depth);
|
||||
(prev, prev_at) = (b'{', i - 1);
|
||||
} else {
|
||||
(prev, prev_at) = (b'`', i.saturating_sub(1));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -814,6 +857,9 @@ pub fn check_nesting(code: &str) -> Result<(), String> {
|
||||
depth += 1;
|
||||
max = max.max(depth);
|
||||
interpolations.push(depth);
|
||||
(prev, prev_at) = (b'{', i - 1);
|
||||
} else {
|
||||
(prev, prev_at) = (b'`', i.saturating_sub(1));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -838,16 +884,22 @@ pub fn check_nesting(code: &str) -> Result<(), String> {
|
||||
while b.get(i).is_some_and(|d| *d != b'\n') {
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
b'/' if b.get(i + 1) == Some(&b'*') => {
|
||||
i += 2;
|
||||
while i < b.len() && !(b.get(i) == Some(&b'*') && b.get(i + 1) == Some(&b'/')) {
|
||||
i += 1;
|
||||
}
|
||||
i += 1;
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
b'/' if regex_may_start(b, prev, prev_at) => skip_regex(b, &mut i),
|
||||
_ => {}
|
||||
}
|
||||
if !c.is_ascii_whitespace() {
|
||||
(prev, prev_at) = (c, i);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
if max > MAX_NESTING {
|
||||
@@ -921,6 +973,23 @@ mod nesting_tests {
|
||||
assert!(check_nesting(&("(".repeat(MAX_NESTING) + &")".repeat(MAX_NESTING))).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regular_expression_literals_are_skipped_not_read_as_strings_or_templates() {
|
||||
let deep = |n: usize| format!("{}1{}", "(".repeat(n), ")".repeat(n));
|
||||
// A backtick, a quote or a slash inside a class in a regular expression opens nothing, so
|
||||
// the deep expression after it is still counted.
|
||||
for re in ["/`/", "/\"/", "/'/", "/[/]/", "/\\//"] {
|
||||
assert!(check_nesting(&format!("`x${{ {re}.test('x') ? {} : 0 }}`", deep(5000))).is_err(), "{re} in an interpolation");
|
||||
assert!(check_nesting(&format!("x = {re}.test(y) ? {} : 0", deep(5000))).is_err(), "{re}");
|
||||
assert!(check_nesting(&format!("x = {re}.test(y) ? {} : 0", deep(10))).is_ok(), "{re} shallow");
|
||||
}
|
||||
// Division is not a regular expression: what follows it is still scanned.
|
||||
assert!(check_nesting(&format!("a = 4 / 2; b = {}", deep(5000))).is_err());
|
||||
assert!(check_nesting(&format!("a = (4) / 2 / 3; b = {}", deep(5000))).is_err());
|
||||
// After a keyword that takes an expression a slash starts a regular expression.
|
||||
assert!(check_nesting(&format!("typeof /`/ ; {}", deep(5000))).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn template_interpolations_are_checked_but_template_text_is_not() {
|
||||
let deep = |n: usize| format!("`a${{{}1{}}}b`", "(".repeat(n), ")".repeat(n));
|
||||
|
||||
@@ -1937,4 +1937,10 @@ fn file_masks_match_without_backtracking() {
|
||||
// `?` 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"));
|
||||
// Greek sigma matches in every case form, as the case-insensitive regular expression did.
|
||||
let r = ok(
|
||||
&mut s,
|
||||
"[__globMatch('\\u03bf\\u03c2.txt', '\\u039f\\u03a3.txt'), __globMatch('\\u03a3*', '\\u03c2x'), __globMatch('k', '\\u212a'), __globMatch('a', 'b')].join()",
|
||||
);
|
||||
assert_eq!(r.result.as_str(), Some("true,true,false,false"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user