Add code to test for unknown timezone names (following some ideas from
authorTom Lane
Sun, 18 May 2003 01:06:26 +0000 (01:06 +0000)
committerTom Lane
Sun, 18 May 2003 01:06:26 +0000 (01:06 +0000)
Ross Reedstrom, a couple months back) and to detect timezones that are
using leap-second timekeeping.  The unknown-zone-name test is pretty
heuristic and ugly, but it seems better than the old behavior of just
switching to GMT given a bad name.  Also make DecodePosixTimezone() a
tad more robust.

src/backend/commands/variable.c
src/backend/utils/adt/datetime.c
src/include/utils/datetime.h

index 25291d34cb819ba28f74f0cf37cbb160afa071ec..aa8d9d361347f63bc6ce0933d63fe0d274dd514e 100644 (file)
@@ -9,7 +9,7 @@
  *
  *
  * IDENTIFICATION
- *   $Header: /cvsroot/pgsql/src/backend/commands/variable.c,v 1.75 2003/04/27 17:31:25 tgl Exp $
+ *   $Header: /cvsroot/pgsql/src/backend/commands/variable.c,v 1.76 2003/05/18 01:06:25 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -235,7 +235,147 @@ show_datestyle(void)
 /*
  * Storage for TZ env var is allocated with an arbitrary size of 64 bytes.
  */
-static char tzbuf[64];
+#define TZBUF_LEN  64
+
+static char tzbuf[TZBUF_LEN];
+
+/*
+ * First time through, we remember the original environment TZ value, if any.
+ */
+static bool have_saved_tz = false;
+static char orig_tzbuf[TZBUF_LEN];
+
+/*
+ * Convenience subroutine for assigning the value of TZ
+ */
+static void
+set_tz(const char *tz)
+{
+   strcpy(tzbuf, "TZ=");
+   strncpy(tzbuf + 3, tz, sizeof(tzbuf) - 4);
+   if (putenv(tzbuf) != 0)     /* shouldn't happen? */
+       elog(LOG, "Unable to set TZ environment variable");
+   tzset();
+}
+
+/*
+ * Remove any value of TZ we have established
+ *
+ * Note: this leaves us with *no* value of TZ in the environment, and
+ * is therefore only appropriate for reverting to that state, not for
+ * reverting to a state where TZ was set to something else.
+ */
+static void
+clear_tz(void)
+{
+   /*
+    * unsetenv() works fine, but is BSD, not POSIX, and is not
+    * available under Solaris, among others. Apparently putenv()
+    * called as below clears the process-specific environment
+    * variables.  Other reasonable arguments to putenv() (e.g.
+    * "TZ=", "TZ", "") result in a core dump (under Linux
+    * anyway). - thomas 1998-01-26
+    */
+   if (tzbuf[0] == 'T')
+   {
+       strcpy(tzbuf, "=");
+       if (putenv(tzbuf) != 0)
+           elog(LOG, "Unable to clear TZ environment variable");
+       tzset();
+   }
+}
+
+/*
+ * Check whether tzset() succeeded
+ *
+ * Unfortunately, tzset doesn't offer any well-defined way to detect that the
+ * value of TZ was bad.  Often it will just select UTC (GMT) as the effective
+ * timezone.  We use the following heuristics:
+ *
+ * If tzname[1] is a nonempty string, *or* the global timezone variable is
+ * not zero, then tzset must have recognized the TZ value as something
+ * different from UTC.  Return true.
+ *
+ * Otherwise, check to see if the TZ name is a known spelling of "UTC"
+ * (ie, appears in our internal tables as a timezone equivalent to UTC).
+ * If so, accept it.
+ *
+ * This will reject nonstandard spellings of UTC unless tzset() chose to
+ * set tzname[1] as well as tzname[0].  The glibc version of tzset() will
+ * do so, but on other systems we may be tightening the spec a little.
+ *
+ * Another problem is that on some platforms (eg HPUX), if tzset thinks the
+ * input is bogus then it will adopt the system default timezone, which we
+ * really can't tell is not the intended translation of the input.
+ *
+ * Still, it beats failing to detect bad TZ names at all, and a silent
+ * failure mode of adopting the system-wide default is much better than
+ * a silent failure mode of adopting UTC.
+ *
+ * NB: this must NOT elog(ERROR).  The caller must get control back so that
+ * it can restore the old value of TZ if we don't like the new one.
+ */
+static bool
+tzset_succeeded(const char *tz)
+{
+   char        tztmp[TZBUF_LEN];
+   char       *cp;
+   int         tzval;
+
+   /*
+    * Check first set of heuristics to say that tzset definitely worked.
+    */
+   if (tzname[1] && tzname[1][0] != '\0')
+       return true;
+   if (TIMEZONE_GLOBAL != 0)
+       return true;
+
+   /*
+    * Check for known spellings of "UTC".  Note we must downcase the input
+    * before passing it to DecodePosixTimezone().
+    */
+   StrNCpy(tztmp, tz, sizeof(tztmp));
+   for (cp = tztmp; *cp; cp++)
+       *cp = tolower((unsigned char) *cp);
+   if (DecodePosixTimezone(tztmp, &tzval) == 0)
+       if (tzval == 0)
+           return true;
+
+   return false;
+}
+
+/*
+ * Check whether timezone is acceptable.
+ *
+ * What we are doing here is checking for leap-second-aware timekeeping.
+ * We need to reject such TZ settings because they'll wreak havoc with our
+ * date/time arithmetic.
+ *
+ * NB: this must NOT elog(ERROR).  The caller must get control back so that
+ * it can restore the old value of TZ if we don't like the new one.
+ */
+static bool
+tz_acceptable(void)
+{
+   struct tm   tt;
+   time_t      time2000;
+
+   /*
+    * To detect leap-second timekeeping, compute the time_t value for
+    * local midnight, 2000-01-01.  Insist that this be a multiple of 60;
+    * any partial-minute offset has to be due to leap seconds.
+    */
+   MemSet(&tt, 0, sizeof(tt));
+   tt.tm_year = 100;
+   tt.tm_mon = 0;
+   tt.tm_mday = 1;
+   tt.tm_isdst = -1;
+   time2000 = mktime(&tt);
+   if ((time2000 % 60) != 0)
+       return false;
+
+   return true;
+}
 
 /*
  * assign_timezone: GUC assign_hook for timezone
@@ -247,6 +387,21 @@ assign_timezone(const char *value, bool doit, bool interactive)
    char       *endptr;
    double      hours;
 
+   /*
+    * On first call, see if there is a TZ in the original environment.
+    * Save that value permanently.
+    */
+   if (!have_saved_tz)
+   {
+       char   *orig_tz = getenv("TZ");
+
+       if (orig_tz)
+           StrNCpy(orig_tzbuf, orig_tz, sizeof(orig_tzbuf));
+       else
+           orig_tzbuf[0] = '\0';
+       have_saved_tz = true;
+   }
+
    /*
     * Check for INTERVAL 'foo'
     */
@@ -313,25 +468,36 @@ assign_timezone(const char *value, bool doit, bool interactive)
        else if (strcasecmp(value, "UNKNOWN") == 0)
        {
            /*
-            * Clear any TZ value we may have established.
-            *
-            * unsetenv() works fine, but is BSD, not POSIX, and is not
-            * available under Solaris, among others. Apparently putenv()
-            * called as below clears the process-specific environment
-            * variables.  Other reasonable arguments to putenv() (e.g.
-            * "TZ=", "TZ", "") result in a core dump (under Linux
-            * anyway). - thomas 1998-01-26
+            * UNKNOWN is the value shown as the "default" for TimeZone
+            * in guc.c.  We interpret it as meaning the original TZ
+            * inherited from the environment.  Note that if there is an
+            * original TZ setting, we will return that rather than UNKNOWN
+            * as the canonical spelling.
             */
            if (doit)
            {
-               if (tzbuf[0] == 'T')
+               bool    ok;
+
+               /* Revert to original setting of TZ, whatever it was */
+               if (orig_tzbuf[0])
                {
-                   strcpy(tzbuf, "=");
-                   if (putenv(tzbuf) != 0)
-                       elog(ERROR, "Unable to clear TZ environment variable");
-                   tzset();
+                   set_tz(orig_tzbuf);
+                   ok = tzset_succeeded(orig_tzbuf) && tz_acceptable();
+               }
+               else
+               {
+                   clear_tz();
+                   ok = tz_acceptable();
+               }
+
+               if (ok)
+                   HasCTZSet = false;
+               else
+               {
+                   /* Bogus, so force UTC (equivalent to INTERVAL 0) */
+                   CTimeZone = 0;
+                   HasCTZSet = true;
                }
-               HasCTZSet = false;
            }
        }
        else
@@ -339,19 +505,58 @@ assign_timezone(const char *value, bool doit, bool interactive)
            /*
             * Otherwise assume it is a timezone name.
             *
-            * XXX unfortunately we have no reasonable way to check whether a
-            * timezone name is good, so we have to just assume that it
-            * is.
+            * We have to actually apply the change before we can have any
+            * hope of checking it.  So, save the old value in case we have
+            * to back out.  Note that it's possible the old setting is in
+            * tzbuf, so we'd better copy it.
             */
-           if (doit)
+           char    save_tzbuf[TZBUF_LEN];
+           char   *save_tz;
+           bool    known,
+                   acceptable;
+
+           save_tz = getenv("TZ");
+           if (save_tz)
+               StrNCpy(save_tzbuf, save_tz, sizeof(save_tzbuf));
+
+           set_tz(value);
+
+           known = tzset_succeeded(value);
+           acceptable = tz_acceptable();
+
+           if (doit && known && acceptable)
            {
-               strcpy(tzbuf, "TZ=");
-               strncat(tzbuf, value, sizeof(tzbuf) - 4);
-               if (putenv(tzbuf) != 0) /* shouldn't happen? */
-                   elog(LOG, "assign_timezone: putenv failed");
-               tzset();
+               /* Keep the changed TZ */
                HasCTZSet = false;
            }
+           else
+           {
+               /*
+                * Revert to prior TZ setting; note we haven't changed
+                * HasCTZSet in this path, so if we were previously using
+                * a fixed offset, we still are.
+                */
+               if (save_tz)
+                   set_tz(save_tzbuf);
+               else
+                   clear_tz();
+               /* Complain if it was bad */
+               if (!known)
+               {
+                   elog(interactive ? ERROR : LOG,
+                        "unrecognized timezone name \"%s\"",
+                        value);
+                   return NULL;
+               }
+               if (!acceptable)
+               {
+                   elog(interactive ? ERROR : LOG,
+                        "timezone \"%s\" appears to use leap seconds"
+                        "\n\tPostgreSQL does not support leap seconds",
+                        value);
+                   return NULL;
+               }
+           }
        }
    }
 
@@ -369,10 +574,7 @@ assign_timezone(const char *value, bool doit, bool interactive)
        return NULL;
 
    if (HasCTZSet)
-   {
-       snprintf(result, sizeof(tzbuf), "%.5f",
-                (double) CTimeZone / 3600.0);
-   }
+       snprintf(result, sizeof(tzbuf), "%.5f", (double) CTimeZone / 3600.0);
    else if (tzbuf[0] == 'T')
        strcpy(result, tzbuf + 3);
    else
index abcdf1321fdcc91393a8add602c438261114e2ad..31b42d4612f5f6ed388bf35147c105924ed84d23 100644 (file)
@@ -8,7 +8,7 @@
  *
  *
  * IDENTIFICATION
- *   $Header: /cvsroot/pgsql/src/backend/utils/adt/datetime.c,v 1.104 2003/05/04 04:30:15 tgl Exp $
+ *   $Header: /cvsroot/pgsql/src/backend/utils/adt/datetime.c,v 1.105 2003/05/18 01:06:26 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -36,7 +36,6 @@ static int DecodeTime(char *str, int fmask, int *tmask,
 static int DecodeTimezone(char *str, int *tzp);
 static datetkn *datebsearch(char *key, datetkn *base, unsigned int nel);
 static int DecodeDate(char *str, int fmask, int *tmask, struct tm * tm);
-static int DecodePosixTimezone(char *str, int *val);
 static void TrimTrailingZeros(char *str);
 
 
@@ -942,8 +941,6 @@ DecodeDateTime(char **field, int *ftype, int nf,
                        return -1;
 
                    val = strtol(field[i], &cp, 10);
-                   if (*cp != '-')
-                       return -1;
 
                    j2date(val, &tm->tm_year, &tm->tm_mon, &tm->tm_mday);
                    /* Get the time zone from the end of the string */
@@ -2555,6 +2552,10 @@ DecodeNumberField(int len, char *str, int fmask,
 /* DecodeTimezone()
  * Interpret string as a numeric timezone.
  *
+ * Return 0 if okay (and set *tzp), nonzero if not okay.
+ *
+ * NB: this must *not* elog on failure; see commands/variable.c.
+ *
  * Note: we allow timezone offsets up to 13:59.  There are places that
  * use +1300 summer time.
  */
@@ -2567,7 +2568,10 @@ DecodeTimezone(char *str, int *tzp)
    char       *cp;
    int         len;
 
-   /* assume leading character is "+" or "-" */
+   /* leading character must be "+" or "-" */
+   if (*str != '+' && *str != '-')
+       return -1;
+
    hr = strtol((str + 1), &cp, 10);
 
    /* explicit delimiter? */
@@ -2589,6 +2593,7 @@ DecodeTimezone(char *str, int *tzp)
        min = 0;
 
    tz = (hr * 60 + min) * 60;
+
    if (*str == '-')
        tz = -tz;
 
@@ -2601,9 +2606,14 @@ DecodeTimezone(char *str, int *tzp)
  * Interpret string as a POSIX-compatible timezone:
  * PST-hh:mm
  * PST+h
+ * PST
  * - thomas 2000-03-15
+ *
+ * Return 0 if okay (and set *tzp), nonzero if not okay.
+ *
+ * NB: this must *not* elog on failure; see commands/variable.c.
  */
-static int
+int
 DecodePosixTimezone(char *str, int *tzp)
 {
    int         val,
@@ -2612,13 +2622,21 @@ DecodePosixTimezone(char *str, int *tzp)
    char       *cp;
    char        delim;
 
+   /* advance over name part */
    cp = str;
-   while ((*cp != '\0') && isalpha((unsigned char) *cp))
+   while (*cp && isalpha((unsigned char) *cp))
        cp++;
 
-   if (DecodeTimezone(cp, &tz) != 0)
-       return -1;
+   /* decode offset, if present */
+   if (*cp)
+   {
+       if (DecodeTimezone(cp, &tz) != 0)
+           return -1;
+   }
+   else
+       tz = 0;
 
+   /* decode name part.  We must temporarily scribble on the input! */
    delim = *cp;
    *cp = '\0';
    type = DecodeSpecial(MAXDATEFIELDS - 1, str, &val);
@@ -2641,8 +2659,12 @@ DecodePosixTimezone(char *str, int *tzp)
 
 /* DecodeSpecial()
  * Decode text string using lookup table.
+ *
  * Implement a cache lookup since it is likely that dates
  * will be related in format.
+ *
+ * NB: this must *not* elog on failure;
+ * see commands/variable.c.
  */
 int
 DecodeSpecial(int field, char *lowtoken, int *val)
index 1f43ffbceaf5c9c13ef846eaad9d397e7ffcafe8..7623095f09cd54b722b58357ab5acda2191e1b20 100644 (file)
@@ -9,7 +9,7 @@
  * Portions Copyright (c) 1996-2002, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
  *
- * $Id: datetime.h,v 1.38 2003/04/18 01:03:42 momjian Exp $
+ * $Id: datetime.h,v 1.39 2003/05/18 01:06:26 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -290,6 +290,8 @@ extern bool ClearDateCache(bool newval, bool doit, bool interactive);
 
 extern int j2day(int jd);
 
+extern int DecodePosixTimezone(char *str, int *tzp);
+
 extern bool CheckDateTokenTables(void);
 
 #endif   /* DATETIME_H */