Branch data 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 *
43 : 12314 : 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 : :
50 [ + + ]: 12314 : if (done_tzdir)
51 : 11166 : return tzdir;
52 : :
53 : 1148 : get_share_path(my_exec_path, tzdir);
54 : 1148 : strlcpy(tzdir + strlen(tzdir), "/timezone", MAXPGPATH - strlen(tzdir));
55 : :
56 : 1148 : done_tzdir = true;
57 : 1148 : 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
76 : 12304 : 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 */
84 : 12304 : strlcpy(fullname, pg_TZDIR(), sizeof(fullname));
85 : 12304 : orignamelen = fullnamelen = strlen(fullname);
86 : :
87 [ - + ]: 12304 : 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 [ + + ]: 12304 : 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 */
108 : 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 : : */
115 : 6334 : fname = name;
116 : : for (;;)
117 : 6037 : {
118 : : const char *slashptr;
119 : : int fnamelen;
120 : :
121 : 12371 : slashptr = strchr(fname, '/');
122 [ + + ]: 12371 : if (slashptr)
123 : 6057 : fnamelen = slashptr - fname;
124 : : else
125 : 6314 : fnamelen = strlen(fname);
126 [ + + ]: 12371 : if (!scan_directory_ci(fullname, fname, fnamelen,
127 : 12371 : fullname + fullnamelen + 1,
128 : : MAXPGPATH - fullnamelen - 1))
129 : 289 : return -1;
130 : 12082 : fullname[fullnamelen++] = '/';
131 : 12082 : fullnamelen += strlen(fullname + fullnamelen);
132 [ + + ]: 12082 : if (slashptr)
133 : 6037 : fname = slashptr + 1;
134 : : else
135 : 6045 : break;
136 : : }
137 : :
138 [ + - ]: 6045 : if (canonname)
139 : 6045 : strlcpy(canonname, fullname + orignamelen + 1, TZ_STRLEN_MAX + 1);
140 : :
141 : 6045 : 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 : 12371 : scan_directory_ci(const char *dirname, const char *fname, int fnamelen,
152 : : char *canonname, int canonnamelen)
153 : : {
154 : 12371 : bool found = false;
155 : : DIR *dirdesc;
156 : : struct dirent *direntry;
157 : :
158 : 12371 : dirdesc = AllocateDir(dirname);
159 : :
160 [ + + ]: 867030 : 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 : : */
166 [ + + ]: 866741 : if (direntry->d_name[0] == '.')
167 : 22043 : continue;
168 : :
169 [ + + + + ]: 958829 : if (strlen(direntry->d_name) == fnamelen &&
170 : 114131 : pg_strncasecmp(direntry->d_name, fname, fnamelen) == 0)
171 : : {
172 : : /* Found our match */
173 : 12082 : strlcpy(canonname, direntry->d_name, canonnamelen);
174 : 12082 : found = true;
175 : 12082 : break;
176 : : }
177 : : }
178 : :
179 : 12371 : FreeDir(dirdesc);
180 : :
181 : 12371 : 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
202 : 1291 : init_timezone_hashtable(void)
203 : : {
204 : : HASHCTL hash_ctl;
205 : :
206 : 1291 : hash_ctl.keysize = TZ_STRLEN_MAX + 1;
207 : 1291 : hash_ctl.entrysize = sizeof(pg_tz_cache);
208 : :
209 : 1291 : timezone_cache = hash_create("Timezones",
210 : : 4,
211 : : &hash_ctl,
212 : : HASH_ELEM | HASH_STRINGS);
213 [ - + ]: 1291 : if (!timezone_cache)
214 : 0 : return false;
215 : :
216 : 1291 : 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 *
224 : 21129 : 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 [ - + ]: 21129 : if (strlen(tzname) > TZ_STRLEN_MAX)
233 : 0 : return NULL; /* not going to fit */
234 : :
235 [ + + ]: 21129 : if (!timezone_cache)
236 [ - + ]: 1291 : 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 : 21129 : p = uppername;
246 [ + + ]: 198719 : while (*tzname)
247 : 177590 : *p++ = pg_toupper((unsigned char) *tzname++);
248 : 21129 : *p = '\0';
249 : :
250 : 21129 : tzp = (pg_tz_cache *) hash_search(timezone_cache,
251 : : uppername,
252 : : HASH_FIND,
253 : : NULL);
254 [ + + ]: 21129 : if (tzp)
255 : : {
256 : : /* Timezone found in cache, nothing more to do */
257 : 13504 : return &tzp->tz;
258 : : }
259 : :
260 : : /*
261 : : * Let the IANA tzdb code interpret the time zone name.
262 : : */
263 [ + + ]: 7625 : if (!pg_tzload(uppername, canonname, &tzstate))
264 : : {
265 [ - + ]: 68 : if (strcmp(uppername, "GMT") == 0)
266 : : {
267 : : /* This really, really should not happen ... */
268 [ # # ]: 0 : elog(ERROR, "could not initialize GMT time zone");
269 : : }
270 : : /* Unknown timezone. Fail our call instead of loading GMT! */
271 : 68 : return NULL;
272 : : }
273 : :
274 : : /* Save timezone in the cache */
275 : 7557 : 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 : 7557 : strcpy(tzp->tz.TZname, canonname);
282 : 7557 : memcpy(&tzp->tz.state, &tzstate, sizeof(tzstate));
283 : :
284 : 7557 : 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 *
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);
308 : 69 : absoffset %= SECS_PER_HOUR;
309 [ + + ]: 69 : if (absoffset != 0)
310 : : {
311 : 12 : snprintf(offsetstr + strlen(offsetstr),
312 : 12 : sizeof(offsetstr) - strlen(offsetstr),
313 : : ":%02ld", absoffset / SECS_PER_MINUTE);
314 : 12 : absoffset %= SECS_PER_MINUTE;
315 [ - + ]: 12 : if (absoffset != 0)
316 : 0 : snprintf(offsetstr + strlen(offsetstr),
317 : 0 : sizeof(offsetstr) - strlen(offsetstr),
318 : : ":%02ld", absoffset);
319 : : }
320 [ + + ]: 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
341 : 1291 : 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 : : */
350 : 1291 : session_timezone = pg_tzset("GMT");
351 : 1291 : log_timezone = session_timezone;
352 : 1291 : }
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 *
377 : 10 : pg_tzenumerate_start(void)
378 : : {
379 : 10 : pg_tzenum *ret = palloc0_object(pg_tzenum);
380 : 10 : char *startdir = pstrdup(pg_TZDIR());
381 : :
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);
386 [ - + ]: 10 : if (!ret->dirdesc[0])
387 [ # # ]: 0 : ereport(ERROR,
388 : : (errcode_for_file_access(),
389 : : errmsg("could not open directory \"%s\": %m", startdir)));
390 : 10 : return ret;
391 : : }
392 : :
393 : : void
394 : 10 : pg_tzenumerate_end(pg_tzenum *dir)
395 : : {
396 [ - + ]: 10 : while (dir->depth >= 0)
397 : : {
398 : 0 : FreeDir(dir->dirdesc[dir->depth]);
399 : 0 : pfree(dir->dirname[dir->depth]);
400 : 0 : dir->depth--;
401 : : }
402 : 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 : :
427 : 6180 : snprintf(fullname, sizeof(fullname), "%s/%s",
428 : 6180 : dir->dirname[dir->depth], direntry->d_name);
429 : :
430 [ + + ]: 6180 : if (get_dirent_type(fullname, direntry, true, ERROR) == PGFILETYPE_DIR)
431 : : {
432 : : /* Step into the subdirectory */
433 [ - + ]: 200 : if (dir->depth >= MAX_TZDIR_DEPTH - 1)
434 [ # # ]: 0 : ereport(ERROR,
435 : : (errmsg_internal("timezone directory stack overflow")));
436 : 200 : dir->depth++;
437 : 200 : dir->dirname[dir->depth] = pstrdup(fullname);
438 : 200 : dir->dirdesc[dir->depth] = AllocateDir(fullname);
439 [ - + ]: 200 : if (!dir->dirdesc[dir->depth])
440 [ # # ]: 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 */
446 : 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 : : */
455 [ - + ]: 5980 : if (!pg_tzload(fullname + dir->baselen, NULL, &dir->tz.state))
456 : : {
457 : : /* Zone could not be loaded, ignore it */
458 : 0 : continue;
459 : : }
460 : :
461 [ - + ]: 5980 : if (!pg_tz_acceptable(&dir->tz))
462 : : {
463 : : /* Ignore leap-second zones */
464 : 0 : continue;
465 : : }
466 : :
467 : : /* OK, return the canonical zone name spelling. */
468 : 5980 : strlcpy(dir->tz.TZname, fullname + dir->baselen,
469 : : sizeof(dir->tz.TZname));
470 : :
471 : : /* Timezone loaded OK. */
472 : 5980 : return &dir->tz;
473 : : }
474 : :
475 : : /* Nothing more found */
476 : 10 : return NULL;
477 : : }
|