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);
    }
 }