1/*
2 This file is part of the KDE libraries
3 Copyright (c) 2005-2010 David Jarvie <djarvie@kde.org>
4 Copyright (c) 2005 S.R.Haque <srhaque@iee.org>.
5
6 This library is free software; you can redistribute it and/or
7 modify it under the terms of the GNU Library General Public
8 License as published by the Free Software Foundation; either
9 version 2 of the License, or (at your option) any later version.
10
11 This library is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 Library General Public License for more details.
15
16 You should have received a copy of the GNU Library General Public License
17 along with this library; see the file COPYING.LIB. If not, write to
18 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19 Boston, MA 02110-1301, USA.
20*/
21
22#include "ktimezoned.moc"
23#include "ktimezonedbase.moc"
24
25#include <climits>
26#include <cstdlib>
27
28#include <QFile>
29#include <QFileInfo>
30#include <QDir>
31#include <QRegExp>
32#include <QStringList>
33#include <QTextStream>
34#include <QtDBus/QtDBus>
35
36#include <kglobal.h>
37#include <klocale.h>
38#include <kcodecs.h>
39#include <kstandarddirs.h>
40#include <kstringhandler.h>
41#include <ktemporaryfile.h>
42#include <kdebug.h>
43#include <kconfiggroup.h>
44
45#include <kpluginfactory.h>
46#include <kpluginloader.h>
47
48K_PLUGIN_FACTORY(KTimeZonedFactory,
49 registerPlugin<KTimeZoned>();
50 )
51K_EXPORT_PLUGIN(KTimeZonedFactory("ktimezoned"))
52
53// The maximum allowed length for reading a zone.tab line. This is set to
54// provide plenty of leeway, given that the maximum length of lines in a valid
55// zone.tab will be around 100 - 120 characters.
56const int MAX_ZONE_TAB_LINE_LENGTH = 2000;
57
58// Config file entry names
59const char ZONEINFO_DIR[] = "ZoneinfoDir"; // path to zoneinfo/ directory
60const char ZONE_TAB[] = "Zonetab"; // path & name of zone.tab
61const char ZONE_TAB_CACHE[] = "ZonetabCache"; // type of cached simulated zone.tab
62const char LOCAL_ZONE[] = "LocalZone"; // name of local time zone
63
64
65KTimeZoned::KTimeZoned(QObject* parent, const QList<QVariant>& l)
66 : KTimeZonedBase(parent, l),
67 mSource(0),
68 mZonetabWatch(0),
69 mDirWatch(0)
70{
71 init(false);
72}
73
74KTimeZoned::~KTimeZoned()
75{
76 delete mSource;
77 mSource = 0;
78 delete mZonetabWatch;
79 mZonetabWatch = 0;
80 delete mDirWatch;
81 mDirWatch = 0;
82}
83
84void KTimeZoned::init(bool restart)
85{
86 if (restart)
87 {
88 kDebug(1221) << "KTimeZoned::init(restart)";
89 delete mSource;
90 mSource = 0;
91 delete mZonetabWatch;
92 mZonetabWatch = 0;
93 delete mDirWatch;
94 mDirWatch = 0;
95 }
96
97 KConfig config(QLatin1String("ktimezonedrc"));
98 if (restart)
99 config.reparseConfiguration();
100 KConfigGroup group(&config, "TimeZones");
101 mZoneinfoDir = group.readEntry(ZONEINFO_DIR);
102 mZoneTab = group.readEntry(ZONE_TAB);
103 mConfigLocalZone = group.readEntry(LOCAL_ZONE);
104 QString ztc = group.readEntry(ZONE_TAB_CACHE, QString());
105 mZoneTabCache = (ztc == "Solaris") ? Solaris : NoCache;
106 if (mZoneinfoDir.length() > 1 && mZoneinfoDir.endsWith('/'))
107 mZoneinfoDir.truncate(mZoneinfoDir.length() - 1); // strip trailing '/'
108
109 // For Unix, read zone.tab.
110
111 QString oldZoneinfoDir = mZoneinfoDir;
112 QString oldZoneTab = mZoneTab;
113 CacheType oldCacheType = mZoneTabCache;
114
115 // Open zone.tab if we already know where it is
116 QFile f;
117 if (!mZoneTab.isEmpty() && !mZoneinfoDir.isEmpty())
118 {
119 f.setFileName(mZoneTab);
120 if (!f.open(QIODevice::ReadOnly))
121 mZoneTab.clear();
122 else if (mZoneTabCache != NoCache)
123 {
124 // Check whether the cached zone.tab is still up to date
125#ifdef __GNUC__
126#warning Implement checking whether Solaris cached zone.tab is up to date
127#endif
128 }
129 }
130
131 if (mZoneTab.isEmpty() || mZoneinfoDir.isEmpty())
132 {
133 // Search for zone.tab
134 if (!findZoneTab(f))
135 return;
136 mZoneTab = f.fileName();
137
138 if (mZoneinfoDir != oldZoneinfoDir
139 || mZoneTab != oldZoneTab
140 || mZoneTabCache != oldCacheType)
141 {
142 // Update config file and notify interested applications
143 group.writeEntry(ZONEINFO_DIR, mZoneinfoDir);
144 group.writeEntry(ZONE_TAB, mZoneTab);
145 QString ztc;
146 switch (mZoneTabCache)
147 {
148 case Solaris: ztc = "Solaris"; break;
149 default: break;
150 }
151 group.writeEntry(ZONE_TAB_CACHE, ztc);
152 group.sync();
153 QDBusMessage message = QDBusMessage::createSignal("/Daemon", "org.kde.KTimeZoned", "configChanged");
154 QDBusConnection::sessionBus().send(message);
155 }
156 }
157
158 // Read zone.tab and create a collection of KTimeZone instances
159 readZoneTab(f);
160
161 mZonetabWatch = new KDirWatch(this);
162 mZonetabWatch->addFile(mZoneTab);
163 connect(mZonetabWatch, SIGNAL(dirty(const QString&)), SLOT(zonetab_Changed(const QString&)));
164
165 // Find the local system time zone and set up file monitors to detect changes
166 findLocalZone();
167}
168
169// Check if the local zone has been updated, and if so, write the new
170// zone to the config file and notify interested parties.
171void KTimeZoned::updateLocalZone()
172{
173 if (mConfigLocalZone != mLocalZone)
174 {
175 KConfig config(QLatin1String("ktimezonedrc"));
176 KConfigGroup group(&config, "TimeZones");
177 mConfigLocalZone = mLocalZone;
178 group.writeEntry(LOCAL_ZONE, mConfigLocalZone);
179 group.sync();
180
181 QDBusMessage message = QDBusMessage::createSignal("/Daemon", "org.kde.KTimeZoned", "configChanged");
182 QDBusConnection::sessionBus().send(message);
183 }
184}
185
186/*
187 * Find the location of the zoneinfo files and store in mZoneinfoDir.
188 * Open or if necessary create zone.tab.
189 */
190bool KTimeZoned::findZoneTab(QFile& f)
191{
192#if defined(SOLARIS) || defined(USE_SOLARIS)
193 const QString ZONE_TAB_FILE = QLatin1String("/tab/zone_sun.tab");
194 const QString ZONE_INFO_DIR = QLatin1String("/usr/share/lib/zoneinfo");
195#else
196 const QString ZONE_TAB_FILE = QLatin1String("/zone.tab");
197 const QString ZONE_INFO_DIR = QLatin1String("/usr/share/zoneinfo");
198#endif
199
200 mZoneTabCache = NoCache;
201
202 // Find and open zone.tab - it's all easy except knowing where to look.
203 // Try the LSB location first.
204 QDir dir;
205 QString zoneinfoDir = ZONE_INFO_DIR;
206 // make a note if the dir exists; whether it contains zone.tab or not
207 if (dir.exists(zoneinfoDir))
208 {
209 mZoneinfoDir = zoneinfoDir;
210 f.setFileName(zoneinfoDir + ZONE_TAB_FILE);
211 if (f.open(QIODevice::ReadOnly))
212 return true;
213 kDebug(1221) << "Can't open " << f.fileName();
214 }
215
216 zoneinfoDir = QLatin1String("/usr/lib/zoneinfo");
217 if (dir.exists(zoneinfoDir))
218 {
219 mZoneinfoDir = zoneinfoDir;
220 f.setFileName(zoneinfoDir + ZONE_TAB_FILE);
221 if (f.open(QIODevice::ReadOnly))
222 return true;
223 kDebug(1221) << "Can't open " << f.fileName();
224 }
225
226 zoneinfoDir = ::getenv("TZDIR");
227 if (!zoneinfoDir.isEmpty() && dir.exists(zoneinfoDir))
228 {
229 mZoneinfoDir = zoneinfoDir;
230 f.setFileName(zoneinfoDir + ZONE_TAB_FILE);
231 if (f.open(QIODevice::ReadOnly))
232 return true;
233 kDebug(1221) << "Can't open " << f.fileName();
234 }
235
236 zoneinfoDir = QLatin1String("/usr/share/lib/zoneinfo");
237 if (dir.exists(zoneinfoDir + QLatin1String("/src")))
238 {
239 mZoneinfoDir = zoneinfoDir;
240 // Solaris support. Synthesise something that looks like a zone.tab,
241 // and cache it between sessions.
242 //
243 // grep -h ^Zone /usr/share/lib/zoneinfo/src/* | awk '{print "??\t+9999+99999\t" $2}'
244 //
245 // where the country code is set to "??" and the latitude/longitude
246 // values are dummies.
247 //
248 QDir d(mZoneinfoDir + QLatin1String("/src"));
249 d.setFilter(QDir::Files | QDir::Hidden | QDir::NoSymLinks);
250 QStringList fileList = d.entryList();
251
252 mZoneTab = KStandardDirs::locateLocal("cache", QLatin1String("zone.tab"));
253 f.setFileName(mZoneTab);
254 if (!f.open(QIODevice::WriteOnly))
255 {
256 kError(1221) << "Could not create zone.tab cache" << endl;
257 return false;
258 }
259
260 QFile zoneFile;
261 QList<QByteArray> tokens;
262 QByteArray line;
263 line.reserve(1024);
264 QTextStream tmpStream(&f);
265 qint64 r;
266 for (int i = 0, end = fileList.count(); i < end; ++i)
267 {
268 zoneFile.setFileName(d.filePath(fileList[i].toLatin1()));
269 if (!zoneFile.open(QIODevice::ReadOnly))
270 {
271 kDebug(1221) << "Could not open file '" << zoneFile.fileName().toLatin1() \
272 << "' for reading." << endl;
273 continue;
274 }
275 while (!zoneFile.atEnd())
276 {
277 if ((r = zoneFile.readLine(line.data(), 1023)) > 0
278 && line.startsWith("Zone"))
279 {
280 line.replace('\t', ' '); // change tabs to spaces
281 tokens = line.split(' ');
282 for (int j = 0, jend = tokens.count(); j < jend; ++j)
283 if (tokens[j].endsWith(' '))
284 tokens[j].chop(1);
285 tmpStream << "??\t+9999+99999\t" << tokens[1] << "\n";
286 }
287 }
288 zoneFile.close();
289 }
290 f.close();
291 if (!f.open(QIODevice::ReadOnly))
292 {
293 kError(1221) << "Could not reopen zone.tab cache file for reading." << endl;
294 return false;
295 }
296 mZoneTabCache = Solaris;
297 return true;
298 }
299 return false;
300}
301
302// Parse zone.tab and for each time zone, create a KSystemTimeZone instance.
303// Note that only data needed by this module is specified to KSystemTimeZone.
304void KTimeZoned::readZoneTab(QFile &f)
305{
306 // Parse the already open real or fake zone.tab.
307 QRegExp lineSeparator("[ \t]");
308 if (!mSource)
309 mSource = new KSystemTimeZoneSource;
310 mZones.clear();
311 QTextStream str(&f);
312 while (!str.atEnd())
313 {
314 // Read the next line, but limit its length to guard against crashing
315 // due to a corrupt very large zone.tab (see KDE bug 224868).
316 QString line = str.readLine(MAX_ZONE_TAB_LINE_LENGTH);
317 if (line.isEmpty() || line[0] == '#')
318 continue;
319 QStringList tokens = KStringHandler::perlSplit(lineSeparator, line, 4);
320 int n = tokens.count();
321 if (n < 3)
322 {
323 kError(1221) << "readZoneTab(): invalid record: " << line << endl;
324 continue;
325 }
326
327 // Add entry to list.
328 if (tokens[0] == "??")
329 tokens[0] = "";
330 else if (!tokens[0].isEmpty())
331 mHaveCountryCodes = true;
332 mZones.add(KSystemTimeZone(mSource, tokens[2], tokens[0]));
333 }
334 f.close();
335}
336
337// Find the local time zone, starting from scratch.
338void KTimeZoned::findLocalZone()
339{
340 delete mDirWatch;
341 mDirWatch = 0;
342 mLocalZone.clear();
343 mLocalIdFile.clear();
344 mLocalIdFile2.clear();
345 mLocalZoneDataFile.clear();
346
347 // SOLUTION 1: DEFINITIVE.
348 // First try the simplest solution of checking for well-formed TZ setting.
349 const char *envtz = ::getenv("TZ");
350 if (checkTZ(envtz))
351 {
352 mSavedTZ = envtz;
353 if (!mLocalZone.isEmpty()) kDebug(1221)<<"TZ: "<<mLocalZone;
354 }
355
356 if (mLocalZone.isEmpty())
357 {
358 // SOLUTION 2: DEFINITIVE.
359 // BSD & Linux support: local time zone id in /etc/timezone.
360 checkTimezone();
361 }
362 if (mLocalZone.isEmpty() && !mZoneinfoDir.isEmpty())
363 {
364 // SOLUTION 3: DEFINITIVE.
365 // Try to follow any /etc/localtime symlink to a zoneinfo file.
366 // SOLUTION 4: DEFINITIVE.
367 // Try to match /etc/localtime against the list of zoneinfo files.
368 matchZoneFile(QLatin1String("/etc/localtime"));
369 }
370 if (mLocalZone.isEmpty())
371 {
372 // SOLUTION 5: DEFINITIVE.
373 // Look for setting in /etc/rc.conf or /etc/rc.local.
374 checkRcFile();
375 }
376 if (mLocalZone.isEmpty())
377 {
378 // SOLUTION 6: DEFINITIVE.
379 // Solaris support using /etc/default/init.
380 checkDefaultInit();
381 }
382
383 if (mLocalZone.isEmpty())
384 {
385 // The local time zone is not defined by a file.
386 // Watch for creation of /etc/localtime in case it gets created later.
387 // TODO: If under BSD it is possible for /etc/timezone to be missing but
388 // created later, we should also watch for its creation.
389 mLocalIdFile = QLatin1String("/etc/localtime");
390 }
391 // Watch for changes in the file defining the local time zone so as to be
392 // notified of any change in it.
393 mDirWatch = new KDirWatch(this);
394 mDirWatch->addFile(mLocalIdFile);
395 if (!mLocalIdFile2.isEmpty())
396 mDirWatch->addFile(mLocalIdFile2);
397 if (!mLocalZoneDataFile.isEmpty())
398 mDirWatch->addFile(mLocalZoneDataFile);
399 connect(mDirWatch, SIGNAL(dirty(const QString&)), SLOT(localChanged(const QString&)));
400 connect(mDirWatch, SIGNAL(deleted(const QString&)), SLOT(localChanged(const QString&)));
401 connect(mDirWatch, SIGNAL(created(const QString&)), SLOT(localChanged(const QString&)));
402
403 if (mLocalZone.isEmpty() && !mZoneinfoDir.isEmpty())
404 {
405 // SOLUTION 7: HEURISTIC.
406 // None of the deterministic stuff above has worked: try a heuristic. We
407 // try to find a pair of matching time zone abbreviations...that way, we'll
408 // likely return a value in the user's own country.
409 tzset();
410 QByteArray tzname0(tzname[0]); // store copies, because zone.parse() will change them
411 QByteArray tzname1(tzname[1]);
412 int bestOffset = INT_MAX;
413 KSystemTimeZoneSource::startParseBlock();
414 const KTimeZones::ZoneMap zmap = mZones.zones();
415 for (KTimeZones::ZoneMap::ConstIterator it = zmap.constBegin(), end = zmap.constEnd(); it != end; ++it)
416 {
417 KTimeZone zone = it.value();
418 int candidateOffset = qAbs(zone.currentOffset(Qt::LocalTime));
419 if (candidateOffset < bestOffset
420 && zone.parse())
421 {
422 QList<QByteArray> abbrs = zone.abbreviations();
423 if (abbrs.contains(tzname0) && abbrs.contains(tzname1))
424 {
425 // kDebug(1221) << "local=" << zone.name();
426 mLocalZone = zone.name();
427 bestOffset = candidateOffset;
428 if (!bestOffset)
429 break;
430 }
431 }
432 }
433 KSystemTimeZoneSource::endParseBlock();
434 if (!mLocalZone.isEmpty())
435 {
436 mLocalMethod = TzName;
437 kDebug(1221)<<"tzname: "<<mLocalZone;
438 }
439 }
440 if (mLocalZone.isEmpty())
441 {
442 // SOLUTION 8: FAILSAFE.
443 mLocalZone = KTimeZone::utc().name();
444 mLocalMethod = Utc;
445 if (!mLocalZone.isEmpty()) kDebug(1221)<<"Failsafe: "<<mLocalZone;
446 }
447
448 // Finally, if the local zone identity has changed, store
449 // the new one in the config file.
450 updateLocalZone();
451}
452
453// Called when KDirWatch detects a change in zone.tab
454void KTimeZoned::zonetab_Changed(const QString& path)
455{
456 kDebug(1221) << "zone.tab changed";
457 if (path != mZoneTab)
458 {
459 kError(1221) << "Wrong path (" << path << ") for zone.tab";
460 return;
461 }
462 QDBusMessage message = QDBusMessage::createSignal("/Daemon", "org.kde.KTimeZoned", "zonetabChanged");
463 QList<QVariant> args;
464 args += mZoneTab;
465 message.setArguments(args);
466 QDBusConnection::sessionBus().send(message);
467
468 // Reread zone.tab and recreate the collection of KTimeZone instances,
469 // in case any zones have been created or deleted and one of them
470 // subsequently becomes the local zone.
471 QFile f;
472 f.setFileName(mZoneTab);
473 if (!f.open(QIODevice::ReadOnly))
474 kError(1221) << "Could not open zone.tab (" << mZoneTab << ") to reread";
475 else
476 readZoneTab(f);
477}
478
479// Called when KDirWatch detects a change
480void KTimeZoned::localChanged(const QString& path)
481{
482 if (path == mLocalZoneDataFile)
483 {
484 // Only need to update the definition of the local zone,
485 // not its identity.
486 QDBusMessage message = QDBusMessage::createSignal("/Daemon", "org.kde.KTimeZoned", "zoneDefinitionChanged");
487 QList<QVariant> args;
488 args += mLocalZone;
489 message.setArguments(args);
490 QDBusConnection::sessionBus().send(message);
491 return;
492 }
493 QString oldDataFile = mLocalZoneDataFile;
494 switch (mLocalMethod)
495 {
496 case EnvTzLink:
497 case EnvTzFile:
498 {
499 const char *envtz = ::getenv("TZ");
500 if (mSavedTZ != envtz)
501 {
502 // TZ has changed - start from scratch again
503 findLocalZone();
504 return;
505 }
506 // The contents of the file pointed to by TZ has changed.
507 }
508 // Fall through to LocaltimeLink
509 case LocaltimeLink:
510 case LocaltimeCopy:
511 // The fallback methods below also set a watch for /etc/localtime in
512 // case it gets created.
513 case TzName:
514 case Utc:
515 matchZoneFile(mLocalIdFile);
516 break;
517 case Timezone:
518 checkTimezone();
519 break;
520 case RcFile:
521 checkRcFile();
522 break;
523 case DefaultInit:
524 checkDefaultInit();
525 break;
526 default:
527 return;
528 }
529 if (oldDataFile != mLocalZoneDataFile)
530 {
531 if (!oldDataFile.isEmpty())
532 mDirWatch->removeFile(oldDataFile);
533 if (!mLocalZoneDataFile.isEmpty())
534 mDirWatch->addFile(mLocalZoneDataFile);
535 }
536 updateLocalZone();
537}
538
539bool KTimeZoned::checkTZ(const char *envZone)
540{
541 // SOLUTION 1: DEFINITIVE.
542 // First try the simplest solution of checking for well-formed TZ setting.
543 if (envZone)
544 {
545 if (envZone[0] == '\0')
546 {
547 mLocalMethod = EnvTz;
548 mLocalZone = KTimeZone::utc().name();
549 mLocalIdFile.clear();
550 mLocalZoneDataFile.clear();
551 return true;
552 }
553 if (envZone[0] == ':')
554 {
555 // TZ specifies a file name, either relative to zoneinfo/ or absolute.
556 QString TZfile = QFile::decodeName(envZone + 1);
557 if (TZfile.startsWith(mZoneinfoDir))
558 {
559 // It's an absolute file name in the zoneinfo directory.
560 // Convert it to a file name relative to zoneinfo/.
561 TZfile = TZfile.mid(mZoneinfoDir.length());
562 }
563 if (TZfile.startsWith(QLatin1Char('/')))
564 {
565 // It's an absolute file name.
566 QString symlink;
567 if (matchZoneFile(TZfile))
568 {
569 mLocalMethod = static_cast<LocalMethod>(EnvTz | (mLocalMethod & TypeMask));
570 return true;
571 }
572 }
573 else if (!TZfile.isEmpty())
574 {
575 // It's a file name relative to zoneinfo/
576 mLocalZone = TZfile;
577 if (!mLocalZone.isEmpty())
578 {
579 mLocalMethod = EnvTz;
580 mLocalZoneDataFile = mZoneinfoDir + '/' + TZfile;
581 mLocalIdFile.clear();
582 return true;
583 }
584 }
585 }
586 }
587 return false;
588}
589
590bool KTimeZoned::checkTimezone()
591{
592 // SOLUTION 2: DEFINITIVE.
593 // BSD support.
594 QFile f;
595 f.setFileName(QLatin1String("/etc/timezone"));
596 if (!f.open(QIODevice::ReadOnly))
597 return false;
598 // Read the first line of the file.
599 QTextStream ts(&f);
600 ts.setCodec("ISO-8859-1");
601 QString zoneName;
602 if (!ts.atEnd())
603 zoneName = ts.readLine();
604 f.close();
605 if (zoneName.isEmpty())
606 return false;
607 if (!setLocalZone(zoneName))
608 return false;
609 mLocalMethod = Timezone;
610 mLocalIdFile = f.fileName();
611 kDebug(1221)<<"/etc/timezone: "<<mLocalZone;
612 return true;
613}
614
615bool KTimeZoned::matchZoneFile(const QString &path)
616{
617 // SOLUTION 3: DEFINITIVE.
618 // Try to follow any symlink to a zoneinfo file.
619 // Get the path of the file which the symlink points to.
620 QFile f;
621 f.setFileName(path);
622 QFileInfo fi(f);
623 if (fi.isSymLink())
624 {
625 // The file is a symlink.
626 QString zoneInfoFileName = fi.canonicalFilePath();
627 QFileInfo fiz(zoneInfoFileName);
628 if (fiz.exists() && fiz.isReadable())
629 {
630 if (zoneInfoFileName.startsWith(mZoneinfoDir))
631 {
632 // We've got the zoneinfo file path.
633 // The time zone name is the part of the path after the zoneinfo directory.
634 // Note that some systems (e.g. Gentoo) have zones under zoneinfo which
635 // are not in zone.tab, so don't validate against mZones.
636 mLocalZone = zoneInfoFileName.mid(mZoneinfoDir.length() + 1);
637 // kDebug(1221) << "local=" << mLocalZone;
638 }
639 else
640 {
641 // It isn't a zoneinfo file or a copy thereof.
642 // Use the absolute path as the time zone name.
643 mLocalZone = f.fileName();
644 }
645 mLocalMethod = LocaltimeLink;
646 mLocalIdFile = f.fileName();
647 mLocalZoneDataFile = zoneInfoFileName;
648 kDebug(1221)<<mLocalIdFile<<": "<<mLocalZone;
649 return true;
650 }
651 }
652 else if (f.open(QIODevice::ReadOnly))
653 {
654 // SOLUTION 4: DEFINITIVE.
655 // Try to match the file against the list of zoneinfo files.
656
657 // Compute the file's MD5 sum.
658 KMD5 context("");
659 context.reset();
660 context.update(f);
661 qlonglong referenceSize = f.size();
662 QString referenceMd5Sum = context.hexDigest();
663 MD5Map::ConstIterator it5, end5;
664 KTimeZone local;
665 QString zoneName;
666
667 if (!mConfigLocalZone.isEmpty())
668 {
669 // We know the local zone from last time.
670 // Check whether the file still matches it.
671 KTimeZone tzone = mZones.zone(mConfigLocalZone);
672 if (tzone.isValid())
673 {
674 local = compareChecksum(tzone, referenceMd5Sum, referenceSize);
675 if (local.isValid())
676 zoneName = local.name();
677 }
678 }
679
680 if (!local.isValid() && mHaveCountryCodes)
681 {
682 /* Look for time zones with the user's country code.
683 * This has two advantages: 1) it shortens the search;
684 * 2) it increases the chance of the correctly titled time zone
685 * being found, since multiple time zones can have identical
686 * definitions. For example, Europe/Guernsey is identical to
687 * Europe/London, but the latter is more likely to be the right
688 * zone name for a user with 'gb' country code.
689 */
690 QString country = KGlobal::locale()->country().toUpper();
691 const KTimeZones::ZoneMap zmap = mZones.zones();
692 for (KTimeZones::ZoneMap::ConstIterator zit = zmap.constBegin(), zend = zmap.constEnd(); zit != zend; ++zit)
693 {
694 KTimeZone tzone = zit.value();
695 if (tzone.countryCode() == country)
696 {
697 local = compareChecksum(tzone, referenceMd5Sum, referenceSize);
698 if (local.isValid())
699 {
700 zoneName = local.name();
701 break;
702 }
703 }
704 }
705 }
706
707 if (!local.isValid())
708 {
709 // Look for a checksum match with the cached checksum values
710 MD5Map oldChecksums = mMd5Sums; // save a copy of the existing checksums
711 for (it5 = mMd5Sums.constBegin(), end5 = mMd5Sums.constEnd(); it5 != end5; ++it5)
712 {
713 if (it5.value() == referenceMd5Sum)
714 {
715 // The cached checksum matches. Ensure that the file hasn't changed.
716 if (compareChecksum(it5, referenceMd5Sum, referenceSize))
717 {
718 zoneName = it5.key();
719 local = mZones.zone(zoneName);
720 if (local.isValid())
721 break;
722 }
723 oldChecksums.clear(); // the cache has been cleared
724 break;
725 }
726 }
727
728 if (!local.isValid())
729 {
730 // The checksum didn't match any in the cache.
731 // Continue building missing entries in the cache on the assumption that
732 // we haven't previously looked at the zoneinfo file which matches.
733 const KTimeZones::ZoneMap zmap = mZones.zones();
734 for (KTimeZones::ZoneMap::ConstIterator zit = zmap.constBegin(), zend = zmap.constEnd(); zit != zend; ++zit)
735 {
736 KTimeZone zone = zit.value();
737 zoneName = zone.name();
738 if (!mMd5Sums.contains(zoneName))
739 {
740 QString candidateMd5Sum = calcChecksum(zoneName, referenceSize);
741 if (candidateMd5Sum == referenceMd5Sum)
742 {
743 // kDebug(1221) << "local=" << zone.name();
744 local = zone;
745 break;
746 }
747 }
748 }
749 }
750
751 if (!local.isValid())
752 {
753 // Didn't find the file, so presumably a previously cached checksum must
754 // have changed. Delete all the old checksums.
755 MD5Map::ConstIterator mit;
756 MD5Map::ConstIterator mend = oldChecksums.constEnd();
757 for (mit = oldChecksums.constBegin(); mit != mend; ++mit)
758 mMd5Sums.remove(mit.key());
759
760 // And recalculate the old checksums
761 for (mit = oldChecksums.constBegin(); mit != mend; ++mit)
762 {
763 zoneName = mit.key();
764 QString candidateMd5Sum = calcChecksum(zoneName, referenceSize);
765 if (candidateMd5Sum == referenceMd5Sum)
766 {
767 // kDebug(1221) << "local=" << zoneName;
768 local = mZones.zone(zoneName);
769 break;
770 }
771 }
772 }
773 }
774 bool success = false;
775 if (local.isValid())
776 {
777 // The file matches a zoneinfo file
778 mLocalZone = zoneName;
779 mLocalZoneDataFile = mZoneinfoDir + '/' + zoneName;
780 success = true;
781 }
782 else
783 {
784 // The file doesn't match a zoneinfo file. If it's a TZfile, use it directly.
785 // Read the file type identifier.
786 char buff[4];
787 f.reset();
788 QDataStream str(&f);
789 if (str.readRawData(buff, 4) == 4
790 && buff[0] == 'T' && buff[1] == 'Z' && buff[2] == 'i' && buff[3] == 'f')
791 {
792 // Use its absolute path as the zone name.
793 mLocalZone = f.fileName();
794 mLocalZoneDataFile.clear();
795 success = true;
796 }
797 }
798 f.close();
799 if (success)
800 {
801 mLocalMethod = LocaltimeCopy;
802 mLocalIdFile = f.fileName();
803 kDebug(1221)<<mLocalIdFile<<": "<<mLocalZone;
804 return true;
805 }
806 }
807 return false;
808}
809
810bool KTimeZoned::checkRcFile()
811{
812 // SOLUTION 5: DEFINITIVE.
813 // Look for setting in /etc/rc.conf or /etc/rc.local,
814 // with priority to /etc/rc.local.
815 if (findKey(QLatin1String("/etc/rc.local"), "TIMEZONE"))
816 {
817 mLocalIdFile2.clear();
818 kDebug(1221)<<"/etc/rc.local: "<<mLocalZone;
819 }
820 else
821 {
822 if (!findKey(QLatin1String("/etc/rc.conf"), "TIMEZONE"))
823 return false;
824 mLocalIdFile2 = mLocalIdFile;
825 mLocalIdFile = QLatin1String("/etc/rc.local");
826 kDebug(1221)<<"/etc/rc.conf: "<<mLocalZone;
827 }
828 mLocalMethod = RcFile;
829 return true;
830}
831
832bool KTimeZoned::checkDefaultInit()
833{
834 // SOLUTION 6: DEFINITIVE.
835 // Solaris support using /etc/default/init.
836 if (!findKey(QLatin1String("/etc/default/init"), "TZ"))
837 return false;
838 mLocalMethod = DefaultInit;
839 kDebug(1221)<<"/etc/default/init: "<<mLocalZone;
840 return true;
841}
842
843bool KTimeZoned::findKey(const QString &path, const QString &key)
844{
845 QFile f;
846 f.setFileName(path);
847 if (!f.open(QIODevice::ReadOnly))
848 return false;
849 QString line;
850 QString zoneName;
851 QRegExp keyexp('^' + key + "\\s*=\\s*");
852 QTextStream ts(&f);
853 ts.setCodec("ISO-8859-1");
854 while (!ts.atEnd())
855 {
856 line = ts.readLine();
857 if (keyexp.indexIn(line) == 0)
858 {
859 zoneName = line.mid(keyexp.matchedLength());
860 break;
861 }
862 }
863 f.close();
864 if (zoneName.isEmpty())
865 return false;
866 if (!setLocalZone(zoneName))
867 return false;
868 kDebug(1221) << "Key:" << key << "->" << zoneName;
869 mLocalIdFile = f.fileName();
870 return true;
871}
872
873// Check whether the zone name is valid, either as a zone in zone.tab or
874// as another file in the zoneinfo directory.
875// If valid, set the local zone information.
876bool KTimeZoned::setLocalZone(const QString &zoneName)
877{
878 KTimeZone local = mZones.zone(zoneName);
879 if (!local.isValid())
880 {
881 // It isn't a recognised zone in zone.tab.
882 // Note that some systems (e.g. Gentoo) have zones under zoneinfo which
883 // are not in zone.tab, so check if it points to another zone file.
884 if (mZoneinfoDir.isEmpty())
885 return false;
886 QString path = mZoneinfoDir + '/' + zoneName;
887 QFile qf;
888 qf.setFileName(path);
889 QFileInfo fi(qf);
890 if (fi.isSymLink())
891 fi.setFile(fi.canonicalFilePath());
892 if (!fi.exists() || !fi.isReadable())
893 return false;
894 }
895 mLocalZone = zoneName;
896 mLocalZoneDataFile = mZoneinfoDir.isEmpty() ? QString() : mZoneinfoDir + '/' + zoneName;
897 return true;
898}
899
900// Check whether the checksum for a time zone matches a given saved checksum.
901KTimeZone KTimeZoned::compareChecksum(const KTimeZone &zone, const QString &referenceMd5Sum, qlonglong size)
902{
903 MD5Map::ConstIterator it5 = mMd5Sums.constFind(zone.name());
904 if (it5 == mMd5Sums.constEnd())
905 {
906 // No checksum has been computed yet for this zone file.
907 // Compute it now.
908 QString candidateMd5Sum = calcChecksum(zone.name(), size);
909 if (candidateMd5Sum == referenceMd5Sum)
910 {
911 // kDebug(1221) << "local=" << zone.name();
912 return zone;
913 }
914 return KTimeZone();
915 }
916 if (it5.value() == referenceMd5Sum)
917 {
918 // The cached checksum matches. Ensure that the file hasn't changed.
919 if (compareChecksum(it5, referenceMd5Sum, size))
920 return mZones.zone(it5.key());
921 }
922 return KTimeZone();
923}
924
925// Check whether a checksum matches a given saved checksum.
926// Returns false if the file no longer matches and cache was cleared.
927bool KTimeZoned::compareChecksum(MD5Map::ConstIterator it5, const QString &referenceMd5Sum, qlonglong size)
928{
929 // The cached checksum matches. Ensure that the file hasn't changed.
930 QString zoneName = it5.key();
931 QString candidateMd5Sum = calcChecksum(zoneName, size);
932 if (candidateMd5Sum.isNull())
933 mMd5Sums.remove(zoneName); // no match - wrong file size
934 else if (candidateMd5Sum == referenceMd5Sum)
935 return true;
936
937 // File(s) have changed, so clear the cache
938 mMd5Sums.clear();
939 mMd5Sums[zoneName] = candidateMd5Sum; // reinsert the newly calculated checksum
940 return false;
941}
942
943// Calculate the MD5 checksum for the given zone file, provided that its size matches.
944// The calculated checksum is cached.
945QString KTimeZoned::calcChecksum(const QString &zoneName, qlonglong size)
946{
947 QString path = mZoneinfoDir + '/' + zoneName;
948 QFileInfo fi(path);
949 if (static_cast<qlonglong>(fi.size()) == size)
950 {
951 // Only do the heavy lifting for file sizes which match.
952 QFile f;
953 f.setFileName(path);
954 if (f.open(QIODevice::ReadOnly))
955 {
956 KMD5 context("");
957 context.reset();
958 context.update(f);
959 QString candidateMd5Sum = context.hexDigest();
960 f.close();
961 mMd5Sums[zoneName] = candidateMd5Sum; // cache the new checksum
962 return candidateMd5Sum;
963 }
964 }
965 return QString();
966}
967