From c88870750cca6468601d636e9476016e6121e1e7 Mon Sep 17 00:00:00 2001 From: Mo Chen Date: Tue, 28 Jul 2026 10:18:36 -0500 Subject: [PATCH] Return an empty view for a non-participating capture group RegexMatches::operator[] only checked the index against the ovector count. A group that does not participate in the match has unset offsets, and an optional group that precedes a participating one is still within that count, so the check passes and the subject pointer is advanced by PCRE2_UNSET. The resulting view has length zero, so callers see an empty string today, but the pointer is invalid. --- src/tsutil/Regex.cc | 7 +++++++ src/tsutil/unit_tests/test_Regex.cc | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/tsutil/Regex.cc b/src/tsutil/Regex.cc index 0e76c50ce18..0e10036c5dd 100644 --- a/src/tsutil/Regex.cc +++ b/src/tsutil/Regex.cc @@ -205,6 +205,13 @@ RegexMatches::operator[](size_t index) const } PCRE2_SIZE *ovector = pcre2_get_ovector_pointer(_MatchData::get(_match_data)); + + // A group that did not participate in the match has an unset offset. This happens for an optional + // group that precedes a participating one, so a valid index is not enough to guarantee an offset. + if (PCRE2_UNSET == ovector[2 * index]) { + return std::string_view(); + } + return std::string_view(_subject.data() + ovector[2 * index], ovector[2 * index + 1] - ovector[2 * index]); } diff --git a/src/tsutil/unit_tests/test_Regex.cc b/src/tsutil/unit_tests/test_Regex.cc index 8312146060f..c88031c6f90 100644 --- a/src/tsutil/unit_tests/test_Regex.cc +++ b/src/tsutil/unit_tests/test_Regex.cc @@ -460,6 +460,23 @@ TEST_CASE("RegexMatches edge cases", "[libts][Regex][RegexMatches]") 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)") == true); + + 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); + } } TEST_CASE("Regex with special characters", "[libts][Regex][special]")