1 | /* |
2 | This file is part of the KDE libraries |
3 | SPDX-FileCopyrightText: 1999-2000 Waldo Bastian <bastian@kde.org> |
4 | SPDX-FileCopyrightText: 2005-2009 David Faure <faure@kde.org> |
5 | SPDX-FileCopyrightText: 2008 Hamish Rodda <rodda@kde.org> |
6 | SPDX-FileCopyrightText: 2020-2022 Harald Sitter <sitter@kde.org> |
7 | |
8 | SPDX-License-Identifier: LGPL-2.0-only |
9 | */ |
10 | |
11 | #include "ksycoca.h" |
12 | #include "ksycoca_p.h" |
13 | #include "ksycocafactory_p.h" |
14 | #include "ksycocatype.h" |
15 | #include "ksycocautils_p.h" |
16 | #include "sycocadebug.h" |
17 | #include <KConfigGroup> |
18 | #include <KSandbox> |
19 | #include <KSharedConfig> |
20 | |
21 | #include <QCoreApplication> |
22 | #include <QDataStream> |
23 | #include <QFile> |
24 | #include <QFileInfo> |
25 | #include <QMetaMethod> |
26 | #include <QStandardPaths> |
27 | #include <QThread> |
28 | #include <QThreadStorage> |
29 | |
30 | #include <QCryptographicHash> |
31 | #include <fcntl.h> |
32 | #include <kmimetypefactory_p.h> |
33 | #include <kservicefactory_p.h> |
34 | #include <kservicegroupfactory_p.h> |
35 | |
36 | #include "kbuildsycoca_p.h" |
37 | #include "ksycocadevices_p.h" |
38 | |
39 | #ifdef Q_OS_UNIX |
40 | #include <sys/time.h> |
41 | #include <utime.h> |
42 | #endif |
43 | |
44 | /** |
45 | * Sycoca file version number. |
46 | * If the existing file is outdated, it will not get read |
47 | * but instead we'll regenerate a new one. |
48 | * However running apps should still be able to read it, so |
49 | * only add to the data, never remove/modify. |
50 | */ |
51 | #define KSYCOCA_VERSION 306 |
52 | |
53 | #if HAVE_MADVISE || HAVE_MMAP |
54 | #include <sys/mman.h> // This #include was checked when looking for posix_madvise |
55 | #endif |
56 | |
57 | #ifndef MAP_FAILED |
58 | #define MAP_FAILED ((void *)-1) |
59 | #endif |
60 | |
61 | QDataStream &(QDataStream &in, KSycocaHeader &h) |
62 | { |
63 | in >> h.prefixes >> h.timeStamp >> h.language >> h.updateSignature; |
64 | return in; |
65 | } |
66 | |
67 | // The following limitations are in place: |
68 | // Maximum length of a single string: 8192 bytes |
69 | // Maximum length of a string list: 1024 strings |
70 | // Maximum number of entries: 8192 |
71 | // |
72 | // The purpose of these limitations is to limit the impact |
73 | // of database corruption. |
74 | |
75 | Q_DECLARE_OPERATORS_FOR_FLAGS(KSycocaPrivate::BehaviorsIfNotFound) |
76 | |
77 | KSycocaPrivate::KSycocaPrivate(KSycoca *qq) |
78 | : databaseStatus(DatabaseNotOpen) |
79 | , readError(false) |
80 | , timeStamp(0) |
81 | , m_databasePath() |
82 | , updateSig(0) |
83 | , m_fileWatcher(new KDirWatch) |
84 | , m_haveListeners(false) |
85 | , q(qq) |
86 | , sycoca_size(0) |
87 | , sycoca_mmap(nullptr) |
88 | , m_mmapFile(nullptr) |
89 | , m_device(nullptr) |
90 | , m_mimeTypeFactory(nullptr) |
91 | , m_serviceFactory(nullptr) |
92 | , m_serviceGroupFactory(nullptr) |
93 | { |
94 | #ifdef Q_OS_WIN |
95 | /* |
96 | on windows we use KMemFile (QSharedMemory) to avoid problems |
97 | with mmap (can't delete a mmap'd file) |
98 | */ |
99 | m_sycocaStrategy = StrategyMemFile; |
100 | #else |
101 | m_sycocaStrategy = StrategyMmap; |
102 | #endif |
103 | KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("KSycoca" )); |
104 | setStrategyFromString(config.readEntry(key: "strategy" )); |
105 | } |
106 | |
107 | void KSycocaPrivate::setStrategyFromString(const QString &strategy) |
108 | { |
109 | if (strategy == QLatin1String("mmap" )) { |
110 | m_sycocaStrategy = StrategyMmap; |
111 | } else if (strategy == QLatin1String("file" )) { |
112 | m_sycocaStrategy = StrategyFile; |
113 | } else if (strategy == QLatin1String("sharedmem" )) { |
114 | m_sycocaStrategy = StrategyMemFile; |
115 | } else if (!strategy.isEmpty()) { |
116 | qCWarning(SYCOCA) << "Unknown sycoca strategy:" << strategy; |
117 | } |
118 | } |
119 | |
120 | bool KSycocaPrivate::tryMmap() |
121 | { |
122 | #if HAVE_MMAP |
123 | Q_ASSERT(!m_databasePath.isEmpty()); |
124 | m_mmapFile = new QFile(m_databasePath); |
125 | const bool canRead = m_mmapFile->open(flags: QIODevice::ReadOnly); |
126 | Q_ASSERT(canRead); |
127 | if (!canRead) { |
128 | return false; |
129 | } |
130 | fcntl(fd: m_mmapFile->handle(), F_SETFD, FD_CLOEXEC); |
131 | sycoca_size = m_mmapFile->size(); |
132 | void *mmapRet = mmap(addr: nullptr, len: sycoca_size, PROT_READ, MAP_SHARED, fd: m_mmapFile->handle(), offset: 0); |
133 | /* POSIX mandates only MAP_FAILED, but we are paranoid so check for |
134 | null pointer too. */ |
135 | if (mmapRet == MAP_FAILED || mmapRet == nullptr) { |
136 | qCDebug(SYCOCA).nospace() << "mmap failed. (length = " << sycoca_size << ")" ; |
137 | sycoca_mmap = nullptr; |
138 | return false; |
139 | } else { |
140 | sycoca_mmap = static_cast<const char *>(mmapRet); |
141 | #if HAVE_MADVISE |
142 | (void)posix_madvise(addr: mmapRet, len: sycoca_size, POSIX_MADV_WILLNEED); |
143 | #endif // HAVE_MADVISE |
144 | return true; |
145 | } |
146 | #else |
147 | return false; |
148 | #endif // HAVE_MMAP |
149 | } |
150 | |
151 | int KSycoca::version() |
152 | { |
153 | return KSYCOCA_VERSION; |
154 | } |
155 | |
156 | class KSycocaSingleton |
157 | { |
158 | public: |
159 | KSycocaSingleton() |
160 | { |
161 | } |
162 | ~KSycocaSingleton() |
163 | { |
164 | } |
165 | |
166 | bool hasSycoca() const |
167 | { |
168 | return m_threadSycocas.hasLocalData(); |
169 | } |
170 | KSycoca *sycoca() |
171 | { |
172 | if (!m_threadSycocas.hasLocalData()) { |
173 | m_threadSycocas.setLocalData(new KSycoca); |
174 | } |
175 | return m_threadSycocas.localData(); |
176 | } |
177 | void setSycoca(KSycoca *s) |
178 | { |
179 | m_threadSycocas.setLocalData(s); |
180 | } |
181 | |
182 | private: |
183 | QThreadStorage<KSycoca *> m_threadSycocas; |
184 | }; |
185 | |
186 | Q_GLOBAL_STATIC(KSycocaSingleton, ksycocaInstance) |
187 | |
188 | QString KSycocaPrivate::findDatabase() |
189 | { |
190 | Q_ASSERT(databaseStatus == DatabaseNotOpen); |
191 | |
192 | const QString path = KSycoca::absoluteFilePath(); |
193 | const QFileInfo info(path); |
194 | if (info.isReadable()) { |
195 | if (m_haveListeners && m_fileWatcher) { |
196 | m_fileWatcher->addFile(file: path); |
197 | } |
198 | return path; |
199 | } |
200 | // Let's be notified when it gets created - by another process or by ourselves |
201 | if (m_fileWatcher) { |
202 | m_fileWatcher->addFile(file: path); |
203 | } |
204 | return QString(); |
205 | } |
206 | |
207 | // Read-only constructor |
208 | // One instance per thread |
209 | KSycoca::KSycoca() |
210 | : d(new KSycocaPrivate(this)) |
211 | { |
212 | if (d->m_fileWatcher) { |
213 | // We always delete and recreate the DB, so KDirWatch normally emits created |
214 | connect(sender: d->m_fileWatcher.get(), signal: &KDirWatch::created, context: this, slot: [this]() { |
215 | d->slotDatabaseChanged(); |
216 | }); |
217 | // In some cases, KDirWatch only thinks the file was modified though |
218 | connect(sender: d->m_fileWatcher.get(), signal: &KDirWatch::dirty, context: this, slot: [this]() { |
219 | d->slotDatabaseChanged(); |
220 | }); |
221 | } |
222 | } |
223 | |
224 | bool KSycocaPrivate::openDatabase() |
225 | { |
226 | Q_ASSERT(databaseStatus == DatabaseNotOpen); |
227 | |
228 | delete m_device; |
229 | m_device = nullptr; |
230 | |
231 | if (m_databasePath.isEmpty()) { |
232 | m_databasePath = findDatabase(); |
233 | } |
234 | |
235 | bool result = true; |
236 | if (!m_databasePath.isEmpty()) { |
237 | static bool firstTime = true; |
238 | if (firstTime) { |
239 | firstTime = false; |
240 | if (KSandbox::isFlatpak()) { |
241 | // We're running inside flatpak, which sets all times to 1970 |
242 | // So the first very time, don't use an existing database, recreate it |
243 | qCDebug(SYCOCA) << "flatpak detected, ignoring" << m_databasePath; |
244 | return false; |
245 | } |
246 | } |
247 | |
248 | qCDebug(SYCOCA) << "Opening ksycoca from" << m_databasePath; |
249 | m_dbLastModified = QFileInfo(m_databasePath).lastModified(); |
250 | result = checkVersion(); |
251 | } else { // No database file |
252 | // qCDebug(SYCOCA) << "Could not open ksycoca"; |
253 | result = false; |
254 | } |
255 | return result; |
256 | } |
257 | |
258 | KSycocaAbstractDevice *KSycocaPrivate::device() |
259 | { |
260 | if (m_device) { |
261 | return m_device; |
262 | } |
263 | |
264 | KSycocaAbstractDevice *device = m_device; |
265 | Q_ASSERT(!m_databasePath.isEmpty()); |
266 | #if HAVE_MMAP |
267 | if (m_sycocaStrategy == StrategyMmap && tryMmap()) { |
268 | device = new KSycocaMmapDevice(sycoca_mmap, sycoca_size); |
269 | if (!device->device()->open(mode: QIODevice::ReadOnly)) { |
270 | delete device; |
271 | device = nullptr; |
272 | } |
273 | } |
274 | #endif |
275 | #ifndef QT_NO_SHAREDMEMORY |
276 | if (!device && m_sycocaStrategy == StrategyMemFile) { |
277 | device = new KSycocaMemFileDevice(m_databasePath); |
278 | if (!device->device()->open(mode: QIODevice::ReadOnly)) { |
279 | delete device; |
280 | device = nullptr; |
281 | } |
282 | } |
283 | #endif |
284 | if (!device) { |
285 | device = new KSycocaFileDevice(m_databasePath); |
286 | if (!device->device()->open(mode: QIODevice::ReadOnly)) { |
287 | qCWarning(SYCOCA) << "Couldn't open" << m_databasePath << "even though it is readable? Impossible." ; |
288 | // delete device; device = 0; // this would crash in the return statement... |
289 | } |
290 | } |
291 | if (device) { |
292 | m_device = device; |
293 | } |
294 | return m_device; |
295 | } |
296 | |
297 | QDataStream *&KSycocaPrivate::stream() |
298 | { |
299 | if (!m_device) { |
300 | if (databaseStatus == DatabaseNotOpen) { |
301 | checkDatabase(ifNotFound: KSycocaPrivate::IfNotFoundRecreate); |
302 | } |
303 | |
304 | device(); // create m_device |
305 | } |
306 | |
307 | return m_device->stream(); |
308 | } |
309 | |
310 | void KSycocaPrivate::slotDatabaseChanged() |
311 | { |
312 | qCDebug(SYCOCA) << QThread::currentThread() << "got a notifyDatabaseChanged signal" ; |
313 | // In case we have changed the database outselves, we have already notified the application |
314 | if (!m_dbLastModified.isValid() || m_dbLastModified != QFileInfo(m_databasePath).lastModified()) { |
315 | // KDirWatch tells us the database file changed |
316 | // We would have found out in the next call to ensureCacheValid(), but for |
317 | // now keep the call to closeDatabase, to help refcounting to 0 the old mmapped file earlier. |
318 | closeDatabase(); |
319 | // Start monitoring the new file right away |
320 | m_databasePath = findDatabase(); |
321 | |
322 | // Now notify applications |
323 | Q_EMIT q->databaseChanged(); |
324 | } |
325 | } |
326 | |
327 | KMimeTypeFactory *KSycocaPrivate::mimeTypeFactory() |
328 | { |
329 | if (!m_mimeTypeFactory) { |
330 | m_mimeTypeFactory = new KMimeTypeFactory(q); |
331 | } |
332 | return m_mimeTypeFactory; |
333 | } |
334 | |
335 | KServiceFactory *KSycocaPrivate::serviceFactory() |
336 | { |
337 | if (!m_serviceFactory) { |
338 | m_serviceFactory = new KServiceFactory(q); |
339 | } |
340 | return m_serviceFactory; |
341 | } |
342 | |
343 | KServiceGroupFactory *KSycocaPrivate::serviceGroupFactory() |
344 | { |
345 | if (!m_serviceGroupFactory) { |
346 | m_serviceGroupFactory = new KServiceGroupFactory(q); |
347 | } |
348 | return m_serviceGroupFactory; |
349 | } |
350 | |
351 | // Read-write constructor - only for KBuildSycoca |
352 | KSycoca::KSycoca(bool /* dummy */) |
353 | : d(new KSycocaPrivate(this)) |
354 | { |
355 | } |
356 | |
357 | KSycoca *KSycoca::self() |
358 | { |
359 | KSycoca *s = ksycocaInstance()->sycoca(); |
360 | Q_ASSERT(s); |
361 | return s; |
362 | } |
363 | |
364 | KSycoca::~KSycoca() |
365 | { |
366 | d->closeDatabase(); |
367 | delete d; |
368 | // if (ksycocaInstance.exists() |
369 | // && ksycocaInstance->self == this) |
370 | // ksycocaInstance->self = 0; |
371 | } |
372 | |
373 | bool KSycoca::isAvailable() // TODO KF6: make it non-static (mostly useful for unittests) |
374 | { |
375 | return self()->d->checkDatabase(ifNotFound: KSycocaPrivate::IfNotFoundDoNothing); |
376 | } |
377 | |
378 | void KSycocaPrivate::closeDatabase() |
379 | { |
380 | delete m_device; |
381 | m_device = nullptr; |
382 | |
383 | // It is very important to delete all factories here |
384 | // since they cache information about the database file |
385 | // But other threads might be using them, so this class is |
386 | // refcounted, and deleted when the last thread is done with them |
387 | qDeleteAll(c: m_factories); |
388 | m_factories.clear(); |
389 | |
390 | m_mimeTypeFactory = nullptr; |
391 | m_serviceFactory = nullptr; |
392 | m_serviceGroupFactory = nullptr; |
393 | |
394 | #if HAVE_MMAP |
395 | if (sycoca_mmap) { |
396 | // Solaris has munmap(char*, size_t) and everything else should |
397 | // be happy with a char* for munmap(void*, size_t) |
398 | munmap(addr: const_cast<char *>(sycoca_mmap), len: sycoca_size); |
399 | sycoca_mmap = nullptr; |
400 | } |
401 | delete m_mmapFile; |
402 | m_mmapFile = nullptr; |
403 | #endif |
404 | |
405 | databaseStatus = DatabaseNotOpen; |
406 | m_databasePath.clear(); |
407 | timeStamp = 0; |
408 | } |
409 | |
410 | void KSycoca::addFactory(KSycocaFactory *factory) |
411 | { |
412 | d->addFactory(factory); |
413 | } |
414 | |
415 | QDataStream *KSycoca::findEntry(int offset, KSycocaType &type) |
416 | { |
417 | QDataStream *str = stream(); |
418 | Q_ASSERT(str); |
419 | // qCDebug(SYCOCA) << QString("KSycoca::_findEntry(offset=%1)").arg(offset,8,16); |
420 | str->device()->seek(pos: offset); |
421 | qint32 aType; |
422 | *str >> aType; |
423 | type = KSycocaType(aType); |
424 | // qCDebug(SYCOCA) << QString("KSycoca::found type %1").arg(aType); |
425 | return str; |
426 | } |
427 | |
428 | KSycocaFactoryList *KSycoca::factories() |
429 | { |
430 | return d->factories(); |
431 | } |
432 | |
433 | // Warning, checkVersion rewinds to the beginning of stream(). |
434 | bool KSycocaPrivate::checkVersion() |
435 | { |
436 | QDataStream *m_str = device()->stream(); |
437 | Q_ASSERT(m_str); |
438 | m_str->device()->seek(pos: 0); |
439 | qint32 aVersion; |
440 | *m_str >> aVersion; |
441 | if (aVersion < KSYCOCA_VERSION) { |
442 | qCDebug(SYCOCA) << "Found version" << aVersion << ", expecting version" << KSYCOCA_VERSION << "or higher." ; |
443 | databaseStatus = BadVersion; |
444 | return false; |
445 | } else { |
446 | databaseStatus = DatabaseOK; |
447 | return true; |
448 | } |
449 | } |
450 | |
451 | // If it returns true, we have a valid database and the stream has rewinded to the beginning |
452 | // and past the version number. |
453 | bool KSycocaPrivate::checkDatabase(BehaviorsIfNotFound ifNotFound) |
454 | { |
455 | if (databaseStatus == DatabaseOK) { |
456 | if (checkVersion()) { // we know the version is ok, but we must rewind the stream anyway |
457 | return true; |
458 | } |
459 | } |
460 | |
461 | closeDatabase(); // close the dummy one |
462 | |
463 | // Check if new database already available |
464 | if (openDatabase()) { |
465 | // Database exists, and version is ok, we can read it. |
466 | |
467 | if (qAppName() != QLatin1String(KBUILDSYCOCA_EXENAME) && ifNotFound != IfNotFoundDoNothing) { |
468 | // Ensure it's up-to-date, rebuild if needed |
469 | checkDirectories(); |
470 | |
471 | // Don't check again for some time |
472 | m_lastCheck.start(); |
473 | } |
474 | |
475 | return true; |
476 | } |
477 | |
478 | if (ifNotFound & IfNotFoundRecreate) { |
479 | return buildSycoca(); |
480 | } |
481 | |
482 | return false; |
483 | } |
484 | |
485 | QDataStream *KSycoca::findFactory(KSycocaFactoryId id) |
486 | { |
487 | // Ensure we have a valid database (right version, and rewinded to beginning) |
488 | if (!d->checkDatabase(ifNotFound: KSycocaPrivate::IfNotFoundRecreate)) { |
489 | return nullptr; |
490 | } |
491 | |
492 | QDataStream *str = stream(); |
493 | Q_ASSERT(str); |
494 | |
495 | qint32 aId; |
496 | qint32 aOffset; |
497 | while (true) { |
498 | *str >> aId; |
499 | if (aId == 0) { |
500 | qCWarning(SYCOCA) << "Error, KSycocaFactory (id =" << int(id) << ") not found!" ; |
501 | break; |
502 | } |
503 | *str >> aOffset; |
504 | if (aId == id) { |
505 | // qCDebug(SYCOCA) << "KSycoca::findFactory(" << id << ") offset " << aOffset; |
506 | str->device()->seek(pos: aOffset); |
507 | return str; |
508 | } |
509 | } |
510 | return nullptr; |
511 | } |
512 | |
513 | bool KSycoca::needsRebuild() |
514 | { |
515 | return d->needsRebuild(); |
516 | } |
517 | |
518 | KSycocaHeader KSycocaPrivate::() |
519 | { |
520 | KSycocaHeader ; |
521 | // do not try to launch kbuildsycoca from here; this code is also called by kbuildsycoca. |
522 | if (!checkDatabase(ifNotFound: KSycocaPrivate::IfNotFoundDoNothing)) { |
523 | return header; |
524 | } |
525 | QDataStream *str = stream(); |
526 | qint64 oldPos = str->device()->pos(); |
527 | |
528 | Q_ASSERT(str); |
529 | qint32 aId; |
530 | qint32 aOffset; |
531 | // skip factories offsets |
532 | while (true) { |
533 | *str >> aId; |
534 | if (aId) { |
535 | *str >> aOffset; |
536 | } else { |
537 | break; // just read 0 |
538 | } |
539 | } |
540 | // We now point to the header |
541 | QStringList directoryList; |
542 | *str >> header >> directoryList; |
543 | allResourceDirs.clear(); |
544 | for (int i = 0; i < directoryList.count(); ++i) { |
545 | qint64 mtime; |
546 | *str >> mtime; |
547 | allResourceDirs.insert(key: directoryList.at(i), value: mtime); |
548 | } |
549 | |
550 | QStringList fileList; |
551 | *str >> fileList; |
552 | extraFiles.clear(); |
553 | for (const auto &fileName : std::as_const(t&: fileList)) { |
554 | qint64 mtime; |
555 | *str >> mtime; |
556 | extraFiles.insert(key: fileName, value: mtime); |
557 | } |
558 | |
559 | str->device()->seek(pos: oldPos); |
560 | |
561 | timeStamp = header.timeStamp; |
562 | |
563 | // for the useless public accessors. KF6: remove these two lines, the accessors and the vars. |
564 | language = header.language; |
565 | updateSig = header.updateSignature; |
566 | |
567 | return header; |
568 | } |
569 | |
570 | class TimestampChecker |
571 | { |
572 | public: |
573 | TimestampChecker() |
574 | : m_now(QDateTime::currentDateTime()) |
575 | { |
576 | } |
577 | |
578 | // Check times of last modification of all directories on which ksycoca depends, |
579 | // If none of them is newer than the mtime we stored for that directory at the |
580 | // last rebuild, this means that there's no need to rebuild ksycoca. |
581 | bool checkDirectoriesTimestamps(const QMap<QString, qint64> &dirs) const |
582 | { |
583 | Q_ASSERT(!dirs.isEmpty()); |
584 | // qCDebug(SYCOCA) << "checking file timestamps"; |
585 | for (auto it = dirs.begin(); it != dirs.end(); ++it) { |
586 | const QString dir = it.key(); |
587 | const qint64 lastStamp = it.value(); |
588 | |
589 | auto visitor = [&](const QFileInfo &fi) { |
590 | const QDateTime mtime = fi.lastModified(); |
591 | if (mtime.toMSecsSinceEpoch() > lastStamp) { |
592 | if (mtime > m_now) { |
593 | qCDebug(SYCOCA) << fi.filePath() << "has a modification time in the future" << mtime; |
594 | } |
595 | qCDebug(SYCOCA) << "dir timestamp changed:" << fi.filePath() << mtime << ">" << QDateTime::fromMSecsSinceEpoch(msecs: lastStamp); |
596 | // no need to continue search |
597 | return false; |
598 | } |
599 | |
600 | return true; |
601 | }; |
602 | |
603 | if (!KSycocaUtilsPrivate::visitResourceDirectory(dirname: dir, visitor)) { |
604 | return false; |
605 | } |
606 | } |
607 | return true; |
608 | } |
609 | |
610 | bool checkFilesTimestamps(const QMap<QString, qint64> &files) const |
611 | { |
612 | for (auto it = files.begin(); it != files.end(); ++it) { |
613 | const QString fileName = it.key(); |
614 | const qint64 lastStamp = it.value(); |
615 | |
616 | QFileInfo fi(fileName); |
617 | if (!fi.exists()) { |
618 | return false; |
619 | } |
620 | const QDateTime mtime = fi.lastModified(); |
621 | if (mtime.toMSecsSinceEpoch() > lastStamp) { |
622 | if (mtime > m_now) { |
623 | qCDebug(SYCOCA) << fi.filePath() << "has a modification time in the future" << mtime; |
624 | } |
625 | qCDebug(SYCOCA) << "file timestamp changed:" << fi.filePath() << mtime << ">" << QDateTime::fromMSecsSinceEpoch(msecs: lastStamp); |
626 | return false; |
627 | } |
628 | } |
629 | return true; |
630 | } |
631 | |
632 | private: |
633 | QDateTime m_now; |
634 | }; |
635 | |
636 | void KSycocaPrivate::checkDirectories() |
637 | { |
638 | if (needsRebuild()) { |
639 | buildSycoca(); |
640 | } |
641 | } |
642 | |
643 | bool KSycocaPrivate::needsRebuild() |
644 | { |
645 | // In case it is not open, it might be due to another process/thread having rebuild it. Thus we read the header for both the not open and ok state |
646 | if (!timeStamp && databaseStatus != BadVersion) { |
647 | (void)readSycocaHeader(); |
648 | } |
649 | // these days timeStamp is really a "bool headerFound", the value itself doesn't matter... |
650 | // KF6: replace it with bool. |
651 | const auto timestampChecker = TimestampChecker(); |
652 | bool ret = timeStamp != 0 |
653 | && (!timestampChecker.checkDirectoriesTimestamps(dirs: allResourceDirs) // |
654 | || !timestampChecker.checkFilesTimestamps(files: extraFiles)); |
655 | if (ret) { |
656 | return true; |
657 | } |
658 | auto files = KBuildSycoca::factoryExtraFiles(); |
659 | // ensure files are ordered so next comparison works |
660 | files.sort(); |
661 | // to cover cases when extra files were added |
662 | return extraFiles.keys() != files; |
663 | } |
664 | |
665 | bool KSycocaPrivate::buildSycoca() |
666 | { |
667 | KBuildSycoca builder; |
668 | if (!builder.recreate()) { |
669 | return false; // error |
670 | } |
671 | |
672 | closeDatabase(); // close the dummy one |
673 | |
674 | // Ok, the new database should be here now, open it. |
675 | if (!openDatabase()) { |
676 | qCDebug(SYCOCA) << "Still no database..." ; |
677 | return false; |
678 | } |
679 | Q_EMIT q->databaseChanged(); |
680 | return true; |
681 | } |
682 | |
683 | QString KSycoca::absoluteFilePath() |
684 | { |
685 | const QStringList paths = QStandardPaths::standardLocations(type: QStandardPaths::GenericDataLocation); |
686 | QString suffix = QLatin1Char('_') + QLocale().bcp47Name(); |
687 | |
688 | const QByteArray ksycoca_env = qgetenv(varName: "KDESYCOCA" ); |
689 | if (ksycoca_env.isEmpty()) { |
690 | const QByteArray pathHash = QCryptographicHash::hash(data: paths.join(sep: QLatin1Char(':')).toUtf8(), method: QCryptographicHash::Sha1); |
691 | suffix += QLatin1Char('_') + QString::fromLatin1(ba: pathHash.toBase64()); |
692 | suffix.replace(before: QLatin1Char('/'), after: QLatin1Char('_')); |
693 | #ifdef Q_OS_WIN |
694 | suffix.replace(QLatin1Char(':'), QLatin1Char('_')); |
695 | #endif |
696 | const QString fileName = QLatin1String("ksycoca6" ) + suffix; |
697 | return QStandardPaths::writableLocation(type: QStandardPaths::GenericCacheLocation) + QLatin1Char('/') + fileName; |
698 | } else { |
699 | return QFile::decodeName(localFileName: ksycoca_env); |
700 | } |
701 | } |
702 | |
703 | QStringList KSycoca::allResourceDirs() |
704 | { |
705 | if (!d->timeStamp) { |
706 | (void)d->readSycocaHeader(); |
707 | } |
708 | return d->allResourceDirs.keys(); |
709 | } |
710 | |
711 | void KSycoca::flagError() |
712 | { |
713 | qCWarning(SYCOCA) << "ERROR: KSycoca database corruption!" ; |
714 | KSycoca *sycoca = self(); |
715 | if (sycoca->d->readError) { |
716 | return; |
717 | } |
718 | sycoca->d->readError = true; |
719 | if (qAppName() != QLatin1String(KBUILDSYCOCA_EXENAME) && !sycoca->isBuilding()) { |
720 | // Rebuild the damned thing. |
721 | KBuildSycoca builder; |
722 | (void)builder.recreate(); |
723 | } |
724 | } |
725 | |
726 | bool KSycoca::isBuilding() |
727 | { |
728 | return false; |
729 | } |
730 | |
731 | void KSycoca::disableAutoRebuild() |
732 | { |
733 | ksycocaInstance->sycoca()->d->m_fileWatcher = nullptr; |
734 | } |
735 | |
736 | QDataStream *&KSycoca::stream() |
737 | { |
738 | return d->stream(); |
739 | } |
740 | |
741 | void KSycoca::connectNotify(const QMetaMethod &signal) |
742 | { |
743 | if (signal.name() == "databaseChanged" && !d->m_haveListeners) { |
744 | d->m_haveListeners = true; |
745 | if (d->m_databasePath.isEmpty()) { |
746 | d->m_databasePath = d->findDatabase(); |
747 | } else if (d->m_fileWatcher) { |
748 | d->m_fileWatcher->addFile(file: d->m_databasePath); |
749 | } |
750 | } |
751 | } |
752 | |
753 | void KSycoca::clearCaches() |
754 | { |
755 | if (ksycocaInstance.exists() && ksycocaInstance()->hasSycoca()) { |
756 | ksycocaInstance()->sycoca()->d->closeDatabase(); |
757 | } |
758 | } |
759 | |
760 | extern KSERVICE_EXPORT int ksycoca_ms_between_checks; |
761 | KSERVICE_EXPORT int ksycoca_ms_between_checks = 1500; |
762 | |
763 | void KSycoca::ensureCacheValid() |
764 | { |
765 | if (qAppName() == QLatin1String(KBUILDSYCOCA_EXENAME)) { |
766 | return; |
767 | } |
768 | |
769 | if (d->databaseStatus != KSycocaPrivate::DatabaseOK) { |
770 | if (!d->checkDatabase(ifNotFound: KSycocaPrivate::IfNotFoundRecreate)) { |
771 | return; |
772 | } |
773 | } |
774 | |
775 | if (d->m_lastCheck.isValid() && d->m_lastCheck.elapsed() < ksycoca_ms_between_checks) { |
776 | return; |
777 | } |
778 | d->m_lastCheck.start(); |
779 | |
780 | // Check if the file on disk was modified since we last checked it. |
781 | QFileInfo info(d->m_databasePath); |
782 | if (info.lastModified() == d->m_dbLastModified) { |
783 | // Check if the watched directories were modified, then the cache needs a rebuild. |
784 | d->checkDirectories(); |
785 | return; |
786 | } |
787 | |
788 | // Close the database and forget all about what we knew. |
789 | // The next call to any public method will recreate |
790 | // everything that's needed. |
791 | d->closeDatabase(); |
792 | } |
793 | |
794 | void KSycoca::() |
795 | { |
796 | const QByteArray content = R"(<?xml version="1.0"?> |
797 | <!DOCTYPE Menu PUBLIC "-//freedesktop//DTD Menu 1.0//EN" "http://www.freedesktop.org/standards/menu-spec/menu-1.0.dtd"> |
798 | <Menu> |
799 | <Name>Applications</Name> |
800 | <Directory>Applications.directory</Directory> |
801 | <DefaultAppDirs/> |
802 | <DefaultDirectoryDirs/> |
803 | <MergeDir>applications-merged</MergeDir> |
804 | <LegacyDir>/usr/share/applnk</LegacyDir> |
805 | <DefaultLayout> |
806 | <Merge type="menus"/> |
807 | <Merge type="files"/> |
808 | <Separator/> |
809 | <Menuname>More</Menuname> |
810 | </DefaultLayout> |
811 | </Menu> |
812 | )" ; |
813 | |
814 | const QString destDir = QStandardPaths::writableLocation(type: QStandardPaths::GenericConfigLocation) + QLatin1String("/menus" ); |
815 | QDir(destDir).mkpath(QStringLiteral("." )); |
816 | QFile output(destDir + QLatin1String("/applications.menu" )); |
817 | output.open(flags: QIODevice::ReadWrite | QIODevice::Truncate); |
818 | output.write(data: content); |
819 | } |
820 | |
821 | #include "moc_ksycoca.cpp" |
822 | |