v8-0004-Validate-ICU-locales.patch
text/x-patch
Filename: v8-0004-Validate-ICU-locales.patch
Type: text/x-patch
Part: 3
Patch
Format: format-patch
Series: patch v8-0004
Subject: Validate ICU locales.
| File | + | − |
|---|---|---|
| doc/src/sgml/config.sgml | 17 | 0 |
| src/backend/commands/collationcmds.c | 5 | 9 |
| src/backend/commands/dbcommands.c | 5 | 9 |
| src/backend/utils/adt/pg_locale.c | 66 | 13 |
| src/backend/utils/misc/guc_tables.c | 9 | 0 |
| src/backend/utils/misc/postgresql.conf.sample | 2 | 0 |
| src/include/utils/pg_locale.h | 2 | 0 |
| src/test/regress/expected/collate.icu.utf8.out | 10 | 2 |
| src/test/regress/sql/collate.icu.utf8.sql | 5 | 1 |
From 1279aa6b7eb92a01cf9a7eaa3f5f15d595f674ed Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Fri, 17 Mar 2023 09:55:31 -0700
Subject: [PATCH v8 4/4] Validate ICU locales.
Ensure that it can be transformed into a language tag in "strict" mode
(which validates the attributes), that the language exists in ICU, and
that it can be opened.
Basic validation helps avoid minor mistakes and misspellings, which
often fall back to the root locale instead of the intended
locale. It's even more important to avoid such mistakes in ICU
versions 54 and earlier, where the same (misspelled) locale string
could fall back to different locales depending on the environment.
Discussion: https://postgr.es/m/11b1eeb7e7667fdd4178497aeb796c48d26e69b9.camel@j-davis.com
Discussion: https://postgr.es/m/df2efad0cae7c65180df8e5ebb709e5eb4f2a82b.camel@j-davis.com
---
doc/src/sgml/config.sgml | 17 ++++
src/backend/commands/collationcmds.c | 14 ++--
src/backend/commands/dbcommands.c | 14 ++--
src/backend/utils/adt/pg_locale.c | 79 ++++++++++++++++---
src/backend/utils/misc/guc_tables.c | 9 +++
src/backend/utils/misc/postgresql.conf.sample | 2 +
src/include/utils/pg_locale.h | 2 +
.../regress/expected/collate.icu.utf8.out | 12 ++-
src/test/regress/sql/collate.icu.utf8.sql | 6 +-
9 files changed, 121 insertions(+), 34 deletions(-)
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 481f93cea1..78eae3ca65 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9784,6 +9784,23 @@ SET XML OPTION { DOCUMENT | CONTENT };
</listitem>
</varlistentry>
+ <varlistentry id="guc-icu-locale-validation" xreflabel="icu_locale_validation">
+ <term><varname>icu_locale_validation</varname> (<type>boolean</type>)
+ <indexterm>
+ <primary><varname>icu_locale_validation</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Validation is performed on an ICU locale specified for a new collation
+ or database. If this parameter is set to <literal>true</literal>, an
+ error is raised for a validation failure; if set to
+ <literal>false</literal>, a warning is issued. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-default-text-search-config" xreflabel="default_text_search_config">
<term><varname>default_text_search_config</varname> (<type>string</type>)
<indexterm>
diff --git a/src/backend/commands/collationcmds.c b/src/backend/commands/collationcmds.c
index fe811229b3..2505ed559d 100644
--- a/src/backend/commands/collationcmds.c
+++ b/src/backend/commands/collationcmds.c
@@ -273,7 +273,9 @@ DefineCollation(ParseState *pstate, List *names, List *parameters, bool if_not_e
*/
if (!IsBinaryUpgrade)
{
- langtag = icu_language_tag(colliculocale, WARNING);
+ int elevel = icu_locale_validation ? ERROR : WARNING;
+
+ langtag = icu_language_tag(colliculocale, elevel);
if (langtag)
{
ereport(NOTICE,
@@ -282,15 +284,9 @@ DefineCollation(ParseState *pstate, List *names, List *parameters, bool if_not_e
colliculocale = langtag;
}
- else
- {
- ereport(WARNING,
- (errmsg("could not convert locale \"%s\" to language tag",
- colliculocale)));
- }
- }
- check_icu_locale(colliculocale);
+ icu_validate_locale(colliculocale);
+ }
#else
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 436f6ec60d..40276af143 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -1068,7 +1068,9 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
*/
if (!IsBinaryUpgrade && dbiculocale != src_iculocale)
{
- langtag = icu_language_tag(dbiculocale, WARNING);
+ int elevel = icu_locale_validation ? ERROR : WARNING;
+
+ langtag = icu_language_tag(dbiculocale, elevel);
if (langtag)
{
ereport(NOTICE,
@@ -1077,15 +1079,9 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
dbiculocale = langtag;
}
- else
- {
- ereport(WARNING,
- (errmsg("could not convert locale \"%s\" to language tag",
- dbiculocale)));
- }
- }
- check_icu_locale(dbiculocale);
+ icu_validate_locale(dbiculocale);
+ }
#else
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index 366ec71b9f..7b962d31bc 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -88,13 +88,14 @@
#define MAX_L10N_DATA 80
-
/* GUC settings */
char *locale_messages;
char *locale_monetary;
char *locale_numeric;
char *locale_time;
+bool icu_locale_validation = false;
+
/*
* lc_time localization cache.
*
@@ -2831,18 +2832,6 @@ icu_set_collation_attributes(UCollator *collator, const char *loc,
pfree(lower_str);
}
-/*
- * Check if the given locale ID is valid, and ereport(ERROR) if it isn't.
- */
-void
-check_icu_locale(const char *icu_locale)
-{
- UCollator *collator;
-
- collator = pg_ucol_open(icu_locale);
- ucol_close(collator);
-}
-
/*
* Return the BCP47 language tag representation of the requested locale.
*
@@ -2920,6 +2909,70 @@ icu_language_tag(const char *loc_str, int elevel)
return langtag;
}
+/*
+ * Perform best-effort check that the locale is a valid one.
+ */
+void
+icu_validate_locale(const char *loc_str)
+{
+ UCollator *collator;
+ UErrorCode status;
+ int elevel = icu_locale_validation ? ERROR : WARNING;
+ char *langtag;
+ char lang[ULOC_LANG_CAPACITY];
+ bool found = false;
+
+ /* check that it can be converted to a language tag */
+ langtag = icu_language_tag(loc_str, elevel);
+ pfree(langtag);
+
+ /* validate that we can extract the language */
+ status = U_ZERO_ERROR;
+ uloc_getLanguage(loc_str, lang, ULOC_LANG_CAPACITY, &status);
+ if (U_FAILURE(status))
+ {
+ ereport(elevel,
+ (errmsg("could not get language from locale \"%s\": %s",
+ loc_str, u_errorName(status))));
+ return;
+ }
+
+ /* check for special language name */
+ if (strcmp(lang, "") == 0 ||
+ strcmp(lang, "root") == 0 || strcmp(lang, "und") == 0 ||
+ strcmp(lang, "c") == 0 || strcmp(lang, "posix") == 0)
+ found = true;
+
+ /* search for matching language within ICU */
+ for (int32_t i = 0; !found && i < uloc_countAvailable(); i++)
+ {
+ const char *otherloc = uloc_getAvailable(i);
+ char otherlang[ULOC_LANG_CAPACITY];
+
+ status = U_ZERO_ERROR;
+ uloc_getLanguage(otherloc, otherlang, ULOC_LANG_CAPACITY, &status);
+ if (U_FAILURE(status))
+ {
+ ereport(elevel,
+ (errmsg("could not get language from locale \"%s\": %s",
+ loc_str, u_errorName(status))));
+ continue;
+ }
+
+ if (strcmp(lang, otherlang) == 0)
+ found = true;
+ }
+
+ if (!found)
+ ereport(elevel,
+ (errmsg("locale \"%s\" has unknown language \"%s\"",
+ loc_str, lang)));
+
+ /* check that it can be opened */
+ collator = pg_ucol_open(loc_str);
+ ucol_close(collator);
+}
+
#endif /* USE_ICU */
/*
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 1c0583fe26..930bbc1877 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -1586,6 +1586,15 @@ struct config_bool ConfigureNamesBool[] =
true,
NULL, NULL, NULL
},
+ {
+ {"icu_locale_validation", PGC_USERSET, CLIENT_CONN_LOCALE,
+ gettext_noop("Raise an error for invalid ICU locale strings."),
+ NULL
+ },
+ &icu_locale_validation,
+ false,
+ NULL, NULL, NULL
+ },
{
{"array_nulls", PGC_USERSET, COMPAT_OPTIONS_PREVIOUS,
gettext_noop("Enable input of NULL elements in arrays."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index d06074b86f..cff927e8be 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -730,6 +730,8 @@
#lc_numeric = 'C' # locale for number formatting
#lc_time = 'C' # locale for time formatting
+#icu_locale_validation = off # validate ICU locale strings
+
# default configuration for text search
#default_text_search_config = 'pg_catalog.simple'
diff --git a/src/include/utils/pg_locale.h b/src/include/utils/pg_locale.h
index 471b111650..a97735185a 100644
--- a/src/include/utils/pg_locale.h
+++ b/src/include/utils/pg_locale.h
@@ -40,6 +40,7 @@ extern PGDLLIMPORT char *locale_messages;
extern PGDLLIMPORT char *locale_monetary;
extern PGDLLIMPORT char *locale_numeric;
extern PGDLLIMPORT char *locale_time;
+extern PGDLLIMPORT bool icu_locale_validation;
/* lc_time localization cache */
extern PGDLLIMPORT char *localized_abbrev_days[];
@@ -122,6 +123,7 @@ extern size_t pg_strnxfrm_prefix(char *dest, size_t destsize, const char *src,
extern int32_t icu_to_uchar(UChar **buff_uchar, const char *buff, size_t nbytes);
extern int32_t icu_from_uchar(char **result, const UChar *buff_uchar, int32_t len_uchar);
extern char *icu_language_tag(const char *loc_str, int elevel);
+extern void icu_validate_locale(const char *loc_str);
#endif
extern void check_icu_locale(const char *icu_locale);
diff --git a/src/test/regress/expected/collate.icu.utf8.out b/src/test/regress/expected/collate.icu.utf8.out
index 0d00df165a..b6b806d753 100644
--- a/src/test/regress/expected/collate.icu.utf8.out
+++ b/src/test/regress/expected/collate.icu.utf8.out
@@ -1037,8 +1037,16 @@ $$;
RESET client_min_messages;
CREATE COLLATION test3 (provider = icu, lc_collate = 'en_US.utf8'); -- fail, needs "locale"
ERROR: parameter "locale" must be specified
-CREATE COLLATION testx (provider = icu, locale = 'nonsense'); /* never fails with ICU */ DROP COLLATION testx;
-NOTICE: using language tag "nonsense" for locale "nonsense"
+SET icu_locale_validation = true;
+CREATE COLLATION testx (provider = icu, locale = 'nonsense-nowhere'); -- fails
+NOTICE: using language tag "nonsense-nowhere" for locale "nonsense-nowhere"
+ERROR: locale "nonsense-nowhere" has unknown language "nonsense"
+CREATE COLLATION testx (provider = icu, locale = '@colStrength=primary;nonsense=yes'); -- fails
+ERROR: could not convert locale name "@colStrength=primary;nonsense=yes" to language tag: U_ILLEGAL_ARGUMENT_ERROR
+RESET icu_locale_validation;
+CREATE COLLATION testx (provider = icu, locale = 'nonsense-nowhere'); DROP COLLATION testx;
+NOTICE: using language tag "nonsense-nowhere" for locale "nonsense-nowhere"
+WARNING: locale "nonsense-nowhere" has unknown language "nonsense"
CREATE COLLATION test4 FROM nonsense;
ERROR: collation "nonsense" for encoding "UTF8" does not exist
CREATE COLLATION test5 FROM test0;
diff --git a/src/test/regress/sql/collate.icu.utf8.sql b/src/test/regress/sql/collate.icu.utf8.sql
index 3bd98868b4..7871bf5386 100644
--- a/src/test/regress/sql/collate.icu.utf8.sql
+++ b/src/test/regress/sql/collate.icu.utf8.sql
@@ -376,7 +376,11 @@ $$;
RESET client_min_messages;
CREATE COLLATION test3 (provider = icu, lc_collate = 'en_US.utf8'); -- fail, needs "locale"
-CREATE COLLATION testx (provider = icu, locale = 'nonsense'); /* never fails with ICU */ DROP COLLATION testx;
+SET icu_locale_validation = true;
+CREATE COLLATION testx (provider = icu, locale = 'nonsense-nowhere'); -- fails
+CREATE COLLATION testx (provider = icu, locale = '@colStrength=primary;nonsense=yes'); -- fails
+RESET icu_locale_validation;
+CREATE COLLATION testx (provider = icu, locale = 'nonsense-nowhere'); DROP COLLATION testx;
CREATE COLLATION test4 FROM nonsense;
CREATE COLLATION test5 FROM test0;
--
2.34.1