Add code to identify_system_timezone() to try all zones in the zic
authorTom Lane <tgl@sss.pgh.pa.us>
Tue, 25 May 2004 18:08:59 +0000 (18:08 +0000)
committerTom Lane <tgl@sss.pgh.pa.us>
Tue, 25 May 2004 18:08:59 +0000 (18:08 +0000)
database, not just ones that we cons up POSIX names for.  This looks
grim but it seems to take less than a second even on a relatively slow
machine, and since it only happens once during postmaster startup, that
seems acceptable.

src/timezone/pgtz.c

index 6d1af5f3a14d2db5d8c91c7a11ea803c1dbf2ddd..77a0533ab338a05f2b6551d2ea381be0c333d3e6 100644 (file)
@@ -6,7 +6,7 @@
  * Portions Copyright (c) 1996-2003, PostgreSQL Global Development Group
  *
  * IDENTIFICATION
- *       $PostgreSQL: pgsql/src/timezone/pgtz.c,v 1.14 2004/05/24 02:30:29 tgl Exp $
+ *       $PostgreSQL: pgsql/src/timezone/pgtz.c,v 1.15 2004/05/25 18:08:59 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
 #include "postgres.h"
 
 #include <ctype.h>
+#include <sys/stat.h>
 
 #include "miscadmin.h"
 #include "pgtime.h"
 #include "pgtz.h"
+#include "storage/fd.h"
 #include "tzfile.h"
 #include "utils/elog.h"
 #include "utils/guc.h"
 
 
+#define T_DAY  ((time_t) (60*60*24))
+#define T_MONTH ((time_t) (60*60*24*31))
+
+struct tztry
+{
+       char            std_zone_name[TZ_STRLEN_MAX + 1],
+                               dst_zone_name[TZ_STRLEN_MAX + 1];
+#define MAX_TEST_TIMES 10
+       int                     n_test_times;
+       time_t          test_times[MAX_TEST_TIMES];
+};
+
 static char tzdir[MAXPGPATH];
 static int     done_tzdir = 0;
 
+static bool scan_available_timezones(char *tzdir, char *tzdirsub,
+                                                                        struct tztry *tt);
+
+
+/*
+ * Return full pathname of timezone data directory
+ */
 char *
 pg_TZDIR(void)
 {
@@ -41,22 +62,69 @@ pg_TZDIR(void)
 }
 
 /*
- * Try to determine the system timezone (as opposed to the timezone
- * set in our own library).
+ * Get GMT offset from a system struct tm
  */
-#define T_DAY  ((time_t) (60*60*24))
-#define T_MONTH ((time_t) (60*60*24*31))
+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
+}
 
-struct tztry
+/*
+ * Grotty kluge for win32 ... do we really need this?
+ */
+#ifdef WIN32
+#define TZABBREV(tz) win32_get_timezone_abbrev(tz)
+
+static char *
+win32_get_timezone_abbrev(const char *tz)
 {
-       char            std_zone_name[TZ_STRLEN_MAX + 1],
-                               dst_zone_name[TZ_STRLEN_MAX + 1];
-#define MAX_TEST_TIMES 5
-       int                     n_test_times;
-       time_t          test_times[MAX_TEST_TIMES];
-};
+       static char w32tzabbr[TZ_STRLEN_MAX + 1];
+       int                     l = 0;
+       const char  *c;
+
+       for (c = tz; *c; c++)
+       {
+               if (isupper((unsigned char) *c))
+                       w32tzabbr[l++] = *c;
+       }
+       w32tzabbr[l] = '\0';
+       return w32tzabbr;
+}
+
+#else
+#define TZABBREV(tz) (tz)
+#endif
+
+/*
+ * Convenience subroutine to convert y/m/d to time_t
+ */
+static time_t
+build_time_t(int year, int month, int day)
+{
+       struct tm       tm;
 
+       memset(&tm, 0, sizeof(tm));
+       tm.tm_mday = day;
+       tm.tm_mon = month - 1;
+       tm.tm_year = year - 1900;
+
+       return mktime(&tm);
+}
 
+/*
+ * Does a system tm value match one we computed ourselves?
+ */
 static bool
 compare_tm(struct tm *s, struct pg_tm *p)
 {
@@ -73,12 +141,16 @@ compare_tm(struct tm *s, struct pg_tm *p)
        return true;
 }
 
+/*
+ * See if a specific timezone setting matches the system behavior
+ */
 static bool
-try_timezone(char *tzname, struct tztry *tt)
+try_timezone(const char *tzname, struct tztry *tt)
 {
        int                     i;
        struct tm          *systm;
        struct pg_tm   *pgtm;
+       char            cbuf[TZ_STRLEN_MAX + 1];
 
        if (!pg_tzset(tzname))
                return false;                   /* can't handle the TZ name at all */
@@ -92,51 +164,25 @@ try_timezone(char *tzname, struct tztry *tt)
                systm = localtime(&(tt->test_times[i]));
                if (!compare_tm(systm, pgtm))
                        return false;
+               if (systm->tm_isdst >= 0)
+               {
+                       /* Check match of zone names, too */
+                       if (pgtm->tm_zone == NULL)
+                               return false;
+                       memset(cbuf, 0, sizeof(cbuf));
+                       strftime(cbuf, sizeof(cbuf) - 1, "%Z", systm); /* zone abbr */
+                       if (strcmp(TZABBREV(cbuf), pgtm->tm_zone) != 0)
+                               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;
+       /* Reject if leap seconds involved */
+       if (!tz_acceptable())
+               return false;
 
-       for (c = tz; *c; c++)
-       {
-               if (isupper(*c))
-                       w32tzabbr[l++] = *c;
-       }
-       w32tzabbr[l] = '\0';
-       return w32tzabbr;
+       return true;
 }
 
-#else
-#define TZABBREV(tz) tz
-#endif
-
 
 /*
  * Try to identify a timezone name (in our terminology) that matches the
@@ -155,6 +201,7 @@ identify_system_timezone(void)
        int                     std_ofs = 0;
        struct tztry tt;
        struct tm  *tm;
+       char            tmptzdir[MAXPGPATH];
        char            cbuf[TZ_STRLEN_MAX + 1];
 
        /* Initialize OS timezone library */
@@ -225,6 +272,20 @@ identify_system_timezone(void)
                }
        }
 
+       /*
+        * Add a couple of historical dates as well; without this we are likely
+        * to choose an accidental match, such as Antartica/Palmer when we
+        * really want America/Santiago.  Ideally we'd probe some dates before
+        * 1970 too, but that is guaranteed to fail if the system TZ library
+        * doesn't cope with DST before 1970.
+        */
+       tt.test_times[tt.n_test_times++] = build_time_t(1970, 1, 15);
+       tt.test_times[tt.n_test_times++] = build_time_t(1970, 7, 15);
+       tt.test_times[tt.n_test_times++] = build_time_t(1990, 4, 1);
+       tt.test_times[tt.n_test_times++] = build_time_t(1990, 10, 1);
+
+       Assert(tt.n_test_times <= MAX_TEST_TIMES);
+
        /* We should have found a STD zone name by now... */
        if (tt.std_zone_name[0] == '\0')
        {
@@ -234,7 +295,17 @@ identify_system_timezone(void)
                return NULL;                    /* go to GMT */
        }
 
-       /* If we found DST too then try STD<ofs>DST */
+       /* Search for a matching timezone file */
+       strcpy(tmptzdir, pg_TZDIR());
+       if (scan_available_timezones(tmptzdir,
+                                                                tmptzdir + strlen(tmptzdir) + 1,
+                                                                &tt))
+       {
+               StrNCpy(resultbuf, pg_get_current_timezone(), sizeof(resultbuf));
+               return resultbuf;
+       }
+
+       /* If we found DST then try STD<ofs>DST */
        if (tt.dst_zone_name[0] != '\0')
        {
                snprintf(resultbuf, sizeof(resultbuf), "%s%d%s",
@@ -270,6 +341,96 @@ identify_system_timezone(void)
        return resultbuf;
 }
 
+/*
+ * Recursively scan the timezone database looking for a usable match to
+ * the system timezone behavior.
+ *
+ * tzdir points to a buffer of size MAXPGPATH.  On entry, it holds the
+ * pathname of a directory containing TZ files.  We internally modify it
+ * to hold pathnames of sub-directories and files, but must restore it
+ * to its original contents before exit.
+ *
+ * tzdirsub points to the part of tzdir that represents the subfile name
+ * (ie, tzdir + the original directory name length, plus one for the
+ * first added '/').
+ *
+ * tt tells about the system timezone behavior we need to match.
+ *
+ * On success, returns TRUE leaving the proper timezone selected.
+ * On failure, returns FALSE with a random timezone selected.
+ */
+static bool
+scan_available_timezones(char *tzdir, char *tzdirsub, struct tztry *tt)
+{
+       int                     tzdir_orig_len = strlen(tzdir);
+       bool            found = false;
+       DIR                *dirdesc;
+
+       dirdesc = AllocateDir(tzdir);
+       if (!dirdesc)
+       {
+               ereport(LOG,
+                               (errcode_for_file_access(),
+                                errmsg("could not open directory \"%s\": %m", tzdir)));
+               return false;
+       }
+
+       for (;;)
+       {
+               struct dirent *direntry;
+               struct stat statbuf;
+
+               errno = 0;
+               direntry = readdir(dirdesc);
+               if (!direntry)
+               {
+                       if (errno)
+                               ereport(LOG,
+                                               (errcode_for_file_access(),
+                                                errmsg("error reading directory: %m")));
+                       break;
+               }
+
+               /* Ignore . and .., plus any other "hidden" files */
+               if (direntry->d_name[0] == '.')
+                       continue;
+
+               snprintf(tzdir + tzdir_orig_len, MAXPGPATH - tzdir_orig_len,
+                                "/%s", direntry->d_name);
+
+               if (stat(tzdir, &statbuf) != 0)
+               {
+                       ereport(LOG,
+                                       (errcode_for_file_access(),
+                                        errmsg("could not stat \"%s\": %m", tzdir)));
+                       continue;
+               }
+
+               if (S_ISDIR(statbuf.st_mode))
+               {
+                       /* Recurse into subdirectory */
+                       found = scan_available_timezones(tzdir, tzdirsub, tt);
+                       if (found)
+                               break;
+               }
+               else
+               {
+                       /* Load and test this file */
+                       found = try_timezone(tzdirsub, tt);
+                       if (found)
+                               break;
+               }
+       }
+
+       FreeDir(dirdesc);
+
+       /* Restore tzdir */
+       tzdir[tzdir_orig_len] = '\0';
+
+       return found;
+}
+
+
 /*
  * Check whether timezone is acceptable.
  *
@@ -351,6 +512,6 @@ pg_timezone_initialize(void)
                /* 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);
+               SetConfigOption("timezone", def_tz, PGC_POSTMASTER, PGC_S_ARGV);
        }
 }