Thanks to visit codestin.com
Credit goes to github.com

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/[email protected]
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

Lines changed: 77 additions & 28 deletions
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

Lines changed: 12 additions & 8 deletions
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

Lines changed: 35 additions & 0 deletions
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

Lines changed: 8 additions & 0 deletions
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)