summaryrefslogtreecommitdiff
path: root/sql-common/my_time.c
diff options
context:
space:
mode:
Diffstat (limited to 'sql-common/my_time.c')
-rw-r--r--sql-common/my_time.c231
1 files changed, 192 insertions, 39 deletions
diff --git a/sql-common/my_time.c b/sql-common/my_time.c
index eea36adcaf3..40bc9c79b63 100644
--- a/sql-common/my_time.c
+++ b/sql-common/my_time.c
@@ -429,7 +429,7 @@ str_to_datetime(const char *str, uint length, MYSQL_TIME *l_time,
goto err;
}
- if (check_date(l_time, not_zero_date, flags, was_cut))
+ if (check_date(l_time, not_zero_date != 0, flags, was_cut))
goto err;
l_time->time_type= (number_of_fields <= 3 ?
@@ -465,8 +465,10 @@ err:
There may be an optional [.second_part] after seconds
length Length of str
l_time Store result here
- was_cut Set to 1 if value was cut during conversion or to 0
- otherwise.
+ warning Set MYSQL_TIME_WARN_TRUNCATED flag if the input string
+ was cut during conversion, and/or
+ MYSQL_TIME_WARN_OUT_OF_RANGE flag, if the value is
+ out of range.
NOTES
Because of the extra days argument, this function can only
@@ -478,15 +480,16 @@ err:
*/
my_bool str_to_time(const char *str, uint length, MYSQL_TIME *l_time,
- int *was_cut)
+ int *warning)
{
- long date[5],value;
+ ulong date[5];
+ ulonglong value;
const char *end=str+length, *end_of_days;
my_bool found_days,found_hours;
uint state;
l_time->neg=0;
- *was_cut= 0;
+ *warning= 0;
for (; str != end && my_isspace(&my_charset_latin1,*str) ; str++)
length--;
if (str != end && *str == '-')
@@ -501,13 +504,16 @@ my_bool str_to_time(const char *str, uint length, MYSQL_TIME *l_time,
/* Check first if this is a full TIMESTAMP */
if (length >= 12)
{ /* Probably full timestamp */
+ int was_cut;
enum enum_mysql_timestamp_type
res= str_to_datetime(str, length, l_time,
- (TIME_FUZZY_DATE | TIME_DATETIME_ONLY), was_cut);
+ (TIME_FUZZY_DATE | TIME_DATETIME_ONLY), &was_cut);
if ((int) res >= (int) MYSQL_TIMESTAMP_ERROR)
+ {
+ if (was_cut)
+ *warning|= MYSQL_TIME_WARN_TRUNCATED;
return res == MYSQL_TIMESTAMP_ERROR;
- /* We need to restore was_cut flag since str_to_datetime can modify it */
- *was_cut= 0;
+ }
}
/* Not a timestamp. Try to get this as a DAYS_TO_SECOND string */
@@ -524,15 +530,15 @@ my_bool str_to_time(const char *str, uint length, MYSQL_TIME *l_time,
if ((uint) (end-str) > 1 && str != end_of_days &&
my_isdigit(&my_charset_latin1, *str))
{ /* Found days part */
- date[0]= value;
+ date[0]= (ulong) value;
state= 1; /* Assume next is hours */
found_days= 1;
}
else if ((end-str) > 1 && *str == time_separator &&
my_isdigit(&my_charset_latin1, str[1]))
{
- date[0]=0; /* Assume we found hours */
- date[1]=value;
+ date[0]= 0; /* Assume we found hours */
+ date[1]= (ulong) value;
state=2;
found_hours=1;
str++; /* skip ':' */
@@ -541,9 +547,9 @@ my_bool str_to_time(const char *str, uint length, MYSQL_TIME *l_time,
{
/* String given as one number; assume HHMMSS format */
date[0]= 0;
- date[1]= value/10000;
- date[2]= value/100 % 100;
- date[3]= value % 100;
+ date[1]= (ulong) (value/10000);
+ date[2]= (ulong) (value/100 % 100);
+ date[3]= (ulong) (value % 100);
state=4;
goto fractional;
}
@@ -553,7 +559,7 @@ my_bool str_to_time(const char *str, uint length, MYSQL_TIME *l_time,
{
for (value=0; str != end && my_isdigit(&my_charset_latin1,*str) ; str++)
value=value*10L + (long) (*str - '0');
- date[state++]=value;
+ date[state++]= (ulong) value;
if (state == 4 || (end-str) < 2 || *str != time_separator ||
!my_isdigit(&my_charset_latin1,str[1]))
break;
@@ -587,8 +593,8 @@ fractional:
if (field_length > 0)
value*= (long) log_10_int[field_length];
else if (field_length < 0)
- *was_cut= 1;
- date[4]=value;
+ *warning|= MYSQL_TIME_WARN_TRUNCATED;
+ date[4]= (ulong) value;
}
else
date[4]=0;
@@ -601,10 +607,7 @@ fractional:
((str[1] == '-' || str[1] == '+') &&
(end - str) > 2 &&
my_isdigit(&my_charset_latin1, str[2]))))
- {
- *was_cut= 1;
return 1;
- }
if (internal_format_positions[7] != 255)
{
@@ -623,12 +626,12 @@ fractional:
}
}
- /* Some simple checks */
- if (date[2] >= 60 || date[3] >= 60)
- {
- *was_cut= 1;
+ /* Integer overflow checks */
+ if (date[0] > UINT_MAX || date[1] > UINT_MAX ||
+ date[2] > UINT_MAX || date[3] > UINT_MAX ||
+ date[4] > UINT_MAX)
return 1;
- }
+
l_time->year= 0; /* For protocol::store_time */
l_time->month= 0;
l_time->day= date[0];
@@ -638,6 +641,10 @@ fractional:
l_time->second_part= date[4];
l_time->time_type= MYSQL_TIMESTAMP_TIME;
+ /* Check if the value is valid and fits into TIME range */
+ if (check_time_range(l_time, warning))
+ return 1;
+
/* Check if there is garbage at end of the TIME specification */
if (str != end)
{
@@ -645,7 +652,7 @@ fractional:
{
if (!my_isspace(&my_charset_latin1,*str))
{
- *was_cut= 1;
+ *warning|= MYSQL_TIME_WARN_TRUNCATED;
break;
}
} while (++str != end);
@@ -655,6 +662,47 @@ fractional:
/*
+ Check 'time' value to lie in the TIME range
+
+ SYNOPSIS:
+ check_time_range()
+ time pointer to TIME value
+ warning set MYSQL_TIME_WARN_OUT_OF_RANGE flag if the value is out of range
+
+ DESCRIPTION
+ If the time value lies outside of the range [-838:59:59, 838:59:59],
+ set it to the closest endpoint of the range and set
+ MYSQL_TIME_WARN_OUT_OF_RANGE flag in the 'warning' variable.
+
+ RETURN
+ 0 time value is valid, but was possibly truncated
+ 1 time value is invalid
+*/
+
+int check_time_range(struct st_mysql_time *time, int *warning)
+{
+ longlong hour;
+
+ if (time->minute >= 60 || time->second >= 60)
+ return 1;
+
+ hour= time->hour + (24*time->day);
+ if (hour <= TIME_MAX_HOUR &&
+ (hour != TIME_MAX_HOUR || time->minute != TIME_MAX_MINUTE ||
+ time->second != TIME_MAX_SECOND || !time->second_part))
+ return 0;
+
+ time->day= 0;
+ time->hour= TIME_MAX_HOUR;
+ time->minute= TIME_MAX_MINUTE;
+ time->second= TIME_MAX_SECOND;
+ time->second_part= 0;
+ *warning|= MYSQL_TIME_WARN_OUT_OF_RANGE;
+ return 0;
+}
+
+
+/*
Prepare offset of system time zone from UTC for my_system_gmt_sec() func.
SYNOPSIS
@@ -730,16 +778,28 @@ long calc_daynr(uint year,uint month,uint day)
RETURN VALUE
Time in UTC seconds since Unix Epoch representation.
*/
-my_time_t
-my_system_gmt_sec(const MYSQL_TIME *t, long *my_timezone,
+my_time_t
+my_system_gmt_sec(const MYSQL_TIME *t_src, long *my_timezone,
my_bool *in_dst_time_gap)
{
uint loop;
- time_t tmp;
+ time_t tmp= 0;
+ int shift= 0;
+ MYSQL_TIME tmp_time;
+ MYSQL_TIME *t= &tmp_time;
struct tm *l_time,tm_tmp;
long diff, current_timezone;
/*
+ Use temp variable to avoid trashing input data, which could happen in
+ case of shift required for boundary dates processing.
+ */
+ memcpy(&tmp_time, t_src, sizeof(MYSQL_TIME));
+
+ if (!validate_timestamp_range(t))
+ return 0;
+
+ /*
Calculate the gmt time based on current time and timezone
The -1 on the end is to ensure that if have a date that exists twice
(like 2002-10-27 02:00:0 MET), we will find the initial date.
@@ -752,13 +812,89 @@ my_system_gmt_sec(const MYSQL_TIME *t, long *my_timezone,
Note: this code assumes that our time_t estimation is not too far away
from real value (we assume that localtime_r(tmp) will return something
within 24 hrs from t) which is probably true for all current time zones.
+
+ Note2: For the dates, which have time_t representation close to
+ MAX_INT32 (efficient time_t limit for supported platforms), we should
+ do a small trick to avoid overflow. That is, convert the date, which is
+ two days earlier, and then add these days to the final value.
+
+ The same trick is done for the values close to 0 in time_t
+ representation for platfroms with unsigned time_t (QNX).
+
+ To be more verbose, here is a sample (extracted from the code below):
+ (calc_daynr(2038, 1, 19) - (long) days_at_timestart)*86400L + 4*3600L
+ would return -2147480896 because of the long type overflow. In result
+ we would get 1901 year in localtime_r(), which is an obvious error.
+
+ Alike problem raises with the dates close to Epoch. E.g.
+ (calc_daynr(1969, 12, 31) - (long) days_at_timestart)*86400L + 23*3600L
+ will give -3600.
+
+ On some platforms, (E.g. on QNX) time_t is unsigned and localtime(-3600)
+ wil give us a date around 2106 year. Which is no good.
+
+ Theoreticaly, there could be problems with the latter conversion:
+ there are at least two timezones, which had time switches near 1 Jan
+ of 1970 (because of political reasons). These are America/Hermosillo and
+ America/Mazatlan time zones. They changed their offset on
+ 1970-01-01 08:00:00 UTC from UTC-8 to UTC-7. For these zones
+ the code below will give incorrect results for dates close to
+ 1970-01-01, in the case OS takes into account these historical switches.
+ Luckily, it seems that we support only one platform with unsigned
+ time_t. It's QNX. And QNX does not support historical timezone data at all.
+ E.g. there are no /usr/share/zoneinfo/ files or any other mean to supply
+ historical information for localtime_r() etc. That is, the problem is not
+ relevant to QNX.
+
+ We are safe with shifts close to MAX_INT32, as there are no known
+ time switches on Jan 2038 yet :)
*/
- tmp=(time_t) (((calc_daynr((uint) t->year,(uint) t->month,(uint) t->day) -
- (long) days_at_timestart)*86400L + (long) t->hour*3600L +
- (long) (t->minute*60 + t->second)) + (time_t) my_time_zone -
- 3600);
- current_timezone= my_time_zone;
+ if ((t->year == TIMESTAMP_MAX_YEAR) && (t->month == 1) && (t->day > 4))
+ {
+ /*
+ Below we will pass (uint) (t->day - shift) to calc_daynr.
+ As we don't want to get an overflow here, we will shift
+ only safe dates. That's why we have (t->day > 4) above.
+ */
+ t->day-= 2;
+ shift= 2;
+ }
+#ifdef TIME_T_UNSIGNED
+ else
+ {
+ /*
+ We can get 0 in time_t representaion only on 1969, 31 of Dec or on
+ 1970, 1 of Jan. For both dates we use shift, which is added
+ to t->day in order to step out a bit from the border.
+ This is required for platforms, where time_t is unsigned.
+ As far as I know, among the platforms we support it's only QNX.
+ Note: the order of below if-statements is significant.
+ */
+ if ((t->year == TIMESTAMP_MIN_YEAR + 1) && (t->month == 1)
+ && (t->day <= 10))
+ {
+ t->day+= 2;
+ shift= -2;
+ }
+
+ if ((t->year == TIMESTAMP_MIN_YEAR) && (t->month == 12)
+ && (t->day == 31))
+ {
+ t->year++;
+ t->month= 1;
+ t->day= 2;
+ shift= -2;
+ }
+ }
+#endif
+
+ tmp= (time_t) (((calc_daynr((uint) t->year, (uint) t->month, (uint) t->day) -
+ (long) days_at_timestart)*86400L + (long) t->hour*3600L +
+ (long) (t->minute*60 + t->second)) + (time_t) my_time_zone -
+ 3600);
+
+ current_timezone= my_time_zone;
localtime_r(&tmp,&tm_tmp);
l_time=&tm_tmp;
for (loop=0;
@@ -810,7 +946,24 @@ my_system_gmt_sec(const MYSQL_TIME *t, long *my_timezone,
*in_dst_time_gap= 1;
}
*my_timezone= current_timezone;
-
+
+
+ /* shift back, if we were dealing with boundary dates */
+ tmp+= shift*86400L;
+
+ /*
+ This is possible for dates, which slightly exceed boundaries.
+ Conversion will pass ok for them, but we don't allow them.
+ First check will pass for platforms with signed time_t.
+ instruction above (tmp+= shift*86400L) could exceed
+ MAX_INT32 (== TIMESTAMP_MAX_VALUE) and overflow will happen.
+ So, tmp < TIMESTAMP_MIN_VALUE will be triggered. On platfroms
+ with unsigned time_t tmp+= shift*86400L might result in a number,
+ larger then TIMESTAMP_MAX_VALUE, so another check will work.
+ */
+ if ((tmp < TIMESTAMP_MIN_VALUE) || (tmp > TIMESTAMP_MAX_VALUE))
+ tmp= 0;
+
return (my_time_t) tmp;
} /* my_system_gmt_sec */
@@ -840,7 +993,7 @@ void set_zero_time(MYSQL_TIME *tm, enum enum_mysql_timestamp_type time_type)
int my_time_to_str(const MYSQL_TIME *l_time, char *to)
{
uint extra_hours= 0;
- return my_sprintf(to, (to, "%s%02d:%02d:%02d",
+ return my_sprintf(to, (to, "%s%02u:%02u:%02u",
(l_time->neg ? "-" : ""),
extra_hours+ l_time->hour,
l_time->minute,
@@ -849,7 +1002,7 @@ int my_time_to_str(const MYSQL_TIME *l_time, char *to)
int my_date_to_str(const MYSQL_TIME *l_time, char *to)
{
- return my_sprintf(to, (to, "%04d-%02d-%02d",
+ return my_sprintf(to, (to, "%04u-%02u-%02u",
l_time->year,
l_time->month,
l_time->day));
@@ -857,7 +1010,7 @@ int my_date_to_str(const MYSQL_TIME *l_time, char *to)
int my_datetime_to_str(const MYSQL_TIME *l_time, char *to)
{
- return my_sprintf(to, (to, "%04d-%02d-%02d %02d:%02d:%02d",
+ return my_sprintf(to, (to, "%04u-%02u-%02u %02u:%02u:%02u",
l_time->year,
l_time->month,
l_time->day,