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 e39f99046
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 e39f99046'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
This commit is contained in:
Tom Lane 2022-04-03 17:04:21 -04:00
parent f3c15cbe50
commit 591e088dd5
4 changed files with 132 additions and 36 deletions

View File

@ -668,19 +668,50 @@ AdjustYears(int64 val, int scale,
}
/* Fetch a fractional-second value with suitable error checking */
/*
* Parse the fractional part of a number (decimal point and optional digits,
* followed by end of string). Returns the fractional value into *frac.
*
* Returns 0 if successful, DTERR code if bogus input detected.
*/
static int
ParseFraction(char *cp, double *frac)
{
/* Caller should always pass the start of the fraction part */
Assert(*cp == '.');
/*
* We want to allow just "." with no digits, but some versions of strtod
* will report EINVAL for that, so special-case it.
*/
if (cp[1] == '\0')
{
*frac = 0;
}
else
{
errno = 0;
*frac = strtod(cp, &cp);
/* check for parse failure */
if (*cp != '\0' || errno != 0)
return DTERR_BAD_FORMAT;
}
return 0;
}
/*
* Fetch a fractional-second value with suitable error checking.
* Same as ParseFraction except we convert the result to integer microseconds.
*/
static int
ParseFractionalSecond(char *cp, fsec_t *fsec)
{
double frac;
int dterr;
/* Caller should always pass the start of the fraction part */
Assert(*cp == '.');
errno = 0;
frac = strtod(cp, &cp);
/* check for parse failure */
if (*cp != '\0' || errno != 0)
return DTERR_BAD_FORMAT;
dterr = ParseFraction(cp, &frac);
if (dterr)
return dterr;
*fsec = rint(frac * 1000000);
return 0;
}
@ -1248,10 +1279,9 @@ DecodeDateTime(char **field, int *ftype, int nf,
{
double time;
errno = 0;
time = strtod(cp, &cp);
if (*cp != '\0' || errno != 0)
return DTERR_BAD_FORMAT;
dterr = ParseFraction(cp, &time);
if (dterr)
return dterr;
time *= USECS_PER_DAY;
dt2time(time,
&tm->tm_hour, &tm->tm_min,
@ -2146,10 +2176,9 @@ DecodeTimeOnly(char **field, int *ftype, int nf,
{
double time;
errno = 0;
time = strtod(cp, &cp);
if (*cp != '\0' || errno != 0)
return DTERR_BAD_FORMAT;
dterr = ParseFraction(cp, &time);
if (dterr)
return dterr;
time *= USECS_PER_DAY;
dt2time(time,
&tm->tm_hour, &tm->tm_min,
@ -3035,13 +3064,21 @@ DecodeNumberField(int len, char *str, int fmask,
* Can we use ParseFractionalSecond here? Not clear whether trailing
* junk should be rejected ...
*/
double frac;
if (cp[1] == '\0')
{
/* avoid assuming that strtod will accept "." */
*fsec = 0;
}
else
{
double frac;
errno = 0;
frac = strtod(cp, NULL);
if (errno != 0)
return DTERR_BAD_FORMAT;
*fsec = rint(frac * 1000000);
errno = 0;
frac = strtod(cp, NULL);
if (errno != 0)
return DTERR_BAD_FORMAT;
*fsec = rint(frac * 1000000);
}
/* Now truncate off the fraction for further processing */
*cp = '\0';
len = strlen(str);
@ -3467,11 +3504,9 @@ DecodeInterval(char **field, int *ftype, int nf, int range,
}
else if (*cp == '.')
{
errno = 0;
fval = strtod(cp, &cp);
if (*cp != '\0' || errno != 0)
return DTERR_BAD_FORMAT;
dterr = ParseFraction(cp, &fval);
if (dterr)
return dterr;
if (*field[i] == '-')
fval = -fval;
}
@ -3650,6 +3685,7 @@ DecodeInterval(char **field, int *ftype, int nf, int range,
* Helper functions to avoid duplicated code in DecodeISO8601Interval.
*
* Parse a decimal value and break it into integer and fractional parts.
* Set *endptr to end+1 of the parsed substring.
* Returns 0 or DTERR code.
*/
static int
@ -3676,7 +3712,20 @@ ParseISO8601Number(char *str, char **endptr, int64 *ipart, double *fpart)
/* Parse fractional part if there is any */
if (**endptr == '.')
*fpart = strtod(*endptr, endptr) * sign;
{
/*
* As in ParseFraction, some versions of strtod insist on seeing some
* digits after '.', but some don't. We want to allow zero digits
* after '.' as long as there were some before it.
*/
if (isdigit((unsigned char) *(*endptr + 1)))
*fpart = strtod(*endptr, endptr) * sign;
else
{
(*endptr)++; /* advance over '.' */
str++; /* so next test will fail if no digits */
}
}
/* did we not see anything that looks like a number? */
if (*endptr == str || errno != 0)

View File

@ -4134,11 +4134,13 @@ timestamp_to_char(PG_FUNCTION_ARGS)
ereport(ERROR,
(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
errmsg("timestamp out of range")));
COPY_tm(tm, &tt);
thisdate = date2j(tm->tm_year, tm->tm_mon, tm->tm_mday);
tm->tm_wday = (thisdate + 1) % 7;
tm->tm_yday = thisdate - date2j(tm->tm_year, 1, 1) + 1;
/* calculate wday and yday, because timestamp2tm doesn't */
thisdate = date2j(tt.tm_year, tt.tm_mon, tt.tm_mday);
tt.tm_wday = (thisdate + 1) % 7;
tt.tm_yday = thisdate - date2j(tt.tm_year, 1, 1) + 1;
COPY_tm(tm, &tt);
if (!(res = datetime_to_char_body(&tmtc, fmt, false, PG_GET_COLLATION())))
PG_RETURN_NULL();
@ -4168,11 +4170,13 @@ timestamptz_to_char(PG_FUNCTION_ARGS)
ereport(ERROR,
(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
errmsg("timestamp out of range")));
COPY_tm(tm, &tt);
thisdate = date2j(tm->tm_year, tm->tm_mon, tm->tm_mday);
tm->tm_wday = (thisdate + 1) % 7;
tm->tm_yday = thisdate - date2j(tm->tm_year, 1, 1) + 1;
/* calculate wday and yday, because timestamp2tm doesn't */
thisdate = date2j(tt.tm_year, tt.tm_mon, tt.tm_mday);
tt.tm_wday = (thisdate + 1) % 7;
tt.tm_yday = thisdate - date2j(tt.tm_year, 1, 1) + 1;
COPY_tm(tm, &tt);
if (!(res = datetime_to_char_body(&tmtc, fmt, false, PG_GET_COLLATION())))
PG_RETURN_NULL();

View File

@ -908,6 +908,41 @@ select interval 'P0002' AS "year only",
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
(1 row)
-- Check handling of fractional fields in ISO8601 format.
select interval 'P1Y0M3DT4H5M6S';
interval
------------------------
1 year 3 days 04:05:06
(1 row)
select interval 'P1.0Y0M3DT4H5M6S';
interval
------------------------
1 year 3 days 04:05:06
(1 row)
select interval 'P1.1Y0M3DT4H5M6S';
interval
------------------------------
1 year 1 mon 3 days 04:05:06
(1 row)
select interval 'P1.Y0M3DT4H5M6S';
interval
------------------------
1 year 3 days 04:05:06
(1 row)
select interval 'P.1Y0M3DT4H5M6S';
interval
-----------------------
1 mon 3 days 04:05:06
(1 row)
select interval 'P.Y0M3DT4H5M6S'; -- error
ERROR: invalid input syntax for type interval: "P.Y0M3DT4H5M6S"
LINE 1: select interval 'P.Y0M3DT4H5M6S';
^
-- test a couple rounding cases that changed since 8.3 w/ HAVE_INT64_TIMESTAMP.
SET IntervalStyle to postgres_verbose;
select interval '-10 mons -3 days +03:55:06.70';

View File

@ -312,6 +312,14 @@ select interval 'P0002' AS "year only",
interval 'PT10' AS "hour only",
interval 'PT10:30' AS "hour minute";
-- Check handling of fractional fields in ISO8601 format.
select interval 'P1Y0M3DT4H5M6S';
select interval 'P1.0Y0M3DT4H5M6S';
select interval 'P1.1Y0M3DT4H5M6S';
select interval 'P1.Y0M3DT4H5M6S';
select interval 'P.1Y0M3DT4H5M6S';
select interval 'P.Y0M3DT4H5M6S'; -- error
-- test a couple rounding cases that changed since 8.3 w/ HAVE_INT64_TIMESTAMP.
SET IntervalStyle to postgres_verbose;
select interval '-10 mons -3 days +03:55:06.70';