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