| /** |
| @file Test for Regex.cc |
| |
| @section license License |
| |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| #include <string_view> |
| #include <vector> |
| |
| #define PCRE2_CODE_UNIT_WIDTH 8 |
| #include <pcre2.h> |
| |
| #include "tscore/ink_assert.h" |
| #include "tscore/ink_defs.h" |
| #include "tsutil/Regex.h" |
| #include <catch2/catch_test_macros.hpp> |
| #include <catch2/generators/catch_generators.hpp> |
| #include <catch2/generators/catch_generators_range.hpp> |
| |
| struct subject_match_t { |
| std::string_view subject; |
| bool match; |
| }; |
| |
| struct test_t { |
| std::string_view regex; |
| std::vector<subject_match_t> tests; |
| }; |
| |
| std::vector<test_t> test_data{ |
| { |
| {{R"(^foo)"}, {{{{"foo"}, true}, {{"bar"}, false}, {{"foobar"}, true}, {{"foobarbaz"}, true}}}}, |
| {{R"(foo$)"}, {{{{"foo"}, true}, {{"bar"}, false}, {{"foobar"}, false}, {{"foobarbaz"}, false}}}}, |
| // url regular expression |
| {{R"(^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$)"}, |
| {{{{"http://www.example.com"}, true}, |
| {{"https://www.example.com"}, true}, |
| {{"http://~example.com"}, false}, |
| {{"http://www.example.com/foo/bar"}, true}}}}, |
| // ip address regular expression |
| {R"(^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$)", |
| {{{{"1.2.3.4"}, true}, {{"127.0.0.1"}, true}, {{"256.256.256.256"}, false}, {{".1.1.1.1"}, false}}}}, |
| } |
| }; |
| |
| // test case insensitive test data |
| std::vector<test_t> test_data_case_insensitive{ |
| { |
| {{R"(^foo)"}, {{{{"FoO"}, true}, {{"bar"}, false}, {{"foObar"}, true}, {{"foobaRbaz"}, true}}}}, |
| {{R"(foo$)"}, {{{{"foO"}, true}, {{"bar"}, false}, {{"foobar"}, false}, {{"foobarbaz"}, false}}}}, |
| } |
| }; |
| |
| // test case for anchored flag |
| std::vector<test_t> test_data_anchored{ |
| { |
| {{R"(foo)"}, {{{{"foo"}, true}, {{"bar"}, false}, {{"foobar"}, true}, {{"foobarbaz"}, true}}}}, |
| {{R"(bar)"}, {{{{"foo"}, false}, {{"bar"}, true}, {{"foobar"}, false}, {{"foobarbaz"}, false}}}}, |
| } |
| }; |
| |
| struct submatch_t { |
| std::string_view subject; |
| int32_t count; |
| std::vector<std::string_view> submatches; |
| }; |
| |
| struct submatch_test_t { |
| std::string_view regex; |
| int32_t capture_count; |
| std::vector<submatch_t> tests; |
| }; |
| |
| std::vector<submatch_test_t> submatch_test_data{ |
| { |
| {{R"(^foo)"}, 0, {{{{"foo"}, 1, {{"foo"}}}, {{"bar"}, -1, {}}, {{"foobar"}, 1, {{"foo"}}}, {{"foobarbaz"}, 1, {{"foo"}}}}}}, |
| {{R"(foo$)"}, 0, {{{{"foo"}, 1, {{"foo"}}}, {{"bar"}, -1, {}}, {{"foobar"}, -1, {}}, {{"foobarbaz"}, -1, {}}}}}, |
| {{R"(^(foo)(bar))"}, 2, {{{{"foobar"}, 3, {{"foobar", "foo", "bar"}}}, {{"barfoo"}, -1, {}}, {{"foo"}, -1, {}}}}}, |
| } |
| }; |
| |
| TEST_CASE("Regex", "[libts][Regex]") |
| { |
| // case sensitive test |
| for (auto &item : test_data) { |
| CAPTURE(item.regex); |
| Regex r; |
| REQUIRE(r.compile(item.regex.data()) == true); |
| |
| for (auto &test : item.tests) { |
| CAPTURE(test.subject, test.match); |
| REQUIRE(r.exec(test.subject.data()) == test.match); |
| } |
| } |
| |
| // case insensitive test |
| for (auto &item : test_data_case_insensitive) { |
| CAPTURE(item.regex); |
| Regex r; |
| REQUIRE(r.compile(item.regex.data(), RE_CASE_INSENSITIVE) == true); |
| |
| for (auto &test : item.tests) { |
| CAPTURE(test.subject, test.match); |
| REQUIRE(r.exec(test.subject.data()) == test.match); |
| } |
| } |
| |
| // case anchored test |
| for (auto &item : test_data_anchored) { |
| CAPTURE(item.regex); |
| Regex r; |
| REQUIRE(r.compile(item.regex.data(), RE_ANCHORED) == true); |
| |
| for (auto &test : item.tests) { |
| CAPTURE(test.subject, test.match); |
| REQUIRE(r.exec(test.subject.data()) == test.match); |
| } |
| } |
| |
| // test getting submatches with operator[] |
| for (auto &item : submatch_test_data) { |
| CAPTURE(item.regex, item.capture_count); |
| Regex r; |
| REQUIRE(r.compile(item.regex.data()) == true); |
| REQUIRE(r.get_capture_count() == item.capture_count); |
| |
| for (auto &test : item.tests) { |
| CAPTURE(test.subject, test.count); |
| RegexMatches matches; |
| REQUIRE(r.exec(test.subject.data(), matches) == test.count); |
| REQUIRE(matches.size() == test.count); |
| |
| for (int32_t i = 0; i < test.count; i++) { |
| REQUIRE(matches[i] == test.submatches[i]); |
| } |
| } |
| } |
| |
| // test getting submatches with ovector pointer |
| for (auto &item : submatch_test_data) { |
| CAPTURE(item.regex, item.capture_count); |
| Regex r; |
| REQUIRE(r.compile(item.regex.data()) == true); |
| REQUIRE(r.get_capture_count() == item.capture_count); |
| |
| for (auto &test : item.tests) { |
| CAPTURE(test.subject, test.count); |
| RegexMatches matches; |
| REQUIRE(r.exec(test.subject.data(), matches) == test.count); |
| REQUIRE(matches.size() == test.count); |
| |
| size_t *ovector = matches.get_ovector_pointer(); |
| for (int32_t i = 0; i < test.count; i++) { |
| REQUIRE(test.submatches[i] == std::string_view{test.subject.data() + ovector[i * 2], ovector[i * 2 + 1] - ovector[i * 2]}); |
| } |
| } |
| } |
| |
| // test for invalid regular expression |
| { |
| Regex r; |
| REQUIRE(r.compile(R"((\d+)", RE_CASE_INSENSITIVE) == false); |
| } |
| |
| // test for not compiling regular expression |
| { |
| Regex r; |
| RegexMatches matches; |
| REQUIRE(r.exec("foo") == false); |
| REQUIRE(r.exec("foo", matches) == RE_ERROR_NULL); |
| } |
| |
| // test for recompiling the regular expression |
| { |
| Regex r; |
| REQUIRE(r.compile(R"(foo)") == true); |
| REQUIRE(r.exec("foo") == true); |
| REQUIRE(r.compile(R"(bar)") == true); |
| REQUIRE(r.exec("bar") == true); |
| } |
| |
| // test with matches set to 100, don't run the test in debug mode or the test will abort with a message to increase the buffer size |
| #ifndef DEBUG |
| { |
| Regex r; |
| RegexMatches matches(100); |
| REQUIRE(r.compile(R"(foo)") == true); |
| REQUIRE(r.exec("foo", matches) == 1); |
| } |
| #endif |
| } |
| |
| TEST_CASE("Regex RE_NOTEMPTY flag behavior", "[libts][Regex][flags][RE_NOTEMPTY]") |
| { |
| // Pattern that only matches empty string |
| Regex r; |
| REQUIRE(r.compile("^$") == true); |
| |
| SECTION("default exec matches empty subject") |
| { |
| // boolean overload |
| CHECK(r.exec(std::string_view("")) == true); |
| |
| // matches overload should return 1 (one match - the whole subject) |
| RegexMatches matches; |
| CHECK(r.exec(std::string_view(""), matches) == 1); |
| CHECK(matches.size() == 1); |
| CHECK(matches[0] == std::string_view("")); |
| } |
| |
| SECTION("RE_NOTEMPTY prevents empty matches") |
| { |
| // boolean overload with RE_NOTEMPTY should not match |
| CHECK(r.exec(std::string_view(""), RE_NOTEMPTY) == false); |
| |
| // matches overload should return RE_ERROR_NOMATCH |
| RegexMatches matches; |
| int rc = r.exec(std::string_view(""), matches, RE_NOTEMPTY); |
| CHECK(rc == RE_ERROR_NOMATCH); |
| } |
| |
| SECTION("non-empty subject unaffected by RE_NOTEMPTY for this pattern") |
| { |
| // '^$' should not match 'a' in any case |
| CHECK(r.exec(std::string_view("a")) == false); |
| CHECK(r.exec(std::string_view("a"), RE_NOTEMPTY) == false); |
| } |
| } |
| |
| TEST_CASE("Regex error codes", "[libts][Regex][errors]") |
| { |
| SECTION("RE_ERROR_NULL when regex not compiled") |
| { |
| Regex r; |
| RegexMatches matches; |
| |
| // exec on uncompiled regex should return RE_ERROR_NULL |
| CHECK(r.exec("test", matches) == RE_ERROR_NULL); |
| } |
| |
| SECTION("RE_ERROR_NOMATCH when pattern does not match") |
| { |
| Regex r; |
| REQUIRE(r.compile(R"(^foo$)") == true); |
| |
| RegexMatches matches; |
| |
| // Pattern does not match, should return RE_ERROR_NOMATCH |
| CHECK(r.exec("bar", matches) == RE_ERROR_NOMATCH); |
| CHECK(r.exec("foobar", matches) == RE_ERROR_NOMATCH); |
| CHECK(r.exec("", matches) == RE_ERROR_NOMATCH); |
| |
| // The following should match and return 1 (which shouldn't be RE_ERROR_NOMATCH) |
| CHECK(r.exec("foo", matches) != RE_ERROR_NOMATCH); |
| CHECK(r.exec("foo", matches) == 1); |
| } |
| |
| SECTION("Compile error returns detailed error message") |
| { |
| Regex r; |
| std::string error; |
| int erroffset; |
| |
| // Unclosed parenthesis should fail with error message |
| CHECK(r.compile(R"((unclosed)", error, erroffset) == false); |
| CHECK(!error.empty()); |
| CHECK(erroffset > 0); |
| |
| // Invalid escape sequence |
| error.clear(); |
| erroffset = 0; |
| CHECK(r.compile(R"(\k)", error, erroffset) == false); |
| CHECK(!error.empty()); |
| |
| // Invalid character class |
| error.clear(); |
| erroffset = 0; |
| CHECK(r.compile(R"([z-a])", error, erroffset) == false); |
| CHECK(!error.empty()); |
| } |
| } |
| |
| TEST_CASE("Regex::empty()", "[libts][Regex][empty]") |
| { |
| SECTION("newly constructed Regex is empty") |
| { |
| Regex r; |
| CHECK(r.empty() == true); |
| } |
| |
| SECTION("compiled Regex is not empty") |
| { |
| Regex r; |
| REQUIRE(r.compile("test") == true); |
| CHECK(r.empty() == false); |
| } |
| |
| SECTION("failed compilation leaves Regex empty") |
| { |
| Regex r; |
| REQUIRE(r.compile("(invalid") == false); |
| CHECK(r.empty() == true); |
| } |
| |
| SECTION("recompiling non-empty Regex") |
| { |
| Regex r; |
| REQUIRE(r.compile("foo") == true); |
| CHECK(r.empty() == false); |
| |
| REQUIRE(r.compile("bar") == true); |
| CHECK(r.empty() == false); |
| } |
| } |
| |
| TEST_CASE("Regex move semantics", "[libts][Regex][move]") |
| { |
| SECTION("move constructor") |
| { |
| Regex r1; |
| REQUIRE(r1.compile("^test$") == true); |
| CHECK(r1.exec("test") == true); |
| CHECK(r1.empty() == false); |
| |
| // Move construct r2 from r1 |
| Regex r2(std::move(r1)); |
| CHECK(r2.empty() == false); |
| CHECK(r2.exec("test") == true); |
| CHECK(r2.exec("foo") == false); |
| } |
| |
| SECTION("move assignment operator") |
| { |
| Regex r1; |
| REQUIRE(r1.compile("^test$") == true); |
| |
| Regex r2; |
| REQUIRE(r2.compile("^foo$") == true); |
| |
| // Move assign r1 to r2 |
| r2 = std::move(r1); |
| CHECK(r2.empty() == false); |
| CHECK(r2.exec("test") == true); |
| CHECK(r2.exec("foo") == false); |
| } |
| |
| SECTION("move empty Regex") |
| { |
| Regex r1; // empty |
| Regex r2(std::move(r1)); |
| CHECK(r2.empty() == true); |
| } |
| } |
| |
| TEST_CASE("Regex RE_UNANCHORED flag", "[libts][Regex][flags][RE_UNANCHORED]") |
| { |
| SECTION("RE_UNANCHORED allows matching anywhere in multiline text") |
| { |
| Regex r; |
| // Pattern that should match "test" at start of any line |
| REQUIRE(r.compile("^test", RE_UNANCHORED) == true); |
| |
| // Should match at start of string |
| CHECK(r.exec("test\nfoo") == true); |
| |
| // Should match after newline (multiline mode) |
| CHECK(r.exec("foo\ntest") == true); |
| |
| // Should not match in middle of line |
| CHECK(r.exec("foo test") == false); |
| } |
| |
| SECTION("default (without RE_UNANCHORED) only matches at string start") |
| { |
| Regex r; |
| REQUIRE(r.compile("^test") == true); |
| |
| // Should match at start |
| CHECK(r.exec("test\nfoo") == true); |
| |
| // Should NOT match after newline without RE_UNANCHORED |
| CHECK(r.exec("foo\ntest") == false); |
| } |
| } |
| |
| TEST_CASE("RegexMatches edge cases", "[libts][Regex][RegexMatches]") |
| { |
| SECTION("RegexMatches size after no match") |
| { |
| Regex r; |
| RegexMatches matches; |
| REQUIRE(r.compile("test") == true); |
| |
| int count = r.exec("nomatch", matches); |
| CHECK(count == RE_ERROR_NOMATCH); |
| CHECK(matches.size() == RE_ERROR_NOMATCH); |
| } |
| |
| SECTION("RegexMatches operator[] with various capture counts") |
| { |
| Regex r; |
| REQUIRE(r.compile("(\\w+)-(\\d+)-(\\w+)") == true); |
| |
| RegexMatches matches; |
| int count = r.exec("foo-123-bar", matches); |
| CHECK(count == 4); // whole match + 3 groups |
| |
| CHECK(matches[0] == "foo-123-bar"); |
| CHECK(matches[1] == "foo"); |
| CHECK(matches[2] == "123"); |
| CHECK(matches[3] == "bar"); |
| } |
| |
| SECTION("RegexMatches with zero-length captures") |
| { |
| Regex r; |
| REQUIRE(r.compile("(\\w*)-(\\w*)") == true); |
| |
| RegexMatches matches; |
| |
| // First group empty, second has content |
| int count = r.exec("-foo", matches); |
| CHECK(count == 3); |
| CHECK(matches[0] == "-foo"); |
| CHECK(matches[1] == ""); |
| CHECK(matches[2] == "foo"); |
| } |
| |
| SECTION("RegexMatches with optional groups") |
| { |
| Regex r; |
| REQUIRE(r.compile("(\\w+)-(\\d+)?") == true); |
| |
| RegexMatches matches; |
| |
| // With optional group present |
| int count = r.exec("foo-123", matches); |
| CHECK(count == 3); |
| CHECK(matches[1] == "foo"); |
| CHECK(matches[2] == "123"); |
| |
| // With optional group absent - note: PCRE2 may still count it |
| count = r.exec("foo-", matches); |
| CHECK(count >= 2); // At least whole match + first group |
| CHECK(matches[1] == "foo"); |
| } |
| |
| SECTION("RegexMatches with a non-participating group before a participating one") |
| { |
| // pcre2_match() returns one past the highest participating group, so an earlier optional group |
| // that did not participate is still within that count. Its offsets are unset. |
| Regex r; |
| REQUIRE(r.compile("(a)?(b)")); |
| |
| RegexMatches matches; |
| int count = r.exec("b", matches); |
| |
| CHECK(count == 3); |
| CHECK(matches[0] == "b"); |
| CHECK(matches[1] == ""); |
| CHECK(matches[2] == "b"); |
| CHECK(matches[1].data() != nullptr); |
| } |
| |
| SECTION("RegexMatches past what the match populated") |
| { |
| // The ovector holds a fixed number of pairs regardless of the pattern, and pcre2_match() writes |
| // no further than the highest participating group. Reading past that must not build a view over |
| // the uninitialized remainder of the buffer. |
| Regex r; |
| REQUIRE(r.compile("(.*-)(\\d+)(\\?.*)?$")); |
| |
| RegexMatches matches; |
| int count = r.exec("/img-7", matches); |
| |
| // Three groups defined, but the trailing optional one did not participate. |
| CHECK(r.get_capture_count() == 3); |
| CHECK(count == 3); |
| CHECK(matches[1] == "/img-"); |
| CHECK(matches[2] == "7"); |
| |
| // A group the pattern defines that the match did not reach, ... |
| CHECK(matches[3] == ""); |
| CHECK(matches[3].data() != nullptr); |
| |
| // ... and an index past the pattern's groups entirely, still inside the allocated ovector. |
| CHECK(matches[9] == ""); |
| CHECK(matches[9].data() != nullptr); |
| } |
| |
| SECTION("RegexMatches after a failed match") |
| { |
| Regex r; |
| REQUIRE(r.compile("(a)(b)")); |
| |
| RegexMatches matches; |
| CHECK(r.exec("zz", matches) < 0); |
| |
| CHECK(matches[0] == ""); |
| CHECK(matches[0].data() != nullptr); |
| } |
| } |
| |
| TEST_CASE("Regex with special characters", "[libts][Regex][special]") |
| { |
| SECTION("escaped special characters") |
| { |
| Regex r; |
| REQUIRE(r.compile(R"(\$\d+\.\d+)") == true); |
| |
| CHECK(r.exec("$123.45") == true); |
| CHECK(r.exec("123.45") == false); |
| CHECK(r.exec("$12.3") == true); |
| } |
| |
| SECTION("character classes") |
| { |
| Regex r; |
| REQUIRE(r.compile(R"([A-Z][a-z]+)") == true); |
| |
| CHECK(r.exec("Hello") == true); |
| CHECK(r.exec("hello") == false); |
| CHECK(r.exec("HELLO") == false); |
| } |
| |
| SECTION("quantifiers") |
| { |
| Regex r; |
| REQUIRE(r.compile(R"(\d{3}-\d{4})") == true); |
| |
| CHECK(r.exec("123-4567") == true); |
| CHECK(r.exec("12-4567") == false); |
| CHECK(r.exec("123-456") == false); |
| } |
| |
| SECTION("alternation") |
| { |
| Regex r; |
| REQUIRE(r.compile(R"(foo|bar|baz)") == true); |
| |
| CHECK(r.exec("foo") == true); |
| CHECK(r.exec("bar") == true); |
| CHECK(r.exec("baz") == true); |
| CHECK(r.exec("qux") == false); |
| } |
| } |
| |
| TEST_CASE("Regex with complex patterns", "[libts][Regex][complex]") |
| { |
| SECTION("greedy vs non-greedy quantifiers") |
| { |
| Regex greedy, non_greedy; |
| REQUIRE(greedy.compile(R"(<.*>)") == true); |
| REQUIRE(non_greedy.compile(R"(<.*?>)") == true); |
| |
| RegexMatches matches; |
| |
| // Greedy matches everything |
| int count = greedy.exec("<div>content</div>", matches); |
| CHECK(count == 1); |
| CHECK(matches[0] == "<div>content</div>"); |
| |
| // Non-greedy matches just first tag |
| count = non_greedy.exec("<div>content</div>", matches); |
| CHECK(count == 1); |
| CHECK(matches[0] == "<div>"); |
| } |
| |
| SECTION("lookahead assertions") |
| { |
| Regex r; |
| // Match "foo" only if followed by "bar" |
| REQUIRE(r.compile(R"(foo(?=bar))") == true); |
| |
| CHECK(r.exec("foobar") == true); |
| CHECK(r.exec("foobaz") == false); |
| CHECK(r.exec("foo") == false); |
| } |
| |
| SECTION("negative lookahead") |
| { |
| Regex r; |
| // Match "foo" only if NOT followed by "bar" |
| REQUIRE(r.compile(R"(foo(?!bar))") == true); |
| |
| CHECK(r.exec("foobar") == false); |
| CHECK(r.exec("foobaz") == true); |
| CHECK(r.exec("foo") == true); |
| } |
| |
| SECTION("word boundaries") |
| { |
| Regex r; |
| REQUIRE(r.compile(R"(\btest\b)") == true); |
| |
| CHECK(r.exec("test") == true); |
| CHECK(r.exec("a test here") == true); |
| CHECK(r.exec("testing") == false); |
| CHECK(r.exec("attest") == false); |
| } |
| } |
| |
| TEST_CASE("Regex recompilation behavior", "[libts][Regex][recompile]") |
| { |
| SECTION("recompile frees previous pattern") |
| { |
| Regex r; |
| |
| REQUIRE(r.compile("foo") == true); |
| CHECK(r.exec("foo") == true); |
| CHECK(r.exec("bar") == false); |
| |
| // Recompile with different pattern |
| REQUIRE(r.compile("bar") == true); |
| CHECK(r.exec("bar") == true); |
| CHECK(r.exec("foo") == false); |
| } |
| |
| SECTION("recompile after failed compilation") |
| { |
| Regex r; |
| |
| // First compilation fails |
| REQUIRE(r.compile("(invalid") == false); |
| CHECK(r.empty() == true); |
| |
| // Should still be able to compile successfully |
| REQUIRE(r.compile("valid") == true); |
| CHECK(r.empty() == false); |
| CHECK(r.exec("valid") == true); |
| } |
| |
| SECTION("a failed recompile leaves the working pattern in place") |
| { |
| // compile() is a transaction. A pattern that fails to compile must not disturb the |
| // pattern already held, because the alternative is worse than either outcome: freeing |
| // the old pattern before knowing the new one compiles leaves a dangling pointer that |
| // empty() reports as compiled and exec() hands to pcre2_match. |
| Regex r; |
| REQUIRE(r.compile("foo") == true); |
| |
| REQUIRE(r.compile("(invalid") == false); |
| |
| CHECK(r.empty() == false); |
| CHECK(r.exec("foo") == true); |
| CHECK(r.exec("bar") == false); |
| |
| // And the object is still usable for a later successful compile. |
| REQUIRE(r.compile("bar") == true); |
| CHECK(r.exec("bar") == true); |
| } |
| |
| SECTION("a failed recompile leaves captures working") |
| { |
| Regex r; |
| REQUIRE(r.compile("^(a+)(b+)$") == true); |
| |
| REQUIRE(r.compile("(unterminated") == false); |
| |
| RegexMatches matches; |
| REQUIRE(r.exec("aaabb", matches) == 3); |
| CHECK(matches[1] == "aaa"); |
| CHECK(matches[2] == "bb"); |
| } |
| |
| SECTION("recompile with different flags") |
| { |
| Regex r; |
| |
| REQUIRE(r.compile("test") == true); |
| CHECK(r.exec("TEST") == false); |
| |
| // Recompile with case insensitive flag |
| REQUIRE(r.compile("test", RE_CASE_INSENSITIVE) == true); |
| CHECK(r.exec("TEST") == true); |
| } |
| } |
| |
| TEST_CASE("Regex copy constructor", "[libts][Regex][copy]") |
| { |
| SECTION("Copy constructor creates independent copy") |
| { |
| Regex original; |
| REQUIRE(original.compile(R"(^test\d+$)") == true); |
| |
| // Test original works |
| CHECK(original.exec("test123") == true); |
| CHECK(original.exec("test") == false); |
| |
| // Copy using copy constructor |
| Regex copy(original); |
| |
| // Both should work independently |
| CHECK(copy.exec("test123") == true); |
| CHECK(copy.exec("test") == false); |
| CHECK(original.exec("test456") == true); |
| CHECK(original.exec("test") == false); |
| } |
| |
| SECTION("Copy constructor with capture groups") |
| { |
| Regex original; |
| REQUIRE(original.compile(R"(^(\w+)@(\w+)\.(\w+)$)") == true); |
| |
| Regex copy(original); |
| |
| // Test both original and copy with captures |
| RegexMatches original_matches; |
| REQUIRE(original.exec("user@example.com", original_matches) == 4); |
| CHECK(original_matches[0] == "user@example.com"); |
| CHECK(original_matches[1] == "user"); |
| CHECK(original_matches[2] == "example"); |
| CHECK(original_matches[3] == "com"); |
| |
| RegexMatches copy_matches; |
| REQUIRE(copy.exec("admin@test.org", copy_matches) == 4); |
| CHECK(copy_matches[0] == "admin@test.org"); |
| CHECK(copy_matches[1] == "admin"); |
| CHECK(copy_matches[2] == "test"); |
| CHECK(copy_matches[3] == "org"); |
| } |
| |
| SECTION("Copy constructor with empty regex") |
| { |
| Regex original; // Not compiled |
| Regex copy(original); |
| |
| // Both should be empty |
| CHECK(original.empty() == true); |
| CHECK(copy.empty() == true); |
| |
| // Neither should match anything |
| CHECK(original.exec("test") == false); |
| CHECK(copy.exec("test") == false); |
| } |
| |
| SECTION("Copy constructor with case insensitive flag") |
| { |
| Regex original; |
| REQUIRE(original.compile(R"(^FOO$)", RE_CASE_INSENSITIVE) == true); |
| |
| Regex copy(original); |
| |
| // Both should match case-insensitively |
| CHECK(original.exec("foo") == true); |
| CHECK(original.exec("FOO") == true); |
| CHECK(original.exec("FoO") == true); |
| CHECK(copy.exec("foo") == true); |
| CHECK(copy.exec("FOO") == true); |
| CHECK(copy.exec("FoO") == true); |
| } |
| |
| SECTION("Multiple copies can coexist") |
| { |
| Regex original; |
| REQUIRE(original.compile(R"(\d+)") == true); |
| |
| Regex copy1(original); |
| Regex copy2(original); |
| Regex copy3(copy1); |
| |
| // All should work independently |
| CHECK(original.exec("123") == true); |
| CHECK(copy1.exec("456") == true); |
| CHECK(copy2.exec("789") == true); |
| CHECK(copy3.exec("000") == true); |
| } |
| |
| SECTION("Copy can be stored in vector") |
| { |
| Regex pattern; |
| REQUIRE(pattern.compile(R"(test\d+)") == true); |
| |
| std::vector<Regex> patterns; |
| patterns.push_back(pattern); |
| patterns.push_back(pattern); |
| patterns.push_back(pattern); |
| |
| // All copies in vector should work |
| for (auto &p : patterns) { |
| CHECK(p.exec("test123") == true); |
| CHECK(p.exec("test") == false); |
| } |
| } |
| } |
| |
| TEST_CASE("Regex copy assignment", "[libts][Regex][copy]") |
| { |
| SECTION("Copy assignment replaces existing pattern") |
| { |
| Regex regex1; |
| Regex regex2; |
| |
| REQUIRE(regex1.compile(R"(foo)") == true); |
| REQUIRE(regex2.compile(R"(bar)") == true); |
| |
| CHECK(regex1.exec("foo") == true); |
| CHECK(regex1.exec("bar") == false); |
| CHECK(regex2.exec("foo") == false); |
| CHECK(regex2.exec("bar") == true); |
| |
| // Copy assign regex1 to regex2 |
| regex2 = regex1; |
| |
| // Now both should match "foo" |
| CHECK(regex1.exec("foo") == true); |
| CHECK(regex1.exec("bar") == false); |
| CHECK(regex2.exec("foo") == true); |
| CHECK(regex2.exec("bar") == false); |
| } |
| |
| SECTION("Copy assignment from empty regex") |
| { |
| Regex compiled; |
| Regex empty; |
| |
| REQUIRE(compiled.compile(R"(test)") == true); |
| CHECK(compiled.exec("test") == true); |
| |
| // Assign empty to compiled |
| compiled = empty; |
| |
| // Now compiled should be empty |
| CHECK(compiled.empty() == true); |
| CHECK(compiled.exec("test") == false); |
| } |
| |
| SECTION("Copy assignment to empty regex") |
| { |
| Regex empty; |
| Regex compiled; |
| |
| REQUIRE(compiled.compile(R"(test)") == true); |
| |
| // Assign compiled to empty |
| empty = compiled; |
| |
| // Now empty should work |
| CHECK(empty.exec("test") == true); |
| CHECK(compiled.exec("test") == true); |
| } |
| |
| SECTION("Self-assignment is safe") |
| { |
| Regex regex; |
| REQUIRE(regex.compile(R"(test)") == true); |
| |
| // Self-assign (disable warning for intentional self-assignment test) |
| // Use a pointer indirection to avoid compiler warnings about self-assignment |
| Regex *ptr = ®ex; |
| regex = *ptr; |
| |
| // Should still work |
| CHECK(regex.exec("test") == true); |
| CHECK(regex.exec("foo") == false); |
| } |
| |
| SECTION("Copy assignment with capture groups") |
| { |
| Regex regex1; |
| Regex regex2; |
| |
| REQUIRE(regex1.compile(R"(^(\d{3})-(\d{3})-(\d{4})$)") == true); |
| REQUIRE(regex2.compile(R"(foo)") == true); |
| |
| regex2 = regex1; |
| |
| RegexMatches matches; |
| REQUIRE(regex2.exec("123-456-7890", matches) == 4); |
| CHECK(matches[0] == "123-456-7890"); |
| CHECK(matches[1] == "123"); |
| CHECK(matches[2] == "456"); |
| CHECK(matches[3] == "7890"); |
| } |
| |
| SECTION("Copy assignment chain") |
| { |
| Regex r1, r2, r3; |
| REQUIRE(r1.compile(R"(test\d+)") == true); |
| |
| // Chain assignment |
| r3 = r2 = r1; |
| |
| // All should work |
| CHECK(r1.exec("test123") == true); |
| CHECK(r2.exec("test456") == true); |
| CHECK(r3.exec("test789") == true); |
| } |
| } |
| |
| TEST_CASE("Regex copy with RE_NOTEMPTY flag", "[libts][Regex][copy][flags]") |
| { |
| SECTION("Copied regex preserves RE_NOTEMPTY behavior") |
| { |
| Regex original; |
| REQUIRE(original.compile("^$") == true); |
| |
| Regex copy(original); |
| |
| // Both should have same behavior with RE_NOTEMPTY |
| CHECK(original.exec(std::string_view("")) == true); |
| CHECK(original.exec(std::string_view(""), RE_NOTEMPTY) == false); |
| |
| CHECK(copy.exec(std::string_view("")) == true); |
| CHECK(copy.exec(std::string_view(""), RE_NOTEMPTY) == false); |
| } |
| } |
| |
| struct backref_test_t { |
| std::string_view regex; |
| bool valid; |
| int32_t backref_max; |
| }; |
| |
| std::vector<backref_test_t> backref_test_data{ |
| {{""}, true, 0 }, |
| {{R"(\b(\w+)\s+\1\b)"}, true, 1 }, |
| {{R"((.)\1)"}, true, 1 }, |
| {{R"((.)(.).\2\1)"}, true, 2 }, |
| {{R"((.\2\1)"}, false, -1}, |
| }; |
| |
| TEST_CASE("Regex back reference counting", "[libts][Regex][get_backref_max]") |
| { |
| auto item = GENERATE(from_range(backref_test_data)); |
| CAPTURE(item.regex, item.valid, item.backref_max); |
| Regex r; |
| REQUIRE(r.compile(item.regex) == item.valid); |
| REQUIRE(r.get_backref_max() == item.backref_max); |
| } |
| |
| struct match_context_test_t { |
| std::string_view regex; |
| std::string_view str; |
| bool valid; |
| int32_t rcode; |
| }; |
| |
| std::vector<match_context_test_t> match_context_test_data{ |
| {{"abc"}, {"abc"}, true, 1 }, |
| {{"abc"}, {"a"}, true, -1 }, |
| {{R"(^(\d{3})-(\d{3})-(\d{4})$)"}, {"123-456-7890"}, true, -47}, |
| {{"(."}, {"a"}, false, -51}, |
| }; |
| |
| TEST_CASE("RegexMatchContext", "[libts][Regex][RegexMatchContext]") |
| { |
| RegexMatchContext match_context; |
| match_context.set_match_limit(2); |
| RegexMatches matches; |
| |
| auto item = GENERATE(from_range(match_context_test_data)); |
| CAPTURE(item.regex, item.str, item.valid, item.rcode); |
| Regex r; |
| REQUIRE(r.compile(item.regex) == item.valid); |
| REQUIRE(r.exec(item.str, matches, 0, &match_context) == item.rcode); |
| } |
| |
| TEST_CASE("Regex RE_FULL_MATCH rejects trailing content", "[libts][Regex][full_match]") |
| { |
| Regex re; |
| REQUIRE(re.compile(R"(example\.com)", REFlags::RE_ANCHORED)); |
| |
| RegexMatches matches; |
| |
| SECTION("exact input matches with RE_FULL_MATCH") |
| { |
| REQUIRE(re.exec("example.com", matches, REFlags::RE_FULL_MATCH) > 0); |
| } |
| |
| SECTION("trailing content rejected with RE_FULL_MATCH") |
| { |
| int rc = re.exec("example.com.evil", matches, REFlags::RE_FULL_MATCH); |
| REQUIRE(rc == RE_ERROR_NOMATCH); |
| } |
| |
| SECTION("trailing content still matches without RE_FULL_MATCH") |
| { |
| REQUIRE(re.exec("example.com.evil", matches) > 0); |
| } |
| |
| SECTION("bool exec overload honors RE_FULL_MATCH") |
| { |
| REQUIRE(re.exec("example.com", REFlags::RE_FULL_MATCH)); |
| REQUIRE_FALSE(re.exec("example.com.evil", REFlags::RE_FULL_MATCH)); |
| } |
| } |
| |
| TEST_CASE("Regex RE_FULL_MATCH preserves capture groups and combines with other flags", "[libts][Regex][full_match]") |
| { |
| RegexMatches matches; |
| |
| SECTION("captures available on full match") |
| { |
| Regex re; |
| REQUIRE(re.compile(R"(^([a-z]+)\.([a-z]+)$)")); |
| REQUIRE(re.exec("foo.bar", matches, REFlags::RE_FULL_MATCH) == 3); |
| REQUIRE(matches[1] == "foo"); |
| REQUIRE(matches[2] == "bar"); |
| } |
| |
| SECTION("RE_FULL_MATCH composes with RE_CASE_INSENSITIVE") |
| { |
| Regex re; |
| REQUIRE(re.compile(R"(example\.com)", REFlags::RE_CASE_INSENSITIVE | REFlags::RE_ANCHORED)); |
| REQUIRE(re.exec("EXAMPLE.COM", matches, REFlags::RE_FULL_MATCH) > 0); |
| REQUIRE(re.exec("EXAMPLE.COM.evil", matches, REFlags::RE_FULL_MATCH) == RE_ERROR_NOMATCH); |
| } |
| |
| SECTION("matches._size reflects RE_ERROR_NOMATCH on length-rejected match") |
| { |
| Regex re; |
| REQUIRE(re.compile(R"(foo)")); |
| REQUIRE(re.exec("foobar", matches, REFlags::RE_FULL_MATCH) == RE_ERROR_NOMATCH); |
| REQUIRE(matches.size() == RE_ERROR_NOMATCH); |
| } |
| } |
| |
| TEST_CASE("DFA RE_FULL_MATCH applied at compile time", "[libts][DFA][full_match]") |
| { |
| SECTION("trailing content rejected when DFA compiled with RE_FULL_MATCH") |
| { |
| DFA dfa; |
| std::string_view pattern = R"(example\.com)"; |
| REQUIRE(dfa.compile(pattern, REFlags::RE_FULL_MATCH) == 1); |
| |
| REQUIRE(dfa.match("example.com") == 0); |
| REQUIRE(dfa.match("example.com.evil") == -1); |
| } |
| |
| SECTION("multi-pattern DFA: RE_FULL_MATCH applies to all patterns") |
| { |
| std::vector<std::string_view> patterns = {R"(foo)", R"(bar)"}; |
| DFA dfa; |
| REQUIRE(dfa.compile(patterns.data(), patterns.size(), REFlags::RE_FULL_MATCH) == 2); |
| |
| REQUIRE(dfa.match("foo") == 0); |
| REQUIRE(dfa.match("bar") == 1); |
| REQUIRE(dfa.match("foobar") == -1); |
| REQUIRE(dfa.match("barbaz") == -1); |
| } |
| |
| SECTION("DFA without RE_FULL_MATCH still permits trailing content (existing behavior)") |
| { |
| DFA dfa; |
| REQUIRE(dfa.compile(R"(foo)") == 1); |
| REQUIRE(dfa.match("foobar") == 0); |
| } |
| } |
| |
| // Regression: RE_ANCHORED | RE_ENDANCHORED on an alternation where a shorter |
| // alternative is a prefix of a longer one must backtrack to the longer alt |
| // when only the longer alt spans the full subject. On modern PCRE2 this is |
| // handled by the native PCRE2_ENDANCHORED flag; on PCRE2 < 10.30 the pattern |
| // is rewritten to "(?:pattern)\z" so pcre2 does the same backtracking. A |
| // naive post-match length check on the first successful pcre2_match would |
| // stop at the shorter alt and incorrectly report no match. |
| TEST_CASE("Regex end-anchor with alternation", "[libts][Regex]") |
| { |
| Regex r; |
| REQUIRE(r.compile(R"(cdn\.example\.com|cdn\.example\.com\.edge)", RE_ANCHORED | RE_ENDANCHORED) == true); |
| |
| RegexMatches matches; |
| CHECK(r.exec("cdn.example.com", matches) > 0); // shorter alt spans fully |
| CHECK(r.exec("cdn.example.com.edge", matches) > 0); // longer alt spans fully -- requires backtracking |
| CHECK(r.exec("cdn.example.com.evil", matches) == RE_ERROR_NOMATCH); |
| CHECK(r.exec("cdn.example.com.evil.com", matches) == RE_ERROR_NOMATCH); |
| CHECK(r.exec("prefix.cdn.example.com", matches) == RE_ERROR_NOMATCH); |
| } |
| |
| // pcre2_code_copy() copies the compiled pattern but not the machine code the JIT produced |
| // for it, because that code is position dependent. A copy that is not passed back through |
| // pcre2_jit_compile() therefore matches on the interpreter: the same answers, far more |
| // slowly, and under a different set of resource limits, so a subject one of them reports |
| // as too expensive the other quietly matches. |
| // |
| // The subject below is sized past the JIT engine's stack bound for this pattern, which is |
| // what makes the two engines disagree. The assertion is that a copy answers the same as |
| // its original, whatever that answer is, so the test needs no knowledge of whether this |
| // build has a JIT. |
| TEST_CASE("Regex copies answer the same as their original", "[libts][Regex][copy]") |
| { |
| Regex original; |
| REQUIRE(original.compile(R"(^/alpha/bravo/[?]((?!action=(newsfeed|calendar|contacts|notepad)).)*$)")); |
| |
| std::string subject{"/alpha/bravo/?"}; |
| subject.append(256 * 1024, 'x'); |
| |
| RegexMatches original_matches; |
| int const original_rc = original.exec(subject, original_matches); |
| CAPTURE(original_rc); |
| |
| SECTION("copy constructor") |
| { |
| Regex copy(original); |
| RegexMatches matches; |
| int const rc = copy.exec(subject, matches); |
| CAPTURE(rc); |
| CHECK(rc == original_rc); |
| } |
| |
| SECTION("copy assignment") |
| { |
| Regex copy; |
| REQUIRE(copy.compile("unrelated")); |
| copy = original; |
| |
| RegexMatches matches; |
| int const rc = copy.exec(subject, matches); |
| CAPTURE(rc); |
| CHECK(rc == original_rc); |
| } |
| |
| SECTION("a copy of a copy") |
| { |
| Regex first(original); |
| Regex second(first); |
| RegexMatches matches; |
| int const rc = second.exec(subject, matches); |
| CAPTURE(rc); |
| CHECK(rc == original_rc); |
| } |
| |
| SECTION("a copy still matches what the original matches") |
| { |
| Regex copy(original); |
| std::string const ordinary{"/alpha/bravo/?action=weather"}; |
| |
| RegexMatches original_ordinary; |
| RegexMatches copy_ordinary; |
| CHECK(original.exec(ordinary, original_ordinary) == copy.exec(ordinary, copy_ordinary)); |
| } |
| } |