OSDN Git Service

Arrange for timezone names to be recognized case-insensitively; for
authorTom Lane <tgl@sss.pgh.pa.us>
Mon, 16 Oct 2006 19:58:27 +0000 (19:58 +0000)
committerTom Lane <tgl@sss.pgh.pa.us>
Mon, 16 Oct 2006 19:58:27 +0000 (19:58 +0000)
example SET TIME ZONE 'america/new_york' works now.  This seems a good
idea on general user-friendliness grounds, and is part of the solution
to the timestamp-input parsing problems I noted recently.

doc/src/sgml/datatype.sgml
src/timezone/localtime.c
src/timezone/pgtz.c
src/timezone/pgtz.h
src/timezone/zic.c

index d9e4ea1..10da8d8 100644 (file)
@@ -1,4 +1,4 @@
-<!-- $PostgreSQL: pgsql/doc/src/sgml/datatype.sgml,v 1.176 2006/09/22 16:20:00 tgl Exp $ -->
+<!-- $PostgreSQL: pgsql/doc/src/sgml/datatype.sgml,v 1.177 2006/10/16 19:58:26 tgl Exp $ -->
 
  <chapter id="datatype">
   <title id="datatype-title">Data Types</title>
@@ -1593,12 +1593,12 @@ SELECT b, char_length(b) FROM test2;
       linkend="datatype-datetime-time-table"> 
       and <xref linkend="datatype-timezone-table">.)  If a time zone is
       specified in the input for <type>time without time zone</type>,
-      it is silently ignored. You can also always specify a date but it will
-      be ignored except for when you use a full time zone name like
+      it is silently ignored. You can also specify a date but it will
+      be ignored, except when you use a full time zone name like
       <literal>America/New_York</literal>. In this case specifying the date
-      is compulsory in order to tell which time zone offset should be
-      applied. It will be applied whatever time zone offset was valid at that
-      date and time at the specified place.
+      is required in order to determine whether standard or daylight-savings
+      time applies.  The appropriate time zone offset is recorded in the
+      <type>time with time zone</type> value.
      </para>
 
       <table id="datatype-datetime-time-table">
@@ -1653,7 +1653,7 @@ SELECT b, char_length(b) FROM test2;
          </row>
          <row>
           <entry><literal>04:05:06 PST</literal></entry>
-          <entry>time zone specified by name</entry>
+          <entry>time zone specified by abbreviation</entry>
          </row>
          <row>
           <entry><literal>2003-04-12 04:05:06 America/New_York</literal></entry>
@@ -2215,6 +2215,12 @@ January 8 04:05:06 1999 PST
     </para>
 
     <para>
+     In all cases, timezone names are recognized case-insensitively.
+     (This is a change from <productname>PostgreSQL</productname> versions
+     prior to 8.2, which were case-sensitive in some contexts and not others.)
+    </para>
+
+    <para>
      Note that timezone names are <emphasis>not</> used for date/time output
      &mdash; all supported output formats use numeric timezone displays to
      avoid ambiguity.
index 35fa21e..f5c6c0d 100644 (file)
@@ -3,7 +3,7 @@
  * 1996-06-05 by Arthur David Olson (arthur_david_olson@nih.gov).
  *
  * IDENTIFICATION
- *       $PostgreSQL: pgsql/src/timezone/localtime.c,v 1.14 2006/06/07 22:24:46 momjian Exp $
+ *       $PostgreSQL: pgsql/src/timezone/localtime.c,v 1.15 2006/10/16 19:58:26 tgl Exp $
  */
 
 /*
@@ -122,7 +122,7 @@ detzcode(const char *codep)
 }
 
 int
-tzload(const char *name, struct state * sp)
+tzload(const char *name, char *canonname, struct state *sp)
 {
        const char *p;
        int                     i;
@@ -130,36 +130,11 @@ tzload(const char *name, struct state * sp)
 
        if (name == NULL && (name = TZDEFAULT) == NULL)
                return -1;
-       {
-               int                     doaccess;
-               char            fullname[MAXPGPATH];
-
-               if (name[0] == ':')
-                       ++name;
-               doaccess = name[0] == '/';
-               if (!doaccess)
-               {
-                       p = pg_TZDIR();
-                       if (p == NULL)
-                               return -1;
-                       if ((strlen(p) + strlen(name) + 1) >= sizeof fullname)
-                               return -1;
-                       (void) strcpy(fullname, p);
-                       (void) strcat(fullname, "/");
-                       (void) strcat(fullname, name);
-
-                       /*
-                        * Set doaccess if '.' (as in "../") shows up in name.
-                        */
-                       if (strchr(name, '.') != NULL)
-                               doaccess = TRUE;
-                       name = fullname;
-               }
-               if (doaccess && access(name, R_OK) != 0)
-                       return -1;
-               if ((fid = open(name, O_RDONLY | PG_BINARY, 0)) == -1)
-                       return -1;
-       }
+       if (name[0] == ':')
+               ++name;
+       fid = pg_open_tzfile(name, canonname);
+       if (fid < 0)
+               return -1;
        {
                struct tzhead *tzhp;
                union
@@ -587,7 +562,7 @@ tzparse(const char *name, struct state * sp, int lastditch)
                if (name == NULL)
                        return -1;
        }
-       load_result = tzload(TZDEFRULES, sp);
+       load_result = tzload(TZDEFRULES, NULL, sp);
        if (load_result != 0)
                sp->leapcnt = 0;                /* so, we're off a little */
        if (*name != '\0')
@@ -794,7 +769,7 @@ tzparse(const char *name, struct state * sp, int lastditch)
 static void
 gmtload(struct state * sp)
 {
-       if (tzload(gmt, sp) != 0)
+       if (tzload(gmt, NULL, sp) != 0)
                (void) tzparse(gmt, sp, TRUE);
 }
 
index 6f9d422..8e6a64d 100644 (file)
@@ -6,7 +6,7 @@
  * Portions Copyright (c) 1996-2006, PostgreSQL Global Development Group
  *
  * IDENTIFICATION
- *       $PostgreSQL: pgsql/src/timezone/pgtz.c,v 1.46 2006/10/04 00:30:14 momjian Exp $
+ *       $PostgreSQL: pgsql/src/timezone/pgtz.c,v 1.47 2006/10/16 19:58:27 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -15,6 +15,7 @@
 #include "postgres.h"
 
 #include <ctype.h>
+#include <fcntl.h>
 #include <sys/stat.h>
 #include <time.h>
 
@@ -31,8 +32,11 @@ pg_tz           *global_timezone = NULL;
 
 
 static char tzdir[MAXPGPATH];
-static int     done_tzdir = 0;
+static bool done_tzdir = false;
 
+static bool scan_directory_ci(const char *dirname,
+                                                         const char *fname, int fnamelen,
+                                                         char *canonname, int canonnamelen);
 static const char *identify_system_timezone(void);
 static const char *select_default_timezone(void);
 static bool set_global_timezone(const char *tzname);
@@ -41,21 +45,124 @@ static bool set_global_timezone(const char *tzname);
 /*
  * Return full pathname of timezone data directory
  */
-char *
+static char *
 pg_TZDIR(void)
 {
        if (done_tzdir)
                return tzdir;
 
        get_share_path(my_exec_path, tzdir);
-       strcat(tzdir, "/timezone");
+       strlcpy(tzdir + strlen(tzdir), "/timezone", MAXPGPATH - strlen(tzdir));
 
-       done_tzdir = 1;
+       done_tzdir = true;
        return tzdir;
 }
 
 
 /*
+ * Given a timezone name, open() the timezone data file.  Return the
+ * file descriptor if successful, -1 if not.
+ *
+ * The input name is searched for case-insensitively (we assume that the
+ * timezone database does not contain case-equivalent names).
+ *
+ * If "canonname" is not NULL, then on success the canonical spelling of the
+ * given name is stored there (it is assumed to be > TZ_STRLEN_MAX bytes!).
+ */
+int
+pg_open_tzfile(const char *name, char *canonname)
+{
+       const char *fname;
+       char            fullname[MAXPGPATH];
+       int                     fullnamelen;
+       int                     orignamelen;
+
+       /*
+        * Loop to split the given name into directory levels; for each level,
+        * search using scan_directory_ci().
+        */
+       strcpy(fullname, pg_TZDIR());
+       orignamelen = fullnamelen = strlen(fullname);
+       fname = name;
+       for (;;)
+       {
+               const char *slashptr;
+               int             fnamelen;
+
+               slashptr = strchr(fname, '/');
+               if (slashptr)
+                       fnamelen = slashptr - fname;
+               else
+                       fnamelen = strlen(fname);
+               if (fullnamelen + 1 + fnamelen >= MAXPGPATH)
+                       return -1;                      /* not gonna fit */
+               if (!scan_directory_ci(fullname, fname, fnamelen,
+                                                          fullname + fullnamelen + 1,
+                                                          MAXPGPATH - fullnamelen - 1))
+                       return -1;
+               fullname[fullnamelen++] = '/';
+               fullnamelen += strlen(fullname + fullnamelen);
+               if (slashptr)
+                       fname = slashptr + 1;
+               else
+                       break;
+       }
+
+       if (canonname)
+               strlcpy(canonname, fullname + orignamelen + 1, TZ_STRLEN_MAX + 1);
+
+       return open(fullname, O_RDONLY | PG_BINARY, 0);
+}
+
+
+/*
+ * Scan specified directory for a case-insensitive match to fname
+ * (of length fnamelen --- fname may not be null terminated!).  If found,
+ * copy the actual filename into canonname and return true.
+ */
+static bool
+scan_directory_ci(const char *dirname, const char *fname, int fnamelen,
+                                 char *canonname, int canonnamelen)
+{
+       bool            found = false;
+       DIR                *dirdesc;
+       struct dirent *direntry;
+
+       dirdesc = AllocateDir(dirname);
+       if (!dirdesc)
+       {
+               ereport(LOG,
+                               (errcode_for_file_access(),
+                                errmsg("could not open directory \"%s\": %m", dirname)));
+               return false;
+       }
+
+       while ((direntry = ReadDir(dirdesc, dirname)) != NULL)
+       {
+               /*
+                * Ignore . and .., plus any other "hidden" files.  This is a security
+                * measure to prevent access to files outside the timezone directory.
+                */
+               if (direntry->d_name[0] == '.')
+                       continue;
+
+               if (strlen(direntry->d_name) == fnamelen &&
+                       pg_strncasecmp(direntry->d_name, fname, fnamelen) == 0)
+               {
+                       /* Found our match */
+                       strlcpy(canonname, direntry->d_name, canonnamelen);
+                       found = true;
+                       break;
+               }
+       }
+
+       FreeDir(dirdesc);
+
+       return found;
+}
+
+
+/*
  * The following block of code attempts to determine which timezone in our
  * timezone database is the best match for the active system timezone.
  *
@@ -167,7 +274,7 @@ score_timezone(const char *tzname, struct tztry * tt)
         * Load timezone directly. Don't use pg_tzset, because we don't want all
         * timezones loaded in the cache at startup.
         */
-       if (tzload(tzname, &tz.state) != 0)
+       if (tzload(tzname, NULL, &tz.state) != 0)
        {
                if (tzname[0] == ':' || tzparse(tzname, &tz.state, FALSE) != 0)
                {
@@ -958,10 +1065,20 @@ identify_system_timezone(void)
 
 /*
  * We keep loaded timezones in a hashtable so we don't have to
- * load and parse the TZ definition file every time it is selected.
+ * load and parse the TZ definition file every time one is selected.
+ * Because we want timezone names to be found case-insensitively,
+ * the hash key is the uppercased name of the zone.
  */
+typedef struct
+{
+       /* tznameupper contains the all-upper-case name of the timezone */
+       char            tznameupper[TZ_STRLEN_MAX + 1];
+       pg_tz           tz;
+} pg_tz_cache;
+
 static HTAB *timezone_cache = NULL;
 
+
 static bool
 init_timezone_hashtable(void)
 {
@@ -970,7 +1087,7 @@ init_timezone_hashtable(void)
        MemSet(&hash_ctl, 0, sizeof(hash_ctl));
 
        hash_ctl.keysize = TZ_STRLEN_MAX + 1;
-       hash_ctl.entrysize = sizeof(pg_tz);
+       hash_ctl.entrysize = sizeof(pg_tz_cache);
 
        timezone_cache = hash_create("Timezones",
                                                                 4,
@@ -989,8 +1106,11 @@ init_timezone_hashtable(void)
 struct pg_tz *
 pg_tzset(const char *name)
 {
-       pg_tz      *tzp;
-       pg_tz           tz;
+       pg_tz_cache *tzp;
+       struct state tzstate;
+       char            uppername[TZ_STRLEN_MAX + 1];
+       char            canonname[TZ_STRLEN_MAX + 1];
+       char       *p;
 
        if (strlen(name) > TZ_STRLEN_MAX)
                return NULL;                    /* not going to fit */
@@ -999,37 +1119,49 @@ pg_tzset(const char *name)
                if (!init_timezone_hashtable())
                        return NULL;
 
-       tzp = (pg_tz *) hash_search(timezone_cache,
-                                                               name,
-                                                               HASH_FIND,
-                                                               NULL);
+       /*
+        * Upcase the given name to perform a case-insensitive hashtable search.
+        * (We could alternatively downcase it, but we prefer upcase so that we
+        * can get consistently upcased results from tzparse() in case the name
+        * is a POSIX-style timezone spec.)
+        */
+       p = uppername;
+       while (*name)
+               *p++ = pg_toupper((unsigned char) *name++);
+       *p = '\0';
+
+       tzp = (pg_tz_cache *) hash_search(timezone_cache,
+                                                                         uppername,
+                                                                         HASH_FIND,
+                                                                         NULL);
        if (tzp)
        {
                /* Timezone found in cache, nothing more to do */
-               return tzp;
+               return &tzp->tz;
        }
 
-       if (tzload(name, &tz.state) != 0)
+       if (tzload(uppername, canonname, &tzstate) != 0)
        {
-               if (name[0] == ':' || tzparse(name, &tz.state, FALSE) != 0)
+               if (uppername[0] == ':' || tzparse(uppername, &tzstate, FALSE) != 0)
                {
                        /* Unknown timezone. Fail our call instead of loading GMT! */
                        return NULL;
                }
+               /* For POSIX timezone specs, use uppercase name as canonical */
+               strcpy(canonname, uppername);
        }
 
-       strcpy(tz.TZname, name);
-
        /* Save timezone in the cache */
-       tzp = hash_search(timezone_cache,
-                                         name,
-                                         HASH_ENTER,
-                                         NULL);
+       tzp = (pg_tz_cache *) hash_search(timezone_cache,
+                                                                         uppername,
+                                                                         HASH_ENTER,
+                                                                         NULL);
 
-       strcpy(tzp->TZname, tz.TZname);
-       memcpy(&tzp->state, &tz.state, sizeof(tz.state));
+       /* hash_search already copied uppername into the hash key */
+       strcpy(tzp->tz.TZname, canonname);
+       memcpy(&tzp->tz.state, &tzstate, sizeof(tzstate));
 
-       return tzp;
+       return &tzp->tz;
 }
 
 
@@ -1241,14 +1373,13 @@ pg_tzenumerate_next(pg_tzenum *dir)
                 * Load this timezone using tzload() not pg_tzset(), so we don't fill
                 * the cache
                 */
-               if (tzload(fullname + dir->baselen, &dir->tz.state) != 0)
+               if (tzload(fullname + dir->baselen, dir->tz.TZname, &dir->tz.state) != 0)
                {
                        /* Zone could not be loaded, ignore it */
                        continue;
                }
 
                /* Timezone loaded OK. */
-               strcpy(dir->tz.TZname, fullname + dir->baselen);
                return &dir->tz;
        }
 
index b58613b..0e1a06f 100644 (file)
@@ -9,7 +9,7 @@
  * Portions Copyright (c) 1996-2006, PostgreSQL Global Development Group
  *
  * IDENTIFICATION
- *       $PostgreSQL: pgsql/src/timezone/pgtz.h,v 1.17 2006/07/11 13:54:25 momjian Exp $
+ *       $PostgreSQL: pgsql/src/timezone/pgtz.h,v 1.18 2006/10/16 19:58:27 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -19,7 +19,6 @@
 #include "tzfile.h"
 #include "pgtime.h"
 
-extern char *pg_TZDIR(void);
 
 #define BIGGEST(a, b)  (((a) > (b)) ? (a) : (b))
 
@@ -55,11 +54,17 @@ struct state
 
 struct pg_tz
 {
+       /* TZname contains the canonically-cased name of the timezone */
        char            TZname[TZ_STRLEN_MAX + 1];
        struct state state;
 };
 
-int                    tzload(const char *name, struct state * sp);
-int                    tzparse(const char *name, struct state * sp, int lastditch);
+
+/* in pgtz.c */
+extern int     pg_open_tzfile(const char *name, char *canonname);
+
+/* in localtime.c */
+extern int     tzload(const char *name, char *canonname, struct state *sp);
+extern int     tzparse(const char *name, struct state *sp, int lastditch);
 
 #endif   /* _PGTZ_H */
index 8ef4253..039f6f7 100644 (file)
@@ -3,7 +3,7 @@
  * 1996-06-05 by Arthur David Olson (arthur_david_olson@nih.gov).
  *
  * IDENTIFICATION
- *       $PostgreSQL: pgsql/src/timezone/zic.c,v 1.16 2005/10/15 02:49:51 momjian Exp $
+ *       $PostgreSQL: pgsql/src/timezone/zic.c,v 1.17 2006/10/16 19:58:27 tgl Exp $
  */
 
 #include "postgres.h"
@@ -2387,11 +2387,11 @@ link(const char *oldpath, const char *newpath)
 #endif
 
 /*
- *     This allows zic to compile by just assigning a dummy value.
+ *     This allows zic to compile by just returning a dummy value.
  *     localtime.c references it, but no one uses it from zic.
  */
-char *
-pg_TZDIR(void)
+int
+pg_open_tzfile(const char *name, char *canonname)
 {
-       return NULL;
+       return -1;
 }