mirror of
https://github.com/postgres/postgres.git
synced 2025-05-05 09:19:17 +03:00
timezone setting in the current year and for 100 years back, rather than always examining years 1904-2004. The original coding would have problems distinguishing zones whose behavior diverged only after 2004; which is a situation we will surely face sometime, if it's not out there already. In passing, also prevent selection of the dummy "Factory" timezone, even if that's exactly what the system is using. Reporting time as GMT seems better than that.
1456 lines
39 KiB
C
1456 lines
39 KiB
C
/*-------------------------------------------------------------------------
|
|
*
|
|
* pgtz.c
|
|
* Timezone Library Integration Functions
|
|
*
|
|
* Portions Copyright (c) 1996-2006, PostgreSQL Global Development Group
|
|
*
|
|
* IDENTIFICATION
|
|
* $PostgreSQL: pgsql/src/timezone/pgtz.c,v 1.48.2.2 2008/07/01 03:41:10 tgl Exp $
|
|
*
|
|
*-------------------------------------------------------------------------
|
|
*/
|
|
#define NO_REDEFINE_TIMEFUNCS
|
|
|
|
#include "postgres.h"
|
|
|
|
#include <ctype.h>
|
|
#include <fcntl.h>
|
|
#include <sys/stat.h>
|
|
#include <time.h>
|
|
|
|
#include "miscadmin.h"
|
|
#include "pgtz.h"
|
|
#include "storage/fd.h"
|
|
#include "tzfile.h"
|
|
#include "utils/datetime.h"
|
|
#include "utils/guc.h"
|
|
#include "utils/hsearch.h"
|
|
|
|
/* Current global timezone */
|
|
pg_tz *global_timezone = NULL;
|
|
|
|
|
|
static char tzdir[MAXPGPATH];
|
|
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);
|
|
|
|
|
|
/*
|
|
* Return full pathname of timezone data directory
|
|
*/
|
|
static char *
|
|
pg_TZDIR(void)
|
|
{
|
|
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;
|
|
}
|
|
|
|
|
|
/*
|
|
* 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.
|
|
*
|
|
* On most systems, we rely on trying to match the observable behavior of
|
|
* the C library's localtime() function. The database zone that matches
|
|
* furthest into the past is the one to use. Often there will be several
|
|
* zones with identical rankings (since the zic database assigns multiple
|
|
* names to many zones). We break ties arbitrarily by preferring shorter,
|
|
* then alphabetically earlier zone names.
|
|
*
|
|
* Win32's native knowledge about timezones appears to be too incomplete
|
|
* and too different from the zic database for the above matching strategy
|
|
* to be of any use. But there is just a limited number of timezones
|
|
* available, so we can rely on a handmade mapping table instead.
|
|
*/
|
|
|
|
#ifndef WIN32
|
|
|
|
#define T_DAY ((time_t) (60*60*24))
|
|
#define T_WEEK ((time_t) (60*60*24*7))
|
|
#define T_MONTH ((time_t) (60*60*24*31))
|
|
|
|
#define MAX_TEST_TIMES (52*100) /* 100 years */
|
|
|
|
struct tztry
|
|
{
|
|
int n_test_times;
|
|
time_t test_times[MAX_TEST_TIMES];
|
|
};
|
|
|
|
static void scan_available_timezones(char *tzdir, char *tzdirsub,
|
|
struct tztry * tt,
|
|
int *bestscore, char *bestzonename);
|
|
|
|
|
|
/*
|
|
* Get GMT offset from a system struct tm
|
|
*/
|
|
static int
|
|
get_timezone_offset(struct tm * tm)
|
|
{
|
|
#if defined(HAVE_STRUCT_TM_TM_ZONE)
|
|
return tm->tm_gmtoff;
|
|
#elif defined(HAVE_INT_TIMEZONE)
|
|
return -TIMEZONE_GLOBAL;
|
|
#else
|
|
#error No way to determine TZ? Can this happen?
|
|
#endif
|
|
}
|
|
|
|
/*
|
|
* Convenience subroutine to convert y/m/d to time_t (NOT pg_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)
|
|
{
|
|
if (s->tm_sec != p->tm_sec ||
|
|
s->tm_min != p->tm_min ||
|
|
s->tm_hour != p->tm_hour ||
|
|
s->tm_mday != p->tm_mday ||
|
|
s->tm_mon != p->tm_mon ||
|
|
s->tm_year != p->tm_year ||
|
|
s->tm_wday != p->tm_wday ||
|
|
s->tm_yday != p->tm_yday ||
|
|
s->tm_isdst != p->tm_isdst)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* See how well a specific timezone setting matches the system behavior
|
|
*
|
|
* We score a timezone setting according to the number of test times it
|
|
* matches. (The test times are ordered later-to-earlier, but this routine
|
|
* doesn't actually know that; it just scans until the first non-match.)
|
|
*
|
|
* We return -1 for a completely unusable setting; this is worse than the
|
|
* score of zero for a setting that works but matches not even the first
|
|
* test time.
|
|
*/
|
|
static int
|
|
score_timezone(const char *tzname, struct tztry * tt)
|
|
{
|
|
int i;
|
|
pg_time_t pgtt;
|
|
struct tm *systm;
|
|
struct pg_tm *pgtm;
|
|
char cbuf[TZ_STRLEN_MAX + 1];
|
|
pg_tz tz;
|
|
|
|
|
|
/*
|
|
* Load timezone directly. Don't use pg_tzset, because we don't want all
|
|
* timezones loaded in the cache at startup.
|
|
*/
|
|
if (tzload(tzname, NULL, &tz.state) != 0)
|
|
{
|
|
if (tzname[0] == ':' || tzparse(tzname, &tz.state, FALSE) != 0)
|
|
{
|
|
return -1; /* can't handle the TZ name at all */
|
|
}
|
|
}
|
|
|
|
/* Reject if leap seconds involved */
|
|
if (!tz_acceptable(&tz))
|
|
{
|
|
elog(DEBUG4, "Reject TZ \"%s\": uses leap seconds", tzname);
|
|
return -1;
|
|
}
|
|
|
|
/* Check for match at all the test times */
|
|
for (i = 0; i < tt->n_test_times; i++)
|
|
{
|
|
pgtt = (pg_time_t) (tt->test_times[i]);
|
|
pgtm = pg_localtime(&pgtt, &tz);
|
|
if (!pgtm)
|
|
return -1; /* probably shouldn't happen */
|
|
systm = localtime(&(tt->test_times[i]));
|
|
if (!systm)
|
|
{
|
|
elog(DEBUG4, "TZ \"%s\" scores %d: at %ld %04d-%02d-%02d %02d:%02d:%02d %s, system had no data",
|
|
tzname, i, (long) pgtt,
|
|
pgtm->tm_year + 1900, pgtm->tm_mon + 1, pgtm->tm_mday,
|
|
pgtm->tm_hour, pgtm->tm_min, pgtm->tm_sec,
|
|
pgtm->tm_isdst ? "dst" : "std");
|
|
return i;
|
|
}
|
|
if (!compare_tm(systm, pgtm))
|
|
{
|
|
elog(DEBUG4, "TZ \"%s\" scores %d: at %ld %04d-%02d-%02d %02d:%02d:%02d %s versus %04d-%02d-%02d %02d:%02d:%02d %s",
|
|
tzname, i, (long) pgtt,
|
|
pgtm->tm_year + 1900, pgtm->tm_mon + 1, pgtm->tm_mday,
|
|
pgtm->tm_hour, pgtm->tm_min, pgtm->tm_sec,
|
|
pgtm->tm_isdst ? "dst" : "std",
|
|
systm->tm_year + 1900, systm->tm_mon + 1, systm->tm_mday,
|
|
systm->tm_hour, systm->tm_min, systm->tm_sec,
|
|
systm->tm_isdst ? "dst" : "std");
|
|
return i;
|
|
}
|
|
if (systm->tm_isdst >= 0)
|
|
{
|
|
/* Check match of zone names, too */
|
|
if (pgtm->tm_zone == NULL)
|
|
return -1; /* probably shouldn't happen */
|
|
memset(cbuf, 0, sizeof(cbuf));
|
|
strftime(cbuf, sizeof(cbuf) - 1, "%Z", systm); /* zone abbr */
|
|
if (strcmp(cbuf, pgtm->tm_zone) != 0)
|
|
{
|
|
elog(DEBUG4, "TZ \"%s\" scores %d: at %ld \"%s\" versus \"%s\"",
|
|
tzname, i, (long) pgtt,
|
|
pgtm->tm_zone, cbuf);
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
|
|
elog(DEBUG4, "TZ \"%s\" gets max score %d", tzname, i);
|
|
return i;
|
|
}
|
|
|
|
|
|
/*
|
|
* Try to identify a timezone name (in our terminology) that best matches the
|
|
* observed behavior of the system timezone library. We cannot assume that
|
|
* the system TZ environment setting (if indeed there is one) matches our
|
|
* terminology, so we ignore it and just look at what localtime() returns.
|
|
*/
|
|
static const char *
|
|
identify_system_timezone(void)
|
|
{
|
|
static char resultbuf[TZ_STRLEN_MAX + 1];
|
|
time_t tnow;
|
|
time_t t;
|
|
struct tztry tt;
|
|
struct tm *tm;
|
|
int thisyear;
|
|
int bestscore;
|
|
char tmptzdir[MAXPGPATH];
|
|
int std_ofs;
|
|
char std_zone_name[TZ_STRLEN_MAX + 1],
|
|
dst_zone_name[TZ_STRLEN_MAX + 1];
|
|
char cbuf[TZ_STRLEN_MAX + 1];
|
|
|
|
/* Initialize OS timezone library */
|
|
tzset();
|
|
|
|
/*
|
|
* Set up the list of dates to be probed to see how well our timezone
|
|
* matches the system zone. We first probe January and July of the
|
|
* current year; this serves to quickly eliminate the vast majority of the
|
|
* TZ database entries. If those dates match, we probe every week for 100
|
|
* years backwards from the current July. (Weekly resolution is good
|
|
* enough to identify DST transition rules, since everybody switches on
|
|
* Sundays.) This is sufficient to cover most of the Unix time_t range,
|
|
* and we don't want to look further than that since many systems won't
|
|
* have sane TZ behavior further back anyway. The further
|
|
* back the zone matches, the better we score it. This may seem like a
|
|
* rather random way of doing things, but experience has shown that
|
|
* system-supplied timezone definitions are likely to have DST behavior
|
|
* that is right for the recent past and not so accurate further back.
|
|
* Scoring in this way allows us to recognize zones that have some
|
|
* commonality with the zic database, without insisting on exact match.
|
|
* (Note: we probe Thursdays, not Sundays, to avoid triggering
|
|
* DST-transition bugs in localtime itself.)
|
|
*/
|
|
tnow = time(NULL);
|
|
tm = localtime(&tnow);
|
|
if (!tm)
|
|
return NULL; /* give up if localtime is broken... */
|
|
thisyear = tm->tm_year + 1900;
|
|
|
|
t = build_time_t(thisyear, 1, 15);
|
|
/*
|
|
* Round back to GMT midnight Thursday. This depends on the knowledge
|
|
* that the time_t origin is Thu Jan 01 1970. (With a different origin
|
|
* we'd be probing some other day of the week, but it wouldn't matter
|
|
* anyway unless localtime() had DST-transition bugs.)
|
|
*/
|
|
t -= (t % T_WEEK);
|
|
|
|
tt.n_test_times = 0;
|
|
tt.test_times[tt.n_test_times++] = t;
|
|
|
|
t = build_time_t(thisyear, 7, 15);
|
|
t -= (t % T_WEEK);
|
|
|
|
tt.test_times[tt.n_test_times++] = t;
|
|
|
|
while (tt.n_test_times < MAX_TEST_TIMES)
|
|
{
|
|
t -= T_WEEK;
|
|
tt.test_times[tt.n_test_times++] = t;
|
|
}
|
|
|
|
/* Search for the best-matching timezone file */
|
|
strcpy(tmptzdir, pg_TZDIR());
|
|
bestscore = -1;
|
|
resultbuf[0] = '\0';
|
|
scan_available_timezones(tmptzdir, tmptzdir + strlen(tmptzdir) + 1,
|
|
&tt,
|
|
&bestscore, resultbuf);
|
|
if (bestscore > 0)
|
|
{
|
|
/* Ignore zic's rather silly "Factory" time zone; use GMT instead */
|
|
if (strcmp(resultbuf, "Factory") == 0)
|
|
return NULL;
|
|
return resultbuf;
|
|
}
|
|
|
|
/*
|
|
* Couldn't find a match in the database, so next we try constructed zone
|
|
* names (like "PST8PDT").
|
|
*
|
|
* First we need to determine the names of the local standard and daylight
|
|
* zones. The idea here is to scan forward from today until we have seen
|
|
* both zones, if both are in use.
|
|
*/
|
|
memset(std_zone_name, 0, sizeof(std_zone_name));
|
|
memset(dst_zone_name, 0, sizeof(dst_zone_name));
|
|
std_ofs = 0;
|
|
|
|
tnow = time(NULL);
|
|
|
|
/*
|
|
* Round back to a GMT midnight so results don't depend on local time of
|
|
* day
|
|
*/
|
|
tnow -= (tnow % T_DAY);
|
|
|
|
/*
|
|
* We have to look a little further ahead than one year, in case today is
|
|
* just past a DST boundary that falls earlier in the year than the next
|
|
* similar boundary. Arbitrarily scan up to 14 months.
|
|
*/
|
|
for (t = tnow; t <= tnow + T_MONTH * 14; t += T_MONTH)
|
|
{
|
|
tm = localtime(&t);
|
|
if (!tm)
|
|
continue;
|
|
if (tm->tm_isdst < 0)
|
|
continue;
|
|
if (tm->tm_isdst == 0 && std_zone_name[0] == '\0')
|
|
{
|
|
/* found STD zone */
|
|
memset(cbuf, 0, sizeof(cbuf));
|
|
strftime(cbuf, sizeof(cbuf) - 1, "%Z", tm); /* zone abbr */
|
|
strcpy(std_zone_name, cbuf);
|
|
std_ofs = get_timezone_offset(tm);
|
|
}
|
|
if (tm->tm_isdst > 0 && dst_zone_name[0] == '\0')
|
|
{
|
|
/* found DST zone */
|
|
memset(cbuf, 0, sizeof(cbuf));
|
|
strftime(cbuf, sizeof(cbuf) - 1, "%Z", tm); /* zone abbr */
|
|
strcpy(dst_zone_name, cbuf);
|
|
}
|
|
/* Done if found both */
|
|
if (std_zone_name[0] && dst_zone_name[0])
|
|
break;
|
|
}
|
|
|
|
/* We should have found a STD zone name by now... */
|
|
if (std_zone_name[0] == '\0')
|
|
{
|
|
ereport(LOG,
|
|
(errmsg("unable to determine system timezone, defaulting to \"%s\"", "GMT"),
|
|
errhint("You can specify the correct timezone in postgresql.conf.")));
|
|
return NULL; /* go to GMT */
|
|
}
|
|
|
|
/* If we found DST then try STD<ofs>DST */
|
|
if (dst_zone_name[0] != '\0')
|
|
{
|
|
snprintf(resultbuf, sizeof(resultbuf), "%s%d%s",
|
|
std_zone_name, -std_ofs / 3600, dst_zone_name);
|
|
if (score_timezone(resultbuf, &tt) > 0)
|
|
return resultbuf;
|
|
}
|
|
|
|
/* Try just the STD timezone (works for GMT at least) */
|
|
strcpy(resultbuf, std_zone_name);
|
|
if (score_timezone(resultbuf, &tt) > 0)
|
|
return resultbuf;
|
|
|
|
/* Try STD<ofs> */
|
|
snprintf(resultbuf, sizeof(resultbuf), "%s%d",
|
|
std_zone_name, -std_ofs / 3600);
|
|
if (score_timezone(resultbuf, &tt) > 0)
|
|
return resultbuf;
|
|
|
|
/*
|
|
* Did not find the timezone. Fallback to use a GMT zone. Note that the
|
|
* zic timezone database names the GMT-offset zones in POSIX style: plus
|
|
* is west of Greenwich. It's unfortunate that this is opposite of SQL
|
|
* conventions. Should we therefore change the names? Probably not...
|
|
*/
|
|
snprintf(resultbuf, sizeof(resultbuf), "Etc/GMT%s%d",
|
|
(-std_ofs > 0) ? "+" : "", -std_ofs / 3600);
|
|
|
|
ereport(LOG,
|
|
(errmsg("could not recognize system timezone, defaulting to \"%s\"",
|
|
resultbuf),
|
|
errhint("You can specify the correct timezone in postgresql.conf.")));
|
|
return resultbuf;
|
|
}
|
|
|
|
/*
|
|
* Recursively scan the timezone database looking for the best 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.
|
|
*
|
|
* *bestscore and *bestzonename on entry hold the best score found so far
|
|
* and the name of the best zone. We overwrite them if we find a better
|
|
* score. bestzonename must be a buffer of length TZ_STRLEN_MAX + 1.
|
|
*/
|
|
static void
|
|
scan_available_timezones(char *tzdir, char *tzdirsub, struct tztry * tt,
|
|
int *bestscore, char *bestzonename)
|
|
{
|
|
int tzdir_orig_len = strlen(tzdir);
|
|
DIR *dirdesc;
|
|
struct dirent *direntry;
|
|
|
|
dirdesc = AllocateDir(tzdir);
|
|
if (!dirdesc)
|
|
{
|
|
ereport(LOG,
|
|
(errcode_for_file_access(),
|
|
errmsg("could not open directory \"%s\": %m", tzdir)));
|
|
return;
|
|
}
|
|
|
|
while ((direntry = ReadDir(dirdesc, tzdir)) != NULL)
|
|
{
|
|
struct stat statbuf;
|
|
|
|
/* 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)));
|
|
tzdir[tzdir_orig_len] = '\0';
|
|
continue;
|
|
}
|
|
|
|
if (S_ISDIR(statbuf.st_mode))
|
|
{
|
|
/* Recurse into subdirectory */
|
|
scan_available_timezones(tzdir, tzdirsub, tt,
|
|
bestscore, bestzonename);
|
|
}
|
|
else
|
|
{
|
|
/* Load and test this file */
|
|
int score = score_timezone(tzdirsub, tt);
|
|
|
|
if (score > *bestscore)
|
|
{
|
|
*bestscore = score;
|
|
StrNCpy(bestzonename, tzdirsub, TZ_STRLEN_MAX + 1);
|
|
}
|
|
else if (score == *bestscore)
|
|
{
|
|
/* Consider how to break a tie */
|
|
if (strlen(tzdirsub) < strlen(bestzonename) ||
|
|
(strlen(tzdirsub) == strlen(bestzonename) &&
|
|
strcmp(tzdirsub, bestzonename) < 0))
|
|
StrNCpy(bestzonename, tzdirsub, TZ_STRLEN_MAX + 1);
|
|
}
|
|
}
|
|
|
|
/* Restore tzdir */
|
|
tzdir[tzdir_orig_len] = '\0';
|
|
}
|
|
|
|
FreeDir(dirdesc);
|
|
}
|
|
#else /* WIN32 */
|
|
|
|
static const struct
|
|
{
|
|
const char *stdname; /* Windows name of standard timezone */
|
|
const char *dstname; /* Windows name of daylight timezone */
|
|
const char *pgtzname; /* Name of pgsql timezone to map to */
|
|
} win32_tzmap[] =
|
|
|
|
{
|
|
/*
|
|
* This list was built from the contents of the registry at
|
|
* HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time
|
|
* Zones on Windows XP Professional SP2
|
|
*
|
|
* The zones have been matched to zic timezones by looking at the cities
|
|
* listed in the win32 display name (in the comment here) in most cases.
|
|
*/
|
|
{
|
|
"Afghanistan Standard Time", "Afghanistan Daylight Time",
|
|
"Asia/Kabul"
|
|
}, /* (GMT+04:30) Kabul */
|
|
{
|
|
"Alaskan Standard Time", "Alaskan Daylight Time",
|
|
"US/Alaska"
|
|
}, /* (GMT-09:00) Alaska */
|
|
{
|
|
"Arab Standard Time", "Arab Daylight Time",
|
|
"Asia/Kuwait"
|
|
}, /* (GMT+03:00) Kuwait, Riyadh */
|
|
{
|
|
"Arabian Standard Time", "Arabian Daylight Time",
|
|
"Asia/Muscat"
|
|
}, /* (GMT+04:00) Abu Dhabi, Muscat */
|
|
{
|
|
"Arabic Standard Time", "Arabic Daylight Time",
|
|
"Asia/Baghdad"
|
|
}, /* (GMT+03:00) Baghdad */
|
|
{
|
|
"Armenian Standard Time", "Armenian Daylight Time",
|
|
"Asia/Yerevan"
|
|
}, /* (GMT+04:00) Yerevan */
|
|
{
|
|
"Atlantic Standard Time", "Atlantic Daylight Time",
|
|
"Canada/Atlantic"
|
|
}, /* (GMT-04:00) Atlantic Time (Canada) */
|
|
{
|
|
"AUS Central Standard Time", "AUS Central Daylight Time",
|
|
"Australia/Darwin"
|
|
}, /* (GMT+09:30) Darwin */
|
|
{
|
|
"AUS Eastern Standard Time", "AUS Eastern Daylight Time",
|
|
"Australia/Canberra"
|
|
}, /* (GMT+10:00) Canberra, Melbourne, Sydney */
|
|
{
|
|
"Azores Standard Time", "Azores Daylight Time",
|
|
"Atlantic/Azores"
|
|
}, /* (GMT-01:00) Azores */
|
|
{
|
|
"Canada Central Standard Time", "Canada Central Daylight Time",
|
|
"Canada/Saskatchewan"
|
|
}, /* (GMT-06:00) Saskatchewan */
|
|
{
|
|
"Cape Verde Standard Time", "Cape Verde Daylight Time",
|
|
"Atlantic/Cape_Verde"
|
|
}, /* (GMT-01:00) Cape Verde Is. */
|
|
{
|
|
"Caucasus Standard Time", "Caucasus Daylight Time",
|
|
"Asia/Baku"
|
|
}, /* (GMT+04:00) Baku, Tbilisi, Yerevan */
|
|
{
|
|
"Cen. Australia Standard Time", "Cen. Australia Daylight Time",
|
|
"Australia/Adelaide"
|
|
}, /* (GMT+09:30) Adelaide */
|
|
{
|
|
"Central America Standard Time", "Central America Daylight Time",
|
|
"CST6CDT"
|
|
}, /* (GMT-06:00) Central America */
|
|
{
|
|
"Central Asia Standard Time", "Central Asia Daylight Time",
|
|
"Asia/Dhaka"
|
|
}, /* (GMT+06:00) Astana, Dhaka */
|
|
{
|
|
"Central Europe Standard Time", "Central Europe Daylight Time",
|
|
"Europe/Belgrade"
|
|
}, /* (GMT+01:00) Belgrade, Bratislava, Budapest,
|
|
* Ljubljana, Prague */
|
|
{
|
|
"Central European Standard Time", "Central European Daylight Time",
|
|
"Europe/Sarajevo"
|
|
}, /* (GMT+01:00) Sarajevo, Skopje, Warsaw,
|
|
* Zagreb */
|
|
{
|
|
"Central Pacific Standard Time", "Central Pacific Daylight Time",
|
|
"Pacific/Noumea"
|
|
}, /* (GMT+11:00) Magadan, Solomon Is., New
|
|
* Caledonia */
|
|
{
|
|
"Central Standard Time", "Central Daylight Time",
|
|
"US/Central"
|
|
}, /* (GMT-06:00) Central Time (US & Canada) */
|
|
{
|
|
"Central Standard Time (Mexico)", "Central Daylight Time (Mexico)",
|
|
"America/Mexico_City"
|
|
}, /* (GMT-06:00) Guadalajara, Mexico City,
|
|
Monterrey - New */
|
|
{
|
|
"China Standard Time", "China Daylight Time",
|
|
"Asia/Hong_Kong"
|
|
}, /* (GMT+08:00) Beijing, Chongqing, Hong Kong,
|
|
* Urumqi */
|
|
{
|
|
"Dateline Standard Time", "Dateline Daylight Time",
|
|
"Etc/GMT+12"
|
|
}, /* (GMT-12:00) International Date Line West */
|
|
{
|
|
"E. Africa Standard Time", "E. Africa Daylight Time",
|
|
"Africa/Nairobi"
|
|
}, /* (GMT+03:00) Nairobi */
|
|
{
|
|
"E. Australia Standard Time", "E. Australia Daylight Time",
|
|
"Australia/Brisbane"
|
|
}, /* (GMT+10:00) Brisbane */
|
|
{
|
|
"E. Europe Standard Time", "E. Europe Daylight Time",
|
|
"Europe/Bucharest"
|
|
}, /* (GMT+02:00) Bucharest */
|
|
{
|
|
"E. South America Standard Time", "E. South America Daylight Time",
|
|
"America/Araguaina"
|
|
}, /* (GMT-03:00) Brasilia */
|
|
{
|
|
"Eastern Standard Time", "Eastern Daylight Time",
|
|
"US/Eastern"
|
|
}, /* (GMT-05:00) Eastern Time (US & Canada) */
|
|
{
|
|
"Egypt Standard Time", "Egypt Daylight Time",
|
|
"Africa/Cairo"
|
|
}, /* (GMT+02:00) Cairo */
|
|
{
|
|
"Ekaterinburg Standard Time", "Ekaterinburg Daylight Time",
|
|
"Asia/Yekaterinburg"
|
|
}, /* (GMT+05:00) Ekaterinburg */
|
|
{
|
|
"Fiji Standard Time", "Fiji Daylight Time",
|
|
"Pacific/Fiji"
|
|
}, /* (GMT+12:00) Fiji, Kamchatka, Marshall Is. */
|
|
{
|
|
"FLE Standard Time", "FLE Daylight Time",
|
|
"Europe/Helsinki"
|
|
}, /* (GMT+02:00) Helsinki, Kyiv, Riga, Sofia,
|
|
* Tallinn, Vilnius */
|
|
{
|
|
"Georgian Standard Time", "Georgian Daylight Time",
|
|
"Asia/Tbilisi"
|
|
}, /* (GMT+03:00) Tbilisi */
|
|
{
|
|
"GMT Standard Time", "GMT Daylight Time",
|
|
"Europe/London"
|
|
}, /* (GMT) Greenwich Mean Time : Dublin,
|
|
* Edinburgh, Lisbon, London */
|
|
{
|
|
"Greenland Standard Time", "Greenland Daylight Time",
|
|
"America/Godthab"
|
|
}, /* (GMT-03:00) Greenland */
|
|
{
|
|
"Greenwich Standard Time", "Greenwich Daylight Time",
|
|
"Africa/Casablanca"
|
|
}, /* (GMT) Casablanca, Monrovia */
|
|
{
|
|
"GTB Standard Time", "GTB Daylight Time",
|
|
"Europe/Athens"
|
|
}, /* (GMT+02:00) Athens, Istanbul, Minsk */
|
|
{
|
|
"Hawaiian Standard Time", "Hawaiian Daylight Time",
|
|
"US/Hawaii"
|
|
}, /* (GMT-10:00) Hawaii */
|
|
{
|
|
"India Standard Time", "India Daylight Time",
|
|
"Asia/Calcutta"
|
|
}, /* (GMT+05:30) Chennai, Kolkata, Mumbai, New
|
|
* Delhi */
|
|
{
|
|
"Iran Standard Time", "Iran Daylight Time",
|
|
"Asia/Tehran"
|
|
}, /* (GMT+03:30) Tehran */
|
|
{
|
|
"Jerusalem Standard Time", "Jerusalem Daylight Time",
|
|
"Asia/Jerusalem"
|
|
}, /* (GMT+02:00) Jerusalem */
|
|
{
|
|
"Jordan Standard Time", "Jordan Daylight Time",
|
|
"Asia/Amman"
|
|
}, /* (GMT+02:00) Amman */
|
|
{
|
|
"Korea Standard Time", "Korea Daylight Time",
|
|
"Asia/Seoul"
|
|
}, /* (GMT+09:00) Seoul */
|
|
{
|
|
"Mexico Standard Time", "Mexico Daylight Time",
|
|
"America/Mexico_City"
|
|
}, /* (GMT-06:00) Guadalajara, Mexico City,
|
|
* Monterrey */
|
|
{
|
|
"Mexico Standard Time 2", "Mexico Daylight Time 2",
|
|
"America/Chihuahua"
|
|
}, /* (GMT-07:00) Chihuahua, La Paz, Mazatlan */
|
|
{
|
|
"Mid-Atlantic Standard Time", "Mid-Atlantic Daylight Time",
|
|
"Atlantic/South_Georgia"
|
|
}, /* (GMT-02:00) Mid-Atlantic */
|
|
{
|
|
"Middle East Standard Time", "Middle East Daylight Time",
|
|
"Asia/Beirut"
|
|
}, /* (GMT+02:00) Beirut */
|
|
{
|
|
"Montevideo Standard Time", "Montevideo Daylight Time",
|
|
"America/Montevideo"
|
|
}, /* (GMT-03:00) Montevideo */
|
|
{
|
|
"Mountain Standard Time", "Mountain Daylight Time",
|
|
"US/Mountain"
|
|
}, /* (GMT-07:00) Mountain Time (US & Canada) */
|
|
{
|
|
"Mountain Standard Time (Mexico)", "Mountain Daylight Time (Mexico)",
|
|
"America/Chihuahua"
|
|
}, /* (GMT-07:00) Chihuahua, La Paz,
|
|
Mazatlan - New */
|
|
{
|
|
"Myanmar Standard Time", "Myanmar Daylight Time",
|
|
"Asia/Rangoon"
|
|
}, /* (GMT+06:30) Rangoon */
|
|
{
|
|
"N. Central Asia Standard Time", "N. Central Asia Daylight Time",
|
|
"Asia/Almaty"
|
|
}, /* (GMT+06:00) Almaty, Novosibirsk */
|
|
{
|
|
"Namibia Standard Time", "Namibia Daylight Time",
|
|
"Africa/Windhoek"
|
|
}, /* (GMT+02:00) Windhoek */
|
|
{
|
|
"Nepal Standard Time", "Nepal Daylight Time",
|
|
"Asia/Katmandu"
|
|
}, /* (GMT+05:45) Kathmandu */
|
|
{
|
|
"New Zealand Standard Time", "New Zealand Daylight Time",
|
|
"Pacific/Auckland"
|
|
}, /* (GMT+12:00) Auckland, Wellington */
|
|
{
|
|
"Newfoundland Standard Time", "Newfoundland Daylight Time",
|
|
"Canada/Newfoundland"
|
|
}, /* (GMT-03:30) Newfoundland */
|
|
{
|
|
"North Asia East Standard Time", "North Asia East Daylight Time",
|
|
"Asia/Irkutsk"
|
|
}, /* (GMT+08:00) Irkutsk, Ulaan Bataar */
|
|
{
|
|
"North Asia Standard Time", "North Asia Daylight Time",
|
|
"Asia/Krasnoyarsk"
|
|
}, /* (GMT+07:00) Krasnoyarsk */
|
|
{
|
|
"Pacific SA Standard Time", "Pacific SA Daylight Time",
|
|
"America/Santiago"
|
|
}, /* (GMT-04:00) Santiago */
|
|
{
|
|
"Pacific Standard Time", "Pacific Daylight Time",
|
|
"US/Pacific"
|
|
}, /* (GMT-08:00) Pacific Time (US & Canada);
|
|
* Tijuana */
|
|
{
|
|
"Pacific Standard Time (Mexico)", "Pacific Daylight Time (Mexico)",
|
|
"America/Tijuana"
|
|
}, /* (GMT-08:00) Tijuana, Baja California */
|
|
{
|
|
"Romance Standard Time", "Romance Daylight Time",
|
|
"Europe/Brussels"
|
|
}, /* (GMT+01:00) Brussels, Copenhagen, Madrid,
|
|
* Paris */
|
|
{
|
|
"Russian Standard Time", "Russian Daylight Time",
|
|
"Europe/Moscow"
|
|
}, /* (GMT+03:00) Moscow, St. Petersburg,
|
|
* Volgograd */
|
|
{
|
|
"SA Eastern Standard Time", "SA Eastern Daylight Time",
|
|
"America/Buenos_Aires"
|
|
}, /* (GMT-03:00) Buenos Aires, Georgetown */
|
|
{
|
|
"SA Pacific Standard Time", "SA Pacific Daylight Time",
|
|
"America/Bogota"
|
|
}, /* (GMT-05:00) Bogota, Lima, Quito */
|
|
{
|
|
"SA Western Standard Time", "SA Western Daylight Time",
|
|
"America/Caracas"
|
|
}, /* (GMT-04:00) Caracas, La Paz */
|
|
{
|
|
"Samoa Standard Time", "Samoa Daylight Time",
|
|
"Pacific/Midway"
|
|
}, /* (GMT-11:00) Midway Island, Samoa */
|
|
{
|
|
"SE Asia Standard Time", "SE Asia Daylight Time",
|
|
"Asia/Bangkok"
|
|
}, /* (GMT+07:00) Bangkok, Hanoi, Jakarta */
|
|
{
|
|
"Malay Peninsula Standard Time", "Malay Peninsula Daylight Time",
|
|
"Asia/Kuala_Lumpur"
|
|
}, /* (GMT+08:00) Kuala Lumpur, Singapore */
|
|
{
|
|
"South Africa Standard Time", "South Africa Daylight Time",
|
|
"Africa/Harare"
|
|
}, /* (GMT+02:00) Harare, Pretoria */
|
|
{
|
|
"Sri Lanka Standard Time", "Sri Lanka Daylight Time",
|
|
"Asia/Colombo"
|
|
}, /* (GMT+06:00) Sri Jayawardenepura */
|
|
{
|
|
"Taipei Standard Time", "Taipei Daylight Time",
|
|
"Asia/Taipei"
|
|
}, /* (GMT+08:00) Taipei */
|
|
{
|
|
"Tasmania Standard Time", "Tasmania Daylight Time",
|
|
"Australia/Hobart"
|
|
}, /* (GMT+10:00) Hobart */
|
|
{
|
|
"Tokyo Standard Time", "Tokyo Daylight Time",
|
|
"Asia/Tokyo"
|
|
}, /* (GMT+09:00) Osaka, Sapporo, Tokyo */
|
|
{
|
|
"Tonga Standard Time", "Tonga Daylight Time",
|
|
"Pacific/Tongatapu"
|
|
}, /* (GMT+13:00) Nuku'alofa */
|
|
{
|
|
"US Eastern Standard Time", "US Eastern Daylight Time",
|
|
"US/Eastern"
|
|
}, /* (GMT-05:00) Indiana (East) */
|
|
{
|
|
"US Mountain Standard Time", "US Mountain Daylight Time",
|
|
"US/Arizona"
|
|
}, /* (GMT-07:00) Arizona */
|
|
{
|
|
"Vladivostok Standard Time", "Vladivostok Daylight Time",
|
|
"Asia/Vladivostok"
|
|
}, /* (GMT+10:00) Vladivostok */
|
|
{
|
|
"W. Australia Standard Time", "W. Australia Daylight Time",
|
|
"Australia/Perth"
|
|
}, /* (GMT+08:00) Perth */
|
|
/* {"W. Central Africa Standard Time", "W. Central Africa Daylight Time",
|
|
* * * * * ""}, Could not find a match for this one. Excluded for now. *//* (
|
|
* G MT+01:00) West Central Africa */
|
|
{
|
|
"W. Europe Standard Time", "W. Europe Daylight Time",
|
|
"CET"
|
|
}, /* (GMT+01:00) Amsterdam, Berlin, Bern, Rome,
|
|
* Stockholm, Vienna */
|
|
{
|
|
"West Asia Standard Time", "West Asia Daylight Time",
|
|
"Asia/Karachi"
|
|
}, /* (GMT+05:00) Islamabad, Karachi, Tashkent */
|
|
{
|
|
"West Pacific Standard Time", "West Pacific Daylight Time",
|
|
"Pacific/Guam"
|
|
}, /* (GMT+10:00) Guam, Port Moresby */
|
|
{
|
|
"Yakutsk Standard Time", "Yakutsk Daylight Time",
|
|
"Asia/Yakutsk"
|
|
}, /* (GMT+09:00) Yakutsk */
|
|
{
|
|
NULL, NULL, NULL
|
|
}
|
|
};
|
|
|
|
static const char *
|
|
identify_system_timezone(void)
|
|
{
|
|
int i;
|
|
char tzname[128];
|
|
char localtzname[256];
|
|
time_t t = time(NULL);
|
|
struct tm *tm = localtime(&t);
|
|
HKEY rootKey;
|
|
int idx;
|
|
|
|
if (!tm)
|
|
{
|
|
ereport(WARNING,
|
|
(errmsg_internal("could not determine current date/time: localtime failed")));
|
|
return NULL;
|
|
}
|
|
|
|
memset(tzname, 0, sizeof(tzname));
|
|
strftime(tzname, sizeof(tzname) - 1, "%Z", tm);
|
|
|
|
for (i = 0; win32_tzmap[i].stdname != NULL; i++)
|
|
{
|
|
if (strcmp(tzname, win32_tzmap[i].stdname) == 0 ||
|
|
strcmp(tzname, win32_tzmap[i].dstname) == 0)
|
|
{
|
|
elog(DEBUG4, "TZ \"%s\" matches Windows timezone \"%s\"",
|
|
win32_tzmap[i].pgtzname, tzname);
|
|
return win32_tzmap[i].pgtzname;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Localized Windows versions return localized names for the timezone.
|
|
* Scan the registry to find the English name, and then try matching
|
|
* against our table again.
|
|
*/
|
|
memset(localtzname, 0, sizeof(localtzname));
|
|
if (RegOpenKeyEx(HKEY_LOCAL_MACHINE,
|
|
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones",
|
|
0,
|
|
KEY_READ,
|
|
&rootKey) != ERROR_SUCCESS)
|
|
{
|
|
ereport(WARNING,
|
|
(errmsg_internal("could not open registry key to identify Windows timezone: %i", (int) GetLastError())));
|
|
return NULL;
|
|
}
|
|
|
|
for (idx = 0;; idx++)
|
|
{
|
|
char keyname[256];
|
|
char zonename[256];
|
|
DWORD namesize;
|
|
FILETIME lastwrite;
|
|
HKEY key;
|
|
LONG r;
|
|
|
|
memset(keyname, 0, sizeof(keyname));
|
|
namesize = sizeof(keyname);
|
|
if ((r = RegEnumKeyEx(rootKey,
|
|
idx,
|
|
keyname,
|
|
&namesize,
|
|
NULL,
|
|
NULL,
|
|
NULL,
|
|
&lastwrite)) != ERROR_SUCCESS)
|
|
{
|
|
if (r == ERROR_NO_MORE_ITEMS)
|
|
break;
|
|
ereport(WARNING,
|
|
(errmsg_internal("could not enumerate registry subkeys to identify Windows timezone: %i", (int) r)));
|
|
break;
|
|
}
|
|
|
|
if ((r = RegOpenKeyEx(rootKey, keyname, 0, KEY_READ, &key)) != ERROR_SUCCESS)
|
|
{
|
|
ereport(WARNING,
|
|
(errmsg_internal("could not open registry subkey to identify Windows timezone: %i", (int) r)));
|
|
break;
|
|
}
|
|
|
|
memset(zonename, 0, sizeof(zonename));
|
|
namesize = sizeof(zonename);
|
|
if ((r = RegQueryValueEx(key, "Std", NULL, NULL, zonename, &namesize)) != ERROR_SUCCESS)
|
|
{
|
|
ereport(WARNING,
|
|
(errmsg_internal("could not query value for 'std' to identify Windows timezone: %i", (int) r)));
|
|
RegCloseKey(key);
|
|
break;
|
|
}
|
|
if (strcmp(tzname, zonename) == 0)
|
|
{
|
|
/* Matched zone */
|
|
strcpy(localtzname, keyname);
|
|
RegCloseKey(key);
|
|
break;
|
|
}
|
|
memset(zonename, 0, sizeof(zonename));
|
|
namesize = sizeof(zonename);
|
|
if ((r = RegQueryValueEx(key, "Dlt", NULL, NULL, zonename, &namesize)) != ERROR_SUCCESS)
|
|
{
|
|
ereport(WARNING,
|
|
(errmsg_internal("could not query value for 'dlt' to identify Windows timezone: %i", (int) r)));
|
|
RegCloseKey(key);
|
|
break;
|
|
}
|
|
if (strcmp(tzname, zonename) == 0)
|
|
{
|
|
/* Matched DST zone */
|
|
strcpy(localtzname, keyname);
|
|
RegCloseKey(key);
|
|
break;
|
|
}
|
|
|
|
RegCloseKey(key);
|
|
}
|
|
|
|
RegCloseKey(rootKey);
|
|
|
|
if (localtzname[0])
|
|
{
|
|
/* Found a localized name, so scan for that one too */
|
|
for (i = 0; win32_tzmap[i].stdname != NULL; i++)
|
|
{
|
|
if (strcmp(localtzname, win32_tzmap[i].stdname) == 0 ||
|
|
strcmp(localtzname, win32_tzmap[i].dstname) == 0)
|
|
{
|
|
elog(DEBUG4, "TZ \"%s\" matches localized Windows timezone \"%s\" (\"%s\")",
|
|
win32_tzmap[i].pgtzname, tzname, localtzname);
|
|
return win32_tzmap[i].pgtzname;
|
|
}
|
|
}
|
|
}
|
|
|
|
ereport(WARNING,
|
|
(errmsg("could not find a match for Windows timezone \"%s\"",
|
|
tzname)));
|
|
return NULL;
|
|
}
|
|
#endif /* WIN32 */
|
|
|
|
|
|
|
|
/*
|
|
* 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;
|
|
|
|
MemSet(&hash_ctl, 0, sizeof(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);
|
|
if (!timezone_cache)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Load a timezone from file or from cache.
|
|
* Does not verify that the timezone is acceptable!
|
|
*/
|
|
struct 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;
|
|
}
|
|
|
|
if (tzload(uppername, canonname, &tzstate) != 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);
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
|
|
/*
|
|
* Check whether timezone is acceptable.
|
|
*
|
|
* What we are doing here is checking for leap-second-aware timekeeping.
|
|
* We need to reject such TZ settings because they'll wreak havoc with our
|
|
* date/time arithmetic.
|
|
*
|
|
* NB: this must NOT ereport(ERROR). The caller must get control back so that
|
|
* it can restore the old value of TZ if we don't like the new one.
|
|
*/
|
|
bool
|
|
tz_acceptable(pg_tz *tz)
|
|
{
|
|
struct pg_tm *tt;
|
|
pg_time_t time2000;
|
|
|
|
/*
|
|
* To detect leap-second timekeeping, run pg_localtime for what should be
|
|
* GMT midnight, 2000-01-01. Insist that the tm_sec value be zero; any
|
|
* other result has to be due to leap seconds.
|
|
*/
|
|
time2000 = (POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * SECS_PER_DAY;
|
|
tt = pg_localtime(&time2000, tz);
|
|
if (!tt || tt->tm_sec != 0)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/*
|
|
* Set the global timezone. Verify that it's acceptable first.
|
|
*/
|
|
static bool
|
|
set_global_timezone(const char *tzname)
|
|
{
|
|
pg_tz *tznew;
|
|
|
|
if (!tzname || !tzname[0])
|
|
return false;
|
|
|
|
tznew = pg_tzset(tzname);
|
|
if (!tznew)
|
|
return false;
|
|
|
|
if (!tz_acceptable(tznew))
|
|
return false;
|
|
|
|
global_timezone = tznew;
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Identify a suitable default timezone setting based on the environment,
|
|
* and make it active.
|
|
*
|
|
* We first look to the TZ environment variable. If not found or not
|
|
* recognized by our own code, we see if we can identify the timezone
|
|
* from the behavior of the system timezone library. When all else fails,
|
|
* fall back to GMT.
|
|
*/
|
|
static const char *
|
|
select_default_timezone(void)
|
|
{
|
|
const char *def_tz;
|
|
|
|
def_tz = getenv("TZ");
|
|
if (set_global_timezone(def_tz))
|
|
return def_tz;
|
|
|
|
def_tz = identify_system_timezone();
|
|
if (set_global_timezone(def_tz))
|
|
return def_tz;
|
|
|
|
if (set_global_timezone("GMT"))
|
|
return "GMT";
|
|
|
|
ereport(FATAL,
|
|
(errmsg("could not select a suitable default timezone"),
|
|
errdetail("It appears that your GMT time zone uses leap seconds. PostgreSQL does not support leap seconds.")));
|
|
return NULL; /* keep compiler quiet */
|
|
}
|
|
|
|
/*
|
|
* Initialize timezone library
|
|
*
|
|
* This is called after initial loading of postgresql.conf. If no TimeZone
|
|
* setting was found therein, we try to derive one from the environment.
|
|
*/
|
|
void
|
|
pg_timezone_initialize(void)
|
|
{
|
|
/* Do we need to try to figure the timezone? */
|
|
if (pg_strcasecmp(GetConfigOption("timezone"), "UNKNOWN") == 0)
|
|
{
|
|
const char *def_tz;
|
|
|
|
/* 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_ARGV);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* 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];
|
|
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, MAXPGPATH, "%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("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
|
|
*/
|
|
if (tzload(fullname + dir->baselen, dir->tz.TZname, &dir->tz.state) != 0)
|
|
{
|
|
/* Zone could not be loaded, ignore it */
|
|
continue;
|
|
}
|
|
|
|
/* Timezone loaded OK. */
|
|
return &dir->tz;
|
|
}
|
|
|
|
/* Nothing more found */
|
|
return NULL;
|
|
}
|