mirror of
https://github.com/postgres/postgres.git
synced 2025-04-24 10:47:04 +03:00
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
|
||||
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)
|
||||
|
@ -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();
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
Loading…
x
Reference in New Issue
Block a user