Commit 1dda70872c for perl
commit 1dda70872c82ca72322d916322cb5a85b54abbc8
Author: Yves Orton <demerphq@gmail.com>
Date: Sun Oct 18 13:45:59 2026 +0200
regex: reject trie words ending inside a character fold
Under /iu, s|zoo must not match sharp-s by consuming only the first s of
its fold. When finding the end of a trie word, reject it if part of the
input character's fold is left over.
Try the next word in alternative order before running a continuation. A
longer word may consume the whole fold and match.
diff --git a/pod/perldelta.pod b/pod/perldelta.pod
index 10aa12472e..310d0b9108 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 *
+A case-insensitive regex trie no longer accepts a word ending partway through
+an input character's fold. For example, C</s|zoo/iu> no longer matches a
+sharp-s, whose full fold is C<ss>. Later alternatives are tried in order.
+
+=item *
+
A folded regex trie now updates its minimum and maximum lengths independently.
An alternative which changes both bounds could previously cause a valid
match to be missed or an assertion failure during Aho-Corasick scanning.
diff --git a/regexec.c b/regexec.c
index 9c0ef28a8b..02c986a8a5 100644
--- a/regexec.c
+++ b/regexec.c
@@ -7479,6 +7479,7 @@ S_regmatch(pTHX_ regmatch_info *reginfo, char *startpos, regnode *prog)
CAPTURE_CLEAR(ST.before_paren+1, ST.after_paren, "TRIE_next_fail");
}
}
+ trie_next_word:
if (!--ST.accepted) {
DEBUG_EXECUTE_r({
re_exec_indentf("%sTRIE failed...%s\n",
@@ -7550,13 +7551,19 @@ S_regmatch(pTHX_ regmatch_info *reginfo, char *startpos, regnode *prog)
uvc = to_uni_fold(uvc, foldbuf, &foldlen);
uscan = foldbuf;
while (foldlen) {
- if (!--chars)
- break;
- /* The folder produces valid UTF-8. Only its
+ /* The folder produces valid UTF-8. Only its
* character boundaries are needed here. */
len = UTF8SKIP(uscan);
uscan += len;
foldlen -= len;
+ if (!--chars) {
+ /* A word ending inside this input
+ * character's fold is not a match. Try
+ * the next word before running its tail. */
+ if (foldlen)
+ goto trie_next_word;
+ break;
+ }
}
}
}
diff --git a/t/re/pat.t b/t/re/pat.t
index 8e33d4104a..c7c60913dd 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 => 2630 + 6 * $Config{uvsize}; # Update when adding/deleting tests.
+plan tests => 2726 + 6 * $Config{uvsize}; # Update when adding/deleting tests.
run_tests() unless caller;
@@ -208,6 +208,43 @@ sub run_tests {
}
}
+ {
+ my @cases = (
+ ['s|zoo', "\x{df}!"],
+ ['s([!]?)|ss([!]?)', "\x{df}!"],
+ ['f([!]?)|ffi([!]?)', "\x{fb03}!"],
+ ['ff([!]?)|ffi([!]?)', "\x{fb03}!"],
+ ['ss([!]?)|s([!]?)', "\x{df}!"],
+ ["\x{3b9}\x{308}([!]?)|\x{3b9}\x{308}\x{301}([!]?)",
+ "\x{390}!"],
+ );
+ for my $case (@cases) {
+ for my $charset ('u', 'a', 'aa', 'd') {
+ for my $pattern_utf8 (0, 1) {
+ my $source = "\\A(?i$charset:$case->[0])";
+ utf8::upgrade($source) if $pattern_utf8;
+ my ($plain, $trie);
+ { local ${^RE_TRIE_MAXBUF} = -1;
+ $plain = qr/$source(?#fold endpoints branches)/; }
+ { local ${^RE_TRIE_MAXBUF} = 65536;
+ $trie = qr/$source(?#fold endpoints 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 acceptance ends at a complete fold: /$charset, pattern_utf8=$pattern_utf8, target_utf8=$target_utf8");
+ }
+ }
+ }
+ }
+ }
+
{
my @folded_backtracks = (
['ss', "\x{df}"],