blob: 6cda5f7a81363546f749183a31101d7ed86e6a37 [file]
/**
@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 = &regex;
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));
}
}