#include <fcntl.h>
#include <sys/stat.h>
#include <time.h>
+#include <unistd.h>
#include "pgtz.h"
* On most systems, we rely on trying to match the observable behavior of
* the C library's localtime() function. The database zone that matches
* furthest into the past is the one to use. Often there will be several
- * zones with identical rankings (since the Olson database assigns multiple
+ * zones with identical rankings (since the IANA database assigns multiple
* names to many zones). We break ties arbitrarily by preferring shorter,
* then alphabetically earlier zone names.
*
+ * Many modern systems use the IANA database, so if we can determine the
+ * system's idea of which zone it is using and its behavior matches our zone
+ * of the same name, we can skip the rather-expensive search through all the
+ * zones in our database. This short-circuit path also ensures that we spell
+ * the zone name the same way the system setting does, even in the presence
+ * of multiple aliases for the same zone.
+ *
* Win32's native knowledge about timezones appears to be too incomplete
- * and too different from the Olson database for the above matching strategy
+ * and too different from the IANA database for the above matching strategy
* to be of any use. But there is just a limited number of timezones
* available, so we can rely on a handmade mapping table instead.
*/
time_t test_times[MAX_TEST_TIMES];
};
+static bool check_system_link_file(const char *linkname, struct tztry *tt,
+ char *bestzonename);
static void scan_available_timezones(char *tzdir, char *tzdirsub,
struct tztry *tt,
int *bestscore, char *bestzonename);
return i;
}
+/*
+ * Test whether given zone name is a perfect match to localtime() behavior
+ */
+static bool
+perfect_timezone_match(const char *tzname, struct tztry *tt)
+{
+ return (score_timezone(tzname, tt) == tt->n_test_times);
+}
+
/*
* Try to identify a timezone name (in our terminology) that best 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.
+ * observed behavior of the system localtime() function.
*/
static const char *
identify_system_timezone(void)
* way of doing things, but experience has shown that system-supplied
* timezone definitions are likely to have DST behavior that is right for
* the recent past and not so accurate further back. Scoring in this way
- * allows us to recognize zones that have some commonality with the Olson
+ * allows us to recognize zones that have some commonality with the IANA
* database, without insisting on exact match. (Note: we probe Thursdays,
* not Sundays, to avoid triggering DST-transition bugs in localtime
* itself.)
tt.test_times[tt.n_test_times++] = t;
}
- /* Search for the best-matching timezone file */
+ /*
+ * Try to avoid the brute-force search by seeing if we can recognize the
+ * system's timezone setting directly.
+ *
+ * Currently we just check /etc/localtime; there are other conventions for
+ * this, but that seems to be the only one used on enough platforms to be
+ * worth troubling over.
+ */
+ if (check_system_link_file("/etc/localtime", &tt, resultbuf))
+ return resultbuf;
+
+ /* No luck, so search for the best-matching timezone file */
strlcpy(tmptzdir, pg_TZDIR(), sizeof(tmptzdir));
bestscore = -1;
resultbuf[0] = '\0';
&bestscore, resultbuf);
if (bestscore > 0)
{
- /* Ignore Olson's rather silly "Factory" zone; use GMT instead */
+ /* Ignore IANA's rather silly "Factory" zone; use GMT instead */
if (strcmp(resultbuf, "Factory") == 0)
return NULL;
return resultbuf;
/*
* Did not find the timezone. Fallback to use a GMT zone. Note that the
- * Olson timezone database names the GMT-offset zones in POSIX style: plus
+ * IANA 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...
*/
return resultbuf;
}
+/*
+ * Examine a system-provided symlink file to see if it tells us the timezone.
+ *
+ * Unfortunately, there is little standardization of how the system default
+ * timezone is determined in the absence of a TZ environment setting.
+ * But a common strategy is to create a symlink at a well-known place.
+ * If "linkname" identifies a readable symlink, and the tail of its contents
+ * matches a zone name we know, and the actual behavior of localtime() agrees
+ * with what we think that zone means, then we may use that zone name.
+ *
+ * We insist on a perfect behavioral match, which might not happen if the
+ * system has a different IANA database version than we do; but in that case
+ * it seems best to fall back to the brute-force search.
+ *
+ * linkname is the symlink file location to probe.
+ *
+ * tt tells about the system timezone behavior we need to match.
+ *
+ * If we successfully identify a zone name, store it in *bestzonename and
+ * return true; else return false. bestzonename must be a buffer of length
+ * TZ_STRLEN_MAX + 1.
+ */
+static bool
+check_system_link_file(const char *linkname, struct tztry *tt,
+ char *bestzonename)
+{
+#ifdef HAVE_READLINK
+ char link_target[MAXPGPATH];
+ int len;
+ const char *cur_name;
+
+ /*
+ * Try to read the symlink. If not there, not a symlink, etc etc, just
+ * quietly fail; the precise reason needn't concern us.
+ */
+ len = readlink(linkname, link_target, sizeof(link_target));
+ if (len < 0 || len >= sizeof(link_target))
+ return false;
+ link_target[len] = '\0';
+
+#ifdef DEBUG_IDENTIFY_TIMEZONE
+ fprintf(stderr, "symbolic link \"%s\" contains \"%s\"\n",
+ linkname, link_target);
+#endif
+
+ /*
+ * The symlink is probably of the form "/path/to/zones/zone/name", or
+ * possibly it is a relative path. Nobody puts their zone DB directly in
+ * the root directory, so we can definitely skip the first component; but
+ * after that it's trial-and-error to identify which path component begins
+ * the zone name.
+ */
+ cur_name = link_target;
+ while (*cur_name)
+ {
+ /* Advance to next segment of path */
+ cur_name = strchr(cur_name + 1, '/');
+ if (cur_name == NULL)
+ break;
+ /* If there are consecutive slashes, skip all, as the kernel would */
+ do
+ {
+ cur_name++;
+ } while (*cur_name == '/');
+
+ /*
+ * Test remainder of path to see if it is a matching zone name.
+ * Relative paths might contain ".."; we needn't bother testing if the
+ * first component is that. Also defend against overlength names.
+ */
+ if (*cur_name && *cur_name != '.' &&
+ strlen(cur_name) <= TZ_STRLEN_MAX &&
+ perfect_timezone_match(cur_name, tt))
+ {
+ /* Success! */
+ strcpy(bestzonename, cur_name);
+ return true;
+ }
+ }
+
+ /* Couldn't extract a matching zone name */
+ return false;
+#else
+ /* No symlinks? Forget it */
+ return false;
+#endif
+}
+
/*
* Recursively scan the timezone database looking for the best match to
* the system timezone behavior.
* HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time
* Zones on Windows 10 and Windows 7.
*
- * The zones have been matched to Olson timezones by looking at the cities
+ * The zones have been matched to IANA timezones by looking at the cities
* listed in the win32 display name (in the comment here) in most cases.
*/
{