diff options
Diffstat (limited to 'src/timezone/pgtz.c')
-rw-r--r-- | src/timezone/pgtz.c | 263 |
1 files changed, 262 insertions, 1 deletions
diff --git a/src/timezone/pgtz.c b/src/timezone/pgtz.c index c51e3b0382b..5356f40e3ca 100644 --- a/src/timezone/pgtz.c +++ b/src/timezone/pgtz.c @@ -6,13 +6,22 @@ * Portions Copyright (c) 1996-2003, PostgreSQL Global Development Group * * IDENTIFICATION - * $PostgreSQL: pgsql/src/timezone/pgtz.c,v 1.9 2004/05/18 03:36:45 momjian Exp $ + * $PostgreSQL: pgsql/src/timezone/pgtz.c,v 1.10 2004/05/21 05:08:06 tgl Exp $ * *------------------------------------------------------------------------- */ +#define NO_REDEFINE_TIMEFUNCS +#include "postgres.h" + +#include <ctype.h> + +#include "miscadmin.h" +#include "pgtime.h" #include "pgtz.h" #include "tzfile.h" +#include "utils/elog.h" +#include "utils/guc.h" static char tzdir[MAXPGPATH]; @@ -30,3 +39,255 @@ pg_TZDIR(void) done_tzdir = 1; return tzdir; } + +/* + * Try to determine the system timezone (as opposed to the timezone + * set in our own library). + */ +#define T_YEAR (60*60*24*365) +#define T_MONTH (60*60*24*30) + +struct tztry { + time_t std_t,dst_t; + char std_time[TZ_STRLEN_MAX+1],dst_time[TZ_STRLEN_MAX+1]; + int std_ofs,dst_ofs; + struct tm std_tm, dst_tm; +}; + + +static bool compare_tm(struct tm *s, struct pg_tm *p) { + if (s->tm_sec != p->tm_sec || + s->tm_min != p->tm_min || + s->tm_hour != p->tm_hour || + s->tm_mday != p->tm_mday || + s->tm_mon != p->tm_mon || + s->tm_year != p->tm_year || + s->tm_wday != p->tm_wday || + s->tm_yday != p->tm_yday || + s->tm_isdst != p->tm_isdst) + return false; + return true; +} + +static bool try_timezone(char *tzname, struct tztry *tt, bool checkdst) { + struct pg_tm *pgtm; + + if (!pg_tzset(tzname)) + return false; /* If this timezone couldn't be picked at all */ + + /* Verify standard time */ + pgtm = pg_localtime(&(tt->std_t)); + if (!pgtm) + return false; + if (!compare_tm(&(tt->std_tm), pgtm)) + return false; + + if (!checkdst) + return true; + + /* Now check daylight time */ + pgtm = pg_localtime(&(tt->dst_t)); + if (!pgtm) + return false; + if (!compare_tm(&(tt->dst_tm), pgtm)) + return false; + + return true; +} + +static int get_timezone_offset(struct tm *tm) { +#if defined(HAVE_STRUCT_TM_TM_ZONE) + return tm->tm_gmtoff; +#elif defined(HAVE_INT_TIMEZONE) +#ifdef HAVE_UNDERSCORE_TIMEZONE + return -_timezone; +#else + return -timezone; +#endif +#else +#error No way to determine TZ? Can this happen? +#endif +} + + +#ifdef WIN32 +#define TZABBREV(tz) win32_get_timezone_abbrev(tz) + +static char *win32_get_timezone_abbrev(char *tz) { + static char w32tzabbr[TZ_STRLEN_MAX+1]; + int l = 0; + char *c; + + for (c = tz; *c; c++) { + if (isupper(*c)) + w32tzabbr[l++] = *c; + } + w32tzabbr[l] = '\0'; + return w32tzabbr; +} + +#else +#define TZABBREV(tz) tz +#endif + + +/* + * Try to identify a timezone name (in our terminology) that matches the + * observed behavior of the system timezone library. We cannot assume that + * the system TZ environment setting (if indeed there is one) matches our + * terminology, so ignore it and just look at what localtime() returns. + */ +static char * +identify_system_timezone(void) +{ + static char __tzbuf[TZ_STRLEN_MAX+1]; + bool std_found=false, + dst_found=false; + time_t tnow = time(NULL); + time_t t; + struct tztry tt; + char cbuf[TZ_STRLEN_MAX+1]; + + /* Initialize OS timezone library */ + tzset(); + + memset(&tt, 0, sizeof(tt)); + + for (t = tnow; t < tnow+T_YEAR; t += T_MONTH) { + struct tm *tm = localtime(&t); + + if (tm->tm_isdst == 0 && !std_found) { + /* Standard time */ + memcpy(&tt.std_tm, tm, sizeof(struct tm)); + memset(cbuf,0,sizeof(cbuf)); + strftime(cbuf, sizeof(cbuf)-1, "%Z", tm); /* zone abbr */ + strcpy(tt.std_time, TZABBREV(cbuf)); + tt.std_ofs = get_timezone_offset(tm); + tt.std_t = t; + std_found = true; + } + else if (tm->tm_isdst == 1 && !dst_found) { + /* Daylight time */ + memcpy(&tt.dst_tm, tm, sizeof(struct tm)); + memset(cbuf,0,sizeof(cbuf)); + strftime(cbuf, sizeof(cbuf)-1, "%Z", tm); /* zone abbr */ + strcpy(tt.dst_time, TZABBREV(cbuf)); + tt.dst_ofs = get_timezone_offset(tm); + tt.dst_t = t; + dst_found = true; + } + if (std_found && dst_found) + break; /* Got both standard and daylight */ + } + + if (!std_found) + { + /* Failed to determine TZ! */ + ereport(LOG, + (errmsg("unable to determine system timezone, defaulting to \"%s\"", "GMT"), + errhint("You can specify the correct timezone in postgresql.conf."))); + return NULL; /* go to GMT */ + } + + if (dst_found) { + /* Try STD<ofs>DST */ + sprintf(__tzbuf,"%s%d%s",tt.std_time,-tt.std_ofs/3600,tt.dst_time); + if (try_timezone(__tzbuf, &tt, dst_found)) + return __tzbuf; + } + /* Try just the STD timezone */ + strcpy(__tzbuf,tt.std_time); + if (try_timezone(__tzbuf, &tt, dst_found)) + return __tzbuf; + + /* Did not find the timezone. Fallback to try a GMT zone. */ + sprintf(__tzbuf,"Etc/GMT%s%d", + (-tt.std_ofs<0)?"+":"",tt.std_ofs/3600); + ereport(LOG, + (errmsg("could not recognize system timezone, defaulting to \"%s\"", + __tzbuf), + errhint("You can specify the correct timezone in postgresql.conf."))); + return __tzbuf; +} + +/* + * 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 ereport(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. + */ +bool +tz_acceptable(void) +{ + struct pg_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 = pg_mktime(&tt); + if ((time2000 % 60) != 0) + return false; + + return true; +} + +/* + * Identify a suitable default timezone setting based on the environment, + * and make it active. + * + * We first look to the TZ environment variable. If not found or not + * recognized by our own code, we see if we can identify the timezone + * from the behavior of the system timezone library. When all else fails, + * fall back to GMT. + */ +const char * +select_default_timezone(void) +{ + char *def_tz; + + def_tz = getenv("TZ"); + if (def_tz && pg_tzset(def_tz) && tz_acceptable()) + return def_tz; + + def_tz = identify_system_timezone(); + if (def_tz && pg_tzset(def_tz) && tz_acceptable()) + return def_tz; + + if (pg_tzset("GMT") && tz_acceptable()) + return "GMT"; + + ereport(FATAL, + (errmsg("could not select a suitable default timezone"), + errdetail("It appears that your GMT time zone uses leap seconds. PostgreSQL does not support leap seconds."))); + return NULL; /* keep compiler quiet */ +} + +/* + * Initialize timezone library + * + * This is called after initial loading of postgresql.conf. If no TimeZone + * setting was found therein, we try to derive one from the environment. + */ +void pg_timezone_initialize(void) { + /* Do we need to try to figure the timezone? */ + if (strcmp(GetConfigOption("timezone"), "UNKNOWN") == 0) { + const char *def_tz; + + /* Select setting */ + def_tz = select_default_timezone(); + /* Tell GUC about the value. Will redundantly call pg_tzset() */ + SetConfigOption("timezone", def_tz, PGC_POSTMASTER, PGC_S_ENV_VAR); + } +} |