diff --git a/CMakeLists.txt b/CMakeLists.txt index 4a080ec..f91d5a7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,129 +1,130 @@ cmake_minimum_required(VERSION 3.5) set(KF5_VERSION "5.56.0") # handled by release scripts set(KF5_DEP_VERSION "5.56.0") # handled by release scripts project(KService VERSION ${KF5_VERSION}) # Disallow in-source build if("${CMAKE_SOURCE_DIR}" STREQUAL "${CMAKE_BINARY_DIR}") message(FATAL_ERROR "KService requires an out of source build. Please create a separate build directory and run 'cmake path_to_kservice [options]' there.") endif() # ECM setup include(FeatureSummary) find_package(ECM 5.56.0 NO_MODULE) set_package_properties(ECM PROPERTIES TYPE REQUIRED DESCRIPTION "Extra CMake Modules." URL "https://projects.kde.org/projects/kdesupport/extra-cmake-modules") feature_summary(WHAT REQUIRED_PACKAGES_NOT_FOUND FATAL_ON_MISSING_REQUIRED_PACKAGES) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/cmake ) include(KDEInstallDirs) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(KDECMakeSettings) include(ECMSetupVersion) include(ECMGenerateHeaders) include(ECMAddQch) include(ECMQtDeclareLoggingCategory) include(GenerateExportHeader) option(BUILD_QCH "Build API documentation in QCH format (for e.g. Qt Assistant, Qt Creator & KDevelop)" OFF) add_feature_info(QCH ${BUILD_QCH} "API documentation in QCH format (for e.g. Qt Assistant, Qt Creator & KDevelop)") ecm_setup_version(PROJECT VARIABLE_PREFIX KSERVICE VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/kservice_version.h" PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KF5ServiceConfigVersion.cmake" SOVERSION 5) set(APPLICATIONS_MENU_NAME applications.menu CACHE STRING "Name to install the applications.menu file as.") # Dependencies set(REQUIRED_QT_VERSION 5.10.0) find_package(Qt5 ${REQUIRED_QT_VERSION} CONFIG REQUIRED Xml) if (NOT ANDROID) find_package(Qt5 ${REQUIRED_QT_VERSION} CONFIG REQUIRED DBus) endif() find_package(KF5Config ${KF5_DEP_VERSION} REQUIRED) find_package(KF5CoreAddons ${KF5_DEP_VERSION} REQUIRED) find_package(KF5Crash ${KF5_DEP_VERSION} REQUIRED) if (NOT ANDROID) find_package(KF5DBusAddons ${KF5_DEP_VERSION} REQUIRED) endif() find_package(KF5I18n ${KF5_DEP_VERSION} REQUIRED) find_package(KF5DocTools ${KF5_DEP_VERSION}) find_package(FLEX REQUIRED) set_package_properties(FLEX PROPERTIES URL "http://flex.sourceforge.net" DESCRIPTION "Fast Lexical Analyzer" TYPE REQUIRED PURPOSE "Required for the Trader parser" ) find_package(BISON REQUIRED) set_package_properties(BISON PROPERTIES URL "http://www.gnu.org/software/bison" DESCRIPTION "general-purpose parser generator" TYPE REQUIRED PURPOSE "Required for the Trader parser" ) add_definitions(-DTRANSLATION_DOMAIN=\"kservice5\") +add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x050c00) if (IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/po") ki18n_install(po) if (KF5DocTools_FOUND) kdoctools_install(po) endif() endif() if (KF5DocTools_FOUND) add_subdirectory(docs) endif() add_subdirectory(src) if (BUILD_TESTING) add_subdirectory(autotests) add_subdirectory(tests) endif() # create a Config.cmake and a ConfigVersion.cmake file and install them set(CMAKECONFIG_INSTALL_DIR "${KDE_INSTALL_CMAKEPACKAGEDIR}/KF5Service") if (BUILD_QCH) ecm_install_qch_export( TARGETS KF5Service_QCH FILE KF5ServiceQchTargets.cmake DESTINATION "${CMAKECONFIG_INSTALL_DIR}" COMPONENT Devel ) set(PACKAGE_INCLUDE_QCHTARGETS "include(\"\${CMAKE_CURRENT_LIST_DIR}/KF5ServiceQchTargets.cmake\")") endif() include(CMakePackageConfigHelpers) configure_package_config_file("${CMAKE_CURRENT_SOURCE_DIR}/KF5ServiceConfig.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/KF5ServiceConfig.cmake" INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR} ) install(EXPORT KF5ServiceTargets DESTINATION "${CMAKECONFIG_INSTALL_DIR}" FILE KF5ServiceTargets.cmake NAMESPACE KF5:: ) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/KF5ServiceConfig.cmake" "${CMAKE_CURRENT_BINARY_DIR}/KF5ServiceConfigVersion.cmake" "${CMAKE_CURRENT_SOURCE_DIR}/KF5ServiceMacros.cmake" DESTINATION "${CMAKECONFIG_INSTALL_DIR}" COMPONENT Devel ) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/kservice_version.h DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5} COMPONENT Devel ) # contains list of debug categories, for kdebugsettings install(FILES kservice.categories DESTINATION ${KDE_INSTALL_CONFDIR}) feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/autotests/kmimeassociationstest.cpp b/autotests/kmimeassociationstest.cpp index b96cb0a..a51a54a 100644 --- a/autotests/kmimeassociationstest.cpp +++ b/autotests/kmimeassociationstest.cpp @@ -1,543 +1,543 @@ /* This file is part of the KDE libraries Copyright (c) 2008 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), 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 Lesser 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 #include #include #include #include #include #include #include #include #include "setupxdgdirs.h" #include "kmimeassociations_p.h" #include #include #include "ksycoca_p.h" #include #include #include // We need a factory that returns the same KService::Ptr every time it's asked for a given service. // Otherwise the changes to the service's serviceTypes by KMimeAssociationsTest have no effect class FakeServiceFactory : public KServiceFactory { public: FakeServiceFactory(KSycoca *db) : KServiceFactory(db) {} ~FakeServiceFactory(); KService::Ptr findServiceByMenuId(const QString &name) override { //qDebug() << name; KService::Ptr result = m_cache.value(name); if (!result) { result = KServiceFactory::findServiceByMenuId(name); m_cache.insert(name, result); } //qDebug() << name << result.data(); return result; } KService::Ptr findServiceByDesktopPath(const QString &name) override { KService::Ptr result = m_cache.value(name); // yeah, same cache, I don't care :) if (!result) { result = KServiceFactory::findServiceByDesktopPath(name); m_cache.insert(name, result); } return result; } private: QMap m_cache; }; // Helper method for all the trader tests, comes from kmimetypetest.cpp static bool offerListHasService(const KService::List &offers, const QString &entryPath, bool expected /* if set, show error if not found */) { bool found = false; Q_FOREACH (const KService::Ptr &serv, offers) { if (serv->entryPath() == entryPath) { if (found) { // should be there only once qWarning("ERROR: %s was found twice in the list", qPrintable(entryPath)); return false; // make test fail } found = true; } } if (!found && expected) { qWarning() << "ERROR:" << entryPath << "not found in offer list. Here's the full list:"; Q_FOREACH (const KService::Ptr &serv, offers) { qDebug() << serv->entryPath(); } } return found; } static void writeAppDesktopFile(const QString &path, const QStringList &mimeTypes, int initialPreference = 1) { KDesktopFile file(path); KConfigGroup group = file.desktopGroup(); group.writeEntry("Name", "FakeApplication"); group.writeEntry("Type", "Application"); group.writeEntry("Exec", "ls"); group.writeEntry("OnlyShowIn", "KDE;UDE"); group.writeEntry("NotShowIn", "GNOME"); group.writeEntry("InitialPreference", initialPreference); group.writeXdgListEntry("MimeType", mimeTypes); } /** * This unit test verifies the parsing of mimeapps.list files, both directly * and via kbuildsycoca (and making trader queries). */ class KMimeAssociationsTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase() { setupXdgDirs(); QStandardPaths::setTestModeEnabled(true); qputenv("XDG_CURRENT_DESKTOP", "KDE"); m_localConfig = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1Char('/'); QDir(m_localConfig).removeRecursively(); QVERIFY(QDir().mkpath(m_localConfig)); m_localApps = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + QLatin1Char('/'); QDir(m_localApps).removeRecursively(); QVERIFY(QDir().mkpath(m_localApps)); QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1Char('/'); QDir(cacheDir).removeRecursively(); // Create fake application (associated with text/plain in mimeapps.list) fakeTextApplication = m_localApps + "faketextapplication.desktop"; writeAppDesktopFile(fakeTextApplication, QStringList() << QStringLiteral("text/plain")); // Create fake application (associated with text/plain in mimeapps.list) fakeTextApplicationPrefixed = m_localApps + "fakepfx/faketextapplicationpfx.desktop"; writeAppDesktopFile(fakeTextApplicationPrefixed, QStringList() << QStringLiteral("text/plain")); // A fake "default" application for text/plain (high initial preference, but not in mimeapps.list) fakeDefaultTextApplication = m_localApps + "fakedefaulttextapplication.desktop"; writeAppDesktopFile(fakeDefaultTextApplication, QStringList() << QStringLiteral("text/plain"), 9); // An app (like emacs) listing explicitly the derived mimetype (c-src); not in mimeapps.list // This interacted badly with mimeapps.list listing another app for text/plain, but the // lookup found this app first, due to c-src. The fix: ignoring derived mimetypes when // the base mimetype is already listed. // // Also include aliases (msword), to check they don't cancel each other out. fakeCSrcApplication = m_localApps + "fakecsrcmswordapplication.desktop"; writeAppDesktopFile(fakeCSrcApplication, QStringList() << QStringLiteral("text/plain") << QStringLiteral("text/c-src") << QStringLiteral("application/vnd.ms-word") << QStringLiteral("application/msword"), 8); fakeJpegApplication = m_localApps + "fakejpegapplication.desktop"; writeAppDesktopFile(fakeJpegApplication, QStringList() << QStringLiteral("image/jpeg")); fakeArkApplication = m_localApps + "fakearkapplication.desktop"; writeAppDesktopFile(fakeArkApplication, QStringList() << QStringLiteral("application/zip")); fakeHtmlApplication = m_localApps + "fakehtmlapplication.desktop"; writeAppDesktopFile(fakeHtmlApplication, QStringList() << QStringLiteral("text/html")); fakeHtmlApplicationPrefixed = m_localApps + "fakepfx/fakehtmlapplicationpfx.desktop"; writeAppDesktopFile(fakeHtmlApplicationPrefixed, QStringList() << QStringLiteral("text/html")); // Update ksycoca in ~/.qttest after creating the above runKBuildSycoca(); // Create factory on the heap and don't delete it. This must happen after // Sycoca is built, in case it did not exist before. // It registers to KSycoca, which deletes it at end of program execution. KServiceFactory *factory = new FakeServiceFactory(KSycoca::self()); KSycocaPrivate::self()->m_serviceFactory = factory; QCOMPARE(KSycocaPrivate::self()->serviceFactory(), factory); // For debugging: print all services and their storageId #if 0 const KService::List lst = KService::allServices(); QVERIFY(!lst.isEmpty()); Q_FOREACH (const KService::Ptr &serv, lst) { qDebug() << serv->entryPath() << serv->storageId() /*<< serv->desktopEntryName()*/; } #endif KService::Ptr fakeApplicationService = KService::serviceByStorageId(QStringLiteral("faketextapplication.desktop")); QVERIFY(fakeApplicationService); m_mimeAppsFileContents = "[Added Associations]\n" "image/jpeg=fakejpegapplication.desktop;\n" "text/html=fakehtmlapplication.desktop;fakehtmlapplicationpfx.desktop;\n" "text/plain=faketextapplication.desktop;fakepfx-faketextapplicationpfx.desktop;gvim.desktop;wine.desktop;idontexist.desktop;\n" // test alias resolution "application/x-pdf=fakejpegapplication.desktop;\n" // test x-scheme-handler (#358159) (missing trailing ';' as per xdg-mime bug...) "x-scheme-handler/mailto=faketextapplication.desktop\n" "[Added KParts/ReadOnlyPart Associations]\n" "text/plain=katepart.desktop;\n" "[Removed Associations]\n" "image/jpeg=firefox.desktop;\n" "text/html=gvim.desktop;abiword.desktop;\n"; // Expected results preferredApps[QStringLiteral("image/jpeg")] << QStringLiteral("fakejpegapplication.desktop"); preferredApps[QStringLiteral("application/pdf")] << QStringLiteral("fakejpegapplication.desktop"); preferredApps[QStringLiteral("text/plain")] << QStringLiteral("faketextapplication.desktop") << QStringLiteral("fakepfx-faketextapplicationpfx.desktop") << QStringLiteral("gvim.desktop"); preferredApps[QStringLiteral("text/x-csrc")] << QStringLiteral("faketextapplication.desktop") << QStringLiteral("fakepfx-faketextapplicationpfx.desktop") << QStringLiteral("gvim.desktop"); preferredApps[QStringLiteral("text/html")] << QStringLiteral("fakehtmlapplication.desktop") << QStringLiteral("fakepfx-fakehtmlapplicationpfx.desktop"); preferredApps[QStringLiteral("application/msword")] << QStringLiteral("fakecsrcmswordapplication.desktop"); preferredApps[QStringLiteral("x-scheme-handler/mailto")] << QStringLiteral("faketextapplication.desktop"); removedApps[QStringLiteral("image/jpeg")] << QStringLiteral("firefox.desktop"); removedApps[QStringLiteral("text/html")] << QStringLiteral("gvim.desktop") << QStringLiteral("abiword.desktop"); // Clean-up non-existing apps removeNonExisting(preferredApps); removeNonExisting(removedApps); } void cleanupTestCase() { QFile::remove(m_localConfig + "/mimeapps.list"); runKBuildSycoca(); } void testParseSingleFile() { KOfferHash offerHash; KMimeAssociations parser(offerHash, KSycocaPrivate::self()->serviceFactory()); QTemporaryDir tempDir; QVERIFY(tempDir.isValid()); QFile tempFile(tempDir.path() + "/mimeapps.list"); QVERIFY(tempFile.open(QIODevice::WriteOnly)); tempFile.write(m_mimeAppsFileContents); const QString fileName = tempFile.fileName(); tempFile.close(); //QTest::ignoreMessage(QtDebugMsg, "findServiceByDesktopPath: idontexist.desktop not found"); parser.parseMimeAppsList(fileName, 100); for (ExpectedResultsMap::const_iterator it = preferredApps.constBegin(), end = preferredApps.constEnd(); it != end; ++it) { const QString mime = it.key(); // The data for derived types and aliases isn't for this test (which only looks at mimeapps.list) if (mime == QLatin1String("text/x-csrc") || mime == QLatin1String("application/msword")) { continue; } const QList offers = offerHash.offersFor(mime); Q_FOREACH (const QString &service, it.value()) { KService::Ptr serv = KService::serviceByStorageId(service); if (serv && !offersContains(offers, serv)) { qDebug() << "expected offer" << serv->entryPath() << "not in offers for" << mime << ":"; Q_FOREACH (const KServiceOffer &offer, offers) { qDebug() << offer.service()->storageId(); } QFAIL("offer does not have servicetype"); } } } for (ExpectedResultsMap::const_iterator it = removedApps.constBegin(), end = removedApps.constEnd(); it != end; ++it) { const QString mime = it.key(); const QList offers = offerHash.offersFor(mime); Q_FOREACH (const QString &service, it.value()) { KService::Ptr serv = KService::serviceByStorageId(service); if (serv && offersContains(offers, serv)) { //qDebug() << serv.data() << serv->entryPath() << "does not have" << mime; QFAIL("offer should not have servicetype"); } } } } void testGlobalAndLocalFiles() { KOfferHash offerHash; KMimeAssociations parser(offerHash, KSycocaPrivate::self()->serviceFactory()); // Write global file QTemporaryDir tempDirGlobal; QVERIFY(tempDirGlobal.isValid()); QFile tempFileGlobal(tempDirGlobal.path() + "/mimeapps.list"); QVERIFY(tempFileGlobal.open(QIODevice::WriteOnly)); QByteArray globalAppsFileContents = "[Added Associations]\n" "image/jpeg=firefox.desktop;\n" // removed by local config "text/html=firefox.desktop;\n" // mdv "image/png=fakejpegapplication.desktop;\n"; tempFileGlobal.write(globalAppsFileContents); const QString globalFileName = tempFileGlobal.fileName(); tempFileGlobal.close(); // We didn't keep it, so we need to write the local file again QTemporaryDir tempDir; QVERIFY(tempDir.isValid()); QFile tempFile(tempDir.path() + "/mimeapps.list"); QVERIFY(tempFile.open(QIODevice::WriteOnly)); tempFile.write(m_mimeAppsFileContents); const QString fileName = tempFile.fileName(); tempFile.close(); parser.parseMimeAppsList(globalFileName, 1000); parser.parseMimeAppsList(fileName, 1050); // += 50 is correct. QList offers = offerHash.offersFor(QStringLiteral("image/jpeg")); - qStableSort(offers); // like kbuildservicefactory.cpp does + std::stable_sort(offers.begin(), offers.end()); // like kbuildservicefactory.cpp does const QStringList expectedJpegApps = preferredApps[QStringLiteral("image/jpeg")]; QCOMPARE(assembleOffers(offers), expectedJpegApps); offers = offerHash.offersFor(QStringLiteral("text/html")); - qStableSort(offers); + std::stable_sort(offers.begin(), offers.end()); QStringList textHtmlApps = preferredApps[QStringLiteral("text/html")]; if (KService::serviceByStorageId(QStringLiteral("firefox.desktop"))) { textHtmlApps.append(QStringLiteral("firefox.desktop")); } qDebug() << assembleOffers(offers); QCOMPARE(assembleOffers(offers), textHtmlApps); offers = offerHash.offersFor(QStringLiteral("image/png")); - qStableSort(offers); + std::stable_sort(offers.begin(), offers.end()); QCOMPARE(assembleOffers(offers), QStringList() << QStringLiteral("fakejpegapplication.desktop")); } void testSetupRealFile() { writeToMimeApps(m_mimeAppsFileContents); // Test a trader query KService::List offers = KMimeTypeTrader::self()->query(QStringLiteral("image/jpeg")); QVERIFY(!offers.isEmpty()); //qDebug() << m_mimeAppsFileContents; //qDebug() << "preferred apps for jpeg: " << preferredApps.value("image/jpeg"); //for (int i = 0; i < offers.count(); ++i) { // qDebug() << "offers for" << "image/jpeg" << ":" << i << offers[i]->storageId(); //} QCOMPARE(offers.first()->storageId(), QStringLiteral("fakejpegapplication.desktop")); // Now the generic variant of the above test: // for each mimetype, check that the preferred apps are as specified for (ExpectedResultsMap::const_iterator it = preferredApps.constBegin(), end = preferredApps.constEnd(); it != end; ++it) { const QString mime = it.key(); const KService::List offers = KMimeTypeTrader::self()->query(mime); const QStringList offerIds = assembleServices(offers, it.value().count()); if (offerIds != it.value()) { qDebug() << "offers for" << mime << ":"; for (int i = 0; i < offers.count(); ++i) { qDebug() << " " << i << ":" << offers[i]->storageId(); } qDebug() << " Expected:" << it.value(); const QStringList expectedPreferredServices = it.value(); for (int i = 0; i < expectedPreferredServices.count(); ++i) { qDebug() << mime << i << expectedPreferredServices[i]; //QCOMPARE(expectedPreferredServices[i], offers[i]->storageId()); } } QCOMPARE(offerIds, it.value()); } } void testMultipleInheritance() { // application/x-shellscript inherits from both text/plain and application/x-executable KService::List offers = KMimeTypeTrader::self()->query(QStringLiteral("application/x-shellscript")); QVERIFY(offerListHasService(offers, fakeTextApplication, true)); } void testRemoveAssociationFromParent() { // I removed kate from text/plain, and it would still appear in text/x-java. // First, let's check our fake app is associated with text/plain KService::List offers = KMimeTypeTrader::self()->query(QStringLiteral("text/plain")); QVERIFY(offerListHasService(offers, fakeTextApplication, true)); writeToMimeApps(QByteArray("[Removed Associations]\n" "text/plain=faketextapplication.desktop;\n")); offers = KMimeTypeTrader::self()->query(QStringLiteral("text/plain")); QVERIFY(!offerListHasService(offers, fakeTextApplication, false)); offers = KMimeTypeTrader::self()->query(QStringLiteral("text/x-java")); QVERIFY(!offerListHasService(offers, fakeTextApplication, false)); } void testRemovedImplicitAssociation() // remove (implicit) assoc from derived mimetype { // #164584: Removing ark from opendocument.text didn't work const QString opendocument = QStringLiteral("application/vnd.oasis.opendocument.text"); // [sanity checking of s-m-i installation] QMimeType mime = QMimeDatabase().mimeTypeForName(opendocument); QVERIFY(mime.isValid()); if (!mime.inherits(QStringLiteral("application/zip"))) { // CentOS patches out the application/zip inheritance from application/vnd.oasis.opendocument.text!! Grmbl. QSKIP("Broken distro where application/vnd.oasis.opendocument.text doesn't inherit from application/zip"); } KService::List offers = KMimeTypeTrader::self()->query(opendocument); QVERIFY(offerListHasService(offers, fakeArkApplication, true)); writeToMimeApps(QByteArray("[Removed Associations]\n" "application/vnd.oasis.opendocument.text=fakearkapplication.desktop;\n")); offers = KMimeTypeTrader::self()->query(opendocument); QVERIFY(!offerListHasService(offers, fakeArkApplication, false)); offers = KMimeTypeTrader::self()->query(QStringLiteral("application/zip")); QVERIFY(offerListHasService(offers, fakeArkApplication, true)); } void testRemovedImplicitAssociation178560() { // #178560: Removing ark from interface/x-winamp-skin didn't work // Using application/x-kns (another zip-derived mimetype) nowadays. const QString mime = QStringLiteral("application/x-kns"); KService::List offers = KMimeTypeTrader::self()->query(mime); QVERIFY(offerListHasService(offers, fakeArkApplication, true)); writeToMimeApps(QByteArray("[Removed Associations]\n" "application/x-kns=fakearkapplication.desktop;\n")); offers = KMimeTypeTrader::self()->query(mime); QVERIFY(!offerListHasService(offers, fakeArkApplication, false)); offers = KMimeTypeTrader::self()->query(QStringLiteral("application/zip")); QVERIFY(offerListHasService(offers, fakeArkApplication, true)); } // remove assoc from a mime which is both a parent and a derived mimetype void testRemovedMiddleAssociation() { // More tricky: x-theme inherits x-desktop inherits text/plain, // if we remove an association for x-desktop then x-theme shouldn't // get it from text/plain... KService::List offers; writeToMimeApps(QByteArray("[Removed Associations]\n" "application/x-desktop=faketextapplication.desktop;\n")); offers = KMimeTypeTrader::self()->query(QStringLiteral("text/plain")); QVERIFY(offerListHasService(offers, fakeTextApplication, true)); offers = KMimeTypeTrader::self()->query(QStringLiteral("application/x-desktop")); QVERIFY(!offerListHasService(offers, fakeTextApplication, false)); offers = KMimeTypeTrader::self()->query(QStringLiteral("application/x-theme")); QVERIFY(!offerListHasService(offers, fakeTextApplication, false)); } private: typedef QMap ExpectedResultsMap; void runKBuildSycoca() { // Wait for notifyDatabaseChanged DBus signal // (The real KCM code simply does the refresh in a slot, asynchronously) QSignalSpy spy(KSycoca::self(), SIGNAL(databaseChanged(QStringList))); KBuildSycoca builder; QVERIFY(builder.recreate()); if (spy.isEmpty()) { spy.wait(); } } void writeToMimeApps(const QByteArray &contents) { QString mimeAppsPath = m_localConfig + "/mimeapps.list"; QFile mimeAppsFile(mimeAppsPath); QVERIFY(mimeAppsFile.open(QIODevice::WriteOnly)); mimeAppsFile.write(contents); mimeAppsFile.close(); runKBuildSycoca(); } static bool offersContains(const QList &offers, KService::Ptr serv) { Q_FOREACH (const KServiceOffer &offer, offers) { if (offer.service()->storageId() == serv->storageId()) { return true; } } return false; } static QStringList assembleOffers(const QList &offers) { QStringList lst; Q_FOREACH (const KServiceOffer &offer, offers) { lst.append(offer.service()->storageId()); } return lst; } static QStringList assembleServices(const QList &services, int maxCount = -1) { QStringList lst; Q_FOREACH (const KService::Ptr &service, services) { lst.append(service->storageId()); if (maxCount > -1 && lst.count() == maxCount) { break; } } return lst; } void removeNonExisting(ExpectedResultsMap &erm) { for (ExpectedResultsMap::iterator it = erm.begin(), end = erm.end(); it != end; ++it) { QMutableStringListIterator serv_it(it.value()); while (serv_it.hasNext()) { if (!KService::serviceByStorageId(serv_it.next())) { //qDebug() << "removing non-existing entry" << serv_it.value(); serv_it.remove(); } } } } QString m_localApps; QString m_localConfig; QByteArray m_mimeAppsFileContents; QString fakeTextApplication; QString fakeTextApplicationPrefixed; QString fakeDefaultTextApplication; QString fakeCSrcApplication; QString fakeJpegApplication; QString fakeHtmlApplication; QString fakeHtmlApplicationPrefixed; QString fakeArkApplication; ExpectedResultsMap preferredApps; ExpectedResultsMap removedApps; }; FakeServiceFactory::~FakeServiceFactory() { } QTEST_GUILESS_MAIN(KMimeAssociationsTest) #include "kmimeassociationstest.moc" diff --git a/autotests/ksycocathreadtest.cpp b/autotests/ksycocathreadtest.cpp index ece1e2c..d19c2cf 100644 --- a/autotests/ksycocathreadtest.cpp +++ b/autotests/ksycocathreadtest.cpp @@ -1,373 +1,373 @@ /* This file is part of the KDE project Copyright 2009 David Faure 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 ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "setupxdgdirs.h" static QString fakeTextPluginDesktopFile() { return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/") + "threadtextplugin.desktop"; } static QString fakeServiceDesktopFile() { return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/") + "threadfakeservice.desktop"; } // Helper method for all the trader tests static bool offerListHasService(const KService::List &offers, const QString &entryPath) { bool found = false; KService::List::const_iterator it = offers.begin(); for (; it != offers.end(); ++it) { if ((*it)->entryPath() == entryPath) { if (found) { // should be there only once qWarning("ERROR: %s was found twice in the list", qPrintable(entryPath)); return false; // make test fail } found = true; } } return found; } static QSet s_threadsWhoSawFakeService; static QMutex s_setMutex; static int threadsWhoSawFakeService() { QMutexLocker locker(&s_setMutex); return s_threadsWhoSawFakeService.count(); } static QAtomicInt s_fakeServiceDeleted = 0; class WorkerObject : public QObject { Q_OBJECT public: WorkerObject() : QObject() {} public Q_SLOTS: void work() { //qDebug() << QThread::currentThread() << "working..."; const KServiceType::List allServiceTypes = KServiceType::allServiceTypes(); Q_ASSERT(!allServiceTypes.isEmpty()); QMimeDatabase db; const QList allMimeTypes = db.allMimeTypes(); Q_ASSERT(!allMimeTypes.isEmpty()); const KService::List lst = KService::allServices(); Q_ASSERT(!lst.isEmpty()); for (KService::List::ConstIterator it = lst.begin(); it != lst.end(); ++it) { const KService::Ptr service = (*it); Q_ASSERT(service->isType(KST_KService)); const QString name = service->name(); const QString entryPath = service->entryPath(); //qDebug() << name << "entryPath=" << entryPath << "menuId=" << service->menuId(); Q_ASSERT(!name.isEmpty()); Q_ASSERT(!entryPath.isEmpty()); KService::Ptr lookedupService = KService::serviceByDesktopPath(entryPath); if (!lookedupService) { if (entryPath == "threadfakeservice.desktop" && s_fakeServiceDeleted) { // ok, it got deleted meanwhile continue; } qWarning() << entryPath << "is gone!"; } Q_ASSERT(lookedupService); // not null QCOMPARE(lookedupService->entryPath(), entryPath); if (service->isApplication()) { const QString menuId = service->menuId(); if (menuId.isEmpty()) { qWarning("%s has an empty menuId!", qPrintable(entryPath)); } Q_ASSERT(!menuId.isEmpty()); lookedupService = KService::serviceByMenuId(menuId); if (!lookedupService) { if (menuId == "threadfakeservice" && s_fakeServiceDeleted) { // ok, it got deleted meanwhile continue; } qWarning() << menuId << "is gone!"; } Q_ASSERT(lookedupService); // not null QCOMPARE(lookedupService->menuId(), menuId); } } KService::List offers = KServiceTypeTrader::self()->query(QStringLiteral("KPluginInfo")); Q_ASSERT(offerListHasService(offers, QStringLiteral("threadtextplugin.desktop"))); offers = KServiceTypeTrader::self()->query(QStringLiteral("KPluginInfo"), QStringLiteral("Library == 'threadtextplugin'")); Q_ASSERT(offers.count() == 1); QVERIFY(offerListHasService(offers, QStringLiteral("threadtextplugin.desktop"))); KServiceGroup::Ptr root = KServiceGroup::root(); Q_ASSERT(root); if (KService::serviceByDesktopPath(QStringLiteral("threadfakeservice.desktop"))) { QMutexLocker locker(&s_setMutex); s_threadsWhoSawFakeService.insert(QThread::currentThread()); } } }; class WorkerThread : public QThread { Q_OBJECT public: WorkerThread() : QThread() { m_stop = false; } void run() override { WorkerObject wo; while (!m_stop) { wo.work(); } } virtual void stop() { m_stop = true; } private: QAtomicInt m_stop; // bool }; /** * Threads with an event loop will be able to process "database changed" signals. * Threads without an event loop (like WorkerThread) cannot, so they will keep using * the old data. */ class EventLoopThread : public WorkerThread { Q_OBJECT public: void run() override { // WorkerObject must belong to this thread, this is why we don't // have the slot work() in WorkerThread itself. Typical QThread trap! WorkerObject wo; QTimer timer; connect(&timer, SIGNAL(timeout()), &wo, SLOT(work())); timer.start(100); exec(); } void stop() override { quit(); } }; // This code runs in the main thread class KSycocaThreadTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void cleanupTestCase(); void testCreateService(); void testDeleteService() { deleteFakeService(); QTimer::singleShot(1000, this, SLOT(slotFinish())); } void slotFinish() { qDebug() << "Terminating"; for (int i = 0; i < threads.size(); i++) { threads[i]->stop(); } for (int i = 0; i < threads.size(); i++) { threads[i]->wait(); } cleanupTestCase(); QCoreApplication::instance()->quit(); } private: void createFakeService(); void deleteFakeService(); QVector threads; }; static void runKBuildSycoca() { QSignalSpy spy(KSycoca::self(), SIGNAL(databaseChanged(QStringList))); KBuildSycoca builder; QVERIFY(builder.recreate()); qDebug() << "waiting for signal"; QVERIFY(spy.wait(20000)); qDebug() << "got signal"; } void KSycocaThreadTest::initTestCase() { // Set up a layer in the bin dir so ksycoca finds the KPluginInfo and Application servicetypes setupXdgDirs(); QStandardPaths::setTestModeEnabled(true); // This service is always there. Used in the trader queries from the thread. const QString fakeTextPlugin = fakeTextPluginDesktopFile(); if (!QFile::exists(fakeTextPlugin)) { KDesktopFile file(fakeTextPlugin); KConfigGroup group = file.desktopGroup(); group.writeEntry("Name", "ThreadTextPlugin"); group.writeEntry("Type", "Service"); group.writeEntry("X-KDE-Library", "threadtextplugin"); group.writeEntry("X-KDE-Protocols", "http,ftp"); group.writeEntry("ServiceTypes", "KPluginInfo"); group.writeEntry("MimeType", "text/plain;"); file.sync(); qDebug() << "Created" << fakeTextPlugin << ", running kbuilsycoca"; runKBuildSycoca(); // Process the event int count = 0; while (!KService::serviceByDesktopPath(QStringLiteral("threadtextplugin.desktop"))) { qApp->processEvents(); if (++count == 20) { qFatal("sycoca doesn't have threadtextplugin.desktop"); } } } // Start clean const QString servPath = fakeServiceDesktopFile(); if (QFile::exists(servPath)) { QFile::remove(servPath); } if (KService::serviceByDesktopPath(QStringLiteral("threadfakeservice.desktop"))) { deleteFakeService(); } threads.resize(5); for (int i = 0; i < threads.size(); i++) { threads[i] = i < 3 ? new WorkerThread : new EventLoopThread; threads[i]->start(); } } void KSycocaThreadTest::cleanupTestCase() { QFile::remove(fakeTextPluginDesktopFile()); } // duplicated from kcoreaddons/autotests/kdirwatch_unittest.cpp static void waitUntilAfter(const QDateTime &ctime) { int totalWait = 0; QDateTime now; Q_FOREVER { now = QDateTime::currentDateTime(); - if (now.toTime_t() == ctime.toTime_t()) // truncate milliseconds + if (now.toSecsSinceEpoch() == ctime.toSecsSinceEpoch()) // truncate milliseconds { totalWait += 50; QTest::qWait(50); } else { QVERIFY(now > ctime); // can't go back in time ;) QTest::qWait(50); // be safe break; } } //if (totalWait > 0) qDebug() << "Waited" << totalWait << "ms so that now" << now.toString(Qt::ISODate) << "is >" << ctime.toString(Qt::ISODate); } void KSycocaThreadTest::testCreateService() { // Wait one second so that ksycoca can detect a mtime change // ## IMHO this is a Qt bug, QFileInfo::lastModified() should include milliseconds waitUntilAfter(QDateTime::currentDateTime()); createFakeService(); QVERIFY(QFile::exists(fakeServiceDesktopFile())); qDebug() << "executing kbuildsycoca (1)"; runKBuildSycoca(); QTRY_VERIFY(KService::serviceByDesktopPath(QStringLiteral("threadfakeservice.desktop"))); // Now wait to check that all threads saw that new service QTRY_COMPARE_WITH_TIMEOUT(threadsWhoSawFakeService(), threads.size(), 20000); } void KSycocaThreadTest::deleteFakeService() { s_fakeServiceDeleted = 1; qDebug() << "now deleting the fake service"; KService::Ptr fakeService = KService::serviceByDesktopPath(QStringLiteral("threadfakeservice.desktop")); QVERIFY(fakeService); const QString servPath = fakeServiceDesktopFile(); QFile::remove(servPath); QSignalSpy spy(KSycoca::self(), SIGNAL(databaseChanged(QStringList))); QVERIFY(spy.isValid()); qDebug() << "executing kbuildsycoca (2)"; runKBuildSycoca(); QVERIFY(!spy.isEmpty()); QVERIFY(spy[0][0].toStringList().contains(QStringLiteral("services"))); QVERIFY(fakeService); // the whole point of refcounting is that this KService instance is still valid. QVERIFY(!QFile::exists(servPath)); } void KSycocaThreadTest::createFakeService() { KDesktopFile file(fakeServiceDesktopFile()); KConfigGroup group = file.desktopGroup(); group.writeEntry("Name", "ThreadFakeService"); group.writeEntry("Type", "Service"); group.writeEntry("X-KDE-Library", "threadfakeservice"); group.writeEntry("X-KDE-Protocols", "http,ftp"); group.writeEntry("ServiceTypes", "KPluginInfo"); group.writeEntry("MimeType", "text/plain;"); } QTEST_MAIN(KSycocaThreadTest) #include "ksycocathreadtest.moc" diff --git a/src/services/kservicetypeprofile.cpp b/src/services/kservicetypeprofile.cpp index 2efdfaa..dd51862 100644 --- a/src/services/kservicetypeprofile.cpp +++ b/src/services/kservicetypeprofile.cpp @@ -1,229 +1,229 @@ /* This file is part of the KDE libraries * Copyright (C) 1999 Torben Weis * Copyright (C) 2006 David Faure * * 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 "kservicetypeprofile.h" #include "kservicetypeprofile_p.h" #include "kservice.h" #include "kserviceoffer.h" #include "kservicetype.h" #include "ksycoca_p.h" #include #include #include #include #include // servicetype -> profile class KServiceTypeProfiles : public QHash { public: KServiceTypeProfiles() { m_parsed = false; ensureParsed(); } ~KServiceTypeProfiles() { clear(); } void clear() { QMutexLocker lock(&m_mutex); qDeleteAll(*this); QHash::clear(); m_parsed = false; } bool hasProfile(const QString &serviceType) { QMutexLocker lock(&m_mutex); ensureParsed(); return contains(serviceType); } void ensureParsed(); // mutex must be locked when calling this QMutex m_mutex; private: bool m_parsed; }; Q_GLOBAL_STATIC(KServiceTypeProfiles, s_serviceTypeProfiles) void KServiceTypeProfiles::ensureParsed() { if (m_parsed) { return; } m_parsed = true; // Read the service type profiles from servicetype_profilerc // See writeServiceTypeProfile for a description of the file format. // ### Since this new format names groups after servicetypes maybe we can even // avoid doing any init upfront, and just look up the group when asked... KConfig configFile(QStringLiteral("servicetype_profilerc"), KConfig::NoGlobals); const QStringList tmpList = configFile.groupList(); for (QStringList::const_iterator aIt = tmpList.begin(); aIt != tmpList.end(); ++aIt) { const QString type = *aIt; KConfigGroup config(&configFile, type); const int count = config.readEntry("NumberOfEntries", 0); KServiceTypeProfileEntry *p = this->value(type, nullptr); if (!p) { p = new KServiceTypeProfileEntry(); this->insert(type, p); } for (int i = 0; i < count; ++i) { const QString num = QLatin1String("Entry") + QString::number(i); const QString serviceId = config.readEntry(num + QLatin1String("_Service"), QString()); if (!serviceId.isEmpty()) { const int pref = config.readEntry(num + QLatin1String("_Preference"), 0); //qDebug() << "adding service " << serviceId << " to profile for " << type << " with preference " << pref; p->addService(serviceId, pref); } } } } //static void KServiceTypeProfile::clearCache() { if (s_serviceTypeProfiles.exists()) s_serviceTypeProfiles()->clear(); } /** * Returns the offers in the profile for the requested service type. * @param list list of offers (including initialPreference) * @param servicetype the service type * @return the weighted and sorted offer list * @internal used by KServiceTypeTrader */ namespace KServiceTypeProfile { KServiceOfferList sortServiceTypeOffers(const KServiceOfferList &list, const QString &servicetype); } KServiceOfferList KServiceTypeProfile::sortServiceTypeOffers(const KServiceOfferList &list, const QString &serviceType) { QMutexLocker lock(&s_serviceTypeProfiles()->m_mutex); s_serviceTypeProfiles()->ensureParsed(); KServiceTypeProfileEntry *profile = s_serviceTypeProfiles()->value(serviceType, nullptr); KServiceOfferList offers; KServiceOfferList::const_iterator it = list.begin(); const KServiceOfferList::const_iterator end = list.end(); for (; it != end; ++it) { const KService::Ptr servPtr = (*it).service(); //qDebug() << "KServiceTypeProfile::offers considering " << servPtr->storageId(); // Look into the profile (if there's one), to find this service's preference. bool foundInProfile = false; if (profile) { QMap::ConstIterator it2 = profile->m_mapServices.constFind(servPtr->storageId()); if (it2 != profile->m_mapServices.constEnd()) { const int pref = it2.value(); //qDebug() << "found in mapServices pref=" << pref; if (pref > 0) { // 0 disables the service offers.append(KServiceOffer(servPtr, pref, 0, servPtr->allowAsDefault())); } foundInProfile = true; } } if (!foundInProfile) { // This offer isn't in the profile // This can be because we have no profile at all, or because the // services have been installed after the profile was written, // but it's also the case for any service that's neither App nor ReadOnlyPart, e.g. RenameDlg/Plugin //qDebug() << "not found in mapServices. Appending."; // If there's a profile, we use 0 as the preference to ensure new apps don't take over existing apps (which default to 1) offers.append(KServiceOffer(servPtr, profile ? 0 : (*it).preference(), 0, servPtr->allowAsDefault())); } } - qStableSort(offers); + std::stable_sort(offers.begin(), offers.end()); //qDebug() << "KServiceTypeProfile::offers returning " << offers.count() << " offers"; return offers; } bool KServiceTypeProfile::hasProfile(const QString &serviceType) { return s_serviceTypeProfiles()->hasProfile(serviceType); } void KServiceTypeProfile::writeServiceTypeProfile(const QString &serviceType, const KService::List &services, const KService::List &disabledServices) { /* * [ServiceType] * NumEntries=3 * Entry0_Service=serv.desktop * Entry0_Preference=10 * Entry1_Service=otherserv.desktop * Entry1_Preference=5 * Entry2_Service=broken_service.desktop * Entry2_Preference=0 */ KConfig configFile(QStringLiteral("servicetype_profilerc"), KConfig::SimpleConfig); configFile.deleteGroup(serviceType); KConfigGroup config(&configFile, serviceType); const int count = services.count(); config.writeEntry("NumberOfEntries", count + disabledServices.count()); KService::List::ConstIterator servit = services.begin(); int i = 0; for (; servit != services.end(); ++servit, ++i) { if (*servit) { const QString num = QLatin1String("Entry") + QString::number(i); config.writeEntry(num + QLatin1String("_Service"), (*servit)->storageId()); config.writeEntry(num + QLatin1String("_Preference"), count - i); } } servit = disabledServices.begin(); for (; servit != disabledServices.end(); ++servit, ++i) { if (*servit) { const QString num = QLatin1String("Entry") + QString::number(i); config.writeEntry(num + QLatin1String("_Service"), (*servit)->storageId()); config.writeEntry(num + QLatin1String("_Preference"), 0); } } configFile.sync(); // Drop the whole cache... clearCache(); } void KServiceTypeProfile::deleteServiceTypeProfile(const QString &serviceType) { KConfig config(QStringLiteral("servicetype_profilerc"), KConfig::SimpleConfig); config.deleteGroup(serviceType); config.sync(); // Not threadsafe, but well the whole idea of using this method isn't // threadsafe in the first place. if (s_serviceTypeProfiles.exists()) { delete s_serviceTypeProfiles()->take(serviceType); } } diff --git a/src/sycoca/kbuildservicefactory.cpp b/src/sycoca/kbuildservicefactory.cpp index 4217487..e67712c 100644 --- a/src/sycoca/kbuildservicefactory.cpp +++ b/src/sycoca/kbuildservicefactory.cpp @@ -1,422 +1,422 @@ /* This file is part of the KDE libraries * Copyright (C) 1999, 2007 David Faure * 1999 Waldo Bastian * * 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 "kbuildservicefactory_p.h" #include "kbuildservicegroupfactory_p.h" #include "kbuildmimetypefactory_p.h" #include "kservicetypefactory_p.h" #include "ksycoca.h" #include "ksycocadict_p.h" #include "ksycocaresourcelist_p.h" #include "kdesktopfile.h" #include "kservicetype.h" #include "sycocadebug.h" #include #include #include #include #include #include KBuildServiceFactory::KBuildServiceFactory(KServiceTypeFactory *serviceTypeFactory, KBuildMimeTypeFactory *mimeTypeFactory, KBuildServiceGroupFactory *serviceGroupFactory) : KServiceFactory(serviceTypeFactory->sycoca()), m_nameMemoryHash(), m_relNameMemoryHash(), m_menuIdMemoryHash(), m_dupeDict(), m_serviceTypeFactory(serviceTypeFactory), m_mimeTypeFactory(mimeTypeFactory), m_serviceGroupFactory(serviceGroupFactory) { m_resourceList = new KSycocaResourceList(); // We directly care about services desktop files. // All the application desktop files are parsed on demand from the vfolder menu code. m_resourceList->add("services", QStringLiteral("kservices5"), QStringLiteral("*.desktop")); m_nameDict = new KSycocaDict(); m_relNameDict = new KSycocaDict(); m_menuIdDict = new KSycocaDict(); } KBuildServiceFactory::~KBuildServiceFactory() { delete m_resourceList; } KService::Ptr KBuildServiceFactory::findServiceByDesktopName(const QString &name) { return m_nameMemoryHash.value(name); } KService::Ptr KBuildServiceFactory::findServiceByDesktopPath(const QString &name) { return m_relNameMemoryHash.value(name); } KService::Ptr KBuildServiceFactory::findServiceByMenuId(const QString &menuId) { return m_menuIdMemoryHash.value(menuId); } KSycocaEntry *KBuildServiceFactory::createEntry(const QString &file) const { Q_ASSERT(!file.startsWith(QLatin1String("kservices5/"))); // we add this ourselves, below const QStringRef name = file.midRef(file.lastIndexOf(QLatin1Char('/')) + 1); // Is it a .desktop file? if (name.endsWith(QLatin1String(".desktop"))) { //qCDebug(SYCOCA) << file; KService *serv; if (QDir::isAbsolutePath(file)) { // vfolder sends us full paths for applications serv = new KService(file); } else { // we get relative paths for services KDesktopFile desktopFile(QStandardPaths::GenericDataLocation, QStringLiteral("kservices5/") + file); // Note that the second arg below MUST be 'file', unchanged. // If the entry path doesn't match the 'file' parameter to createEntry, reusing old entries // (via time dict, which uses the entry path as key) cannot work. serv = new KService(&desktopFile, file); } //qCDebug(SYCOCA) << "Creating KService from" << file << "entryPath=" << serv->entryPath(); // Note that the menuId will be set by the vfolder_menu.cpp code just after // createEntry returns. if (serv->isValid() && !serv->isDeleted()) { //qCDebug(SYCOCA) << "Creating KService from" << file << "entryPath=" << serv->entryPath() << "storageId=" << serv->storageId(); return serv; } else { if (!serv->isDeleted()) { qCWarning(SYCOCA) << "Invalid Service : " << file; } delete serv; return nullptr; } } // TODO else if a Windows application, new KService(name, exec, icon) return nullptr; } void KBuildServiceFactory::saveHeader(QDataStream &str) { KSycocaFactory::saveHeader(str); str << qint32(m_nameDictOffset); str << qint32(m_relNameDictOffset); str << qint32(m_offerListOffset); str << qint32(m_menuIdDictOffset); } void KBuildServiceFactory::save(QDataStream &str) { KSycocaFactory::save(str); m_nameDictOffset = str.device()->pos(); m_nameDict->save(str); m_relNameDictOffset = str.device()->pos(); m_relNameDict->save(str); saveOfferList(str); m_menuIdDictOffset = str.device()->pos(); m_menuIdDict->save(str); qint64 endOfFactoryData = str.device()->pos(); // Update header (pass #3) saveHeader(str); // Seek to end. str.device()->seek(endOfFactoryData); } void KBuildServiceFactory::collectInheritedServices() { // For each mimetype, go up the parent-mimetype chains and collect offers. // For "removed associations" to work, we can't just grab everything from all parents. // We need to process parents before children, hence the recursive call in // collectInheritedServices(mime) and the QSet to process a given parent only once. QSet visitedMimes; Q_FOREACH (const QString &mimeType, m_mimeTypeFactory->allMimeTypes()) { collectInheritedServices(mimeType, visitedMimes); } } void KBuildServiceFactory::collectInheritedServices(const QString &mimeTypeName, QSet &visitedMimes) { if (visitedMimes.contains(mimeTypeName)) { return; } visitedMimes.insert(mimeTypeName); // With multiple inheritance, the "mimeTypeInheritanceLevel" isn't exactly // correct (it should only be increased when going up a level, not when iterating // through the multiple parents at a given level). I don't think we care, though. int mimeTypeInheritanceLevel = 0; QMimeDatabase db; QMimeType qmime = db.mimeTypeForName(mimeTypeName); Q_FOREACH (QString parentMimeType, qmime.parentMimeTypes()) { // Workaround issue in shared-mime-info and/or Qt, which sometimes return an alias as parent parentMimeType = db.mimeTypeForName(parentMimeType).name(); collectInheritedServices(parentMimeType, visitedMimes); ++mimeTypeInheritanceLevel; const QList &offers = m_offerHash.offersFor(parentMimeType); QList::const_iterator itserv = offers.begin(); const QList::const_iterator endserv = offers.end(); for (; itserv != endserv; ++itserv) { if (!m_offerHash.hasRemovedOffer(mimeTypeName, (*itserv).service())) { KServiceOffer offer(*itserv); offer.setMimeTypeInheritanceLevel(mimeTypeInheritanceLevel); //qCDebug(SYCOCA) << "INHERITANCE: Adding service" << (*itserv).service()->entryPath() << "to" << mimeTypeName << "mimeTypeInheritanceLevel=" << mimeTypeInheritanceLevel; m_offerHash.addServiceOffer(mimeTypeName, offer); } } } } void KBuildServiceFactory::postProcessServices() { // By doing all this here rather than in addEntry (and removing when replacing // with local override), we only do it for the final applications. // Note that this also affects resolution of the by-desktop-name lookup, // as name resolution is only performed *after* all the duplicates (based on // storage ID) have been removed. // For every service... KSycocaEntryDict::const_iterator itserv = m_entryDict->constBegin(); const KSycocaEntryDict::const_iterator endserv = m_entryDict->constEnd(); for (; itserv != endserv; ++itserv) { KSycocaEntry::Ptr entry = *itserv; KService::Ptr service(static_cast(entry.data())); if (!service->isDeleted()) { const QString parent = service->parentApp(); if (!parent.isEmpty()) { m_serviceGroupFactory->addNewChild(parent, KSycocaEntry::Ptr(service)); } } const QString name = service->desktopEntryName(); KService::Ptr dup = m_nameMemoryHash.value(name); if (dup) { // The rule is that searching for the desktop name "foo" should find // the desktop file with the storage id "foo.desktop" before it // finds "bar/foo.desktop" (or "bar-foo.desktop"). // "bar/foo.desktop" and "baz/foo.desktop" are arbitrarily ordered // (in practice, the one later in the alphabet wins). if (dup->storageId().endsWith(service->storageId())) { // allow dup to be overridden m_nameDict->remove(name); dup = nullptr; } } if (!dup) { m_nameDict->add(name, entry); m_nameMemoryHash.insert(name, service); } const QString relName = service->entryPath(); //qCDebug(SYCOCA) << "adding service" << service.data() << "isApp=" << service->isApplication() << "menuId=" << service->menuId() << "name=" << name << "relName=" << relName; m_relNameDict->add(relName, entry); m_relNameMemoryHash.insert(relName, service); // for KMimeAssociations const QString menuId = service->menuId(); if (!menuId.isEmpty()) { // empty for services, non-empty for applications m_menuIdDict->add(menuId, entry); m_menuIdMemoryHash.insert(menuId, service); // for KMimeAssociations } } populateServiceTypes(); } void KBuildServiceFactory::populateServiceTypes() { QMimeDatabase db; // For every service... KSycocaEntryDict::const_iterator itserv = m_entryDict->constBegin(); const KSycocaEntryDict::const_iterator endserv = m_entryDict->constEnd(); for (; itserv != endserv; ++itserv) { KService::Ptr service(static_cast((*itserv).data())); QVector serviceTypeList = service->_k_accessServiceTypes(); //bool hasAllAll = false; //bool hasAllFiles = false; // Add this service to all its servicetypes (and their parents) and to all its mimetypes for (int i = 0; i < serviceTypeList.count() /*don't cache it, it can change during iteration!*/; ++i) { const QString stName = serviceTypeList[i].serviceType; // It could be a servicetype or a mimetype. KServiceType::Ptr serviceType = m_serviceTypeFactory->findServiceTypeByName(stName); if (serviceType) { const int preference = serviceTypeList[i].preference; const QString parent = serviceType->parentServiceType(); if (!parent.isEmpty()) { serviceTypeList.append(KService::ServiceTypeAndPreference(preference, parent)); } //qCDebug(SYCOCA) << "Adding service" << service->entryPath() << "to" << serviceType->name() << "pref=" << preference; m_offerHash.addServiceOffer(stName, KServiceOffer(service, preference, 0, service->allowAsDefault())); } else { KServiceOffer offer(service, serviceTypeList[i].preference, 0, service->allowAsDefault()); QMimeType mime = db.mimeTypeForName(stName); if (!mime.isValid()) { if (stName.startsWith(QLatin1String("x-scheme-handler/"))) { // Create those on demand m_mimeTypeFactory->createFakeMimeType(stName); m_offerHash.addServiceOffer(stName, offer); } else { //qCDebug(SYCOCA) << service->entryPath() << "specifies undefined mimetype/servicetype" << stName; // technically we could call addServiceOffer here, 'mime' isn't used. But it // would be useless, since we have no mimetype entry where to write the offers offset. continue; } } else { bool shouldAdd = true; foreach (const QString &otherType, service->serviceTypes()) { // Skip derived types if the base class is listed (#321706) if (stName != otherType && mime.inherits(otherType)) { // But don't skip aliases (they got resolved into mime->name() already, but don't let two aliases cancel out) if (db.mimeTypeForName(otherType).name() != mime.name()) { //qCDebug(SYCOCA) << "Skipping" << mime->name() << "because of" << otherType << "(canonical" << KMimeTypeRepository::self()->canonicalName(otherType) << ") while parsing" << service->entryPath(); shouldAdd = false; } } } if (shouldAdd) { //qCDebug(SYCOCA) << "Adding service" << service->entryPath() << "to" << mime->name(); m_offerHash.addServiceOffer(mime.name(), offer); // mime->name so that we resolve aliases } } } } } // Read user preferences (added/removed associations) and add/remove serviceoffers to m_offerHash KMimeAssociations mimeAssociations(m_offerHash, this); mimeAssociations.parseAllMimeAppsList(); // Now for each mimetype, collect services from parent mimetypes collectInheritedServices(); // Now collect the offsets into the (future) offer list // The loops look very much like the ones in saveOfferList obviously. int offersOffset = 0; const int offerEntrySize = sizeof(qint32) * 4; // four qint32s, see saveOfferList. const auto &offerHash = m_offerHash.serviceTypeData(); auto it = offerHash.constBegin(); const auto end = offerHash.constEnd(); for ( ; it != end ; ++it ) { const QString stName = it.key(); const ServiceTypeOffersData offersData = it.value(); const int numOffers = offersData.offers.count(); KServiceType::Ptr serviceType = m_serviceTypeFactory->findServiceTypeByName(stName); if (serviceType) { serviceType->setServiceOffersOffset(offersOffset); offersOffset += offerEntrySize * numOffers; } else { KMimeTypeFactory::MimeTypeEntry::Ptr entry = m_mimeTypeFactory->findMimeTypeEntryByName(stName); if (entry) { entry->setServiceOffersOffset(offersOffset); offersOffset += offerEntrySize * numOffers; } else if (stName.startsWith(QLatin1String("x-scheme-handler/"))) { // Create those on demand entry = m_mimeTypeFactory->createFakeMimeType(stName); entry->setServiceOffersOffset(offersOffset); offersOffset += offerEntrySize * numOffers; } else { if (stName.isEmpty()) { qCDebug(SYCOCA) << "Empty service type"; } else { qCWarning(SYCOCA) << "Service type not found:" << stName; } } } } } void KBuildServiceFactory::saveOfferList(QDataStream &str) { m_offerListOffset = str.device()->pos(); //qCDebug(SYCOCA) << "Saving offer list at offset" << m_offerListOffset; const auto &offerHash = m_offerHash.serviceTypeData(); auto it = offerHash.constBegin(); const auto end = offerHash.constEnd(); for ( ; it != end ; ++it ) { const QString stName = it.key(); const ServiceTypeOffersData offersData = it.value(); QList offers = offersData.offers; - qStableSort(offers); // by initial preference + std::stable_sort(offers.begin(), offers.end()); // by initial preference int offset = -1; KServiceType::Ptr serviceType = m_serviceTypeFactory->findServiceTypeByName(stName); if (serviceType) { offset = serviceType->offset(); } else { KMimeTypeFactory::MimeTypeEntry::Ptr entry = m_mimeTypeFactory->findMimeTypeEntryByName(stName); if (entry) { offset = entry->offset(); //Q_ASSERT(str.device()->pos() == entry->serviceOffersOffset() + m_offerListOffset); } } if (offset == -1) { qCDebug(SYCOCA) << "Didn't find servicetype or mimetype" << stName; continue; } for (QList::const_iterator it2 = offers.constBegin(); it2 != offers.constEnd(); ++it2) { //qCDebug(SYCOCA) << stName << ":" << "writing offer" << (*it2).service()->desktopEntryName() << offset << (*it2).service()->offset() << "in sycoca at pos" << str.device()->pos(); Q_ASSERT((*it2).service()->offset() != 0); str << qint32(offset); str << qint32((*it2).service()->offset()); str << qint32((*it2).preference()); str << qint32((*it2).mimeTypeInheritanceLevel()); // update offerEntrySize in populateServiceTypes if you add/remove something here } } str << qint32(0); // End of list marker (0) } void KBuildServiceFactory::addEntry(const KSycocaEntry::Ptr &newEntry) { Q_ASSERT(newEntry); if (m_dupeDict.contains(newEntry)) { return; } const KService::Ptr service(static_cast(newEntry.data())); m_dupeDict.insert(newEntry); KSycocaFactory::addEntry(newEntry); } diff --git a/src/sycoca/kbuildsycoca.cpp b/src/sycoca/kbuildsycoca.cpp index 46a5d71..f078491 100644 --- a/src/sycoca/kbuildsycoca.cpp +++ b/src/sycoca/kbuildsycoca.cpp @@ -1,661 +1,661 @@ /* This file is part of the KDE libraries * Copyright (C) 1999 David Faure * Copyright (C) 2002-2003 Waldo Bastian * * 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 "kbuildsycoca_p.h" #include "ksycoca_p.h" #include "ksycocaresourcelist_p.h" #include "vfolder_menu_p.h" #include "ksycocautils_p.h" #include "sycocadebug.h" #include #include #include #include "kbuildservicetypefactory_p.h" #include "kbuildmimetypefactory_p.h" #include "kbuildservicefactory_p.h" #include "kbuildservicegroupfactory_p.h" #include "kctimefactory_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include // auto_ptr #include #include static const char *s_cSycocaPath = nullptr; KBuildSycocaInterface::~KBuildSycocaInterface() {} KBuildSycoca::KBuildSycoca(bool globalDatabase) : KSycoca(true), m_allEntries(nullptr), m_currentFactory(nullptr), m_ctimeFactory(nullptr), m_ctimeDict(nullptr), m_currentEntryDict(nullptr), m_serviceGroupEntryDict(nullptr), m_vfolder(nullptr), m_newTimestamp(0), m_globalDatabase(globalDatabase), m_menuTest(false), m_changed(false) { } KBuildSycoca::~KBuildSycoca() { // Delete the factories while we exist, so that the virtual isBuilding() still works qDeleteAll(*factories()); factories()->clear(); } KSycocaEntry::Ptr KBuildSycoca::createEntry(const QString &file, bool addToFactory) { quint32 timeStamp = m_ctimeFactory->dict()->ctime(file, m_resource); if (!timeStamp) { timeStamp = calcResourceHash(m_resourceSubdir, file); } KSycocaEntry::Ptr entry; if (m_allEntries) { Q_ASSERT(m_ctimeDict); quint32 oldTimestamp = m_ctimeDict->ctime(file, m_resource); if (file.contains(QLatin1String("fake"))) { qCDebug(SYCOCA) << "m_ctimeDict->ctime(" << file << ") = " << oldTimestamp << "compared with" << timeStamp; } if (timeStamp && (timeStamp == oldTimestamp)) { // Re-use old entry if (m_currentFactory == d->m_serviceFactory) { // Strip .directory from service-group entries entry = m_currentEntryDict->value(file.left(file.length() - 10)); } else { entry = m_currentEntryDict->value(file); } // remove from m_ctimeDict; if m_ctimeDict is not empty // after all files have been processed, it means // some files were removed since last time if (file.contains(QLatin1String("fake"))) { qCDebug(SYCOCA) << "reusing (and removing) old entry for:" << file << "entry=" << entry; } m_ctimeDict->remove(file, m_resource); } else if (oldTimestamp) { m_changed = true; m_ctimeDict->remove(file, m_resource); qCDebug(SYCOCA) << "modified:" << file; } else { m_changed = true; qCDebug(SYCOCA) << "new:" << file; } } m_ctimeFactory->dict()->addCTime(file, m_resource, timeStamp); if (!entry) { // Create a new entry entry = m_currentFactory->createEntry(file); } if (entry && entry->isValid()) { if (addToFactory) { m_currentFactory->addEntry(entry); } else { m_tempStorage.append(entry); } return entry; } return KSycocaEntry::Ptr(); } KService::Ptr KBuildSycoca::createService(const QString &path) { KSycocaEntry::Ptr entry = createEntry(path, false); return KService::Ptr(static_cast(entry.data())); } // returns false if the database is up to date, true if it needs to be saved bool KBuildSycoca::build() { typedef QList KBSEntryDictList; KBSEntryDictList entryDictList; KBSEntryDict *serviceEntryDict = nullptr; // Convert for each factory the entryList to a Dict. entryDictList.reserve(factories()->size()); int i = 0; // For each factory Q_FOREACH (KSycocaFactory* factory, *factories()) { KBSEntryDict *entryDict = new KBSEntryDict; if (m_allEntries) { // incremental build Q_FOREACH (const KSycocaEntry::Ptr &entry, (*m_allEntries)[i++]) { //if (entry->entryPath().contains("fake")) // qCDebug(SYCOCA) << "inserting into entryDict:" << entry->entryPath() << entry; entryDict->insert(entry->entryPath(), entry); } } if (factory == d->m_serviceFactory) { serviceEntryDict = entryDict; } else if (factory == m_buildServiceGroupFactory) { m_serviceGroupEntryDict = entryDict; } entryDictList.append(entryDict); } // Save the mtime of each dir, just before we list them // ## should we convert to UTC to avoid surprises when summer time kicks in? Q_FOREACH (const QString &dir, factoryResourceDirs()) { qint64 stamp = 0; KSycocaUtilsPrivate::visitResourceDirectory(dir, [&stamp] (const QFileInfo &info) { stamp = qMax(stamp, info.lastModified().toMSecsSinceEpoch()); return true; }); m_allResourceDirs.insert(dir, stamp); } QMap allResourcesSubDirs; // dirs, kstandarddirs-resource-name // For each factory Q_FOREACH (KSycocaFactory* factory, *factories()) { // For each resource the factory deals with const KSycocaResourceList *list = factory->resourceList(); if (!list) { continue; } Q_FOREACH (const KSycocaResource &res, *list) { // With this we would get dirs, but not a unique list of relative files (for global+local merging to work) //const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, res.subdir, QStandardPaths::LocateDirectory); //allResourcesSubDirs[res.resource] += dirs; allResourcesSubDirs.insert(res.subdir, res.resource); } } m_ctimeFactory = new KCTimeFactory(this); // This is a build factory too, don't delete!! for (QMap::ConstIterator it1 = allResourcesSubDirs.constBegin(); it1 != allResourcesSubDirs.constEnd(); ++it1) { m_changed = false; m_resourceSubdir = it1.key(); m_resource = it1.value(); QSet relFiles; const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, m_resourceSubdir, QStandardPaths::LocateDirectory); qCDebug(SYCOCA) << "Looking for subdir" << m_resourceSubdir << "=>" << dirs; Q_FOREACH (const QString &dir, dirs) { QDirIterator it(dir, QDirIterator::Subdirectories); while (it.hasNext()) { const QString filePath = it.next(); Q_ASSERT(filePath.startsWith(dir)); // due to the line below... const QString relPath = filePath.mid(dir.length() + 1); relFiles.insert(relPath); } } // Now find all factories that use this resource.... // For each factory KBSEntryDictList::const_iterator ed_it = entryDictList.constBegin(); const KBSEntryDictList::const_iterator ed_end = entryDictList.constEnd(); KSycocaFactoryList::const_iterator it = factories()->constBegin(); const KSycocaFactoryList::const_iterator end = factories()->constEnd(); for (; it != end; ++it, ++ed_it) { m_currentFactory = (*it); // m_ctimeInfo gets created after the initial loop, so it has no entryDict. m_currentEntryDict = ed_it == ed_end ? nullptr : *ed_it; // For each resource the factory deals with const KSycocaResourceList *list = m_currentFactory->resourceList(); if (!list) { continue; } Q_FOREACH (const KSycocaResource &res, *list) { if (res.resource != (*it1)) { continue; } // For each file in the resource for (auto entryPath = relFiles.constBegin(); entryPath != relFiles.constEnd(); ++entryPath) { // Check if file matches filter if ((*entryPath).endsWith(res.extension)) { createEntry(*entryPath, true); } } } } if (m_changed || !m_allEntries) { //qCDebug(SYCOCA) << "CHANGED:" << m_resource; m_changedResources.append(QString::fromLatin1(m_resource)); } } bool result = true; const bool createVFolder = true; // we need to always run the VFolderMenu code if (createVFolder || m_menuTest) { m_resource = "apps"; m_resourceSubdir = QStringLiteral("applications"); m_currentFactory = d->m_serviceFactory; m_currentEntryDict = serviceEntryDict; m_changed = false; m_vfolder = new VFolderMenu(d->m_serviceFactory, this); if (!m_trackId.isEmpty()) { m_vfolder->setTrackId(m_trackId); } VFolderMenu::SubMenu *kdeMenu = m_vfolder->parseMenu(QStringLiteral(APPLICATIONS_MENU_NAME)); KServiceGroup::Ptr entry = m_buildServiceGroupFactory->addNew(QStringLiteral("/"), kdeMenu->directoryFile, KServiceGroup::Ptr(), false); entry->setLayoutInfo(kdeMenu->layoutList); createMenu(QString(), QString(), kdeMenu); // Storing the mtime *after* looking at these dirs is a tiny race condition, // but I'm not sure how to get the vfolder dirs upfront... Q_FOREACH (QString dir, m_vfolder->allDirectories()) { if (dir.endsWith(QLatin1Char('/'))) { dir.chop(1); // remove trailing slash, to avoid having ~/.local/share/applications twice } if (!m_allResourceDirs.contains(dir)) { qint64 stamp = 0; KSycocaUtilsPrivate::visitResourceDirectory(dir, [&stamp] (const QFileInfo &info) { stamp = qMax(stamp, info.lastModified().toMSecsSinceEpoch()); return true; }); m_allResourceDirs.insert(dir, stamp); } } if (m_changed || !m_allEntries) { //qCDebug(SYCOCA) << "CHANGED:" << m_resource; m_changedResources.append(QString::fromLatin1(m_resource)); } if (m_menuTest) { result = false; } } if (m_ctimeDict && !m_ctimeDict->isEmpty()) { qCDebug(SYCOCA) << "Still in time dict:"; m_ctimeDict->dump(); // Get the list of resources from which some files were deleted QStringList resources = m_ctimeDict->remainingResourceList(); qCDebug(SYCOCA) << "Still in the time dict (i.e. deleted files)" << resources; m_changedResources += resources; } qDeleteAll(entryDictList); return result; } void KBuildSycoca::createMenu(const QString &caption_, const QString &name_, VFolderMenu::SubMenu *menu) { QString caption = caption_; QString name = name_; foreach (VFolderMenu::SubMenu *subMenu, menu->subMenus) { QString subName = name + subMenu->name + QLatin1Char('/'); QString directoryFile = subMenu->directoryFile; if (directoryFile.isEmpty()) { directoryFile = subName + QStringLiteral(".directory"); } quint32 timeStamp = m_ctimeFactory->dict()->ctime(directoryFile, m_resource); if (!timeStamp) { timeStamp = calcResourceHash(m_resourceSubdir, directoryFile); } KServiceGroup::Ptr entry; if (m_allEntries) { const quint32 oldTimestamp = m_ctimeDict->ctime(directoryFile, m_resource); if (timeStamp && (timeStamp == oldTimestamp)) { KSycocaEntry::Ptr group = m_serviceGroupEntryDict->value(subName); if (group) { entry = KServiceGroup::Ptr(static_cast(group.data())); if (entry->directoryEntryPath() != directoryFile) { entry = nullptr; // Can't reuse this one! } } } } if (timeStamp) { // bug? (see calcResourceHash). There might not be a .directory file... m_ctimeFactory->dict()->addCTime(directoryFile, m_resource, timeStamp); } entry = m_buildServiceGroupFactory->addNew(subName, subMenu->directoryFile, entry, subMenu->isDeleted); entry->setLayoutInfo(subMenu->layoutList); if (!(m_menuTest && entry->noDisplay())) { createMenu(caption + entry->caption() + QLatin1Char('/'), subName, subMenu); } } if (caption.isEmpty()) { caption += QLatin1Char('/'); } if (name.isEmpty()) { name += QLatin1Char('/'); } foreach (const KService::Ptr &p, menu->items) { if (m_menuTest) { if (!menu->isDeleted && !p->noDisplay()) printf("%s\t%s\t%s\n", qPrintable(caption), qPrintable(p->menuId()), qPrintable(QStandardPaths::locate(QStandardPaths::ApplicationsLocation, p->entryPath()))); } else { m_buildServiceGroupFactory->addNewEntryTo(name, p); } } } bool KBuildSycoca::recreate(bool incremental) { QFileInfo fi(KSycoca::absoluteFilePath(m_globalDatabase ? KSycoca::GlobalDatabase : KSycoca::LocalDatabase)); if (!QDir().mkpath(fi.absolutePath())) { qCWarning(SYCOCA) << "Couldn't create" << fi.absolutePath(); return false; } QString path(fi.absoluteFilePath()); QLockFile lockFile(path + QLatin1String(".lock")); if (!lockFile.tryLock()) { qCDebug(SYCOCA) << "Waiting for already running" << KBUILDSYCOCA_EXENAME << "to finish."; if (!lockFile.lock()) { qCWarning(SYCOCA) << "Couldn't lock" << path + QStringLiteral(".lock"); return false; } if (!needsRebuild()) { //qCDebug(SYCOCA) << "Up-to-date, skipping."; return true; } } QByteArray qSycocaPath = QFile::encodeName(path); s_cSycocaPath = qSycocaPath.data(); m_allEntries = nullptr; m_ctimeDict = nullptr; if (incremental && checkGlobalHeader()) { qCDebug(SYCOCA) << "Reusing existing ksycoca"; KSycoca *oldSycoca = KSycoca::self(); m_allEntries = new KSycocaEntryListList; m_ctimeDict = new KCTimeDict; // Must be in same order as in KBuildSycoca::recreate()! m_allEntries->append(KSycocaPrivate::self()->serviceTypeFactory()->allEntries()); m_allEntries->append(KSycocaPrivate::self()->mimeTypeFactory()->allEntries()); m_allEntries->append(KSycocaPrivate::self()->serviceGroupFactory()->allEntries()); m_allEntries->append(KSycocaPrivate::self()->serviceFactory()->allEntries()); KCTimeFactory *ctimeInfo = new KCTimeFactory(oldSycoca); *m_ctimeDict = ctimeInfo->loadDict(); } s_cSycocaPath = nullptr; QSaveFile database(path); bool openedOK = database.open(QIODevice::WriteOnly); if (!openedOK && database.error() == QFile::WriteError && QFile::exists(path)) { QFile::remove(path); openedOK = database.open(QIODevice::WriteOnly); } if (!openedOK) { qCWarning(SYCOCA) << "ERROR creating database" << path << ":" << database.errorString(); return false; } QDataStream *str = new QDataStream(&database); str->setVersion(QDataStream::Qt_5_3); m_newTimestamp = QDateTime::currentMSecsSinceEpoch(); qCDebug(SYCOCA).nospace() << "Recreating ksycoca file (" << path << ", version " << KSycoca::version() << ")"; // It is very important to build the servicetype one first KBuildServiceTypeFactory *buildServiceTypeFactory = new KBuildServiceTypeFactory(this); d->m_serviceTypeFactory = buildServiceTypeFactory; KBuildMimeTypeFactory *buildMimeTypeFactory = new KBuildMimeTypeFactory(this); d->m_mimeTypeFactory = buildMimeTypeFactory; m_buildServiceGroupFactory = new KBuildServiceGroupFactory(this); d->m_serviceGroupFactory = m_buildServiceGroupFactory; d->m_serviceFactory = new KBuildServiceFactory(buildServiceTypeFactory, buildMimeTypeFactory, m_buildServiceGroupFactory); if (build()) { // Parse dirs save(str); // Save database if (str->status() != QDataStream::Ok) { // Probably unnecessary now in Qt5, since QSaveFile detects write errors database.cancelWriting(); // Error } delete str; str = nullptr; //if we are currently via sudo, preserve the original owner //as $HOME may also be that of another user rather than /root #ifdef Q_OS_UNIX if (qEnvironmentVariableIsSet("SUDO_UID")) { const int uid = qEnvironmentVariableIntValue("SUDO_UID"); const int gid = qEnvironmentVariableIntValue("SUDO_GID"); if (uid && gid) { fchown(database.handle(), uid, gid); } } #endif if (!database.commit()) { qCWarning(SYCOCA) << "ERROR writing database" << database.fileName() << ". Disk full?"; return false; } if (!m_globalDatabase) { // Compatibility code for KF < 5.15: provide a ksycoca5 symlink after the filename change, for old apps to keep working during the upgrade const QString oldSycoca = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/ksycoca5"); if (QFile::exists(oldSycoca)) { QFile::remove(oldSycoca); #ifdef Q_OS_UNIX if (::link(QFile::encodeName(path).constData(), QFile::encodeName(oldSycoca).constData()) != 0) { QFile::copy(path, oldSycoca); } #else QFile::copy(path, oldSycoca); #endif } } } else { delete str; str = nullptr; database.cancelWriting(); if (m_menuTest) { return true; } qCDebug(SYCOCA) << "Database is up to date"; } if (m_globalDatabase) { // These directories may have been created with 0700 permission // better delete them if they are empty QString appsDir = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation); QDir().remove(appsDir); // was doing the same with servicetypes, but I don't think any of these gets created-by-mistake anymore. } if (d->m_sycocaStrategy == KSycocaPrivate::StrategyMemFile) { KMemFile::fileContentsChanged(path); } delete m_ctimeDict; delete m_allEntries; delete m_vfolder; return true; } void KBuildSycoca::save(QDataStream *str) { // Write header (#pass 1) str->device()->seek(0); (*str) << qint32(KSycoca::version()); //KSycocaFactory * servicetypeFactory = 0; //KBuildMimeTypeFactory * mimeTypeFactory = 0; KBuildServiceFactory *serviceFactory = nullptr; Q_FOREACH (KSycocaFactory* factory, *factories()) { qint32 aId; qint32 aOffset; aId = factory->factoryId(); //if ( aId == KST_KServiceTypeFactory ) // servicetypeFactory = factory; //else if ( aId == KST_KMimeTypeFactory ) // mimeTypeFactory = static_cast( factory ); if (aId == KST_KServiceFactory) { serviceFactory = static_cast(factory); } aOffset = factory->offset(); // not set yet, so always 0 (*str) << aId; (*str) << aOffset; } (*str) << qint32(0); // No more factories. // Write XDG_DATA_DIRS (*str) << QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation).join(QString(QLatin1Char(':'))); (*str) << m_newTimestamp; (*str) << QLocale().bcp47Name(); // This makes it possible to trigger a ksycoca update for all users (KIOSK feature) (*str) << calcResourceHash(QStringLiteral("kservices5"), QStringLiteral("update_ksycoca")); (*str) << m_allResourceDirs.keys(); for (auto it = m_allResourceDirs.constBegin(); it != m_allResourceDirs.constEnd(); ++it) { (*str) << it.value(); } // Calculate per-servicetype/mimetype data if (serviceFactory) serviceFactory->postProcessServices(); // Here so that it's the last debug message qCDebug(SYCOCA) << "Saving"; // Write factory data.... Q_FOREACH (KSycocaFactory* factory, *factories()) { factory->save(*str); if (str->status() != QDataStream::Ok) { // ######## TODO: does this detect write errors, e.g. disk full? return; // error } } qint64 endOfData = str->device()->pos(); // Write header (#pass 2) str->device()->seek(0); (*str) << qint32(KSycoca::version()); Q_FOREACH (KSycocaFactory* factory, *factories()) { qint32 aId; qint32 aOffset; aId = factory->factoryId(); aOffset = factory->offset(); (*str) << aId; (*str) << aOffset; } (*str) << qint32(0); // No more factories. // Jump to end of database str->device()->seek(endOfData); } QStringList KBuildSycoca::factoryResourceDirs() { static QStringList *dirs = nullptr; if (dirs != nullptr) { return *dirs; } dirs = new QStringList; // these are all resource dirs cached by ksycoca *dirs += KServiceTypeFactory::resourceDirs(); *dirs += KMimeTypeFactory::resourceDirs(); *dirs += KServiceFactory::resourceDirs(); return *dirs; } QStringList KBuildSycoca::existingResourceDirs() { static QStringList *dirs = nullptr; if (dirs != nullptr) { return *dirs; } dirs = new QStringList(factoryResourceDirs()); for (QStringList::Iterator it = dirs->begin(); it != dirs->end();) { QFileInfo inf(*it); if (!inf.exists() || !inf.isReadable()) { it = dirs->erase(it); } else { ++it; } } return *dirs; } static quint32 updateHash(const QString &file, quint32 hash) { QFileInfo fi(file); if (fi.isReadable() && fi.isFile()) { // This was using buff.st_ctime (in Waldo's initial commit to kstandarddirs.cpp in 2001), but that looks wrong? // Surely we want to catch manual editing, while a chmod doesn't matter much? - hash += fi.lastModified().toTime_t(); + hash += fi.lastModified().toSecsSinceEpoch(); } return hash; } quint32 KBuildSycoca::calcResourceHash(const QString &resourceSubDir, const QString &filename) { quint32 hash = 0; if (!QDir::isRelativePath(filename)) { return updateHash(filename, hash); } const QStringList files = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, resourceSubDir + QLatin1Char('/') + filename); Q_FOREACH (const QString &file, files) { hash = updateHash(file, hash); } if (hash == 0 && !filename.endsWith(QLatin1String("update_ksycoca")) && !filename.endsWith(QLatin1String(".directory")) // bug? needs investigation from someone who understands the VFolder spec ) { qCWarning(SYCOCA) << "File not found or not readable:" << filename << "found:" << files; Q_ASSERT(hash != 0); } return hash; } bool KBuildSycoca::checkGlobalHeader() { // Since it's part of the filename, we are 99% sure that the locale and prefixes will match. const QString current_language = QLocale().bcp47Name(); const quint32 current_update_sig = KBuildSycoca::calcResourceHash(QStringLiteral("kservices5"), QStringLiteral("update_ksycoca")); const QString current_prefixes = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation).join(QString(QLatin1Char(':'))); const KSycocaHeader header = KSycocaPrivate::self()->readSycocaHeader(); Q_ASSERT(!header.prefixes.split(QLatin1Char(':')).contains(QDir::homePath())); return (current_update_sig == header.updateSignature) && (current_language == header.language) && (current_prefixes == header.prefixes) && (header.timeStamp != 0); } const char *KBuildSycoca::sycocaPath() { return s_cSycocaPath; }