diff --git a/api/dbus/org.desktopspec.ApplicationManager1.Application.xml b/api/dbus/org.desktopspec.ApplicationManager1.Application.xml index d6a91da..f5a48ed 100644 --- a/api/dbus/org.desktopspec.ApplicationManager1.Application.xml +++ b/api/dbus/org.desktopspec.ApplicationManager1.Application.xml @@ -11,6 +11,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/dde-application-manager/src/main.cpp b/apps/dde-application-manager/src/main.cpp index 88787f3..0e12ef8 100644 --- a/apps/dde-application-manager/src/main.cpp +++ b/apps/dde-application-manager/src/main.cpp @@ -21,7 +21,7 @@ void registerComplexDbusType() qDBusRegisterMetaType(); qRegisterMetaType(); qDBusRegisterMetaType(); - qDBusRegisterMetaType>(); + qDBusRegisterMetaType(); qRegisterMetaType(); qDBusRegisterMetaType(); qDBusRegisterMetaType(); diff --git a/apps/dde-application-manager/src/utils.cpp b/apps/dde-application-manager/src/utils.cpp index c472dad..835dc85 100644 --- a/apps/dde-application-manager/src/utils.cpp +++ b/apps/dde-application-manager/src/utils.cpp @@ -9,9 +9,9 @@ bool registerObjectToDBus(QObject *o, const QString &path, const QString &interf auto &con = ApplicationManager1DBus::instance().globalServerBus(); if (!con.registerObject(path, interface, o, QDBusConnection::RegisterOption::ExportAdaptors)) { qCritical() << "register object failed:" << path << interface << con.lastError(); - } else { - qDebug() << "register object:" << path << interface; + return false; } + qDebug() << "register object:" << path << interface; return true; } diff --git a/src/applicationmimeinfo.cpp b/src/applicationmimeinfo.cpp new file mode 100644 index 0000000..80a65e0 --- /dev/null +++ b/src/applicationmimeinfo.cpp @@ -0,0 +1,363 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "applicationmimeinfo.h" +#include "global.h" + +constexpr decltype(auto) desktopSuffix = u8".desktop"; + +QStringList getListFiles() noexcept +{ + QStringList files; + auto configDirs = getXDGConfigDirs(); + auto dataDirs = getXDGDataDirs(); + auto desktop = getCurrentDesktop().toLower(); + + auto appendListFile = [&files, &desktop](const QString &dir) { + QFileInfo cur{dir}; + if (!cur.exists() or !cur.isDir()) { + return; + } + + QDir tmp{cur.absoluteFilePath()}; + auto desktopList = tmp.filePath(QString{"%1-mimeapps.list"}.arg(desktop)); + if (QFileInfo::exists(desktopList)) { + files.append(desktopList); + } + + desktopList = tmp.filePath("mimeapps.list"); + if (QFileInfo::exists(desktopList)) { + files.append(desktopList); + } + }; + + std::for_each(configDirs.cbegin(), configDirs.cend(), appendListFile); + std::for_each(dataDirs.cbegin(), dataDirs.cend(), appendListFile); + std::reverse(files.begin(), files.end()); + return files; +} +namespace { +QString toString(const MimeContent &content) noexcept +{ + QString ret; + + for (auto it = content.constKeyValueBegin(); it != content.constKeyValueEnd(); ++it) { + ret.append(QString{"[%1]\n"}.arg(it->first)); + const auto &kvPairs = it->second; + for (auto inner = kvPairs.constKeyValueBegin(); inner != kvPairs.constKeyValueEnd(); ++inner) { + ret.append(QString{"%1="}.arg(inner->first)); + for (const auto &app : inner->second) { + ret.append(QString{"%2;"}.arg(app)); + } + ret.append('\n'); + } + ret.append('\n'); + } + + return ret; +} + +QString removeDesktopSuffix(const QString &str) noexcept +{ + return str.chopped(sizeof(desktopSuffix) - 1); +} +} // namespace + +std::optional MimeFileBase::loadFromFile(const QFileInfo &fileInfo, bool desktopSpec) +{ + bool isWritable{false}; + auto filePath = fileInfo.absoluteFilePath(); + if (filePath.startsWith(getXDGConfigHome()) or filePath.startsWith(getXDGDataHome())) { + isWritable = true; + } + + QFile file{filePath}; + if (!file.open(QFile::ReadOnly | QFile::Text | QFile::ExistingOnly)) { + qWarning() << "open" << filePath << "failed:" << file.errorString(); + return std::nullopt; + } + + QTextStream stream{&file}; + stream.setEncoding(QStringConverter::Utf8); + + MimeContent content; + MimeFileParser parser{stream, desktopSpec}; + if (auto err = parser.parse(content); err != ParserError::NoError) { + qWarning() << "file:" << filePath << "parse failed:" << err; + return std::nullopt; + } + + if (content.isEmpty()) { + qInfo() << "ignore empty file:" << filePath; + return std::nullopt; + } + + return MimeFileBase{fileInfo, std::move(content), desktopSpec, isWritable}; +} + +MimeFileBase::MimeFileBase(const QFileInfo &info, MimeContent &&content, bool desktopSpec, bool writable) + : m_desktopSpec(desktopSpec) + , m_writable(writable) + , m_info(info) + , m_content(std::move(content)) +{ +} + +void MimeFileBase::reload() noexcept +{ + auto newBase = MimeFileBase::loadFromFile(fileInfo(), isDesktopSpecific()); + if (!newBase) { + qWarning() << "reload" << fileInfo().absoluteFilePath() << "failed, content wouldn't be changed."; + return; + } + + m_content = std::move(newBase->m_content); +} + +MimeApps::MimeApps(MimeFileBase &&base) + : MimeFileBase(std::move(base)) +{ +} + +std::optional MimeApps::createMimeApps(const QString &filePath, bool desktopSpec) noexcept +{ + auto baseOpt = MimeFileBase::loadFromFile(QFileInfo{filePath}, desktopSpec); + + if (!baseOpt) { + return std::nullopt; + } + + return MimeApps{std::move(baseOpt).value()}; +} + +void MimeApps::insertToSection(const QString §ion, const QString &mimeType, const QString &appId) noexcept +{ + auto &map = content(); + + auto targetSection = map.find(section); + if (targetSection == map.end()) { + targetSection = map.insert(section, {}); + } + + QStringList newApps{appId + desktopSuffix}; + auto oldApps = targetSection->find(mimeType); + if (oldApps != targetSection->end()) { + newApps.append(*oldApps); + } + + targetSection->insert(mimeType, newApps); +} + +void MimeApps::addAssociation(const QString &mimeType, const QString &appId) noexcept +{ + insertToSection(addedAssociations, mimeType, appId); +} + +void MimeApps::removeAssociation(const QString &mimeType, const QString &appId) noexcept +{ + insertToSection(removedAssociations, mimeType, appId); +} + +void MimeApps::setDefaultApplication(const QString &mimeType, const QString &appId) noexcept +{ + auto &map = content(); + + auto defaultSection = map.find(defaultApplications); + if (defaultSection == map.end()) { + defaultSection = map.insert(defaultApplications, {}); + } + + defaultSection->insert(mimeType, {appId + desktopSuffix}); +} + +void MimeApps::unsetDefaultApplication(const QString &mimeType) noexcept +{ + auto &map = content(); + + auto defaultSection = map.find(defaultApplications); + if (defaultSection == map.end()) { + return; + } + + defaultSection->remove(mimeType); +} + +AppList MimeApps::queryTypes(QString appId) const noexcept +{ + AppList ret; + appId.append(desktopSuffix); + const auto &lists = content(); + + if (const auto &adds = lists.constFind(addedAssociations); adds != lists.cend()) { + std::for_each(adds->constKeyValueBegin(), adds->constKeyValueEnd(), [&ret, &appId](const auto &it) { + if (it.second.contains(appId)) { + ret.added.append(it.first); + } + }); + } + + if (const auto &removes = lists.constFind(removedAssociations); removes != lists.cend()) { + std::for_each(removes->constKeyValueBegin(), removes->constKeyValueEnd(), [&ret, &appId](const auto &it) { + if (it.second.contains(appId)) { + ret.removed.removeOne(it.first); + } + }); + } + + return ret; +} + +bool MimeApps::writeToFile() const noexcept +{ + auto filePath = fileInfo().absoluteFilePath(); + if (!isWritable()) { + qDebug() << "shouldn't write file:" << filePath; + return false; + } + + QFile file{filePath}; + + if (!file.open(QFile::ExistingOnly | QFile::Text | QFile::WriteOnly | QFile::Truncate)) { + qWarning() << "open" << filePath << "failed:" << file.errorString(); + return false; + } + + auto newContent = toString(content()).toLocal8Bit(); + auto bytes = file.write(newContent); + + if (bytes != newContent.size()) { + qWarning() << "incomplete content, write:" << bytes << "total:" << newContent.size(); + return false; + } + + return true; +} + +QString MimeApps::queryDefaultApp(const QMimeType &type) const noexcept +{ + const auto &map = content(); + auto defaultSection = map.find(defaultApplications); + if (defaultSection == map.end()) { + return {}; + } + + auto defaultApp = defaultSection->find(type.name()); + if (defaultApp == defaultSection->end()) { + return {}; + } + + const auto &app = defaultApp.value(); + if (app.isEmpty()) { + return {}; + } + + return removeDesktopSuffix(app.first()); +} + +QStringList MimeCache::queryTypes(QString appId) const noexcept +{ + QStringList ret; + appId.append(desktopSuffix); + const auto &cache = content()[mimeCache]; + for (auto it = cache.constKeyValueBegin(); it != cache.constKeyValueEnd(); ++it) { + if (it->second.contains(appId)) { + ret.append(it->first); + } + } + return ret; +} + +std::optional MimeCache::createMimeCache(const QString &filePath) noexcept +{ + auto baseOpt = MimeFileBase::loadFromFile(QFileInfo{filePath}, false); + + if (!baseOpt) { + return std::nullopt; + } + + return MimeCache{std::move(baseOpt).value()}; +} + +MimeCache::MimeCache(MimeFileBase &&base) + : MimeFileBase(std::move(base)) +{ +} + +QStringList MimeCache::queryApps(const QMimeType &type) const noexcept +{ + const auto &content = this->content(); + auto it = content.constFind(mimeCache); + if (it == content.constEnd()) { + qDebug() << "this cache is broken, please reload."; + return {}; + } + + QStringList ret; + if (auto kv = it->constFind(type.name()); kv != it->constEnd()) { + const auto &apps = kv.value(); + for (const auto &e : apps) { + if (!e.endsWith(desktopSuffix)) { + continue; + } + ret.append(removeDesktopSuffix(e)); + } + } + return ret; +} + +std::optional MimeInfo::createMimeInfo(const QString &directory) noexcept +{ + MimeInfo ret; + auto dirPath = QDir::cleanPath(directory); + QDir dir; + + if (!QFileInfo::exists(dirPath)) { + qCritical() << "directory " << dirPath << "doesn't exists."; + return std::nullopt; + } + dir.setPath(dirPath); + ret.m_directory = dirPath; + + QFileInfo cacheFile{dir.filePath("mimeinfo.cache")}; + if (cacheFile.exists() and cacheFile.isFile()) { + ret.m_cache = MimeCache::createMimeCache(cacheFile.absoluteFilePath()); + } + + QFileInfo desktopAppList{dir.filePath(QString{"%1-mimeapps.list"}.arg(getCurrentDesktop().toLower()))}; + if (desktopAppList.exists() and desktopAppList.isFile()) { + auto desktopApps = MimeApps::createMimeApps(desktopAppList.absoluteFilePath(), true); + if (desktopApps) { + ret.m_appsList.emplace_back(std::move(desktopApps).value()); + } + } + + QFileInfo appList{dir.filePath("mimeapps.list")}; + if (auto userMimeApps = appList.absoluteFilePath(); + userMimeApps.startsWith(getXDGConfigHome()) and (!appList.exists() or !appList.isFile())) [[unlikely]] { + QFile userFile{userMimeApps}; + if (!userFile.open(QFile::WriteOnly | QFile::Text)) { + qCritical() << "failed to create user file:" << userMimeApps << userFile.errorString(); + } + } + + if (appList.exists() and appList.isFile()) { + auto apps = MimeApps::createMimeApps(appList.absoluteFilePath(), false); + if (apps) { + ret.m_appsList.emplace_back(std::move(apps).value()); + } + } + + return ret; +} + +void MimeInfo::reload() noexcept +{ + for (auto &app : m_appsList) { + app.reload(); + } + + if (m_cache) { + m_cache->reload(); + } +} diff --git a/src/applicationmimeinfo.h b/src/applicationmimeinfo.h new file mode 100644 index 0000000..f95bdba --- /dev/null +++ b/src/applicationmimeinfo.h @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef APPLICATIONMIMEINFO_H +#define APPLICATIONMIMEINFO_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include "mimefileparser.h" + +using MimeContent = MimeFileParser::Groups; + +QStringList getListFiles() noexcept; + +class MimeFileBase +{ +public: + static std::optional loadFromFile(const QFileInfo &fileInfo, bool desktopSpec); + MimeFileBase(MimeFileBase &&) = default; + MimeFileBase &operator=(MimeFileBase &&) = default; + MimeFileBase(const MimeFileBase &) = delete; + MimeFileBase &operator=(const MimeFileBase &) = delete; + + virtual ~MimeFileBase() = default; + [[nodiscard]] const QFileInfo &fileInfo() const noexcept { return m_info; } + [[nodiscard]] MimeContent &content() noexcept { return m_content; } + [[nodiscard]] const MimeContent &content() const noexcept { return m_content; } + [[nodiscard]] bool isDesktopSpecific() const noexcept { return m_desktopSpec; } + [[nodiscard]] bool isWritable() const noexcept { return m_writable; } + + void reload() noexcept; + +private: + MimeFileBase(const QFileInfo &info, MimeContent &&content, bool desktopSpec, bool writable); + bool m_desktopSpec{false}; + bool m_writable{false}; + QFileInfo m_info; + MimeContent m_content; +}; + +struct AppList +{ + QStringList added; + QStringList removed; +}; + +class MimeApps : public MimeFileBase +{ +public: + static std::optional createMimeApps(const QString &filePath, bool desktopSpec) noexcept; + + MimeApps(MimeApps &&) = default; + MimeApps &operator=(MimeApps &&) = default; + MimeApps(const MimeApps &) = delete; + MimeApps &operator=(const MimeApps &) = delete; + ~MimeApps() override = default; + + void addAssociation(const QString &mimeType, const QString &appId) noexcept; + void removeAssociation(const QString &mimeType, const QString &appId) noexcept; + void setDefaultApplication(const QString &mimeType, const QString &appId) noexcept; + void unsetDefaultApplication(const QString &mimeType) noexcept; + + [[nodiscard]] QString queryDefaultApp(const QMimeType &type) const noexcept; + [[nodiscard]] AppList queryTypes(QString appId) const noexcept; + [[nodiscard]] bool writeToFile() const noexcept; + +private: + void insertToSection(const QString §ion, const QString &mimeType, const QString &appId) noexcept; + explicit MimeApps(MimeFileBase &&base); +}; + +class MimeCache : public MimeFileBase +{ +public: + static std::optional createMimeCache(const QString &filePath) noexcept; + + MimeCache(MimeCache &&) = default; + MimeCache &operator=(MimeCache &&) = default; + MimeCache(const MimeCache &) = delete; + MimeCache &operator=(const MimeCache &) = delete; + ~MimeCache() override = default; + + [[nodiscard]] QStringList queryApps(const QMimeType &type) const noexcept; + [[nodiscard]] QStringList queryTypes(QString appId) const noexcept; + +private: + explicit MimeCache(MimeFileBase &&base); +}; + +class MimeInfo +{ +public: + static std::optional createMimeInfo(const QString &directory) noexcept; + + MimeInfo(MimeInfo &&) = default; + MimeInfo &operator=(MimeInfo &&) = default; + MimeInfo(const MimeInfo &) = delete; + MimeInfo &operator=(const MimeInfo &) = delete; + ~MimeInfo() = default; + + [[nodiscard]] std::vector &appsList() noexcept { return m_appsList; } + [[nodiscard]] const std::vector &appsList() const noexcept { return m_appsList; } + [[nodiscard]] std::optional &cacheInfo() noexcept { return m_cache; } + [[nodiscard]] const std::optional &cacheInfo() const noexcept { return m_cache; } + [[nodiscard]] const QString &directory() const noexcept { return m_directory; } + + void reload() noexcept; + +private: + MimeInfo() = default; + std::vector m_appsList; + std::optional m_cache{std::nullopt}; + QString m_directory; +}; + +#endif diff --git a/src/constant.h b/src/constant.h index b510eb8..27f8321 100644 --- a/src/constant.h +++ b/src/constant.h @@ -17,7 +17,8 @@ constexpr auto DDEApplicationManager1ServiceName = constexpr auto DDEApplicationManager1ObjectPath = u8"/org/desktopspec/ApplicationManager1"; constexpr auto DDEAutoStartManager1ObjectPath = u8"/org/desktopspec/AutoStartManager1"; -constexpr auto DDEApplicationManager1JobManagerObjectPath = u8"/org/desktopspec/ApplicationManager1/JobManager1"; +constexpr auto DDEApplicationManager1JobManager1ObjectPath = u8"/org/desktopspec/ApplicationManager1/JobManager1"; +constexpr auto DDEApplicationManager1MimeManager1ObjectPath = u8"/org/desktopspec/ApplicationManager1/MimeManager1"; constexpr auto DesktopFileEntryKey = u8"Desktop Entry"; constexpr auto DesktopFileActionKey = u8"Desktop Action "; @@ -36,12 +37,13 @@ constexpr auto ApplicationManagerDestDBusName = #endif constexpr auto ObjectManagerInterface = "org.desktopspec.DBus.ObjectManager"; -constexpr auto JobManagerInterface = "org.desktopspec.JobManager1"; +constexpr auto JobManager1Interface = "org.desktopspec.JobManager1"; constexpr auto JobInterface = "org.desktopspec.JobManager1.Job"; -constexpr auto ApplicationManagerInterface = "org.desktopspec.ApplicationManager1"; +constexpr auto ApplicationManager1Interface = "org.desktopspec.ApplicationManager1"; constexpr auto InstanceInterface = "org.desktopspec.ApplicationManager1.Instance"; constexpr auto ApplicationInterface = "org.desktopspec.ApplicationManager1.Application"; constexpr auto PropertiesInterface = u8"org.freedesktop.DBus.Properties"; +constexpr auto MimeManager1Interface = u8"org.desktopspec.MimeManager1"; constexpr auto systemdOption = u8"systemd"; constexpr auto splitOption = u8"split"; diff --git a/src/dbus/CMakeLists.txt b/src/dbus/CMakeLists.txt index afbfd82..92ffdd6 100644 --- a/src/dbus/CMakeLists.txt +++ b/src/dbus/CMakeLists.txt @@ -13,6 +13,7 @@ qt_add_dbus_adaptor(dde_am_dbus_SOURCE ${PROJECT_SOURCE_DIR}/api/dbus/org.deskto qt_add_dbus_adaptor(dde_am_dbus_SOURCE ${PROJECT_SOURCE_DIR}/api/dbus/org.desktopspec.JobManager1.Job.xml dbus/jobservice.h JobService) qt_add_dbus_adaptor(dde_am_dbus_SOURCE ${PROJECT_SOURCE_DIR}/api/dbus/org.desktopspec.ObjectManager1.xml dbus/applicationmanager1service.h ApplicationManager1Service AMobjectmanager1adaptor AMObjectManagerAdaptor) qt_add_dbus_adaptor(dde_am_dbus_SOURCE ${PROJECT_SOURCE_DIR}/api/dbus/org.desktopspec.ObjectManager1.xml dbus/applicationservice.h ApplicationService APPobjectmanager1adaptor APPObjectManagerAdaptor) +qt_add_dbus_adaptor(dde_am_dbus_SOURCE ${PROJECT_SOURCE_DIR}/api/dbus/org.desktopspec.MimeManager1.xml dbus/mimemanager1service.h MimeManager1Service) target_sources(dde_am_dbus PRIVATE ${dde_am_dbus_SOURCE} diff --git a/src/dbus/applicationmanager1service.cpp b/src/dbus/applicationmanager1service.cpp index f68731b..32d2364 100644 --- a/src/dbus/applicationmanager1service.cpp +++ b/src/dbus/applicationmanager1service.cpp @@ -37,17 +37,20 @@ void ApplicationManager1Service::initService(QDBusConnection &connection) noexce std::terminate(); } - if (!registerObjectToDBus(this, DDEApplicationManager1ObjectPath, ApplicationManagerInterface)) { + if (!registerObjectToDBus(this, DDEApplicationManager1ObjectPath, ApplicationManager1Interface)) { std::terminate(); } - m_jobManager.reset(new (std::nothrow) JobManager1Service(this)); - - if (!m_jobManager) { + if (m_jobManager.reset(new (std::nothrow) JobManager1Service(this)); !m_jobManager) { qCritical() << "new JobManager failed."; std::terminate(); } + if (m_mimeManager.reset(new (std::nothrow) MimeManager1Service(this)); !m_mimeManager) { + qCritical() << "new MimeManager failed."; + std::terminate(); + } + auto &dispatcher = SystemdSignalDispatcher::instance(); connect(&dispatcher, &SystemdSignalDispatcher::SystemdUnitNew, this, &ApplicationManager1Service::addInstanceToApplication); @@ -67,6 +70,8 @@ void ApplicationManager1Service::initService(QDBusConnection &connection) noexce qFatal("connect to ApplicationUpdated failed."); } + scanMimeInfos(); + scanApplications(); scanInstances(); @@ -208,6 +213,20 @@ void ApplicationManager1Service::removeInstanceFromApplication(const QString &un }); } +void ApplicationManager1Service::scanMimeInfos() noexcept +{ + QStringList dirs; + dirs.append(getXDGConfigDirs()); + dirs.append(getDesktopFileDirs()); + + for (const auto &dir : dirs) { + auto info = MimeInfo::createMimeInfo(dir); + if (info) { + m_mimeManager->appendMimeInfo(std::move(info).value()); + } + } +} + void ApplicationManager1Service::scanApplications() noexcept { const auto &desktopFileDirs = getDesktopFileDirs(); @@ -215,7 +234,7 @@ void ApplicationManager1Service::scanApplications() noexcept applyIteratively( QList(desktopFileDirs.cbegin(), desktopFileDirs.cend()), [this](const QFileInfo &info) -> bool { - DesktopErrorCode err{DesktopErrorCode::NoError}; + ParserError err{ParserError::NoError}; auto ret = DesktopFile::searchDesktopFileByPath(info.absoluteFilePath(), err); if (!ret.has_value()) { qWarning() << "failed to search File:" << err; @@ -449,7 +468,7 @@ void ApplicationManager1Service::updateApplication(const QSharedPointerparse(desktopFile); - if (err != DesktopErrorCode::NoError) { + if (err != ParserError::NoError) { qWarning() << "update desktop file failed:" << err << ", content wouldn't change."; return; } @@ -475,7 +494,7 @@ void ApplicationManager1Service::ReloadApplications() applyIteratively( QList(desktopFileDirs.cbegin(), desktopFileDirs.cend()), [this, &apps](const QFileInfo &info) -> bool { - DesktopErrorCode err{DesktopErrorCode::NoError}; + ParserError err{ParserError::NoError}; auto ret = DesktopFile::searchDesktopFileByPath(info.absoluteFilePath(), err); if (!ret.has_value()) { return false; @@ -488,7 +507,7 @@ void ApplicationManager1Service::ReloadApplications() m_applicationList.cend(), [&file](const QSharedPointer &app) { return file.desktopId() == app->id(); }); - if (err != DesktopErrorCode::NoError) { + if (err != ParserError::NoError) { qWarning() << "error occurred:" << err << " skip this application."; return false; } @@ -515,3 +534,17 @@ ObjectMap ApplicationManager1Service::GetManagedObjects() const { return dumpDBusObject(m_applicationList); } + +QMap> +ApplicationManager1Service::findApplicationsByIds(const QStringList &appIds) const noexcept +{ + QMap> ret; + for (auto it = m_applicationList.constKeyValueBegin(); it != m_applicationList.constKeyValueEnd(); ++it) { + const auto &ptr = it->second; + if (appIds.contains(ptr->id())) { + ret.insert(it->first, it->second); + } + } + + return ret; +} diff --git a/src/dbus/applicationmanager1service.h b/src/dbus/applicationmanager1service.h index 11cd57f..5445d49 100644 --- a/src/dbus/applicationmanager1service.h +++ b/src/dbus/applicationmanager1service.h @@ -14,6 +14,7 @@ #include #include "applicationmanagerstorage.h" #include "dbus/jobmanager1service.h" +#include "dbus/mimemanager1service.h" #include "desktopentry.h" #include "identifier.h" @@ -38,12 +39,15 @@ public: bool addApplication(DesktopFile desktopFileSource) noexcept; void removeOneApplication(const QDBusObjectPath &application) noexcept; void removeAllApplication() noexcept; - + [[nodiscard]] QMap> + findApplicationsByIds(const QStringList &appIds) const noexcept; void updateApplication(const QSharedPointer &destApp, DesktopFile desktopFile) noexcept; [[nodiscard]] JobManager1Service &jobManager() noexcept { return *m_jobManager; } [[nodiscard]] const JobManager1Service &jobManager() const noexcept { return *m_jobManager; } [[nodiscard]] const QStringList &applicationHooks() const noexcept { return m_hookElements; } + [[nodiscard]] MimeManager1Service &mimeManager() noexcept { return *m_mimeManager; } + [[nodiscard]] const MimeManager1Service &mimeManager() const noexcept { return *m_mimeManager; } public Q_SLOTS: QString Identify(const QDBusUnixFileDescriptor &pidfd, @@ -60,10 +64,12 @@ Q_SIGNALS: private: std::unique_ptr m_identifier; std::weak_ptr m_storage; + QScopedPointer m_mimeManager{nullptr}; QScopedPointer m_jobManager{nullptr}; QStringList m_hookElements; QMap> m_applicationList; + void scanMimeInfos() noexcept; void scanApplications() noexcept; void scanInstances() noexcept; void scanAutoStart() noexcept; diff --git a/src/dbus/applicationservice.cpp b/src/dbus/applicationservice.cpp index 4aa7090..f242c5d 100644 --- a/src/dbus/applicationservice.cpp +++ b/src/dbus/applicationservice.cpp @@ -5,7 +5,6 @@ #include "dbus/applicationservice.h" #include "APPobjectmanager1adaptor.h" #include "applicationchecker.h" -#include "applicationmanager1service.h" #include "applicationmanagerstorage.h" #include "propertiesForwarder.h" #include "dbus/instanceadaptor.h" @@ -83,7 +82,7 @@ QSharedPointer ApplicationService::createApplicationService( std::unique_ptr entry{std::make_unique()}; auto error = entry->parse(sourceStream); - if (error != DesktopErrorCode::NoError) { + if (error != ParserError::NoError) { qWarning() << "parse failed:" << error << app->desktopFileSource().sourcePath(); return nullptr; } @@ -486,6 +485,79 @@ void ApplicationService::setAutoStart(bool autostart) noexcept emit autostartChanged(); } +QStringList ApplicationService::mimeTypes() const noexcept +{ + QStringList ret; + const auto &desktopFilePath = m_desktopSource.sourcePath(); + const auto &cacheList = parent()->mimeManager().infos(); + auto cache = std::find_if(cacheList.cbegin(), cacheList.cend(), [&desktopFilePath](const MimeInfo &info) { + return desktopFilePath.startsWith(info.directory()); + }); + + const auto &info = cache->cacheInfo(); + if (info) { + ret.append(info->queryTypes(id())); + } + + AppList tmp; + + for (auto it = cacheList.crbegin(); it != cacheList.crend(); ++it) { + const auto &list = it->appsList(); + std::for_each(list.crbegin(), list.crend(), [&tmp, this](const MimeApps &app) { + auto [added, removed] = app.queryTypes(id()); + tmp.added.append(std::move(added)); + tmp.removed.append(std::move(removed)); + }); + }; + + tmp.added.removeDuplicates(); + tmp.removed.removeDuplicates(); + for (const auto &it : tmp.removed) { + tmp.added.removeOne(it); + } + + ret.append(std::move(tmp.added)); + ret.removeDuplicates(); + return ret; +} + +void ApplicationService::setMimeTypes(const QStringList &value) noexcept +{ + auto oldMimes = mimeTypes(); + auto newMimes = value; + std::sort(oldMimes.begin(), oldMimes.end()); + std::sort(newMimes.begin(), newMimes.end()); + + QStringList newAdds; + QStringList newRemoved; + + std::set_difference(oldMimes.begin(), oldMimes.end(), newMimes.begin(), newMimes.end(), std::back_inserter(newRemoved)); + std::set_difference(newMimes.begin(), newMimes.end(), oldMimes.begin(), oldMimes.end(), std::back_inserter(newAdds)); + + static QString userDir = getXDGConfigHome(); + auto &infos = parent()->mimeManager().infos(); + auto userInfo = std::find_if(infos.begin(), infos.end(), [](const MimeInfo &info) { return info.directory() == userDir; }); + if (userInfo == infos.cend()) { + sendErrorReply(QDBusError::Failed, "user-specific config file doesn't exists."); + return; + } + + const auto &list = userInfo->appsList().rbegin(); + const auto &appId = id(); + for (const auto &add : newAdds) { + list->addAssociation(add, appId); + } + for (const auto &remove : newRemoved) { + list->removeAssociation(remove, appId); + } + + if (!list->writeToFile()) { + qWarning() << "error occurred when write mime association to file"; + } + + emit MimeTypesChanged(); +} + QList ApplicationService::instances() const noexcept { return m_Instances.keys(); @@ -567,6 +639,7 @@ void ApplicationService::resetEntry(DesktopEntry *newEntry) noexcept emit actionNameChanged(); emit actionsChanged(); emit categoriesChanged(); + emit MimeTypesChanged(); } LaunchTask ApplicationService::unescapeExec(const QString &str, const QStringList &fields) diff --git a/src/dbus/applicationservice.h b/src/dbus/applicationservice.h index 986d6a1..281d94c 100644 --- a/src/dbus/applicationservice.h +++ b/src/dbus/applicationservice.h @@ -23,6 +23,7 @@ #include "global.h" #include "desktopentry.h" #include "dbus/jobmanager1service.h" +#include "applicationmanager1service.h" QString getDeepinWineScaleFactor(const QString &appId) noexcept; @@ -67,6 +68,10 @@ public: [[nodiscard]] bool isAutoStart() const noexcept; void setAutoStart(bool autostart) noexcept; + Q_PROPERTY(QStringList MimeTypes READ mimeTypes WRITE setMimeTypes) + [[nodiscard]] QStringList mimeTypes() const noexcept; + void setMimeTypes(const QStringList &value) noexcept; + Q_PROPERTY(QList Instances READ instances NOTIFY instanceChanged) [[nodiscard]] QList instances() const noexcept; @@ -129,6 +134,7 @@ Q_SIGNALS: void actionNameChanged(); void actionsChanged(); void categoriesChanged(); + void MimeTypesChanged(); private: friend class ApplicationManager1Service; diff --git a/src/dbus/jobmanager1service.cpp b/src/dbus/jobmanager1service.cpp index 62ba2a9..d471fba 100644 --- a/src/dbus/jobmanager1service.cpp +++ b/src/dbus/jobmanager1service.cpp @@ -9,7 +9,7 @@ JobManager1Service::JobManager1Service(ApplicationManager1Service *parent) : m_parent(parent) { new JobManager1Adaptor{this}; - if (!registerObjectToDBus(this, DDEApplicationManager1JobManagerObjectPath, JobManagerInterface)) { + if (!registerObjectToDBus(this, DDEApplicationManager1JobManager1ObjectPath, JobManager1Interface)) { std::terminate(); } qRegisterMetaType(); diff --git a/src/dbus/jobmanager1service.h b/src/dbus/jobmanager1service.h index 4d9dfe4..ff559bf 100644 --- a/src/dbus/jobmanager1service.h +++ b/src/dbus/jobmanager1service.h @@ -51,7 +51,7 @@ public: static_assert(std::is_invocable_v, "param type must be QVariant."); QString objectPath = - QString{"%1/%2"}.arg(DDEApplicationManager1JobManagerObjectPath).arg(QUuid::createUuid().toString(QUuid::Id128)); + QString{"%1/%2"}.arg(DDEApplicationManager1JobManager1ObjectPath).arg(QUuid::createUuid().toString(QUuid::Id128)); QFuture future = QtConcurrent::mappedReduced(std::move(args), func, qOverload(&QVariantList::append), diff --git a/src/dbus/mimemanager1service.cpp b/src/dbus/mimemanager1service.cpp new file mode 100644 index 0000000..92f9bac --- /dev/null +++ b/src/dbus/mimemanager1service.cpp @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "dbus/mimemanager1adaptor.h" +#include "applicationmanager1service.h" +#include "applicationservice.h" +#include "constant.h" + +MimeManager1Service::MimeManager1Service(ApplicationManager1Service *parent) + : QObject(parent) +{ + new MimeManager1Adaptor{this}; + if (!registerObjectToDBus(this, DDEApplicationManager1MimeManager1ObjectPath, MimeManager1Interface)) { + std::terminate(); + } +} + +MimeManager1Service::~MimeManager1Service() = default; + +ObjectMap MimeManager1Service::listApplications(const QString &mimeType) const noexcept +{ + auto mime = m_database.mimeTypeForName(mimeType); + if (!mime.isValid()) { + qWarning() << "can't find" << mimeType; + return {}; + } + + QStringList appIds; + + for (auto it = m_infos.rbegin(); it != m_infos.rend(); ++it) { + const auto &info = it->cacheInfo(); + if (!info) { + continue; + } + auto apps = info->queryApps(mime); + appIds.append(std::move(apps)); + } + appIds.removeDuplicates(); + qInfo() << "query" << mimeType << "find:" << appIds; + const auto &apps = static_cast(parent())->findApplicationsByIds(appIds); + return dumpDBusObject(apps); +} + +QString MimeManager1Service::queryFileTypeAndDefaultApplication(const QString &filePath, + QDBusObjectPath &application) const noexcept +{ + QString mimeType; + application = QDBusObjectPath{"/"}; + + auto mime = m_database.mimeTypeForFile(filePath); + if (mime.isValid()) { + mimeType = mime.name(); + } + + QString defaultAppId; + for (auto it1 = m_infos.rbegin(); it1 != m_infos.rend(); ++it1) { + const auto &list = it1->appsList(); + for (auto it2 = list.rbegin(); it2 != list.rend(); ++it2) { + if (auto app = it2->queryDefaultApp(mime); !app.isEmpty()) { + defaultAppId = app; + } + } + } + + if (defaultAppId.isEmpty()) { + qInfo() << "file's mimeType:" << mime.name() << "but can't find a default application for this type."; + return mimeType; + } + + const auto &apps = static_cast(parent())->findApplicationsByIds({defaultAppId}); + if (apps.isEmpty()) { + qWarning() << "default application has been found:" << defaultAppId + << " but we can't find corresponding application in ApplicationManagerService."; + } else { + application = apps.firstKey(); + } + + return mimeType; +} + +void MimeManager1Service::setDefaultApplication(const KVPairs &defaultApps) noexcept +{ + auto &app = m_infos.front().appsList(); + auto userConfig = std::find_if( + app.begin(), app.end(), [](const MimeApps &config) { return !config.isDesktopSpecific(); }); // always find this + + for (auto it = defaultApps.constKeyValueBegin(); it != defaultApps.constKeyValueEnd(); ++it) { + userConfig->setDefaultApplication(it->first, it->second); + } + + if (!userConfig->writeToFile()) { + sendErrorReply(QDBusError::Failed, "set default app failed, these config will be reset after re-login."); + } +} + +void MimeManager1Service::unsetDefaultApplication(const QStringList &mimeTypes) noexcept +{ + auto &app = m_infos.front().appsList(); + auto userConfig = std::find_if(app.begin(), app.end(), [](const MimeApps &config) { return !config.isDesktopSpecific(); }); + + for (const auto &mime : mimeTypes) { + userConfig->unsetDefaultApplication(mime); + } + + if (!userConfig->writeToFile()) { + sendErrorReply(QDBusError::Failed, "unset default app failed, these config will be reset after re-login."); + } +} + +void MimeManager1Service::appendMimeInfo(MimeInfo &&info) +{ + m_infos.emplace_back(std::move(info)); +} diff --git a/src/dbus/mimemanager1service.h b/src/dbus/mimemanager1service.h new file mode 100644 index 0000000..8773743 --- /dev/null +++ b/src/dbus/mimemanager1service.h @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef MIMEMANAGERSERVICE_H +#define MIMEMANAGERSERVICE_H + +#include +#include +#include +#include "global.h" +#include "applicationmimeinfo.h" + +class ApplicationManager1Service; + +class MimeManager1Service : public QObject, public QDBusContext +{ + Q_OBJECT +public: + explicit MimeManager1Service(ApplicationManager1Service *parent); + ~MimeManager1Service() override; + + void appendMimeInfo(MimeInfo &&info); + [[nodiscard]] const auto &infos() const noexcept { return m_infos; } + [[nodiscard]] auto &infos() noexcept { return m_infos; } + +public Q_SLOTS: + [[nodiscard]] ObjectMap listApplications(const QString &mimeType) const noexcept; + [[nodiscard]] QString queryFileTypeAndDefaultApplication(const QString &filePath, + QDBusObjectPath &application) const noexcept; + void setDefaultApplication(const KVPairs &defaultApps) noexcept; + void unsetDefaultApplication(const QStringList &mimeTypes) noexcept; + +private: + QMimeDatabase m_database; + std::vector m_infos; +}; + +#endif diff --git a/src/desktopentry.cpp b/src/desktopentry.cpp index 7874b1f..f76146e 100644 --- a/src/desktopentry.cpp +++ b/src/desktopentry.cpp @@ -4,6 +4,7 @@ #include "desktopentry.h" #include "global.h" +#include "desktopfileparser.h" #include #include #include @@ -15,203 +16,6 @@ #include #include -namespace { -bool isInvalidLocaleString(const QString &str) noexcept -{ - constexpr auto Language = R"((?:[a-z]+))"; // language of locale postfix. eg.(en, zh) - constexpr auto Country = R"((?:_[A-Z]+))"; // country of locale postfix. eg.(US, CN) - constexpr auto Encoding = R"((?:\.[0-9A-Z-]+))"; // encoding of locale postfix. eg.(UFT-8) - constexpr auto Modifier = R"((?:@[a-zA-Z=;]+))"; // modifier of locale postfix. eg.(euro;collation=traditional) - const static auto validKey = QString(R"(^%1%2?%3?%4?$)").arg(Language, Country, Encoding, Modifier); - // example: https://regex101.com/r/hylOay/2 - static const QRegularExpression _re = []() -> QRegularExpression { - QRegularExpression tmp{validKey}; - tmp.optimize(); - return tmp; - }(); - thread_local const auto re = _re; - - return re.match(str).hasMatch(); -} - -bool hasNonAsciiAndControlCharacters(const QString &str) noexcept -{ - static const QRegularExpression _matchControlChars = []() { - QRegularExpression tmp{R"(\p{Cc})"}; - tmp.optimize(); - return tmp; - }(); - thread_local const auto matchControlChars = _matchControlChars; - static const QRegularExpression _matchNonAsciiChars = []() { - QRegularExpression tmp{R"([^\x00-\x7f])"}; - tmp.optimize(); - return tmp; - }(); - thread_local const auto matchNonAsciiChars = _matchNonAsciiChars; - if (str.contains(matchControlChars) and str.contains(matchNonAsciiChars)) { - return true; - } - - return false; -} - -struct Parser -{ - Parser(QTextStream &stream) - : m_stream(stream){}; - QTextStream &m_stream; - QString m_line; - - using Groups = QMap>; - - DesktopErrorCode parse(Groups &groups) noexcept; - -private: - void skip() noexcept; - DesktopErrorCode addGroup(Groups &groups) noexcept; - DesktopErrorCode addEntry(Groups::iterator &group) noexcept; -}; - -void Parser::skip() noexcept -{ - while (!m_stream.atEnd() and (m_line.startsWith('#') or m_line.isEmpty())) { - m_line = m_stream.readLine().trimmed(); - } -} - -DesktopErrorCode Parser::parse(Groups &ret) noexcept -{ - std::remove_reference_t groups; - while (!m_stream.atEnd()) { - auto err = addGroup(groups); - if (err != DesktopErrorCode::NoError) { - return err; - } - - if (groups.size() != 1) { - continue; - } - - if (groups.keys().first() != DesktopFileEntryKey) { - qWarning() << "There should be nothing preceding " - "'Desktop Entry' group in the desktop entry file " - "but possibly one or more comments."; - return DesktopErrorCode::InvalidFormat; - } - } - - if (!m_line.isEmpty()) { - qCritical() << "Something is wrong in Desktop file parser, check logic."; - return DesktopErrorCode::InternalError; - } - - ret = std::move(groups); - return DesktopErrorCode::NoError; -} - -DesktopErrorCode Parser::addGroup(Groups &ret) noexcept -{ - skip(); - if (!m_line.startsWith('[')) { - qWarning() << "Invalid desktop file format: unexpected line:" << m_line; - return DesktopErrorCode::InvalidFormat; - } - - // Parsing group header. - // https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#group-header - - auto groupHeader = m_line.sliced(1, m_line.size() - 2).trimmed(); - - if (groupHeader.contains('[') || groupHeader.contains(']') || hasNonAsciiAndControlCharacters(groupHeader)) { - qWarning() << "group header invalid:" << m_line; - return DesktopErrorCode::InvalidFormat; - } - - if (ret.find(groupHeader) != ret.end()) { - qWarning() << "duplicated group header detected:" << groupHeader; - return DesktopErrorCode::InvalidFormat; - } - - auto group = ret.insert(groupHeader, {}); - - m_line.clear(); - while (!m_stream.atEnd() && !m_line.startsWith('[')) { - skip(); - if (m_line.startsWith('[')) { - break; - } - auto err = addEntry(group); - if (err != DesktopErrorCode::NoError) { - return err; - } - } - return DesktopErrorCode::NoError; -} - -DesktopErrorCode Parser::addEntry(Groups::iterator &group) noexcept -{ - auto line = m_line; - m_line.clear(); - auto splitCharIndex = line.indexOf('='); - if (splitCharIndex == -1) { - qWarning() << "invalid line in desktop file, skip it:" << line; - return DesktopErrorCode::NoError; - } - auto keyStr = line.first(splitCharIndex).trimmed(); - auto valueStr = line.sliced(splitCharIndex + 1).trimmed(); - - QString key{""}; - QString localeStr{defaultKeyStr}; - // NOTE: - // We are process "localized keys" here, for usage check: - // https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#localized-keys - qsizetype localeBegin = keyStr.indexOf('['); - qsizetype localeEnd = keyStr.lastIndexOf(']'); - if ((localeBegin == -1 && localeEnd != -1) || (localeBegin != -1 && localeEnd == -1)) { - qWarning() << "unmatched [] detected in desktop file, skip this line: " << line; - return DesktopErrorCode::NoError; - } - - if (localeBegin == -1 && localeEnd == -1) { - key = keyStr; - } else { - key = keyStr.sliced(0, localeBegin); - localeStr = keyStr.sliced(localeBegin + 1, localeEnd - localeBegin - 1); // strip '[' and ']' - } - - static const QRegularExpression _re = []() { - QRegularExpression tmp{"R([^A-Za-z0-9-])"}; - tmp.optimize(); - return tmp; - }(); - // NOTE: https://stackoverflow.com/a/25583104 - thread_local const QRegularExpression re = _re; - if (re.match(key).hasMatch()) { - qWarning() << "invalid key name, skip this line:" << line; - return DesktopErrorCode::NoError; - } - - if (localeStr != defaultKeyStr && !isInvalidLocaleString(localeStr)) { - qWarning().noquote() << QString("invalid LOCALE (%2) for key \"%1\"").arg(key, localeStr); - } - - auto keyIt = group->find(key); - if (keyIt != group->end() && keyIt->find(localeStr) != keyIt->end()) { - qWarning() << "duplicated localestring, skip this line:" << line; - return DesktopErrorCode::NoError; - } - - if (keyIt == group->end()) { - group->insert(key, {{localeStr, valueStr}}); - return DesktopErrorCode::NoError; - } - - keyIt->insert(localeStr, valueStr); - return DesktopErrorCode::NoError; -} - -} // namespace - QString DesktopFile::sourcePath() const noexcept { if (!m_fileSource) { @@ -293,20 +97,20 @@ std::optional DesktopFile::createTemporaryDesktopFile(const QString return createTemporaryDesktopFile(std::move(tempFile)); } -std::optional DesktopFile::searchDesktopFileByPath(const QString &desktopFile, DesktopErrorCode &err) noexcept +std::optional DesktopFile::searchDesktopFileByPath(const QString &desktopFile, ParserError &err) noexcept { decltype(auto) desktopSuffix = ".desktop"; if (!desktopFile.endsWith(desktopSuffix)) { qWarning() << "file isn't a desktop file:" << desktopFile; - err = DesktopErrorCode::MismatchedFile; + err = ParserError::MismatchedFile; return std::nullopt; } QFileInfo fileinfo{desktopFile}; if (!fileinfo.isAbsolute() or !fileinfo.exists()) { qWarning() << "desktop file not found."; - err = DesktopErrorCode::NotFound; + err = ParserError::NotFound; return std::nullopt; } @@ -334,11 +138,11 @@ std::optional DesktopFile::searchDesktopFileByPath(const QString &d auto [ctime, mtime, _] = getFileTimeInfo(QFileInfo{*filePtr}); - err = DesktopErrorCode::NoError; + err = ParserError::NoError; return DesktopFile{std::move(filePtr), std::move(id), mtime, ctime}; } -std::optional DesktopFile::searchDesktopFileById(const QString &appId, DesktopErrorCode &err) noexcept +std::optional DesktopFile::searchDesktopFileById(const QString &appId, ParserError &err) noexcept { auto XDGDataDirs = getDesktopFileDirs(); constexpr auto desktopSuffix = u8".desktop"; @@ -360,7 +164,7 @@ std::optional DesktopFile::searchDesktopFileById(const QString &app } } - err = DesktopErrorCode::NotFound; + err = ParserError::NotFound; return std::nullopt; } @@ -369,13 +173,13 @@ bool DesktopFile::modified(qint64 time) const noexcept return time != m_mtime; } -DesktopErrorCode DesktopEntry::parse(const DesktopFile &file) noexcept +ParserError DesktopEntry::parse(const DesktopFile &file) noexcept { DesktopFileGuard guard{file}; if (!guard.try_open()) { qWarning() << file.sourcePath() << "can't open."; - return DesktopErrorCode::OpenFailed; + return ParserError::OpenFailed; } QTextStream stream; @@ -383,29 +187,29 @@ DesktopErrorCode DesktopEntry::parse(const DesktopFile &file) noexcept return parse(stream); } -DesktopErrorCode DesktopEntry::parse(QTextStream &stream) noexcept +ParserError DesktopEntry::parse(QTextStream &stream) noexcept { - if (m_parsed == true) { - return DesktopErrorCode::Parsed; + if (m_parsed) { + return ParserError::Parsed; } if (stream.atEnd()) { - return DesktopErrorCode::OpenFailed; + return ParserError::OpenFailed; } stream.setEncoding(QStringConverter::Utf8); - DesktopErrorCode err{DesktopErrorCode::NoError}; - Parser p(stream); + ParserError err{ParserError::NoError}; + DesktopFileParser p(stream); err = p.parse(m_entryMap); m_parsed = true; - if (err != DesktopErrorCode::NoError) { + if (err != ParserError::NoError) { return err; } if (!checkMainEntryValidation()) { qWarning() << "invalid MainEntry, abort."; - err = DesktopErrorCode::MissingInfo; + err = ParserError::MissingInfo; } return err; @@ -584,40 +388,3 @@ QDebug operator<<(QDebug debug, const DesktopEntry::Value &v) debug << static_cast &>(v); return debug; } - -QDebug operator<<(QDebug debug, const DesktopErrorCode &v) -{ - QDebugStateSaver saver{debug}; - QString errMsg; - switch (v) { - case DesktopErrorCode::NoError: { - errMsg = "no error."; - } break; - case DesktopErrorCode::NotFound: { - errMsg = "file not found."; - } break; - case DesktopErrorCode::MismatchedFile: { - errMsg = "file type is mismatched."; - } break; - case DesktopErrorCode::InvalidLocation: { - errMsg = "file location is invalid, please check $XDG_DATA_DIRS."; - } break; - case DesktopErrorCode::OpenFailed: { - errMsg = "couldn't open the file."; - } break; - case DesktopErrorCode::InvalidFormat: { - errMsg = "the format of desktopEntry file is invalid."; - } break; - case DesktopErrorCode::MissingInfo: { - errMsg = "missing required infomation."; - } break; - case DesktopErrorCode::Parsed: { - errMsg = "this desktop entry is parsed."; - } break; - case DesktopErrorCode::InternalError: { - errMsg = "internal error of parser."; - } break; - } - debug << errMsg; - return debug; -} diff --git a/src/desktopentry.h b/src/desktopentry.h index 0bf85f5..789a356 100644 --- a/src/desktopentry.h +++ b/src/desktopentry.h @@ -12,21 +12,10 @@ #include #include #include +#include "iniParser.h" constexpr static auto defaultKeyStr = "default"; -enum class DesktopErrorCode { - NoError, - NotFound, - MismatchedFile, - InvalidLocation, - InvalidFormat, - OpenFailed, - MissingInfo, - Parsed, - InternalError, -}; - enum class EntryContext { Unknown, EntryOuter, Entry, Done }; enum class EntryValueType { String, LocaleString, Boolean, IconString }; @@ -53,8 +42,8 @@ struct DesktopFile friend bool operator==(const DesktopFile &lhs, const DesktopFile &rhs); friend bool operator!=(const DesktopFile &lhs, const DesktopFile &rhs); - static std::optional searchDesktopFileById(const QString &appId, DesktopErrorCode &err) noexcept; - static std::optional searchDesktopFileByPath(const QString &desktopFilePath, DesktopErrorCode &err) noexcept; + static std::optional searchDesktopFileById(const QString &appId, ParserError &err) noexcept; + static std::optional searchDesktopFileByPath(const QString &desktopFilePath, ParserError &err) noexcept; static std::optional createTemporaryDesktopFile(const QString &temporaryFile) noexcept; static std::optional createTemporaryDesktopFile(std::unique_ptr temporaryFile) noexcept; @@ -138,8 +127,8 @@ public: DesktopEntry &operator=(DesktopEntry &&) = default; ~DesktopEntry() = default; - [[nodiscard]] DesktopErrorCode parse(const DesktopFile &file) noexcept; - [[nodiscard]] DesktopErrorCode parse(QTextStream &stream) noexcept; + [[nodiscard]] ParserError parse(const DesktopFile &file) noexcept; + [[nodiscard]] ParserError parse(QTextStream &stream) noexcept; [[nodiscard]] std::optional> group(const QString &key) const noexcept; [[nodiscard]] std::optional value(const QString &key, const QString &valueKey) const noexcept; @@ -154,8 +143,6 @@ private: QDebug operator<<(QDebug debug, const DesktopEntry::Value &v); -QDebug operator<<(QDebug debug, const DesktopErrorCode &v); - bool operator==(const DesktopEntry &lhs, const DesktopEntry &rhs); bool operator!=(const DesktopEntry &lhs, const DesktopEntry &rhs); diff --git a/src/desktopfileparser.cpp b/src/desktopfileparser.cpp new file mode 100644 index 0000000..fa45a7e --- /dev/null +++ b/src/desktopfileparser.cpp @@ -0,0 +1,159 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include +#include "desktopfileparser.h" +#include "constant.h" + +namespace { +bool isInvalidLocaleString(const QString &str) noexcept +{ + constexpr auto Language = R"((?:[a-z]+))"; // language of locale postfix. eg.(en, zh) + constexpr auto Country = R"((?:_[A-Z]+))"; // country of locale postfix. eg.(US, CN) + constexpr auto Encoding = R"((?:\.[0-9A-Z-]+))"; // encoding of locale postfix. eg.(UFT-8) + constexpr auto Modifier = R"((?:@[a-zA-Z=;]+))"; // modifier of locale postfix. eg.(euro;collation=traditional) + const static auto validKey = QString(R"(^%1%2?%3?%4?$)").arg(Language, Country, Encoding, Modifier); + // example: https://regex101.com/r/hylOay/2 + static const QRegularExpression _re = []() -> QRegularExpression { + QRegularExpression tmp{validKey}; + tmp.optimize(); + return tmp; + }(); + thread_local const auto re = _re; + + return re.match(str).hasMatch(); +} + +} // namespace + +ParserError DesktopFileParser::parse(Groups &ret) noexcept +{ + std::remove_reference_t groups; + while (!m_stream.atEnd()) { + auto err = addGroup(groups); + if (err != ParserError::NoError) { + return err; + } + + if (groups.size() != 1) { + continue; + } + + if (groups.keys().first() != DesktopFileEntryKey) { + qWarning() << "There should be nothing preceding " + "'Desktop Entry' group in the desktop entry file " + "but possibly one or more comments."; + return ParserError::InvalidFormat; + } + } + + if (!m_line.isEmpty()) { + qCritical() << "Something is wrong in Desktop file parser, check logic."; + return ParserError::InternalError; + } + + ret = std::move(groups); + return ParserError::NoError; +} + +ParserError DesktopFileParser::addGroup(Groups &ret) noexcept +{ + skip(); + if (!m_line.startsWith('[')) { + qWarning() << "Invalid desktop file format: unexpected line:" << m_line; + return ParserError::InvalidFormat; + } + + // Parsing group header. + // https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#group-header + + auto groupHeader = m_line.sliced(1, m_line.size() - 2).trimmed(); + + if (groupHeader.contains('[') || groupHeader.contains(']') || hasNonAsciiAndControlCharacters(groupHeader)) { + qWarning() << "group header invalid:" << m_line; + return ParserError::InvalidFormat; + } + + if (ret.find(groupHeader) != ret.end()) { + qWarning() << "duplicated group header detected:" << groupHeader; + return ParserError::InvalidFormat; + } + + auto group = ret.insert(groupHeader, {}); + + m_line.clear(); + while (!m_stream.atEnd() && !m_line.startsWith('[')) { + skip(); + if (m_line.startsWith('[')) { + break; + } + auto err = addEntry(group); + if (err != ParserError::NoError) { + return err; + } + } + return ParserError::NoError; +} + +ParserError DesktopFileParser::addEntry(typename Groups::iterator &group) noexcept +{ + auto line = m_line; + m_line.clear(); + auto splitCharIndex = line.indexOf('='); + if (splitCharIndex == -1) { + qWarning() << "invalid line in desktop file, skip it:" << line; + return ParserError::NoError; + } + auto keyStr = line.first(splitCharIndex).trimmed(); + auto valueStr = line.sliced(splitCharIndex + 1).trimmed(); + + QString key{""}; + QString localeStr{defaultKeyStr}; + // NOTE: + // We are process "localized keys" here, for usage check: + // https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#localized-keys + qsizetype localeBegin = keyStr.indexOf('['); + qsizetype localeEnd = keyStr.lastIndexOf(']'); + if ((localeBegin == -1 && localeEnd != -1) || (localeBegin != -1 && localeEnd == -1)) { + qWarning() << "unmatched [] detected in desktop file, skip this line: " << line; + return ParserError::NoError; + } + + if (localeBegin == -1 && localeEnd == -1) { + key = keyStr; + } else { + key = keyStr.sliced(0, localeBegin); + localeStr = keyStr.sliced(localeBegin + 1, localeEnd - localeBegin - 1); // strip '[' and ']' + } + + static const QRegularExpression _re = []() { + QRegularExpression tmp{"R([^A-Za-z0-9-])"}; + tmp.optimize(); + return tmp; + }(); + // NOTE: https://stackoverflow.com/a/25583104 + thread_local const QRegularExpression re = _re; + if (re.match(key).hasMatch()) { + qWarning() << "invalid key name, skip this line:" << line; + return ParserError::NoError; + } + + if (localeStr != defaultKeyStr && !isInvalidLocaleString(localeStr)) { + qWarning().noquote() << QString("invalid LOCALE (%2) for key \"%1\"").arg(key, localeStr); + } + + auto keyIt = group->find(key); + if (keyIt != group->end() && keyIt->find(localeStr) != keyIt->end()) { + qWarning() << "duplicated localestring, skip this line:" << line; + return ParserError::NoError; + } + + if (keyIt == group->end()) { + group->insert(key, {{localeStr, valueStr}}); + return ParserError::NoError; + } + + keyIt->insert(localeStr, valueStr); + return ParserError::NoError; +} diff --git a/src/desktopfileparser.h b/src/desktopfileparser.h new file mode 100644 index 0000000..a0d0666 --- /dev/null +++ b/src/desktopfileparser.h @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef DESKTOPFILEPARSER_H +#define DESKTOPFILEPARSER_H + +#include "iniParser.h" +#include "desktopentry.h" + +class DesktopFileParser : public Parser +{ +public: + using Parser::Parser; + ParserError parse(Groups &ret) noexcept override; + ParserError addGroup(Groups &ret) noexcept override; + ParserError addEntry(Groups::iterator &group) noexcept override; +}; + +#endif diff --git a/src/global.h b/src/global.h index 0e93d58..b867d09 100644 --- a/src/global.h +++ b/src/global.h @@ -29,10 +29,12 @@ Q_DECLARE_LOGGING_CATEGORY(DDEAMProf) using ObjectInterfaceMap = QMap; using ObjectMap = QMap; -using PropMap = QMap>; +using KVPairs = QMap; +using PropMap = QMap; Q_DECLARE_METATYPE(ObjectInterfaceMap) Q_DECLARE_METATYPE(ObjectMap) +Q_DECLARE_METATYPE(KVPairs) Q_DECLARE_METATYPE(PropMap) struct SystemdUnitDBusMessage @@ -401,23 +403,36 @@ inline QStringList getDesktopFileDirs() return XDGDataDirs; } -inline QStringList getAutoStartDirs() +inline QString getXDGConfigHome() +{ + auto XDGConfigHome = QString::fromLocal8Bit(qgetenv("XDG_CONFIG_HOME")); + if (XDGConfigHome.isEmpty()) { + XDGConfigHome = QString::fromLocal8Bit(qgetenv("HOME")) + QDir::separator() + ".config"; + } + + return XDGConfigHome; +} + +inline QStringList getXDGConfigDirs() { auto XDGConfigDirs = QString::fromLocal8Bit(qgetenv("XDG_CONFIG_DIRS")).split(':', Qt::SkipEmptyParts); if (XDGConfigDirs.isEmpty()) { XDGConfigDirs.append("/etc/xdg"); } - auto XDGConfigHome = QString::fromLocal8Bit(qgetenv("XDG_CONFIG_HOME")); - if (XDGConfigHome.isEmpty()) { - XDGConfigHome = QString::fromLocal8Bit(qgetenv("HOME")) + QDir::separator() + ".config"; - } + auto XDGConfigHome = getXDGConfigHome(); if (XDGConfigDirs.constFirst() != XDGConfigHome) { XDGConfigDirs.removeAll(XDGConfigHome); XDGConfigDirs.push_front(std::move(XDGConfigHome)); // guarantee XDG_CONFIG_HOME is first element. } + return XDGConfigDirs; +} + +inline QStringList getAutoStartDirs() +{ + auto XDGConfigDirs = getXDGConfigDirs(); std::for_each(XDGConfigDirs.begin(), XDGConfigDirs.end(), [](QString &str) { if (!str.endsWith(QDir::separator())) { str.append(QDir::separator()); @@ -428,6 +443,17 @@ inline QStringList getAutoStartDirs() return XDGConfigDirs; } +inline QString getCurrentDesktop() +{ + auto desktops = QString::fromLocal8Bit(qgetenv("XDG_CURRENT_DESKTOP")).split(';', Qt::SkipEmptyParts); + + if (desktops.size() > 1) { + qWarning() << "multi-DE is detected, use first value."; + } + + return desktops.first(); +} + inline bool isApplication(const QDBusObjectPath &path) { return path.path().split('/').last().startsWith("app"); diff --git a/src/iniParser.h b/src/iniParser.h new file mode 100644 index 0000000..846c93f --- /dev/null +++ b/src/iniParser.h @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef INIPARSER_H +#define INIPARSER_H + +#include +#include +#include +#include + +enum class ParserError { + NoError, + NotFound, + MismatchedFile, + InvalidLocation, + InvalidFormat, + OpenFailed, + MissingInfo, + Parsed, + InternalError +}; + +template +class Parser +{ +public: + explicit Parser(QTextStream &stream) + : m_stream(stream){}; + virtual ~Parser() = default; + using Groups = QMap>; + + Parser(const Parser &) = delete; + Parser(Parser &&) = delete; + Parser &operator=(const Parser &) = delete; + Parser &operator=(Parser &&) = delete; + + virtual ParserError parse(Groups &groups) noexcept = 0; + virtual ParserError addGroup(Groups &groups) noexcept = 0; + virtual ParserError addEntry(typename Groups::iterator &group) noexcept = 0; + void skip() noexcept + { + while (!m_stream.atEnd() and (m_line.startsWith('#') or m_line.isEmpty())) { + m_line = m_stream.readLine().trimmed(); + } + }; + +protected: + QTextStream &m_stream; + QString m_line; +}; + +inline bool hasNonAsciiAndControlCharacters(const QString &str) noexcept +{ + static const QRegularExpression _matchControlChars = []() { + QRegularExpression tmp{R"(\p{Cc})"}; + tmp.optimize(); + return tmp; + }(); + thread_local const auto matchControlChars = _matchControlChars; + static const QRegularExpression _matchNonAsciiChars = []() { + QRegularExpression tmp{R"([^\x00-\x7f])"}; + tmp.optimize(); + return tmp; + }(); + thread_local const auto matchNonAsciiChars = _matchNonAsciiChars; + return str.contains(matchControlChars) and str.contains(matchNonAsciiChars); +} + +inline QDebug operator<<(QDebug debug, const ParserError &v) +{ + QDebugStateSaver saver{debug}; + QString errMsg; + switch (v) { + case ParserError::NoError: { + errMsg = "no error."; + } break; + case ParserError::NotFound: { + errMsg = "file not found."; + } break; + case ParserError::MismatchedFile: { + errMsg = "file type is mismatched."; + } break; + case ParserError::InvalidLocation: { + errMsg = "file location is invalid, please check $XDG_DATA_DIRS."; + } break; + case ParserError::OpenFailed: { + errMsg = "couldn't open the file."; + } break; + case ParserError::InvalidFormat: { + errMsg = "the format of desktopEntry file is invalid."; + } break; + case ParserError::MissingInfo: { + errMsg = "missing required infomation."; + } break; + case ParserError::Parsed: { + errMsg = "this desktop entry is parsed."; + } break; + case ParserError::InternalError: { + errMsg = "internal error of parser."; + } break; + } + debug << errMsg; + return debug; +} + +#endif diff --git a/src/mimefileparser.cpp b/src/mimefileparser.cpp new file mode 100644 index 0000000..9d71b06 --- /dev/null +++ b/src/mimefileparser.cpp @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "mimefileparser.h" +#include + +ParserError MimeFileParser::parse(Groups &ret) noexcept +{ + std::remove_reference_t groups; + while (!m_stream.atEnd()) { + auto err = addGroup(groups); + if (err != ParserError::NoError) { + ret.clear(); + return err; + } + } + + if (!m_line.isEmpty()) { + qCritical() << "Something is wrong in mimeapp.list parser, check logic."; + ret.clear(); + return ParserError::InternalError; + } + + ret = std::move(groups); + return ParserError::NoError; +} + +ParserError MimeFileParser::addGroup(Groups &ret) noexcept +{ + skip(); + if (!m_line.startsWith('[')) { + qWarning() << "Invalid mimeapp.list format: unexpected line:" << m_line; + return ParserError::InvalidFormat; + } + + // Parsing group header, this format is same as desktop file's group + + auto groupHeader = m_line.sliced(1, m_line.size() - 2).trimmed(); + + if (groupHeader.contains('[') || groupHeader.contains(']') || hasNonAsciiAndControlCharacters(groupHeader)) { + qWarning() << "group header invalid:" << m_line; + return ParserError::InvalidFormat; + } + + if (m_desktopSpec and (groupHeader == addedAssociations or groupHeader == removedAssociations)) { + qWarning() + << "desktop-specific mimeapp.list is not possible to add or remove associations from these files, skip this group."; + while (!m_stream.atEnd() && !m_line.startsWith('[')) { + skip(); + if (m_line.startsWith('[')) { + break; + } + } + return ParserError::NoError; + } + + Groups::iterator group; + if (group = ret.find(groupHeader); group == ret.end()) { + group = ret.insert(groupHeader, {}); + } + + m_line.clear(); + while (!m_stream.atEnd() && !m_line.startsWith('[')) { + skip(); + if (m_line.startsWith('[')) { + break; + } + auto err = addEntry(group); + if (err != ParserError::NoError) { + return err; + } + } + return ParserError::NoError; +} + +ParserError MimeFileParser::addEntry(Groups::iterator &group) noexcept +{ + auto line = m_line; + m_line.clear(); + auto splitCharIndex = line.indexOf('='); + if (splitCharIndex == -1) { + qWarning() << "invalid line in desktop file, skip it:" << line; + return ParserError::NoError; + } + auto keyStr = line.first(splitCharIndex).trimmed(); + auto valueStr = line.sliced(splitCharIndex + 1).trimmed(); + + if (valueStr.isEmpty()) { + return ParserError::InvalidFormat; + } + + auto newValues = valueStr.split(';', Qt::SkipEmptyParts); + auto value = group->value(keyStr); + value.append(newValues); + group->insert(keyStr, newValues); + + return ParserError::NoError; +} diff --git a/src/mimefileparser.h b/src/mimefileparser.h new file mode 100644 index 0000000..93b8940 --- /dev/null +++ b/src/mimefileparser.h @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef MIMEAPPFILEPARSER_H +#define MIMEAPPFILEPARSER_H + +#include "iniParser.h" +#include +#include + +constexpr auto defaultApplications = "Default Applications"; +constexpr auto addedAssociations = "Added Associations"; +constexpr auto removedAssociations = "Removed Associations"; +constexpr auto mimeCache = "MIME Cache"; + +class MimeFileParser : public Parser +{ +public: + explicit MimeFileParser(QTextStream &stream, bool isDesktopSpecific) + : Parser(stream) + , m_desktopSpec(isDesktopSpecific) + { + } + ParserError parse(Groups &ret) noexcept override; + ParserError addGroup(Groups &ret) noexcept override; + ParserError addEntry(Groups::iterator &group) noexcept override; + +private: + bool m_desktopSpec; +}; + +#endif diff --git a/tests/ut_desktopentry.cpp b/tests/ut_desktopentry.cpp index f50c30c..f29a883 100644 --- a/tests/ut_desktopentry.cpp +++ b/tests/ut_desktopentry.cpp @@ -18,8 +18,8 @@ public: env = qgetenv("XDG_DATA_DIRS"); auto curDir = QDir::current(); QByteArray fakeXDG = (curDir.absolutePath() + QDir::separator() + "data").toLocal8Bit(); - ASSERT_TRUE(qputenv("XDG_DATA_DIRS", fakeXDG)) ; - DesktopErrorCode err; + ASSERT_TRUE(qputenv("XDG_DATA_DIRS", fakeXDG)); + ParserError err; auto file = DesktopFile::searchDesktopFileById("deepin-editor", err); if (!file.has_value()) { qWarning() << "search failed:" << err; @@ -58,7 +58,7 @@ TEST_F(TestDesktopEntry, prase) ASSERT_TRUE(in.open(QFile::ExistingOnly | QFile::ReadOnly | QFile::Text)); QTextStream fs{&in}; auto err = entry.parse(fs); - ASSERT_EQ(err, DesktopErrorCode::NoError); + ASSERT_EQ(err, ParserError::NoError); auto group = entry.group("Desktop Entry"); ASSERT_TRUE(group);