diff --git a/src/platforms/linux/daemon/apptracker.cpp b/src/platforms/linux/daemon/apptracker.cpp index aba3b5efb0..aecda3a68c 100644 --- a/src/platforms/linux/daemon/apptracker.cpp +++ b/src/platforms/linux/daemon/apptracker.cpp @@ -26,7 +26,6 @@ constexpr const char* DBUS_SYSTEMD_UNIT = "org.freedesktop.systemd1.Unit"; namespace { Logger logger("AppTracker"); -QString s_cgroupMount; } // namespace AppTracker::AppTracker(QObject* parent) : QObject(parent) { @@ -34,17 +33,14 @@ AppTracker::AppTracker(QObject* parent) : QObject(parent) { logger.debug() << "AppTracker created."; /* Monitor for changes to the user's application control groups. */ - s_cgroupMount = LinuxDependencies::findCgroup2Path(); + m_cgroupMount = LinuxDependencies::findCgroup2Path(); } AppTracker::~AppTracker() { MZ_COUNT_DTOR(AppTracker); logger.debug() << "AppTracker destroyed."; - for (AppData* data : m_runningApps) { - delete data; - } - m_runningApps.clear(); + m_runningCgroups.clear(); } void AppTracker::userCreated(uint userid, const QString& xdgRuntimePath) { @@ -69,22 +65,13 @@ void AppTracker::userCreated(uint userid, const QString& xdgRuntimePath) { QDBusConnection connection = QDBusConnection::connectToBus(busPath, "user-" + QString::number(userid)); - /* Connect to the user's GTK launch event. */ - bool gtkConnected = connection.connect( - "", GTK_DESKTOP_APP_PATH, GTK_DESKTOP_APP_SERVICE, "Launched", this, - SLOT(gtkLaunchEvent(const QByteArray&, const QString&, qlonglong, - const QStringList&, const QVariantMap&))); - if (!gtkConnected) { - logger.warning() << "Failed to connect to GTK Launched signal"; - } - // Watch the user's control groups for new application scopes. m_systemdInterface = new QDBusInterface(DBUS_SYSTEMD_SERVICE, DBUS_SYSTEMD_PATH, DBUS_SYSTEMD_MANAGER, connection, this); QVariant qv = m_systemdInterface->property("ControlGroup"); - if (!s_cgroupMount.isEmpty() && qv.type() == QVariant::String) { - QString userCgroupPath = s_cgroupMount + qv.toString(); + if (!m_cgroupMount.isEmpty() && qv.type() == QVariant::String) { + QString userCgroupPath = m_cgroupMount + qv.toString(); logger.debug() << "Monitoring Control Groups v2 at:" << userCgroupPath; connect(&m_cgroupWatcher, SIGNAL(directoryChanged(QString)), this, @@ -104,62 +91,135 @@ void AppTracker::userRemoved(uint userid) { QDBusConnection::disconnectFromBus("user-" + QString::number(userid)); } -void AppTracker::gtkLaunchEvent(const QByteArray& appid, const QString& display, - qlonglong pid, const QStringList& uris, - const QVariantMap& extra) { - Q_UNUSED(display); - Q_UNUSED(uris); - Q_UNUSED(extra); +// static +// Expand unicode escape sequences. +QString AppTracker::decodeUnicodeEscape(const QString& str) { + static const QRegularExpression re("(\\\\x[0-9A-Fa-f][0-9A-Fa-f])"); + + QString result = str; + qsizetype offset = 0; + while (offset < result.length()) { + // Search for the next unicode escape sequence. + QRegularExpressionMatch match = re.match(result, offset); + if (!match.hasMatch()) { + break; + } + + bool okay; + qsizetype start = match.capturedStart(0); + QChar code = match.captured(0).mid(2).toUShort(&okay, 16); + if (okay && (code != 0)) { + // Replace the matched escape sequence with the decoded character. + result.replace(start, match.capturedLength(0), QString(code)); + offset = start + 1; + } else { + // If we failed to decode the character, skip passed the matched string. + offset = match.capturedEnd(0); + } + } + + return result; +} - QString appIdName(appid); - while (appIdName.endsWith('\0')) { - appIdName.chop(1); +// The naming convention for snaps follows one of the following formats: +// snap...service - assigned by systemd for services +// snap..-.scope - transient scope for apps +// snap..hook.-.scope - transient scope for hooks +// +// However, at some point the separator between the app and UUID was +// swapped from a dot to a dash. Which makes the parsing a bit of a pain. +// +// See: https://github.com/snapcore/snapd/blob/master/sandbox/cgroup/scanning.go +QString AppTracker::snapDesktopFileId(const QString& scope) { + static const QRegularExpression snapuuid( + "[-.][0-9a-fA-F]{8}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}" + "\\b-[0-9a-fA-F]{12}"); + + // Strip the UUID out of the scope name + QString stripped(scope); + stripped.remove(snapuuid); + + // Split the remainder on dots and discard the extension. + QStringList split = stripped.split('.'); + split.removeLast(); + + // Parse the package and application. + QString package = split.value(1); + QString app = split.value(2); + if (app == "hook") { + app = split.value(3); } - if (!appIdName.isEmpty()) { - m_lastLaunchName = appIdName; - m_lastLaunchPid = pid; + if (package.isEmpty() || app.isEmpty()) { + return QString(); } + + // Reassemble the desktop identifier. + return QString("%1_%2.desktop").arg(package).arg(app); } -void AppTracker::appHeuristicMatch(AppData* data) { - // If this cgroup contains the last-launched PID, then we have a fairly - // strong indication of which application this control group is running. - for (int pid : data->pids()) { - if ((pid != 0) && (pid == m_lastLaunchPid)) { - logger.debug() << data->cgroup << "matches app:" << m_lastLaunchName; - data->appId = m_lastLaunchName; - data->rootpid = m_lastLaunchPid; - break; +// Make an attempt to resolve the desktop ID from a cgroup scope. +QString AppTracker::findDesktopFileId(const QString& cgroup) { + QString scopeName = QFileInfo(cgroup).fileName(); + + // Reverse the desktop ID from a cgroup scope and known launcher tools. + if (scopeName.startsWith("app-gnome-") || + scopeName.startsWith("app-flatpak-")) { + // These take the forms: + // app-gnome--.scope + // app-flatpak--.scope + // + // Some characters (typically hyphens) in the desktop file ID may be encoded + // as unicode escape sequences to preseve this formatting. + QString raw = scopeName.section('-', 2, 2); + if (!raw.isEmpty()) { + return QString("%1.desktop").arg(decodeUnicodeEscape(raw)); } } + QString gnomeLaunchdPrefix("gnome-launched-"); + if (scopeName.startsWith(gnomeLaunchdPrefix)) { + // These take the form: + // gnome-launched--.scope + // + // We have seen this on older Gnome desktop environments (eg: Ubuntu 20.04), + // and there is no escaping on the desktopFileId, meaning that it might + // contain embedded hyphens. Therefore, we search for the final hyphen that + // separates the desktopFileId from the PID. + qsizetype start = gnomeLaunchdPrefix.length(); + qsizetype end = scopeName.lastIndexOf('-'); + if (end > start) { + return scopeName.mid(start, end - start); + } + } + + // Snaps have their own format. + if (scopeName.startsWith("snap.")) { + return snapDesktopFileId(scopeName); + } + // Query the systemd unit for its SourcePath property, which is set to the // desktop file's full path on KDE - QString unit = QFileInfo(data->cgroup).fileName(); QDBusReply objPath = - m_systemdInterface->call("GetUnit", unit); + m_systemdInterface->call("GetUnit", scopeName); QDBusInterface interface(DBUS_SYSTEMD_SERVICE, objPath.value().path(), DBUS_SYSTEMD_UNIT, m_systemdInterface->connection(), this); QString source = interface.property("SourcePath").toString(); if (!source.isEmpty() && source.endsWith(".desktop")) { - data->appId = source; + return LinuxDependencies::desktopFileId(source); } - // TODO: Some comparison between the .desktop file and the directory name - // of the control group is also very likely to produce viable application - // matching, but this will have to be a fuzzy match of some sort because - // there's a lot of variability in how desktop environments choose to name - // them. + // Otherwise, we don't know the desktop ID for this control group. + return QString(); } void AppTracker::cgroupsChanged(const QString& directory) { QDir dir(directory); - QDir mountpoint(s_cgroupMount); + QDir mountpoint(m_cgroupMount); QFileInfoList newScopes = dir.entryInfoList( QStringList{"*.scope", "*@autostart.service"}, QDir::Dirs); - QStringList oldScopes = m_runningApps.keys(); + QStringList oldScopes = m_runningCgroups.keys(); // Figure out what has been added. for (const QFileInfo& scope : newScopes) { @@ -172,46 +232,22 @@ void AppTracker::cgroupsChanged(const QString& directory) { if (oldScopes.removeAll(path) == 0) { // This is a new scope, let's add it. logger.debug() << "Control group created:" << path; - AppData* data = new AppData(path); - - m_runningApps[path] = data; - appHeuristicMatch(data); + QString desktopFileId = findDesktopFileId(path); + m_runningCgroups[path] = desktopFileId; - emit appLaunched(data->cgroup, data->appId, data->rootpid); + emit appLaunched(path, desktopFileId); } } // Anything left, if it shares the same root directory, has been removed. for (const QString& scope : oldScopes) { - QFileInfo scopeInfo(s_cgroupMount + scope); + QFileInfo scopeInfo(m_cgroupMount + scope); if (scopeInfo.absolutePath() == directory) { logger.debug() << "Control group removed:" << scope; - Q_ASSERT(m_runningApps.contains(scope)); - AppData* data = m_runningApps.take(scope); + Q_ASSERT(m_runningCgroups.contains(scope)); + QString desktopFileId = m_runningCgroups.take(scope); - emit appTerminated(data->cgroup, data->appId); - delete data; + emit appTerminated(scope, desktopFileId); } } } - -QList AppData::pids() const { - QList results; - QFile cgroupProcs(s_cgroupMount + cgroup + "/cgroup.procs"); - - if (cgroupProcs.open(QIODevice::ReadOnly | QIODevice::Text)) { - while (true) { - QString line = QString::fromLocal8Bit(cgroupProcs.readLine()); - if (line.isEmpty()) { - break; - } - int pid = line.trimmed().toInt(); - if (pid != 0) { - results.append(pid); - } - } - cgroupProcs.close(); - } - - return results; -} diff --git a/src/platforms/linux/daemon/apptracker.h b/src/platforms/linux/daemon/apptracker.h index 2b2b5dca92..f525744216 100644 --- a/src/platforms/linux/daemon/apptracker.h +++ b/src/platforms/linux/daemon/apptracker.h @@ -13,18 +13,31 @@ class QDBusInterface; -class AppData { - public: - AppData(const QString& path) : cgroup(path) { MZ_COUNT_CTOR(AppData); } - ~AppData() { MZ_COUNT_DTOR(AppData); } - - QList pids() const; - - const QString cgroup; - QString appId; - int rootpid = 0; -}; - +// Applications on Linux can be a bit vague and hard to define at runtime, so +// we need to make some assumptions to try and tackle the problem. +// +// First off, we try to identify applications based on the application menu +// entries. According to the Freedesktop specification, these are described by +// the `*.desktop` files found under the user's XDG_DATA_DIRS environment +// variable. The Freedesktop specification also defines that the path to this +// file can be converted into a Desktop File ID, which we will use as the +// identifier of the application. +// +// However, the `*.desktop` files only describe how to launch an application. +// Once an application has started, there is no definitive way to track the +// processes of that application. For this, we rely on Linux control groups, +// or cgroups, which are used to group processes together for the purpose of +// establishing shared resource constraints and containerization. +// +// It just so happens that many modern desktop environments will group their +// processes forked from the application launchers into cgroups for resource +// management and containerization. This class attempts to track those cgroups +// and match them to the destop file ID from which they originated. +// +// This means that we only support application environments which make use of +// control groups for application containerization, and apps which can be +// launched via the applications menu. This limits us to Gnome, KDE, Flatpaks, +// and Snaps. class AppTracker final : public QObject { Q_OBJECT Q_DISABLE_COPY_MOVE(AppTracker) @@ -33,26 +46,44 @@ class AppTracker final : public QObject { explicit AppTracker(QObject* parent = nullptr); ~AppTracker(); + /** + * @brief Track a new user session for control group scopes where applications + * may be running. + * + * @param userid Unix User identifier. + * @param xdgRuntimePath User's runtime path (eg: "/run/user/"). + */ void userCreated(uint userid, const QString& xdgRuntimePath); + + /** + * @brief Terminate tracking of a user session. + * + * @param userid Unix User identifier. + */ void userRemoved(uint userid); - QHash::iterator begin() { return m_runningApps.begin(); } - QHash::iterator end() { return m_runningApps.end(); } - AppData* find(const QString& cgroup) { return m_runningApps.value(cgroup); } + /** + * @brief Return a list of control groups matching a given desktop file ID. + * + * @param desktopFileId desktop file ID to match. + * + * @returns a list control group scopes. + */ + QStringList findByDesktopFileId(const QString& desktopFileId) const { + return m_runningCgroups.keys(desktopFileId); + } signals: - void appLaunched(const QString& cgroup, const QString& appId, int rootpid); - void appTerminated(const QString& cgroup, const QString& appId); + void appLaunched(const QString& cgroup, const QString& desktopFileId); + void appTerminated(const QString& cgroup, const QString& desktopFileId); private slots: - void gtkLaunchEvent(const QByteArray& appid, const QString& display, - qlonglong pid, const QStringList& uris, - const QVariantMap& extra); - void cgroupsChanged(const QString& directory); private: - void appHeuristicMatch(AppData* data); + QString findDesktopFileId(const QString& cgroup); + static QString snapDesktopFileId(const QString& cgroup); + static QString decodeUnicodeEscape(const QString& str); private: // Monitoring of the user's control groups. @@ -60,10 +91,11 @@ class AppTracker final : public QObject { QFileSystemWatcher m_cgroupWatcher; QDBusInterface* m_systemdInterface = nullptr; - // The set of applications that we have tracked. - QHash m_runningApps; - QString m_lastLaunchName; - int m_lastLaunchPid; + // The set of control groups that are currently running, and the desktop file + // IDs to which we have mapped them. The key to this QHash is the control + // group path, and the value is the mapped desktop file ID, or an empty + // QString if unknown. + QHash m_runningCgroups; }; #endif // APPTRACKER_H diff --git a/src/platforms/linux/daemon/dbusservice.cpp b/src/platforms/linux/daemon/dbusservice.cpp index a310b17058..64f015853c 100644 --- a/src/platforms/linux/daemon/dbusservice.cpp +++ b/src/platforms/linux/daemon/dbusservice.cpp @@ -19,15 +19,12 @@ #include "leakdetector.h" #include "logger.h" #include "loghandler.h" +#include "platforms/linux/linuxdependencies.h" namespace { Logger logger("DBusService"); } -constexpr const char* APP_STATE_ACTIVE = "active"; -constexpr const char* APP_STATE_EXCLUDED = "excluded"; -constexpr const char* APP_STATE_BLOCKED = "blocked"; - constexpr const char* DBUS_LOGIN_SERVICE = "org.freedesktop.login1"; constexpr const char* DBUS_LOGIN_PATH = "/org/freedesktop/login1"; constexpr const char* DBUS_LOGIN_MANAGER = "org.freedesktop.login1.Manager"; @@ -44,8 +41,8 @@ DBusService::DBusService(QObject* parent) : Daemon(parent) { } m_appTracker = new AppTracker(this); - connect(m_appTracker, SIGNAL(appLaunched(QString, QString, int)), this, - SLOT(appLaunched(QString, QString, int))); + connect(m_appTracker, SIGNAL(appLaunched(QString, QString)), this, + SLOT(appLaunched(QString, QString))); connect(m_appTracker, SIGNAL(appTerminated(QString, QString)), this, SLOT(appTerminated(QString, QString))); @@ -138,11 +135,11 @@ bool DBusService::activate(const QString& jsonConfig) { } // (Re)load the split tunnelling configuration. - firewallClear(); + clearAppStates(); if (obj.contains("vpnDisabledApps")) { QJsonArray disabledApps = obj["vpnDisabledApps"].toArray(); for (const QJsonValue& app : disabledApps) { - firewallApp(app.toString(), APP_STATE_EXCLUDED); + setAppState(LinuxDependencies::desktopFileId(app.toString()), Excluded); } } @@ -157,7 +154,7 @@ bool DBusService::deactivate(bool emitSignals) { } m_sessionUid = 0; - firewallClear(); + clearAppStates(); return Daemon::deactivate(emitSignals); } @@ -224,119 +221,68 @@ void DBusService::userRemoved(uint uid, const QDBusObjectPath& path) { m_appTracker->userRemoved(uid); } -void DBusService::appLaunched(const QString& cgroup, const QString& appId, - int rootpid) { - logger.debug() << "tracking:" << cgroup << "appId:" << appId - << "PID:" << rootpid; +void DBusService::appLaunched(const QString& cgroup, + const QString& desktopFileId) { + logger.debug() << "tracking:" << cgroup << "id:" << desktopFileId; - // HACK: Quick and dirty split tunnelling. - // TODO: Apply filtering to currently-running apps too. - if (m_excludedApps.contains(appId)) { + AppState state = m_excludedApps.value(desktopFileId, Active); + if (state == Active) { + // Nothing to do here. + return; + } + + // Apply firewall rules to this control group. + m_excludedCgroups[cgroup] = state; + if (state == Excluded) { m_wgutils->excludeCgroup(cgroup); } } -void DBusService::appTerminated(const QString& cgroup, const QString& appId) { - logger.debug() << "terminate:" << cgroup; +void DBusService::appTerminated(const QString& cgroup, + const QString& desktopFileId) { + logger.debug() << "terminate:" << cgroup << "id:" << desktopFileId; - // HACK: Quick and dirty split tunnelling. - // TODO: Apply filtering to currently-running apps too. - if (m_excludedApps.contains(appId)) { + // Remove any firewall rules applied to this control group. + if (m_excludedCgroups.remove(cgroup)) { m_wgutils->resetCgroup(cgroup); } } -/* Get the list of running applications that the firewall knows about. */ -QString DBusService::runningApps() { - QJsonArray result; - - for (auto i = m_appTracker->begin(); i != m_appTracker->end(); i++) { - const AppData* data = *i; - QJsonObject appObject; - QJsonArray pidList; - appObject.insert("appId", QJsonValue(data->appId)); - appObject.insert("cgroup", QJsonValue(data->cgroup)); - appObject.insert("rootpid", QJsonValue(data->rootpid)); +void DBusService::setAppState(const QString& desktopFileId, AppState state) { + logger.debug() << "Setting" << desktopFileId << "to firewall state" << state; - for (int pid : data->pids()) { - pidList.append(QJsonValue(pid)); + // When the App is "Active" there is no special manipulation to do. + if (state == Active) { + m_excludedApps.remove(desktopFileId); + for (const QString& cgroup : + m_appTracker->findByDesktopFileId(desktopFileId)) { + m_wgutils->resetCgroup(cgroup); } - - appObject.insert("pids", pidList); - result.append(appObject); - } - - return QJsonDocument(result).toJson(QJsonDocument::Compact); -} - -/* Update the firewall for running applications matching the application ID. */ -bool DBusService::firewallApp(const QString& appName, const QString& state) { - if (!isCallerAuthorized()) { - logger.error() << "Insufficient caller permissions"; - return false; + return; } - logger.debug() << "Setting" << appName << "to firewall state" << state; - - // Update the split tunnelling state for any running apps. - for (auto i = m_appTracker->begin(); i != m_appTracker->end(); i++) { - const AppData* data = *i; - if (data->appId != appName) { - continue; + // Otherwise, apply special handling to any matching control groups. + m_excludedApps[desktopFileId] = state; + for (const QString& cgroup : + m_appTracker->findByDesktopFileId(desktopFileId)) { + if (m_excludedCgroups.contains(cgroup)) { + m_wgutils->resetCgroup(cgroup); } - if (state == APP_STATE_EXCLUDED) { - m_wgutils->excludeCgroup(data->cgroup); - } else { - m_wgutils->resetCgroup(data->cgroup); + m_excludedCgroups[cgroup] = state; + if (state == Excluded) { + // Excluded control groups are given special netfilter rules to direct + // their traffic outside of the VPN tunnel. + m_wgutils->excludeCgroup(cgroup); } } - - // Update the list of apps to exclude from the VPN. - if (state != APP_STATE_EXCLUDED) { - m_excludedApps.removeAll(appName); - } else if (!m_excludedApps.contains(appName)) { - m_excludedApps.append(appName); - } - return true; -} - -/* Update the firewall for the application matching the desired PID. */ -bool DBusService::firewallPid(int rootpid, const QString& state) { - if (!isCallerAuthorized()) { - logger.error() << "Insufficient caller permissions"; - return false; - } - -#if 0 - ProcessGroup* group = m_pidtracker->group(rootpid); - if (!group) { - return false; - } - - group->state = state; - group->moveToCgroup(getAppStateCgroup(group->state)); - - logger.debug() << "Setting" << group->name << "PID:" << rootpid - << "to firewall state" << state; - return true; -#else - Q_UNUSED(rootpid); - Q_UNUSED(state); - return false; -#endif } /* Clear the firewall and return all applications to the active state */ -bool DBusService::firewallClear() { - if (!isCallerAuthorized()) { - logger.error() << "Insufficient caller permissions"; - return false; - } - +void DBusService::clearAppStates() { logger.debug() << "Clearing excluded app list"; m_wgutils->resetAllCgroups(); + m_excludedCgroups.clear(); m_excludedApps.clear(); - return true; } /* Drop root permissions from the daemon. */ diff --git a/src/platforms/linux/daemon/dbusservice.h b/src/platforms/linux/daemon/dbusservice.h index 46d7e463d7..c2ca2913e2 100644 --- a/src/platforms/linux/daemon/dbusservice.h +++ b/src/platforms/linux/daemon/dbusservice.h @@ -6,6 +6,7 @@ #define DBUSSERVICE_H #include +#include #include "apptracker.h" #include "daemon/daemon.h" @@ -24,6 +25,9 @@ class DBusService final : public Daemon, protected QDBusContext { DBusService(QObject* parent); ~DBusService(); + enum AppState { Active, Excluded }; + Q_ENUM(AppState) + void setAdaptor(DbusAdaptor* adaptor); using Daemon::activate; @@ -38,11 +42,6 @@ class DBusService final : public Daemon, protected QDBusContext { QString getLogs(); void cleanupLogs() { cleanLogs(); } - QString runningApps(); - bool firewallApp(const QString& appName, const QString& state); - bool firewallPid(int rootpid, const QString& state); - bool firewallClear(); - protected: WireguardUtils* wgutils() const override { return m_wgutils; } bool supportIPUtils() const override { return true; } @@ -55,9 +54,12 @@ class DBusService final : public Daemon, protected QDBusContext { bool isCallerAuthorized(); void dropRootPermissions(); + void setAppState(const QString& desktopFileId, AppState state); + void clearAppStates(); + private slots: - void appLaunched(const QString& cgroup, const QString& appId, int rootpid); - void appTerminated(const QString& cgroup, const QString& appId); + void appLaunched(const QString& cgroup, const QString& desktopFileId); + void appTerminated(const QString& cgroup, const QString& desktopFileId); void userListCompleted(QDBusPendingCallWatcher* call); void userCreated(uint uid, const QDBusObjectPath& path); @@ -70,7 +72,8 @@ class DBusService final : public Daemon, protected QDBusContext { DnsUtilsLinux* m_dnsutils = nullptr; AppTracker* m_appTracker = nullptr; - QList m_excludedApps; + QHash m_excludedApps; + QHash m_excludedCgroups; uint m_sessionUid = 0; }; diff --git a/src/platforms/linux/daemon/org.mozilla.vpn.dbus.xml b/src/platforms/linux/daemon/org.mozilla.vpn.dbus.xml index 25c33c7904..50981cee01 100644 --- a/src/platforms/linux/daemon/org.mozilla.vpn.dbus.xml +++ b/src/platforms/linux/daemon/org.mozilla.vpn.dbus.xml @@ -14,22 +14,6 @@ - - - - - - - - - - - - - - - - diff --git a/src/platforms/linux/linuxdependencies.cpp b/src/platforms/linux/linuxdependencies.cpp index cde2baeb77..354e796690 100644 --- a/src/platforms/linux/linuxdependencies.cpp +++ b/src/platforms/linux/linuxdependencies.cpp @@ -156,3 +156,33 @@ QString LinuxDependencies::kdeFrameworkVersion() { return QString(); } + +// static +QString LinuxDependencies::desktopFileId(const QString& path) { + // Given the path to a .desktop file, return its Desktop File ID as per + // the freedesktop.org's Desktop Entry Spec. See: + // https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#desktop-file-id + // + // To determine the ID of a desktop file, make its full path relative to the + // $XDG_DATA_DIRS component in which the desktop file is installed, remove the + // "applications/" prefix, and turn '/' into '-'. + + // If the path contains no slashes, assume this conversion is already done. + if (!path.contains('/')) { + return path; + } + + // Find the application dir in the path. + const QString dirComponent("/applications/"); + qsizetype index = path.lastIndexOf(dirComponent); + if (index >= 0) { + index += dirComponent.length(); + } else if (index < 0) { + // If no applications dir was found, let's just use the filename. + index = path.lastIndexOf('/') + 1; + Q_ASSERT(index > 0); + } + + // Convert it. + return path.mid(index).replace('/', '-'); +} diff --git a/src/platforms/linux/linuxdependencies.h b/src/platforms/linux/linuxdependencies.h index 70ff6f18bd..4f863590bf 100644 --- a/src/platforms/linux/linuxdependencies.h +++ b/src/platforms/linux/linuxdependencies.h @@ -14,6 +14,7 @@ class LinuxDependencies final { static QString findCgroup2Path(); static QString gnomeShellVersion(); static QString kdeFrameworkVersion(); + static QString desktopFileId(const QString& path); private: LinuxDependencies() = default;