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