Fix portability issues in datetime parsing.
authorTom Lane <tgl@sss.pgh.pa.us>
Sun, 3 Apr 2022 21:04:21 +0000 (17:04 -0400)
committerTom Lane <tgl@sss.pgh.pa.us>
Sun, 3 Apr 2022 21:04:33 +0000 (17:04 -0400)
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

src/backend/utils/adt/datetime.c
src/backend/utils/adt/formatting.c
src/test/regress/expected/interval.out
src/test/regress/sql/interval.sql

index 462f2ed7a8ac043d7c34e9b1620c4dbaed00ee22..4c12c4d66302d21a1a143ef28822402792f47612 100644 (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)
index 843b07d7d24c918301f9cfd867269d4c5b5a499f..97a4544ffc630e1234dd387c9ebb0bcd58ec586b 100644 (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();
index 86c8d4bc990573d31b66e69071cd77916e1cd58a..03f77c01dcf7f6d6d0a19bef9267d88065ae521a 100644 (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';
index f05055e03a98b1ac8af64ace85080562cf96b6f7..97d33a13236a662a42139cd2955fdcf35e9847ed 100644 (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';