postgres.git / summary / log / commit / refs

commit    49a712f32ceebfeecac9017a49f5b1fe89ae399f
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 | 137 +++++++++++++++++++---- src/test/regress/expected/collate.linux.utf8.out | 18 +++ src/test/regress/sql/collate.linux.utf8.sql | 3 + 3 files changed, 138 insertions(+), 20 deletions(-) diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c index 3412119789e..b7d93c1a9f2 100644 --- a/src/backend/utils/adt/formatting.c +++ b/src/backend/utils/adt/formatting.c @@ -156,6 +156,7 @@ do { \ #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 @@ -1058,8 +1059,8 @@ static int from_char_parse_int_len(int *dest, const char **src, const int len, FormatNode *node, bool *have_error); static int from_char_parse_int(int *dest, const char **src, FormatNode *node, bool *have_error); -static int seq_search_ascii(const char *name, const char *const *array, int *len); -static int seq_search_localized(const char *name, char **array, int *len, +static int 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); static int from_char_seq_search(int *dest, const char **src, const char *const *array, @@ -2463,7 +2464,7 @@ from_char_parse_int(int *dest, const char **src, FormatNode *node, bool *have_er * suitable for comparisons to ASCII strings. */ static int -seq_search_ascii(const char *name, const char *const *array, int *len) +seq_search_ascii(const char *name, const char *const *array, size_t *len) { unsigned char firstc; const char *const *a; @@ -2508,6 +2509,31 @@ seq_search_ascii(const char *name, const char *const *array, int *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, + Oid collid) +{ + char *upper_substr; + char *lower_substr; + bool result; + + upper_substr = str_toupper(name, name_len, collid); + lower_substr = str_tolower(upper_substr, strlen(upper_substr), collid); + result = strcmp(lower_substr, element) == 0; + + pfree(lower_substr); + pfree(upper_substr); + return result; +} + /* * Sequentially search an array of possibly non-English words for * a case-insensitive match to the initial character(s) of "name". @@ -2520,9 +2546,10 @@ seq_search_ascii(const char *name, const char *const *array, int *len) * the arrays exported by pg_locale.c aren't const. */ static int -seq_search_localized(const char *name, char **array, int *len, Oid collid) +seq_search_localized(const char *name, char **array, size_t *len, Oid collid) { - char **a; + size_t name_len = strlen(name); + const char *name_end = name + name_len; char *upper_name; char *lower_name; @@ -2536,9 +2563,9 @@ seq_search_localized(const char *name, char **array, int *len, Oid collid) * The case-folding processing done below is fairly expensive, so before * doing that, make a quick pass to see if there is an exact match. */ - for (a = array; *a != NULL; a++) + for (char **a = array; *a != NULL; a++) { - int element_len = strlen(*a); + size_t element_len = strlen(*a); if (strncmp(name, *a, element_len) == 0) { @@ -2551,30 +2578,100 @@ seq_search_localized(const char *name, char **array, int *len, Oid 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(unconstify(char *, 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 (a = array; *a != NULL; a++) + for (char **a = array; *a != NULL; a++) { char *upper_element; + size_t upper_element_len; char *lower_element; - int element_len; + 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); + upper_element_len = strlen(upper_element); + lower_element = str_tolower(upper_element, upper_element_len, collid); + lower_element_len = strlen(lower_element); pfree(upper_element); - element_len = strlen(lower_element); - /* Match? */ - if (strncmp(lower_name, lower_element, element_len) == 0) + /* 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_element); + 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, collid)) + { + *len = substr_len; + pfree(lower_element); + 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, + collid)) + { + *len = substr_len; + pfree(lower_element); + pfree(lower_name); + return a - array; + } + } } pfree(lower_element); } @@ -2608,7 +2705,7 @@ from_char_seq_search(int *dest, const char **src, const char *const *array, char **localized_array, Oid collid, FormatNode *node, bool *have_error) { - int len; + size_t len; if (localized_array == NULL) *dest = seq_search_ascii(*src, array, &len); diff --git a/src/test/regress/expected/collate.linux.utf8.out b/src/test/regress/expected/collate.linux.utf8.out index f06ae543e49..e452140bfcc 100644 --- a/src/test/regress/expected/collate.linux.utf8.out +++ b/src/test/regress/expected/collate.linux.utf8.out @@ -477,6 +477,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 cbbd2203e41..f4094bc4fa0 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: e9d53cf45941]