summaryrefslogtreecommitdiffstats
path: root/src/timezone/pgtz.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/timezone/pgtz.c')
-rw-r--r--src/timezone/pgtz.c502
1 files changed, 502 insertions, 0 deletions
diff --git a/src/timezone/pgtz.c b/src/timezone/pgtz.c
new file mode 100644
index 0000000..82f14ee
--- /dev/null
+++ b/src/timezone/pgtz.c
@@ -0,0 +1,502 @@
+/*-------------------------------------------------------------------------
+ *
+ * pgtz.c
+ * Timezone Library Integration Functions
+ *
+ * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * src/timezone/pgtz.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include <ctype.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <time.h>
+
+#include "datatype/timestamp.h"
+#include "miscadmin.h"
+#include "pgtz.h"
+#include "storage/fd.h"
+#include "utils/hsearch.h"
+
+
+/* Current session timezone (controlled by TimeZone GUC) */
+pg_tz *session_timezone = NULL;
+
+/* Current log timezone (controlled by log_timezone GUC) */
+pg_tz *log_timezone = NULL;
+
+
+static bool scan_directory_ci(const char *dirname,
+ const char *fname, int fnamelen,
+ char *canonname, int canonnamelen);
+
+
+/*
+ * Return full pathname of timezone data directory
+ */
+static const char *
+pg_TZDIR(void)
+{
+#ifndef SYSTEMTZDIR
+ /* normal case: timezone stuff is under our share dir */
+ static bool done_tzdir = false;
+ static char tzdir[MAXPGPATH];
+
+ if (done_tzdir)
+ return tzdir;
+
+ get_share_path(my_exec_path, tzdir);
+ strlcpy(tzdir + strlen(tzdir), "/timezone", MAXPGPATH - strlen(tzdir));
+
+ done_tzdir = true;
+ return tzdir;
+#else
+ /* we're configured to use system's timezone database */
+ return SYSTEMTZDIR;
+#endif
+}
+
+
+/*
+ * 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 (the buffer must be > TZ_STRLEN_MAX bytes!).
+ */
+int
+pg_open_tzfile(const char *name, char *canonname)
+{
+ const char *fname;
+ char fullname[MAXPGPATH];
+ int fullnamelen;
+ int orignamelen;
+
+ /* Initialize fullname with base name of tzdata directory */
+ strlcpy(fullname, pg_TZDIR(), sizeof(fullname));
+ orignamelen = fullnamelen = strlen(fullname);
+
+ if (fullnamelen + 1 + strlen(name) >= MAXPGPATH)
+ return -1; /* not gonna fit */
+
+ /*
+ * If the caller doesn't need the canonical spelling, first just try to
+ * open the name as-is. This can be expected to succeed if the given name
+ * is already case-correct, or if the filesystem is case-insensitive; and
+ * we don't need to distinguish those situations if we aren't tasked with
+ * reporting the canonical spelling.
+ */
+ if (canonname == NULL)
+ {
+ int result;
+
+ fullname[fullnamelen] = '/';
+ /* test above ensured this will fit: */
+ strcpy(fullname + fullnamelen + 1, name);
+ result = open(fullname, O_RDONLY | PG_BINARY, 0);
+ if (result >= 0)
+ return result;
+ /* If that didn't work, fall through to do it the hard way */
+ fullname[fullnamelen] = '\0';
+ }
+
+ /*
+ * Loop to split the given name into directory levels; for each level,
+ * search using scan_directory_ci().
+ */
+ fname = name;
+ for (;;)
+ {
+ const char *slashptr;
+ int fnamelen;
+
+ slashptr = strchr(fname, '/');
+ if (slashptr)
+ fnamelen = slashptr - fname;
+ else
+ fnamelen = strlen(fname);
+ 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);
+
+ while ((direntry = ReadDirExtended(dirdesc, dirname, LOG)) != 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;
+}
+
+
+/*
+ * We keep loaded timezones in a hashtable so we don't have to
+ * 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)
+{
+ HASHCTL hash_ctl;
+
+ hash_ctl.keysize = TZ_STRLEN_MAX + 1;
+ hash_ctl.entrysize = sizeof(pg_tz_cache);
+
+ timezone_cache = hash_create("Timezones",
+ 4,
+ &hash_ctl,
+ HASH_ELEM | HASH_STRINGS);
+ if (!timezone_cache)
+ return false;
+
+ return true;
+}
+
+/*
+ * Load a timezone from file or from cache.
+ * Does not verify that the timezone is acceptable!
+ *
+ * "GMT" is always interpreted as the tzparse() definition, without attempting
+ * to load a definition from the filesystem. This has a number of benefits:
+ * 1. It's guaranteed to succeed, so we don't have the failure mode wherein
+ * the bootstrap default timezone setting doesn't work (as could happen if
+ * the OS attempts to supply a leap-second-aware version of "GMT").
+ * 2. Because we aren't accessing the filesystem, we can safely initialize
+ * the "GMT" zone definition before my_exec_path is known.
+ * 3. It's quick enough that we don't waste much time when the bootstrap
+ * default timezone setting is later overridden from postgresql.conf.
+ */
+pg_tz *
+pg_tzset(const char *name)
+{
+ 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 */
+
+ if (!timezone_cache)
+ if (!init_timezone_hashtable())
+ return 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->tz;
+ }
+
+ /*
+ * "GMT" is always sent to tzparse(), as per discussion above.
+ */
+ if (strcmp(uppername, "GMT") == 0)
+ {
+ if (!tzparse(uppername, &tzstate, true))
+ {
+ /* This really, really should not happen ... */
+ elog(ERROR, "could not initialize GMT time zone");
+ }
+ /* Use uppercase name as canonical */
+ strcpy(canonname, uppername);
+ }
+ else if (tzload(uppername, canonname, &tzstate, true) != 0)
+ {
+ if (uppername[0] == ':' || !tzparse(uppername, &tzstate, false))
+ {
+ /* Unknown timezone. Fail our call instead of loading GMT! */
+ return NULL;
+ }
+ /* For POSIX timezone specs, use uppercase name as canonical */
+ strcpy(canonname, uppername);
+ }
+
+ /* Save timezone in the cache */
+ tzp = (pg_tz_cache *) hash_search(timezone_cache,
+ uppername,
+ HASH_ENTER,
+ NULL);
+
+ /* hash_search already copied uppername into the hash key */
+ strcpy(tzp->tz.TZname, canonname);
+ memcpy(&tzp->tz.state, &tzstate, sizeof(tzstate));
+
+ return &tzp->tz;
+}
+
+/*
+ * Load a fixed-GMT-offset timezone.
+ * This is used for SQL-spec SET TIME ZONE INTERVAL 'foo' cases.
+ * It's otherwise equivalent to pg_tzset().
+ *
+ * The GMT offset is specified in seconds, positive values meaning west of
+ * Greenwich (ie, POSIX not ISO sign convention). However, we use ISO
+ * sign convention in the displayable abbreviation for the zone.
+ *
+ * Caution: this can fail (return NULL) if the specified offset is outside
+ * the range allowed by the zic library.
+ */
+pg_tz *
+pg_tzset_offset(long gmtoffset)
+{
+ long absoffset = (gmtoffset < 0) ? -gmtoffset : gmtoffset;
+ char offsetstr[64];
+ char tzname[128];
+
+ snprintf(offsetstr, sizeof(offsetstr),
+ "%02ld", absoffset / SECS_PER_HOUR);
+ absoffset %= SECS_PER_HOUR;
+ if (absoffset != 0)
+ {
+ snprintf(offsetstr + strlen(offsetstr),
+ sizeof(offsetstr) - strlen(offsetstr),
+ ":%02ld", absoffset / SECS_PER_MINUTE);
+ absoffset %= SECS_PER_MINUTE;
+ if (absoffset != 0)
+ snprintf(offsetstr + strlen(offsetstr),
+ sizeof(offsetstr) - strlen(offsetstr),
+ ":%02ld", absoffset);
+ }
+ if (gmtoffset > 0)
+ snprintf(tzname, sizeof(tzname), "<-%s>+%s",
+ offsetstr, offsetstr);
+ else
+ snprintf(tzname, sizeof(tzname), "<+%s>-%s",
+ offsetstr, offsetstr);
+
+ return pg_tzset(tzname);
+}
+
+
+/*
+ * Initialize timezone library
+ *
+ * This is called before GUC variable initialization begins. Its purpose
+ * is to ensure that log_timezone has a valid value before any logging GUC
+ * variables could become set to values that require elog.c to provide
+ * timestamps (e.g., log_line_prefix). We may as well initialize
+ * session_timezone to something valid, too.
+ */
+void
+pg_timezone_initialize(void)
+{
+ /*
+ * We may not yet know where PGSHAREDIR is (in particular this is true in
+ * an EXEC_BACKEND subprocess). So use "GMT", which pg_tzset forces to be
+ * interpreted without reference to the filesystem. This corresponds to
+ * the bootstrap default for these variables in guc.c, although in
+ * principle it could be different.
+ */
+ session_timezone = pg_tzset("GMT");
+ log_timezone = session_timezone;
+}
+
+
+/*
+ * Functions to enumerate available timezones
+ *
+ * Note that pg_tzenumerate_next() will return a pointer into the pg_tzenum
+ * structure, so the data is only valid up to the next call.
+ *
+ * All data is allocated using palloc in the current context.
+ */
+#define MAX_TZDIR_DEPTH 10
+
+struct pg_tzenum
+{
+ int baselen;
+ int depth;
+ DIR *dirdesc[MAX_TZDIR_DEPTH];
+ char *dirname[MAX_TZDIR_DEPTH];
+ struct pg_tz tz;
+};
+
+/* typedef pg_tzenum is declared in pgtime.h */
+
+pg_tzenum *
+pg_tzenumerate_start(void)
+{
+ pg_tzenum *ret = (pg_tzenum *) palloc0(sizeof(pg_tzenum));
+ char *startdir = pstrdup(pg_TZDIR());
+
+ ret->baselen = strlen(startdir) + 1;
+ ret->depth = 0;
+ ret->dirname[0] = startdir;
+ ret->dirdesc[0] = AllocateDir(startdir);
+ if (!ret->dirdesc[0])
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not open directory \"%s\": %m", startdir)));
+ return ret;
+}
+
+void
+pg_tzenumerate_end(pg_tzenum *dir)
+{
+ while (dir->depth >= 0)
+ {
+ FreeDir(dir->dirdesc[dir->depth]);
+ pfree(dir->dirname[dir->depth]);
+ dir->depth--;
+ }
+ pfree(dir);
+}
+
+pg_tz *
+pg_tzenumerate_next(pg_tzenum *dir)
+{
+ while (dir->depth >= 0)
+ {
+ struct dirent *direntry;
+ char fullname[MAXPGPATH * 2];
+ struct stat statbuf;
+
+ direntry = ReadDir(dir->dirdesc[dir->depth], dir->dirname[dir->depth]);
+
+ if (!direntry)
+ {
+ /* End of this directory */
+ FreeDir(dir->dirdesc[dir->depth]);
+ pfree(dir->dirname[dir->depth]);
+ dir->depth--;
+ continue;
+ }
+
+ if (direntry->d_name[0] == '.')
+ continue;
+
+ snprintf(fullname, sizeof(fullname), "%s/%s",
+ dir->dirname[dir->depth], direntry->d_name);
+ if (stat(fullname, &statbuf) != 0)
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not stat \"%s\": %m", fullname)));
+
+ if (S_ISDIR(statbuf.st_mode))
+ {
+ /* Step into the subdirectory */
+ if (dir->depth >= MAX_TZDIR_DEPTH - 1)
+ ereport(ERROR,
+ (errmsg_internal("timezone directory stack overflow")));
+ dir->depth++;
+ dir->dirname[dir->depth] = pstrdup(fullname);
+ dir->dirdesc[dir->depth] = AllocateDir(fullname);
+ if (!dir->dirdesc[dir->depth])
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not open directory \"%s\": %m",
+ fullname)));
+
+ /* Start over reading in the new directory */
+ continue;
+ }
+
+ /*
+ * Load this timezone using tzload() not pg_tzset(), so we don't fill
+ * the cache. Also, don't ask for the canonical spelling: we already
+ * know it, and pg_open_tzfile's way of finding it out is pretty
+ * inefficient.
+ */
+ if (tzload(fullname + dir->baselen, NULL, &dir->tz.state, true) != 0)
+ {
+ /* Zone could not be loaded, ignore it */
+ continue;
+ }
+
+ if (!pg_tz_acceptable(&dir->tz))
+ {
+ /* Ignore leap-second zones */
+ continue;
+ }
+
+ /* OK, return the canonical zone name spelling. */
+ strlcpy(dir->tz.TZname, fullname + dir->baselen,
+ sizeof(dir->tz.TZname));
+
+ /* Timezone loaded OK. */
+ return &dir->tz;
+ }
+
+ /* Nothing more found */
+ return NULL;
+}