summaryrefslogtreecommitdiffstats
path: root/kdecore/ktimezones.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'kdecore/ktimezones.cpp')
-rw-r--r--kdecore/ktimezones.cpp790
1 files changed, 790 insertions, 0 deletions
diff --git a/kdecore/ktimezones.cpp b/kdecore/ktimezones.cpp
new file mode 100644
index 000000000..c74dc8143
--- /dev/null
+++ b/kdecore/ktimezones.cpp
@@ -0,0 +1,790 @@
+/*
+ This file is part of the KDE libraries
+ Copyright (c) 2005 S.R.Haque <srhaque@iee.org>.
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Library General Public
+ License version 2 as published by the Free Software Foundation.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Library General Public License for more details.
+
+ You should have received a copy of the GNU Library General Public License
+ along with this library; see the file COPYING.LIB. If not, write to
+ the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ Boston, MA 02110-1301, USA.
+*/
+
+#include <config.h>
+
+#include <ktimezones.h>
+#include <kdebug.h>
+#include <kmdcodec.h>
+#include <kprocess.h>
+#include <kstringhandler.h>
+#include <ktempfile.h>
+
+#include <qdatetime.h>
+#include <qfile.h>
+#include <qregexp.h>
+#include <qstringlist.h>
+#include <qtextstream.h>
+
+#include <cerrno>
+#include <climits>
+#include <cstdlib>
+#include <cstring>
+#include <ctime>
+
+#define UTC_ZONE "UTC"
+
+/**
+ * Find out if the given standard (e.g. "GMT") and daylight savings time
+ * (e.g. "BST", but which may be empty) abbreviated timezone names match
+ * this timezone.
+ *
+ * Thus, this class can be used as a heuristic when trying to lookup the
+ * real timezone from the abbreviated zone names.
+ */
+class AbbreviationsMatch :
+ public KTimezoneDetails
+{
+public:
+ AbbreviationsMatch(const QString &stdZone, const QString &dstZone = "")
+ {
+ m_stdZone = stdZone;
+ m_dstZone = dstZone;
+ }
+
+ void parseStarted()
+ {
+ m_foundStd = false;
+ m_foundDst = m_dstZone.isEmpty();
+ }
+
+ bool test()
+ {
+ return (m_foundStd && m_foundDst);
+ }
+
+private:
+ bool m_foundStd;
+ bool m_foundDst;
+ QString m_stdZone;
+ QString m_dstZone;
+
+ virtual void gotAbbreviation(int /*index*/, const QString &value)
+ {
+ if (m_stdZone == value)
+ {
+ m_foundStd = true;
+ }
+ if (m_dstZone == value)
+ {
+ m_foundDst = true;
+ }
+ }
+};
+
+/**
+ * Internal dummy source for UTC timezone.
+ */
+class DummySource :
+ public KTimezoneSource
+{
+public:
+ DummySource() :
+ KTimezoneSource("")
+ {
+ }
+
+ virtual bool parse(const QString &/*zone*/, KTimezoneDetails &/*dataReceiver*/) const
+ {
+ return true;
+ }
+};
+
+/**
+ * Find offset at a particular point in time.
+ */
+class OffsetFind :
+ public KTimezoneDetails
+{
+public:
+ OffsetFind(unsigned dateTime)
+ {
+ m_dateTime = dateTime;
+ }
+
+ void parseStarted()
+ {
+ m_transitionTimeIndex = 0;
+ m_localTimeIndex = -1;
+ m_abbrIndex = -1;
+ m_offset = 0;
+ m_isDst = false;
+ m_abbr = UTC_ZONE;
+ }
+
+ int offset()
+ {
+ return m_offset;
+ }
+
+ bool isDst()
+ {
+ return m_isDst;
+ }
+
+ QString abbreviation()
+ {
+ return m_abbr;
+ }
+
+private:
+ unsigned m_dateTime;
+ int m_transitionTimeIndex;
+ int m_localTimeIndex;
+ int m_abbrIndex;
+ int m_offset;
+ bool m_isDst;
+ QString m_abbr;
+
+ virtual void gotTransitionTime(int index, unsigned transitionTime)
+ {
+ if (transitionTime <= m_dateTime)
+ {
+ // Remember the index of the transition time that relates to dateTime.
+ m_transitionTimeIndex = index;
+ }
+ }
+
+ virtual void gotLocalTimeIndex(int index, unsigned localTimeIndex)
+ {
+ if (index == m_transitionTimeIndex)
+ {
+ // Remember the index of the local time that relates to dateTime.
+ m_localTimeIndex = localTimeIndex;
+ }
+ }
+
+ virtual void gotLocalTime(int index, int gmtOff, bool isDst, unsigned abbrInd)
+ {
+ if (index == m_localTimeIndex)
+ {
+ // Remember the results that relate to gmtOffset.
+ m_offset = gmtOff;
+ m_isDst = isDst;
+ m_abbrIndex = abbrInd;
+ }
+ }
+
+ virtual void gotAbbreviation(int index, const QString &value)
+ {
+ if (index == m_abbrIndex)
+ {
+ m_abbr = value;
+ }
+ }
+};
+
+const float KTimezone::UNKNOWN = 1000.0;
+
+bool KTimezone::isValidLatitude(float latitude)
+{
+ return (latitude >= -90.0) && (latitude <= 90.0);
+}
+
+bool KTimezone::isValidLongitude(float longitude)
+{
+ return (longitude >= -180.0) && (longitude <= 180.0);
+}
+
+KTimezone::KTimezone(
+ KSharedPtr<KTimezoneSource> db, const QString& name,
+ const QString &countryCode, float latitude, float longitude,
+ const QString &comment) :
+ m_db(db),
+ m_name(name),
+ m_countryCode(countryCode),
+ m_latitude(latitude),
+ m_longitude(longitude),
+ m_comment(comment),
+ d(0)
+{
+ // Detect duff values.
+ if (m_latitude * m_latitude > 90 * 90)
+ m_latitude = UNKNOWN;
+ if (m_longitude * m_longitude > 180 * 180)
+ m_longitude = UNKNOWN;
+}
+
+KTimezone::~KTimezone()
+{
+ // FIXME when needed:
+ // delete d;
+}
+
+QString KTimezone::comment() const
+{
+ return m_comment;
+}
+
+QDateTime KTimezone::convert(const KTimezone *newZone, const QDateTime &dateTime) const
+{
+ char *originalZone = ::getenv("TZ");
+
+ // Convert the given localtime to UTC.
+ ::setenv("TZ", m_name.utf8(), 1);
+ tzset();
+ unsigned utc = dateTime.toTime_t();
+
+ // Set the timezone and convert UTC to localtime.
+ ::setenv("TZ", newZone->name().utf8(), 1);
+ tzset();
+ QDateTime remoteTime;
+ remoteTime.setTime_t(utc, Qt::LocalTime);
+
+ // Now restore things
+ if (!originalZone)
+ {
+ ::unsetenv("TZ");
+ }
+ else
+ {
+ ::setenv("TZ", originalZone, 1);
+ }
+ tzset();
+ return remoteTime;
+}
+
+QString KTimezone::countryCode() const
+{
+ return m_countryCode;
+}
+
+float KTimezone::latitude() const
+{
+ return m_latitude;
+}
+
+float KTimezone::longitude() const
+{
+ return m_longitude;
+}
+
+QString KTimezone::name() const
+{
+ return m_name;
+}
+
+int KTimezone::offset(Qt::TimeSpec basisSpec) const
+{
+ char *originalZone = ::getenv("TZ");
+
+ // Get the time in the current timezone.
+ QDateTime basisTime = QDateTime::currentDateTime(basisSpec);
+
+ // Set the timezone and find out what time it is there compared to the basis.
+ ::setenv("TZ", m_name.utf8(), 1);
+ tzset();
+ QDateTime remoteTime = QDateTime::currentDateTime(Qt::LocalTime);
+ int offset = remoteTime.secsTo(basisTime);
+
+ // Now restore things
+ if (!originalZone)
+ {
+ ::unsetenv("TZ");
+ }
+ else
+ {
+ ::setenv("TZ", originalZone, 1);
+ }
+ tzset();
+ return offset;
+}
+
+int KTimezone::offset(const QDateTime &dateTime) const
+{
+ OffsetFind finder(dateTime.toTime_t());
+ int result = 0;
+ if (parse(finder))
+ {
+ result = finder.offset();
+ }
+ return result;
+}
+
+bool KTimezone::parse(KTimezoneDetails &dataReceiver) const
+{
+ dataReceiver.parseStarted();
+ bool result = m_db->parse(m_name, dataReceiver);
+ dataReceiver.parseEnded();
+ return result;
+}
+
+KTimezones::KTimezones() :
+ m_zoneinfoDir(),
+ m_zones(0),
+ d(0)
+{
+ // Create the database (and resolve m_zoneinfoDir!).
+ allZones();
+ m_UTC = new KTimezone(new DummySource(), UTC_ZONE);
+ add(m_UTC);
+}
+
+KTimezones::~KTimezones()
+{
+ // FIXME when needed:
+ // delete d;
+
+ // Autodelete behavior.
+ if (m_zones)
+ {
+ for (ZoneMap::ConstIterator it = m_zones->begin(); it != m_zones->end(); ++it)
+ {
+ delete it.data();
+ }
+ }
+ delete m_zones;
+}
+
+void KTimezones::add(KTimezone *zone)
+{
+ m_zones->insert(zone->name(), zone);
+}
+
+const KTimezones::ZoneMap KTimezones::allZones()
+{
+ // Have we already done all the hard work? If not, create the cache.
+ if (m_zones)
+ return *m_zones;
+ m_zones = new ZoneMap();
+
+ // Go read the database.
+ //
+ // On Windows, HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones
+ // is the place to look. The TZI binary value is the TIME_ZONE_INFORMATION structure.
+ //
+ // For Unix its all easy except knowing where to look. Try the LSB location first.
+ QFile f;
+ m_zoneinfoDir = "/usr/share/zoneinfo";
+ f.setName(m_zoneinfoDir + "/zone.tab");
+ if (!f.open(IO_ReadOnly))
+ {
+ kdDebug() << "Can't open " << f.name() << endl;
+ m_zoneinfoDir = "/usr/lib/zoneinfo";
+ f.setName(m_zoneinfoDir + "/zone.tab");
+ if (!f.open(IO_ReadOnly))
+ {
+ kdDebug() << "Can't open " << f.name() << endl;
+ m_zoneinfoDir = ::getenv("TZDIR");
+ f.setName(m_zoneinfoDir + "/zone.tab");
+ if (m_zoneinfoDir.isEmpty() || !f.open(IO_ReadOnly))
+ {
+ kdDebug() << "Can't open " << f.name() << endl;
+
+ // Solaris support. Synthesise something that looks like a zone.tab.
+ //
+ // /bin/grep -h ^Zone /usr/share/lib/zoneinfo/src/* | /bin/awk '{print "??\t+9999+99999\t" $2}'
+ //
+ // where the country code is set to "??" and the lattitude/longitude
+ // values are dummies.
+ m_zoneinfoDir = "/usr/share/lib/zoneinfo";
+ KTempFile temp;
+ KShellProcess reader;
+ reader << "/bin/grep" << "-h" << "^Zone" << m_zoneinfoDir << "/src/*" << temp.name() << "|" <<
+ "/bin/awk" << "'{print \"??\\t+9999+99999\\t\" $2}'";
+ // Note the use of blocking here...it is a trivial amount of data!
+ temp.close();
+ reader.start(KProcess::Block);
+ f.setName(temp.name());
+ if (!temp.status() || !f.open(IO_ReadOnly))
+ {
+ kdDebug() << "Can't open " << f.name() << endl;
+ return *m_zones;
+ }
+ }
+ }
+ }
+
+ // Parse the zone.tab.
+ QTextStream str(&f);
+ QRegExp lineSeparator("[ \t]");
+ QRegExp ordinateSeparator("[+-]");
+ KSharedPtr<KTimezoneSource> db(new KTimezoneSource(m_zoneinfoDir));
+ while (!str.atEnd())
+ {
+ QString line = str.readLine();
+ if (line.isEmpty() || '#' == line[0])
+ continue;
+ QStringList tokens = KStringHandler::perlSplit(lineSeparator, line, 4);
+ if (tokens.count() < 3)
+ {
+ kdError() << "invalid record: " << line << endl;
+ continue;
+ }
+
+ // Got three tokens. Now check for two ordinates plus first one is "".
+ QStringList ordinates = KStringHandler::perlSplit(ordinateSeparator, tokens[1], 2);
+ if (ordinates.count() < 2)
+ {
+ kdError() << "invalid coordinates: " << tokens[1] << endl;
+ continue;
+ }
+
+ float latitude = convertCoordinate(ordinates[1]);
+ float longitude = convertCoordinate(ordinates[2]);
+
+ // Add entry to list.
+ if (tokens[0] == "??")
+ tokens[0] = "";
+ KTimezone *timezone = new KTimezone(db, tokens[2], tokens[0], latitude, longitude, tokens[3]);
+ add(timezone);
+ }
+ f.close();
+ return *m_zones;
+}
+
+/**
+ * Convert sHHMM or sHHMMSS to a floating point number of degrees.
+ */
+float KTimezones::convertCoordinate(const QString &coordinate)
+{
+ int value = coordinate.toInt();
+ int degrees = 0;
+ int minutes = 0;
+ int seconds = 0;
+
+ if (coordinate.length() > 11)
+ {
+ degrees = value / 10000;
+ value -= degrees * 10000;
+ minutes = value / 100;
+ value -= minutes * 100;
+ seconds = value;
+ }
+ else
+ {
+ degrees = value / 100;
+ value -= degrees * 100;
+ minutes = value;
+ }
+ value = degrees * 3600 + minutes * 60 + seconds;
+ return value / 3600.0;
+}
+
+const KTimezone *KTimezones::local()
+{
+ const KTimezone *local = 0;
+
+ // First try the simplest solution of checking for well-formed TZ setting.
+ char *envZone = ::getenv("TZ");
+ if (envZone)
+ {
+ if (envZone[0] == '\0')
+ {
+ return m_UTC;
+ }
+ else
+ if (envZone[0] == ':')
+ {
+ envZone++;
+ }
+ local = zone(envZone);
+ }
+ if (local)
+ return local;
+
+ // Try to match /etc/localtime against the list of zoneinfo files.
+ QFile f;
+ f.setName("/etc/localtime");
+ if (f.open(IO_ReadOnly))
+ {
+ // Compute the MD5 sum of /etc/localtime.
+ KMD5 context("");
+ context.reset();
+ context.update(f);
+ QIODevice::Offset referenceSize = f.size();
+ QString referenceMd5Sum = context.hexDigest();
+ f.close();
+ if (!m_zoneinfoDir.isEmpty())
+ {
+ // Compare it with each zoneinfo file.
+ for (ZoneMap::Iterator it = m_zones->begin(); it != m_zones->end(); ++it)
+ {
+ KTimezone *zone = it.data();
+ f.setName(m_zoneinfoDir + '/' + zone->name());
+ if (f.open(IO_ReadOnly))
+ {
+ QIODevice::Offset candidateSize = f.size();
+ QString candidateMd5Sum;
+ if (candidateSize == referenceSize)
+ {
+ // Only do the heavy lifting for file sizes which match.
+ context.reset();
+ context.update(f);
+ candidateMd5Sum = context.hexDigest();
+ }
+ f.close();
+ if (candidateMd5Sum == referenceMd5Sum)
+ {
+ // kdError() << "local=" << zone->name() << endl;
+ local = zone;
+ break;
+ }
+ }
+ }
+ }
+ }
+ if (local)
+ return local;
+
+ // BSD support.
+ QString fileZone;
+ f.setName("/etc/timezone");
+ if (!f.open(IO_ReadOnly))
+ {
+ kdDebug() << "Can't open " << f.name() << endl;
+
+ // Solaris support using /etc/default/init.
+ f.setName("/etc/default/init");
+ if (!f.open(IO_ReadOnly))
+ {
+ kdDebug() << "Can't open " << f.name() << endl;
+ }
+ else
+ {
+ QTextStream ts(&f);
+ ts.setEncoding(QTextStream::Latin1);
+
+ // Read the last line starting "TZ=".
+ while (!ts.atEnd())
+ {
+ fileZone = ts.readLine();
+ if (fileZone.startsWith("TZ="))
+ {
+ fileZone = fileZone.mid(3);
+
+ // kdError() << "local=" << fileZone << endl;
+ local = zone(fileZone);
+ }
+ }
+ f.close();
+ }
+ }
+ else
+ {
+ QTextStream ts(&f);
+ ts.setEncoding(QTextStream::Latin1);
+
+ // Read the first line.
+ if (!ts.atEnd())
+ {
+ fileZone = ts.readLine();
+
+ // kdError() << "local=" << fileZone << endl;
+ local = zone(fileZone);
+ }
+ f.close();
+ }
+ if (local)
+ return local;
+
+ // None of the deterministic stuff above has worked: try a heuristic. We
+ // try to find a pair of matching timezone abbreviations...that way, we'll
+ // likely return a value in the user's own country.
+ if (!m_zoneinfoDir.isEmpty())
+ {
+ tzset();
+ AbbreviationsMatch matcher(tzname[0], tzname[1]);
+ int bestOffset = INT_MAX;
+ for (ZoneMap::Iterator it = m_zones->begin(); it != m_zones->end(); ++it)
+ {
+ KTimezone *zone = it.data();
+ int candidateOffset = QABS(zone->offset(Qt::LocalTime));
+ if (zone->parse(matcher) && matcher.test() && (candidateOffset < bestOffset))
+ {
+ // kdError() << "local=" << zone->name() << endl;
+ bestOffset = candidateOffset;
+ local = zone;
+ }
+ }
+ }
+ if (local)
+ return local;
+ return m_UTC;
+}
+
+const KTimezone *KTimezones::zone(const QString &name)
+{
+ if (name.isEmpty())
+ return m_UTC;
+ ZoneMap::ConstIterator it = m_zones->find(name);
+ if (it != m_zones->end())
+ return it.data();
+
+ // Error.
+ return 0;
+}
+
+KTimezoneDetails::KTimezoneDetails()
+{
+}
+
+KTimezoneDetails::~KTimezoneDetails()
+{
+}
+
+void KTimezoneDetails::gotAbbreviation(int /*index*/, const QString &)
+{}
+
+void KTimezoneDetails::gotHeader(
+ unsigned, unsigned, unsigned,
+ unsigned, unsigned, unsigned)
+{}
+
+void KTimezoneDetails::gotLeapAdjustment(int /*index*/, unsigned, unsigned)
+{}
+
+void KTimezoneDetails::gotLocalTime(int /*index*/, int, bool, unsigned)
+{}
+
+void KTimezoneDetails::gotLocalTimeIndex(int /*index*/, unsigned)
+{}
+
+void KTimezoneDetails::gotIsStandard(int /*index*/, bool)
+{}
+
+void KTimezoneDetails::gotTransitionTime(int /*index*/, unsigned)
+{}
+
+void KTimezoneDetails::gotIsUTC(int /*index*/, bool)
+{}
+
+void KTimezoneDetails::parseEnded()
+{}
+
+void KTimezoneDetails::parseStarted()
+{}
+
+KTimezoneSource::KTimezoneSource(const QString &db) :
+ m_db(db)
+{
+}
+
+KTimezoneSource::~KTimezoneSource()
+{
+}
+
+QString KTimezoneSource::db()
+{
+ return m_db;
+}
+
+bool KTimezoneSource::parse(const QString &zone, KTimezoneDetails &dataReceiver) const
+{
+ QFile f(m_db + '/' + zone);
+ if (!f.open(IO_ReadOnly))
+ {
+ kdError() << "Cannot open " << f.name() << endl;
+ return false;
+ }
+
+ // Structures that represent the zoneinfo file.
+ Q_UINT8 T, z, i_, f_;
+ struct
+ {
+ Q_UINT32 ttisgmtcnt;
+ Q_UINT32 ttisstdcnt;
+ Q_UINT32 leapcnt;
+ Q_UINT32 timecnt;
+ Q_UINT32 typecnt;
+ Q_UINT32 charcnt;
+ } tzh;
+ Q_UINT32 transitionTime;
+ Q_UINT8 localTimeIndex;
+ struct
+ {
+ Q_INT32 gmtoff;
+ Q_INT8 isdst;
+ Q_UINT8 abbrind;
+ } tt;
+ Q_UINT32 leapTime;
+ Q_UINT32 leapSeconds;
+ Q_UINT8 isStandard;
+ Q_UINT8 isUTC;
+
+ QDataStream str(&f);
+ str >> T >> z >> i_ >> f_;
+ // kdError() << "signature: " << QChar(T) << QChar(z) << QChar(i_) << QChar(f_) << endl;
+ unsigned i;
+ for (i = 0; i < 4; i++)
+ str >> tzh.ttisgmtcnt;
+ str >> tzh.ttisgmtcnt >> tzh.ttisstdcnt >> tzh.leapcnt >> tzh.timecnt >> tzh.typecnt >> tzh.charcnt;
+ // kdError() << "header: " << tzh.ttisgmtcnt << ", " << tzh.ttisstdcnt << ", " << tzh.leapcnt << ", " <<
+ // tzh.timecnt << ", " << tzh.typecnt << ", " << tzh.charcnt << endl;
+ dataReceiver.gotHeader(tzh.ttisgmtcnt, tzh.ttisstdcnt, tzh.leapcnt, tzh.timecnt, tzh.typecnt, tzh.charcnt);
+ for (i = 0; i < tzh.timecnt; i++)
+ {
+ str >> transitionTime;
+ dataReceiver.gotTransitionTime(i, transitionTime);
+ }
+ for (i = 0; i < tzh.timecnt; i++)
+ {
+ // NB: these appear to be 1-based, not zero-based!
+ str >> localTimeIndex;
+ dataReceiver.gotLocalTimeIndex(i, localTimeIndex);
+ }
+ for (i = 0; i < tzh.typecnt; i++)
+ {
+ str >> tt.gmtoff >> tt.isdst >> tt.abbrind;
+ // kdError() << "local type: " << tt.gmtoff << ", " << tt.isdst << ", " << tt.abbrind << endl;
+ dataReceiver.gotLocalTime(i, tt.gmtoff, (tt.isdst != 0), tt.abbrind);
+ }
+
+ // Make sure we don't run foul of maliciously coded timezone abbreviations.
+ if (tzh.charcnt > 64)
+ {
+ kdError() << "excessive length for timezone abbreviations: " << tzh.charcnt << endl;
+ return false;
+ }
+ QByteArray array(tzh.charcnt);
+ str.readRawBytes(array.data(), array.size());
+ char *abbrs = array.data();
+ if (abbrs[tzh.charcnt - 1] != 0)
+ {
+ // These abbrevations are corrupt!
+ kdError() << "timezone abbreviations not terminated: " << abbrs[tzh.charcnt - 1] << endl;
+ return false;
+ }
+ char *abbr = abbrs;
+ while (abbr < abbrs + tzh.charcnt)
+ {
+ // kdError() << "abbr: " << abbr << endl;
+ dataReceiver.gotAbbreviation((abbr - abbrs), abbr);
+ abbr += strlen(abbr) + 1;
+ }
+ for (i = 0; i < tzh.leapcnt; i++)
+ {
+ str >> leapTime >> leapSeconds;
+ // kdError() << "leap entry: " << leapTime << ", " << leapSeconds << endl;
+ dataReceiver.gotLeapAdjustment(i, leapTime, leapSeconds);
+ }
+ for (i = 0; i < tzh.ttisstdcnt; i++)
+ {
+ str >> isStandard;
+ // kdError() << "standard: " << isStandard << endl;
+ dataReceiver.gotIsStandard(i, (isStandard != 0));
+ }
+ for (i = 0; i < tzh.ttisgmtcnt; i++)
+ {
+ str >> isUTC;
+ // kdError() << "UTC: " << isUTC << endl;
+ dataReceiver.gotIsUTC(i, (isUTC != 0));
+ }
+ return true;
+}