Commit 3e98e31d6d for perl
commit 3e98e31d6d13a987f4b5031fb0811c6d08b9e68e
Author: Yves Orton <demerphq@gmail.com>
Date: Sun Oct 18 13:39:20 2026 +0200
regex: do not split folds when extracting trie prefixes
A UTF-8-flagged ssx|sy pattern must match sharp-s followed by x under
/iu. Extracting the first s as a prefix splits the fold, so neither node
can match the input character. This regressed in 9c721a4477.
Record the boundaries inside multi-character folds while building the
words. Only extract a prefix at a boundary which is safe for every
alternative, including overlapping folds in sss.
diff --git a/pod/perldelta.pod b/pod/perldelta.pod
index 0d79c45d3e..60ac918de8 100644
--- a/pod/perldelta.pod
+++ b/pod/perldelta.pod
@@ -431,6 +431,12 @@ manager will later use a regex to expand these into links.
=item *
+Case-insensitive trie prefix extraction no longer splits a multi-character
+fold between regex nodes. For example, a UTF-8 flagged C<ssx|sy> pattern
+under C</iu> now matches C<"\x{df}x">.
+
+=item *
+
A case-insensitive regular expression with a folded literal start class
could loop indefinitely when searching at the end of a UTF-8 string.
This could occur on a subsequent C</g> match after a successful match.
diff --git a/regcomp_trie.c b/regcomp_trie.c
index aeacb3d8b1..323da0aea4 100644
--- a/regcomp_trie.c
+++ b/regcomp_trie.c
@@ -588,6 +588,7 @@ Perl_make_trie(pTHX_ RExC_state_t *pRExC_state, regnode *startbranch,
* the same representation in both modes; awkward and high codepoints
* require processed mode. */
bool trie_needs_codepoint_processing;
+ bool unsafe_prefix[256] = { FALSE };
/* Store the trie and, in debugging builds, its word list. */
#ifdef DEBUGGING
@@ -805,6 +806,22 @@ Perl_make_trie(pTHX_ RExC_state_t *pRExC_state, regnode *startbranch,
uvc = (U32)*uc;
len = 1;
}
+ /* An input character can fold to several source
+ * characters. Do not extract a prefix ending inside
+ * that fold, including overlapping folds in "sss". */
+ if (folder && UTF && wordlen < 256) {
+ const STRLEN folded_octets
+ = is_MULTI_CHAR_FOLD_utf8_safe(uc, e);
+ if (folded_octets) {
+ const U32 folded_chars
+ = is_THREE_CHAR_FOLD_HEAD_utf8_safe(
+ uc, uc + folded_octets)
+ ? 3 : 2;
+ for (U32 i = 1; i < folded_chars
+ && wordlen - 1 + i < 256; i++)
+ unsafe_prefix[wordlen - 1 + i] = TRUE;
+ }
+ }
TRIE_CHARCOUNT(trie)++;
/* YJO: KHW says that we dont need a NATIVE_TO_UNI() on uvc here */
@@ -1473,9 +1490,12 @@ Perl_make_trie(pTHX_ RExC_state_t *pRExC_state, regnode *startbranch,
octets++;
state = next_state;
if (!need) {
- complete_state = state;
- complete_octets = octets;
- complete_chars = ++chars;
+ chars++;
+ if (!unsafe_prefix[chars]) {
+ complete_state = state;
+ complete_octets = octets;
+ complete_chars = chars;
+ }
}
}
state = complete_state;
diff --git a/t/re/pat.t b/t/re/pat.t
index 46989c9a2e..8e33d4104a 100644
--- a/t/re/pat.t
+++ b/t/re/pat.t
@@ -28,7 +28,7 @@ skip_all_without_unicode_tables();
my $has_locales = locales_enabled('LC_CTYPE');
my $utf8_locale = find_utf8_ctype_locale();
-plan tests => 2438 + 6 * $Config{uvsize}; # Update when adding/deleting tests.
+plan tests => 2630 + 6 * $Config{uvsize}; # Update when adding/deleting tests.
run_tests() unless caller;
@@ -167,6 +167,47 @@ sub run_tests {
}
}
+ {
+ my @cases = (
+ ['ssx|sy', "\x{df}x!"],
+ ['sssx|ssy', "s\x{df}x!"],
+ ['ffix|fy', "\x{fb03}x!"],
+ ['ffix|ffy', "\x{fb03}x!"],
+ ["\x{3b9}\x{308}\x{301}x|\x{3b9}y", "\x{390}x!"],
+ ["\x{565}\x{582}x|\x{565}y", "\x{587}x!"],
+ );
+ for my $case (@cases) {
+ for my $charset ('u', 'a', 'aa', 'd') {
+ for my $jump (0, 1) {
+ my $pattern = join '|', map {
+ $_ . ($jump ? '([!]?)' : '')
+ } split /\|/, $case->[0];
+ for my $pattern_utf8 (0, 1) {
+ my $source = "(?i$charset:$pattern)";
+ utf8::upgrade($source) if $pattern_utf8;
+ my ($plain, $trie);
+ { local ${^RE_TRIE_MAXBUF} = -1;
+ $plain = qr/$source(?#whole fold branches)/; }
+ { local ${^RE_TRIE_MAXBUF} = 65536;
+ $trie = qr/$source(?#whole fold trie)/; }
+ for my $target_utf8 (0, 1) {
+ my $target = '??' . $case->[1];
+ utf8::upgrade($target) if $target_utf8;
+ my @result;
+ for my $re ($plain, $trie) {
+ push @result, $target =~ $re
+ ? join(':', map { defined $_ ? $_ : '-' } @-, @+)
+ : 'no match';
+ }
+ is($result[1], $result[0],
+ "trie prefix preserves folds: /$charset, jump=$jump, pattern_utf8=$pattern_utf8, target_utf8=$target_utf8");
+ }
+ }
+ }
+ }
+ }
+ }
+
{
my @folded_backtracks = (
['ss', "\x{df}"],