feat: add mimeManager Service

Signed-off-by: ComixHe <heyuming@deepin.org>
This commit is contained in:
ComixHe 2023-09-20 18:29:42 +08:00 committed by black-desk
parent 8970298ad0
commit f63741b023
25 changed files with 1282 additions and 296 deletions

View File

@ -11,6 +11,7 @@
<property name="X_Flatpak" type="b" access="read"/>
<property name="installedTime" type="t" access="read"/>
<property name="NoDisplay" type="b" access="read"/>
<property name="MimeTypes" type="as" access="readwrite"/>
<property name="Actions" type="as" access="read">
<annotation

View File

@ -0,0 +1,26 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "https://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.desktopspec.MimeManager1">
<method name="queryFileTypeAndDefaultApplication">
<arg type="s" name="filePath" direction="in"/>
<arg type="s" name="mimeType" direction="out"/>
<arg type="o" name="application" direction="out"/>
</method>
<method name="setDefaultApplication">
<arg type="a{ss}" name="defaultApps" direction="in"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="KVPairs"/>
</method>
<method name="unsetDefaultApplication">
<arg type="as" name="mimeTypes" direction="in"/>
</method>
<method name="listApplications">
<arg type="s" name="mimeType" direction="in"/>
<arg name="applications_and_properties" type="a{oa{sa{sv}}}" direction="out"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="ObjectMap"/>
</method>
</interface>
</node>

View File

@ -21,7 +21,7 @@ void registerComplexDbusType()
qDBusRegisterMetaType<ObjectInterfaceMap>();
qRegisterMetaType<ObjectMap>();
qDBusRegisterMetaType<ObjectMap>();
qDBusRegisterMetaType<QMap<QString, QString>>();
qDBusRegisterMetaType<KVPairs>();
qRegisterMetaType<PropMap>();
qDBusRegisterMetaType<PropMap>();
qDBusRegisterMetaType<QDBusObjectPath>();

View File

@ -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;
}

363
src/applicationmimeinfo.cpp Normal file
View File

@ -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> 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> 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 &section, 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> 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> 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();
}
}

123
src/applicationmimeinfo.h Normal file
View File

@ -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 <QStringList>
#include <QMap>
#include <QFile>
#include <QMimeData>
#include <memory>
#include <QSemaphore>
#include <optional>
#include <QFileInfo>
#include "mimefileparser.h"
using MimeContent = MimeFileParser::Groups;
QStringList getListFiles() noexcept;
class MimeFileBase
{
public:
static std::optional<MimeFileBase> 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<MimeApps> 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 &section, const QString &mimeType, const QString &appId) noexcept;
explicit MimeApps(MimeFileBase &&base);
};
class MimeCache : public MimeFileBase
{
public:
static std::optional<MimeCache> 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<MimeInfo> 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<MimeApps> &appsList() noexcept { return m_appsList; }
[[nodiscard]] const std::vector<MimeApps> &appsList() const noexcept { return m_appsList; }
[[nodiscard]] std::optional<MimeCache> &cacheInfo() noexcept { return m_cache; }
[[nodiscard]] const std::optional<MimeCache> &cacheInfo() const noexcept { return m_cache; }
[[nodiscard]] const QString &directory() const noexcept { return m_directory; }
void reload() noexcept;
private:
MimeInfo() = default;
std::vector<MimeApps> m_appsList;
std::optional<MimeCache> m_cache{std::nullopt};
QString m_directory;
};
#endif

View File

@ -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";

View File

@ -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}

View File

@ -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<QDir>(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 QSharedPointer<Applicat
}
auto err = newEntry->parse(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<QDir>(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<ApplicationService> &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<QDBusObjectPath, QSharedPointer<ApplicationService>>
ApplicationManager1Service::findApplicationsByIds(const QStringList &appIds) const noexcept
{
QMap<QDBusObjectPath, QSharedPointer<ApplicationService>> 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;
}

View File

@ -14,6 +14,7 @@
#include <QMap>
#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<QDBusObjectPath, QSharedPointer<ApplicationService>>
findApplicationsByIds(const QStringList &appIds) const noexcept;
void updateApplication(const QSharedPointer<ApplicationService> &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<Identifier> m_identifier;
std::weak_ptr<ApplicationManager1Storage> m_storage;
QScopedPointer<MimeManager1Service> m_mimeManager{nullptr};
QScopedPointer<JobManager1Service> m_jobManager{nullptr};
QStringList m_hookElements;
QMap<QDBusObjectPath, QSharedPointer<ApplicationService>> m_applicationList;
void scanMimeInfos() noexcept;
void scanApplications() noexcept;
void scanInstances() noexcept;
void scanAutoStart() noexcept;

View File

@ -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> ApplicationService::createApplicationService(
std::unique_ptr<DesktopEntry> entry{std::make_unique<DesktopEntry>()};
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<QDBusObjectPath> 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)

View File

@ -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<QDBusObjectPath> Instances READ instances NOTIFY instanceChanged)
[[nodiscard]] QList<QDBusObjectPath> instances() const noexcept;
@ -129,6 +134,7 @@ Q_SIGNALS:
void actionNameChanged();
void actionsChanged();
void categoriesChanged();
void MimeTypesChanged();
private:
friend class ApplicationManager1Service;

View File

@ -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<LaunchTask>();

View File

@ -51,7 +51,7 @@ public:
static_assert(std::is_invocable_v<F, QVariant>, "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<QVariantList> future = QtConcurrent::mappedReduced(std::move(args),
func,
qOverload<QVariantList::parameter_type>(&QVariantList::append),

View File

@ -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<ApplicationManager1Service *>(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<ApplicationManager1Service *>(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));
}

View File

@ -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 <QObject>
#include <QDBusContext>
#include <QDBusObjectPath>
#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<MimeInfo> m_infos;
};
#endif

View File

@ -4,6 +4,7 @@
#include "desktopentry.h"
#include "global.h"
#include "desktopfileparser.h"
#include <QFileInfo>
#include <QDir>
#include <algorithm>
@ -15,203 +16,6 @@
#include <chrono>
#include <cstdio>
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<QString, QMap<QString, DesktopEntry::Value>>;
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<decltype(ret)> 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> DesktopFile::createTemporaryDesktopFile(const QString
return createTemporaryDesktopFile(std::move(tempFile));
}
std::optional<DesktopFile> DesktopFile::searchDesktopFileByPath(const QString &desktopFile, DesktopErrorCode &err) noexcept
std::optional<DesktopFile> 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> 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> DesktopFile::searchDesktopFileById(const QString &appId, DesktopErrorCode &err) noexcept
std::optional<DesktopFile> DesktopFile::searchDesktopFileById(const QString &appId, ParserError &err) noexcept
{
auto XDGDataDirs = getDesktopFileDirs();
constexpr auto desktopSuffix = u8".desktop";
@ -360,7 +164,7 @@ std::optional<DesktopFile> 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<const QMap<QString, QString> &>(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;
}

View File

@ -12,21 +12,10 @@
#include <QTextStream>
#include <optional>
#include <QFile>
#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<DesktopFile> searchDesktopFileById(const QString &appId, DesktopErrorCode &err) noexcept;
static std::optional<DesktopFile> searchDesktopFileByPath(const QString &desktopFilePath, DesktopErrorCode &err) noexcept;
static std::optional<DesktopFile> searchDesktopFileById(const QString &appId, ParserError &err) noexcept;
static std::optional<DesktopFile> searchDesktopFileByPath(const QString &desktopFilePath, ParserError &err) noexcept;
static std::optional<DesktopFile> createTemporaryDesktopFile(const QString &temporaryFile) noexcept;
static std::optional<DesktopFile> createTemporaryDesktopFile(std::unique_ptr<QFile> 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<QMap<QString, Value>> group(const QString &key) const noexcept;
[[nodiscard]] std::optional<Value> 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);

159
src/desktopfileparser.cpp Normal file
View File

@ -0,0 +1,159 @@
// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#include <QRegularExpression>
#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<decltype(ret)> 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;
}

20
src/desktopfileparser.h Normal file
View File

@ -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<DesktopEntry::Value>
{
public:
using Parser<DesktopEntry::Value>::Parser;
ParserError parse(Groups &ret) noexcept override;
ParserError addGroup(Groups &ret) noexcept override;
ParserError addEntry(Groups::iterator &group) noexcept override;
};
#endif

View File

@ -29,10 +29,12 @@ Q_DECLARE_LOGGING_CATEGORY(DDEAMProf)
using ObjectInterfaceMap = QMap<QString, QVariantMap>;
using ObjectMap = QMap<QDBusObjectPath, ObjectInterfaceMap>;
using PropMap = QMap<QString, QMap<QString, QString>>;
using KVPairs = QMap<QString, QString>;
using PropMap = QMap<QString, KVPairs>;
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");

108
src/iniParser.h Normal file
View File

@ -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 <QTextStream>
#include <QString>
#include <QMap>
#include <QRegularExpression>
enum class ParserError {
NoError,
NotFound,
MismatchedFile,
InvalidLocation,
InvalidFormat,
OpenFailed,
MissingInfo,
Parsed,
InternalError
};
template <typename Value>
class Parser
{
public:
explicit Parser(QTextStream &stream)
: m_stream(stream){};
virtual ~Parser() = default;
using Groups = QMap<QString, QMap<QString, Value>>;
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

99
src/mimefileparser.cpp Normal file
View File

@ -0,0 +1,99 @@
// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#include "mimefileparser.h"
#include <QDebug>
ParserError MimeFileParser::parse(Groups &ret) noexcept
{
std::remove_reference_t<decltype(ret)> 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;
}

33
src/mimefileparser.h Normal file
View File

@ -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 <QMimeDatabase>
#include <QMimeType>
constexpr auto defaultApplications = "Default Applications";
constexpr auto addedAssociations = "Added Associations";
constexpr auto removedAssociations = "Removed Associations";
constexpr auto mimeCache = "MIME Cache";
class MimeFileParser : public Parser<QStringList>
{
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

View File

@ -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);