Skip to content

Commit 591e088

Browse files
committed
Fix portability issues in datetime parsing.
datetime.c's parsing logic has assumed that strtod() will accept a string that looks like ".", which it does in glibc, but not on some less-common platforms such as AIX. The result of this was that datetime fields like "123." would be accepted on some platforms but not others; which is a sufficiently odd case that it's not that surprising we've heard no field complaints. But commit e39f990 extended that assumption to new places, and happened to add a test case that exposed the platform dependency. Remove this dependency by special-casing situations without any digits after the decimal point. (Again, this is in part a pre-existing bug but I don't feel a compulsion to back-patch.) Also, rearrange e39f990's changes in formatting.c to avoid a Coverity complaint that we were copying an uninitialized field. Discussion: https://postgr.es/m/1592893.1648969747@sss.pgh.pa.us
1 parent f3c15cb commit 591e088

File tree

4 files changed

+132
-36
lines changed

4 files changed

+132
-36
lines changed

src/backend/utils/adt/datetime.c

+77-28
Original file line numberDiff line numberDiff line change
@@ -668,19 +668,50 @@ AdjustYears(int64 val, int scale,
668668
}
669669

670670

671-
/* Fetch a fractional-second value with suitable error checking */
671+
/*
672+
* Parse the fractional part of a number (decimal point and optional digits,
673+
* followed by end of string). Returns the fractional value into *frac.
674+
*
675+
* Returns 0 if successful, DTERR code if bogus input detected.
676+
*/
677+
static int
678+
ParseFraction(char *cp, double *frac)
679+
{
680+
/* Caller should always pass the start of the fraction part */
681+
Assert(*cp == '.');
682+
683+
/*
684+
* We want to allow just "." with no digits, but some versions of strtod
685+
* will report EINVAL for that, so special-case it.
686+
*/
687+
if (cp[1] == '\0')
688+
{
689+
*frac = 0;
690+
}
691+
else
692+
{
693+
errno = 0;
694+
*frac = strtod(cp, &cp);
695+
/* check for parse failure */
696+
if (*cp != '\0' || errno != 0)
697+
return DTERR_BAD_FORMAT;
698+
}
699+
return 0;
700+
}
701+
702+
/*
703+
* Fetch a fractional-second value with suitable error checking.
704+
* Same as ParseFraction except we convert the result to integer microseconds.
705+
*/
672706
static int
673707
ParseFractionalSecond(char *cp, fsec_t *fsec)
674708
{
675709
double frac;
710+
int dterr;
676711

677-
/* Caller should always pass the start of the fraction part */
678-
Assert(*cp == '.');
679-
errno = 0;
680-
frac = strtod(cp, &cp);
681-
/* check for parse failure */
682-
if (*cp != '\0' || errno != 0)
683-
return DTERR_BAD_FORMAT;
712+
dterr = ParseFraction(cp, &frac);
713+
if (dterr)
714+
return dterr;
684715
*fsec = rint(frac * 1000000);
685716
return 0;
686717
}
@@ -1248,10 +1279,9 @@ DecodeDateTime(char **field, int *ftype, int nf,
12481279
{
12491280
double time;
12501281

1251-
errno = 0;
1252-
time = strtod(cp, &cp);
1253-
if (*cp != '\0' || errno != 0)
1254-
return DTERR_BAD_FORMAT;
1282+
dterr = ParseFraction(cp, &time);
1283+
if (dterr)
1284+
return dterr;
12551285
time *= USECS_PER_DAY;
12561286
dt2time(time,
12571287
&tm->tm_hour, &tm->tm_min,
@@ -2146,10 +2176,9 @@ DecodeTimeOnly(char **field, int *ftype, int nf,
21462176
{
21472177
double time;
21482178

2149-
errno = 0;
2150-
time = strtod(cp, &cp);
2151-
if (*cp != '\0' || errno != 0)
2152-
return DTERR_BAD_FORMAT;
2179+
dterr = ParseFraction(cp, &time);
2180+
if (dterr)
2181+
return dterr;
21532182
time *= USECS_PER_DAY;
21542183
dt2time(time,
21552184
&tm->tm_hour, &tm->tm_min,
@@ -3035,13 +3064,21 @@ DecodeNumberField(int len, char *str, int fmask,
30353064
* Can we use ParseFractionalSecond here? Not clear whether trailing
30363065
* junk should be rejected ...
30373066
*/
3038-
double frac;
3067+
if (cp[1] == '\0')
3068+
{
3069+
/* avoid assuming that strtod will accept "." */
3070+
*fsec = 0;
3071+
}
3072+
else
3073+
{
3074+
double frac;
30393075

3040-
errno = 0;
3041-
frac = strtod(cp, NULL);
3042-
if (errno != 0)
3043-
return DTERR_BAD_FORMAT;
3044-
*fsec = rint(frac * 1000000);
3076+
errno = 0;
3077+
frac = strtod(cp, NULL);
3078+
if (errno != 0)
3079+
return DTERR_BAD_FORMAT;
3080+
*fsec = rint(frac * 1000000);
3081+
}
30453082
/* Now truncate off the fraction for further processing */
30463083
*cp = '\0';
30473084
len = strlen(str);
@@ -3467,11 +3504,9 @@ DecodeInterval(char **field, int *ftype, int nf, int range,
34673504
}
34683505
else if (*cp == '.')
34693506
{
3470-
errno = 0;
3471-
fval = strtod(cp, &cp);
3472-
if (*cp != '\0' || errno != 0)
3473-
return DTERR_BAD_FORMAT;
3474-
3507+
dterr = ParseFraction(cp, &fval);
3508+
if (dterr)
3509+
return dterr;
34753510
if (*field[i] == '-')
34763511
fval = -fval;
34773512
}
@@ -3650,6 +3685,7 @@ DecodeInterval(char **field, int *ftype, int nf, int range,
36503685
* Helper functions to avoid duplicated code in DecodeISO8601Interval.
36513686
*
36523687
* Parse a decimal value and break it into integer and fractional parts.
3688+
* Set *endptr to end+1 of the parsed substring.
36533689
* Returns 0 or DTERR code.
36543690
*/
36553691
static int
@@ -3676,7 +3712,20 @@ ParseISO8601Number(char *str, char **endptr, int64 *ipart, double *fpart)
36763712

36773713
/* Parse fractional part if there is any */
36783714
if (**endptr == '.')
3679-
*fpart = strtod(*endptr, endptr) * sign;
3715+
{
3716+
/*
3717+
* As in ParseFraction, some versions of strtod insist on seeing some
3718+
* digits after '.', but some don't. We want to allow zero digits
3719+
* after '.' as long as there were some before it.
3720+
*/
3721+
if (isdigit((unsigned char) *(*endptr + 1)))
3722+
*fpart = strtod(*endptr, endptr) * sign;
3723+
else
3724+
{
3725+
(*endptr)++; /* advance over '.' */
3726+
str++; /* so next test will fail if no digits */
3727+
}
3728+
}
36803729

36813730
/* did we not see anything that looks like a number? */
36823731
if (*endptr == str || errno != 0)

src/backend/utils/adt/formatting.c

+12-8
Original file line numberDiff line numberDiff line change
@@ -4134,11 +4134,13 @@ timestamp_to_char(PG_FUNCTION_ARGS)
41344134
ereport(ERROR,
41354135
(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
41364136
errmsg("timestamp out of range")));
4137-
COPY_tm(tm, &tt);
41384137

4139-
thisdate = date2j(tm->tm_year, tm->tm_mon, tm->tm_mday);
4140-
tm->tm_wday = (thisdate + 1) % 7;
4141-
tm->tm_yday = thisdate - date2j(tm->tm_year, 1, 1) + 1;
4138+
/* calculate wday and yday, because timestamp2tm doesn't */
4139+
thisdate = date2j(tt.tm_year, tt.tm_mon, tt.tm_mday);
4140+
tt.tm_wday = (thisdate + 1) % 7;
4141+
tt.tm_yday = thisdate - date2j(tt.tm_year, 1, 1) + 1;
4142+
4143+
COPY_tm(tm, &tt);
41424144

41434145
if (!(res = datetime_to_char_body(&tmtc, fmt, false, PG_GET_COLLATION())))
41444146
PG_RETURN_NULL();
@@ -4168,11 +4170,13 @@ timestamptz_to_char(PG_FUNCTION_ARGS)
41684170
ereport(ERROR,
41694171
(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
41704172
errmsg("timestamp out of range")));
4171-
COPY_tm(tm, &tt);
41724173

4173-
thisdate = date2j(tm->tm_year, tm->tm_mon, tm->tm_mday);
4174-
tm->tm_wday = (thisdate + 1) % 7;
4175-
tm->tm_yday = thisdate - date2j(tm->tm_year, 1, 1) + 1;
4174+
/* calculate wday and yday, because timestamp2tm doesn't */
4175+
thisdate = date2j(tt.tm_year, tt.tm_mon, tt.tm_mday);
4176+
tt.tm_wday = (thisdate + 1) % 7;
4177+
tt.tm_yday = thisdate - date2j(tt.tm_year, 1, 1) + 1;
4178+
4179+
COPY_tm(tm, &tt);
41764180

41774181
if (!(res = datetime_to_char_body(&tmtc, fmt, false, PG_GET_COLLATION())))
41784182
PG_RETURN_NULL();

src/test/regress/expected/interval.out

+35
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,41 @@ select interval 'P0002' AS "year only",
908908
2 years | 2 years 10 mons | 2 years 10 mons 15 days | 2 years 00:00:01 | 2 years 10 mons 00:00:01 | 2 years 10 mons 15 days 00:00:01 | 10:00:00 | 10:30:00
909909
(1 row)
910910

911+
-- Check handling of fractional fields in ISO8601 format.
912+
select interval 'P1Y0M3DT4H5M6S';
913+
interval
914+
------------------------
915+
1 year 3 days 04:05:06
916+
(1 row)
917+
918+
select interval 'P1.0Y0M3DT4H5M6S';
919+
interval
920+
------------------------
921+
1 year 3 days 04:05:06
922+
(1 row)
923+
924+
select interval 'P1.1Y0M3DT4H5M6S';
925+
interval
926+
------------------------------
927+
1 year 1 mon 3 days 04:05:06
928+
(1 row)
929+
930+
select interval 'P1.Y0M3DT4H5M6S';
931+
interval
932+
------------------------
933+
1 year 3 days 04:05:06
934+
(1 row)
935+
936+
select interval 'P.1Y0M3DT4H5M6S';
937+
interval
938+
-----------------------
939+
1 mon 3 days 04:05:06
940+
(1 row)
941+
942+
select interval 'P.Y0M3DT4H5M6S'; -- error
943+
ERROR: invalid input syntax for type interval: "P.Y0M3DT4H5M6S"
944+
LINE 1: select interval 'P.Y0M3DT4H5M6S';
945+
^
911946
-- test a couple rounding cases that changed since 8.3 w/ HAVE_INT64_TIMESTAMP.
912947
SET IntervalStyle to postgres_verbose;
913948
select interval '-10 mons -3 days +03:55:06.70';

src/test/regress/sql/interval.sql

+8
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,14 @@ select interval 'P0002' AS "year only",
312312
interval 'PT10' AS "hour only",
313313
interval 'PT10:30' AS "hour minute";
314314

315+
-- Check handling of fractional fields in ISO8601 format.
316+
select interval 'P1Y0M3DT4H5M6S';
317+
select interval 'P1.0Y0M3DT4H5M6S';
318+
select interval 'P1.1Y0M3DT4H5M6S';
319+
select interval 'P1.Y0M3DT4H5M6S';
320+
select interval 'P.1Y0M3DT4H5M6S';
321+
select interval 'P.Y0M3DT4H5M6S'; -- error
322+
315323
-- test a couple rounding cases that changed since 8.3 w/ HAVE_INT64_TIMESTAMP.
316324
SET IntervalStyle to postgres_verbose;
317325
select interval '-10 mons -3 days +03:55:06.70';

0 commit comments

Comments
 (0)