diff options
Diffstat (limited to 'tdecore/ktimezones.cpp')
-rw-r--r-- | tdecore/ktimezones.cpp | 790 |
1 files changed, 790 insertions, 0 deletions
diff --git a/tdecore/ktimezones.cpp b/tdecore/ktimezones.cpp new file mode 100644 index 000000000..dbd589d85 --- /dev/null +++ b/tdecore/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 <tqdatetime.h> +#include <tqfile.h> +#include <tqregexp.h> +#include <tqstringlist.h> +#include <tqtextstream.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 TQString &stdZone, const TQString &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; + TQString m_stdZone; + TQString m_dstZone; + + virtual void gotAbbreviation(int /*index*/, const TQString &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 TQString &/*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; + } + + TQString abbreviation() + { + return m_abbr; + } + +private: + unsigned m_dateTime; + int m_transitionTimeIndex; + int m_localTimeIndex; + int m_abbrIndex; + int m_offset; + bool m_isDst; + TQString 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 TQString &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 TQString& name, + const TQString &countryCode, float latitude, float longitude, + const TQString &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; +} + +TQString KTimezone::comment() const +{ + return m_comment; +} + +TQDateTime KTimezone::convert(const KTimezone *newZone, const TQDateTime &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(); + TQDateTime remoteTime; + remoteTime.setTime_t(utc, Qt::LocalTime); + + // Now restore things + if (!originalZone) + { + ::unsetenv("TZ"); + } + else + { + ::setenv("TZ", originalZone, 1); + } + tzset(); + return remoteTime; +} + +TQString KTimezone::countryCode() const +{ + return m_countryCode; +} + +float KTimezone::latitude() const +{ + return m_latitude; +} + +float KTimezone::longitude() const +{ + return m_longitude; +} + +TQString KTimezone::name() const +{ + return m_name; +} + +int KTimezone::offset(Qt::TimeSpec basisSpec) const +{ + char *originalZone = ::getenv("TZ"); + + // Get the time in the current timezone. + TQDateTime basisTime = TQDateTime::tqcurrentDateTime(basisSpec); + + // Set the timezone and find out what time it is there compared to the basis. + ::setenv("TZ", m_name.utf8(), 1); + tzset(); + TQDateTime remoteTime = TQDateTime::tqcurrentDateTime(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 TQDateTime &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. + TQFile 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. + TQTextStream str(&f); + TQRegExp lineSeparator("[ \t]"); + TQRegExp ordinateSeparator("[+-]"); + KSharedPtr<KTimezoneSource> db(new KTimezoneSource(m_zoneinfoDir)); + while (!str.atEnd()) + { + TQString line = str.readLine(); + if (line.isEmpty() || '#' == line[0]) + continue; + TQStringList 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 "". + TQStringList 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 TQString &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. + TQFile f; + f.setName("/etc/localtime"); + if (f.open(IO_ReadOnly)) + { + // Compute the MD5 sum of /etc/localtime. + KMD5 context(""); + context.reset(); + context.update(TQT_TQIODEVICE_OBJECT(f)); + TQIODevice::Offset referenceSize = f.size(); + TQString 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)) + { + TQIODevice::Offset candidateSize = f.size(); + TQString candidateMd5Sum; + if (candidateSize == referenceSize) + { + // Only do the heavy lifting for file sizes which match. + context.reset(); + context.update(TQT_TQIODEVICE_OBJECT(f)); + candidateMd5Sum = context.hexDigest(); + } + f.close(); + if (candidateMd5Sum == referenceMd5Sum) + { + // kdError() << "local=" << zone->name() << endl; + local = zone; + break; + } + } + } + } + } + if (local) + return local; + + // BSD support. + TQString 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 + { + TQTextStream ts(&f); + ts.setEncoding(TQTextStream::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 + { + TQTextStream ts(&f); + ts.setEncoding(TQTextStream::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 TQString &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 TQString &) +{} + +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 TQString &db) : + m_db(db) +{ +} + +KTimezoneSource::~KTimezoneSource() +{ +} + +TQString KTimezoneSource::db() +{ + return m_db; +} + +bool KTimezoneSource::parse(const TQString &zone, KTimezoneDetails &dataReceiver) const +{ + TQFile f(m_db + '/' + zone); + if (!f.open(IO_ReadOnly)) + { + kdError() << "Cannot open " << f.name() << endl; + return false; + } + + // Structures that represent the zoneinfo file. + TQ_UINT8 T, z, i_, f_; + struct + { + TQ_UINT32 ttisgmtcnt; + TQ_UINT32 ttisstdcnt; + TQ_UINT32 leapcnt; + TQ_UINT32 timecnt; + TQ_UINT32 typecnt; + TQ_UINT32 charcnt; + } tzh; + TQ_UINT32 transitionTime; + TQ_UINT8 localTimeIndex; + struct + { + TQ_INT32 gmtoff; + TQ_INT8 isdst; + TQ_UINT8 abbrind; + } tt; + TQ_UINT32 leapTime; + TQ_UINT32 leapSeconds; + TQ_UINT8 isStandard; + TQ_UINT8 isUTC; + + TQDataStream str(&f); + str >> T >> z >> i_ >> f_; + // kdError() << "signature: " << TQChar(T) << TQChar(z) << TQChar(i_) << TQChar(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; + } + TQByteArray 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; +} |