Re: BUG #17795: Erroneous parsing of floating-poing components in DecodeISO8601Interval() - Mailing list pgsql-bugs
From | Tom Lane |
---|---|
Subject | Re: BUG #17795: Erroneous parsing of floating-poing components in DecodeISO8601Interval() |
Date | |
Msg-id | 3449142.1676850059@sss.pgh.pa.us Whole thread Raw |
In response to | BUG #17795: Erroneous parsing of floating-poing components in DecodeISO8601Interval() (PG Bug reporting form <noreply@postgresql.org>) |
Responses |
Re: BUG #17795: Erroneous parsing of floating-poing components in DecodeISO8601Interval()
|
List | pgsql-bugs |
PG Bug reporting form <noreply@postgresql.org> writes: > I've encountered a pair of relatively new anomalies when using ISO-8601 > intervals. Thanks for the report! All of these seem to trace to the fact that ParseISO8601Number isn't considering the possibility that the input string ends with "eNN". This allows the reported "fraction" output to have values greater than 1, which breaks some calling code as you show, and it also fails to apply that scale factor to the integer part of the output, which accounts for some of the other cases. On the whole I think the right fix is to go back to using strtod() to parse the whole input string, as we did before e39f99046. This results in some inaccuracy if there's more than 15 digits in the input, but I don't care enough about that scenario to work harder. There are a couple of other places where datetime.c is using strtod() to parse what it expects to be a fractional value. I'm inclined to just barf if the result is out of range. Maybe we shouldn't back-patch that aspect into v15, not sure. Anyway I propose the attached. regards, tom lane diff --git a/src/backend/utils/adt/datetime.c b/src/backend/utils/adt/datetime.c index b74889039d..245651a778 100644 --- a/src/backend/utils/adt/datetime.c +++ b/src/backend/utils/adt/datetime.c @@ -698,6 +698,9 @@ ParseFraction(char *cp, double *frac) /* check for parse failure */ if (*cp != '\0' || errno != 0) return DTERR_BAD_FORMAT; + /* check for bogus input such as .1e10 */ + if (*frac >= 1.0) + return DTERR_BAD_FORMAT; } return 0; } @@ -3080,6 +3083,9 @@ DecodeNumberField(int len, char *str, int fmask, frac = strtod(cp, NULL); if (errno != 0) return DTERR_BAD_FORMAT; + /* check for bogus input such as .1e10 */ + if (frac >= 1.0) + return DTERR_BAD_FORMAT; *fsec = rint(frac * 1000000); } /* Now truncate off the fraction for further processing */ @@ -3702,46 +3708,38 @@ DecodeInterval(char **field, int *ftype, int nf, int range, static int ParseISO8601Number(char *str, char **endptr, int64 *ipart, double *fpart) { - int sign = 1; - - *ipart = 0; - *fpart = 0.0; - - /* Parse sign if there is any */ - if (*str == '-') - { - sign = -1; - str++; - } + double val; - *endptr = str; + /* + * Historically this has accepted anything that strtod() would take, + * notably including "e" notation, so continue doing that. This is + * slightly annoying because the precision of double is less than that of + * int64, so we would lose accuracy for inputs larger than 2^53 or so. + * However, historically we rejected inputs outside the int32 range, + * making that concern moot. What we do now is reject abs(val) above + * 1.0e15 (a round number a bit less than 2^50), so that any accepted + * value will have an exact integer part, and thereby a fraction part with + * abs(*fpart) less than 1. In the absence of field complaints it doesn't + * seem worth working harder. + */ + if (!(isdigit((unsigned char) *str) || *str == '-' || *str == '.')) + return DTERR_BAD_FORMAT; errno = 0; - - /* Parse int64 part if there is any */ - if (isdigit((unsigned char) **endptr)) - *ipart = strtoi64(*endptr, endptr, 10) * sign; - - /* Parse fractional part if there is any */ - if (**endptr == '.') - { - /* - * 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? */ + val = strtod(str, endptr); + /* did we not see anything that looks like a double? */ if (*endptr == str || errno != 0) return DTERR_BAD_FORMAT; - + /* watch out for overflow, including infinities; reject NaN too */ + if (isnan(val) || val < -1.0e15 || val > 1.0e15) + return DTERR_FIELD_OVERFLOW; + /* be very sure we truncate towards zero (cf dtrunc()) */ + if (val >= 0) + *ipart = (int64) floor(val); + else + *ipart = (int64) -floor(-val); + *fpart = val - *ipart; + /* Callers expect this to hold */ + Assert(*fpart > -1.0 && *fpart < 1.0); return 0; } diff --git a/src/test/regress/expected/interval.out b/src/test/regress/expected/interval.out index c7ac408bec..a154840c85 100644 --- a/src/test/regress/expected/interval.out +++ b/src/test/regress/expected/interval.out @@ -975,6 +975,12 @@ select interval 'P.1Y0M3DT4H5M6S'; 1 mon 3 days 04:05:06 (1 row) +select interval 'P10.5e4Y'; -- not per spec, but we've historically taken it + interval +-------------- + 105000 years +(1 row) + select interval 'P.Y0M3DT4H5M6S'; -- error ERROR: invalid input syntax for type interval: "P.Y0M3DT4H5M6S" LINE 1: select interval 'P.Y0M3DT4H5M6S'; @@ -1081,13 +1087,13 @@ select interval 'PT2562047788:00:54.775807'; select interval 'PT2562047788.0152155019444'; interval ----------------------------------- - @ 2562047788 hours 54.775807 secs + @ 2562047788 hours 54.775429 secs (1 row) select interval 'PT-2562047788.0152155022222'; interval --------------------------------------- - @ 2562047788 hours 54.775808 secs ago + @ 2562047788 hours 54.775429 secs ago (1 row) -- overflow each date/time field diff --git a/src/test/regress/sql/interval.sql b/src/test/regress/sql/interval.sql index 54745c40d6..af8e1ca0f4 100644 --- a/src/test/regress/sql/interval.sql +++ b/src/test/regress/sql/interval.sql @@ -328,6 +328,7 @@ select interval 'P1.0Y0M3DT4H5M6S'; select interval 'P1.1Y0M3DT4H5M6S'; select interval 'P1.Y0M3DT4H5M6S'; select interval 'P.1Y0M3DT4H5M6S'; +select interval 'P10.5e4Y'; -- not per spec, but we've historically taken it select interval 'P.Y0M3DT4H5M6S'; -- error -- test a couple rounding cases that changed since 8.3 w/ HAVE_INT64_TIMESTAMP.
pgsql-bugs by date: