LCOV - code coverage report
Current view: top level - src/timezone - pgtz.c (source / functions) Coverage Total Hit
Test: PostgreSQL 19devel Lines: 89.0 % 145 129
Test Date: 2026-03-03 13:15:30 Functions: 100.0 % 10 10
Legend: Lines:     hit not hit

            Line data    Source code
       1              : /*-------------------------------------------------------------------------
       2              :  *
       3              :  * pgtz.c
       4              :  *    Timezone Library Integration Functions
       5              :  *
       6              :  * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
       7              :  *
       8              :  * IDENTIFICATION
       9              :  *    src/timezone/pgtz.c
      10              :  *
      11              :  *-------------------------------------------------------------------------
      12              :  */
      13              : #include "postgres.h"
      14              : 
      15              : #include <ctype.h>
      16              : #include <fcntl.h>
      17              : #include <time.h>
      18              : 
      19              : #include "common/file_utils.h"
      20              : #include "datatype/timestamp.h"
      21              : #include "miscadmin.h"
      22              : #include "pgtz.h"
      23              : #include "storage/fd.h"
      24              : #include "utils/hsearch.h"
      25              : 
      26              : 
      27              : /* Current session timezone (controlled by TimeZone GUC) */
      28              : pg_tz      *session_timezone = NULL;
      29              : 
      30              : /* Current log timezone (controlled by log_timezone GUC) */
      31              : pg_tz      *log_timezone = NULL;
      32              : 
      33              : 
      34              : static bool scan_directory_ci(const char *dirname,
      35              :                               const char *fname, int fnamelen,
      36              :                               char *canonname, int canonnamelen);
      37              : 
      38              : 
      39              : /*
      40              :  * Return full pathname of timezone data directory
      41              :  */
      42              : static const char *
      43        10244 : pg_TZDIR(void)
      44              : {
      45              : #ifndef SYSTEMTZDIR
      46              :     /* normal case: timezone stuff is under our share dir */
      47              :     static bool done_tzdir = false;
      48              :     static char tzdir[MAXPGPATH];
      49              : 
      50        10244 :     if (done_tzdir)
      51         9185 :         return tzdir;
      52              : 
      53         1059 :     get_share_path(my_exec_path, tzdir);
      54         1059 :     strlcpy(tzdir + strlen(tzdir), "/timezone", MAXPGPATH - strlen(tzdir));
      55              : 
      56         1059 :     done_tzdir = true;
      57         1059 :     return tzdir;
      58              : #else
      59              :     /* we're configured to use system's timezone database */
      60              :     return SYSTEMTZDIR;
      61              : #endif
      62              : }
      63              : 
      64              : 
      65              : /*
      66              :  * Given a timezone name, open() the timezone data file.  Return the
      67              :  * file descriptor if successful, -1 if not.
      68              :  *
      69              :  * The input name is searched for case-insensitively (we assume that the
      70              :  * timezone database does not contain case-equivalent names).
      71              :  *
      72              :  * If "canonname" is not NULL, then on success the canonical spelling of the
      73              :  * given name is stored there (the buffer must be > TZ_STRLEN_MAX bytes!).
      74              :  */
      75              : int
      76        10236 : pg_open_tzfile(const char *name, char *canonname)
      77              : {
      78              :     const char *fname;
      79              :     char        fullname[MAXPGPATH];
      80              :     int         fullnamelen;
      81              :     int         orignamelen;
      82              : 
      83              :     /* Initialize fullname with base name of tzdata directory */
      84        10236 :     strlcpy(fullname, pg_TZDIR(), sizeof(fullname));
      85        10236 :     orignamelen = fullnamelen = strlen(fullname);
      86              : 
      87        10236 :     if (fullnamelen + 1 + strlen(name) >= MAXPGPATH)
      88            0 :         return -1;              /* not gonna fit */
      89              : 
      90              :     /*
      91              :      * If the caller doesn't need the canonical spelling, first just try to
      92              :      * open the name as-is.  This can be expected to succeed if the given name
      93              :      * is already case-correct, or if the filesystem is case-insensitive; and
      94              :      * we don't need to distinguish those situations if we aren't tasked with
      95              :      * reporting the canonical spelling.
      96              :      */
      97        10236 :     if (canonname == NULL)
      98              :     {
      99              :         int         result;
     100              : 
     101         4978 :         fullname[fullnamelen] = '/';
     102              :         /* test above ensured this will fit: */
     103         4978 :         strcpy(fullname + fullnamelen + 1, name);
     104         4978 :         result = open(fullname, O_RDONLY | PG_BINARY, 0);
     105         4978 :         if (result >= 0)
     106         4978 :             return result;
     107              :         /* If that didn't work, fall through to do it the hard way */
     108            0 :         fullname[fullnamelen] = '\0';
     109              :     }
     110              : 
     111              :     /*
     112              :      * Loop to split the given name into directory levels; for each level,
     113              :      * search using scan_directory_ci().
     114              :      */
     115         5258 :     fname = name;
     116              :     for (;;)
     117         5021 :     {
     118              :         const char *slashptr;
     119              :         int         fnamelen;
     120              : 
     121        10279 :         slashptr = strchr(fname, '/');
     122        10279 :         if (slashptr)
     123         5036 :             fnamelen = slashptr - fname;
     124              :         else
     125         5243 :             fnamelen = strlen(fname);
     126        10279 :         if (!scan_directory_ci(fullname, fname, fnamelen,
     127        10279 :                                fullname + fullnamelen + 1,
     128              :                                MAXPGPATH - fullnamelen - 1))
     129          230 :             return -1;
     130        10049 :         fullname[fullnamelen++] = '/';
     131        10049 :         fullnamelen += strlen(fullname + fullnamelen);
     132        10049 :         if (slashptr)
     133         5021 :             fname = slashptr + 1;
     134              :         else
     135         5028 :             break;
     136              :     }
     137              : 
     138         5028 :     if (canonname)
     139         5028 :         strlcpy(canonname, fullname + orignamelen + 1, TZ_STRLEN_MAX + 1);
     140              : 
     141         5028 :     return open(fullname, O_RDONLY | PG_BINARY, 0);
     142              : }
     143              : 
     144              : 
     145              : /*
     146              :  * Scan specified directory for a case-insensitive match to fname
     147              :  * (of length fnamelen --- fname may not be null terminated!).  If found,
     148              :  * copy the actual filename into canonname and return true.
     149              :  */
     150              : static bool
     151        10279 : scan_directory_ci(const char *dirname, const char *fname, int fnamelen,
     152              :                   char *canonname, int canonnamelen)
     153              : {
     154        10279 :     bool        found = false;
     155              :     DIR        *dirdesc;
     156              :     struct dirent *direntry;
     157              : 
     158        10279 :     dirdesc = AllocateDir(dirname);
     159              : 
     160       710614 :     while ((direntry = ReadDirExtended(dirdesc, dirname, LOG)) != NULL)
     161              :     {
     162              :         /*
     163              :          * Ignore . and .., plus any other "hidden" files.  This is a security
     164              :          * measure to prevent access to files outside the timezone directory.
     165              :          */
     166       710384 :         if (direntry->d_name[0] == '.')
     167        18136 :             continue;
     168              : 
     169       786615 :         if (strlen(direntry->d_name) == fnamelen &&
     170        94367 :             pg_strncasecmp(direntry->d_name, fname, fnamelen) == 0)
     171              :         {
     172              :             /* Found our match */
     173        10049 :             strlcpy(canonname, direntry->d_name, canonnamelen);
     174        10049 :             found = true;
     175        10049 :             break;
     176              :         }
     177              :     }
     178              : 
     179        10279 :     FreeDir(dirdesc);
     180              : 
     181        10279 :     return found;
     182              : }
     183              : 
     184              : 
     185              : /*
     186              :  * We keep loaded timezones in a hashtable so we don't have to
     187              :  * load and parse the TZ definition file every time one is selected.
     188              :  * Because we want timezone names to be found case-insensitively,
     189              :  * the hash key is the uppercased name of the zone.
     190              :  */
     191              : typedef struct
     192              : {
     193              :     /* tznameupper contains the all-upper-case name of the timezone */
     194              :     char        tznameupper[TZ_STRLEN_MAX + 1];
     195              :     pg_tz       tz;
     196              : } pg_tz_cache;
     197              : 
     198              : static HTAB *timezone_cache = NULL;
     199              : 
     200              : 
     201              : static bool
     202         1188 : init_timezone_hashtable(void)
     203              : {
     204              :     HASHCTL     hash_ctl;
     205              : 
     206         1188 :     hash_ctl.keysize = TZ_STRLEN_MAX + 1;
     207         1188 :     hash_ctl.entrysize = sizeof(pg_tz_cache);
     208              : 
     209         1188 :     timezone_cache = hash_create("Timezones",
     210              :                                  4,
     211              :                                  &hash_ctl,
     212              :                                  HASH_ELEM | HASH_STRINGS);
     213         1188 :     if (!timezone_cache)
     214            0 :         return false;
     215              : 
     216         1188 :     return true;
     217              : }
     218              : 
     219              : /*
     220              :  * Load a timezone from file or from cache.
     221              :  * Does not verify that the timezone is acceptable!
     222              :  *
     223              :  * "GMT" is always interpreted as the tzparse() definition, without attempting
     224              :  * to load a definition from the filesystem.  This has a number of benefits:
     225              :  * 1. It's guaranteed to succeed, so we don't have the failure mode wherein
     226              :  * the bootstrap default timezone setting doesn't work (as could happen if
     227              :  * the OS attempts to supply a leap-second-aware version of "GMT").
     228              :  * 2. Because we aren't accessing the filesystem, we can safely initialize
     229              :  * the "GMT" zone definition before my_exec_path is known.
     230              :  * 3. It's quick enough that we don't waste much time when the bootstrap
     231              :  * default timezone setting is later overridden from postgresql.conf.
     232              :  */
     233              : pg_tz *
     234        17755 : pg_tzset(const char *tzname)
     235              : {
     236              :     pg_tz_cache *tzp;
     237              :     struct state tzstate;
     238              :     char        uppername[TZ_STRLEN_MAX + 1];
     239              :     char        canonname[TZ_STRLEN_MAX + 1];
     240              :     char       *p;
     241              : 
     242        17755 :     if (strlen(tzname) > TZ_STRLEN_MAX)
     243            0 :         return NULL;            /* not going to fit */
     244              : 
     245        17755 :     if (!timezone_cache)
     246         1188 :         if (!init_timezone_hashtable())
     247            0 :             return NULL;
     248              : 
     249              :     /*
     250              :      * Upcase the given name to perform a case-insensitive hashtable search.
     251              :      * (We could alternatively downcase it, but we prefer upcase so that we
     252              :      * can get consistently upcased results from tzparse() in case the name is
     253              :      * a POSIX-style timezone spec.)
     254              :      */
     255        17755 :     p = uppername;
     256       164591 :     while (*tzname)
     257       146836 :         *p++ = pg_toupper((unsigned char) *tzname++);
     258        17755 :     *p = '\0';
     259              : 
     260        17755 :     tzp = (pg_tz_cache *) hash_search(timezone_cache,
     261              :                                       uppername,
     262              :                                       HASH_FIND,
     263              :                                       NULL);
     264        17755 :     if (tzp)
     265              :     {
     266              :         /* Timezone found in cache, nothing more to do */
     267        11309 :         return &tzp->tz;
     268              :     }
     269              : 
     270              :     /*
     271              :      * "GMT" is always sent to tzparse(), as per discussion above.
     272              :      */
     273         6446 :     if (strcmp(uppername, "GMT") == 0)
     274              :     {
     275         1188 :         if (!tzparse(uppername, &tzstate, true))
     276              :         {
     277              :             /* This really, really should not happen ... */
     278            0 :             elog(ERROR, "could not initialize GMT time zone");
     279              :         }
     280              :         /* Use uppercase name as canonical */
     281         1188 :         strcpy(canonname, uppername);
     282              :     }
     283         5258 :     else if (tzload(uppername, canonname, &tzstate, true) != 0)
     284              :     {
     285          230 :         if (uppername[0] == ':' || !tzparse(uppername, &tzstate, false))
     286              :         {
     287              :             /* Unknown timezone. Fail our call instead of loading GMT! */
     288           51 :             return NULL;
     289              :         }
     290              :         /* For POSIX timezone specs, use uppercase name as canonical */
     291          179 :         strcpy(canonname, uppername);
     292              :     }
     293              : 
     294              :     /* Save timezone in the cache */
     295         6395 :     tzp = (pg_tz_cache *) hash_search(timezone_cache,
     296              :                                       uppername,
     297              :                                       HASH_ENTER,
     298              :                                       NULL);
     299              : 
     300              :     /* hash_search already copied uppername into the hash key */
     301         6395 :     strcpy(tzp->tz.TZname, canonname);
     302         6395 :     memcpy(&tzp->tz.state, &tzstate, sizeof(tzstate));
     303              : 
     304         6395 :     return &tzp->tz;
     305              : }
     306              : 
     307              : /*
     308              :  * Load a fixed-GMT-offset timezone.
     309              :  * This is used for SQL-spec SET TIME ZONE INTERVAL 'foo' cases.
     310              :  * It's otherwise equivalent to pg_tzset().
     311              :  *
     312              :  * The GMT offset is specified in seconds, positive values meaning west of
     313              :  * Greenwich (ie, POSIX not ISO sign convention).  However, we use ISO
     314              :  * sign convention in the displayable abbreviation for the zone.
     315              :  *
     316              :  * Caution: this can fail (return NULL) if the specified offset is outside
     317              :  * the range allowed by the zic library.
     318              :  */
     319              : pg_tz *
     320           51 : pg_tzset_offset(long gmtoffset)
     321              : {
     322           51 :     long        absoffset = (gmtoffset < 0) ? -gmtoffset : gmtoffset;
     323              :     char        offsetstr[64];
     324              :     char        tzname[128];
     325              : 
     326           51 :     snprintf(offsetstr, sizeof(offsetstr),
     327              :              "%02ld", absoffset / SECS_PER_HOUR);
     328           51 :     absoffset %= SECS_PER_HOUR;
     329           51 :     if (absoffset != 0)
     330              :     {
     331            9 :         snprintf(offsetstr + strlen(offsetstr),
     332            9 :                  sizeof(offsetstr) - strlen(offsetstr),
     333              :                  ":%02ld", absoffset / SECS_PER_MINUTE);
     334            9 :         absoffset %= SECS_PER_MINUTE;
     335            9 :         if (absoffset != 0)
     336            0 :             snprintf(offsetstr + strlen(offsetstr),
     337            0 :                      sizeof(offsetstr) - strlen(offsetstr),
     338              :                      ":%02ld", absoffset);
     339              :     }
     340           51 :     if (gmtoffset > 0)
     341           12 :         snprintf(tzname, sizeof(tzname), "<-%s>+%s",
     342              :                  offsetstr, offsetstr);
     343              :     else
     344           39 :         snprintf(tzname, sizeof(tzname), "<+%s>-%s",
     345              :                  offsetstr, offsetstr);
     346              : 
     347           51 :     return pg_tzset(tzname);
     348              : }
     349              : 
     350              : 
     351              : /*
     352              :  * Initialize timezone library
     353              :  *
     354              :  * This is called before GUC variable initialization begins.  Its purpose
     355              :  * is to ensure that log_timezone has a valid value before any logging GUC
     356              :  * variables could become set to values that require elog.c to provide
     357              :  * timestamps (e.g., log_line_prefix).  We may as well initialize
     358              :  * session_timezone to something valid, too.
     359              :  */
     360              : void
     361         1188 : pg_timezone_initialize(void)
     362              : {
     363              :     /*
     364              :      * We may not yet know where PGSHAREDIR is (in particular this is true in
     365              :      * an EXEC_BACKEND subprocess).  So use "GMT", which pg_tzset forces to be
     366              :      * interpreted without reference to the filesystem.  This corresponds to
     367              :      * the bootstrap default for these variables in guc_parameters.dat,
     368              :      * although in principle it could be different.
     369              :      */
     370         1188 :     session_timezone = pg_tzset("GMT");
     371         1188 :     log_timezone = session_timezone;
     372         1188 : }
     373              : 
     374              : 
     375              : /*
     376              :  * Functions to enumerate available timezones
     377              :  *
     378              :  * Note that pg_tzenumerate_next() will return a pointer into the pg_tzenum
     379              :  * structure, so the data is only valid up to the next call.
     380              :  *
     381              :  * All data is allocated using palloc in the current context.
     382              :  */
     383              : #define MAX_TZDIR_DEPTH 10
     384              : 
     385              : struct pg_tzenum
     386              : {
     387              :     int         baselen;
     388              :     int         depth;
     389              :     DIR        *dirdesc[MAX_TZDIR_DEPTH];
     390              :     char       *dirname[MAX_TZDIR_DEPTH];
     391              :     struct pg_tz tz;
     392              : };
     393              : 
     394              : /* typedef pg_tzenum is declared in pgtime.h */
     395              : 
     396              : pg_tzenum *
     397            8 : pg_tzenumerate_start(void)
     398              : {
     399            8 :     pg_tzenum  *ret = palloc0_object(pg_tzenum);
     400            8 :     char       *startdir = pstrdup(pg_TZDIR());
     401              : 
     402            8 :     ret->baselen = strlen(startdir) + 1;
     403            8 :     ret->depth = 0;
     404            8 :     ret->dirname[0] = startdir;
     405            8 :     ret->dirdesc[0] = AllocateDir(startdir);
     406            8 :     if (!ret->dirdesc[0])
     407            0 :         ereport(ERROR,
     408              :                 (errcode_for_file_access(),
     409              :                  errmsg("could not open directory \"%s\": %m", startdir)));
     410            8 :     return ret;
     411              : }
     412              : 
     413              : void
     414            8 : pg_tzenumerate_end(pg_tzenum *dir)
     415              : {
     416            8 :     while (dir->depth >= 0)
     417              :     {
     418            0 :         FreeDir(dir->dirdesc[dir->depth]);
     419            0 :         pfree(dir->dirname[dir->depth]);
     420            0 :         dir->depth--;
     421              :     }
     422            8 :     pfree(dir);
     423            8 : }
     424              : 
     425              : pg_tz *
     426         4792 : pg_tzenumerate_next(pg_tzenum *dir)
     427              : {
     428         5456 :     while (dir->depth >= 0)
     429              :     {
     430              :         struct dirent *direntry;
     431              :         char        fullname[MAXPGPATH * 2];
     432              : 
     433         5448 :         direntry = ReadDir(dir->dirdesc[dir->depth], dir->dirname[dir->depth]);
     434              : 
     435         5448 :         if (!direntry)
     436              :         {
     437              :             /* End of this directory */
     438          168 :             FreeDir(dir->dirdesc[dir->depth]);
     439          168 :             pfree(dir->dirname[dir->depth]);
     440          168 :             dir->depth--;
     441          664 :             continue;
     442              :         }
     443              : 
     444         5280 :         if (direntry->d_name[0] == '.')
     445          336 :             continue;
     446              : 
     447         4944 :         snprintf(fullname, sizeof(fullname), "%s/%s",
     448         4944 :                  dir->dirname[dir->depth], direntry->d_name);
     449              : 
     450         4944 :         if (get_dirent_type(fullname, direntry, true, ERROR) == PGFILETYPE_DIR)
     451              :         {
     452              :             /* Step into the subdirectory */
     453          160 :             if (dir->depth >= MAX_TZDIR_DEPTH - 1)
     454            0 :                 ereport(ERROR,
     455              :                         (errmsg_internal("timezone directory stack overflow")));
     456          160 :             dir->depth++;
     457          160 :             dir->dirname[dir->depth] = pstrdup(fullname);
     458          160 :             dir->dirdesc[dir->depth] = AllocateDir(fullname);
     459          160 :             if (!dir->dirdesc[dir->depth])
     460            0 :                 ereport(ERROR,
     461              :                         (errcode_for_file_access(),
     462              :                          errmsg("could not open directory \"%s\": %m",
     463              :                                 fullname)));
     464              : 
     465              :             /* Start over reading in the new directory */
     466          160 :             continue;
     467              :         }
     468              : 
     469              :         /*
     470              :          * Load this timezone using tzload() not pg_tzset(), so we don't fill
     471              :          * the cache.  Also, don't ask for the canonical spelling: we already
     472              :          * know it, and pg_open_tzfile's way of finding it out is pretty
     473              :          * inefficient.
     474              :          */
     475         4784 :         if (tzload(fullname + dir->baselen, NULL, &dir->tz.state, true) != 0)
     476              :         {
     477              :             /* Zone could not be loaded, ignore it */
     478            0 :             continue;
     479              :         }
     480              : 
     481         4784 :         if (!pg_tz_acceptable(&dir->tz))
     482              :         {
     483              :             /* Ignore leap-second zones */
     484            0 :             continue;
     485              :         }
     486              : 
     487              :         /* OK, return the canonical zone name spelling. */
     488         4784 :         strlcpy(dir->tz.TZname, fullname + dir->baselen,
     489              :                 sizeof(dir->tz.TZname));
     490              : 
     491              :         /* Timezone loaded OK. */
     492         4784 :         return &dir->tz;
     493              :     }
     494              : 
     495              :     /* Nothing more found */
     496            8 :     return NULL;
     497              : }
        

Generated by: LCOV version 2.0-1