diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c index 0c55b68fbf..eaaffa7137 100644 --- a/src/backend/utils/adt/date.c +++ b/src/backend/utils/adt/date.c @@ -18,6 +18,7 @@ #include #include #include +#include #include #include "access/xact.h" @@ -1270,6 +1271,65 @@ tm2time(struct pg_tm *tm, fsec_t fsec, TimeADT *result) return 0; } +/* time_overflows() + * Check to see if a broken-down time-of-day is out of range. + */ +bool +time_overflows(int hour, int min, int sec, fsec_t fsec) +{ + /* Range-check the fields individually. */ + if (hour < 0 || hour > HOURS_PER_DAY || + min < 0 || min >= MINS_PER_HOUR || + sec < 0 || sec > SECS_PER_MINUTE || + fsec < 0 || fsec > USECS_PER_SEC) + return true; + + /* + * Because we allow, eg, hour = 24 or sec = 60, we must check separately + * that the total time value doesn't exceed 24:00:00. + */ + if ((((((hour * MINS_PER_HOUR + min) * SECS_PER_MINUTE) + + sec) * USECS_PER_SEC) + fsec) > USECS_PER_DAY) + return true; + + return false; +} + +/* float_time_overflows() + * Same, when we have seconds + fractional seconds as one "double" value. + */ +bool +float_time_overflows(int hour, int min, double sec) +{ + /* Range-check the fields individually. */ + if (hour < 0 || hour > HOURS_PER_DAY || + min < 0 || min >= MINS_PER_HOUR) + return true; + + /* + * "sec", being double, requires extra care. Cope with NaN, and round off + * before applying the range check to avoid unexpected errors due to + * imprecise input. (We assume rint() behaves sanely with infinities.) + */ + if (isnan(sec)) + return true; + sec = rint(sec * USECS_PER_SEC); + if (sec < 0 || sec > SECS_PER_MINUTE * USECS_PER_SEC) + return true; + + /* + * Because we allow, eg, hour = 24 or sec = 60, we must check separately + * that the total time value doesn't exceed 24:00:00. This must match the + * way that callers will convert the fields to a time. + */ + if (((((hour * MINS_PER_HOUR + min) * SECS_PER_MINUTE) + * USECS_PER_SEC) + (int64) sec) > USECS_PER_DAY) + return true; + + return false; +} + + /* time2tm() * Convert time data type to POSIX time structure. * @@ -1374,12 +1434,8 @@ make_time(PG_FUNCTION_ARGS) double sec = PG_GETARG_FLOAT8(2); TimeADT time; - /* This should match the checks in DecodeTimeOnly */ - if (tm_hour < 0 || tm_min < 0 || tm_min > MINS_PER_HOUR - 1 || - sec < 0 || sec > SECS_PER_MINUTE || - tm_hour > HOURS_PER_DAY || - /* test for > 24:00:00 */ - (tm_hour == HOURS_PER_DAY && (tm_min > 0 || sec > 0))) + /* Check for time overflow */ + if (float_time_overflows(tm_hour, tm_min, sec)) ereport(ERROR, (errcode(ERRCODE_DATETIME_FIELD_OVERFLOW), errmsg("time field value out of range: %d:%02d:%02g", @@ -1387,7 +1443,7 @@ make_time(PG_FUNCTION_ARGS) /* This should match tm2time */ time = (((tm_hour * MINS_PER_HOUR + tm_min) * SECS_PER_MINUTE) - * USECS_PER_SEC) + rint(sec * USECS_PER_SEC); + * USECS_PER_SEC) + (int64) rint(sec * USECS_PER_SEC); PG_RETURN_TIMEADT(time); } diff --git a/src/backend/utils/adt/datetime.c b/src/backend/utils/adt/datetime.c index 1914138a24..0b6dfb248c 100644 --- a/src/backend/utils/adt/datetime.c +++ b/src/backend/utils/adt/datetime.c @@ -936,14 +936,9 @@ DecodeDateTime(char **field, int *ftype, int nf, if (dterr) return dterr; - /* - * Check upper limit on hours; other limits checked in - * DecodeTime() - */ - /* test for > 24:00:00 */ - if (tm->tm_hour > HOURS_PER_DAY || - (tm->tm_hour == HOURS_PER_DAY && - (tm->tm_min > 0 || tm->tm_sec > 0 || *fsec > 0))) + /* check for time overflow */ + if (time_overflows(tm->tm_hour, tm->tm_min, tm->tm_sec, + *fsec)) return DTERR_FIELD_OVERFLOW; break; @@ -2218,16 +2213,8 @@ DecodeTimeOnly(char **field, int *ftype, int nf, else if (mer == PM && tm->tm_hour != HOURS_PER_DAY / 2) tm->tm_hour += HOURS_PER_DAY / 2; - /* - * This should match the checks in make_timestamp_internal - */ - if (tm->tm_hour < 0 || tm->tm_min < 0 || tm->tm_min > MINS_PER_HOUR - 1 || - tm->tm_sec < 0 || tm->tm_sec > SECS_PER_MINUTE || - tm->tm_hour > HOURS_PER_DAY || - /* test for > 24:00:00 */ - (tm->tm_hour == HOURS_PER_DAY && - (tm->tm_min > 0 || tm->tm_sec > 0 || *fsec > 0)) || - *fsec < INT64CONST(0) || *fsec > USECS_PER_SEC) + /* check for time overflow */ + if (time_overflows(tm->tm_hour, tm->tm_min, tm->tm_sec, *fsec)) return DTERR_FIELD_OVERFLOW; if ((fmask & DTK_TIME_M) != DTK_TIME_M) diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c index 7ea97d0c8e..5fe304cea7 100644 --- a/src/backend/utils/adt/timestamp.c +++ b/src/backend/utils/adt/timestamp.c @@ -32,6 +32,7 @@ #include "parser/scansup.h" #include "utils/array.h" #include "utils/builtins.h" +#include "utils/date.h" #include "utils/datetime.h" #include "utils/float.h" @@ -581,18 +582,8 @@ make_timestamp_internal(int year, int month, int day, date = date2j(tm.tm_year, tm.tm_mon, tm.tm_mday) - POSTGRES_EPOCH_JDATE; - /* - * This should match the checks in DecodeTimeOnly, except that since we're - * dealing with a float "sec" value, we also explicitly reject NaN. (An - * infinity input should get rejected by the range comparisons, but we - * can't be sure how those will treat a NaN.) - */ - if (hour < 0 || min < 0 || min > MINS_PER_HOUR - 1 || - isnan(sec) || - sec < 0 || sec > SECS_PER_MINUTE || - hour > HOURS_PER_DAY || - /* test for > 24:00:00 */ - (hour == HOURS_PER_DAY && (min > 0 || sec > 0))) + /* Check for time overflow */ + if (float_time_overflows(hour, min, sec)) ereport(ERROR, (errcode(ERRCODE_DATETIME_FIELD_OVERFLOW), errmsg("time field value out of range: %d:%02d:%02g", @@ -600,7 +591,7 @@ make_timestamp_internal(int year, int month, int day, /* This should match tm2time */ time = (((hour * MINS_PER_HOUR + min) * SECS_PER_MINUTE) - * USECS_PER_SEC) + rint(sec * USECS_PER_SEC); + * USECS_PER_SEC) + (int64) rint(sec * USECS_PER_SEC); result = date * USECS_PER_DAY + time; /* check for major overflow */ diff --git a/src/include/utils/date.h b/src/include/utils/date.h index 91c8b36e66..4cdb1f97cc 100644 --- a/src/include/utils/date.h +++ b/src/include/utils/date.h @@ -80,6 +80,8 @@ extern int time2tm(TimeADT time, struct pg_tm *tm, fsec_t *fsec); extern int timetz2tm(TimeTzADT *time, struct pg_tm *tm, fsec_t *fsec, int *tzp); extern int tm2time(struct pg_tm *tm, fsec_t fsec, TimeADT *result); extern int tm2timetz(struct pg_tm *tm, fsec_t fsec, int tz, TimeTzADT *result); +extern bool time_overflows(int hour, int min, int sec, fsec_t fsec); +extern bool float_time_overflows(int hour, int min, double sec); extern void AdjustTimeForTypmod(TimeADT *time, int32 typmod); #endif /* DATE_H */ diff --git a/src/test/regress/expected/time.out b/src/test/regress/expected/time.out index 8e0afe69e0..780d7f5455 100644 --- a/src/test/regress/expected/time.out +++ b/src/test/regress/expected/time.out @@ -73,6 +73,47 @@ SELECT f1 AS "Eight" FROM TIME_TBL WHERE f1 >= '00:00'; 15:36:39 (10 rows) +-- Check edge cases +SELECT '23:59:59.999999'::time; + time +----------------- + 23:59:59.999999 +(1 row) + +SELECT '23:59:59.9999999'::time; -- rounds up + time +---------- + 24:00:00 +(1 row) + +SELECT '23:59:60'::time; -- rounds up + time +---------- + 24:00:00 +(1 row) + +SELECT '24:00:00'::time; -- allowed + time +---------- + 24:00:00 +(1 row) + +SELECT '24:00:00.01'::time; -- not allowed +ERROR: date/time field value out of range: "24:00:00.01" +LINE 1: SELECT '24:00:00.01'::time; + ^ +SELECT '23:59:60.01'::time; -- not allowed +ERROR: date/time field value out of range: "23:59:60.01" +LINE 1: SELECT '23:59:60.01'::time; + ^ +SELECT '24:01:00'::time; -- not allowed +ERROR: date/time field value out of range: "24:01:00" +LINE 1: SELECT '24:01:00'::time; + ^ +SELECT '25:00:00'::time; -- not allowed +ERROR: date/time field value out of range: "25:00:00" +LINE 1: SELECT '25:00:00'::time; + ^ -- -- TIME simple math -- diff --git a/src/test/regress/expected/timetz.out b/src/test/regress/expected/timetz.out index 482a3463b3..6be408f528 100644 --- a/src/test/regress/expected/timetz.out +++ b/src/test/regress/expected/timetz.out @@ -90,6 +90,47 @@ SELECT f1 AS "Ten" FROM TIMETZ_TBL WHERE f1 >= '00:00-07'; 15:36:39-04 (12 rows) +-- Check edge cases +SELECT '23:59:59.999999'::timetz; + timetz +-------------------- + 23:59:59.999999-07 +(1 row) + +SELECT '23:59:59.9999999'::timetz; -- rounds up + timetz +------------- + 24:00:00-07 +(1 row) + +SELECT '23:59:60'::timetz; -- rounds up + timetz +------------- + 24:00:00-07 +(1 row) + +SELECT '24:00:00'::timetz; -- allowed + timetz +------------- + 24:00:00-07 +(1 row) + +SELECT '24:00:00.01'::timetz; -- not allowed +ERROR: date/time field value out of range: "24:00:00.01" +LINE 1: SELECT '24:00:00.01'::timetz; + ^ +SELECT '23:59:60.01'::timetz; -- not allowed +ERROR: date/time field value out of range: "23:59:60.01" +LINE 1: SELECT '23:59:60.01'::timetz; + ^ +SELECT '24:01:00'::timetz; -- not allowed +ERROR: date/time field value out of range: "24:01:00" +LINE 1: SELECT '24:01:00'::timetz; + ^ +SELECT '25:00:00'::timetz; -- not allowed +ERROR: date/time field value out of range: "25:00:00" +LINE 1: SELECT '25:00:00'::timetz; + ^ -- -- TIME simple math -- diff --git a/src/test/regress/sql/time.sql b/src/test/regress/sql/time.sql index 99a1562ed2..ea5f8b639f 100644 --- a/src/test/regress/sql/time.sql +++ b/src/test/regress/sql/time.sql @@ -30,6 +30,16 @@ SELECT f1 AS "None" FROM TIME_TBL WHERE f1 < '00:00'; SELECT f1 AS "Eight" FROM TIME_TBL WHERE f1 >= '00:00'; +-- Check edge cases +SELECT '23:59:59.999999'::time; +SELECT '23:59:59.9999999'::time; -- rounds up +SELECT '23:59:60'::time; -- rounds up +SELECT '24:00:00'::time; -- allowed +SELECT '24:00:00.01'::time; -- not allowed +SELECT '23:59:60.01'::time; -- not allowed +SELECT '24:01:00'::time; -- not allowed +SELECT '25:00:00'::time; -- not allowed + -- -- TIME simple math -- diff --git a/src/test/regress/sql/timetz.sql b/src/test/regress/sql/timetz.sql index 2ad4948e85..a1fa4ef3b7 100644 --- a/src/test/regress/sql/timetz.sql +++ b/src/test/regress/sql/timetz.sql @@ -35,6 +35,16 @@ SELECT f1 AS "None" FROM TIMETZ_TBL WHERE f1 < '00:00-07'; SELECT f1 AS "Ten" FROM TIMETZ_TBL WHERE f1 >= '00:00-07'; +-- Check edge cases +SELECT '23:59:59.999999'::timetz; +SELECT '23:59:59.9999999'::timetz; -- rounds up +SELECT '23:59:60'::timetz; -- rounds up +SELECT '24:00:00'::timetz; -- allowed +SELECT '24:00:00.01'::timetz; -- not allowed +SELECT '23:59:60.01'::timetz; -- not allowed +SELECT '24:01:00'::timetz; -- not allowed +SELECT '25:00:00'::timetz; -- not allowed + -- -- TIME simple math --