diff --git a/src/core/regex/CMakeLists.txt b/src/core/regex/CMakeLists.txt index 3e956ae35..958144e9d 100644 --- a/src/core/regex/CMakeLists.txt +++ b/src/core/regex/CMakeLists.txt @@ -1,5 +1,5 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME regex - SOURCES regex.cc preprocess.h) + SOURCES regex.cc iregexp.h permissive.h) if(SOURCEMETA_CORE_INSTALL) sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME regex) diff --git a/src/core/regex/include/sourcemeta/core/regex.h b/src/core/regex/include/sourcemeta/core/regex.h index d2b2c618c..d9e1f5b1c 100644 --- a/src/core/regex/include/sourcemeta/core/regex.h +++ b/src/core/regex/include/sourcemeta/core/regex.h @@ -73,7 +73,13 @@ enum class RegexIndex : std::uint8_t { /// The dialects that a regular expression pattern can be interpreted with. enum class RegexDialect : std::uint8_t { /// A permissive superset of ECMA 262 with PCRE2 extensions - Permissive + Permissive, + /// Strict RFC 9485 I-Regexp, where any pattern outside the grammar is + /// rejected and matching considers the whole input. Patterns are + /// interpreted through the RFC 9485 Section 5.4 engine mapping, so an + /// unescaped caret or dollar acts as an assertion instead of a literal + /// and rejects quantification + IRegexp }; /// @ingroup regex @@ -83,6 +89,7 @@ enum class RegexDialect : std::uint8_t { /// /// - Permissive regexes are NOT automatically anchored /// - Permissive regexes assume `DOTALL` +/// - RFC 9485 regexes match the whole input /// - Regexes assume Unicode /// - Regexes are case sensitive /// - No matching happens (only boolean validation) diff --git a/src/core/regex/iregexp.h b/src/core/regex/iregexp.h new file mode 100644 index 000000000..dc33d3481 --- /dev/null +++ b/src/core/regex/iregexp.h @@ -0,0 +1,387 @@ +#ifndef SOURCEMETA_CORE_REGEX_IREGEXP_H_ +#define SOURCEMETA_CORE_REGEX_IREGEXP_H_ + +#include + +#include // std::from_chars +#include // std::size_t +#include // std::uint32_t +#include // std::optional, std::nullopt +#include // std::string, std::to_string +#include // std::string_view +#include // std::errc + +namespace sourcemeta::core { + +namespace { + +// RFC 9485 Section 8 permits limiting the composability of patterns that +// challenge the engine. Grouping recurses, so depth is capped to keep +// attacker controlled patterns from overflowing the stack +constexpr std::size_t IREGEXP_MAXIMUM_GROUP_DEPTH{64}; + +// SingleCharEsc = "\" ( %x28-2B ; '('-'+' +// / "-" / "." / "?" / %x5B-5E ; '['-'^' +// / %s"n" / %s"r" / %s"t" / %x7B-7D ; '{'-'}' ) +inline auto is_iregexp_single_char_escape(const char character) -> bool { + return (character >= 0x28 && character <= 0x2B) || character == '-' || + character == '.' || character == '?' || + (character >= 0x5B && character <= 0x5E) || character == 'n' || + character == 'r' || character == 't' || + (character >= 0x7B && character <= 0x7D); +} + +// charProp = IsCategory, where the Others production is %s"C" with an +// optional second letter drawn from c, f, n, and o, so the bare major +// category conforms while the surrogate form does not. Accepting the major +// category cannot reintroduce surrogates, as they have no representation in +// well formed UTF-8 input +inline auto is_iregexp_category(const std::string_view name) -> bool { + if (name.empty() || name.size() > 2) { + return false; + } + + switch (name.front()) { + case 'L': + return name.size() == 1 || std::string_view{"lmotu"}.contains(name[1]); + case 'M': + return name.size() == 1 || std::string_view{"cen"}.contains(name[1]); + case 'N': + return name.size() == 1 || std::string_view{"dlo"}.contains(name[1]); + case 'P': + return name.size() == 1 || std::string_view{"cdefios"}.contains(name[1]); + case 'Z': + return name.size() == 1 || std::string_view{"lps"}.contains(name[1]); + case 'S': + return name.size() == 1 || std::string_view{"ckmo"}.contains(name[1]); + case 'C': + return name.size() == 1 || std::string_view{"cfno"}.contains(name[1]); + default: + return false; + } +} + +// Copy one full UTF-8 encoded code point, rejecting malformed sequences. +// The engine later rejects overlong forms and surrogates when compiling +inline auto iregexp_copy_character(const std::string_view pattern, + std::size_t &position, std::string &output) + -> bool { + const auto lead{static_cast(pattern[position])}; + const auto size{utf8_lead_byte_size(lead)}; + if (size == 0 || position + size > pattern.size()) { + return false; + } + + for (std::size_t offset{1}; offset < size; ++offset) { + if (!is_utf8_continuation( + static_cast(pattern[position + offset]))) { + return false; + } + } + + output.append(pattern.substr(position, size)); + position += size; + return true; +} + +// charClassEsc = catEsc / complEsc, which along with SingleCharEsc are the +// only escape forms in the entire grammar +inline auto iregexp_escape(const std::string_view pattern, + std::size_t &position, std::string &output) -> bool { + if (position + 1 >= pattern.size()) { + return false; + } + + const char next{pattern[position + 1]}; + if (is_iregexp_single_char_escape(next)) { + output += '\\'; + output += next; + position += 2; + return true; + } + + // catEsc = %s"\p{" charProp "}" and complEsc = %s"\P{" charProp "}" + if (next == 'p' || next == 'P') { + if (position + 2 >= pattern.size() || pattern[position + 2] != '{') { + return false; + } + + const auto closing{pattern.find('}', position + 3)}; + if (closing == std::string_view::npos) { + return false; + } + + const auto name{pattern.substr(position + 3, closing - position - 3)}; + if (!is_iregexp_category(name)) { + return false; + } + + output.append(pattern.substr(position, closing - position + 1)); + position = closing + 1; + return true; + } + + return false; +} + +// CCchar = ( %x00-2C / %x2E-5A / %x5E-D7FF / %xE000-10FFFF ) / SingleCharEsc, +// which excludes exactly the dash, both brackets, and the backslash +inline auto iregexp_class_character(const std::string_view pattern, + std::size_t &position, std::string &output) + -> bool { + const char character{pattern[position]}; + if (character == '\\') { + if (position + 1 < pattern.size() && + is_iregexp_single_char_escape(pattern[position + 1])) { + output += '\\'; + output += pattern[position + 1]; + position += 2; + return true; + } + + return false; + } + + if (character == '-' || character == '[' || character == ']') { + return false; + } + + return iregexp_copy_character(pattern, position, output); +} + +// CCE1 = ( CCchar [ "-" CCchar ] ) / charClassEsc +inline auto iregexp_class_element(const std::string_view pattern, + std::size_t &position, std::string &output) + -> bool { + if (pattern[position] == '\\' && position + 1 < pattern.size() && + (pattern[position + 1] == 'p' || pattern[position + 1] == 'P')) { + return iregexp_escape(pattern, position, output); + } + + if (!iregexp_class_character(pattern, position, output)) { + return false; + } + + // A dash followed by a closing bracket is the optional trailing dash of + // the enclosing class expression, not a range + if (position + 1 < pattern.size() && pattern[position] == '-' && + pattern[position + 1] != ']') { + output += '-'; + position += 1; + return iregexp_class_character(pattern, position, output); + } + + return true; +} + +// charClassExpr = "[" [ "^" ] ( "-" / CCE1 ) *CCE1 [ "-" ] "]", with the +// prose restriction that it is not allowed to match "[^]" +inline auto iregexp_class_expression(const std::string_view pattern, + std::size_t &position, std::string &output) + -> bool { + output += '['; + position += 1; + if (position < pattern.size() && pattern[position] == '^') { + output += '^'; + position += 1; + } + + if (position < pattern.size() && pattern[position] == '-') { + output += '-'; + position += 1; + } else { + if (position >= pattern.size() || pattern[position] == ']') { + return false; + } + + if (!iregexp_class_element(pattern, position, output)) { + return false; + } + } + + while (position < pattern.size() && pattern[position] != ']' && + pattern[position] != '-') { + if (!iregexp_class_element(pattern, position, output)) { + return false; + } + } + + if (position < pattern.size() && pattern[position] == '-') { + output += '-'; + position += 1; + } + + if (position >= pattern.size() || pattern[position] != ']') { + return false; + } + + output += ']'; + position += 1; + return true; +} + +// quantifier = ( "*" / "+" / "?" ) / range-quantifier +// range-quantifier = "{" QuantExact [ "," [ QuantExact ] ] "}" +inline auto iregexp_quantifier(const std::string_view pattern, + std::size_t &position, std::string &output) + -> bool { + const char character{pattern[position]}; + if (character == '*' || character == '+' || character == '?') { + output += character; + position += 1; + return true; + } + + const auto minimum_begin{position + 1}; + auto cursor{minimum_begin}; + while (cursor < pattern.size() && pattern[cursor] >= '0' && + pattern[cursor] <= '9') { + cursor += 1; + } + + // QuantExact = 1*%x30-39, so at least one digit is required. Counts that + // overflow the representable range are rejected, as RFC 9485 Section 8 + // permits for quantifiers of excessive magnitude + if (cursor == minimum_begin) { + return false; + } + + std::uint32_t minimum{0}; + const auto minimum_result{std::from_chars(pattern.data() + minimum_begin, + pattern.data() + cursor, minimum)}; + if (minimum_result.ec != std::errc{}) { + return false; + } + + output += '{'; + output += std::to_string(minimum); + if (cursor < pattern.size() && pattern[cursor] == ',') { + output += ','; + cursor += 1; + const auto maximum_begin{cursor}; + while (cursor < pattern.size() && pattern[cursor] >= '0' && + pattern[cursor] <= '9') { + cursor += 1; + } + + if (cursor > maximum_begin) { + std::uint32_t maximum{0}; + const auto maximum_result{std::from_chars( + pattern.data() + maximum_begin, pattern.data() + cursor, maximum)}; + // Out of order bounds are an error, consistent with how the permissive + // dialect rejects them for ECMA 262 + if (maximum_result.ec != std::errc{} || minimum > maximum) { + return false; + } + + output += std::to_string(maximum); + } + } + + if (cursor >= pattern.size() || pattern[cursor] != '}') { + return false; + } + + output += '}'; + position = cursor + 1; + return true; +} + +// i-regexp = branch *( "|" branch ) with branch = *piece, so empty branches +// conform. The caller at depth zero owns the whole input while group +// recursion stops at the closing parenthesis for its caller to consume +inline auto iregexp_branches(const std::string_view pattern, + std::size_t &position, std::string &output, + const std::size_t depth) -> bool { + // piece = atom [ quantifier ], so a quantifier is only valid directly + // after a quantifiable atom and never twice in a row + bool quantifiable{false}; + while (position < pattern.size()) { + const char character{pattern[position]}; + if (character == '|') { + output += '|'; + position += 1; + quantifiable = false; + } else if (character == ')') { + return depth > 0; + } else if (character == '(') { + if (depth >= IREGEXP_MAXIMUM_GROUP_DEPTH) { + return false; + } + + output += '('; + position += 1; + if (!iregexp_branches(pattern, position, output, depth + 1)) { + return false; + } + + if (position >= pattern.size() || pattern[position] != ')') { + return false; + } + + output += ')'; + position += 1; + quantifiable = true; + } else if (character == '*' || character == '+' || character == '?' || + character == '{') { + if (!quantifiable || !iregexp_quantifier(pattern, position, output)) { + return false; + } + + quantifiable = false; + } else if (character == '.') { + // RFC 9485 Section 5.3: for any unescaped dots outside character + // classes, "replace the dot with [^\n\r]" + output += "[^\\n\\r]"; + position += 1; + quantifiable = true; + } else if (character == '[') { + if (!iregexp_class_expression(pattern, position, output)) { + return false; + } + + quantifiable = true; + } else if (character == '\\') { + if (!iregexp_escape(pattern, position, output)) { + return false; + } + + quantifiable = true; + } else if (character == ']' || character == '}') { + // NormalChar excludes both closing delimiters, so they may only + // appear escaped + return false; + } else { + if (!iregexp_copy_character(pattern, position, output)) { + return false; + } + + quantifiable = true; + } + } + + return depth == 0; +} + +} // namespace + +// Validate a pattern against the RFC 9485 grammar and produce the equivalent +// engine pattern following the RFC 9485 Section 5.4 mapping, which encloses +// the translation "in \A(?: and )\z" +inline auto translate_iregexp(const std::string_view pattern) + -> std::optional { + std::string result; + result.reserve(pattern.size() + 8); + result += "\\A(?:"; + std::size_t position{0}; + if (!iregexp_branches(pattern, position, result, 0)) { + return std::nullopt; + } + + result += ")\\z"; + return result; +} + +} // namespace sourcemeta::core + +#endif diff --git a/src/core/regex/preprocess.h b/src/core/regex/permissive.h similarity index 98% rename from src/core/regex/preprocess.h rename to src/core/regex/permissive.h index 447c6172d..3898fffb3 100644 --- a/src/core/regex/preprocess.h +++ b/src/core/regex/permissive.h @@ -1,5 +1,5 @@ -#ifndef SOURCEMETA_CORE_REGEX_PREPROCESS_H_ -#define SOURCEMETA_CORE_REGEX_PREPROCESS_H_ +#ifndef SOURCEMETA_CORE_REGEX_PERMISSIVE_H_ +#define SOURCEMETA_CORE_REGEX_PERMISSIVE_H_ #include @@ -632,8 +632,9 @@ inline auto find_shorthand(char escape) -> const ShorthandExpansion * { } // namespace -// The result of preprocessing a regex pattern into PCRE2-compatible form. -struct PreprocessResult { +// The result of translating a permissive dialect pattern into +// PCRE2-compatible form +struct PermissiveResult { // True if the input pattern is strict ECMA-262. bool ecma_valid; // The PCRE2-compatible transformed pattern, if any. @@ -667,7 +668,8 @@ inline auto exceeds_class_depth(const std::string &pattern) -> bool { return false; } -inline auto preprocess_regex(const std::string &pattern) -> PreprocessResult { +inline auto translate_permissive(const std::string &pattern) + -> PermissiveResult { if (exceeds_class_depth(pattern)) { return {.ecma_valid = false, .transformed = std::nullopt}; } diff --git a/src/core/regex/regex.cc b/src/core/regex/regex.cc index b4e667bf1..9a26db2c8 100644 --- a/src/core/regex/regex.cc +++ b/src/core/regex/regex.cc @@ -3,10 +3,12 @@ #include -#include "preprocess.h" +#include "iregexp.h" +#include "permissive.h" #include // std::from_chars #include // std::size_t +#include // std::uint32_t #include // std::regex, std::smatch, std::regex_match #include // std::string #include // std::string_view @@ -15,9 +17,44 @@ namespace sourcemeta::core { -auto to_regex(const std::string_view pattern, - [[maybe_unused]] const RegexDialect dialect) +namespace { + +auto compile_pcre2(const std::string &pattern, const std::uint32_t options) -> std::optional { + int pcre2_error_code{0}; + PCRE2_SIZE pcre2_error_offset{0}; + pcre2_code *pcre2_regex_raw{pcre2_compile( + reinterpret_cast(pattern.c_str()), pattern.size(), options, + &pcre2_error_code, &pcre2_error_offset, nullptr)}; + if (pcre2_regex_raw == nullptr) { + return std::nullopt; + } + + std::shared_ptr pcre2_regex{pcre2_regex_raw, pcre2_code_free}; + pcre2_jit_compile(pcre2_regex.get(), PCRE2_JIT_COMPLETE); + return RegexTypePCRE2{std::shared_ptr(pcre2_regex)}; +} + +} // namespace + +auto to_regex(const std::string_view pattern, const RegexDialect dialect) + -> std::optional { + if (dialect == RegexDialect::IRegexp) { + const auto translated{translate_iregexp(pattern)}; + if (!translated.has_value()) { + return std::nullopt; + } + + // Grouping in RFC 9485 carries no capturing semantics, as matching is + // strictly Boolean, so capturing is disabled altogether. The dollar + // option keeps a trailing dollar from also matching before a final + // newline, mirroring the ECMA 262 interpretation that the RFC mapping + // is defined against + return compile_pcre2(translated.value(), + PCRE2_UTF | PCRE2_UCP | PCRE2_DOLLAR_ENDONLY | + PCRE2_NO_AUTO_CAPTURE | PCRE2_MATCH_INVALID_UTF); + } + if (pattern == ".*" || pattern == "^.*$" || pattern == "^(.*)$" || pattern == "(.*)" || pattern == "[\\s\\S]*" || pattern == "^[\\s\\S]*$") { return RegexTypeNoop{}; @@ -66,30 +103,17 @@ auto to_regex(const std::string_view pattern, return RegexTypeRange{minimum, maximum}; } - const auto preprocessed{preprocess_regex(std::string{pattern})}; - if (!preprocessed.transformed.has_value()) { + const auto translated{translate_permissive(std::string{pattern})}; + if (!translated.transformed.has_value()) { return std::nullopt; } - int pcre2_error_code{0}; - PCRE2_SIZE pcre2_error_offset{0}; - pcre2_code *pcre2_regex_raw{pcre2_compile( - reinterpret_cast(preprocessed.transformed.value().c_str()), - preprocessed.transformed.value().size(), - // Capturing groups are kept enabled so that ECMA-262 numbered - // backreferences like (a)\1 compile and match, at a small tracking cost - PCRE2_UTF | PCRE2_UCP | PCRE2_DOTALL | PCRE2_DOLLAR_ENDONLY | - PCRE2_NEVER_BACKSLASH_C | PCRE2_MATCH_INVALID_UTF | - PCRE2_ALLOW_EMPTY_CLASS, - &pcre2_error_code, &pcre2_error_offset, nullptr)}; - - if (pcre2_regex_raw != nullptr) { - std::shared_ptr pcre2_regex{pcre2_regex_raw, pcre2_code_free}; - pcre2_jit_compile(pcre2_regex.get(), PCRE2_JIT_COMPLETE); - return RegexTypePCRE2{std::shared_ptr(pcre2_regex)}; - } - - return std::nullopt; + // Capturing groups are kept enabled so that ECMA-262 numbered + // backreferences like (a)\1 compile and match, at a small tracking cost + return compile_pcre2(translated.transformed.value(), + PCRE2_UTF | PCRE2_UCP | PCRE2_DOTALL | + PCRE2_DOLLAR_ENDONLY | PCRE2_NEVER_BACKSLASH_C | + PCRE2_MATCH_INVALID_UTF | PCRE2_ALLOW_EMPTY_CLASS); } auto matches(const Regex ®ex, const std::string_view value) -> bool { @@ -137,16 +161,16 @@ auto matches_if_valid(const std::string_view pattern, } auto is_regex_ecma(const std::string_view pattern) -> bool { - const auto preprocessed{preprocess_regex(std::string{pattern})}; - if (!preprocessed.ecma_valid || !preprocessed.transformed.has_value()) { + const auto translated{translate_permissive(std::string{pattern})}; + if (!translated.ecma_valid || !translated.transformed.has_value()) { return false; } int pcre2_error_code{0}; PCRE2_SIZE pcre2_error_offset{0}; pcre2_code *pcre2_regex_raw{pcre2_compile( - reinterpret_cast(preprocessed.transformed.value().c_str()), - preprocessed.transformed.value().size(), + reinterpret_cast(translated.transformed.value().c_str()), + translated.transformed.value().size(), // Capturing groups are kept enabled so that ECMA-262 numbered // backreferences like (a)\1 compile and match, at a small tracking cost PCRE2_UTF | PCRE2_UCP | PCRE2_DOTALL | PCRE2_DOLLAR_ENDONLY | diff --git a/test/regex/CMakeLists.txt b/test/regex/CMakeLists.txt index 38bfee8d1..9f1bd6434 100644 --- a/test/regex/CMakeLists.txt +++ b/test/regex/CMakeLists.txt @@ -3,6 +3,7 @@ sourcemeta_test(NAMESPACE sourcemeta PROJECT core NAME regex regex_matches_if_valid_test.cc regex_permissive_matches_ecma262_test.cc regex_permissive_matches_rfc9485_test.cc + regex_rfc9485_matches_test.cc regex_is_ecma_test.cc regex_to_regex_test.cc regex_test.cc) diff --git a/test/regex/regex_rfc9485_matches_test.cc b/test/regex/regex_rfc9485_matches_test.cc new file mode 100644 index 000000000..787982052 --- /dev/null +++ b/test/regex/regex_rfc9485_matches_test.cc @@ -0,0 +1,1118 @@ +#include +#include + +#include +#include + +TEST(rfc9485_matches_empty_pattern) { + const auto regex{ + sourcemeta::core::to_regex("", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "anything")); +} + +TEST(rfc9485_matches_literal_single) { + const auto regex{ + sourcemeta::core::to_regex("a", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "a")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "bar")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "ab")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "")); +} + +TEST(rfc9485_matches_literal_word) { + const auto regex{sourcemeta::core::to_regex( + "hello", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "hello")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "hello world")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "say hello")); +} + +TEST(rfc9485_matches_literal_astral) { + const auto regex{sourcemeta::core::to_regex( + "πŸ€”", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "πŸ€”")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "a")); +} + +TEST(rfc9485_matches_empty_branch_trailing) { + const auto regex{sourcemeta::core::to_regex( + "a|", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "a")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "xyz")); +} + +TEST(rfc9485_matches_empty_branch_leading) { + const auto regex{sourcemeta::core::to_regex( + "|b", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "b")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "xyz")); +} + +TEST(rfc9485_matches_empty_branches_only) { + const auto regex{sourcemeta::core::to_regex( + "||", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "x")); +} + +TEST(rfc9485_matches_mapped_leading_caret) { + const auto regex{sourcemeta::core::to_regex( + "^ab.*", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "ab")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "abc")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "^ab")); +} + +TEST(rfc9485_matches_mapped_trailing_dollar) { + const auto regex{sourcemeta::core::to_regex( + ".*bc$", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "abc")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "bc$")); +} + +TEST(rfc9485_matches_mapped_trailing_dollar_before_final_newline) { + const auto regex{sourcemeta::core::to_regex( + "ab$", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "ab")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "ab\n")); +} + +TEST(rfc9485_matches_mapped_middle_caret_never_matches) { + // NOTE: This test deviates from RFC 9485, which treats an unescaped caret + // as a literal character. We follow the RFC 9485 Section 5.4 engine + // mapping instead, where a caret in the middle of a pattern never matches + const auto regex{sourcemeta::core::to_regex( + "a^b", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "a^b")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "ab")); +} + +TEST(rfc9485_matches_mapped_middle_dollar_never_matches) { + // NOTE: This test deviates from RFC 9485, which treats an unescaped dollar + // as a literal character. We follow the RFC 9485 Section 5.4 engine + // mapping instead, where a dollar in the middle of a pattern never matches + const auto regex{sourcemeta::core::to_regex( + "a$b", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "a$b")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "ab")); +} + +TEST(rfc9485_matches_bare_comma) { + const auto regex{sourcemeta::core::to_regex( + "a,b", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "a,b")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "ab")); +} + +TEST(rfc9485_matches_bare_dash) { + const auto regex{sourcemeta::core::to_regex( + "a-b", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "a-b")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "ab")); +} + +TEST(rfc9485_matches_single_char_escapes_punctuation) { + const auto regex{sourcemeta::core::to_regex( + "\\(\\)\\*\\+\\.\\?\\[\\\\", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "()*+.?[\\")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "()*+.?[")); +} + +TEST(rfc9485_matches_single_char_escapes_delimiters) { + const auto regex{sourcemeta::core::to_regex( + "\\]\\{\\|\\}\\t\\r\\n", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "]{|}\t\r\n")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "]{|}trn")); +} + +TEST(rfc9485_matches_escaped_caret) { + const auto regex{sourcemeta::core::to_regex( + "a\\^b", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "a^b")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "ab")); +} + +TEST(rfc9485_matches_escaped_dash) { + const auto regex{sourcemeta::core::to_regex( + "a\\-b", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "a-b")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "ab")); +} + +TEST(rfc9485_matches_escaped_newline) { + const auto regex{sourcemeta::core::to_regex( + "a\\nb", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "a\nb")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "anb")); +} + +TEST(rfc9485_matches_quantifier_star) { + const auto regex{sourcemeta::core::to_regex( + "ab*", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "a")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "ab")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "abbb")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "b")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "xab")); +} + +TEST(rfc9485_matches_quantifier_plus) { + const auto regex{sourcemeta::core::to_regex( + "ab+", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "ab")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "abbb")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "a")); +} + +TEST(rfc9485_matches_quantifier_question) { + const auto regex{sourcemeta::core::to_regex( + "ab?", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "a")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "ab")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "abb")); +} + +TEST(rfc9485_matches_quantifier_exact) { + const auto regex{sourcemeta::core::to_regex( + "a{3}", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "aaa")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "aa")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "aaaa")); +} + +TEST(rfc9485_matches_quantifier_at_least) { + const auto regex{sourcemeta::core::to_regex( + "a{2,}", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "aa")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "aaaa")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "a")); +} + +TEST(rfc9485_matches_quantifier_bounded) { + const auto regex{sourcemeta::core::to_regex( + "a{2,4}", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "aa")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "aaa")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "aaaa")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "a")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "aaaaa")); +} + +TEST(rfc9485_matches_quantifier_zero) { + const auto regex{sourcemeta::core::to_regex( + "a{0}", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "a")); +} + +TEST(rfc9485_matches_quantifier_leading_zeros) { + const auto regex{sourcemeta::core::to_regex( + "a{002,004}", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "aaa")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "a")); +} + +TEST(rfc9485_matches_quantifier_on_astral) { + const auto regex{sourcemeta::core::to_regex( + "πŸ€”*", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "πŸ€”πŸ€”")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "a")); +} + +TEST(rfc9485_matches_quantifier_on_group) { + const auto regex{sourcemeta::core::to_regex( + "(ab)+", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "ab")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "abab")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "aba")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "")); +} + +TEST(rfc9485_matches_quantifier_on_class) { + const auto regex{sourcemeta::core::to_regex( + "[ab]{2}", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "ab")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "ba")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "aa")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "a")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "abc")); +} + +TEST(rfc9485_matches_quantifier_on_category) { + const auto regex{sourcemeta::core::to_regex( + "\\p{Lu}+", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "AB")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "Ab")); +} + +TEST(rfc9485_matches_alternation) { + const auto regex{sourcemeta::core::to_regex( + "cat|dog", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "cat")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "dog")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "cats")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "a cat")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "bird")); +} + +TEST(rfc9485_matches_alternation_quantified) { + const auto regex{sourcemeta::core::to_regex( + "aa|bb{6,8}", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "aa")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "bbbbbbb")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "bb")); +} + +TEST(rfc9485_matches_group) { + const auto regex{sourcemeta::core::to_regex( + "(aa)", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "aa")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "a")); +} + +TEST(rfc9485_matches_group_nested) { + const auto regex{sourcemeta::core::to_regex( + "((a)b)", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "ab")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "a")); +} + +TEST(rfc9485_matches_group_alternation) { + const auto regex{sourcemeta::core::to_regex( + "aa(bb|cc)dd", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "aabbdd")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "aaccdd")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "aadd")); +} + +TEST(rfc9485_matches_class_simple) { + const auto regex{sourcemeta::core::to_regex( + "[abc]", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "a")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "b")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "c")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "d")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "ab")); +} + +TEST(rfc9485_matches_class_negated) { + const auto regex{sourcemeta::core::to_regex( + "[^abc]", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "d")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "\n")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "a")); +} + +TEST(rfc9485_matches_class_range) { + const auto regex{sourcemeta::core::to_regex( + "[a-z]", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "m")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "M")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "0")); +} + +TEST(rfc9485_matches_class_astral_member) { + const auto regex{sourcemeta::core::to_regex( + "[aπŸ€”b]", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "πŸ€”")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "a")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "c")); +} + +TEST(rfc9485_matches_class_astral_range) { + const auto regex{sourcemeta::core::to_regex( + "[Ξ±-Ο‰]", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "Ξ²")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "a")); +} + +TEST(rfc9485_matches_class_leading_dash) { + const auto regex{sourcemeta::core::to_regex( + "[-ab]", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "-")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "a")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "x")); +} + +TEST(rfc9485_matches_class_trailing_dash) { + const auto regex{sourcemeta::core::to_regex( + "[ab-]", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "-")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "a")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "x")); +} + +TEST(rfc9485_matches_class_negated_leading_dash) { + const auto regex{sourcemeta::core::to_regex( + "[^-ab]", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "x")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "-")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "a")); +} + +TEST(rfc9485_matches_class_only_dash) { + const auto regex{sourcemeta::core::to_regex( + "[-]", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "-")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "a")); +} + +TEST(rfc9485_matches_class_escaped_range_endpoints) { + const auto regex{sourcemeta::core::to_regex( + "[a\\n-\\r-]", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "a")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "\n")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "\x0B")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "\r")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "-")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "x")); +} + +TEST(rfc9485_matches_class_escaped_brackets) { + const auto regex{sourcemeta::core::to_regex( + "[\\[\\]]", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "[")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "]")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "a")); +} + +TEST(rfc9485_matches_class_escaped_dash) { + const auto regex{sourcemeta::core::to_regex( + "[a\\-z]", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "a")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "-")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "z")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "m")); +} + +TEST(rfc9485_matches_class_category) { + const auto regex{sourcemeta::core::to_regex( + "[\\p{Nd}]", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "5")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "a")); +} + +TEST(rfc9485_matches_class_category_complement) { + const auto regex{sourcemeta::core::to_regex( + "[\\P{Nd}]", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "a")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "5")); +} + +TEST(rfc9485_matches_class_negated_categories) { + const auto regex{sourcemeta::core::to_regex( + "[^\\p{Nd}\\p{Ll}]", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "A")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "5")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "a")); +} + +TEST(rfc9485_matches_class_literal_dot) { + const auto regex{sourcemeta::core::to_regex( + "a[.b]c", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "a.c")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "abc")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "axc")); +} + +TEST(rfc9485_matches_category_letter) { + const auto regex{sourcemeta::core::to_regex( + "\\p{L}", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "a")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "Γ©")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "δΈ­")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "5")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "ab")); +} + +TEST(rfc9485_matches_category_uppercase) { + const auto regex{sourcemeta::core::to_regex( + "\\p{Lu}", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "A")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "a")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "AB")); +} + +TEST(rfc9485_matches_category_complement) { + const auto regex{sourcemeta::core::to_regex( + "\\P{Lu}", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "a")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "A")); +} + +TEST(rfc9485_matches_category_private_use) { + const auto regex{sourcemeta::core::to_regex( + "\\p{Co}", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "\xEE\x80\x80")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "a")); +} + +TEST(rfc9485_matches_dot) { + const auto regex{ + sourcemeta::core::to_regex(".", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "a")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "πŸ€”")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "\n")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "\r")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "ab")); +} + +TEST(rfc9485_matches_dot_line_separator) { + const auto regex{ + sourcemeta::core::to_regex(".", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "\xE2\x80\xA8")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "\xE2\x80\xA9")); +} + +TEST(rfc9485_matches_dot_star) { + const auto regex{sourcemeta::core::to_regex( + ".*", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "abc")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "a\nb")); +} + +TEST(rfc9485_matches_dot_plus) { + const auto regex{sourcemeta::core::to_regex( + ".+", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "abc")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "\n")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "")); +} + +TEST(rfc9485_matches_dot_star_is_not_noop) { + const auto regex{sourcemeta::core::to_regex( + ".*", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE( + std::holds_alternative(regex.value())); +} + +TEST(rfc9485_matches_dot_plus_is_not_non_empty) { + const auto regex{sourcemeta::core::to_regex( + ".+", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE( + std::holds_alternative(regex.value())); +} + +TEST(rfc9485_matches_dot_between_letters_astral) { + const auto regex{sourcemeta::core::to_regex( + "a.b", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "a\xF0\x90\x84\x81" + "b")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "ab")); +} + +TEST(rfc9485_matches_escaped_dot) { + const auto regex{sourcemeta::core::to_regex( + "a\\.c", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "a.c")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "axc")); +} + +TEST(rfc9485_matches_dot_after_escaped_backslash) { + const auto regex{sourcemeta::core::to_regex( + "a\\\\.c", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "a\\xc")); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "a\\.c")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "axc")); +} + +TEST(rfc9485_matches_if_valid_dialect) { + EXPECT_TRUE(sourcemeta::core::matches_if_valid( + "ab*", "abb", sourcemeta::core::RegexDialect::IRegexp)); + EXPECT_FALSE(sourcemeta::core::matches_if_valid( + "ab*", "xabb", sourcemeta::core::RegexDialect::IRegexp)); + EXPECT_FALSE(sourcemeta::core::matches_if_valid( + "\\d+", "5", sourcemeta::core::RegexDialect::IRegexp)); +} + +TEST(rfc9485_invalid_trailing_backslash) { + EXPECT_FALSE( + sourcemeta::core::to_regex("\\", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_escape_v) { + EXPECT_FALSE( + sourcemeta::core::to_regex("\\v", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_escape_f) { + EXPECT_FALSE( + sourcemeta::core::to_regex("\\f", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_escape_b) { + EXPECT_FALSE( + sourcemeta::core::to_regex("\\b", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_escape_uppercase_b) { + EXPECT_FALSE( + sourcemeta::core::to_regex("\\B", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_escape_d) { + EXPECT_FALSE( + sourcemeta::core::to_regex("\\d", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_escape_uppercase_d) { + EXPECT_FALSE( + sourcemeta::core::to_regex("\\D", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_escape_s) { + EXPECT_FALSE( + sourcemeta::core::to_regex("\\s", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_escape_uppercase_s) { + EXPECT_FALSE( + sourcemeta::core::to_regex("\\S", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_escape_w) { + EXPECT_FALSE( + sourcemeta::core::to_regex("\\w", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_escape_uppercase_w) { + EXPECT_FALSE( + sourcemeta::core::to_regex("\\W", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_escape_uppercase_a) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "\\Aabc", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_escape_z) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "abc\\z", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_escape_uppercase_z) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "abc\\Z", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_escape_zero) { + EXPECT_FALSE( + sourcemeta::core::to_regex("\\0", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_backreference) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "(a)\\1", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_escape_hex) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "\\x41", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_escape_unicode) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "\\u0041", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_escape_control) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "\\ca", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_escape_dollar) { + EXPECT_FALSE( + sourcemeta::core::to_regex("\\$", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_escape_slash) { + EXPECT_FALSE( + sourcemeta::core::to_regex("\\/", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_escape_after_literal_backslash) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "\\\\\\v", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_odd_backslashes) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "\\\\\\", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_double_star) { + EXPECT_FALSE( + sourcemeta::core::to_regex("a**", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_lazy_quantifier) { + EXPECT_FALSE( + sourcemeta::core::to_regex("a*?", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_possessive_quantifier) { + EXPECT_FALSE( + sourcemeta::core::to_regex("a*+", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_leading_star) { + EXPECT_FALSE( + sourcemeta::core::to_regex("*a", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_leading_plus) { + EXPECT_FALSE( + sourcemeta::core::to_regex("+a", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_quantifier_after_alternation) { + EXPECT_FALSE( + sourcemeta::core::to_regex("a|*", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_bare_range_quantifier) { + EXPECT_FALSE( + sourcemeta::core::to_regex("{3}", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_quantifier_missing_minimum) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "a{,3}", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_quantifier_empty) { + EXPECT_FALSE( + sourcemeta::core::to_regex("a{}", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_quantifier_non_digit) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "a{b}", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_quantifier_unterminated) { + EXPECT_FALSE( + sourcemeta::core::to_regex("a{3", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_quantifier_reversed) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "a{3,2}", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_quantifier_huge) { + EXPECT_FALSE( + sourcemeta::core::to_regex("a{99999999999999999999}", + sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_quantifier_above_engine_bound) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "a{70000}", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_bare_open_brace) { + EXPECT_FALSE( + sourcemeta::core::to_regex("a{b", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_bare_close_brace) { + EXPECT_FALSE( + sourcemeta::core::to_regex("a}b", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_unclosed_group) { + EXPECT_FALSE( + sourcemeta::core::to_regex("(aa", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_stray_close_paren) { + EXPECT_FALSE( + sourcemeta::core::to_regex("aa)", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_unclosed_group_alternation) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "aa(bb|ccdd", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_stray_close_paren_alternation) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "aabb|cc)dd", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_non_capturing_group) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "(?:a)", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_lookahead) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "(?=a)", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_negative_lookahead) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "(?!a)", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_lookbehind) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "(?<=a)b", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_named_group) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "(?a)", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_inline_flag) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "(?i)a", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_backtracking_verb) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "(*COMMIT)a", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_group_nesting_overflow) { + const std::string pattern{std::string(100, '(') + "a" + + std::string(100, ')')}; + EXPECT_FALSE(sourcemeta::core::to_regex( + pattern, sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_unterminated_class) { + EXPECT_FALSE( + sourcemeta::core::to_regex("[a", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_stray_close_bracket) { + EXPECT_FALSE( + sourcemeta::core::to_regex("a]", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_empty_class) { + EXPECT_FALSE( + sourcemeta::core::to_regex("[]", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_negated_empty_class) { + EXPECT_FALSE( + sourcemeta::core::to_regex("[^]", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_nested_class) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "[[a]]", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_posix_class) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "[[:alpha:]]", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_class_subtraction) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "[a-z-[aeiou]]", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_class_shorthand_digit) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "[\\d]", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_class_shorthand_space) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "[\\s]", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_class_dash_dash_element) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "[a--]", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_class_element_after_trailing_dash) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "[a-b-c]", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_class_reversed_range) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "[z-a]", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_category_block) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "\\p{IsBasicLatin}", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_category_script) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "\\p{Greek}", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_category_surrogate) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "\\p{Cs}", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_category_empty) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "\\p{}", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_category_unknown) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "\\p{Xx}", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_category_lowercase_major) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "\\p{l}", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_category_no_brace) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "\\pL", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_category_unterminated) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "\\p{Ll", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_category_too_long) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "\\p{Llx}", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_utf8_stray_continuation) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "\x80", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_utf8_truncated_sequence) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "\xF0\x9F", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_utf8_bad_lead) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "\xFF", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_invalid_utf8_in_class) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "[\xF0\x9F]", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(rfc9485_matches_mapped_quantified_caret_rejected) { + // NOTE: This test deviates from RFC 9485, whose grammar permits a + // quantifier on an unescaped caret. We follow the RFC 9485 Section 5.4 + // engine mapping instead, under which a quantified caret is not a valid + // expression, exactly as in the Section 5.3 ECMAScript mapping + const auto regex{sourcemeta::core::to_regex( + "^*", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_FALSE(regex.has_value()); +} + +TEST(rfc9485_matches_mapped_quantified_dollar_rejected) { + // NOTE: This test deviates from RFC 9485, whose grammar permits a + // quantifier on an unescaped dollar. We follow the RFC 9485 Section 5.4 + // engine mapping instead, under which a quantified dollar is not a valid + // expression, exactly as in the Section 5.3 ECMAScript mapping + const auto regex{sourcemeta::core::to_regex( + "a$?", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_FALSE(regex.has_value()); +} + +TEST(rfc9485_matches_empty_group) { + const auto regex{sourcemeta::core::to_regex( + "()", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "a")); +} + +TEST(rfc9485_matches_quantifier_zero_bounds) { + const auto regex{sourcemeta::core::to_regex( + "a{0,0}", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "a")); +} + +TEST(rfc9485_invalid_double_brace_quantifier) { + const auto regex{sourcemeta::core::to_regex( + "a{2}{3}", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_FALSE(regex.has_value()); +} + +TEST(rfc9485_invalid_category_as_range_start) { + const auto regex{sourcemeta::core::to_regex( + "[\\p{Lu}-z]", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_FALSE(regex.has_value()); +} + +TEST(rfc9485_invalid_close_bracket_first_in_class) { + const auto regex{sourcemeta::core::to_regex( + "[]-a]", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_FALSE(regex.has_value()); +} + +TEST(rfc9485_matches_class_double_dash) { + const auto regex{sourcemeta::core::to_regex( + "[--]", sourcemeta::core::RegexDialect::IRegexp)}; + EXPECT_TRUE(regex.has_value()); + EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "-")); + EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "a")); +}