Skip to content
104 changes: 91 additions & 13 deletions stl/inc/regex
Original file line number Diff line number Diff line change
Expand Up @@ -4861,6 +4861,23 @@ void _Matcher3<_Elem, _RxTraits, _It, _Alloc>::_Copy_captures(match_results<_Bid
_Matches._Null().second = _End;
}

template <class _FwdIt, class _Int>
void _Advance_at_most(_FwdIt& _Pos, const _FwdIt& _Last, _Int _Amount) {
if constexpr (_Is_ranges_random_iter_v<_FwdIt>) {
auto _Shift_amount = _Last - _Pos;
if (_Shift_amount > _Amount) {
_Shift_amount = static_cast<decltype(_Shift_amount)>(_Amount);
}

_Pos += _Shift_amount;
} else {
while (_Amount > 0 && _Pos != _Last) {
++_Pos;
--_Amount;
}
}
}

template <class _Elem, class _RxTraits, class _It, class _Alloc>
_It _Matcher3<_Elem, _RxTraits, _It, _Alloc>::_Skip(
_It _First, const _It _Last, const _Node_base* const _Node_arg, const unsigned int _Recursion_depth) {
Expand All @@ -4870,6 +4887,7 @@ _It _Matcher3<_Elem, _RxTraits, _It, _Alloc>::_Skip(
static constexpr wchar_t _Line_terminators_wchar_t[] = {static_cast<wchar_t>(_Meta_cr),
static_cast<wchar_t>(_Meta_nl), static_cast<wchar_t>(_Meta_ls), static_cast<wchar_t>(_Meta_ps)};
constexpr unsigned int _Max_recursion_depth = 50U;
constexpr short _Max_lookahead = 512;
const _Node_base* _Nx = _Node_arg ? _Node_arg : _Start;

while (_First != _Last && _Nx) { // check current node
Expand Down Expand Up @@ -4917,17 +4935,29 @@ _It _Matcher3<_Elem, _RxTraits, _It, _Alloc>::_Skip(

case _N_str:
{ // check for string match
const auto _Node = static_cast<const _Node_str<_Elem>*>(_Nx);
const auto _Str = _Node->_Data._Str();
return _STD _Search_translate_left(_First, _Last, _Str, _Str + _Node->_Data._Size(), _Traits, _Sflags);
const auto _Node = static_cast<const _Node_str<_Elem>*>(_Nx);
const auto _Str = _Node->_Data._Str();
const unsigned int _Size = _Node->_Data._Size();

auto _Shifted_last = _Last;
if (_Last != _End && _Size > 1U) { // have to continue search beyond the search window
_STD _Advance_at_most(_Shifted_last, _End, static_cast<long long>(_Size - 1U));
}

auto _Result = _STD _Search_translate_left(_First, _Shifted_last, _Str, _Str + _Size, _Traits, _Sflags);
if (_Shifted_last == _Result) { // correct for search window shift
return _Last;
} else {
return _Result;
}
}

case _N_class:
{ // check for string match
const auto _Node = static_cast<const _Node_class<_Elem, _RxTraits>*>(_Nx);

for (; _First != _Last; ++_First) { // look for starting match
if (_Do_class(_Node, _First) != _First) {
if (_Do_class(_Node, _First) != _First) { // may read beyond search window
return _First;
}
}
Expand All @@ -4952,27 +4982,75 @@ _It _Matcher3<_Elem, _RxTraits, _It, _Alloc>::_Skip(

case _N_if:
{
// GH-5452: If this node has two or more branches,
// examining all these branches has quadratic worst-case complexity.
// Thus, we only continue if this node has a single branch only.
const auto _Node = static_cast<const _Node_if*>(_Nx);

// TRANSITION, ABI: After GH-5539, the parser no longer generates single-branch _N_if nodes.
// But we have to retain this special handling to avoid performance regression
// But we retain this special handling to avoid some performance regression
// when an old parser gets mixed with a new matcher.
const auto _Node = static_cast<const _Node_if*>(_Nx);
if (!_Node->_Child) {
break;
}

if (_Node->_Child) {
if (_Recursion_depth >= _Max_recursion_depth) {
return _First;
}
break;

// GH-5452: If this node has two or more branches, examining all alternatives in a disjunction
// until the end of the input string could result in quadratic worst-case complexity.
// For this reason, we split the input string into search windows with a constant maximum length.
// This ensures that this heuristic has linear time complexity, because this bounds the number
// of characters that were read beyond the finally determined skip position by a constant.

for (;;) {
_It _Lookahead_last = _First;
_STD _Advance_at_most(_Lookahead_last, _Last, _Max_lookahead);

_It _Alt_last = _Lookahead_last;

for (const _Node_if* _Alternative = _Node; _Alternative && _First != _Alt_last;
_Alternative = _Alternative->_Child) {
_Alt_last = _Skip(_First, _Alt_last, _Alternative->_Next, _Recursion_depth + 1U);
}

_First = _Alt_last;
if (_First != _Lookahead_last || _First == _Last) {
break;
}
}

return _First;
}

case _N_rep:
{
const auto _Node = static_cast<const _Node_rep*>(_Nx);
if (_Node->_Min == 0) {

if (_Node->_Min > 0) {
break;
}

if (_Recursion_depth >= _Max_recursion_depth) {
return _First;
}
break;

// As in GH-5452, examining the cases with no repetition and at least one repetition
// until the end of the input string could result in quadratic worst-case complexity.
// For this reason, we split the input string into search windows with a constant maximum length.
// This ensures that this heuristic has linear time complexity, because this bounds the number
// of characters that were read beyond the finally determined skip position by a constant.

for (;;) {
_It _Lookahead_last = _First;
_STD _Advance_at_most(_Lookahead_last, _Last, _Max_lookahead);
const _It _Intermediate = _Skip(_First, _Lookahead_last, _Node->_Next, _Recursion_depth + 1U);
_First = _Skip(_First, _Intermediate, _Node->_End_rep->_Next, _Recursion_depth + 1U);

if (_First != _Lookahead_last || _First == _Last) {
break;
}
}

return _First;
}

case _N_assert:
Expand Down
20 changes: 20 additions & 0 deletions tests/std/tests/GH_005204_regex_collating_ranges/test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

#include <algorithm>
#include <cassert>
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <locale>
Expand Down Expand Up @@ -656,11 +658,29 @@ void test_gh_5437() {
#endif // !defined(SKIP_COLLATE_TESTS)
}

void test_gh_6191() {
// GH-6191: Optimize searches for patterns with initial branching
// Check that collating elements are handled correctly at search window boundaries.
const gh_994_regex re("[[.dzs.]]|abc");
smatch sm;
for (size_t prefix_size = 510; prefix_size < 516; ++prefix_size) {
const string prefix(prefix_size, 'h');
const string dzs_before_abc = prefix + "dzshhhhhabchh";
assert(regex_search(dzs_before_abc, sm, re));
assert(string(sm[0].first, sm[0].second) == "dzs");

const string abc_before_dzs = prefix + "abchhhhhdzshh";
assert(regex_search(abc_before_dzs, sm, re));
assert(string(sm[0].first, sm[0].second) == "abc");
}
}

int main() {
test_collating_ranges_german();
test_gh_994();
test_gh_5435();
test_gh_5437();
test_gh_6191();

return g_regexTester.result();
}
58 changes: 58 additions & 0 deletions tests/std/tests/VSO_0000000_regex_use/test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

#include <cassert>
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <list>
#include <regex>
#include <string>

Expand Down Expand Up @@ -2522,6 +2524,61 @@ void test_gh_6189() {
re.should_search_fail("d");
}

void test_gh_6191() {
// GH-6191: Optimize searches for patterns with initial branching
// We must check that we handle matches near search window boundaries correctly.

{
const test_regex test_alt_re(&g_regexTester, "abcdef|uvwxyz");
test_alt_re.should_search_match("abcdef", "abcdef");
test_alt_re.should_search_match("uvwxyz", "uvwxyz");
test_alt_re.should_search_match("hhabcdef", "abcdef");
test_alt_re.should_search_match("hhuvwxyz", "uvwxyz");
test_alt_re.should_search_match("hhhhhuvwxyzhhhhhabcdefhhhhh", "uvwxyz");
for (size_t prefix_size = 510; prefix_size < 516; ++prefix_size) {
const string prefix(prefix_size, 'h');
test_alt_re.should_search_match(prefix + "abcdef", "abcdef");
test_alt_re.should_search_match(prefix + "uvwxyz", "uvwxyz");
test_alt_re.should_search_match(prefix + "abcdefhhhhhhhuvwxyz", "abcdef");
test_alt_re.should_search_match(prefix + "uvwxyzhhhhhhhabcdef", "uvwxyz");
test_alt_re.should_search_match(prefix + "abcdefhhhhhhhuvwxyzhhhh", "abcdef");
test_alt_re.should_search_match(prefix + "uvwxyzhhhhhhhabcdefhhhh", "uvwxyz");
}
}

{
const test_regex optional_prefix_re(&g_regexTester, "(abc)?def");
optional_prefix_re.should_search_match("abcdef", "abcdef");
optional_prefix_re.should_search_match("def", "def");
optional_prefix_re.should_search_match("hhabcdef", "abcdef");
optional_prefix_re.should_search_match("hhdef", "def");
optional_prefix_re.should_search_match("hhhhabcdefhhhhdefhhh", "abcdef");
optional_prefix_re.should_search_match("hhhdefhhhhabcdefhhh", "def");
for (size_t prefix_size = 510; prefix_size < 516; ++prefix_size) {
const string prefix(prefix_size, 'h');
optional_prefix_re.should_search_match(prefix + "abcdef", "abcdef");
optional_prefix_re.should_search_match(prefix + "def", "def");
optional_prefix_re.should_search_match(prefix + "abcdefhhhhhhhdef", "abcdef");
optional_prefix_re.should_search_match(prefix + "defhhhhhhhabcdef", "def");
}
}

// test bidirectional iterators
{
const regex alt_re("abcdef|uvwxyz");
const string suffix = "abcdefhhhhhhhuvwxyz";
list<char> input(509, 'h');
input.insert(input.end(), suffix.begin(), suffix.end());

for (size_t prefixes_to_test = 0; prefixes_to_test < 6; ++prefixes_to_test) {
input.push_front('h');
match_results<list<char>::const_iterator> results;
assert(regex_search(input.cbegin(), input.cend(), results, alt_re));
assert(string(results[0].first, results[0].second) == "abcdef");
}
}
}

int main() {
test_dev10_449367_case_insensitivity_should_work();
test_dev11_462743_regex_collate_should_not_disable_regex_icase();
Expand Down Expand Up @@ -2588,6 +2645,7 @@ int main() {
test_gh_6147();
test_gh_6181();
test_gh_6189();
test_gh_6191();

return g_regexTester.result();
}