Skip to content

Commit c0a8dd4

Browse files
aplefullgmta
authored andcommitted
LibRegex: Reject invalid /v class set expressions in negated classes
Some string literals were incorrectly accepted inside negated classes because the negated class check computed the actual result string set instead of applying the structural rules. This commit replaces class_set_expression_strings with a structural class_set_expression_may_contain_strings check. Also removes some incorrect tests.
1 parent 201c129 commit c0a8dd4

4 files changed

Lines changed: 55 additions & 85 deletions

File tree

Libraries/LibRegex/Rust/src/parser.rs

Lines changed: 21 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1182,7 +1182,7 @@ impl Parser {
11821182
self.parse_class_set_expression()?
11831183
};
11841184
self.in_negated_class = saved_negated;
1185-
if negated && !Self::class_set_expression_strings(&expr).is_empty() {
1185+
if negated && Self::class_set_expression_may_contain_strings(&expr) {
11861186
return Err(Error::InvalidCharacterClass);
11871187
}
11881188
self.expect(']')?;
@@ -1413,6 +1413,9 @@ impl Parser {
14131413
self.in_negated_class = false;
14141414
while self.peek_pair() == Some(('&', '&')) {
14151415
self.pos += 2; // consume '&&'
1416+
if self.peek() == Some('&') {
1417+
return Err(Error::InvalidCharacterClass);
1418+
}
14161419
operands.push(self.parse_class_set_operand()?);
14171420
}
14181421
self.in_negated_class = saved_negated;
@@ -1454,93 +1457,38 @@ impl Parser {
14541457
Ok(())
14551458
}
14561459

1457-
fn get_string_property_strings(name: &str) -> std::collections::BTreeSet<Vec<u32>> {
1458-
let buf = libunicode_rust::character_types::get_string_property_data(name);
1459-
if buf.is_empty() {
1460-
return std::collections::BTreeSet::new();
1461-
}
1462-
1463-
let count = buf[0] as usize;
1464-
let mut strings = std::collections::BTreeSet::new();
1465-
let mut offset = 1;
1466-
for _ in 0..count {
1467-
if offset >= buf.len() {
1468-
break;
1469-
}
1470-
let len = buf[offset] as usize;
1471-
offset += 1;
1472-
if offset + len > buf.len() {
1473-
break;
1474-
}
1475-
if len > 1 {
1476-
strings.insert(buf[offset..offset + len].to_vec());
1477-
}
1478-
offset += len;
1479-
}
1480-
strings
1481-
}
1482-
1483-
fn class_set_expression_strings(expr: &ClassSetExpression) -> std::collections::BTreeSet<Vec<u32>> {
1460+
/// - Union: may contain strings if ANY operand may contain strings
1461+
/// - Intersection: may contain strings if ALL operands may contain strings
1462+
/// (i.e., no operand is "purely characters")
1463+
/// - Subtraction: may contain strings if the leftmost operand may contain strings
1464+
fn class_set_expression_may_contain_strings(expr: &ClassSetExpression) -> bool {
14841465
match expr {
1485-
ClassSetExpression::Union(operands) => {
1486-
operands
1487-
.iter()
1488-
.fold(std::collections::BTreeSet::new(), |mut strings, operand| {
1489-
strings.extend(Self::class_set_operand_strings(operand));
1490-
strings
1491-
})
1492-
}
1466+
ClassSetExpression::Union(operands) => operands.iter().any(Self::class_set_operand_may_contain_strings),
14931467
ClassSetExpression::Intersection(operands) => {
1494-
let Some((first, rest)) = operands.split_first() else {
1495-
return std::collections::BTreeSet::new();
1496-
};
1497-
let mut strings = Self::class_set_operand_strings(first);
1498-
for operand in rest {
1499-
let operand_strings = Self::class_set_operand_strings(operand);
1500-
strings.retain(|string| operand_strings.contains(string));
1501-
}
1502-
strings
1503-
}
1504-
ClassSetExpression::Subtraction(operands) => {
1505-
let Some((first, rest)) = operands.split_first() else {
1506-
return std::collections::BTreeSet::new();
1507-
};
1508-
let mut strings = Self::class_set_operand_strings(first);
1509-
for operand in rest {
1510-
let operand_strings = Self::class_set_operand_strings(operand);
1511-
strings.retain(|string| !operand_strings.contains(string));
1512-
}
1513-
strings
1468+
operands.iter().all(Self::class_set_operand_may_contain_strings)
15141469
}
1470+
ClassSetExpression::Subtraction(operands) => operands
1471+
.first()
1472+
.is_some_and(Self::class_set_operand_may_contain_strings),
15151473
}
15161474
}
15171475

1518-
fn class_set_operand_strings(operand: &ClassSetOperand) -> std::collections::BTreeSet<Vec<u32>> {
1476+
fn class_set_operand_may_contain_strings(operand: &ClassSetOperand) -> bool {
15191477
match operand {
15201478
ClassSetOperand::NestedClass(class) => {
15211479
if class.negated {
1522-
return std::collections::BTreeSet::new();
1480+
return false;
15231481
}
15241482
match &class.body {
1525-
CharacterClassBody::Ranges(_) => std::collections::BTreeSet::new(),
1526-
CharacterClassBody::UnicodeSet(expr) => Self::class_set_expression_strings(expr),
1483+
CharacterClassBody::Ranges(_) => false,
1484+
CharacterClassBody::UnicodeSet(expr) => Self::class_set_expression_may_contain_strings(expr),
15271485
}
15281486
}
15291487
ClassSetOperand::UnicodeProperty(property) => {
1530-
if !property.negated && property.value.is_none() && Self::is_string_property(&property.name) {
1531-
return Self::get_string_property_strings(&property.name);
1532-
}
1533-
std::collections::BTreeSet::new()
1534-
}
1535-
ClassSetOperand::StringLiteral(chars) => {
1536-
if chars.len() > 1 {
1537-
return [chars.iter().map(|ch| *ch as u32).collect()].into_iter().collect();
1538-
}
1539-
std::collections::BTreeSet::new()
1540-
}
1541-
ClassSetOperand::Char(_) | ClassSetOperand::Range(_, _) | ClassSetOperand::BuiltinClass(_) => {
1542-
std::collections::BTreeSet::new()
1488+
!property.negated && property.value.is_none() && Self::is_string_property(&property.name)
15431489
}
1490+
ClassSetOperand::StringLiteral(chars) => chars.len() != 1,
1491+
ClassSetOperand::Char(_) | ClassSetOperand::Range(_, _) | ClassSetOperand::BuiltinClass(_) => false,
15441492
}
15451493
}
15461494

Tests/LibJS/Runtime/builtins/RegExp/RegExp.js

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,7 @@ describe("errors", () => {
3636
});
3737

3838
test("valid pattern (negated v-mode class set ops can eliminate strings)", () => {
39-
for (const pattern of [
40-
"[^[[a-z]--[\\q{ab}]]]",
41-
"[^[[\\q{ab}]&&[a-z]]]",
42-
"[^[[\\q{ab}]--[\\q{ab}]]]",
43-
"[^[[\\q{ab}]&&[\\q{cd}]]]",
44-
]) {
39+
for (const pattern of ["[^[[a-z]--[\\q{ab}]]]", "[^[[\\q{ab}]&&[a-z]]]"]) {
4540
expect(() => {
4641
RegExp(pattern, "v");
4742
}).not.toThrow();
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
test("triple & in unicode sets class is a syntax error", () => {
2+
expect(() => eval("/[a&&&]/v")).toThrow(SyntaxError);
3+
expect(() => eval("/[a&&b&&&c]/v")).toThrow(SyntaxError);
4+
});
5+
6+
test("empty \\q{} in negated unicode sets class is a syntax error", () => {
7+
expect(() => eval("/[^\\q{}]/v")).toThrow(SyntaxError);
8+
});
9+
10+
test("multi-char string intersection where all operands contain strings is a syntax error in negated class", () => {
11+
expect(() => eval("/[^\\q{foo}&&\\q{bar}]/v")).toThrow(SyntaxError);
12+
expect(() => eval("/[^\\q{foo}&&\\q{foo}]/v")).toThrow(SyntaxError);
13+
});
14+
15+
test("multi-char string in negated unicode sets class union is a syntax error", () => {
16+
expect(() => eval("/[^\\q{ab}]/v")).toThrow(SyntaxError);
17+
expect(() => eval("/[^\\q{foo}]/v")).toThrow(SyntaxError);
18+
});
19+
20+
test("subtraction with string-valued left operand is a syntax error in negated class", () => {
21+
expect(() => eval("/[^\\q{foo}--[a-z]]/v")).toThrow(SyntaxError);
22+
});
23+
24+
test("valid unicode sets class set operations in negated class", () => {
25+
expect(() => eval("/[^\\q{a}]/v")).not.toThrow();
26+
expect(() => eval("/[^\\q{foo}&&[a]]/v")).not.toThrow();
27+
expect(() => eval("/[^\\q{a}&&\\q{b}]/v")).not.toThrow();
28+
expect(() => eval("/[^\\q{a}&&[a-z]]/v")).not.toThrow();
29+
expect(() => eval("/[^[a-z]--\\q{foo}]/v")).not.toThrow();
30+
expect(() => eval("/[a&&b]/v")).not.toThrow();
31+
expect(() => eval("/[a&&b&&c]/v")).not.toThrow();
32+
});

Tests/LibJS/Runtime/syntax/regex-literal-errors.js

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,7 @@ test("negated v-mode classes containing nested strings are syntax errors", () =>
8989
});
9090

9191
test("negated v-mode class set ops that eliminate strings are valid", () => {
92-
for (const source of [
93-
"/[^[[a-z]--[\\q{ab}]]]/v",
94-
"/[^[[\\q{ab}]&&[a-z]]]/v",
95-
"/[^[[\\q{ab}]--[\\q{ab}]]]/v",
96-
"/[^[[\\q{ab}]&&[\\q{cd}]]]/v",
97-
]) {
92+
for (const source of ["/[^[[a-z]--[\\q{ab}]]]/v", "/[^[[\\q{ab}]&&[a-z]]]/v"]) {
9893
expect(source).toEval();
9994
expect(() => eval(source)).not.toThrow();
10095
expect(() => new Function(source)).not.toThrow();

0 commit comments

Comments
 (0)