Commit 7fb635f95f for perl
commit 7fb635f95f08105b5e84dcec3eab228b4a9dee5c
Author: Alin Iacob <alinbsp@gmail.com>
Date: Thu Sep 24 13:04:24 2026 +0300
sv.c: restore UTF-8 offset caching
The midway paths left canonical_position false, so substr() wasn't
updating the UTF-8 position cache.
Set at_end for the end position so it only updates the length cache.
Check for out-of-range offsets before calculating the backward
distance.
Based on a patch by kaz-utashiro.
Fixes #24531
diff --git a/sv.c b/sv.c
index 902c4f9c15..71458822ed 100644
--- a/sv.c
+++ b/sv.c
@@ -8676,6 +8676,10 @@ S_sv_pos_u2b_midway(const U8 *const start, const U8 *send,
{
PERL_ARGS_ASSERT_SV_POS_U2B_MIDWAY;
+ /* Avoid unsigned underflow for offsets beyond the end. */
+ if (uoffset >= uend)
+ return send - start;
+
STRLEN backw = uend - uoffset;
if (uoffset < 2 * backw) {
@@ -8745,6 +8749,8 @@ S_sv_pos_u2b_cached(pTHX_ SV *const sv, MAGIC **const mgp, const U8 *const start
}
if ((*mgp)->mg_len != -1) {
/* And we know the end too. */
+ canonical_position = uoffset <= (STRLEN)(*mgp)->mg_len;
+ at_end = uoffset >= (STRLEN)(*mgp)->mg_len;
boffset = boffset0
+ sv_pos_u2b_midway(start + boffset0, send,
uoffset - uoffset0,
@@ -8766,12 +8772,14 @@ S_sv_pos_u2b_cached(pTHX_ SV *const sv, MAGIC **const mgp, const U8 *const start
boffset0 = cache[3];
}
+ canonical_position = TRUE;
boffset = boffset0
+ sv_pos_u2b_midway(start + boffset0,
start + cache[1],
uoffset - uoffset0,
cache[0] - uoffset0);
} else {
+ canonical_position = TRUE;
boffset = boffset0
+ sv_pos_u2b_midway(start + boffset0,
start + cache[3],
@@ -8784,6 +8792,8 @@ S_sv_pos_u2b_cached(pTHX_ SV *const sv, MAGIC **const mgp, const U8 *const start
/* If we can take advantage of a passed in offset, do so. */
/* In fact, offset0 is either 0, or less than offset, so don't
need to worry about the other possibility. */
+ canonical_position = uoffset <= (STRLEN)(*mgp)->mg_len;
+ at_end = uoffset >= (STRLEN)(*mgp)->mg_len;
boffset = boffset0
+ sv_pos_u2b_midway(start + boffset0, send,
uoffset - uoffset0,
diff --git a/t/op/index.t b/t/op/index.t
index a218848851..2904ee0251 100644
--- a/t/op/index.t
+++ b/t/op/index.t
@@ -8,7 +8,7 @@ BEGIN {
}
use strict;
-plan( tests => 415 );
+plan( tests => 424 );
run_tests() unless caller;
@@ -366,4 +366,16 @@ EOS
is(index($s, "", $len+1), 3, 'Overlong index doesn\'t confuse utf8 cache');
}
+ {
+ local ${^UTF8CACHE} = 1; # enable cache, disable debugging
+ for my $text ("", "abc", "\x{100}" x 3) {
+ my $s = $text;
+ utf8::upgrade($s);
+ my $len = length($s);
+ is(index($s, "", $len), $len, 'index at the cached end');
+ is(index($s, "", $len+1), $len, 'index beyond the cached end');
+ is(rindex($s, "", $len+1), $len, 'rindex beyond the cached end');
+ }
+ }
+
} # end of sub run_tests
diff --git a/t/perf/speed.t b/t/perf/speed.t
index 7df4b4e9a9..ef456ec1b1 100644
--- a/t/perf/speed.t
+++ b/t/perf/speed.t
@@ -24,7 +24,7 @@ use 5.010;
$| = 1;
-plan tests => 1;
+plan tests => 2;
watchdog(60);
@@ -42,6 +42,15 @@ SKIP: {
pass("COW 1Mb strings");
}
+{
+ # GH #24531: substr wasn't updating the UTF-8 position cache.
+ local ${^UTF8CACHE} = 1; # enable cache, disable debugging
+ my $s = "\x{100}" x 1_000_000;
+ my $part;
+ $part = substr($s, 500_000, 10) for 1..100_000;
+ is($part, "\x{100}" x 10, 'repeated substr on a UTF-8 string');
+}
+
watchdog(0);
1;