mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-01-06 19:19:44 +03:00
LibRegex: Allow references to capture groups that aren't parsed yet
This only applies to the ECMA262 parser. This behaviour is an ECMA262-specific quirk, such references always generate zero-length matches (even on subsequent passes). Also adds a test in LibJS's test suite. Fixes #6039.
This commit is contained in:
parent
804ab79995
commit
6bbb26fdaf
Notes:
sideshowbarker
2024-07-18 20:55:08 +09:00
Author: https://github.com/alimpfard Commit: https://github.com/SerenityOS/serenity/commit/6bbb26fdaf1 Pull-request: https://github.com/SerenityOS/serenity/pull/6057 Issue: https://github.com/SerenityOS/serenity/issues/6039
@ -66,3 +66,15 @@ test("not matching", () => {
|
||||
|
||||
expect(res).toBe(null);
|
||||
});
|
||||
|
||||
// Backreference to a group not yet parsed: #6039
|
||||
test("Future group backreference, #6039", () => {
|
||||
let re = /(\3)(\1)(a)/;
|
||||
let result = re.exec("cat");
|
||||
expect(result.length).toBe(4);
|
||||
expect(result[0]).toBe("a");
|
||||
expect(result[1]).toBe("");
|
||||
expect(result[2]).toBe("");
|
||||
expect(result[3]).toBe("a");
|
||||
expect(result.index).toBe(1);
|
||||
});
|
||||
|
@ -399,6 +399,7 @@ ALWAYS_INLINE ExecutionResult OpCode_Compare::execute(const MatchInput& input, M
|
||||
|
||||
size_t string_position = state.string_position;
|
||||
bool inverse_matched { false };
|
||||
bool had_zero_length_match { false };
|
||||
|
||||
size_t offset { state.instruction_position + 3 };
|
||||
for (size_t i = 0; i < arguments_count(); ++i) {
|
||||
@ -454,7 +455,7 @@ ALWAYS_INLINE ExecutionResult OpCode_Compare::execute(const MatchInput& input, M
|
||||
if (input.view.length() - state.string_position < length)
|
||||
return ExecutionResult::Failed_ExecuteLowPrioForks;
|
||||
|
||||
if (!compare_string(input, state, str_builder.string_view().characters_without_null_termination(), length))
|
||||
if (!compare_string(input, state, str_builder.string_view().characters_without_null_termination(), length, had_zero_length_match))
|
||||
return ExecutionResult::Failed_ExecuteLowPrioForks;
|
||||
|
||||
} else if (compare_type == CharacterCompareType::CharClass) {
|
||||
@ -488,7 +489,7 @@ ALWAYS_INLINE ExecutionResult OpCode_Compare::execute(const MatchInput& input, M
|
||||
if (input.view.length() - state.string_position < str.length())
|
||||
return ExecutionResult::Failed_ExecuteLowPrioForks;
|
||||
|
||||
if (!compare_string(input, state, str.characters_without_null_termination(), str.length()))
|
||||
if (!compare_string(input, state, str.characters_without_null_termination(), str.length(), had_zero_length_match))
|
||||
return ExecutionResult::Failed_ExecuteLowPrioForks;
|
||||
|
||||
} else if (compare_type == CharacterCompareType::NamedReference) {
|
||||
@ -506,7 +507,7 @@ ALWAYS_INLINE ExecutionResult OpCode_Compare::execute(const MatchInput& input, M
|
||||
if (input.view.length() - state.string_position < str.length())
|
||||
return ExecutionResult::Failed_ExecuteLowPrioForks;
|
||||
|
||||
if (!compare_string(input, state, str.characters_without_null_termination(), str.length()))
|
||||
if (!compare_string(input, state, str.characters_without_null_termination(), str.length(), had_zero_length_match))
|
||||
return ExecutionResult::Failed_ExecuteLowPrioForks;
|
||||
|
||||
} else {
|
||||
@ -519,7 +520,7 @@ ALWAYS_INLINE ExecutionResult OpCode_Compare::execute(const MatchInput& input, M
|
||||
if (current_inversion_state() && !inverse_matched)
|
||||
++state.string_position;
|
||||
|
||||
if (string_position == state.string_position || state.string_position > input.view.length())
|
||||
if ((!had_zero_length_match && string_position == state.string_position) || state.string_position > input.view.length())
|
||||
return ExecutionResult::Failed_ExecuteLowPrioForks;
|
||||
|
||||
return ExecutionResult::Continue;
|
||||
@ -542,7 +543,7 @@ ALWAYS_INLINE void OpCode_Compare::compare_char(const MatchInput& input, MatchSt
|
||||
}
|
||||
}
|
||||
|
||||
ALWAYS_INLINE bool OpCode_Compare::compare_string(const MatchInput& input, MatchState& state, const char* str, size_t length)
|
||||
ALWAYS_INLINE bool OpCode_Compare::compare_string(const MatchInput& input, MatchState& state, const char* str, size_t length, bool& had_zero_length_match)
|
||||
{
|
||||
if (input.view.is_u8_view()) {
|
||||
auto str_view1 = StringView(str, length);
|
||||
@ -558,6 +559,8 @@ ALWAYS_INLINE bool OpCode_Compare::compare_string(const MatchInput& input, Match
|
||||
|
||||
if (str_view1 == str_view2) {
|
||||
state.string_position += length;
|
||||
if (length == 0)
|
||||
had_zero_length_match = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -768,7 +768,7 @@ public:
|
||||
|
||||
private:
|
||||
ALWAYS_INLINE static void compare_char(const MatchInput& input, MatchState& state, u32 ch1, bool inverse, bool& inverse_matched);
|
||||
ALWAYS_INLINE static bool compare_string(const MatchInput& input, MatchState& state, const char* str, size_t length);
|
||||
ALWAYS_INLINE static bool compare_string(const MatchInput& input, MatchState& state, const char* str, size_t length, bool& had_zero_length_match);
|
||||
ALWAYS_INLINE static void compare_character_class(const MatchInput& input, MatchState& state, CharClass character_class, u32 ch, bool inverse, bool& inverse_matched);
|
||||
ALWAYS_INLINE static void compare_character_range(const MatchInput& input, MatchState& state, u32 from, u32 to, u32 ch, bool inverse, bool& inverse_matched);
|
||||
};
|
||||
|
@ -93,6 +93,7 @@ public:
|
||||
void set_source(const StringView source) { m_source = source; }
|
||||
bool try_skip(char);
|
||||
char skip();
|
||||
const auto& source() const { return m_source; }
|
||||
|
||||
private:
|
||||
ALWAYS_INLINE char peek(size_t offset = 0) const;
|
||||
|
@ -1114,12 +1114,19 @@ bool ECMA262Parser::parse_atom_escape(ByteCode& stack, size_t& match_length_mini
|
||||
{
|
||||
if (auto escape_str = read_digits_as_string(ReadDigitsInitialZeroState::Disallow, ReadDigitFollowPolicy::DisallowNonDigit); !escape_str.is_empty()) {
|
||||
if (auto escape = escape_str.to_uint(); escape.has_value()) {
|
||||
// See if this is a "back"-reference (we've already parsed the group it refers to)
|
||||
auto maybe_length = m_parser_state.capture_group_minimum_lengths.get(escape.value());
|
||||
if (maybe_length.has_value()) {
|
||||
match_length_minimum += maybe_length.value();
|
||||
stack.insert_bytecode_compare_values({ { CharacterCompareType::Reference, (ByteCodeValueType)escape.value() } });
|
||||
return true;
|
||||
}
|
||||
// It's not a pattern seen before, so we have to see if it's a valid reference to a future group.
|
||||
if (escape.value() <= ensure_total_number_of_capturing_parenthesis()) {
|
||||
// This refers to a future group, and it will _always_ be matching an empty string
|
||||
// So just match nothing and move on.
|
||||
return true;
|
||||
}
|
||||
if (!m_should_use_browser_extended_grammar) {
|
||||
set_error(Error::InvalidNumber);
|
||||
return false;
|
||||
@ -1729,4 +1736,47 @@ bool ECMA262Parser::parse_capture_group(ByteCode& stack, size_t& match_length_mi
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t ECMA262Parser::ensure_total_number_of_capturing_parenthesis()
|
||||
{
|
||||
if (m_total_number_of_capturing_parenthesis.has_value())
|
||||
return m_total_number_of_capturing_parenthesis.value();
|
||||
|
||||
GenericLexer lexer { m_parser_state.lexer.source() };
|
||||
size_t count = 0;
|
||||
while (!lexer.is_eof()) {
|
||||
switch (lexer.peek()) {
|
||||
case '\\':
|
||||
lexer.consume(2);
|
||||
continue;
|
||||
case '[':
|
||||
while (!lexer.is_eof()) {
|
||||
if (lexer.consume_specific('\\'))
|
||||
lexer.consume();
|
||||
else if (lexer.consume_specific(']'))
|
||||
break;
|
||||
lexer.consume();
|
||||
}
|
||||
break;
|
||||
case '(':
|
||||
if (lexer.consume_specific('?')) {
|
||||
// non-capturing group '(?:', lookaround '(?<='/'(?<!', or named capture '(?<'
|
||||
if (!lexer.consume_specific('<'))
|
||||
break;
|
||||
|
||||
if (lexer.next_is(is_any_of("=!")))
|
||||
break;
|
||||
|
||||
++count;
|
||||
} else {
|
||||
++count;
|
||||
}
|
||||
break;
|
||||
}
|
||||
lexer.consume();
|
||||
}
|
||||
|
||||
m_total_number_of_capturing_parenthesis = count;
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
@ -210,6 +210,14 @@ private:
|
||||
bool parse_legacy_octal_escape_sequence(ByteCode& bytecode_stack, size_t& length);
|
||||
Optional<u8> parse_legacy_octal_escape();
|
||||
|
||||
size_t ensure_total_number_of_capturing_parenthesis();
|
||||
|
||||
// ECMA-262's flavour of regex is a bit weird in that it allows backrefs to reference "future" captures, and such backrefs
|
||||
// always match the empty string. So we have to know how many capturing parenthesis there are, but we don't want to always
|
||||
// parse it twice, so we'll just do so when it's actually needed.
|
||||
// Most patterns should have no need to ever populate this field.
|
||||
Optional<size_t> m_total_number_of_capturing_parenthesis;
|
||||
|
||||
// Keep the Annex B. behaviour behind a flag, the users can enable it by passing the `ECMAScriptFlags::BrowserExtended` flag.
|
||||
bool m_should_use_browser_extended_grammar { false };
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user