On native Windows, LC_MESSAGES does not exist in <locale.h>; Gnulib adds
a definition of it. Accordingly, setlocale() needs to be taught to handle
the LC_MESSAGES category as part of LC_ALL. The code for doing this was
incomplete.

The first of these patches fixes it. The second patch adds a unit test.


2024-12-23  Bruno Haible  <br...@clisp.org>

        setlocale tests: Add unit test for LC_MESSAGES handling.
        * tests/test-setlocale-w32.c: New file.
        * modules/setlocale-tests (Files): Add it.
        (Makefile.am): Arrange to compile and run test-setlocale-w32.

        setlocale: Handle LC_MESSAGES correctly on native Windows.
        * lib/setlocale.c (setlocale_improved): Revamp the LC_ALL case if
        LC_MESSAGES is 1729.

>From 42d37457039260786d2a75e96749a2d74ee7e0d4 Mon Sep 17 00:00:00 2001
From: Bruno Haible <br...@clisp.org>
Date: Mon, 23 Dec 2024 16:43:17 +0100
Subject: [PATCH 1/2] setlocale: Handle LC_MESSAGES correctly on native
 Windows.

* lib/setlocale.c (setlocale_improved): Revamp the LC_ALL case if
LC_MESSAGES is 1729.
---
 ChangeLog       |   6 ++
 lib/setlocale.c | 166 +++++++++++++++++++++++++++++++++++++++---------
 2 files changed, 143 insertions(+), 29 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index fa56b8cda3..ceab98b058 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,9 @@
+2024-12-23  Bruno Haible  <br...@clisp.org>
+
+	setlocale: Handle LC_MESSAGES correctly on native Windows.
+	* lib/setlocale.c (setlocale_improved): Revamp the LC_ALL case if
+	LC_MESSAGES is 1729.
+
 2024-12-22  Bruno Haible  <br...@clisp.org>
 
 	c32isprint tests: Avoid test failure on mingw/ucrt.
diff --git a/lib/setlocale.c b/lib/setlocale.c
index eb263617dc..62dce81de3 100644
--- a/lib/setlocale.c
+++ b/lib/setlocale.c
@@ -1608,7 +1608,7 @@ setlocale_improved (int category, const char *locale)
 
           /* All steps were successful.  */
           free (saved_locale);
-          return setlocale (LC_ALL, NULL);
+          goto ret_all;
 
         fail:
           if (saved_locale[0] != '\0') /* don't risk an endless recursion */
@@ -1628,44 +1628,152 @@ setlocale_improved (int category, const char *locale)
     }
   else
     {
-#  if defined _WIN32 && ! defined __CYGWIN__
-      if (category == LC_ALL && locale != NULL && strchr (locale, '.') != NULL)
+#  if LC_MESSAGES == 1729
+      if (locale != NULL)
         {
-          char *saved_locale;
+          char truncated_locale[SETLOCALE_NULL_ALL_MAX];
+          const char *native_locale;
+          const char *messages_locale;
 
-          /* Back up the old locale.  */
-          saved_locale = setlocale (LC_ALL, NULL);
-          if (saved_locale == NULL)
-            return NULL;
-          saved_locale = strdup (saved_locale);
-          if (saved_locale == NULL)
-            return NULL;
-
-          if (setlocale_unixlike (LC_ALL, locale) == NULL)
+          if (strncmp (locale, "LC_COLLATE=", 11) == 0)
             {
-              free (saved_locale);
-              return NULL;
+              /* The locale argument indicates a mixed locale.  It must be of
+                 the form
+                 "LC_COLLATE=...;LC_CTYPE=...;LC_MONETARY=...;LC_NUMERIC=...;LC_TIME=...;LC_MESSAGES=..."
+                 since that is what this function returns (see ret_all below).
+                 Decompose it.  */
+              const char *last_semicolon = strrchr (locale, ';');
+              if (!(last_semicolon != NULL
+                    && strncmp (last_semicolon + 1, "LC_MESSAGES=", 12) == 0))
+                return NULL;
+              if (category == LC_MESSAGES)
+                return setlocale_single (category, last_semicolon + 13);
+              size_t truncated_locale_len = last_semicolon - locale;
+              if (truncated_locale_len >= sizeof (truncated_locale))
+                return NULL;
+              memcpy (truncated_locale, locale, truncated_locale_len);
+              truncated_locale[truncated_locale_len] = '\0';
+              native_locale = truncated_locale;
+              messages_locale = last_semicolon + 13;
             }
-
-          /* On native Windows, setlocale(LC_ALL,...) may succeed but set the
-             LC_CTYPE category to an invalid value ("C") when it does not
-             support the specified encoding.  Report a failure instead.  */
-          if (strcmp (setlocale (LC_CTYPE, NULL), "C") == 0)
+          else
             {
-              if (saved_locale[0] != '\0') /* don't risk an endless recursion */
-                setlocale (LC_ALL, saved_locale);
-              free (saved_locale);
-              return NULL;
+              native_locale = locale;
+              messages_locale = locale;
             }
 
-          /* It was really successful.  */
-          free (saved_locale);
-          return setlocale (LC_ALL, NULL);
+          if (category == LC_ALL)
+            {
+              /* In the underlying implementation, LC_ALL does not contain
+                 LC_MESSAGES.  Therefore we need to handle LC_MESSAGES
+                 separately.  */
+              char *result;
+
+#   if defined _WIN32 && ! defined __CYGWIN__
+              if (strchr (native_locale, '.') != NULL)
+                {
+                  char *saved_locale;
+
+                  /* Back up the old locale.  */
+                  saved_locale = setlocale (LC_ALL, NULL);
+                  if (saved_locale == NULL)
+                    return NULL;
+                  saved_locale = strdup (saved_locale);
+                  if (saved_locale == NULL)
+                    return NULL;
+
+                  if (setlocale_unixlike (LC_ALL, native_locale) == NULL)
+                    {
+                      free (saved_locale);
+                      return NULL;
+                    }
+
+                  /* On native Windows, setlocale(LC_ALL,...) may succeed but
+                     set the LC_CTYPE category to an invalid value ("C") when
+                     it does not support the specified encoding.  Report a
+                     failure instead.  */
+                  if (strcmp (setlocale (LC_CTYPE, NULL), "C") == 0)
+                    {
+                      /* Don't risk an endless recursion.  */
+                      if (saved_locale[0] != '\0')
+                        setlocale (LC_ALL, saved_locale);
+                      free (saved_locale);
+                      return NULL;
+                    }
+
+                  /* It was really successful.  */
+                  free (saved_locale);
+                  result = setlocale (LC_ALL, NULL);
+                }
+              else
+#   endif
+                result = setlocale_single (LC_ALL, native_locale);
+              if (result == NULL)
+                return NULL;
+
+              setlocale_single (LC_MESSAGES, messages_locale);
+
+              goto ret_all;
+            }
+          else
+            {
+              if (category == LC_MESSAGES)
+                return setlocale_single (category, messages_locale);
+              else
+                return setlocale_single (category, native_locale);
+            }
         }
-      else
+      else /* locale == NULL */
+        {
+          if (category == LC_ALL)
+            goto ret_all;
+          else
+            return setlocale_single (category, NULL);
+        }
+#  else
+      return setlocale_single (category, locale);
 #  endif
-        return setlocale_single (category, locale);
     }
+
+ ret_all:
+  /* Return the name of all categories of the current locale.  */
+#  if LC_MESSAGES == 1729 /* native Windows */
+  /* The locale name for mixed locales looks like this:
+     "LC_COLLATE=...;LC_CTYPE=...;LC_MONETARY=...;LC_NUMERIC=...;LC_TIME=..."
+     If necessary, add ";LC_MESSAGES=..." at the end.  */
+  {
+    char *name1 = setlocale (LC_ALL, NULL);
+    char *name2 = setlocale_single (LC_MESSAGES, NULL);
+    if (strcmp (name1, name2) == 0)
+      /* Not a mixed locale.  */
+      return name1;
+    else
+      {
+        static char resultbuf[SETLOCALE_NULL_ALL_MAX];
+        /* Prepare the result in a stack-allocated buffer, in order to reduce
+           race conditions in a multithreaded situation.  */
+        char stackbuf[SETLOCALE_NULL_ALL_MAX];
+        if (strncmp (name1, "LC_COLLATE=", 11) == 0)
+          {
+            if (strlen (name1) + strlen (name2) + 13 >= sizeof (stackbuf))
+              return NULL;
+            sprintf (stackbuf, "%s;LC_MESSAGES=%s", name1, name2);
+          }
+        else
+          {
+            if (5 * strlen (name1) + strlen (name2) + 68 >= sizeof (stackbuf))
+              return NULL;
+            sprintf (stackbuf,
+                     "LC_COLLATE=%s;LC_CTYPE=%s;LC_MONETARY=%s;LC_NUMERIC=%s;LC_TIME=%s;LC_MESSAGES=%s",
+                     name1, name1, name1, name1, name1, name2);
+          }
+        strcpy (resultbuf, stackbuf);
+        return resultbuf;
+      }
+  }
+#  else
+  return setlocale (LC_ALL, NULL);
+#  endif
 }
 
 # endif /* NEED_SETLOCALE_IMPROVED */
-- 
2.43.0

>From e5e97faef77abb7521f12e42d39b4cfcc4b8f1de Mon Sep 17 00:00:00 2001
From: Bruno Haible <br...@clisp.org>
Date: Mon, 23 Dec 2024 16:51:51 +0100
Subject: [PATCH 2/2] setlocale tests: Add unit test for LC_MESSAGES handling.

* tests/test-setlocale-w32.c: New file.
* modules/setlocale-tests (Files): Add it.
(Makefile.am): Arrange to compile and run test-setlocale-w32.
---
 ChangeLog                  |   5 ++
 modules/setlocale-tests    |   6 +-
 tests/test-setlocale-w32.c | 145 +++++++++++++++++++++++++++++++++++++
 3 files changed, 154 insertions(+), 2 deletions(-)
 create mode 100644 tests/test-setlocale-w32.c

diff --git a/ChangeLog b/ChangeLog
index ceab98b058..c294898828 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,10 @@
 2024-12-23  Bruno Haible  <br...@clisp.org>
 
+	setlocale tests: Add unit test for LC_MESSAGES handling.
+	* tests/test-setlocale-w32.c: New file.
+	* modules/setlocale-tests (Files): Add it.
+	(Makefile.am): Arrange to compile and run test-setlocale-w32.
+
 	setlocale: Handle LC_MESSAGES correctly on native Windows.
 	* lib/setlocale.c (setlocale_improved): Revamp the LC_ALL case if
 	LC_MESSAGES is 1729.
diff --git a/modules/setlocale-tests b/modules/setlocale-tests
index 78060d1ccc..ad0a536bc6 100644
--- a/modules/setlocale-tests
+++ b/modules/setlocale-tests
@@ -3,6 +3,7 @@ tests/test-setlocale1.sh
 tests/test-setlocale1.c
 tests/test-setlocale2.sh
 tests/test-setlocale2.c
+tests/test-setlocale-w32.c
 tests/signature.h
 tests/macros.h
 m4/locale-fr.m4
@@ -20,12 +21,13 @@ gt_LOCALE_JA
 gt_LOCALE_ZH_CN
 
 Makefile.am:
-TESTS += test-setlocale1.sh test-setlocale2.sh
+TESTS += test-setlocale1.sh test-setlocale2.sh test-setlocale-w32
 TESTS_ENVIRONMENT += \
   LOCALE_FR='@LOCALE_FR@' \
   LOCALE_FR_UTF8='@LOCALE_FR_UTF8@' \
   LOCALE_JA='@LOCALE_JA@' \
   LOCALE_ZH_CN='@LOCALE_ZH_CN@'
-check_PROGRAMS += test-setlocale1 test-setlocale2
+check_PROGRAMS += test-setlocale1 test-setlocale2 test-setlocale-w32
 test_setlocale1_LDADD = $(LDADD) @SETLOCALE_LIB@
 test_setlocale2_LDADD = $(LDADD) @SETLOCALE_LIB@
+test_setlocale_w32_LDADD = $(LDADD) @SETLOCALE_LIB@
diff --git a/tests/test-setlocale-w32.c b/tests/test-setlocale-w32.c
new file mode 100644
index 0000000000..5dd1d8cf84
--- /dev/null
+++ b/tests/test-setlocale-w32.c
@@ -0,0 +1,145 @@
+/* Test of setting the current locale.
+   Copyright (C) 2024 Free Software Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
+
+#include <config.h>
+
+#include <locale.h>
+
+#include <stdio.h>
+#include <string.h>
+
+#include "macros.h"
+
+#if defined _WIN32 && !defined __CYGWIN__
+
+# define ENGLISH   "English_United States"
+# define FRENCH    "French_France"
+# define GERMAN    "German_Germany"
+# define BRAZILIAN "Portuguese_Brazil"
+# define CATALAN   "Catalan_Spain"
+# define ENCODING ".1252"
+
+# define LOCALE1 GERMAN ENCODING
+# define LOCALE2 FRENCH ENCODING
+# define LOCALE3 ENGLISH ENCODING
+# define LOCALE4 BRAZILIAN ENCODING
+# define LOCALE5 CATALAN ENCODING
+
+int
+main ()
+{
+  const char *ret;
+  const char *ret_all;
+
+  /* Set a mixed locale.  */
+  if (setlocale (LC_ALL, LOCALE1) == NULL)
+    {
+      fprintf (stderr, "Setting locale %s failed.\n", LOCALE1);
+      return 1;
+    }
+  if (setlocale (LC_NUMERIC, LOCALE2) == NULL)
+    {
+      fprintf (stderr, "Setting locale %s failed.\n", LOCALE2);
+      return 1;
+    }
+  if (setlocale (LC_TIME, LOCALE3) == NULL)
+    {
+      fprintf (stderr, "Setting locale %s failed.\n", LOCALE3);
+      return 1;
+    }
+  if (setlocale (LC_MESSAGES, LOCALE4) == NULL)
+    {
+      fprintf (stderr, "Setting locale %s failed.\n", LOCALE4);
+      return 1;
+    }
+  if (setlocale (LC_MONETARY, LOCALE5) == NULL)
+    {
+      fprintf (stderr, "Setting locale %s failed.\n", LOCALE5);
+      return 1;
+    }
+
+  /* Check the individual categories.  */
+  ret = setlocale (LC_COLLATE, NULL);
+  ASSERT (ret != NULL && strcmp (ret, LOCALE1) == 0);
+  ret = setlocale (LC_CTYPE, NULL);
+  ASSERT (ret != NULL && strcmp (ret, LOCALE1) == 0);
+  ret = setlocale (LC_MONETARY, NULL);
+  ASSERT (ret != NULL && strcmp (ret, LOCALE5) == 0);
+  ret = setlocale (LC_NUMERIC, NULL);
+  ASSERT (ret != NULL && strcmp (ret, LOCALE2) == 0);
+  ret = setlocale (LC_TIME, NULL);
+  ASSERT (ret != NULL && strcmp (ret, LOCALE3) == 0);
+  ret = setlocale (LC_MESSAGES, NULL);
+  ASSERT (ret != NULL && strcmp (ret, LOCALE4) == 0);
+  ret = setlocale (LC_ALL, NULL);
+  ASSERT (ret != NULL
+          && strcmp (ret, "LC_COLLATE=" LOCALE1 ";"
+                          "LC_CTYPE=" LOCALE1 ";"
+                          "LC_MONETARY=" LOCALE5 ";"
+                          "LC_NUMERIC=" LOCALE2 ";"
+                          "LC_TIME=" LOCALE3 ";"
+                          "LC_MESSAGES=" LOCALE4)
+             == 0);
+  ret_all = _strdup (ret);
+
+  /* Reset the locale.  */
+  ASSERT (setlocale (LC_ALL, "C") != NULL);
+
+  ret = setlocale (LC_ALL, NULL);
+  ASSERT (ret != NULL && strcmp (ret, "C") == 0);
+  ret = setlocale (LC_MESSAGES, NULL);
+  ASSERT (ret != NULL && strcmp (ret, "C") == 0);
+
+  /* Restore the locale.  */
+  ret = setlocale (LC_ALL, ret_all);
+  ASSERT (ret != NULL && strcmp (ret, ret_all) == 0);
+
+  /* Check the individual categories again.  */
+  ret = setlocale (LC_COLLATE, NULL);
+  ASSERT (ret != NULL && strcmp (ret, LOCALE1) == 0);
+  ret = setlocale (LC_CTYPE, NULL);
+  ASSERT (ret != NULL && strcmp (ret, LOCALE1) == 0);
+  ret = setlocale (LC_MONETARY, NULL);
+  ASSERT (ret != NULL && strcmp (ret, LOCALE5) == 0);
+  ret = setlocale (LC_NUMERIC, NULL);
+  ASSERT (ret != NULL && strcmp (ret, LOCALE2) == 0);
+  ret = setlocale (LC_TIME, NULL);
+  ASSERT (ret != NULL && strcmp (ret, LOCALE3) == 0);
+  ret = setlocale (LC_MESSAGES, NULL);
+  ASSERT (ret != NULL && strcmp (ret, LOCALE4) == 0);
+  ret = setlocale (LC_ALL, NULL);
+  ASSERT (ret != NULL
+          && strcmp (ret, "LC_COLLATE=" LOCALE1 ";"
+                          "LC_CTYPE=" LOCALE1 ";"
+                          "LC_MONETARY=" LOCALE5 ";"
+                          "LC_NUMERIC=" LOCALE2 ";"
+                          "LC_TIME=" LOCALE3 ";"
+                          "LC_MESSAGES=" LOCALE4)
+             == 0);
+
+  return test_exit_status;
+}
+
+#else
+
+int
+main ()
+{
+  fputs ("Skipping test: not a native Windows system\n", stderr);
+  return 77;
+}
+
+#endif
-- 
2.43.0

Reply via email to