postgres.git / summary / log / commit / refs
commit 55ea764269b6fe8b3fcc221a970c09e12ef7a96b
Author: Álvaro Herrera <alvherre@kurilemu.de>
Date: Wed Aug 05 09:40:39 2026 +0000
Fix calculating length of match to localized month/weekday names
seq_search_localized() returns the length of the matching prefix in
*len, but because it internally case-folds the inputs, it gets
confused on the length. The caller expects to get the length of the
prefix in the original string, but what it actually returns is the
length of the prefix after case-folding, which can be different if the
case-folded characters have different byte-length than the original,
or with ICU, if the case-folding changes the number of characters
(e.g. "ß", the German double s).
To fix, once we have determined that we have a match, work harder to
find the match's length in the original string. This adds some
overhead, but the strings are expected to be short.
The function does "case-folding" by converting a string to upper-case,
then to lower-case, which is a little ugly given that we have
dedicated functions for case-folding nowadays. But switching to that
doesn't seem appropriate to backpatch in a security fix, and that's
not available in older stable versions, anyway.
Author: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Reported-by: Xint Code
Reviewed-by: Jeff Davis <pgsql@j-davis.com>
src/backend/utils/adt/formatting.c | 146 ++++++++++++++++++++---
src/test/regress/expected/collate.linux.utf8.out | 18 +++
src/test/regress/sql/collate.linux.utf8.sql | 3 +
3 files changed, 150 insertions(+), 17 deletions(-)
diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c
index d52d71b0a8c..641b7aa679e 100644
--- a/src/backend/utils/adt/formatting.c
+++ b/src/backend/utils/adt/formatting.c
@@ -103,6 +103,7 @@
#define DCH_MAX_ITEM_SIZ 12 /* max localized day name */
#define NUM_MAX_ITEM_SIZ 8 /* roman number (RN has 15 chars) */
+#define MAX_L10N_DATA 80 /* max localized day or month name */
/*
* Format parser structs
@@ -2328,6 +2329,41 @@ seq_search_ascii(const char *name, const char *const *array, size_t *len)
return -1;
}
+/*
+ * Compare 'name' with 'element' in a case-insensitive way, by first
+ * converting 'name' to upper case, then lower case. ('element' is already
+ * case-folded that way.)
+ *
+ * A helper function for seq_search_localized().
+ */
+static bool
+casefold_str_cmp(const char *name, size_t name_len,
+ const char *element, size_t element_len,
+ pg_locale_t mylocale)
+{
+ /*
+ * 'name' is expected to fit in MAX_L10N_DATA, even with the case
+ * conversions.
+ */
+ char upper_substr[MAX_L10N_DATA];
+ size_t upper_substr_len;
+ char lower_substr[MAX_L10N_DATA];
+ size_t lower_substr_len;
+
+ upper_substr_len = pg_strupper(upper_substr, sizeof(upper_substr),
+ name, name_len,
+ mylocale);
+ if (upper_substr_len > sizeof(upper_substr) - 1)
+ return false; /* shouldn't happen */
+ lower_substr_len = pg_strlower(lower_substr, sizeof(lower_substr),
+ upper_substr, upper_substr_len,
+ mylocale);
+ if (lower_substr_len > sizeof(lower_substr) - 1)
+ return false; /* shouldn't happen */
+
+ return strcmp(lower_substr, element) == 0;
+}
+
/*
* Sequentially search an array of possibly non-English words for
* a case-insensitive match to the initial character(s) of "name".
@@ -2342,8 +2378,11 @@ seq_search_ascii(const char *name, const char *const *array, size_t *len)
static int
seq_search_localized(const char *name, char **array, size_t *len, Oid collid)
{
+ size_t name_len = strlen(name);
+ const char *name_end = name + name_len;
char *upper_name;
char *lower_name;
+ pg_locale_t mylocale;
*len = 0;
@@ -2366,36 +2405,109 @@ seq_search_localized(const char *name, char **array, size_t *len, Oid collid)
}
}
+ mylocale = pg_newlocale_from_collation(collid);
+
/*
* Fold to upper case, then to lower case, so that we can match reliably
* even in languages in which case conversions are not injective.
*/
- upper_name = str_toupper(name, strlen(name), collid);
+ upper_name = str_toupper(name, name_len, collid);
lower_name = str_tolower(upper_name, strlen(upper_name), collid);
pfree(upper_name);
for (char **a = array; *a != NULL; a++)
{
- char *upper_element;
- char *lower_element;
- size_t element_len;
+ char upper_element[MAX_L10N_DATA];
+ size_t upper_element_len;
+ char lower_element[MAX_L10N_DATA];
+ size_t lower_element_len;
/* Likewise upper/lower-case array element */
- upper_element = str_toupper(*a, strlen(*a), collid);
- lower_element = str_tolower(upper_element, strlen(upper_element),
- collid);
- pfree(upper_element);
- element_len = strlen(lower_element);
-
- /* Match? */
- if (strncmp(lower_name, lower_element, element_len) == 0)
+ upper_element_len = pg_strupper(upper_element, sizeof(upper_element),
+ *a, strlen(*a),
+ mylocale);
+ if (upper_element_len > sizeof(upper_element) - 1)
+ continue; /* shouldn't happen */
+ lower_element_len = pg_strlower(lower_element, sizeof(lower_element),
+ upper_element, upper_element_len,
+ mylocale);
+ if (lower_element_len > sizeof(lower_element) - 1)
+ continue; /* shouldn't happen */
+
+ /* Is 'lower_element' a prefix of 'lower_name' ? */
+ if (strncmp(lower_name, lower_element, lower_element_len) == 0)
{
- *len = element_len;
- pfree(lower_element);
- pfree(lower_name);
- return a - array;
+ /*
+ * We have a match, but we still need to figure out how long the
+ * match is. The case conversions could have changed the lengths
+ * of either string, or both.
+ */
+ const char *ep;
+ const char *element_end;
+ size_t element_nchars;
+ size_t substr_len;
+ size_t substr_nchars;
+
+ /*
+ * First, check the easy case that the string matches as whole.
+ */
+ if (strlen(lower_name) == lower_element_len)
+ {
+ *len = name_len;
+ pfree(lower_name);
+ return a - array;
+ }
+
+ /*
+ * Another good guess is that the case conversions did not change
+ * the number of characters.
+ */
+
+ /* count characters in the element */
+ ep = lower_element;
+ element_end = lower_element + lower_element_len;
+ for (element_nchars = 0; ep < element_end; element_nchars++)
+ ep += pg_mblen_range(ep, element_end);
+
+ /*
+ * count the byte length of a substring of 'name' having the same
+ * character count as the element
+ */
+ substr_len = 0;
+ for (substr_nchars = 0;
+ substr_nchars < element_nchars && substr_len < name_len;
+ substr_nchars++)
+ {
+ substr_len += pg_mblen_range(name + substr_len, name_end);
+ }
+
+ if (casefold_str_cmp(name, substr_len, lower_element, lower_element_len, mylocale))
+ {
+ *len = substr_len;
+ pfree(lower_name);
+ return a - array;
+ }
+
+ /*
+ * As last resort, try the case conversion and comparison for
+ * every substring from the beginning of the original string until
+ * we find a match.
+ */
+ substr_len = 0;
+ while (substr_len < name_len)
+ {
+ substr_len += pg_mblen_range(name + substr_len, name_end);
+
+ if (casefold_str_cmp(name, substr_len,
+ lower_element, lower_element_len,
+ mylocale))
+ {
+ *len = substr_len;
+ pfree(lower_name);
+ return a - array;
+ }
+ }
}
- pfree(lower_element);
}
pfree(lower_name);
diff --git a/src/test/regress/expected/collate.linux.utf8.out b/src/test/regress/expected/collate.linux.utf8.out
index c6e84c27b69..e0a39e4c300 100644
--- a/src/test/regress/expected/collate.linux.utf8.out
+++ b/src/test/regress/expected/collate.linux.utf8.out
@@ -479,6 +479,24 @@ SELECT to_date('01 Şub 2010', 'DD TMMON YYYY');
SELECT to_date('1234567890ab 2010', 'TMMONTH YYYY'); -- fail
ERROR: invalid value "1234567890ab" for "MONTH"
DETAIL: The given value did not match any of the allowed values for this field.
+SELECT to_date('01 Aralık 2010', 'DD TMMONTH YYYY');
+ to_date
+------------
+ 12-01-2010
+(1 row)
+
+SELECT to_date('01 aralık 2010', 'DD TMMONTH YYYY');
+ to_date
+------------
+ 12-01-2010
+(1 row)
+
+SELECT to_date('2010 01 araLık', 'YYYY DD TMMONTH');
+ to_date
+------------
+ 12-01-2010
+(1 row)
+
-- backwards parsing
CREATE VIEW collview1 AS SELECT * FROM collate_test1 WHERE b COLLATE "C" >= 'bbc';
CREATE VIEW collview2 AS SELECT a, b FROM collate_test1 ORDER BY b COLLATE "C";
diff --git a/src/test/regress/sql/collate.linux.utf8.sql b/src/test/regress/sql/collate.linux.utf8.sql
index 132d13af0a8..6d726ee9c99 100644
--- a/src/test/regress/sql/collate.linux.utf8.sql
+++ b/src/test/regress/sql/collate.linux.utf8.sql
@@ -188,6 +188,9 @@ SELECT to_date('01 ŞUB 2010', 'DD TMMON YYYY');
SELECT to_date('01 Şub 2010', 'DD TMMON YYYY');
SELECT to_date('1234567890ab 2010', 'TMMONTH YYYY'); -- fail
+SELECT to_date('01 Aralık 2010', 'DD TMMONTH YYYY');
+SELECT to_date('01 aralık 2010', 'DD TMMONTH YYYY');
+SELECT to_date('2010 01 araLık', 'YYYY DD TMMONTH');
-- backwards parsing
[parent: 8ce749f8f65c]