summaryrefslogtreecommitdiff
path: root/src/timezone/pgtz.c
blob: 6d1af5f3a14d2db5d8c91c7a11ea803c1dbf2ddd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
/*-------------------------------------------------------------------------
 *
 * pgtz.c
 *	  Timezone Library Integration Functions
 *
 * Portions Copyright (c) 1996-2003, PostgreSQL Global Development Group
 *
 * IDENTIFICATION
 *	  $PostgreSQL: pgsql/src/timezone/pgtz.c,v 1.14 2004/05/24 02:30:29 tgl Exp $
 *
 *-------------------------------------------------------------------------
 */
#define NO_REDEFINE_TIMEFUNCS

#include "postgres.h"

#include <ctype.h>

#include "miscadmin.h"
#include "pgtime.h"
#include "pgtz.h"
#include "tzfile.h"
#include "utils/elog.h"
#include "utils/guc.h"


static char tzdir[MAXPGPATH];
static int	done_tzdir = 0;

char *
pg_TZDIR(void)
{
	if (done_tzdir)
		return tzdir;

	get_share_path(my_exec_path, tzdir);
	strcat(tzdir, "/timezone");

	done_tzdir = 1;
	return tzdir;
}

/*
 * Try to determine the system timezone (as opposed to the timezone
 * set in our own library).
 */
#define T_DAY	((time_t) (60*60*24))
#define T_MONTH ((time_t) (60*60*24*31))

struct tztry
{
	char		std_zone_name[TZ_STRLEN_MAX + 1],
				dst_zone_name[TZ_STRLEN_MAX + 1];
#define MAX_TEST_TIMES 5
	int			n_test_times;
	time_t		test_times[MAX_TEST_TIMES];
};


static bool
compare_tm(struct tm *s, struct pg_tm *p)
{
	if (s->tm_sec != p->tm_sec ||
		s->tm_min != p->tm_min ||
		s->tm_hour != p->tm_hour ||
		s->tm_mday != p->tm_mday ||
		s->tm_mon != p->tm_mon ||
		s->tm_year != p->tm_year ||
		s->tm_wday != p->tm_wday ||
		s->tm_yday != p->tm_yday ||
		s->tm_isdst != p->tm_isdst)
		return false;
	return true;
}

static bool
try_timezone(char *tzname, struct tztry *tt)
{
	int			i;
	struct tm	   *systm;
	struct pg_tm   *pgtm;

	if (!pg_tzset(tzname))
		return false;			/* can't handle the TZ name at all */

	/* Check for match at all the test times */
	for (i = 0; i < tt->n_test_times; i++)
	{
		pgtm = pg_localtime(&(tt->test_times[i]));
		if (!pgtm)
			return false;		/* probably shouldn't happen */
		systm = localtime(&(tt->test_times[i]));
		if (!compare_tm(systm, pgtm))
			return false;
	}

	return true;
}

static int
get_timezone_offset(struct tm *tm)
{
#if defined(HAVE_STRUCT_TM_TM_ZONE)
	return tm->tm_gmtoff;
#elif defined(HAVE_INT_TIMEZONE)
#ifdef HAVE_UNDERSCORE_TIMEZONE
	return -_timezone;
#else
	return -timezone;
#endif
#else
#error No way to determine TZ? Can this happen?
#endif
}


#ifdef WIN32
#define TZABBREV(tz) win32_get_timezone_abbrev(tz)

static char *
win32_get_timezone_abbrev(char *tz)
{
	static char w32tzabbr[TZ_STRLEN_MAX + 1];
	int			l = 0;
	char	   *c;

	for (c = tz; *c; c++)
	{
		if (isupper(*c))
			w32tzabbr[l++] = *c;
	}
	w32tzabbr[l] = '\0';
	return w32tzabbr;
}

#else
#define TZABBREV(tz) tz
#endif


/*
 * Try to identify a timezone name (in our terminology) that matches the
 * observed behavior of the system timezone library.  We cannot assume that
 * the system TZ environment setting (if indeed there is one) matches our
 * terminology, so we ignore it and just look at what localtime() returns.
 */
static char *
identify_system_timezone(void)
{
	static char resultbuf[TZ_STRLEN_MAX + 1];
	time_t		tnow;
	time_t		t;
	int			nowisdst,
				curisdst;
	int			std_ofs = 0;
	struct tztry tt;
	struct tm  *tm;
	char		cbuf[TZ_STRLEN_MAX + 1];

	/* Initialize OS timezone library */
	tzset();

	/* No info yet */
	memset(&tt, 0, sizeof(tt));

	/*
	 * The idea here is to scan forward from today and try to locate the
	 * next two daylight-savings transition boundaries.  We will test for
	 * correct results on the day before and after each boundary; this
	 * gives at least some confidence that we've selected the right DST
	 * rule set.
	 */
	tnow = time(NULL);

	/*
	 * Round back to a GMT midnight so results don't depend on local time
	 * of day
	 */
	tnow -= (tnow % T_DAY);

	/* Always test today, so we have at least one test point */
	tt.test_times[tt.n_test_times++] = tnow;

	tm = localtime(&tnow);
	nowisdst = tm->tm_isdst;
	curisdst = nowisdst;

	if (curisdst == 0)
	{
		/* Set up STD zone name, in case we are in a non-DST zone */
		memset(cbuf, 0, sizeof(cbuf));
		strftime(cbuf, sizeof(cbuf) - 1, "%Z", tm); /* zone abbr */
		strcpy(tt.std_zone_name, TZABBREV(cbuf));
		/* Also preset std_ofs */
		std_ofs = get_timezone_offset(tm);
	}

	/*
	 * We have to look a little further ahead than one year, in case today
	 * is just past a DST boundary that falls earlier in the year than the
	 * next similar boundary.  Arbitrarily scan up to 14 months.
	 */
	for (t = tnow + T_DAY; t < tnow + T_MONTH * 14; t += T_DAY)
	{
		tm = localtime(&t);
		if (tm->tm_isdst >= 0 && tm->tm_isdst != curisdst)
		{
			/* Found a boundary */
			tt.test_times[tt.n_test_times++] = t - T_DAY;
			tt.test_times[tt.n_test_times++] = t;
			curisdst = tm->tm_isdst;
			/* Save STD or DST zone name, also std_ofs */
			memset(cbuf, 0, sizeof(cbuf));
			strftime(cbuf, sizeof(cbuf) - 1, "%Z", tm); /* zone abbr */
			if (curisdst == 0)
			{
				strcpy(tt.std_zone_name, TZABBREV(cbuf));
				std_ofs = get_timezone_offset(tm);
			}
			else
				strcpy(tt.dst_zone_name, TZABBREV(cbuf));
			/* Have we found two boundaries? */
			if (tt.n_test_times >= 5)
				break;
		}
	}

	/* We should have found a STD zone name by now... */
	if (tt.std_zone_name[0] == '\0')
	{
		ereport(LOG,
				(errmsg("unable to determine system timezone, defaulting to \"%s\"", "GMT"),
				 errhint("You can specify the correct timezone in postgresql.conf.")));
		return NULL;			/* go to GMT */
	}

	/* If we found DST too then try STD<ofs>DST */
	if (tt.dst_zone_name[0] != '\0')
	{
		snprintf(resultbuf, sizeof(resultbuf), "%s%d%s",
				 tt.std_zone_name, -std_ofs / 3600, tt.dst_zone_name);
		if (try_timezone(resultbuf, &tt))
			return resultbuf;
	}

	/* Try just the STD timezone (works for GMT at least) */
	strcpy(resultbuf, tt.std_zone_name);
	if (try_timezone(resultbuf, &tt))
		return resultbuf;

	/* Try STD<ofs> */
	snprintf(resultbuf, sizeof(resultbuf), "%s%d",
			 tt.std_zone_name, -std_ofs / 3600);
	if (try_timezone(resultbuf, &tt))
		return resultbuf;

	/*
	 * Did not find the timezone.  Fallback to use a GMT zone.  Note that the
	 * zic timezone database names the GMT-offset zones in POSIX style: plus
	 * is west of Greenwich.  It's unfortunate that this is opposite of SQL
	 * conventions.  Should we therefore change the names?  Probably not...
	 */
	snprintf(resultbuf, sizeof(resultbuf), "Etc/GMT%s%d",
			(-std_ofs > 0) ? "+" : "", -std_ofs / 3600);

	ereport(LOG,
			(errmsg("could not recognize system timezone, defaulting to \"%s\"",
					resultbuf),
			 errhint("You can specify the correct timezone in postgresql.conf.")));
	return resultbuf;
}

/*
 * Check whether timezone is acceptable.
 *
 * What we are doing here is checking for leap-second-aware timekeeping.
 * We need to reject such TZ settings because they'll wreak havoc with our
 * date/time arithmetic.
 *
 * NB: this must NOT ereport(ERROR).  The caller must get control back so that
 * it can restore the old value of TZ if we don't like the new one.
 */
bool
tz_acceptable(void)
{
	struct pg_tm tt;
	time_t		time2000;

	/*
	 * To detect leap-second timekeeping, compute the time_t value for
	 * local midnight, 2000-01-01.	Insist that this be a multiple of 60;
	 * any partial-minute offset has to be due to leap seconds.
	 */
	MemSet(&tt, 0, sizeof(tt));
	tt.tm_year = 100;
	tt.tm_mon = 0;
	tt.tm_mday = 1;
	tt.tm_isdst = -1;
	time2000 = pg_mktime(&tt);
	if ((time2000 % 60) != 0)
		return false;

	return true;
}

/*
 * Identify a suitable default timezone setting based on the environment,
 * and make it active.
 *
 * We first look to the TZ environment variable.  If not found or not
 * recognized by our own code, we see if we can identify the timezone
 * from the behavior of the system timezone library.  When all else fails,
 * fall back to GMT.
 */
const char *
select_default_timezone(void)
{
	char	   *def_tz;

	def_tz = getenv("TZ");
	if (def_tz && pg_tzset(def_tz) && tz_acceptable())
		return def_tz;

	def_tz = identify_system_timezone();
	if (def_tz && pg_tzset(def_tz) && tz_acceptable())
		return def_tz;

	if (pg_tzset("GMT") && tz_acceptable())
		return "GMT";

	ereport(FATAL,
			(errmsg("could not select a suitable default timezone"),
			 errdetail("It appears that your GMT time zone uses leap seconds. PostgreSQL does not support leap seconds.")));
	return NULL;				/* keep compiler quiet */
}

/*
 * Initialize timezone library
 *
 * This is called after initial loading of postgresql.conf.  If no TimeZone
 * setting was found therein, we try to derive one from the environment.
 */
void
pg_timezone_initialize(void)
{
	/* Do we need to try to figure the timezone? */
	if (pg_strcasecmp(GetConfigOption("timezone"), "UNKNOWN") == 0)
	{
		const char *def_tz;

		/* Select setting */
		def_tz = select_default_timezone();
		/* Tell GUC about the value. Will redundantly call pg_tzset() */
		SetConfigOption("timezone", def_tz, PGC_POSTMASTER, PGC_S_ENV_VAR);
	}
}