diff options
author | Alexander Barkov <bar@mariadb.com> | 2018-10-20 19:51:14 +0400 |
---|---|---|
committer | Alexander Barkov <bar@mariadb.com> | 2018-10-20 19:51:14 +0400 |
commit | f6a20205148853f4cd352a21de3b77f2372ad50d (patch) | |
tree | db68549ebba514202189fcd970ebf0c4333feb73 /sql-common | |
parent | 0e5a4ac2532c64a545796c787354dc41d61d0e62 (diff) | |
download | mariadb-git-f6a20205148853f4cd352a21de3b77f2372ad50d.tar.gz |
MDEV-17477 Wrong result for TIME('-2001-01-01 10:20:30') and numerous other str-to-time conversion problems
MDEV-17478 Wrong result for TIME('+100:20:30')
Diffstat (limited to 'sql-common')
-rw-r--r-- | sql-common/my_time.c | 303 |
1 files changed, 203 insertions, 100 deletions
diff --git a/sql-common/my_time.c b/sql-common/my_time.c index ce6e4846fad..4f03482f6e0 100644 --- a/sql-common/my_time.c +++ b/sql-common/my_time.c @@ -271,6 +271,128 @@ int check_time_range(MYSQL_TIME *ltime, uint dec, int *warning) } + /* Remove trailing spaces and garbage */ +static my_bool get_suffix(const char *str, size_t length, size_t *new_length) +{ + /* + QQ: perhaps 'T' should be considered as a date/time delimiter only + if it's followed by a digit. Learn ISO 8601 details. + */ + my_bool garbage= FALSE; + for ( ; length > 0 ; length--) + { + char ch= str[length - 1]; + if (my_isdigit(&my_charset_latin1, ch) || + my_ispunct(&my_charset_latin1, ch)) + break; + if (my_isspace(&my_charset_latin1, ch)) + continue; + if (ch == 'T') + { + /* 'T' has a meaning only after a digit. Otherwise it's a garbage */ + if (length >= 2 && my_isdigit(&my_charset_latin1, str[length - 2])) + break; + } + garbage= TRUE; + } + *new_length= length; + return garbage; +} + + +static size_t get_prefix(const char *str, size_t length, const char **endptr) +{ + const char *str0= str, *end= str + length; + for (; str < end && my_isspace(&my_charset_latin1, *str) ; str++) + { } + *endptr= str; + return str - str0; +} + + +static size_t get_sign(my_bool *neg, const char *str, size_t length, + const char **endptr) +{ + const char *str0= str; + if (length) + { + if ((*neg= (*str == '-')) || (*str == '+')) + str++; + } + else + *neg= FALSE; + *endptr= str; + return str - str0; +} + + +static my_bool find_body(my_bool *neg, const char *str, size_t length, + MYSQL_TIME *to, int *warn, + const char **new_str, size_t *new_length) +{ + size_t sign_length; + *warn= 0; + length-= get_prefix(str, length, &str); + sign_length= get_sign(neg, str, length, &str); + length-= sign_length; + /* There can be a space after a sign again: '- 10:20:30' or '- 1 10:20:30' */ + length-= get_prefix(str, length, &str); + if (get_suffix(str, length, &length)) + *warn|= MYSQL_TIME_WARN_TRUNCATED; + *new_str= str; + *new_length= length; + if (!length || !my_isdigit(&my_charset_latin1, *str)) + { + *warn|= MYSQL_TIME_WARN_TRUNCATED; + set_zero_time(to, MYSQL_TIMESTAMP_ERROR); + return TRUE; + } + return FALSE; +} + + +static my_bool +is_datetime_body_candidate(const char *str, size_t length) +{ + static uint min_date_length= 5; /* '1-1-1' -> '0001-01-01' */ + uint pos; + if (length >= 12) + return TRUE; + /* + The shortest possible DATE is '1-1-1', which is 5 characters. + To make a full datetime it should be at least followed by a space or a 'T'. + */ + if (length < min_date_length + 1/* DATE/TIME separator */) + return FALSE; + for (pos= min_date_length; pos < length; pos++) + { + if (str[pos] == 'T') /* Date/time separator */ + return TRUE; + if (str[pos] == ' ') + { + /* + We found a space. If can be a DATE/TIME separator: + TIME('1-1-1 1:1:1.0) -> '0001-01-01 01:01:01.0' + + But it can be also a DAY/TIME separator: + TIME('1 11') -> 35:00:00 = 1 day 11 hours + TIME('1 111') -> 135:00:00 = 1 day 111 hours + TIME('11 11') -> 275:00:00 = 11 days 11 hours + TIME('111 11') -> 838:59:59 = 111 days 11 hours with overflow + TIME('1111 11') -> 838:59:59 = 1111 days 11 hours with overflow + */ + for (pos= 0 ; pos < min_date_length; pos++) + { + if (my_ispunct(&my_charset_latin1, str[pos])) /* Can be a DATE */ + return TRUE; + } + return FALSE; + } + } + return FALSE; +} + + static my_bool str_to_DDhhmmssff_internal(my_bool neg, const char *str, size_t length, MYSQL_TIME *l_time, ulong max_hour, @@ -282,7 +404,7 @@ str_to_DDhhmmssff_internal(my_bool neg, const char *str, size_t length, Convert a timestamp string to a MYSQL_TIME value. SYNOPSIS - str_to_datetime() + str_to_datetime_or_date_body() str String to parse length Length of string l_time Date is stored here @@ -323,34 +445,16 @@ str_to_DDhhmmssff_internal(my_bool neg, const char *str, size_t length, #define MAX_DATE_PARTS 8 -my_bool -str_to_datetime(const char *str, size_t length, MYSQL_TIME *l_time, - ulonglong flags, MYSQL_TIME_STATUS *status) +static my_bool +str_to_datetime_or_date_body(const char *str, size_t length, MYSQL_TIME *l_time, + ulonglong flags, MYSQL_TIME_STATUS *status) { const char *end=str+length, *pos; uint number_of_fields= 0, digits, year_length, not_zero_date; - DBUG_ENTER("str_to_datetime"); + DBUG_ENTER("str_to_datetime_or_date_body"); DBUG_ASSERT(C_FLAGS_OK(flags)); bzero(l_time, sizeof(*l_time)); - if (flags & C_TIME_TIME_ONLY) - { - my_bool ret= str_to_time(str, length, l_time, flags, status); - DBUG_RETURN(ret); - } - - my_time_status_init(status); - - /* Skip space at start */ - for (; str != end && my_isspace(&my_charset_latin1, *str) ; str++) - ; - if (str == end || ! my_isdigit(&my_charset_latin1, *str)) - { - status->warnings= MYSQL_TIME_WARN_TRUNCATED; - l_time->time_type= MYSQL_TIMESTAMP_NONE; - DBUG_RETURN(1); - } - /* Calculate number of digits in first part. If length= 8 or >= 14 then year is of format YYYY. @@ -442,43 +546,22 @@ str_to_datetime(const char *str, size_t length, MYSQL_TIME *l_time, l_time->time_type= (number_of_fields <= 3 ? MYSQL_TIMESTAMP_DATE : MYSQL_TIMESTAMP_DATETIME); - for (; str != end ; str++) - { - if (!my_isspace(&my_charset_latin1,*str)) - { - status->warnings= MYSQL_TIME_WARN_TRUNCATED; - break; - } - } + if (str != end) + status->warnings= MYSQL_TIME_WARN_TRUNCATED; DBUG_RETURN(FALSE); err: - bzero((char*) l_time, sizeof(*l_time)); - l_time->time_type= MYSQL_TIMESTAMP_ERROR; + set_zero_time(l_time, MYSQL_TIMESTAMP_ERROR); DBUG_RETURN(TRUE); } -static size_t get_prefix_and_sign(my_bool *neg, const char *str, size_t length) -{ - const char *str0= str, *end= str + length; - for (; str < end && my_isspace(&my_charset_latin1, *str) ; str++) - { } - if (str < end && *str == '-') - { - *neg= TRUE; - str++; - } - return str - str0; -} - - /* Convert a time string to a MYSQL_TIME struct. SYNOPSIS - str_to_time() + str_to_datetime_or_date_or_time_body() str A string in full TIMESTAMP format or [-] DAYS [H]H:MM:SS, [H]H:MM:SS, [M]M:SS, [H]HMMSS, [M]MSS or [S]S @@ -503,44 +586,31 @@ static size_t get_prefix_and_sign(my_bool *neg, const char *str, size_t length) TRUE on error */ -my_bool str_to_time(const char *str, size_t length, MYSQL_TIME *l_time, - ulonglong fuzzydate, MYSQL_TIME_STATUS *status) +my_bool str_to_datetime_or_date_or_time_body(const char *str, size_t length, + MYSQL_TIME *l_time, + ulonglong fuzzydate, + MYSQL_TIME_STATUS *status) { - my_bool neg= 0; - size_t tmp_length; const char *endptr; DBUG_ASSERT(C_FLAGS_OK(fuzzydate)); - my_time_status_init(status); - - if ((tmp_length= get_prefix_and_sign(&neg, str, length))) - { - str+= tmp_length; - length-= tmp_length; - } - if (!length) - { - status->warnings|= MYSQL_TIME_WARN_TRUNCATED; - goto err; - } - /* Check first if this is a full TIMESTAMP */ - if (length >= 12) + if (is_datetime_body_candidate(str, length)) { /* Probably full timestamp */ - (void) str_to_datetime(str, length, l_time, - (fuzzydate & ~C_TIME_TIME_ONLY) | C_TIME_DATETIME_ONLY, - status); + (void) str_to_datetime_or_date_body(str, length, l_time, + (fuzzydate & ~C_TIME_TIME_ONLY) | + C_TIME_DATETIME_ONLY, + status); if (l_time->time_type >= MYSQL_TIMESTAMP_ERROR) return l_time->time_type == MYSQL_TIMESTAMP_ERROR; my_time_status_init(status); } - if (!str_to_DDhhmmssff_internal(neg, str, length, l_time, TIME_MAX_HOUR, + if (!str_to_DDhhmmssff_internal(FALSE, str, length, l_time, TIME_MAX_HOUR, status, &endptr)) return FALSE; -err: - bzero((char*) l_time, sizeof(*l_time)); - l_time->time_type= MYSQL_TIMESTAMP_ERROR; + + set_zero_time(l_time, MYSQL_TIMESTAMP_ERROR); return TRUE; } @@ -548,31 +618,22 @@ err: my_bool str_to_DDhhmmssff(const char *str, size_t length, MYSQL_TIME *ltime, ulong max_hour, MYSQL_TIME_STATUS *status) { - my_bool neg= 0; - size_t tmp_length; + my_bool neg; const char *endptr; - my_time_status_init(status); + int warn; - /* Remove trailing spaces */ - for ( ; length > 0 && my_isspace(&my_charset_latin1, str[length - 1]) ; ) - length--; - - if ((tmp_length= get_prefix_and_sign(&neg, str, length))) - { - str+= tmp_length; - length-= tmp_length; - } - if (!length) + my_time_status_init(status); + if (find_body(&neg, str, length, ltime, &warn, &str, &length)) { - status->warnings|= MYSQL_TIME_WARN_TRUNCATED; - set_zero_time(ltime, MYSQL_TIMESTAMP_ERROR); + status->warnings= warn; return TRUE; } /* Reject anything that might be parsed as a full TIMESTAMP */ - if (length >= 12) /* The same condition with str_to_time() */ + if (is_datetime_body_candidate(str, length)) { - (void) str_to_datetime(str, length, ltime, C_TIME_DATETIME_ONLY, status); + (void) str_to_datetime_or_date_body(str, length, ltime, + C_TIME_DATETIME_ONLY, status); if (ltime->time_type > MYSQL_TIMESTAMP_ERROR) { status->warnings|= MYSQL_TIME_WARN_TRUNCATED; @@ -591,6 +652,57 @@ my_bool str_to_DDhhmmssff(const char *str, size_t length, MYSQL_TIME *ltime, status, &endptr) || (endptr < str + length && endptr[0] == '-')) return TRUE; + status->warnings|= warn; + return FALSE; +} + + +my_bool str_to_time(const char *str, size_t length, MYSQL_TIME *l_time, + ulonglong fuzzydate, MYSQL_TIME_STATUS *status) +{ + my_bool neg; + int warn; + DBUG_ASSERT(C_FLAGS_OK(fuzzydate)); + my_time_status_init(status); + if (find_body(&neg, str, length, l_time, &warn, &str, &length)) + { + status->warnings= warn; + return TRUE; + } + /* + QQ: Perhaps we should modify xxx_body() to return endptr. + If endptr points to '-', return an error. + */ + if (str_to_datetime_or_date_or_time_body(str, length, l_time, + fuzzydate, status)) + return TRUE; + status->warnings|= warn; + l_time->neg= neg; + return FALSE; +} + + +my_bool +str_to_datetime(const char *str, size_t length, MYSQL_TIME *l_time, + ulonglong flags, MYSQL_TIME_STATUS *status) +{ + my_bool neg, rc; + int warn; + DBUG_ASSERT(C_FLAGS_OK(flags)); + my_time_status_init(status); + if (find_body(&neg, str, length, l_time, &warn, &str, &length)) + { + status->warnings= warn; + return TRUE; + } + rc= (flags & C_TIME_TIME_ONLY) ? + str_to_datetime_or_date_or_time_body(str, length, l_time, flags, status) : + str_to_datetime_or_date_body(str, length, l_time, flags, status); + status->warnings|= warn; + if (rc) + return rc; + if ((l_time->neg= neg) && l_time->time_type != MYSQL_TIMESTAMP_TIME) + return TRUE; return FALSE; } @@ -749,16 +861,7 @@ fractional: /* Check if there is garbage at end of the MYSQL_TIME specification */ if (str != end) - { - do - { - if (!my_isspace(&my_charset_latin1,*str)) - { - status->warnings|= MYSQL_TIME_WARN_TRUNCATED; - break; - } - } while (++str != end); - } + status->warnings|= MYSQL_TIME_WARN_TRUNCATED; return FALSE; err: |