Coverage Report

Created: 2026-09-28 06:55

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/postgres/src/timezone/pgtz.c
Line
Count
Source
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
pg_TZDIR(void)
44
0
{
45
0
#ifndef SYSTEMTZDIR
46
  /* normal case: timezone stuff is under our share dir */
47
0
  static bool done_tzdir = false;
48
0
  static char tzdir[MAXPGPATH];
49
50
0
  if (done_tzdir)
51
0
    return tzdir;
52
53
0
  get_share_path(my_exec_path, tzdir);
54
0
  strlcpy(tzdir + strlen(tzdir), "/timezone", MAXPGPATH - strlen(tzdir));
55
56
0
  done_tzdir = true;
57
0
  return tzdir;
58
#else
59
  /* we're configured to use system's timezone database */
60
  return SYSTEMTZDIR;
61
#endif
62
0
}
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
pg_open_tzfile(const char *name, char *canonname)
77
0
{
78
0
  const char *fname;
79
0
  char    fullname[MAXPGPATH];
80
0
  int     fullnamelen;
81
0
  int     orignamelen;
82
83
  /* Initialize fullname with base name of tzdata directory */
84
0
  strlcpy(fullname, pg_TZDIR(), sizeof(fullname));
85
0
  orignamelen = fullnamelen = strlen(fullname);
86
87
0
  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
0
  if (canonname == NULL)
98
0
  {
99
0
    int     result;
100
101
0
    fullname[fullnamelen] = '/';
102
    /* test above ensured this will fit: */
103
0
    strcpy(fullname + fullnamelen + 1, name);
104
0
    result = open(fullname, O_RDONLY | PG_BINARY, 0);
105
0
    if (result >= 0)
106
0
      return result;
107
    /* If that didn't work, fall through to do it the hard way */
108
0
    fullname[fullnamelen] = '\0';
109
0
  }
110
111
  /*
112
   * Loop to split the given name into directory levels; for each level,
113
   * search using scan_directory_ci().
114
   */
115
0
  fname = name;
116
0
  for (;;)
117
0
  {
118
0
    const char *slashptr;
119
0
    int     fnamelen;
120
121
0
    slashptr = strchr(fname, '/');
122
0
    if (slashptr)
123
0
      fnamelen = slashptr - fname;
124
0
    else
125
0
      fnamelen = strlen(fname);
126
0
    if (!scan_directory_ci(fullname, fname, fnamelen,
127
0
                 fullname + fullnamelen + 1,
128
0
                 MAXPGPATH - fullnamelen - 1))
129
0
      return -1;
130
0
    fullname[fullnamelen++] = '/';
131
0
    fullnamelen += strlen(fullname + fullnamelen);
132
0
    if (slashptr)
133
0
      fname = slashptr + 1;
134
0
    else
135
0
      break;
136
0
  }
137
138
0
  if (canonname)
139
0
    strlcpy(canonname, fullname + orignamelen + 1, TZ_STRLEN_MAX + 1);
140
141
0
  return open(fullname, O_RDONLY | PG_BINARY, 0);
142
0
}
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
scan_directory_ci(const char *dirname, const char *fname, int fnamelen,
152
          char *canonname, int canonnamelen)
153
0
{
154
0
  bool    found = false;
155
0
  DIR      *dirdesc;
156
0
  struct dirent *direntry;
157
158
0
  dirdesc = AllocateDir(dirname);
159
160
0
  while ((direntry = ReadDirExtended(dirdesc, dirname, LOG)) != NULL)
161
0
  {
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
0
    if (direntry->d_name[0] == '.')
167
0
      continue;
168
169
0
    if (strlen(direntry->d_name) == fnamelen &&
170
0
      pg_strncasecmp(direntry->d_name, fname, fnamelen) == 0)
171
0
    {
172
      /* Found our match */
173
0
      strlcpy(canonname, direntry->d_name, canonnamelen);
174
0
      found = true;
175
0
      break;
176
0
    }
177
0
  }
178
179
0
  FreeDir(dirdesc);
180
181
0
  return found;
182
0
}
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
init_timezone_hashtable(void)
203
2
{
204
2
  HASHCTL   hash_ctl;
205
206
2
  hash_ctl.keysize = TZ_STRLEN_MAX + 1;
207
2
  hash_ctl.entrysize = sizeof(pg_tz_cache);
208
209
2
  timezone_cache = hash_create("Timezones",
210
2
                 4,
211
2
                 &hash_ctl,
212
2
                 HASH_ELEM | HASH_STRINGS);
213
2
  if (!timezone_cache)
214
0
    return false;
215
216
2
  return true;
217
2
}
218
219
/*
220
 * Load a timezone from file or from cache.
221
 * Does not verify that the timezone is acceptable!
222
 */
223
pg_tz *
224
pg_tzset(const char *tzname)
225
6
{
226
6
  pg_tz_cache *tzp;
227
6
  struct state tzstate;
228
6
  char    uppername[TZ_STRLEN_MAX + 1];
229
6
  char    canonname[TZ_STRLEN_MAX + 1];
230
6
  char     *p;
231
232
6
  if (strlen(tzname) > TZ_STRLEN_MAX)
233
0
    return NULL;     /* not going to fit */
234
235
6
  if (!timezone_cache)
236
2
    if (!init_timezone_hashtable())
237
0
      return NULL;
238
239
  /*
240
   * Upcase the given name to perform a case-insensitive hashtable search.
241
   * (We could alternatively downcase it, but we prefer upcase so that we
242
   * can get consistently upcased results from pg_tzload() in case the name
243
   * is a POSIX-style timezone spec.)
244
   */
245
6
  p = uppername;
246
24
  while (*tzname)
247
18
    *p++ = pg_toupper((unsigned char) *tzname++);
248
6
  *p = '\0';
249
250
6
  tzp = (pg_tz_cache *) hash_search(timezone_cache,
251
6
                    uppername,
252
6
                    HASH_FIND,
253
6
                    NULL);
254
6
  if (tzp)
255
4
  {
256
    /* Timezone found in cache, nothing more to do */
257
4
    return &tzp->tz;
258
4
  }
259
260
  /*
261
   * Let the IANA tzdb code interpret the time zone name.
262
   */
263
2
  if (!pg_tzload(uppername, canonname, &tzstate))
264
0
  {
265
0
    if (strcmp(uppername, "GMT") == 0)
266
0
    {
267
      /* This really, really should not happen ... */
268
0
      elog(ERROR, "could not initialize GMT time zone");
269
0
    }
270
    /* Unknown timezone. Fail our call instead of loading GMT! */
271
0
    return NULL;
272
0
  }
273
274
  /* Save timezone in the cache */
275
2
  tzp = (pg_tz_cache *) hash_search(timezone_cache,
276
2
                    uppername,
277
2
                    HASH_ENTER,
278
2
                    NULL);
279
280
  /* hash_search already copied uppername into the hash key */
281
2
  strcpy(tzp->tz.TZname, canonname);
282
2
  memcpy(&tzp->tz.state, &tzstate, sizeof(tzstate));
283
284
2
  return &tzp->tz;
285
2
}
286
287
/*
288
 * Load a fixed-GMT-offset timezone.
289
 * This is used for SQL-spec SET TIME ZONE INTERVAL 'foo' cases.
290
 * It's otherwise equivalent to pg_tzset().
291
 *
292
 * The GMT offset is specified in seconds, positive values meaning west of
293
 * Greenwich (ie, POSIX not ISO sign convention).  However, we use ISO
294
 * sign convention in the displayable abbreviation for the zone.
295
 *
296
 * Caution: this can fail (return NULL) if the specified offset is outside
297
 * the range allowed by the zic library.
298
 */
299
pg_tz *
300
pg_tzset_offset(long gmtoffset)
301
0
{
302
0
  long    absoffset = (gmtoffset < 0) ? -gmtoffset : gmtoffset;
303
0
  char    offsetstr[64];
304
0
  char    tzname[128];
305
306
0
  snprintf(offsetstr, sizeof(offsetstr),
307
0
       "%02ld", absoffset / SECS_PER_HOUR);
308
0
  absoffset %= SECS_PER_HOUR;
309
0
  if (absoffset != 0)
310
0
  {
311
0
    snprintf(offsetstr + strlen(offsetstr),
312
0
         sizeof(offsetstr) - strlen(offsetstr),
313
0
         ":%02ld", absoffset / SECS_PER_MINUTE);
314
0
    absoffset %= SECS_PER_MINUTE;
315
0
    if (absoffset != 0)
316
0
      snprintf(offsetstr + strlen(offsetstr),
317
0
           sizeof(offsetstr) - strlen(offsetstr),
318
0
           ":%02ld", absoffset);
319
0
  }
320
0
  if (gmtoffset > 0)
321
0
    snprintf(tzname, sizeof(tzname), "<-%s>+%s",
322
0
         offsetstr, offsetstr);
323
0
  else
324
0
    snprintf(tzname, sizeof(tzname), "<+%s>-%s",
325
0
         offsetstr, offsetstr);
326
327
0
  return pg_tzset(tzname);
328
0
}
329
330
331
/*
332
 * Initialize timezone library
333
 *
334
 * This is called before GUC variable initialization begins.  Its purpose
335
 * is to ensure that log_timezone has a valid value before any logging GUC
336
 * variables could become set to values that require elog.c to provide
337
 * timestamps (e.g., log_line_prefix).  We may as well initialize
338
 * session_timezone to something valid, too.
339
 */
340
void
341
pg_timezone_initialize(void)
342
2
{
343
  /*
344
   * We may not yet know where PGSHAREDIR is (in particular this is true in
345
   * an EXEC_BACKEND subprocess).  So use "GMT", which pg_tzset forces to be
346
   * interpreted without reference to the filesystem.  This corresponds to
347
   * the bootstrap default for these variables in guc_parameters.dat,
348
   * although in principle it could be different.
349
   */
350
2
  session_timezone = pg_tzset("GMT");
351
2
  log_timezone = session_timezone;
352
2
}
353
354
355
/*
356
 * Functions to enumerate available timezones
357
 *
358
 * Note that pg_tzenumerate_next() will return a pointer into the pg_tzenum
359
 * structure, so the data is only valid up to the next call.
360
 *
361
 * All data is allocated using palloc in the current context.
362
 */
363
0
#define MAX_TZDIR_DEPTH 10
364
365
struct pg_tzenum
366
{
367
  int     baselen;
368
  int     depth;
369
  DIR      *dirdesc[MAX_TZDIR_DEPTH];
370
  char     *dirname[MAX_TZDIR_DEPTH];
371
  struct pg_tz tz;
372
};
373
374
/* typedef pg_tzenum is declared in pgtime.h */
375
376
pg_tzenum *
377
pg_tzenumerate_start(void)
378
0
{
379
0
  pg_tzenum  *ret = palloc0_object(pg_tzenum);
380
0
  char     *startdir = pstrdup(pg_TZDIR());
381
382
0
  ret->baselen = strlen(startdir) + 1;
383
0
  ret->depth = 0;
384
0
  ret->dirname[0] = startdir;
385
0
  ret->dirdesc[0] = AllocateDir(startdir);
386
0
  if (!ret->dirdesc[0])
387
0
    ereport(ERROR,
388
0
        (errcode_for_file_access(),
389
0
         errmsg("could not open directory \"%s\": %m", startdir)));
390
0
  return ret;
391
0
}
392
393
void
394
pg_tzenumerate_end(pg_tzenum *dir)
395
0
{
396
0
  while (dir->depth >= 0)
397
0
  {
398
0
    FreeDir(dir->dirdesc[dir->depth]);
399
0
    pfree(dir->dirname[dir->depth]);
400
0
    dir->depth--;
401
0
  }
402
0
  pfree(dir);
403
0
}
404
405
pg_tz *
406
pg_tzenumerate_next(pg_tzenum *dir)
407
0
{
408
0
  while (dir->depth >= 0)
409
0
  {
410
0
    struct dirent *direntry;
411
0
    char    fullname[MAXPGPATH * 2];
412
413
0
    direntry = ReadDir(dir->dirdesc[dir->depth], dir->dirname[dir->depth]);
414
415
0
    if (!direntry)
416
0
    {
417
      /* End of this directory */
418
0
      FreeDir(dir->dirdesc[dir->depth]);
419
0
      pfree(dir->dirname[dir->depth]);
420
0
      dir->depth--;
421
0
      continue;
422
0
    }
423
424
0
    if (direntry->d_name[0] == '.')
425
0
      continue;
426
427
0
    snprintf(fullname, sizeof(fullname), "%s/%s",
428
0
         dir->dirname[dir->depth], direntry->d_name);
429
430
0
    if (get_dirent_type(fullname, direntry, true, ERROR) == PGFILETYPE_DIR)
431
0
    {
432
      /* Step into the subdirectory */
433
0
      if (dir->depth >= MAX_TZDIR_DEPTH - 1)
434
0
        ereport(ERROR,
435
0
            (errmsg_internal("timezone directory stack overflow")));
436
0
      dir->depth++;
437
0
      dir->dirname[dir->depth] = pstrdup(fullname);
438
0
      dir->dirdesc[dir->depth] = AllocateDir(fullname);
439
0
      if (!dir->dirdesc[dir->depth])
440
0
        ereport(ERROR,
441
0
            (errcode_for_file_access(),
442
0
             errmsg("could not open directory \"%s\": %m",
443
0
                fullname)));
444
445
      /* Start over reading in the new directory */
446
0
      continue;
447
0
    }
448
449
    /*
450
     * Load this timezone using pg_tzload() not pg_tzset(), so we don't
451
     * fill the cache.  Also, don't ask for the canonical spelling: we
452
     * already know it, and pg_open_tzfile's way of finding it out is
453
     * pretty inefficient.
454
     */
455
0
    if (!pg_tzload(fullname + dir->baselen, NULL, &dir->tz.state))
456
0
    {
457
      /* Zone could not be loaded, ignore it */
458
0
      continue;
459
0
    }
460
461
0
    if (!pg_tz_acceptable(&dir->tz))
462
0
    {
463
      /* Ignore leap-second zones */
464
0
      continue;
465
0
    }
466
467
    /* OK, return the canonical zone name spelling. */
468
0
    strlcpy(dir->tz.TZname, fullname + dir->baselen,
469
0
        sizeof(dir->tz.TZname));
470
471
    /* Timezone loaded OK. */
472
0
    return &dir->tz;
473
0
  }
474
475
  /* Nothing more found */
476
0
  return NULL;
477
0
}