diff --git a/ktimezoned/ktimezoned.cpp b/ktimezoned/ktimezoned.cpp index d92c899dab..bef6d6215f 100644 --- a/ktimezoned/ktimezoned.cpp +++ b/ktimezoned/ktimezoned.cpp @@ -1,967 +1,968 @@ /* This file is part of the KDE libraries Copyright (c) 2005-2010 David Jarvie Copyright (c) 2005 S.R.Haque . This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. 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 "ktimezoned.moc" #include "ktimezonedbase.moc" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include K_PLUGIN_FACTORY(KTimeZonedFactory, registerPlugin(); ) K_EXPORT_PLUGIN(KTimeZonedFactory("ktimezoned")) // The maximum allowed length for reading a zone.tab line. This is set to // provide plenty of leeway, given that the maximum length of lines in a valid // zone.tab will be around 100 - 120 characters. const int MAX_ZONE_TAB_LINE_LENGTH = 2000; // Config file entry names const char ZONEINFO_DIR[] = "ZoneinfoDir"; // path to zoneinfo/ directory const char ZONE_TAB[] = "Zonetab"; // path & name of zone.tab const char ZONE_TAB_CACHE[] = "ZonetabCache"; // type of cached simulated zone.tab const char LOCAL_ZONE[] = "LocalZone"; // name of local time zone KTimeZoned::KTimeZoned(QObject* parent, const QList& l) : KTimeZonedBase(parent, l), mSource(0), mZonetabWatch(0), mDirWatch(0) { init(false); } KTimeZoned::~KTimeZoned() { delete mSource; mSource = 0; delete mZonetabWatch; mZonetabWatch = 0; delete mDirWatch; mDirWatch = 0; } void KTimeZoned::init(bool restart) { if (restart) { kDebug(1221) << "KTimeZoned::init(restart)"; delete mSource; mSource = 0; delete mZonetabWatch; mZonetabWatch = 0; delete mDirWatch; mDirWatch = 0; } KConfig config(QLatin1String("ktimezonedrc")); if (restart) config.reparseConfiguration(); KConfigGroup group(&config, "TimeZones"); mZoneinfoDir = group.readEntry(ZONEINFO_DIR); - mZoneinfoDir = QFileInfo(mZoneinfoDir).canonicalFilePath(); // just in case /usr or /usr/share is a symlink mZoneTab = group.readEntry(ZONE_TAB); mConfigLocalZone = group.readEntry(LOCAL_ZONE); QString ztc = group.readEntry(ZONE_TAB_CACHE, QString()); mZoneTabCache = (ztc == "Solaris") ? Solaris : NoCache; if (mZoneinfoDir.length() > 1 && mZoneinfoDir.endsWith('/')) mZoneinfoDir.truncate(mZoneinfoDir.length() - 1); // strip trailing '/' // For Unix, read zone.tab. QString oldZoneinfoDir = mZoneinfoDir; QString oldZoneTab = mZoneTab; CacheType oldCacheType = mZoneTabCache; // Open zone.tab if we already know where it is QFile f; if (!mZoneTab.isEmpty() && !mZoneinfoDir.isEmpty()) { f.setFileName(mZoneTab); if (!f.open(QIODevice::ReadOnly)) mZoneTab.clear(); else if (mZoneTabCache != NoCache) { // Check whether the cached zone.tab is still up to date #ifdef __GNUC__ #warning Implement checking whether Solaris cached zone.tab is up to date #endif } } if (mZoneTab.isEmpty() || mZoneinfoDir.isEmpty()) { // Search for zone.tab if (!findZoneTab(f)) return; mZoneTab = f.fileName(); if (mZoneinfoDir != oldZoneinfoDir || mZoneTab != oldZoneTab || mZoneTabCache != oldCacheType) { // Update config file and notify interested applications group.writeEntry(ZONEINFO_DIR, mZoneinfoDir); group.writeEntry(ZONE_TAB, mZoneTab); QString ztc; switch (mZoneTabCache) { case Solaris: ztc = "Solaris"; break; default: break; } group.writeEntry(ZONE_TAB_CACHE, ztc); group.sync(); QDBusMessage message = QDBusMessage::createSignal("/Daemon", "org.kde.KTimeZoned", "configChanged"); QDBusConnection::sessionBus().send(message); } } // Read zone.tab and create a collection of KTimeZone instances readZoneTab(f); mZonetabWatch = new KDirWatch(this); mZonetabWatch->addFile(mZoneTab); connect(mZonetabWatch, SIGNAL(dirty(const QString&)), SLOT(zonetab_Changed(const QString&))); // Find the local system time zone and set up file monitors to detect changes findLocalZone(); } // Check if the local zone has been updated, and if so, write the new // zone to the config file and notify interested parties. void KTimeZoned::updateLocalZone() { if (mConfigLocalZone != mLocalZone) { KConfig config(QLatin1String("ktimezonedrc")); KConfigGroup group(&config, "TimeZones"); mConfigLocalZone = mLocalZone; group.writeEntry(LOCAL_ZONE, mConfigLocalZone); group.sync(); QDBusMessage message = QDBusMessage::createSignal("/Daemon", "org.kde.KTimeZoned", "configChanged"); QDBusConnection::sessionBus().send(message); } } /* * Find the location of the zoneinfo files and store in mZoneinfoDir. * Open or if necessary create zone.tab. */ bool KTimeZoned::findZoneTab(QFile& f) { #if defined(SOLARIS) || defined(USE_SOLARIS) const QString ZONE_TAB_FILE = QLatin1String("/tab/zone_sun.tab"); const QString ZONE_INFO_DIR = QLatin1String("/usr/share/lib/zoneinfo"); #else const QString ZONE_TAB_FILE = QLatin1String("/zone.tab"); const QString ZONE_INFO_DIR = QLatin1String("/usr/share/zoneinfo"); #endif mZoneTabCache = NoCache; // Find and open zone.tab - it's all easy except knowing where to look. // Try the LSB location first. QDir dir; QString zoneinfoDir = ZONE_INFO_DIR; // make a note if the dir exists; whether it contains zone.tab or not if (dir.exists(zoneinfoDir)) { mZoneinfoDir = zoneinfoDir; f.setFileName(zoneinfoDir + ZONE_TAB_FILE); if (f.open(QIODevice::ReadOnly)) return true; kDebug(1221) << "Can't open " << f.fileName(); } zoneinfoDir = QLatin1String("/usr/lib/zoneinfo"); if (dir.exists(zoneinfoDir)) { mZoneinfoDir = zoneinfoDir; f.setFileName(zoneinfoDir + ZONE_TAB_FILE); if (f.open(QIODevice::ReadOnly)) return true; kDebug(1221) << "Can't open " << f.fileName(); } zoneinfoDir = ::getenv("TZDIR"); if (!zoneinfoDir.isEmpty() && dir.exists(zoneinfoDir)) { mZoneinfoDir = zoneinfoDir; f.setFileName(zoneinfoDir + ZONE_TAB_FILE); if (f.open(QIODevice::ReadOnly)) return true; kDebug(1221) << "Can't open " << f.fileName(); } zoneinfoDir = QLatin1String("/usr/share/lib/zoneinfo"); if (dir.exists(zoneinfoDir + QLatin1String("/src"))) { mZoneinfoDir = zoneinfoDir; // Solaris support. Synthesise something that looks like a zone.tab, // and cache it between sessions. // // grep -h ^Zone /usr/share/lib/zoneinfo/src/* | awk '{print "??\t+9999+99999\t" $2}' // // where the country code is set to "??" and the latitude/longitude // values are dummies. // QDir d(mZoneinfoDir + QLatin1String("/src")); d.setFilter(QDir::Files | QDir::Hidden | QDir::NoSymLinks); QStringList fileList = d.entryList(); mZoneTab = KStandardDirs::locateLocal("cache", QLatin1String("zone.tab")); f.setFileName(mZoneTab); if (!f.open(QIODevice::WriteOnly)) { kError(1221) << "Could not create zone.tab cache" << endl; return false; } QFile zoneFile; QList tokens; QByteArray line; line.reserve(1024); QTextStream tmpStream(&f); qint64 r; for (int i = 0, end = fileList.count(); i < end; ++i) { zoneFile.setFileName(d.filePath(fileList[i].toLatin1())); if (!zoneFile.open(QIODevice::ReadOnly)) { kDebug(1221) << "Could not open file '" << zoneFile.fileName().toLatin1() \ << "' for reading." << endl; continue; } while (!zoneFile.atEnd()) { if ((r = zoneFile.readLine(line.data(), 1023)) > 0 && line.startsWith("Zone")) { line.replace('\t', ' '); // change tabs to spaces tokens = line.split(' '); for (int j = 0, jend = tokens.count(); j < jend; ++j) if (tokens[j].endsWith(' ')) tokens[j].chop(1); tmpStream << "??\t+9999+99999\t" << tokens[1] << "\n"; } } zoneFile.close(); } f.close(); if (!f.open(QIODevice::ReadOnly)) { kError(1221) << "Could not reopen zone.tab cache file for reading." << endl; return false; } mZoneTabCache = Solaris; return true; } return false; } // Parse zone.tab and for each time zone, create a KSystemTimeZone instance. // Note that only data needed by this module is specified to KSystemTimeZone. void KTimeZoned::readZoneTab(QFile &f) { // Parse the already open real or fake zone.tab. QRegExp lineSeparator("[ \t]"); if (!mSource) mSource = new KSystemTimeZoneSource; mZones.clear(); QTextStream str(&f); while (!str.atEnd()) { // Read the next line, but limit its length to guard against crashing // due to a corrupt very large zone.tab (see KDE bug 224868). QString line = str.readLine(MAX_ZONE_TAB_LINE_LENGTH); if (line.isEmpty() || line[0] == '#') continue; QStringList tokens = KStringHandler::perlSplit(lineSeparator, line, 4); int n = tokens.count(); if (n < 3) { kError(1221) << "readZoneTab(): invalid record: " << line << endl; continue; } // Add entry to list. if (tokens[0] == "??") tokens[0] = ""; else if (!tokens[0].isEmpty()) mHaveCountryCodes = true; mZones.add(KSystemTimeZone(mSource, tokens[2], tokens[0])); } f.close(); } // Find the local time zone, starting from scratch. void KTimeZoned::findLocalZone() { delete mDirWatch; mDirWatch = 0; mLocalZone.clear(); mLocalIdFile.clear(); mLocalIdFile2.clear(); mLocalZoneDataFile.clear(); // SOLUTION 1: DEFINITIVE. // First try the simplest solution of checking for well-formed TZ setting. const char *envtz = ::getenv("TZ"); if (checkTZ(envtz)) { mSavedTZ = envtz; if (!mLocalZone.isEmpty()) kDebug(1221)<<"TZ: "<addFile(mLocalIdFile); if (!mLocalIdFile2.isEmpty()) mDirWatch->addFile(mLocalIdFile2); if (!mLocalZoneDataFile.isEmpty()) mDirWatch->addFile(mLocalZoneDataFile); connect(mDirWatch, SIGNAL(dirty(const QString&)), SLOT(localChanged(const QString&))); connect(mDirWatch, SIGNAL(deleted(const QString&)), SLOT(localChanged(const QString&))); connect(mDirWatch, SIGNAL(created(const QString&)), SLOT(localChanged(const QString&))); if (mLocalZone.isEmpty() && !mZoneinfoDir.isEmpty()) { // SOLUTION 7: HEURISTIC. // None of the deterministic stuff above has worked: try a heuristic. We // try to find a pair of matching time zone abbreviations...that way, we'll // likely return a value in the user's own country. tzset(); QByteArray tzname0(tzname[0]); // store copies, because zone.parse() will change them QByteArray tzname1(tzname[1]); int bestOffset = INT_MAX; KSystemTimeZoneSource::startParseBlock(); const KTimeZones::ZoneMap zmap = mZones.zones(); for (KTimeZones::ZoneMap::ConstIterator it = zmap.constBegin(), end = zmap.constEnd(); it != end; ++it) { KTimeZone zone = it.value(); int candidateOffset = qAbs(zone.currentOffset(Qt::LocalTime)); if (candidateOffset < bestOffset && zone.parse()) { QList abbrs = zone.abbreviations(); if (abbrs.contains(tzname0) && abbrs.contains(tzname1)) { // kDebug(1221) << "local=" << zone.name(); mLocalZone = zone.name(); bestOffset = candidateOffset; if (!bestOffset) break; } } } KSystemTimeZoneSource::endParseBlock(); if (!mLocalZone.isEmpty()) { mLocalMethod = TzName; kDebug(1221)<<"tzname: "< args; args += mZoneTab; message.setArguments(args); QDBusConnection::sessionBus().send(message); // Reread zone.tab and recreate the collection of KTimeZone instances, // in case any zones have been created or deleted and one of them // subsequently becomes the local zone. QFile f; f.setFileName(mZoneTab); if (!f.open(QIODevice::ReadOnly)) kError(1221) << "Could not open zone.tab (" << mZoneTab << ") to reread"; else readZoneTab(f); } // Called when KDirWatch detects a change void KTimeZoned::localChanged(const QString& path) { if (path == mLocalZoneDataFile) { // Only need to update the definition of the local zone, // not its identity. QDBusMessage message = QDBusMessage::createSignal("/Daemon", "org.kde.KTimeZoned", "zoneDefinitionChanged"); QList args; args += mLocalZone; message.setArguments(args); QDBusConnection::sessionBus().send(message); return; } QString oldDataFile = mLocalZoneDataFile; switch (mLocalMethod) { case EnvTzLink: case EnvTzFile: { const char *envtz = ::getenv("TZ"); if (mSavedTZ != envtz) { // TZ has changed - start from scratch again findLocalZone(); return; } // The contents of the file pointed to by TZ has changed. } // Fall through to LocaltimeLink case LocaltimeLink: case LocaltimeCopy: // The fallback methods below also set a watch for /etc/localtime in // case it gets created. case TzName: case Utc: matchZoneFile(mLocalIdFile); break; case Timezone: checkTimezone(); break; case RcFile: checkRcFile(); break; case DefaultInit: checkDefaultInit(); break; default: return; } if (oldDataFile != mLocalZoneDataFile) { if (!oldDataFile.isEmpty()) mDirWatch->removeFile(oldDataFile); if (!mLocalZoneDataFile.isEmpty()) mDirWatch->addFile(mLocalZoneDataFile); } updateLocalZone(); } bool KTimeZoned::checkTZ(const char *envZone) { // SOLUTION 1: DEFINITIVE. // First try the simplest solution of checking for well-formed TZ setting. if (envZone) { if (envZone[0] == '\0') { mLocalMethod = EnvTz; mLocalZone = KTimeZone::utc().name(); mLocalIdFile.clear(); mLocalZoneDataFile.clear(); return true; } if (envZone[0] == ':') { // TZ specifies a file name, either relative to zoneinfo/ or absolute. QString TZfile = QFile::decodeName(envZone + 1); - if (TZfile.startsWith(mZoneinfoDir)) + const QString realZoneinfoDir = QFileInfo(mZoneinfoDir).canonicalFilePath(); // just in case /usr or /usr/share is a symlink + if (TZfile.startsWith(realZoneinfoDir)) { // It's an absolute file name in the zoneinfo directory. // Convert it to a file name relative to zoneinfo/. - TZfile = TZfile.mid(mZoneinfoDir.length()); + TZfile = TZfile.mid(realZoneinfoDir.length()); } if (TZfile.startsWith(QLatin1Char('/'))) { // It's an absolute file name. QString symlink; if (matchZoneFile(TZfile)) { mLocalMethod = static_cast(EnvTz | (mLocalMethod & TypeMask)); return true; } } else if (!TZfile.isEmpty()) { // It's a file name relative to zoneinfo/ mLocalZone = TZfile; if (!mLocalZone.isEmpty()) { mLocalMethod = EnvTz; mLocalZoneDataFile = mZoneinfoDir + '/' + TZfile; mLocalIdFile.clear(); return true; } } } } return false; } bool KTimeZoned::checkTimezone() { // SOLUTION 2: DEFINITIVE. // BSD support. QFile f; f.setFileName(QLatin1String("/etc/timezone")); if (!f.open(QIODevice::ReadOnly)) return false; // Read the first line of the file. QTextStream ts(&f); ts.setCodec("ISO-8859-1"); QString zoneName; if (!ts.atEnd()) zoneName = ts.readLine(); f.close(); if (zoneName.isEmpty()) return false; if (!setLocalZone(zoneName)) return false; mLocalMethod = Timezone; mLocalIdFile = f.fileName(); kDebug(1221)<<"/etc/timezone: "<country().toUpper(); const KTimeZones::ZoneMap zmap = mZones.zones(); for (KTimeZones::ZoneMap::ConstIterator zit = zmap.constBegin(), zend = zmap.constEnd(); zit != zend; ++zit) { KTimeZone tzone = zit.value(); if (tzone.countryCode() == country) { local = compareChecksum(tzone, referenceMd5Sum, referenceSize); if (local.isValid()) { zoneName = local.name(); break; } } } } if (!local.isValid()) { // Look for a checksum match with the cached checksum values MD5Map oldChecksums = mMd5Sums; // save a copy of the existing checksums for (it5 = mMd5Sums.constBegin(), end5 = mMd5Sums.constEnd(); it5 != end5; ++it5) { if (it5.value() == referenceMd5Sum) { // The cached checksum matches. Ensure that the file hasn't changed. if (compareChecksum(it5, referenceMd5Sum, referenceSize)) { zoneName = it5.key(); local = mZones.zone(zoneName); if (local.isValid()) break; } oldChecksums.clear(); // the cache has been cleared break; } } if (!local.isValid()) { // The checksum didn't match any in the cache. // Continue building missing entries in the cache on the assumption that // we haven't previously looked at the zoneinfo file which matches. const KTimeZones::ZoneMap zmap = mZones.zones(); for (KTimeZones::ZoneMap::ConstIterator zit = zmap.constBegin(), zend = zmap.constEnd(); zit != zend; ++zit) { KTimeZone zone = zit.value(); zoneName = zone.name(); if (!mMd5Sums.contains(zoneName)) { QString candidateMd5Sum = calcChecksum(zoneName, referenceSize); if (candidateMd5Sum == referenceMd5Sum) { // kDebug(1221) << "local=" << zone.name(); local = zone; break; } } } } if (!local.isValid()) { // Didn't find the file, so presumably a previously cached checksum must // have changed. Delete all the old checksums. MD5Map::ConstIterator mit; MD5Map::ConstIterator mend = oldChecksums.constEnd(); for (mit = oldChecksums.constBegin(); mit != mend; ++mit) mMd5Sums.remove(mit.key()); // And recalculate the old checksums for (mit = oldChecksums.constBegin(); mit != mend; ++mit) { zoneName = mit.key(); QString candidateMd5Sum = calcChecksum(zoneName, referenceSize); if (candidateMd5Sum == referenceMd5Sum) { // kDebug(1221) << "local=" << zoneName; local = mZones.zone(zoneName); break; } } } } bool success = false; if (local.isValid()) { // The file matches a zoneinfo file mLocalZone = zoneName; mLocalZoneDataFile = mZoneinfoDir + '/' + zoneName; success = true; } else { // The file doesn't match a zoneinfo file. If it's a TZfile, use it directly. // Read the file type identifier. char buff[4]; f.reset(); QDataStream str(&f); if (str.readRawData(buff, 4) == 4 && buff[0] == 'T' && buff[1] == 'Z' && buff[2] == 'i' && buff[3] == 'f') { // Use its absolute path as the zone name. mLocalZone = f.fileName(); mLocalZoneDataFile.clear(); success = true; } } f.close(); if (success) { mLocalMethod = LocaltimeCopy; mLocalIdFile = f.fileName(); kDebug(1221)<" << zoneName; mLocalIdFile = f.fileName(); return true; } // Check whether the zone name is valid, either as a zone in zone.tab or // as another file in the zoneinfo directory. // If valid, set the local zone information. bool KTimeZoned::setLocalZone(const QString &zoneName) { KTimeZone local = mZones.zone(zoneName); if (!local.isValid()) { // It isn't a recognised zone in zone.tab. // Note that some systems (e.g. Gentoo) have zones under zoneinfo which // are not in zone.tab, so check if it points to another zone file. if (mZoneinfoDir.isEmpty()) return false; QString path = mZoneinfoDir + '/' + zoneName; QFile qf; qf.setFileName(path); QFileInfo fi(qf); if (fi.isSymLink()) fi.setFile(fi.canonicalFilePath()); if (!fi.exists() || !fi.isReadable()) return false; } mLocalZone = zoneName; mLocalZoneDataFile = mZoneinfoDir.isEmpty() ? QString() : mZoneinfoDir + '/' + zoneName; return true; } // Check whether the checksum for a time zone matches a given saved checksum. KTimeZone KTimeZoned::compareChecksum(const KTimeZone &zone, const QString &referenceMd5Sum, qlonglong size) { MD5Map::ConstIterator it5 = mMd5Sums.constFind(zone.name()); if (it5 == mMd5Sums.constEnd()) { // No checksum has been computed yet for this zone file. // Compute it now. QString candidateMd5Sum = calcChecksum(zone.name(), size); if (candidateMd5Sum == referenceMd5Sum) { // kDebug(1221) << "local=" << zone.name(); return zone; } return KTimeZone(); } if (it5.value() == referenceMd5Sum) { // The cached checksum matches. Ensure that the file hasn't changed. if (compareChecksum(it5, referenceMd5Sum, size)) return mZones.zone(it5.key()); } return KTimeZone(); } // Check whether a checksum matches a given saved checksum. // Returns false if the file no longer matches and cache was cleared. bool KTimeZoned::compareChecksum(MD5Map::ConstIterator it5, const QString &referenceMd5Sum, qlonglong size) { // The cached checksum matches. Ensure that the file hasn't changed. QString zoneName = it5.key(); QString candidateMd5Sum = calcChecksum(zoneName, size); if (candidateMd5Sum.isNull()) mMd5Sums.remove(zoneName); // no match - wrong file size else if (candidateMd5Sum == referenceMd5Sum) return true; // File(s) have changed, so clear the cache mMd5Sums.clear(); mMd5Sums[zoneName] = candidateMd5Sum; // reinsert the newly calculated checksum return false; } // Calculate the MD5 checksum for the given zone file, provided that its size matches. // The calculated checksum is cached. QString KTimeZoned::calcChecksum(const QString &zoneName, qlonglong size) { QString path = mZoneinfoDir + '/' + zoneName; QFileInfo fi(path); if (static_cast(fi.size()) == size) { // Only do the heavy lifting for file sizes which match. QFile f; f.setFileName(path); if (f.open(QIODevice::ReadOnly)) { KMD5 context(""); context.reset(); context.update(f); QString candidateMd5Sum = context.hexDigest(); f.close(); mMd5Sums[zoneName] = candidateMd5Sum; // cache the new checksum return candidateMd5Sum; } } return QString(); }