summaryrefslogtreecommitdiff
path: root/sql-common
diff options
context:
space:
mode:
authorAlexander Barkov <bar@mariadb.com>2018-10-20 19:51:14 +0400
committerAlexander Barkov <bar@mariadb.com>2018-10-20 19:51:14 +0400
commitf6a20205148853f4cd352a21de3b77f2372ad50d (patch)
treedb68549ebba514202189fcd970ebf0c4333feb73 /sql-common
parent0e5a4ac2532c64a545796c787354dc41d61d0e62 (diff)
downloadmariadb-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.c303
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: