/*------------------------------------------------------------------------- * * pg_localeconv_r.c * Thread-safe implementations of localeconv() * * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * * IDENTIFICATION * src/port/pg_localeconv_r.c * *------------------------------------------------------------------------- */ #include "c.h" #if !defined(WIN32) #include #include #endif #include #ifdef MON_THOUSANDS_SEP /* * One of glibc's extended langinfo items detected. Assume that the full set * is present, which means we can use nl_langinfo_l() instead of localeconv(). */ #define TRANSLATE_FROM_LANGINFO #endif struct lconv_member_info { bool is_string; int category; size_t offset; #ifdef TRANSLATE_FROM_LANGINFO nl_item item; #endif }; /* Some macros to declare the lconv members compactly. */ #ifdef TRANSLATE_FROM_LANGINFO #define LCONV_M(is_string, category, name, item) \ { is_string, category, offsetof(struct lconv, name), item } #else #define LCONV_M(is_string, category, name, item) \ { is_string, category, offsetof(struct lconv, name) } #endif #define LCONV_S(c, n, i) LCONV_M(true, c, n, i) #define LCONV_C(c, n, i) LCONV_M(false, c, n, i) /* * The work of populating lconv objects is driven by this table. Since we * tolerate non-matching encodings in LC_NUMERIC and LC_MONETARY, we have to * call the underlying OS routine multiple times, with the correct locales. * The first column of this table says which locale category applies to each struct * member. The second column is the name of the struct member. The third * column is the name of the nl_item, if translating from nl_langinfo_l() (it's * always the member name, in upper case). */ static const struct lconv_member_info table[] = { /* String fields. */ LCONV_S(LC_NUMERIC, decimal_point, DECIMAL_POINT), LCONV_S(LC_NUMERIC, thousands_sep, THOUSANDS_SEP), LCONV_S(LC_NUMERIC, grouping, GROUPING), LCONV_S(LC_MONETARY, int_curr_symbol, INT_CURR_SYMBOL), LCONV_S(LC_MONETARY, currency_symbol, CURRENCY_SYMBOL), LCONV_S(LC_MONETARY, mon_decimal_point, MON_DECIMAL_POINT), LCONV_S(LC_MONETARY, mon_thousands_sep, MON_THOUSANDS_SEP), LCONV_S(LC_MONETARY, mon_grouping, MON_GROUPING), LCONV_S(LC_MONETARY, positive_sign, POSITIVE_SIGN), LCONV_S(LC_MONETARY, negative_sign, NEGATIVE_SIGN), /* Character fields. */ LCONV_C(LC_MONETARY, int_frac_digits, INT_FRAC_DIGITS), LCONV_C(LC_MONETARY, frac_digits, FRAC_DIGITS), LCONV_C(LC_MONETARY, p_cs_precedes, P_CS_PRECEDES), LCONV_C(LC_MONETARY, p_sep_by_space, P_SEP_BY_SPACE), LCONV_C(LC_MONETARY, n_cs_precedes, N_CS_PRECEDES), LCONV_C(LC_MONETARY, n_sep_by_space, N_SEP_BY_SPACE), LCONV_C(LC_MONETARY, p_sign_posn, P_SIGN_POSN), LCONV_C(LC_MONETARY, n_sign_posn, N_SIGN_POSN), }; static inline char ** lconv_string_member(struct lconv *lconv, int i) { return (char **) ((char *) lconv + table[i].offset); } static inline char * lconv_char_member(struct lconv *lconv, int i) { return (char *) lconv + table[i].offset; } /* * Free the members of a struct lconv populated by pg_localeconv_r(). The * struct itself is in storage provided by the caller of pg_localeconv_r(). */ void pg_localeconv_free(struct lconv *lconv) { for (int i = 0; i < lengthof(table); ++i) if (table[i].is_string) free(*lconv_string_member(lconv, i)); } #ifdef TRANSLATE_FROM_LANGINFO /* * Fill in struct lconv members using the equivalent nl_langinfo_l() items. */ static int pg_localeconv_from_langinfo(struct lconv *dst, locale_t monetary_locale, locale_t numeric_locale) { for (int i = 0; i < lengthof(table); ++i) { locale_t locale; locale = table[i].category == LC_NUMERIC ? numeric_locale : monetary_locale; if (table[i].is_string) { char *string; string = nl_langinfo_l(table[i].item, locale); if (!(string = strdup(string))) { pg_localeconv_free(dst); errno = ENOMEM; return -1; } *lconv_string_member(dst, i) = string; } else { *lconv_char_member(dst, i) = *nl_langinfo_l(table[i].item, locale); } } return 0; } #else /* not TRANSLATE_FROM_LANGINFO */ /* * Copy members from a given category. Note that you have to call this twice * to copy the LC_MONETARY and then LC_NUMERIC members. */ static int pg_localeconv_copy_members(struct lconv *dst, struct lconv *src, int category) { for (int i = 0; i < lengthof(table); ++i) { if (table[i].category != category) continue; if (table[i].is_string) { char *string; string = *lconv_string_member(src, i); if (!(string = strdup(string))) { pg_localeconv_free(dst); errno = ENOMEM; return -1; } *lconv_string_member(dst, i) = string; } else { *lconv_char_member(dst, i) = *lconv_char_member(src, i); } } return 0; } #endif /* not TRANSLATE_FROM_LANGINFO */ /* * A thread-safe routine to get a copy of the lconv struct for a given * LC_NUMERIC and LC_MONETARY. Different approaches are used on different * OSes, because the standard interface is so multi-threading unfriendly. * * 1. On Windows, there is no uselocale(), but there is a way to put * setlocale() into a thread-local mode temporarily. Its localeconv() is * documented as returning a pointer to thread-local storage, so we don't have * to worry about concurrent callers. * * 2. On Glibc, as an extension, all the information required to populate * struct lconv is also available via nl_langpath_l(), which is thread-safe. * * 3. On macOS and *BSD, there is localeconv_l(), so we can create a temporary * locale_t to pass in, and the result is a pointer to storage associated with * the locale_t so we control its lifetime and we don't have to worry about * concurrent calls clobbering it. * * 4. Otherwise, we wrap plain old localeconv() in uselocale() to avoid * touching the global locale, but the output buffer is allowed by the standard * to be overwritten by concurrent calls to localeconv(). We protect against * _this_ function doing that with a Big Lock, but there isn't much we can do * about code outside our tree that might call localeconv(), given such a poor * interface. * * The POSIX standard explicitly says that it is undefined what happens if * LC_MONETARY or LC_NUMERIC imply an encoding (codeset) different from that * implied by LC_CTYPE. In practice, all Unix-ish platforms seem to believe * that localeconv() should return strings that are encoded in the codeset * implied by the LC_MONETARY or LC_NUMERIC locale name. On Windows, LC_CTYPE * has to match to get sane results. * * To get predictable results on all platforms, we'll call the underlying * routines with LC_ALL set to the appropriate locale for each set of members, * and merge the results. Three members of the resulting object are therefore * guaranteed to be encoded with LC_NUMERIC's codeset: "decimal_point", * "thousands_sep" and "grouping". All other members are encoded with * LC_MONETARY's codeset. * * Returns 0 on success. Returns non-zero on failure, and sets errno. On * success, the caller is responsible for calling pg_localeconv_free() on the * output struct to free the string members it contains. */ int pg_localeconv_r(const char *lc_monetary, const char *lc_numeric, struct lconv *output) { #ifdef WIN32 wchar_t *save_lc_ctype = NULL; wchar_t *save_lc_monetary = NULL; wchar_t *save_lc_numeric = NULL; int save_config_thread_locale; int result = -1; /* Put setlocale() into thread-local mode. */ save_config_thread_locale = _configthreadlocale(_ENABLE_PER_THREAD_LOCALE); /* * Capture the current values as wide strings. Otherwise, we might not be * able to restore them if their names contain non-ASCII characters and * the intermediate locale changes the expected encoding. We don't want * to leave the caller in an unexpected state by failing to restore, or * crash the runtime library. */ save_lc_ctype = _wsetlocale(LC_CTYPE, NULL); if (!save_lc_ctype || !(save_lc_ctype = wcsdup(save_lc_ctype))) goto exit; save_lc_monetary = _wsetlocale(LC_MONETARY, NULL); if (!save_lc_monetary || !(save_lc_monetary = wcsdup(save_lc_monetary))) goto exit; save_lc_numeric = _wsetlocale(LC_NUMERIC, NULL); if (!save_lc_numeric || !(save_lc_numeric = wcsdup(save_lc_numeric))) goto exit; memset(output, 0, sizeof(*output)); /* Copy the LC_MONETARY members. */ if (!setlocale(LC_ALL, lc_monetary)) goto exit; result = pg_localeconv_copy_members(output, localeconv(), LC_MONETARY); if (result != 0) goto exit; /* Copy the LC_NUMERIC members. */ if (!setlocale(LC_ALL, lc_numeric)) goto exit; result = pg_localeconv_copy_members(output, localeconv(), LC_NUMERIC); exit: /* Restore everything we changed. */ if (save_lc_ctype) { _wsetlocale(LC_CTYPE, save_lc_ctype); free(save_lc_ctype); } if (save_lc_monetary) { _wsetlocale(LC_MONETARY, save_lc_monetary); free(save_lc_monetary); } if (save_lc_numeric) { _wsetlocale(LC_NUMERIC, save_lc_numeric); free(save_lc_numeric); } _configthreadlocale(save_config_thread_locale); return result; #else /* !WIN32 */ locale_t monetary_locale; locale_t numeric_locale; int result; /* * All variations on Unix require locale_t objects for LC_MONETARY and * LC_NUMERIC. We'll set all locale categories, so that we can don't have * to worry about POSIX's undefined behavior if LC_CTYPE's encoding * doesn't match. */ errno = ENOENT; monetary_locale = newlocale(LC_ALL_MASK, lc_monetary, 0); if (monetary_locale == 0) return -1; numeric_locale = newlocale(LC_ALL_MASK, lc_numeric, 0); if (numeric_locale == 0) { freelocale(monetary_locale); return -1; } memset(output, 0, sizeof(*output)); #if defined(TRANSLATE_FROM_LANGINFO) /* Copy from non-standard nl_langinfo_l() extended items. */ result = pg_localeconv_from_langinfo(output, monetary_locale, numeric_locale); #elif defined(HAVE_LOCALECONV_L) /* Copy the LC_MONETARY members from a thread-safe lconv object. */ result = pg_localeconv_copy_members(output, localeconv_l(monetary_locale), LC_MONETARY); if (result == 0) { /* Copy the LC_NUMERIC members from a thread-safe lconv object. */ result = pg_localeconv_copy_members(output, localeconv_l(numeric_locale), LC_NUMERIC); } #else /* We have nothing better than standard POSIX facilities. */ { static pthread_mutex_t big_lock = PTHREAD_MUTEX_INITIALIZER; locale_t save_locale; pthread_mutex_lock(&big_lock); /* Copy the LC_MONETARY members. */ save_locale = uselocale(monetary_locale); result = pg_localeconv_copy_members(output, localeconv(), LC_MONETARY); if (result == 0) { /* Copy the LC_NUMERIC members. */ uselocale(numeric_locale); result = pg_localeconv_copy_members(output, localeconv(), LC_NUMERIC); } pthread_mutex_unlock(&big_lock); uselocale(save_locale); } #endif freelocale(monetary_locale); freelocale(numeric_locale); return result; #endif /* !WIN32 */ }