Coverage Report

Created: 2025-08-12 06:43

/src/postgres/src/backend/utils/misc/tzparser.c
Line
Count
Source (jump to first uncovered line)
1
/*-------------------------------------------------------------------------
2
 *
3
 * tzparser.c
4
 *    Functions for parsing timezone offset files
5
 *
6
 * Note: this code is invoked from the check_hook for the GUC variable
7
 * timezone_abbreviations.  Therefore, it should report problems using
8
 * GUC_check_errmsg() and related functions, and try to avoid throwing
9
 * elog(ERROR).  This is not completely bulletproof at present --- in
10
 * particular out-of-memory will throw an error.  Could probably fix with
11
 * PG_TRY if necessary.
12
 *
13
 *
14
 * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
15
 * Portions Copyright (c) 1994, Regents of the University of California
16
 *
17
 * IDENTIFICATION
18
 *    src/backend/utils/misc/tzparser.c
19
 *
20
 *-------------------------------------------------------------------------
21
 */
22
23
#include "postgres.h"
24
25
#include <ctype.h>
26
27
#include "miscadmin.h"
28
#include "storage/fd.h"
29
#include "utils/datetime.h"
30
#include "utils/guc.h"
31
#include "utils/memutils.h"
32
#include "utils/tzparser.h"
33
34
35
0
#define WHITESPACE " \t\n\r"
36
37
static bool validateTzEntry(tzEntry *tzentry);
38
static bool splitTzLine(const char *filename, int lineno,
39
            char *line, tzEntry *tzentry);
40
static int  addToArray(tzEntry **base, int *arraysize, int n,
41
             tzEntry *entry, bool override);
42
static int  ParseTzFile(const char *filename, int depth,
43
            tzEntry **base, int *arraysize, int n);
44
45
46
/*
47
 * Apply additional validation checks to a tzEntry
48
 *
49
 * Returns true if OK, else false
50
 */
51
static bool
52
validateTzEntry(tzEntry *tzentry)
53
0
{
54
0
  unsigned char *p;
55
56
  /*
57
   * Check restrictions imposed by datetktbl storage format (see datetime.c)
58
   */
59
0
  if (strlen(tzentry->abbrev) > TOKMAXLEN)
60
0
  {
61
0
    GUC_check_errmsg("time zone abbreviation \"%s\" is too long (maximum %d characters) in time zone file \"%s\", line %d",
62
0
             tzentry->abbrev, TOKMAXLEN,
63
0
             tzentry->filename, tzentry->lineno);
64
0
    return false;
65
0
  }
66
67
  /*
68
   * Sanity-check the offset: shouldn't exceed 14 hours
69
   */
70
0
  if (tzentry->offset > 14 * SECS_PER_HOUR ||
71
0
    tzentry->offset < -14 * SECS_PER_HOUR)
72
0
  {
73
0
    GUC_check_errmsg("time zone offset %d is out of range in time zone file \"%s\", line %d",
74
0
             tzentry->offset,
75
0
             tzentry->filename, tzentry->lineno);
76
0
    return false;
77
0
  }
78
79
  /*
80
   * Convert abbrev to lowercase (must match datetime.c's conversion)
81
   */
82
0
  for (p = (unsigned char *) tzentry->abbrev; *p; p++)
83
0
    *p = pg_tolower(*p);
84
85
0
  return true;
86
0
}
87
88
/*
89
 * Attempt to parse the line as a timezone abbrev spec
90
 *
91
 * Valid formats are:
92
 *  name  zone
93
 *  name  offset  dst
94
 *
95
 * Returns true if OK, else false; data is stored in *tzentry
96
 */
97
static bool
98
splitTzLine(const char *filename, int lineno, char *line, tzEntry *tzentry)
99
0
{
100
0
  char     *brkl;
101
0
  char     *abbrev;
102
0
  char     *offset;
103
0
  char     *offset_endptr;
104
0
  char     *remain;
105
0
  char     *is_dst;
106
107
0
  tzentry->lineno = lineno;
108
0
  tzentry->filename = filename;
109
110
0
  abbrev = strtok_r(line, WHITESPACE, &brkl);
111
0
  if (!abbrev)
112
0
  {
113
0
    GUC_check_errmsg("missing time zone abbreviation in time zone file \"%s\", line %d",
114
0
             filename, lineno);
115
0
    return false;
116
0
  }
117
0
  tzentry->abbrev = pstrdup(abbrev);
118
119
0
  offset = strtok_r(NULL, WHITESPACE, &brkl);
120
0
  if (!offset)
121
0
  {
122
0
    GUC_check_errmsg("missing time zone offset in time zone file \"%s\", line %d",
123
0
             filename, lineno);
124
0
    return false;
125
0
  }
126
127
  /* We assume zone names don't begin with a digit or sign */
128
0
  if (isdigit((unsigned char) *offset) || *offset == '+' || *offset == '-')
129
0
  {
130
0
    tzentry->zone = NULL;
131
0
    tzentry->offset = strtol(offset, &offset_endptr, 10);
132
0
    if (offset_endptr == offset || *offset_endptr != '\0')
133
0
    {
134
0
      GUC_check_errmsg("invalid number for time zone offset in time zone file \"%s\", line %d",
135
0
               filename, lineno);
136
0
      return false;
137
0
    }
138
139
0
    is_dst = strtok_r(NULL, WHITESPACE, &brkl);
140
0
    if (is_dst && pg_strcasecmp(is_dst, "D") == 0)
141
0
    {
142
0
      tzentry->is_dst = true;
143
0
      remain = strtok_r(NULL, WHITESPACE, &brkl);
144
0
    }
145
0
    else
146
0
    {
147
      /* there was no 'D' dst specifier */
148
0
      tzentry->is_dst = false;
149
0
      remain = is_dst;
150
0
    }
151
0
  }
152
0
  else
153
0
  {
154
    /*
155
     * Assume entry is a zone name.  We do not try to validate it by
156
     * looking up the zone, because that would force loading of a lot of
157
     * zones that probably will never be used in the current session.
158
     */
159
0
    tzentry->zone = pstrdup(offset);
160
0
    tzentry->offset = 0 * SECS_PER_HOUR;
161
0
    tzentry->is_dst = false;
162
0
    remain = strtok_r(NULL, WHITESPACE, &brkl);
163
0
  }
164
165
0
  if (!remain)       /* no more non-whitespace chars */
166
0
    return true;
167
168
0
  if (remain[0] != '#')   /* must be a comment */
169
0
  {
170
0
    GUC_check_errmsg("invalid syntax in time zone file \"%s\", line %d",
171
0
             filename, lineno);
172
0
    return false;
173
0
  }
174
0
  return true;
175
0
}
176
177
/*
178
 * Insert entry into sorted array
179
 *
180
 * *base: base address of array (changeable if must enlarge array)
181
 * *arraysize: allocated length of array (changeable if must enlarge array)
182
 * n: current number of valid elements in array
183
 * entry: new data to insert
184
 * override: true if OK to override
185
 *
186
 * Returns the new array length (new value for n), or -1 if error
187
 */
188
static int
189
addToArray(tzEntry **base, int *arraysize, int n,
190
       tzEntry *entry, bool override)
191
0
{
192
0
  tzEntry    *arrayptr;
193
0
  int     low;
194
0
  int     high;
195
196
  /*
197
   * Search the array for a duplicate; as a useful side effect, the array is
198
   * maintained in sorted order.  We use strcmp() to ensure we match the
199
   * sort order datetime.c expects.
200
   */
201
0
  arrayptr = *base;
202
0
  low = 0;
203
0
  high = n - 1;
204
0
  while (low <= high)
205
0
  {
206
0
    int     mid = (low + high) >> 1;
207
0
    tzEntry    *midptr = arrayptr + mid;
208
0
    int     cmp;
209
210
0
    cmp = strcmp(entry->abbrev, midptr->abbrev);
211
0
    if (cmp < 0)
212
0
      high = mid - 1;
213
0
    else if (cmp > 0)
214
0
      low = mid + 1;
215
0
    else
216
0
    {
217
      /*
218
       * Found a duplicate entry; complain unless it's the same.
219
       */
220
0
      if ((midptr->zone == NULL && entry->zone == NULL &&
221
0
         midptr->offset == entry->offset &&
222
0
         midptr->is_dst == entry->is_dst) ||
223
0
        (midptr->zone != NULL && entry->zone != NULL &&
224
0
         strcmp(midptr->zone, entry->zone) == 0))
225
0
      {
226
        /* return unchanged array */
227
0
        return n;
228
0
      }
229
0
      if (override)
230
0
      {
231
        /* same abbrev but something is different, override */
232
0
        midptr->zone = entry->zone;
233
0
        midptr->offset = entry->offset;
234
0
        midptr->is_dst = entry->is_dst;
235
0
        return n;
236
0
      }
237
      /* same abbrev but something is different, complain */
238
0
      GUC_check_errmsg("time zone abbreviation \"%s\" is multiply defined",
239
0
               entry->abbrev);
240
0
      GUC_check_errdetail("Entry in time zone file \"%s\", line %d, conflicts with entry in file \"%s\", line %d.",
241
0
                midptr->filename, midptr->lineno,
242
0
                entry->filename, entry->lineno);
243
0
      return -1;
244
0
    }
245
0
  }
246
247
  /*
248
   * No match, insert at position "low".
249
   */
250
0
  if (n >= *arraysize)
251
0
  {
252
0
    *arraysize *= 2;
253
0
    *base = (tzEntry *) repalloc(*base, *arraysize * sizeof(tzEntry));
254
0
  }
255
256
0
  arrayptr = *base + low;
257
258
0
  memmove(arrayptr + 1, arrayptr, (n - low) * sizeof(tzEntry));
259
260
0
  memcpy(arrayptr, entry, sizeof(tzEntry));
261
262
0
  return n + 1;
263
0
}
264
265
/*
266
 * Parse a single timezone abbrev file --- can recurse to handle @INCLUDE
267
 *
268
 * filename: user-specified file name (does not include path)
269
 * depth: current recursion depth
270
 * *base: array for results (changeable if must enlarge array)
271
 * *arraysize: allocated length of array (changeable if must enlarge array)
272
 * n: current number of valid elements in array
273
 *
274
 * Returns the new array length (new value for n), or -1 if error
275
 */
276
static int
277
ParseTzFile(const char *filename, int depth,
278
      tzEntry **base, int *arraysize, int n)
279
0
{
280
0
  char    share_path[MAXPGPATH];
281
0
  char    file_path[MAXPGPATH];
282
0
  FILE     *tzFile;
283
0
  char    tzbuf[1024];
284
0
  char     *line;
285
0
  tzEntry   tzentry;
286
0
  int     lineno = 0;
287
0
  bool    override = false;
288
0
  const char *p;
289
290
  /*
291
   * We enforce that the filename is all alpha characters.  This may be
292
   * overly restrictive, but we don't want to allow access to anything
293
   * outside the timezonesets directory, so for instance '/' *must* be
294
   * rejected.
295
   */
296
0
  for (p = filename; *p; p++)
297
0
  {
298
0
    if (!isalpha((unsigned char) *p))
299
0
    {
300
      /* at level 0, just use guc.c's regular "invalid value" message */
301
0
      if (depth > 0)
302
0
        GUC_check_errmsg("invalid time zone file name \"%s\"",
303
0
                 filename);
304
0
      return -1;
305
0
    }
306
0
  }
307
308
  /*
309
   * The maximal recursion depth is a pretty arbitrary setting. It is hard
310
   * to imagine that someone needs more than 3 levels so stick with this
311
   * conservative setting until someone complains.
312
   */
313
0
  if (depth > 3)
314
0
  {
315
0
    GUC_check_errmsg("time zone file recursion limit exceeded in file \"%s\"",
316
0
             filename);
317
0
    return -1;
318
0
  }
319
320
0
  get_share_path(my_exec_path, share_path);
321
0
  snprintf(file_path, sizeof(file_path), "%s/timezonesets/%s",
322
0
       share_path, filename);
323
0
  tzFile = AllocateFile(file_path, "r");
324
0
  if (!tzFile)
325
0
  {
326
    /*
327
     * Check to see if the problem is not the filename but the directory.
328
     * This is worth troubling over because if the installation share/
329
     * directory is missing or unreadable, this is likely to be the first
330
     * place we notice a problem during postmaster startup.
331
     */
332
0
    int     save_errno = errno;
333
0
    DIR      *tzdir;
334
335
0
    snprintf(file_path, sizeof(file_path), "%s/timezonesets",
336
0
         share_path);
337
0
    tzdir = AllocateDir(file_path);
338
0
    if (tzdir == NULL)
339
0
    {
340
0
      GUC_check_errmsg("could not open directory \"%s\": %m",
341
0
               file_path);
342
0
      GUC_check_errhint("This may indicate an incomplete PostgreSQL installation, or that the file \"%s\" has been moved away from its proper location.",
343
0
                my_exec_path);
344
0
      return -1;
345
0
    }
346
0
    FreeDir(tzdir);
347
0
    errno = save_errno;
348
349
    /*
350
     * otherwise, if file doesn't exist and it's level 0, guc.c's
351
     * complaint is enough
352
     */
353
0
    if (errno != ENOENT || depth > 0)
354
0
      GUC_check_errmsg("could not read time zone file \"%s\": %m",
355
0
               filename);
356
357
0
    return -1;
358
0
  }
359
360
0
  while (!feof(tzFile))
361
0
  {
362
0
    lineno++;
363
0
    if (fgets(tzbuf, sizeof(tzbuf), tzFile) == NULL)
364
0
    {
365
0
      if (ferror(tzFile))
366
0
      {
367
0
        GUC_check_errmsg("could not read time zone file \"%s\": %m",
368
0
                 filename);
369
0
        n = -1;
370
0
        break;
371
0
      }
372
      /* else we're at EOF after all */
373
0
      break;
374
0
    }
375
0
    if (strlen(tzbuf) == sizeof(tzbuf) - 1)
376
0
    {
377
      /* the line is too long for tzbuf */
378
0
      GUC_check_errmsg("line is too long in time zone file \"%s\", line %d",
379
0
               filename, lineno);
380
0
      n = -1;
381
0
      break;
382
0
    }
383
384
    /* skip over whitespace */
385
0
    line = tzbuf;
386
0
    while (*line && isspace((unsigned char) *line))
387
0
      line++;
388
389
0
    if (*line == '\0')   /* empty line */
390
0
      continue;
391
0
    if (*line == '#')   /* comment line */
392
0
      continue;
393
394
0
    if (pg_strncasecmp(line, "@INCLUDE", strlen("@INCLUDE")) == 0)
395
0
    {
396
      /* pstrdup so we can use filename in result data structure */
397
0
      char     *includeFile = pstrdup(line + strlen("@INCLUDE"));
398
0
      char     *brki;
399
400
0
      includeFile = strtok_r(includeFile, WHITESPACE, &brki);
401
0
      if (!includeFile || !*includeFile)
402
0
      {
403
0
        GUC_check_errmsg("@INCLUDE without file name in time zone file \"%s\", line %d",
404
0
                 filename, lineno);
405
0
        n = -1;
406
0
        break;
407
0
      }
408
0
      n = ParseTzFile(includeFile, depth + 1,
409
0
              base, arraysize, n);
410
0
      if (n < 0)
411
0
        break;
412
0
      continue;
413
0
    }
414
415
0
    if (pg_strncasecmp(line, "@OVERRIDE", strlen("@OVERRIDE")) == 0)
416
0
    {
417
0
      override = true;
418
0
      continue;
419
0
    }
420
421
0
    if (!splitTzLine(filename, lineno, line, &tzentry))
422
0
    {
423
0
      n = -1;
424
0
      break;
425
0
    }
426
0
    if (!validateTzEntry(&tzentry))
427
0
    {
428
0
      n = -1;
429
0
      break;
430
0
    }
431
0
    n = addToArray(base, arraysize, n, &tzentry, override);
432
0
    if (n < 0)
433
0
      break;
434
0
  }
435
436
0
  FreeFile(tzFile);
437
438
0
  return n;
439
0
}
440
441
/*
442
 * load_tzoffsets --- read and parse the specified timezone offset file
443
 *
444
 * On success, return a filled-in TimeZoneAbbrevTable, which must have been
445
 * guc_malloc'd not palloc'd.  On failure, return NULL, using GUC_check_errmsg
446
 * and friends to give details of the problem.
447
 */
448
TimeZoneAbbrevTable *
449
load_tzoffsets(const char *filename)
450
0
{
451
0
  TimeZoneAbbrevTable *result = NULL;
452
0
  MemoryContext tmpContext;
453
0
  MemoryContext oldContext;
454
0
  tzEntry    *array;
455
0
  int     arraysize;
456
0
  int     n;
457
458
  /*
459
   * Create a temp memory context to work in.  This makes it easy to clean
460
   * up afterwards.
461
   */
462
0
  tmpContext = AllocSetContextCreate(CurrentMemoryContext,
463
0
                     "TZParserMemory",
464
0
                     ALLOCSET_SMALL_SIZES);
465
0
  oldContext = MemoryContextSwitchTo(tmpContext);
466
467
  /* Initialize array at a reasonable size */
468
0
  arraysize = 128;
469
0
  array = (tzEntry *) palloc(arraysize * sizeof(tzEntry));
470
471
  /* Parse the file(s) */
472
0
  n = ParseTzFile(filename, 0, &array, &arraysize, 0);
473
474
  /* If no errors so far, let datetime.c allocate memory & convert format */
475
0
  if (n >= 0)
476
0
  {
477
0
    result = ConvertTimeZoneAbbrevs(array, n);
478
0
    if (!result)
479
0
      GUC_check_errmsg("out of memory");
480
0
  }
481
482
  /* Clean up */
483
0
  MemoryContextSwitchTo(oldContext);
484
0
  MemoryContextDelete(tmpContext);
485
486
0
  return result;
487
0
}