*
* pgtz.cpp
* Timezone Library Integration Functions
*
* Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
*
* IDENTIFICATION
* src/common/timezone/pgtz.cpp
*
* -------------------------------------------------------------------------
*/
#include "postgres.h"
#include "knl/knl_variable.h"
#include <ctype.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <time.h>
#include "miscadmin.h"
#include "pgtz.h"
#include "storage/smgr/fd.h"
#include "utils/hsearch.h"
#ifdef ENABLE_UT
#define static
#endif
THR_LOCAL pg_tz* session_timezone = NULL;
THR_LOCAL 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
static THR_LOCAL bool done_tzdir = false;
static THR_LOCAL char tzdir[MAXPGPATH] = {0};
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
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 = NULL;
char fullname[MAXPGPATH];
int fullnamelen;
int orignamelen;
* Loop to split the given name into directory levels; for each level,
* search using scan_directory_ci().
*/
strlcpy(fullname, pg_TZDIR(), sizeof(fullname));
orignamelen = fullnamelen = strlen(fullname);
fname = name;
for (;;) {
const char* slashptr = NULL;
int fnamelen;
slashptr = strchr(fname, '/');
if (slashptr != NULL) {
fnamelen = slashptr - fname;
} else {
fnamelen = strlen(fname);
}
if (fullnamelen + 1 + fnamelen >= MAXPGPATH) {
return -1;
}
if (!scan_directory_ci(fullname, fname, fnamelen, fullname + fullnamelen + 1, MAXPGPATH - fullnamelen - 1)) {
return -1;
}
fullname[fullnamelen++] = '/';
fullnamelen += strlen(fullname + fullnamelen);
if (slashptr != NULL) {
fname = slashptr + 1;
} else {
break;
}
}
if (canonname != NULL) {
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 = NULL;
struct dirent* direntry = NULL;
dirdesc = AllocateDir(dirname);
if (dirdesc == NULL) {
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) == (size_t)(unsigned int)fnamelen &&
pg_strncasecmp(direntry->d_name, fname, fnamelen) == 0) {
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 {
char tznameupper[TZ_STRLEN_MAX + 1];
pg_tz tz;
} pg_tz_cache;
static bool init_timezone_hashtable(void)
{
HASHCTL hash_ctl;
errno_t rc = EOK;
rc = memset_s(&hash_ctl, sizeof(hash_ctl), 0, sizeof(hash_ctl));
securec_check(rc, "\0", "\0");
hash_ctl.keysize = TZ_STRLEN_MAX + 1;
hash_ctl.entrysize = sizeof(pg_tz_cache);
hash_ctl.hcxt = u_sess->cache_mem_cxt;
u_sess->time_cxt.timezone_cache = hash_create("Timezones", 4, &hash_ctl, HASH_ELEM | HASH_CONTEXT);
if (u_sess->time_cxt.timezone_cache == NULL) {
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 = NULL;
struct state tzstate;
char uppername[TZ_STRLEN_MAX + 1];
char canonname[TZ_STRLEN_MAX + 1];
char* p = NULL;
errno_t rc = EOK;
if (strlen(name) > TZ_STRLEN_MAX) {
return NULL;
}
if (u_sess->time_cxt.timezone_cache == NULL) {
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(u_sess->time_cxt.timezone_cache, uppername, HASH_FIND, NULL);
if (tzp != NULL) {
return &tzp->tz;
}
* "GMT" is always sent to tzparse(), as per discussion above.
*/
if (strcmp(uppername, "GMT") == 0) {
if (tzparse(uppername, &tzstate, TRUE) != 0) {
elog(ERROR, "could not initialize GMT time zone");
}
rc = strcpy_s(canonname, TZ_STRLEN_MAX + 1, uppername);
securec_check(rc, "\0", "\0");
} else if (tzload(uppername, canonname, &tzstate, TRUE) != 0) {
if (uppername[0] == ':' || tzparse(uppername, &tzstate, FALSE) != 0) {
return NULL;
}
rc = strcpy_s(canonname, TZ_STRLEN_MAX + 1, uppername);
securec_check(rc, "\0", "\0");
}
tzp = (pg_tz_cache*)hash_search(u_sess->time_cxt.timezone_cache, uppername, HASH_ENTER, NULL);
rc = strcpy_s(tzp->tz.TZname, TZ_STRLEN_MAX + 1, canonname);
securec_check(rc, "\0", "\0");
rc = memcpy_s(&tzp->tz.state, sizeof(tzp->tz.state), &tzstate, sizeof(tzstate));
securec_check(rc, "\0", "\0");
return &tzp->tz;
}
* 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_timestamp 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;
};
pg_tzenum* pg_tzenumerate_start(void)
{
pg_tzenum* ret = (pg_tzenum*)palloc0(sizeof(pg_tzenum));
if (ret == NULL) {
ereport(ERROR, (errcode_for_file_access(), errmsg("could not alloc memory ")));
}
char* startdir = pstrdup(pg_TZDIR());
if (startdir != NULL) {
ret->baselen = strlen(startdir) + 1;
ret->depth = 0;
ret->dirname[0] = startdir;
ret->dirdesc[0] = AllocateDir(startdir);
}
if (ret->dirdesc[0] == NULL) {
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_ext(dir->dirname[dir->depth]);
dir->depth--;
}
pfree_ext(dir);
}
pg_tz* pg_tzenumerate_next(pg_tzenum* dir)
{
while (dir->depth >= 0) {
struct dirent* direntry = NULL;
char fullname[MAXPGPATH];
struct stat statbuf;
errno_t rc = EOK;
direntry = ReadDir(dir->dirdesc[dir->depth], dir->dirname[dir->depth]);
if (direntry == NULL) {
FreeDir(dir->dirdesc[dir->depth]);
pfree_ext(dir->dirname[dir->depth]);
dir->depth--;
continue;
}
if (direntry->d_name[0] == '.') {
continue;
}
rc = snprintf_s(fullname, MAXPGPATH, MAXPGPATH - 1, "%s/%s", dir->dirname[dir->depth], direntry->d_name);
securec_check_ss(rc, "\0", "\0");
if (stat(fullname, &statbuf) != 0) {
ereport(ERROR, (errcode_for_file_access(), errmsg("could not stat \"%s\": %m", fullname)));
}
if (S_ISDIR(statbuf.st_mode)) {
if (dir->depth >= MAX_TZDIR_DEPTH - 1) {
ereport(ERROR, (errcode(ERRCODE_IO_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] == NULL) {
ereport(ERROR, (errcode_for_file_access(), errmsg("could not open directory \"%s\": %m", fullname)));
}
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, TRUE) != 0) {
continue;
}
if (!pg_tz_acceptable(&dir->tz)) {
continue;
}
return &dir->tz;
}
return NULL;
}