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";