You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tdelibs/tdecore/ktimezones.cpp

791 lines
20 KiB

/*
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 <tdetempfile.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(
TDESharedPtr<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, TQt::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(TQt::TimeSpec basisSpec) const
{
char *originalZone = ::getenv("TZ");
// Get the time in the current timezone.
TQDateTime basisTime = TQDateTime::currentDateTime(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::currentDateTime(TQt::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(TDEProcess::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("[+-]");
TDESharedPtr<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(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(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 = TQABS(zone->offset(TQt::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;
}