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-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 *
8120 bruce@momjian.us 43 :CBC 12156 : 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 : :
8121 50 [ + + ]: 12156 : if (done_tzdir)
51 : 11032 : return tzdir;
52 : :
8103 53 : 1124 : get_share_path(my_exec_path, tzdir);
7222 tgl@sss.pgh.pa.us 54 : 1124 : strlcpy(tzdir + strlen(tzdir), "/timezone", MAXPGPATH - strlen(tzdir));
55 : :
56 : 1124 : done_tzdir = true;
8121 bruce@momjian.us 57 : 1124 : 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
7222 tgl@sss.pgh.pa.us 76 : 12146 : 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 */
3371 84 : 12146 : strlcpy(fullname, pg_TZDIR(), sizeof(fullname));
85 : 12146 : orignamelen = fullnamelen = strlen(fullname);
86 : :
87 [ - + ]: 12146 : if (fullnamelen + 1 + strlen(name) >= MAXPGPATH)
3371 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 : : */
3371 tgl@sss.pgh.pa.us 97 [ + + ]:CBC 12146 : if (canonname == NULL)
98 : : {
99 : : int result;
100 : :
101 : 5970 : fullname[fullnamelen] = '/';
102 : : /* test above ensured this will fit: */
103 : 5970 : strcpy(fullname + fullnamelen + 1, name);
104 : 5970 : result = open(fullname, O_RDONLY | PG_BINARY, 0);
105 [ + - ]: 5970 : if (result >= 0)
106 : 5970 : return result;
107 : : /* If that didn't work, fall through to do it the hard way */
3366 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 : : */
7222 tgl@sss.pgh.pa.us 115 :CBC 6176 : fname = name;
116 : : for (;;)
117 : 5881 : {
118 : : const char *slashptr;
119 : : int fnamelen;
120 : :
121 : 12057 : slashptr = strchr(fname, '/');
122 [ + + ]: 12057 : if (slashptr)
123 : 5901 : fnamelen = slashptr - fname;
124 : : else
125 : 6156 : fnamelen = strlen(fname);
126 [ + + ]: 12057 : if (!scan_directory_ci(fullname, fname, fnamelen,
127 : 12057 : fullname + fullnamelen + 1,
128 : : MAXPGPATH - fullnamelen - 1))
129 : 289 : return -1;
130 : 11768 : fullname[fullnamelen++] = '/';
131 : 11768 : fullnamelen += strlen(fullname + fullnamelen);
132 [ + + ]: 11768 : if (slashptr)
133 : 5881 : fname = slashptr + 1;
134 : : else
135 : 5887 : break;
136 : : }
137 : :
138 [ + - ]: 5887 : if (canonname)
139 : 5887 : strlcpy(canonname, fullname + orignamelen + 1, TZ_STRLEN_MAX + 1);
140 : :
141 : 5887 : 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 : 12057 : scan_directory_ci(const char *dirname, const char *fname, int fnamelen,
152 : : char *canonname, int canonnamelen)
153 : : {
154 : 12057 : bool found = false;
155 : : DIR *dirdesc;
156 : : struct dirent *direntry;
157 : :
158 : 12057 : dirdesc = AllocateDir(dirname);
159 : :
3155 160 [ + + ]: 843974 : 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 : : */
7222 166 [ + + ]: 843685 : if (direntry->d_name[0] == '.')
167 : 21465 : continue;
168 : :
169 [ + + + + ]: 933362 : if (strlen(direntry->d_name) == fnamelen &&
170 : 111142 : pg_strncasecmp(direntry->d_name, fname, fnamelen) == 0)
171 : : {
172 : : /* Found our match */
173 : 11768 : strlcpy(canonname, direntry->d_name, canonnamelen);
174 : 11768 : found = true;
175 : 11768 : break;
176 : : }
177 : : }
178 : :
179 : 12057 : FreeDir(dirdesc);
180 : :
181 : 12057 : 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
7767 bruce@momjian.us 202 : 1265 : init_timezone_hashtable(void)
203 : : {
204 : : HASHCTL hash_ctl;
205 : :
7624 tgl@sss.pgh.pa.us 206 : 1265 : hash_ctl.keysize = TZ_STRLEN_MAX + 1;
7222 207 : 1265 : hash_ctl.entrysize = sizeof(pg_tz_cache);
208 : :
7767 bruce@momjian.us 209 : 1265 : timezone_cache = hash_create("Timezones",
210 : : 4,
211 : : &hash_ctl,
212 : : HASH_ELEM | HASH_STRINGS);
213 [ - + ]: 1265 : if (!timezone_cache)
7767 bruce@momjian.us 214 :UBC 0 : return false;
215 : :
7767 bruce@momjian.us 216 :CBC 1265 : 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 : : pg_tz *
1405 pg@bowt.ie 224 : 20869 : pg_tzset(const char *tzname)
225 : : {
226 : : pg_tz_cache *tzp;
227 : : struct state tzstate;
228 : : char uppername[TZ_STRLEN_MAX + 1];
229 : : char canonname[TZ_STRLEN_MAX + 1];
230 : : char *p;
231 : :
232 [ - + ]: 20869 : if (strlen(tzname) > TZ_STRLEN_MAX)
7767 bruce@momjian.us 233 :UBC 0 : return NULL; /* not going to fit */
234 : :
7767 bruce@momjian.us 235 [ + + ]:CBC 20869 : if (!timezone_cache)
236 [ - + ]: 1265 : if (!init_timezone_hashtable())
7767 bruce@momjian.us 237 :UBC 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 : : */
7222 tgl@sss.pgh.pa.us 245 :CBC 20869 : p = uppername;
1405 pg@bowt.ie 246 [ + + ]: 195343 : while (*tzname)
247 : 174474 : *p++ = pg_toupper((unsigned char) *tzname++);
7222 tgl@sss.pgh.pa.us 248 : 20869 : *p = '\0';
249 : :
250 : 20869 : tzp = (pg_tz_cache *) hash_search(timezone_cache,
251 : : uppername,
252 : : HASH_FIND,
253 : : NULL);
7767 bruce@momjian.us 254 [ + + ]: 20869 : if (tzp)
255 : : {
256 : : /* Timezone found in cache, nothing more to do */
7222 tgl@sss.pgh.pa.us 257 : 13428 : return &tzp->tz;
258 : : }
259 : :
260 : : /*
261 : : * Let the IANA tzdb code interpret the time zone name.
262 : : */
24 tgl@sss.pgh.pa.us 263 [ + + ]:GNC 7441 : if (!pg_tzload(uppername, canonname, &tzstate))
264 : : {
265 [ - + ]: 68 : if (strcmp(uppername, "GMT") == 0)
266 : : {
267 : : /* This really, really should not happen ... */
5433 tgl@sss.pgh.pa.us 268 [ # # ]:UBC 0 : elog(ERROR, "could not initialize GMT time zone");
269 : : }
270 : : /* Unknown timezone. Fail our call instead of loading GMT! */
24 tgl@sss.pgh.pa.us 271 :GNC 68 : return NULL;
272 : : }
273 : :
274 : : /* Save timezone in the cache */
7222 tgl@sss.pgh.pa.us 275 :CBC 7373 : tzp = (pg_tz_cache *) hash_search(timezone_cache,
276 : : uppername,
277 : : HASH_ENTER,
278 : : NULL);
279 : :
280 : : /* hash_search already copied uppername into the hash key */
281 : 7373 : strcpy(tzp->tz.TZname, canonname);
282 : 7373 : memcpy(&tzp->tz.state, &tzstate, sizeof(tzstate));
283 : :
284 : 7373 : return &tzp->tz;
285 : : }
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 *
4649 300 : 69 : pg_tzset_offset(long gmtoffset)
301 : : {
302 : 69 : long absoffset = (gmtoffset < 0) ? -gmtoffset : gmtoffset;
303 : : char offsetstr[64];
304 : : char tzname[128];
305 : :
306 : 69 : snprintf(offsetstr, sizeof(offsetstr),
307 : : "%02ld", absoffset / SECS_PER_HOUR);
3373 308 : 69 : absoffset %= SECS_PER_HOUR;
4649 309 [ + + ]: 69 : if (absoffset != 0)
310 : : {
311 : 12 : snprintf(offsetstr + strlen(offsetstr),
312 : 12 : sizeof(offsetstr) - strlen(offsetstr),
313 : : ":%02ld", absoffset / SECS_PER_MINUTE);
3373 314 : 12 : absoffset %= SECS_PER_MINUTE;
4649 315 [ - + ]: 12 : if (absoffset != 0)
4649 tgl@sss.pgh.pa.us 316 :UBC 0 : snprintf(offsetstr + strlen(offsetstr),
317 : 0 : sizeof(offsetstr) - strlen(offsetstr),
318 : : ":%02ld", absoffset);
319 : : }
4649 tgl@sss.pgh.pa.us 320 [ + + ]:CBC 69 : if (gmtoffset > 0)
321 : 16 : snprintf(tzname, sizeof(tzname), "<-%s>+%s",
322 : : offsetstr, offsetstr);
323 : : else
324 : 53 : snprintf(tzname, sizeof(tzname), "<+%s>-%s",
325 : : offsetstr, offsetstr);
326 : :
327 : 69 : return pg_tzset(tzname);
328 : : }
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
8100 bruce@momjian.us 341 : 1265 : pg_timezone_initialize(void)
342 : : {
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 : : */
5433 tgl@sss.pgh.pa.us 350 : 1265 : session_timezone = pg_tzset("GMT");
351 : 1265 : log_timezone = session_timezone;
8100 352 : 1265 : }
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 : : #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 *
7234 bruce@momjian.us 377 : 10 : pg_tzenumerate_start(void)
378 : : {
228 michael@paquier.xyz 379 : 10 : pg_tzenum *ret = palloc0_object(pg_tzenum);
7234 bruce@momjian.us 380 : 10 : char *startdir = pstrdup(pg_TZDIR());
381 : :
7252 tgl@sss.pgh.pa.us 382 : 10 : ret->baselen = strlen(startdir) + 1;
383 : 10 : ret->depth = 0;
384 : 10 : ret->dirname[0] = startdir;
385 : 10 : ret->dirdesc[0] = AllocateDir(startdir);
7234 bruce@momjian.us 386 [ - + ]: 10 : if (!ret->dirdesc[0])
7252 tgl@sss.pgh.pa.us 387 [ # # ]:UBC 0 : ereport(ERROR,
388 : : (errcode_for_file_access(),
389 : : errmsg("could not open directory \"%s\": %m", startdir)));
7252 tgl@sss.pgh.pa.us 390 :CBC 10 : return ret;
391 : : }
392 : :
393 : : void
394 : 10 : pg_tzenumerate_end(pg_tzenum *dir)
395 : : {
396 [ - + ]: 10 : while (dir->depth >= 0)
397 : : {
7252 tgl@sss.pgh.pa.us 398 :UBC 0 : FreeDir(dir->dirdesc[dir->depth]);
399 : 0 : pfree(dir->dirname[dir->depth]);
400 : 0 : dir->depth--;
401 : : }
7252 tgl@sss.pgh.pa.us 402 :CBC 10 : pfree(dir);
403 : 10 : }
404 : :
405 : : pg_tz *
406 : 5990 : pg_tzenumerate_next(pg_tzenum *dir)
407 : : {
408 [ + + ]: 6820 : while (dir->depth >= 0)
409 : : {
410 : : struct dirent *direntry;
411 : : char fullname[MAXPGPATH * 2];
412 : :
413 : 6810 : direntry = ReadDir(dir->dirdesc[dir->depth], dir->dirname[dir->depth]);
414 : :
415 [ + + ]: 6810 : if (!direntry)
416 : : {
417 : : /* End of this directory */
418 : 210 : FreeDir(dir->dirdesc[dir->depth]);
419 : 210 : pfree(dir->dirname[dir->depth]);
420 : 210 : dir->depth--;
421 : 830 : continue;
422 : : }
423 : :
424 [ + + ]: 6600 : if (direntry->d_name[0] == '.')
425 : 420 : continue;
426 : :
3392 peter_e@gmx.net 427 : 6180 : snprintf(fullname, sizeof(fullname), "%s/%s",
7252 tgl@sss.pgh.pa.us 428 : 6180 : dir->dirname[dir->depth], direntry->d_name);
429 : :
1422 michael@paquier.xyz 430 [ + + ]: 6180 : if (get_dirent_type(fullname, direntry, true, ERROR) == PGFILETYPE_DIR)
431 : : {
432 : : /* Step into the subdirectory */
7234 bruce@momjian.us 433 [ - + ]: 200 : if (dir->depth >= MAX_TZDIR_DEPTH - 1)
7252 tgl@sss.pgh.pa.us 434 [ # # ]:UBC 0 : ereport(ERROR,
435 : : (errmsg_internal("timezone directory stack overflow")));
7252 tgl@sss.pgh.pa.us 436 :CBC 200 : dir->depth++;
437 : 200 : dir->dirname[dir->depth] = pstrdup(fullname);
438 : 200 : dir->dirdesc[dir->depth] = AllocateDir(fullname);
7234 bruce@momjian.us 439 [ - + ]: 200 : if (!dir->dirdesc[dir->depth])
7252 tgl@sss.pgh.pa.us 440 [ # # ]:UBC 0 : ereport(ERROR,
441 : : (errcode_for_file_access(),
442 : : errmsg("could not open directory \"%s\": %m",
443 : : fullname)));
444 : :
445 : : /* Start over reading in the new directory */
7252 tgl@sss.pgh.pa.us 446 :CBC 200 : continue;
447 : : }
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 : : */
24 tgl@sss.pgh.pa.us 455 [ - + ]:GNC 5980 : if (!pg_tzload(fullname + dir->baselen, NULL, &dir->tz.state))
456 : : {
457 : : /* Zone could not be loaded, ignore it */
7252 tgl@sss.pgh.pa.us 458 :UBC 0 : continue;
459 : : }
460 : :
5433 tgl@sss.pgh.pa.us 461 [ - + ]:CBC 5980 : if (!pg_tz_acceptable(&dir->tz))
462 : : {
463 : : /* Ignore leap-second zones */
6463 tgl@sss.pgh.pa.us 464 :UBC 0 : continue;
465 : : }
466 : :
467 : : /* OK, return the canonical zone name spelling. */
3371 tgl@sss.pgh.pa.us 468 :CBC 5980 : strlcpy(dir->tz.TZname, fullname + dir->baselen,
469 : : sizeof(dir->tz.TZname));
470 : :
471 : : /* Timezone loaded OK. */
7252 472 : 5980 : return &dir->tz;
473 : : }
474 : :
475 : : /* Nothing more found */
476 : 10 : return NULL;
477 : : }
|