From 76c50c080bfd63dd26b92f0ddf5fede704bcba04 Mon Sep 17 00:00:00 2001
From: Tom Lane <tgl@sss.pgh.pa.us>
Date: Tue, 25 May 2004 18:08:59 +0000
Subject: [PATCH] Add code to identify_system_timezone() to try all zones in
 the zic database, not just ones that we cons up POSIX names for.  This looks
 grim but it seems to take less than a second even on a relatively slow
 machine, and since it only happens once during postmaster startup, that seems
 acceptable.

---
 src/timezone/pgtz.c | 271 +++++++++++++++++++++++++++++++++++---------
 1 file changed, 216 insertions(+), 55 deletions(-)

diff --git a/src/timezone/pgtz.c b/src/timezone/pgtz.c
index 6d1af5f3a14..77a0533ab33 100644
--- a/src/timezone/pgtz.c
+++ b/src/timezone/pgtz.c
@@ -6,7 +6,7 @@
  * 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 $
+ *	  $PostgreSQL: pgsql/src/timezone/pgtz.c,v 1.15 2004/05/25 18:08:59 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -15,18 +15,39 @@
 #include "postgres.h"
 
 #include <ctype.h>
+#include <sys/stat.h>
 
 #include "miscadmin.h"
 #include "pgtime.h"
 #include "pgtz.h"
+#include "storage/fd.h"
 #include "tzfile.h"
 #include "utils/elog.h"
 #include "utils/guc.h"
 
 
+#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 10
+	int			n_test_times;
+	time_t		test_times[MAX_TEST_TIMES];
+};
+
 static char tzdir[MAXPGPATH];
 static int	done_tzdir = 0;
 
+static bool scan_available_timezones(char *tzdir, char *tzdirsub,
+									 struct tztry *tt);
+
+
+/*
+ * Return full pathname of timezone data directory
+ */
 char *
 pg_TZDIR(void)
 {
@@ -41,22 +62,69 @@ pg_TZDIR(void)
 }
 
 /*
- * Try to determine the system timezone (as opposed to the timezone
- * set in our own library).
+ * Get GMT offset from a system struct tm
  */
-#define T_DAY	((time_t) (60*60*24))
-#define T_MONTH ((time_t) (60*60*24*31))
+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
+}
 
-struct tztry
+/*
+ * Grotty kluge for win32 ... do we really need this?
+ */
+#ifdef WIN32
+#define TZABBREV(tz) win32_get_timezone_abbrev(tz)
+
+static char *
+win32_get_timezone_abbrev(const char *tz)
 {
-	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 char w32tzabbr[TZ_STRLEN_MAX + 1];
+	int			l = 0;
+	const char  *c;
+
+	for (c = tz; *c; c++)
+	{
+		if (isupper((unsigned char) *c))
+			w32tzabbr[l++] = *c;
+	}
+	w32tzabbr[l] = '\0';
+	return w32tzabbr;
+}
+
+#else
+#define TZABBREV(tz) (tz)
+#endif
+
+/*
+ * Convenience subroutine to convert y/m/d to time_t
+ */
+static time_t
+build_time_t(int year, int month, int day)
+{
+	struct tm	tm;
 
+	memset(&tm, 0, sizeof(tm));
+	tm.tm_mday = day;
+	tm.tm_mon = month - 1;
+	tm.tm_year = year - 1900;
+
+	return mktime(&tm);
+}
 
+/*
+ * Does a system tm value match one we computed ourselves?
+ */
 static bool
 compare_tm(struct tm *s, struct pg_tm *p)
 {
@@ -73,12 +141,16 @@ compare_tm(struct tm *s, struct pg_tm *p)
 	return true;
 }
 
+/*
+ * See if a specific timezone setting matches the system behavior
+ */
 static bool
-try_timezone(char *tzname, struct tztry *tt)
+try_timezone(const char *tzname, struct tztry *tt)
 {
 	int			i;
 	struct tm	   *systm;
 	struct pg_tm   *pgtm;
+	char		cbuf[TZ_STRLEN_MAX + 1];
 
 	if (!pg_tzset(tzname))
 		return false;			/* can't handle the TZ name at all */
@@ -92,51 +164,25 @@ try_timezone(char *tzname, struct tztry *tt)
 		systm = localtime(&(tt->test_times[i]));
 		if (!compare_tm(systm, pgtm))
 			return false;
+		if (systm->tm_isdst >= 0)
+		{
+			/* Check match of zone names, too */
+			if (pgtm->tm_zone == NULL)
+				return false;
+			memset(cbuf, 0, sizeof(cbuf));
+			strftime(cbuf, sizeof(cbuf) - 1, "%Z", systm); /* zone abbr */
+			if (strcmp(TZABBREV(cbuf), pgtm->tm_zone) != 0)
+				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;
+	/* Reject if leap seconds involved */
+	if (!tz_acceptable())
+		return false;
 
-	for (c = tz; *c; c++)
-	{
-		if (isupper(*c))
-			w32tzabbr[l++] = *c;
-	}
-	w32tzabbr[l] = '\0';
-	return w32tzabbr;
+	return true;
 }
 
-#else
-#define TZABBREV(tz) tz
-#endif
-
 
 /*
  * Try to identify a timezone name (in our terminology) that matches the
@@ -155,6 +201,7 @@ identify_system_timezone(void)
 	int			std_ofs = 0;
 	struct tztry tt;
 	struct tm  *tm;
+	char		tmptzdir[MAXPGPATH];
 	char		cbuf[TZ_STRLEN_MAX + 1];
 
 	/* Initialize OS timezone library */
@@ -225,6 +272,20 @@ identify_system_timezone(void)
 		}
 	}
 
+	/*
+	 * Add a couple of historical dates as well; without this we are likely
+	 * to choose an accidental match, such as Antartica/Palmer when we
+	 * really want America/Santiago.  Ideally we'd probe some dates before
+	 * 1970 too, but that is guaranteed to fail if the system TZ library
+	 * doesn't cope with DST before 1970.
+	 */
+	tt.test_times[tt.n_test_times++] = build_time_t(1970, 1, 15);
+	tt.test_times[tt.n_test_times++] = build_time_t(1970, 7, 15);
+	tt.test_times[tt.n_test_times++] = build_time_t(1990, 4, 1);
+	tt.test_times[tt.n_test_times++] = build_time_t(1990, 10, 1);
+
+	Assert(tt.n_test_times <= MAX_TEST_TIMES);
+
 	/* We should have found a STD zone name by now... */
 	if (tt.std_zone_name[0] == '\0')
 	{
@@ -234,7 +295,17 @@ identify_system_timezone(void)
 		return NULL;			/* go to GMT */
 	}
 
-	/* If we found DST too then try STD<ofs>DST */
+	/* Search for a matching timezone file */
+	strcpy(tmptzdir, pg_TZDIR());
+	if (scan_available_timezones(tmptzdir,
+								 tmptzdir + strlen(tmptzdir) + 1,
+								 &tt))
+	{
+		StrNCpy(resultbuf, pg_get_current_timezone(), sizeof(resultbuf));
+		return resultbuf;
+	}
+
+	/* If we found DST then try STD<ofs>DST */
 	if (tt.dst_zone_name[0] != '\0')
 	{
 		snprintf(resultbuf, sizeof(resultbuf), "%s%d%s",
@@ -270,6 +341,96 @@ identify_system_timezone(void)
 	return resultbuf;
 }
 
+/*
+ * Recursively scan the timezone database looking for a usable match to
+ * the system timezone behavior.
+ *
+ * tzdir points to a buffer of size MAXPGPATH.  On entry, it holds the
+ * pathname of a directory containing TZ files.  We internally modify it
+ * to hold pathnames of sub-directories and files, but must restore it
+ * to its original contents before exit.
+ *
+ * tzdirsub points to the part of tzdir that represents the subfile name
+ * (ie, tzdir + the original directory name length, plus one for the
+ * first added '/').
+ *
+ * tt tells about the system timezone behavior we need to match.
+ *
+ * On success, returns TRUE leaving the proper timezone selected.
+ * On failure, returns FALSE with a random timezone selected.
+ */
+static bool
+scan_available_timezones(char *tzdir, char *tzdirsub, struct tztry *tt)
+{
+	int			tzdir_orig_len = strlen(tzdir);
+	bool		found = false;
+	DIR		   *dirdesc;
+
+	dirdesc = AllocateDir(tzdir);
+	if (!dirdesc)
+	{
+		ereport(LOG,
+				(errcode_for_file_access(),
+				 errmsg("could not open directory \"%s\": %m", tzdir)));
+		return false;
+	}
+
+	for (;;)
+	{
+		struct dirent *direntry;
+		struct stat statbuf;
+
+		errno = 0;
+		direntry = readdir(dirdesc);
+		if (!direntry)
+		{
+			if (errno)
+				ereport(LOG,
+						(errcode_for_file_access(),
+						 errmsg("error reading directory: %m")));
+			break;
+		}
+
+		/* Ignore . and .., plus any other "hidden" files */
+		if (direntry->d_name[0] == '.')
+			continue;
+
+		snprintf(tzdir + tzdir_orig_len, MAXPGPATH - tzdir_orig_len,
+				 "/%s", direntry->d_name);
+
+		if (stat(tzdir, &statbuf) != 0)
+		{
+			ereport(LOG,
+					(errcode_for_file_access(),
+					 errmsg("could not stat \"%s\": %m", tzdir)));
+			continue;
+		}
+
+		if (S_ISDIR(statbuf.st_mode))
+		{
+			/* Recurse into subdirectory */
+			found = scan_available_timezones(tzdir, tzdirsub, tt);
+			if (found)
+				break;
+		}
+		else
+		{
+			/* Load and test this file */
+			found = try_timezone(tzdirsub, tt);
+			if (found)
+				break;
+		}
+	}
+
+	FreeDir(dirdesc);
+
+	/* Restore tzdir */
+	tzdir[tzdir_orig_len] = '\0';
+
+	return found;
+}
+
+
 /*
  * Check whether timezone is acceptable.
  *
@@ -351,6 +512,6 @@ pg_timezone_initialize(void)
 		/* 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);
+		SetConfigOption("timezone", def_tz, PGC_POSTMASTER, PGC_S_ARGV);
 	}
 }
-- 
GitLab