Due to a potential getenv() call for LANGUAGE envvar, multi-threaded programs may crash in gettext(), when there is another thread calling setenv(). To mitigate this, add a new API set_i18n_language() to allow setting language precedence list programatically. Suggested by Bruno Haible in: https://lists.gnu.org/archive/html/bug-gettext/2016-05/msg00009.html * gettext-runtime/intl/dcigettext.c (_nl_default_i18n_language) (_nl_current_i18n_language): New variable. (GET_I18N_LANGUAGE, SET_I18N_LANGUAGE): New functions. (guess_category_value): Check LANGUAGE envvar only when _nl_current_i18n_language is not set. * gettext-runtime/intl/libgnuintl.in.h (get_i18n_language) (set_i18n_language): New function declaration. * gettext-tools/tests/Makefile.am (TESTS): Add test for set_i18n_language(). * gettext-tools/tests/gettext-9: New test. * gettext-tools/tests/gettext-9-prg.c: New test program. * gettext-tools/tests/.gitignore: Ignore gettext-9-prg. --- gettext-runtime/intl/dcigettext.c | 54 ++++++++++++++++++++++-- gettext-runtime/intl/libgnuintl.in.h | 34 +++++++++++++++ gettext-tools/tests/.gitignore | 1 + gettext-tools/tests/Makefile.am | 4 +- gettext-tools/tests/gettext-9 | 56 ++++++++++++++++++++++++ gettext-tools/tests/gettext-9-prg.c | 82 ++++++++++++++++++++++++++++++++++++ 6 files changed, 226 insertions(+), 5 deletions(-) create mode 100755 gettext-tools/tests/gettext-9 create mode 100644 gettext-tools/tests/gettext-9-prg.c
diff --git a/gettext-runtime/intl/dcigettext.c b/gettext-runtime/intl/dcigettext.c index 83bd775..f474786 100644 --- a/gettext-runtime/intl/dcigettext.c +++ b/gettext-runtime/intl/dcigettext.c @@ -342,6 +342,9 @@ libc_hidden_data_def (_nl_default_dirname) struct binding *_nl_domain_bindings; #endif +static char _nl_default_i18n_language[] = ""; +static char *_nl_current_i18n_language = _nl_default_i18n_language; + /* Prototypes for local functions. */ static char *plural_lookup (struct loaded_l10nfile *domain, unsigned long int n, @@ -430,8 +433,12 @@ typedef unsigned char transmem_block_t; prefix. So we have to make a difference here. */ #ifdef _LIBC # define DCIGETTEXT __dcigettext +# define GET_I18N_LANGUAGE __get_i18n_language +# define SET_I18N_LANGUAGE __set_i18n_language #else # define DCIGETTEXT libintl_dcigettext +# define GET_I18N_LANGUAGE libintl_get_i18n_language +# define SET_I18N_LANGUAGE libintl_set_i18n_language #endif /* Lock variable to protect the global data in the gettext implementation. */ @@ -875,6 +882,43 @@ DCIGETTEXT (const char *domainname, const char *msgid1, const char *msgid2, } +const char * +GET_I18N_LANGUAGE (void) +{ + return _nl_current_i18n_language; +} + + +const char * +SET_I18N_LANGUAGE (const char *language) +{ + gl_rwlock_wrlock (_nl_state_lock); + + if (language == NULL) + language = getenv ("LANGUAGE"); + + if (language != NULL && language[0] != '\0') + { + char *new_language; + + new_language = strdup (language); + if (__builtin_expect (new_language == NULL, 0)) + { + gl_rwlock_unlock (_nl_state_lock); + return NULL; + } + + if (_nl_current_i18n_language != _nl_default_i18n_language) + free (_nl_current_i18n_language); + _nl_current_i18n_language = new_language; + } + + gl_rwlock_unlock (_nl_state_lock); + + return _nl_current_i18n_language; +} + + /* Look up the translation of msgid within DOMAIN_FILE and DOMAINBINDING. Return it if found. Return NULL if not found or in case of a conversion failure (problem in the particular message catalog). Return (char *) -1 @@ -1582,9 +1626,13 @@ guess_category_value (int category, const char *categoryname) if (strcmp (locale, "C") == 0) return locale; - /* The highest priority value is the value of the 'LANGUAGE' environment - variable. */ - language = getenv ("LANGUAGE"); + /* The highest priority value is the language precedence list + supplied either by calling set_i18n_language() or the value of + the 'LANGUAGE' environment variable. If the former is used, the + value is stored in _nl_current_i18n_language. */ + language = _nl_current_i18n_language; + if (language == _nl_default_i18n_language) + language = getenv ("LANGUAGE"); if (language != NULL && language[0] != '\0') return language; #if !defined IN_LIBGLOCALE && !defined _LIBC diff --git a/gettext-runtime/intl/libgnuintl.in.h b/gettext-runtime/intl/libgnuintl.in.h index 4fb1514..3526555 100644 --- a/gettext-runtime/intl/libgnuintl.in.h +++ b/gettext-runtime/intl/libgnuintl.in.h @@ -302,6 +302,40 @@ extern char *bind_textdomain_codeset (const char *__domainname, #endif /* IN_LIBGLOCALE */ +#ifndef IN_LIBGLOCALE + +/* Returns the language precedence list for the program. */ +#ifdef _INTL_REDIRECT_INLINE +extern const char *libintl_get_i18n_language (void); +static inline const char *get_i18n_language (void) +{ + return libintl_get_i18n_language (); +} +#else +#ifdef _INTL_REDIRECT_MACROS +# define get_i18n_language libintl_get_i18n_language +#endif +extern const char *get_i18n_language (void) + _INTL_ASM (libintl_get_i18n_language); +#endif + +/* Sets the language precedence list for the program. + NULL means to use the one inferred from the environment variable. */ +#ifdef _INTL_REDIRECT_INLINE +extern const char *libintl_set_i18n_language (const char *__language); +static inline const char *set_i18n_language (const char *__language) +{ + return libintl_set_i18n_language (__language); +} +#else +#ifdef _INTL_REDIRECT_MACROS +# define set_i18n_language libintl_set_i18n_language +#endif +extern const char *set_i18n_language (const char *__language) + _INTL_ASM (libintl_set_i18n_language); +#endif + +#endif /* IN_LIBGLOCALE */ /* Support for format strings with positions in *printf(), following the POSIX/XSI specification. diff --git a/gettext-tools/tests/.gitignore b/gettext-tools/tests/.gitignore index 9e9b3b6..8513d2e 100644 --- a/gettext-tools/tests/.gitignore +++ b/gettext-tools/tests/.gitignore @@ -16,6 +16,7 @@ /gettext-6-prg /gettext-7-prg /gettext-8-prg +/gettext-9-prg /gettextpo-1-prg /sentence /testlocale diff --git a/gettext-tools/tests/Makefile.am b/gettext-tools/tests/Makefile.am index 20ad11c..260483e 100644 --- a/gettext-tools/tests/Makefile.am +++ b/gettext-tools/tests/Makefile.am @@ -21,7 +21,7 @@ EXTRA_DIST = MOSTLYCLEANFILES = core *.stackdump TESTS = gettext-1 gettext-2 gettext-3 gettext-4 gettext-5 gettext-6 gettext-7 \ - gettext-8 \ + gettext-8 gettext-9 \ msgattrib-1 msgattrib-2 msgattrib-3 msgattrib-4 msgattrib-5 \ msgattrib-6 msgattrib-7 msgattrib-8 msgattrib-9 msgattrib-10 \ msgattrib-11 msgattrib-12 msgattrib-13 msgattrib-14 msgattrib-15 \ @@ -216,7 +216,7 @@ DEFS = -DLOCALEDIR=\"$(localedir)\" @DEFS@ LDADD = $(LDADD_@USE_INCLUDED_LIBINTL@) @INTL_MACOSX_LIBS@ LDADD_yes = ../intl/libintl.la @LTLIBTHREAD@ LDADD_no = ../intl/libgnuintl.la @LTLIBTHREAD@ @LTLIBINTL@ -check_PROGRAMS = tstgettext tstngettext testlocale gettext-3-prg gettext-4-prg gettext-5-prg gettext-6-prg gettext-7-prg gettext-8-prg cake fc3 fc4 fc5 gettextpo-1-prg sentence +check_PROGRAMS = tstgettext tstngettext testlocale gettext-3-prg gettext-4-prg gettext-5-prg gettext-6-prg gettext-7-prg gettext-8-prg gettext-9-prg cake fc3 fc4 fc5 gettextpo-1-prg sentence tstgettext_SOURCES = tstgettext.c setlocale.c tstgettext_CFLAGS = -DINSTALLDIR=\".\" tstgettext_LDADD = ../gnulib-lib/libgettextlib.la $(LDADD) diff --git a/gettext-tools/tests/gettext-9 b/gettext-tools/tests/gettext-9 new file mode 100755 index 0000000..754383a --- /dev/null +++ b/gettext-tools/tests/gettext-9 @@ -0,0 +1,56 @@ +#! /bin/sh +. "${srcdir=.}/init.sh"; path_prepend_ . ../src + +# Test that on glibc systems, gettext() works right even with intermediate +# setlocale() calls. + +# This test works only on glibc systems. +: ${GLIBC2=no} +test "$GLIBC2" = yes || { + echo "Skipping test: not a glibc system" + exit 77 +} + +# This test works only on systems that have a de_DE and fr_FR locale installed. +LC_ALL=de_DE ../testlocale || { + if test -f /usr/bin/localedef; then + echo "Skipping test: locale de_DE not installed" + else + echo "Skipping test: locale de_DE not supported" + fi + exit 77 +} +LC_ALL=fr_FR ../testlocale || { + if test -f /usr/bin/localedef; then + echo "Skipping test: locale fr_FR not installed" + else + echo "Skipping test: locale fr_FR not supported" + fi + exit 77 +} + +test -d gt-9 || mkdir gt-9 +test -d gt-9/de_DE || mkdir gt-9/de_DE +test -d gt-9/de_DE/LC_MESSAGES || mkdir gt-9/de_DE/LC_MESSAGES +test -d gt-9/fr_FR || mkdir gt-9/fr_FR +test -d gt-9/fr_FR/LC_MESSAGES || mkdir gt-9/fr_FR/LC_MESSAGES + +: ${MSGFMT=msgfmt} +${MSGFMT} -o gt-9/de_DE/LC_MESSAGES/tstlang.mo "$abs_srcdir"/gettext-3-1.po +${MSGFMT} -o gt-9/fr_FR/LC_MESSAGES/tstlang.mo "$abs_srcdir"/gettext-3-2.po + +cat <<EOF > gt-9.ok +String1 - Lang2: 1st string +String2 - Lang2: 2nd string +String1 - Lang2: 1st string +String2 - Lang2: 2nd string +String1 - First string for testing. +String2 - Another string for testing. +EOF + +../gettext-9-prg > gt-9.out || exit 1 + +: ${DIFF=diff} +${DIFF} gt-9.ok gt-9.out || exit 1 + +exit 0 diff --git a/gettext-tools/tests/gettext-9-prg.c b/gettext-tools/tests/gettext-9-prg.c new file mode 100644 index 0000000..31ee078 --- /dev/null +++ b/gettext-tools/tests/gettext-9-prg.c @@ -0,0 +1,82 @@ +/* Test that gettext() honors language precedence list. + Copyright (C) 2007, 2015-2016 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 <http://www.gnu.org/licenses/>. */ + +/* Written by Bruno Haible <hai...@clisp.cons.org>, 2007. */ + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <assert.h> + +/* Make sure we use the included libintl, not the system's one. */ +#undef _LIBINTL_H +#include "libgnuintl.h" + +#define N_(string) string + +struct data_t +{ + const char *selection; + const char *description; +}; + +struct data_t strings[] = +{ + { "String1", N_("First string for testing.") }, + { "String2", N_("Another string for testing.") } +}; +const int data_cnt = sizeof (strings) / sizeof (strings[0]); + +const char *lang[] = { "de_DE", "fr_FR", "ll_CC" }; +const int lang_cnt = sizeof (lang) / sizeof (lang[0]); + +int +main () +{ + int i; + + /* Clean up environment. */ + unsetenv ("LANGUAGE"); + unsetenv ("LC_ALL"); + unsetenv ("LC_MESSAGES"); + unsetenv ("LC_CTYPE"); + unsetenv ("LANG"); + unsetenv ("OUTPUT_CHARSET"); + + textdomain ("tstlang"); + + set_i18n_language ("fr:fr_FR:de:de_DE"); + + for (i = 0; i < lang_cnt; ++i) + { + int j; + + if (setlocale (LC_ALL, lang[i]) == NULL) + setlocale (LC_ALL, "C"); + + bindtextdomain ("tstlang", "gt-9"); + + for (j = 0; j < data_cnt; ++j) + printf ("%s - %s\n", strings[j].selection, + gettext (strings[j].description)); + } + + return 0; +} -- 2.5.5