Age Owner Branch data TLA Line data Source code
1 : : /*-------------------------------------------------------------------------
2 : : *
3 : : * pgtz.c
4 : : * Timezone Library Integration Functions
5 : : *
6 : : * Portions Copyright (c) 1996-2024, 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 *
7288 bruce@momjian.us 43 :CBC 9478 : 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 : :
7289 50 [ + + ]: 9478 : if (done_tzdir)
51 : 8653 : return tzdir;
52 : :
7271 53 : 825 : get_share_path(my_exec_path, tzdir);
6390 tgl@sss.pgh.pa.us 54 : 825 : strlcpy(tzdir + strlen(tzdir), "/timezone", MAXPGPATH - strlen(tzdir));
55 : :
56 : 825 : done_tzdir = true;
7289 bruce@momjian.us 57 : 825 : 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
6390 tgl@sss.pgh.pa.us 76 : 9470 : 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 */
2539 84 : 9470 : strlcpy(fullname, pg_TZDIR(), sizeof(fullname));
85 : 9470 : orignamelen = fullnamelen = strlen(fullname);
86 : :
87 [ - + ]: 9470 : if (fullnamelen + 1 + strlen(name) >= MAXPGPATH)
2539 tgl@sss.pgh.pa.us 88 :UBC 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 : : */
2539 tgl@sss.pgh.pa.us 97 [ + + ]:CBC 9470 : if (canonname == NULL)
98 : : {
99 : : int result;
100 : :
101 : 4950 : fullname[fullnamelen] = '/';
102 : : /* test above ensured this will fit: */
103 : 4950 : strcpy(fullname + fullnamelen + 1, name);
104 : 4950 : result = open(fullname, O_RDONLY | PG_BINARY, 0);
105 [ + - ]: 4950 : if (result >= 0)
106 : 4950 : return result;
107 : : /* If that didn't work, fall through to do it the hard way */
2534 tgl@sss.pgh.pa.us 108 :UBC 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 : : */
6390 tgl@sss.pgh.pa.us 115 :CBC 4520 : fname = name;
116 : : for (;;)
117 : 994 : {
118 : : const char *slashptr;
119 : : int fnamelen;
120 : :
121 : 5514 : slashptr = strchr(fname, '/');
122 [ + + ]: 5514 : if (slashptr)
123 : 1009 : fnamelen = slashptr - fname;
124 : : else
125 : 4505 : fnamelen = strlen(fname);
126 [ + + ]: 5514 : if (!scan_directory_ci(fullname, fname, fnamelen,
127 : 5514 : fullname + fullnamelen + 1,
128 : : MAXPGPATH - fullnamelen - 1))
129 : 214 : return -1;
130 : 5300 : fullname[fullnamelen++] = '/';
131 : 5300 : fullnamelen += strlen(fullname + fullnamelen);
132 [ + + ]: 5300 : if (slashptr)
133 : 994 : fname = slashptr + 1;
134 : : else
135 : 4306 : break;
136 : : }
137 : :
138 [ + - ]: 4306 : if (canonname)
139 : 4306 : strlcpy(canonname, fullname + orignamelen + 1, TZ_STRLEN_MAX + 1);
140 : :
141 : 4306 : 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 : 5514 : scan_directory_ci(const char *dirname, const char *fname, int fnamelen,
152 : : char *canonname, int canonnamelen)
153 : : {
154 : 5514 : bool found = false;
155 : : DIR *dirdesc;
156 : : struct dirent *direntry;
157 : :
158 : 5514 : dirdesc = AllocateDir(dirname);
159 : :
2323 160 [ + + ]: 215665 : 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 : : */
6390 166 [ + + ]: 215451 : if (direntry->d_name[0] == '.')
167 : 11028 : continue;
168 : :
169 [ + + + + ]: 238464 : if (strlen(direntry->d_name) == fnamelen &&
170 : 34041 : pg_strncasecmp(direntry->d_name, fname, fnamelen) == 0)
171 : : {
172 : : /* Found our match */
173 : 5300 : strlcpy(canonname, direntry->d_name, canonnamelen);
174 : 5300 : found = true;
175 : 5300 : break;
176 : : }
177 : : }
178 : :
179 : 5514 : FreeDir(dirdesc);
180 : :
181 : 5514 : 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
6935 bruce@momjian.us 202 : 928 : init_timezone_hashtable(void)
203 : : {
204 : : HASHCTL hash_ctl;
205 : :
6792 tgl@sss.pgh.pa.us 206 : 928 : hash_ctl.keysize = TZ_STRLEN_MAX + 1;
6390 207 : 928 : hash_ctl.entrysize = sizeof(pg_tz_cache);
208 : :
6935 bruce@momjian.us 209 : 928 : timezone_cache = hash_create("Timezones",
210 : : 4,
211 : : &hash_ctl,
212 : : HASH_ELEM | HASH_STRINGS);
213 [ - + ]: 928 : if (!timezone_cache)
6935 bruce@momjian.us 214 :UBC 0 : return false;
215 : :
6935 bruce@momjian.us 216 :CBC 928 : 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 *
573 pg@bowt.ie 234 : 15421 : 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 [ - + ]: 15421 : if (strlen(tzname) > TZ_STRLEN_MAX)
6935 bruce@momjian.us 243 :UBC 0 : return NULL; /* not going to fit */
244 : :
6935 bruce@momjian.us 245 [ + + ]:CBC 15421 : if (!timezone_cache)
246 [ - + ]: 928 : if (!init_timezone_hashtable())
6935 bruce@momjian.us 247 :UBC 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 : : */
6390 tgl@sss.pgh.pa.us 255 :CBC 15421 : p = uppername;
573 pg@bowt.ie 256 [ + + ]: 160113 : while (*tzname)
257 : 144692 : *p++ = pg_toupper((unsigned char) *tzname++);
6390 tgl@sss.pgh.pa.us 258 : 15421 : *p = '\0';
259 : :
260 : 15421 : tzp = (pg_tz_cache *) hash_search(timezone_cache,
261 : : uppername,
262 : : HASH_FIND,
263 : : NULL);
6935 bruce@momjian.us 264 [ + + ]: 15421 : if (tzp)
265 : : {
266 : : /* Timezone found in cache, nothing more to do */
6390 tgl@sss.pgh.pa.us 267 : 9973 : return &tzp->tz;
268 : : }
269 : :
270 : : /*
271 : : * "GMT" is always sent to tzparse(), as per discussion above.
272 : : */
4601 273 [ + + ]: 5448 : if (strcmp(uppername, "GMT") == 0)
274 : : {
2939 275 [ - + ]: 928 : if (!tzparse(uppername, &tzstate, true))
276 : : {
277 : : /* This really, really should not happen ... */
4601 tgl@sss.pgh.pa.us 278 [ # # ]:UBC 0 : elog(ERROR, "could not initialize GMT time zone");
279 : : }
280 : : /* Use uppercase name as canonical */
4601 tgl@sss.pgh.pa.us 281 :CBC 928 : strcpy(canonname, uppername);
282 : : }
2939 283 [ + + ]: 4520 : else if (tzload(uppername, canonname, &tzstate, true) != 0)
284 : : {
285 [ + - + + ]: 214 : if (uppername[0] == ':' || !tzparse(uppername, &tzstate, false))
286 : : {
287 : : /* Unknown timezone. Fail our call instead of loading GMT! */
6935 bruce@momjian.us 288 : 48 : return NULL;
289 : : }
290 : : /* For POSIX timezone specs, use uppercase name as canonical */
6390 tgl@sss.pgh.pa.us 291 : 166 : strcpy(canonname, uppername);
292 : : }
293 : :
294 : : /* Save timezone in the cache */
295 : 5400 : 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 : 5400 : strcpy(tzp->tz.TZname, canonname);
302 : 5400 : memcpy(&tzp->tz.state, &tzstate, sizeof(tzstate));
303 : :
304 : 5400 : 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 *
3817 320 : 39 : pg_tzset_offset(long gmtoffset)
321 : : {
322 : 39 : long absoffset = (gmtoffset < 0) ? -gmtoffset : gmtoffset;
323 : : char offsetstr[64];
324 : : char tzname[128];
325 : :
326 : 39 : snprintf(offsetstr, sizeof(offsetstr),
327 : : "%02ld", absoffset / SECS_PER_HOUR);
2541 328 : 39 : absoffset %= SECS_PER_HOUR;
3817 329 [ + + ]: 39 : if (absoffset != 0)
330 : : {
331 : 9 : snprintf(offsetstr + strlen(offsetstr),
332 : 9 : sizeof(offsetstr) - strlen(offsetstr),
333 : : ":%02ld", absoffset / SECS_PER_MINUTE);
2541 334 : 9 : absoffset %= SECS_PER_MINUTE;
3817 335 [ - + ]: 9 : if (absoffset != 0)
3817 tgl@sss.pgh.pa.us 336 :UBC 0 : snprintf(offsetstr + strlen(offsetstr),
337 : 0 : sizeof(offsetstr) - strlen(offsetstr),
338 : : ":%02ld", absoffset);
339 : : }
3817 tgl@sss.pgh.pa.us 340 [ + + ]:CBC 39 : if (gmtoffset > 0)
341 : 12 : snprintf(tzname, sizeof(tzname), "<-%s>+%s",
342 : : offsetstr, offsetstr);
343 : : else
344 : 27 : snprintf(tzname, sizeof(tzname), "<+%s>-%s",
345 : : offsetstr, offsetstr);
346 : :
347 : 39 : 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
7268 bruce@momjian.us 361 : 928 : 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_tables.c, although in
368 : : * principle it could be different.
369 : : */
4601 tgl@sss.pgh.pa.us 370 : 928 : session_timezone = pg_tzset("GMT");
371 : 928 : log_timezone = session_timezone;
7268 372 : 928 : }
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 *
6402 bruce@momjian.us 397 : 8 : pg_tzenumerate_start(void)
398 : : {
399 : 8 : pg_tzenum *ret = (pg_tzenum *) palloc0(sizeof(pg_tzenum));
400 : 8 : char *startdir = pstrdup(pg_TZDIR());
401 : :
6420 tgl@sss.pgh.pa.us 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);
6402 bruce@momjian.us 406 [ - + ]: 8 : if (!ret->dirdesc[0])
6420 tgl@sss.pgh.pa.us 407 [ # # ]:UBC 0 : ereport(ERROR,
408 : : (errcode_for_file_access(),
409 : : errmsg("could not open directory \"%s\": %m", startdir)));
6420 tgl@sss.pgh.pa.us 410 :CBC 8 : return ret;
411 : : }
412 : :
413 : : void
414 : 8 : pg_tzenumerate_end(pg_tzenum *dir)
415 : : {
416 [ - + ]: 8 : while (dir->depth >= 0)
417 : : {
6420 tgl@sss.pgh.pa.us 418 :UBC 0 : FreeDir(dir->dirdesc[dir->depth]);
419 : 0 : pfree(dir->dirname[dir->depth]);
420 : 0 : dir->depth--;
421 : : }
6420 tgl@sss.pgh.pa.us 422 :CBC 8 : pfree(dir);
423 : 8 : }
424 : :
425 : : pg_tz *
426 : 4784 : pg_tzenumerate_next(pg_tzenum *dir)
427 : : {
428 [ + + ]: 5448 : while (dir->depth >= 0)
429 : : {
430 : : struct dirent *direntry;
431 : : char fullname[MAXPGPATH * 2];
432 : :
433 : 5440 : direntry = ReadDir(dir->dirdesc[dir->depth], dir->dirname[dir->depth]);
434 : :
435 [ + + ]: 5440 : 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 [ + + ]: 5272 : if (direntry->d_name[0] == '.')
445 : 336 : continue;
446 : :
2560 peter_e@gmx.net 447 : 4936 : snprintf(fullname, sizeof(fullname), "%s/%s",
6420 tgl@sss.pgh.pa.us 448 : 4936 : dir->dirname[dir->depth], direntry->d_name);
449 : :
590 michael@paquier.xyz 450 [ + + ]: 4936 : if (get_dirent_type(fullname, direntry, true, ERROR) == PGFILETYPE_DIR)
451 : : {
452 : : /* Step into the subdirectory */
6402 bruce@momjian.us 453 [ - + ]: 160 : if (dir->depth >= MAX_TZDIR_DEPTH - 1)
6420 tgl@sss.pgh.pa.us 454 [ # # ]:UBC 0 : ereport(ERROR,
455 : : (errmsg_internal("timezone directory stack overflow")));
6420 tgl@sss.pgh.pa.us 456 :CBC 160 : dir->depth++;
457 : 160 : dir->dirname[dir->depth] = pstrdup(fullname);
458 : 160 : dir->dirdesc[dir->depth] = AllocateDir(fullname);
6402 bruce@momjian.us 459 [ - + ]: 160 : if (!dir->dirdesc[dir->depth])
6420 tgl@sss.pgh.pa.us 460 [ # # ]:UBC 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 */
6420 tgl@sss.pgh.pa.us 466 :CBC 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 : : */
2539 475 [ - + ]: 4776 : if (tzload(fullname + dir->baselen, NULL, &dir->tz.state, true) != 0)
476 : : {
477 : : /* Zone could not be loaded, ignore it */
6420 tgl@sss.pgh.pa.us 478 :UBC 0 : continue;
479 : : }
480 : :
4601 tgl@sss.pgh.pa.us 481 [ - + ]:CBC 4776 : if (!pg_tz_acceptable(&dir->tz))
482 : : {
483 : : /* Ignore leap-second zones */
5631 tgl@sss.pgh.pa.us 484 :UBC 0 : continue;
485 : : }
486 : :
487 : : /* OK, return the canonical zone name spelling. */
2539 tgl@sss.pgh.pa.us 488 :CBC 4776 : strlcpy(dir->tz.TZname, fullname + dir->baselen,
489 : : sizeof(dir->tz.TZname));
490 : :
491 : : /* Timezone loaded OK. */
6420 492 : 4776 : return &dir->tz;
493 : : }
494 : :
495 : : /* Nothing more found */
496 : 8 : return NULL;
497 : : }
|