Arrange for timezone names to be recognized case-insensitively; for
authorTom Lane
Mon, 16 Oct 2006 19:58:27 +0000 (19:58 +0000)
committerTom Lane
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 d9e4ea11b378fd4d5951edfce9f0a69963b45263..10da8d8d7b477fa7939159564c60edd98a4592ba 100644 (file)
@@ -1,4 +1,4 @@
-
+
 
  
   Data Types
@@ -1593,12 +1593,12 @@ SELECT b, char_length(b) FROM test2;
       linkend="datatype-datetime-time-table"> 
       and .)  If a time zone is
       specified in the input for time without time zone,
-      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
       America/New_York. 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
+      time with time zone value.
      
 
       
@@ -1653,7 +1653,7 @@ SELECT b, char_length(b) FROM test2;
          
          
           04:05:06 PST
-          time zone specified by name
+          time zone specified by abbreviation
          
          
           2003-04-12 04:05:06 America/New_York
@@ -2214,6 +2214,12 @@ January 8 04:05:06 1999 PST
      will always know the correct UTC offset for your region.
     
 
+    
+     In all cases, timezone names are recognized case-insensitively.
+     (This is a change from PostgreSQL versions
+     prior to 8.2, which were case-sensitive in some contexts and not others.)
+    
+
     
      Note that timezone names are not used for date/time output
      — all supported output formats use numeric timezone displays to
index 35fa21ef87432b1c65c317bd78926de1570a0486..f5c6c0db8dfba899e459e41ec709cb21b1641c93 100644 (file)
@@ -3,7 +3,7 @@
  * 1996-06-05 by Arthur David Olson ([email protected]).
  *
  * 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 6f9d4225b56aff19da4146d74a9d77b3ba5c6450..8e6a64d7869d0212e3c8461604a83031821afebf 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 
+#include 
 #include 
 #include 
 
@@ -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,20 +45,123 @@ 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 b58613bb6283a60034d9238a42c919d753d3af8e..0e1a06f82d44bb167311b2361f3520487c0cbb6a 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 8ef425367fc867c3b31b83bff60454ad6845b3ae..039f6f73e077d34dcf222eee755edfc68c310235 100644 (file)
@@ -3,7 +3,7 @@
  * 1996-06-05 by Arthur David Olson ([email protected]).
  *
  * 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;
 }