Commit 5336cf65f6 for perl
commit 5336cf65f6bdbf8127f126bc26a3ad71eaeecdad
Author: Karl Williamson <khw@cpan.org>
Date: Mon Oct 19 20:38:33 2026 -0600
Pass raw bytes through to strftime()
Fixes #23307
strftime() implementations copy the input format string parameter to the
output unchanged, except for any sequences beginning with '%' that it
recognizes as special. Those are instead replaced by their documented
values, based on the current locale.
All these special sequences are ASCII-only, hence the same whether the
format is UTF-8 or not. Prior to this commit, our code rejected UTF-8
inputs with a non-UTF-8 locale. But to strftime(), they're just bytes,
so the rejection was unnecessary, and wrong.
This commit rectifies that. I retained the rejection when the locale is
unsupported by perl. These are multi-byte locales that potentially have
shift states. It seems risky to call strftime() on something where we
don't know anything about the shift states; there are still open tickets
about perl hanging on similar things.
diff --git a/locale.c b/locale.c
index ff59ff21df..982da82ab2 100644
--- a/locale.c
+++ b/locale.c
@@ -9226,20 +9226,41 @@ S_strftime8(pTHX_ const char * fmt,
locale_utf8ness = LOCALE_NOT_UTF8;
break;
- case UTF8NESS_YES: /* Known to be UTF-8; must be UTF-8 locale if can't
- downgrade. */
+ case UTF8NESS_YES:
+
+ /* Here, the format is known to be UTF-8. If the locale also is UTF-8,
+ * it should all work fine */
if (is_locale_utf8(locale)) {
locale_utf8ness = LOCALE_IS_UTF8;
}
else {
- locale_utf8ness = LOCALE_NOT_UTF8;
- Size_t fmt_len = strlen(fmt);
- if (! utf8_to_bytes_new_pv((const U8 **) &fmt, &fmt_len, &free_me))
- {
+ /* If the locale is unsupported, things could go wrong if we
+ * actually call strftime(), such as with shift states, so just
+ * return an empty result, setting errno. Such a locale would be
+ * a non-UTF-8 multi-byte locale. */
+ PERL_MB_CUR_MAX_LOCK;
+ const int mb_cur_max = MB_CUR_MAX;
+ PERL_MB_CUR_MAX_UNLOCK;
+
+ /* (Comments elsewhere in this file talk about the unreliability of
+ * the libc MB_CUR_MAX when the locale is C.) */
+ if (mb_cur_max > 1 && ! isNAME_C_OR_POSIX(locale)) {
SET_EINVAL;
return false;
}
+
+ /* strftime() implementations copy the input format string
+ * parameter to the output unchanged, except for any sequences
+ * beginning with '%' that it recognizes as special. Those are
+ * instead replaced by their documented values, based on the
+ * current locale. All the special sequences are ASCII-only,
+ * hence the same whether the format is UTF-8 or not. This means
+ * that the locale's UTF8ness and the format's UTF8ness being
+ * mismatched are irrelevant to strftime(). See GH #23307. But
+ * the result shouldn't be considered as UTF-8, just bytes,
+ * because this isn't a UTF-8 locale. */
+ locale_utf8ness = LOCALE_NOT_UTF8;
}
break;
diff --git a/t/run/locale.t b/t/run/locale.t
index 4cf2a97c72..5676603ee1 100644
--- a/t/run/locale.t
+++ b/t/run/locale.t
@@ -48,6 +48,7 @@ delete local @ENV{'LANGUAGE', 'LANG', (grep /^LC_[A-Z]+$/, keys %ENV)};
delete local $ENV{'PERL_DEBUG_LOCALE_INIT'} unless $debug;
my $has_ctype = grep { $_ eq "LC_CTYPE" } platform_locale_categories();
+my $has_time = grep { $_ eq "LC_TIME" } platform_locale_categories();
SKIP: {
skip("LC_CTYPE not available on the system", 1 ) unless $has_ctype;
@@ -74,6 +75,25 @@ EOF
}
+SKIP: { # GH #23307
+ skip("LC_TIME not available on the system", 1 ) unless $has_time;
+ my $test_case = "\x{430}\x{431}\x{432}";
+ my $bytes = $test_case;
+ utf8::encode($bytes); # Result in C locale should be byte string
+ fresh_perl_like(<<"EOF",
+ use strict;
+ use warnings;
+ use POSIX;
+ use utf8;
+
+ setlocale(LC_TIME(), "C");
+ setlocale(LC_CTYPE(), "C");
+ print strftime("$test_case %T\n", localtime);
+EOF
+ qr/^$bytes /, {},
+ "strftime in single-byte locale with non-ASCII chars works");
+}
+
my $non_C_locale;
foreach my $locale (@locales) {
next if $locale eq "C" || $locale eq 'POSIX' || $locale eq "C.UTF-8";