Skip to content

Commit 6d7ff84

Browse files
committed
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.
1 parent 6d8c774 commit 6d7ff84

File tree

3 files changed

+265
-39
lines changed

3 files changed

+265
-39
lines changed

src/backend/commands/variable.c

Lines changed: 231 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
*
1010
*
1111
* IDENTIFICATION
12-
* $Header: /cvsroot/pgsql/src/backend/commands/variable.c,v 1.75 2003/04/27 17:31:25 tgl Exp $
12+
* $Header: /cvsroot/pgsql/src/backend/commands/variable.c,v 1.76 2003/05/18 01:06:25 tgl Exp $
1313
*
1414
*-------------------------------------------------------------------------
1515
*/
@@ -235,7 +235,147 @@ show_datestyle(void)
235235
/*
236236
* Storage for TZ env var is allocated with an arbitrary size of 64 bytes.
237237
*/
238-
static char tzbuf[64];
238+
#define TZBUF_LEN 64
239+
240+
static char tzbuf[TZBUF_LEN];
241+
242+
/*
243+
* First time through, we remember the original environment TZ value, if any.
244+
*/
245+
static bool have_saved_tz = false;
246+
static char orig_tzbuf[TZBUF_LEN];
247+
248+
/*
249+
* Convenience subroutine for assigning the value of TZ
250+
*/
251+
static void
252+
set_tz(const char *tz)
253+
{
254+
strcpy(tzbuf, "TZ=");
255+
strncpy(tzbuf + 3, tz, sizeof(tzbuf) - 4);
256+
if (putenv(tzbuf) != 0) /* shouldn't happen? */
257+
elog(LOG, "Unable to set TZ environment variable");
258+
tzset();
259+
}
260+
261+
/*
262+
* Remove any value of TZ we have established
263+
*
264+
* Note: this leaves us with *no* value of TZ in the environment, and
265+
* is therefore only appropriate for reverting to that state, not for
266+
* reverting to a state where TZ was set to something else.
267+
*/
268+
static void
269+
clear_tz(void)
270+
{
271+
/*
272+
* unsetenv() works fine, but is BSD, not POSIX, and is not
273+
* available under Solaris, among others. Apparently putenv()
274+
* called as below clears the process-specific environment
275+
* variables. Other reasonable arguments to putenv() (e.g.
276+
* "TZ=", "TZ", "") result in a core dump (under Linux
277+
* anyway). - thomas 1998-01-26
278+
*/
279+
if (tzbuf[0] == 'T')
280+
{
281+
strcpy(tzbuf, "=");
282+
if (putenv(tzbuf) != 0)
283+
elog(LOG, "Unable to clear TZ environment variable");
284+
tzset();
285+
}
286+
}
287+
288+
/*
289+
* Check whether tzset() succeeded
290+
*
291+
* Unfortunately, tzset doesn't offer any well-defined way to detect that the
292+
* value of TZ was bad. Often it will just select UTC (GMT) as the effective
293+
* timezone. We use the following heuristics:
294+
*
295+
* If tzname[1] is a nonempty string, *or* the global timezone variable is
296+
* not zero, then tzset must have recognized the TZ value as something
297+
* different from UTC. Return true.
298+
*
299+
* Otherwise, check to see if the TZ name is a known spelling of "UTC"
300+
* (ie, appears in our internal tables as a timezone equivalent to UTC).
301+
* If so, accept it.
302+
*
303+
* This will reject nonstandard spellings of UTC unless tzset() chose to
304+
* set tzname[1] as well as tzname[0]. The glibc version of tzset() will
305+
* do so, but on other systems we may be tightening the spec a little.
306+
*
307+
* Another problem is that on some platforms (eg HPUX), if tzset thinks the
308+
* input is bogus then it will adopt the system default timezone, which we
309+
* really can't tell is not the intended translation of the input.
310+
*
311+
* Still, it beats failing to detect bad TZ names at all, and a silent
312+
* failure mode of adopting the system-wide default is much better than
313+
* a silent failure mode of adopting UTC.
314+
*
315+
* NB: this must NOT elog(ERROR). The caller must get control back so that
316+
* it can restore the old value of TZ if we don't like the new one.
317+
*/
318+
static bool
319+
tzset_succeeded(const char *tz)
320+
{
321+
char tztmp[TZBUF_LEN];
322+
char *cp;
323+
int tzval;
324+
325+
/*
326+
* Check first set of heuristics to say that tzset definitely worked.
327+
*/
328+
if (tzname[1] && tzname[1][0] != '\0')
329+
return true;
330+
if (TIMEZONE_GLOBAL != 0)
331+
return true;
332+
333+
/*
334+
* Check for known spellings of "UTC". Note we must downcase the input
335+
* before passing it to DecodePosixTimezone().
336+
*/
337+
StrNCpy(tztmp, tz, sizeof(tztmp));
338+
for (cp = tztmp; *cp; cp++)
339+
*cp = tolower((unsigned char) *cp);
340+
if (DecodePosixTimezone(tztmp, &tzval) == 0)
341+
if (tzval == 0)
342+
return true;
343+
344+
return false;
345+
}
346+
347+
/*
348+
* Check whether timezone is acceptable.
349+
*
350+
* What we are doing here is checking for leap-second-aware timekeeping.
351+
* We need to reject such TZ settings because they'll wreak havoc with our
352+
* date/time arithmetic.
353+
*
354+
* NB: this must NOT elog(ERROR). The caller must get control back so that
355+
* it can restore the old value of TZ if we don't like the new one.
356+
*/
357+
static bool
358+
tz_acceptable(void)
359+
{
360+
struct tm tt;
361+
time_t time2000;
362+
363+
/*
364+
* To detect leap-second timekeeping, compute the time_t value for
365+
* local midnight, 2000-01-01. Insist that this be a multiple of 60;
366+
* any partial-minute offset has to be due to leap seconds.
367+
*/
368+
MemSet(&tt, 0, sizeof(tt));
369+
tt.tm_year = 100;
370+
tt.tm_mon = 0;
371+
tt.tm_mday = 1;
372+
tt.tm_isdst = -1;
373+
time2000 = mktime(&tt);
374+
if ((time2000 % 60) != 0)
375+
return false;
376+
377+
return true;
378+
}
239379

240380
/*
241381
* assign_timezone: GUC assign_hook for timezone
@@ -247,6 +387,21 @@ assign_timezone(const char *value, bool doit, bool interactive)
247387
char *endptr;
248388
double hours;
249389

390+
/*
391+
* On first call, see if there is a TZ in the original environment.
392+
* Save that value permanently.
393+
*/
394+
if (!have_saved_tz)
395+
{
396+
char *orig_tz = getenv("TZ");
397+
398+
if (orig_tz)
399+
StrNCpy(orig_tzbuf, orig_tz, sizeof(orig_tzbuf));
400+
else
401+
orig_tzbuf[0] = '\0';
402+
have_saved_tz = true;
403+
}
404+
250405
/*
251406
* Check for INTERVAL 'foo'
252407
*/
@@ -313,45 +468,95 @@ assign_timezone(const char *value, bool doit, bool interactive)
313468
else if (strcasecmp(value, "UNKNOWN") == 0)
314469
{
315470
/*
316-
* Clear any TZ value we may have established.
317-
*
318-
* unsetenv() works fine, but is BSD, not POSIX, and is not
319-
* available under Solaris, among others. Apparently putenv()
320-
* called as below clears the process-specific environment
321-
* variables. Other reasonable arguments to putenv() (e.g.
322-
* "TZ=", "TZ", "") result in a core dump (under Linux
323-
* anyway). - thomas 1998-01-26
471+
* UNKNOWN is the value shown as the "default" for TimeZone
472+
* in guc.c. We interpret it as meaning the original TZ
473+
* inherited from the environment. Note that if there is an
474+
* original TZ setting, we will return that rather than UNKNOWN
475+
* as the canonical spelling.
324476
*/
325477
if (doit)
326478
{
327-
if (tzbuf[0] == 'T')
479+
bool ok;
480+
481+
/* Revert to original setting of TZ, whatever it was */
482+
if (orig_tzbuf[0])
328483
{
329-
strcpy(tzbuf, "=");
330-
if (putenv(tzbuf) != 0)
331-
elog(ERROR, "Unable to clear TZ environment variable");
332-
tzset();
484+
set_tz(orig_tzbuf);
485+
ok = tzset_succeeded(orig_tzbuf) && tz_acceptable();
486+
}
487+
else
488+
{
489+
clear_tz();
490+
ok = tz_acceptable();
491+
}
492+
493+
if (ok)
494+
HasCTZSet = false;
495+
else
496+
{
497+
/* Bogus, so force UTC (equivalent to INTERVAL 0) */
498+
CTimeZone = 0;
499+
HasCTZSet = true;
333500
}
334-
HasCTZSet = false;
335501
}
336502
}
337503
else
338504
{
339505
/*
340506
* Otherwise assume it is a timezone name.
341507
*
342-
* XXX unfortunately we have no reasonable way to check whether a
343-
* timezone name is good, so we have to just assume that it
344-
* is.
508+
* We have to actually apply the change before we can have any
509+
* hope of checking it. So, save the old value in case we have
510+
* to back out. Note that it's possible the old setting is in
511+
* tzbuf, so we'd better copy it.
345512
*/
346-
if (doit)
513+
char save_tzbuf[TZBUF_LEN];
514+
char *save_tz;
515+
bool known,
516+
acceptable;
517+
518+
save_tz = getenv("TZ");
519+
if (save_tz)
520+
StrNCpy(save_tzbuf, save_tz, sizeof(save_tzbuf));
521+
522+
set_tz(value);
523+
524+
known = tzset_succeeded(value);
525+
acceptable = tz_acceptable();
526+
527+
if (doit && known && acceptable)
347528
{
348-
strcpy(tzbuf, "TZ=");
349-
strncat(tzbuf, value, sizeof(tzbuf) - 4);
350-
if (putenv(tzbuf) != 0) /* shouldn't happen? */
351-
elog(LOG, "assign_timezone: putenv failed");
352-
tzset();
529+
/* Keep the changed TZ */
353530
HasCTZSet = false;
354531
}
532+
else
533+
{
534+
/*
535+
* Revert to prior TZ setting; note we haven't changed
536+
* HasCTZSet in this path, so if we were previously using
537+
* a fixed offset, we still are.
538+
*/
539+
if (save_tz)
540+
set_tz(save_tzbuf);
541+
else
542+
clear_tz();
543+
/* Complain if it was bad */
544+
if (!known)
545+
{
546+
elog(interactive ? ERROR : LOG,
547+
"unrecognized timezone name \"%s\"",
548+
value);
549+
return NULL;
550+
}
551+
if (!acceptable)
552+
{
553+
elog(interactive ? ERROR : LOG,
554+
"timezone \"%s\" appears to use leap seconds"
555+
"\n\tPostgreSQL does not support leap seconds",
556+
value);
557+
return NULL;
558+
}
559+
}
355560
}
356561
}
357562

@@ -369,10 +574,7 @@ assign_timezone(const char *value, bool doit, bool interactive)
369574
return NULL;
370575

371576
if (HasCTZSet)
372-
{
373-
snprintf(result, sizeof(tzbuf), "%.5f",
374-
(double) CTimeZone / 3600.0);
375-
}
577+
snprintf(result, sizeof(tzbuf), "%.5f", (double) CTimeZone / 3600.0);
376578
else if (tzbuf[0] == 'T')
377579
strcpy(result, tzbuf + 3);
378580
else

0 commit comments

Comments
 (0)