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:
parent
f3c15cbe50
commit
591e088dd5
@ -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
|
static int
|
||||||
ParseFractionalSecond(char *cp, fsec_t *fsec)
|
ParseFractionalSecond(char *cp, fsec_t *fsec)
|
||||||
{
|
{
|
||||||
double frac;
|
double frac;
|
||||||
|
int dterr;
|
||||||
|
|
||||||
/* Caller should always pass the start of the fraction part */
|
dterr = ParseFraction(cp, &frac);
|
||||||
Assert(*cp == '.');
|
if (dterr)
|
||||||
errno = 0;
|
return dterr;
|
||||||
frac = strtod(cp, &cp);
|
|
||||||
/* check for parse failure */
|
|
||||||
if (*cp != '\0' || errno != 0)
|
|
||||||
return DTERR_BAD_FORMAT;
|
|
||||||
*fsec = rint(frac * 1000000);
|
*fsec = rint(frac * 1000000);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -1248,10 +1279,9 @@ DecodeDateTime(char **field, int *ftype, int nf,
|
|||||||
{
|
{
|
||||||
double time;
|
double time;
|
||||||
|
|
||||||
errno = 0;
|
dterr = ParseFraction(cp, &time);
|
||||||
time = strtod(cp, &cp);
|
if (dterr)
|
||||||
if (*cp != '\0' || errno != 0)
|
return dterr;
|
||||||
return DTERR_BAD_FORMAT;
|
|
||||||
time *= USECS_PER_DAY;
|
time *= USECS_PER_DAY;
|
||||||
dt2time(time,
|
dt2time(time,
|
||||||
&tm->tm_hour, &tm->tm_min,
|
&tm->tm_hour, &tm->tm_min,
|
||||||
@ -2146,10 +2176,9 @@ DecodeTimeOnly(char **field, int *ftype, int nf,
|
|||||||
{
|
{
|
||||||
double time;
|
double time;
|
||||||
|
|
||||||
errno = 0;
|
dterr = ParseFraction(cp, &time);
|
||||||
time = strtod(cp, &cp);
|
if (dterr)
|
||||||
if (*cp != '\0' || errno != 0)
|
return dterr;
|
||||||
return DTERR_BAD_FORMAT;
|
|
||||||
time *= USECS_PER_DAY;
|
time *= USECS_PER_DAY;
|
||||||
dt2time(time,
|
dt2time(time,
|
||||||
&tm->tm_hour, &tm->tm_min,
|
&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
|
* Can we use ParseFractionalSecond here? Not clear whether trailing
|
||||||
* junk should be rejected ...
|
* junk should be rejected ...
|
||||||
*/
|
*/
|
||||||
double frac;
|
if (cp[1] == '\0')
|
||||||
|
{
|
||||||
|
/* avoid assuming that strtod will accept "." */
|
||||||
|
*fsec = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
double frac;
|
||||||
|
|
||||||
errno = 0;
|
errno = 0;
|
||||||
frac = strtod(cp, NULL);
|
frac = strtod(cp, NULL);
|
||||||
if (errno != 0)
|
if (errno != 0)
|
||||||
return DTERR_BAD_FORMAT;
|
return DTERR_BAD_FORMAT;
|
||||||
*fsec = rint(frac * 1000000);
|
*fsec = rint(frac * 1000000);
|
||||||
|
}
|
||||||
/* Now truncate off the fraction for further processing */
|
/* Now truncate off the fraction for further processing */
|
||||||
*cp = '\0';
|
*cp = '\0';
|
||||||
len = strlen(str);
|
len = strlen(str);
|
||||||
@ -3467,11 +3504,9 @@ DecodeInterval(char **field, int *ftype, int nf, int range,
|
|||||||
}
|
}
|
||||||
else if (*cp == '.')
|
else if (*cp == '.')
|
||||||
{
|
{
|
||||||
errno = 0;
|
dterr = ParseFraction(cp, &fval);
|
||||||
fval = strtod(cp, &cp);
|
if (dterr)
|
||||||
if (*cp != '\0' || errno != 0)
|
return dterr;
|
||||||
return DTERR_BAD_FORMAT;
|
|
||||||
|
|
||||||
if (*field[i] == '-')
|
if (*field[i] == '-')
|
||||||
fval = -fval;
|
fval = -fval;
|
||||||
}
|
}
|
||||||
@ -3650,6 +3685,7 @@ DecodeInterval(char **field, int *ftype, int nf, int range,
|
|||||||
* Helper functions to avoid duplicated code in DecodeISO8601Interval.
|
* Helper functions to avoid duplicated code in DecodeISO8601Interval.
|
||||||
*
|
*
|
||||||
* Parse a decimal value and break it into integer and fractional parts.
|
* 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.
|
* Returns 0 or DTERR code.
|
||||||
*/
|
*/
|
||||||
static int
|
static int
|
||||||
@ -3676,7 +3712,20 @@ ParseISO8601Number(char *str, char **endptr, int64 *ipart, double *fpart)
|
|||||||
|
|
||||||
/* Parse fractional part if there is any */
|
/* Parse fractional part if there is any */
|
||||||
if (**endptr == '.')
|
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? */
|
/* did we not see anything that looks like a number? */
|
||||||
if (*endptr == str || errno != 0)
|
if (*endptr == str || errno != 0)
|
||||||
|
@ -4134,11 +4134,13 @@ timestamp_to_char(PG_FUNCTION_ARGS)
|
|||||||
ereport(ERROR,
|
ereport(ERROR,
|
||||||
(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
|
(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
|
||||||
errmsg("timestamp out of range")));
|
errmsg("timestamp out of range")));
|
||||||
COPY_tm(tm, &tt);
|
|
||||||
|
|
||||||
thisdate = date2j(tm->tm_year, tm->tm_mon, tm->tm_mday);
|
/* calculate wday and yday, because timestamp2tm doesn't */
|
||||||
tm->tm_wday = (thisdate + 1) % 7;
|
thisdate = date2j(tt.tm_year, tt.tm_mon, tt.tm_mday);
|
||||||
tm->tm_yday = thisdate - date2j(tm->tm_year, 1, 1) + 1;
|
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())))
|
if (!(res = datetime_to_char_body(&tmtc, fmt, false, PG_GET_COLLATION())))
|
||||||
PG_RETURN_NULL();
|
PG_RETURN_NULL();
|
||||||
@ -4168,11 +4170,13 @@ timestamptz_to_char(PG_FUNCTION_ARGS)
|
|||||||
ereport(ERROR,
|
ereport(ERROR,
|
||||||
(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
|
(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
|
||||||
errmsg("timestamp out of range")));
|
errmsg("timestamp out of range")));
|
||||||
COPY_tm(tm, &tt);
|
|
||||||
|
|
||||||
thisdate = date2j(tm->tm_year, tm->tm_mon, tm->tm_mday);
|
/* calculate wday and yday, because timestamp2tm doesn't */
|
||||||
tm->tm_wday = (thisdate + 1) % 7;
|
thisdate = date2j(tt.tm_year, tt.tm_mon, tt.tm_mday);
|
||||||
tm->tm_yday = thisdate - date2j(tm->tm_year, 1, 1) + 1;
|
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())))
|
if (!(res = datetime_to_char_body(&tmtc, fmt, false, PG_GET_COLLATION())))
|
||||||
PG_RETURN_NULL();
|
PG_RETURN_NULL();
|
||||||
|
@ -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
|
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)
|
(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.
|
-- test a couple rounding cases that changed since 8.3 w/ HAVE_INT64_TIMESTAMP.
|
||||||
SET IntervalStyle to postgres_verbose;
|
SET IntervalStyle to postgres_verbose;
|
||||||
select interval '-10 mons -3 days +03:55:06.70';
|
select interval '-10 mons -3 days +03:55:06.70';
|
||||||
|
@ -312,6 +312,14 @@ select interval 'P0002' AS "year only",
|
|||||||
interval 'PT10' AS "hour only",
|
interval 'PT10' AS "hour only",
|
||||||
interval 'PT10:30' AS "hour minute";
|
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.
|
-- test a couple rounding cases that changed since 8.3 w/ HAVE_INT64_TIMESTAMP.
|
||||||
SET IntervalStyle to postgres_verbose;
|
SET IntervalStyle to postgres_verbose;
|
||||||
select interval '-10 mons -3 days +03:55:06.70';
|
select interval '-10 mons -3 days +03:55:06.70';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user