From 6d7ff848e55bb7173d2db8550fd6617bc05be255 Mon Sep 17 00:00:00 2001 From: Tom Lane <tgl@sss.pgh.pa.us> Date: Sun, 18 May 2003 01:06:26 +0000 Subject: [PATCH] Add code to test for unknown timezone names (following some ideas from 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 | 260 +++++++++++++++++++++++++++---- src/backend/utils/adt/datetime.c | 40 +++-- src/include/utils/datetime.h | 4 +- 3 files changed, 265 insertions(+), 39 deletions(-) diff --git a/src/backend/commands/variable.c b/src/backend/commands/variable.c index 25291d34cb8..aa8d9d36134 100644 --- a/src/backend/commands/variable.c +++ b/src/backend/commands/variable.c @@ -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 diff --git a/src/backend/utils/adt/datetime.c b/src/backend/utils/adt/datetime.c index abcdf1321fd..31b42d4612f 100644 --- a/src/backend/utils/adt/datetime.c +++ b/src/backend/utils/adt/datetime.c @@ -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) diff --git a/src/include/utils/datetime.h b/src/include/utils/datetime.h index 1f43ffbceaf..7623095f09c 100644 --- a/src/include/utils/datetime.h +++ b/src/include/utils/datetime.h @@ -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 */ -- GitLab