[kde-linux/package-compatibility-helper] src: Grant folder temporary access when a tool is sandboxed away from the file

Hadi Chokr <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 002edf6de6972a995ee9001f49a27b32b521bf4a by Hadi Chokr.
Committed on 18/08/2026 at 11:29.
Pushed by silverhadch into branch 'master'.

Grant folder temporary access when a tool is sandboxed away from the file

Closes #6 


Co-authored-by: Thomas Duckworth <[email protected]>
Signed-off-by: Hadi Chokr <[email protected]>

M  +76   -6    src/ICompatibilityHelper.cpp
M  +10   -2    src/ICompatibilityHelper.h
M  +10   -0    src/contents/ui/Main.qml

https://invent.kde.org/kde-linux/package-compatibility-helper/-/commit/002edf6de6972a995ee9001f49a27b32b521bf4a

diff --git a/src/ICompatibilityHelper.cpp b/src/ICompatibilityHelper.cpp
index a7c6c6e..814d83d 100644
--- a/src/ICompatibilityHelper.cpp
+++ b/src/ICompatibilityHelper.cpp
@@ -10,7 +10,11 @@
 #include <KIO/OpenUrlJob>
 #include <KLocalizedContext>
 #include <KLocalizedString>
+#include <KShell>
+#include <QDir>
+#include <QFileInfo>
 #include <QIcon>
+#include <QRegularExpression>
 
 #define DOCUMENTATION_URL QUrl(u"https://kde.org/linux/docs/more-software"_s)
 
@@ -25,9 +29,37 @@ void ICompatibilityHelper::openAppInAppStore(const QString &ref) const
     job->start();
 }
 
-void ICompatibilityHelper::openApp(const QString &ref, const QList<QUrl> &urls) const
+void ICompatibilityHelper::openApp(const QString &ref, const QList<QUrl> &urls, const QString &transientFolderAccess) const
 {
-    KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(KService::serviceByDesktopName(ref));
+    // Exported Flatpak entries spell out the path to flatpak, e.g.
+    // "/usr/bin/flatpak run --command=wine --file-forwarding org.winehq.Wine @@u %u @@".
+    static const QRegularExpression flatpakRun(u"^(?:\\S*/)?flatpak\\s+run\\s+"_s);
+    static const QRegularExpression fileForwarding(u"\\s--file-forwarding\\b|\\s@@u?(?=\\s|$)"_s);
+
+    KService::Ptr service = KService::serviceByDesktopName(ref);
+
+    if (service && !urls.isEmpty()) {
+        QString exec = service->exec();
+        const QRegularExpressionMatch match = flatpakRun.match(exec);
+
+        if (match.hasMatch()) {
+            // Document portal paths put the file in a directory of its own, so
+            // hand over the real path and open its folder instead.
+            exec.remove(fileForwarding);
+            exec.replace(u"%u"_s, u"%f"_s);
+            exec.replace(u"%U"_s, u"%F"_s);
+
+            // Amending the line keeps --branch, --arch and --command.
+            if (!transientFolderAccess.isEmpty()) {
+                exec.insert(match.capturedEnd(), u"--filesystem=%1:rw "_s.arg(KShell::quoteArg(transientFolderAccess)));
+            }
+            service->setExec(exec);
+        } else if (!transientFolderAccess.isEmpty()) {
+            qWarning() << "The desktop entry for" << ref << "does not launch a Flatpak, so its Exec line was left as-is:" << exec;
+        }
+    }
+
+    KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(service);
     job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, nullptr));
     job->setUrls(urls);
     job->start();
@@ -204,6 +236,36 @@ QString ICompatibilityHelper::compatibilityToolActionIcon() const
     return m_compatibilityToolInstaller->icon();
 }
 
+QString ICompatibilityHelper::compatibilityToolWarning() const
+{
+    if (!hasCompatibilityTool()) {
+        return QString();
+    }
+
+    // With two tools on offer the user has not picked one yet, so don't name one.
+    if (hasSecondaryCompatibilityTool()) {
+        if (isCompatibilityToolInstalled() && isSecondaryCompatibilityToolInstalled()) {
+            return i18nc("@info",
+                         "Running this file temporarily gives the compatibility tool access to everything in this folder, in addition to its existing "
+                         "permissions. The file is not checked for safety. <b>Only run files from trusted sources.</b>");
+        }
+        return i18nc("@info",
+                     "Installing a compatibility tool and running this file with it temporarily gives it access to everything in this folder, in addition to "
+                     "the permissions it comes with. The file is not checked for safety. <b>Only run files from trusted sources.</b>");
+    }
+
+    if (isCompatibilityToolInstalled()) {
+        return i18nc("@info %1 is an application name, e.g. \"Wine\"",
+                     "Running this file temporarily gives %1 access to everything in this folder, in addition to its existing permissions. The file is not "
+                     "checked for safety. <b>Only run files from trusted sources.</b>",
+                     compatibilityToolName());
+    }
+    return i18nc("@info %1 is an application name, e.g. \"Wine\"",
+                 "Installing %1 and running this file with it temporarily gives it access to everything in this folder, in addition to the permissions it "
+                 "comes with. The file is not checked for safety. <b>Only run files from trusted sources.</b>",
+                 compatibilityToolName());
+}
+
 void ICompatibilityHelper::launchCompatibilityTool() const
 {
     if (!hasCompatibilityTool()) {
@@ -215,7 +277,7 @@ void ICompatibilityHelper::launchCompatibilityTool() const
         return;
     }
     m_compatibilityToolInstaller->takeOverMimeTypes();
-    openApp(m_compatibilityToolInstaller->appId(), {m_filePath});
+    openApp(m_compatibilityToolInstaller->appId(), {m_filePath}, fileFolder());
 }
 
 void ICompatibilityHelper::compatibilityToolInstallFinished() const
@@ -224,7 +286,7 @@ void ICompatibilityHelper::compatibilityToolInstallFinished() const
         return;
     }
     m_compatibilityToolInstaller->runPostInstall();
-    openApp(m_compatibilityToolInstaller->appId(), {m_filePath});
+    openApp(m_compatibilityToolInstaller->appId(), {m_filePath}, fileFolder());
 }
 
 bool ICompatibilityHelper::hasSecondaryCompatibilityTool() const
@@ -289,7 +351,7 @@ void ICompatibilityHelper::launchSecondaryCompatibilityTool() const
     }
     // Deliberately not calling takeOverMimeTypes() here: the secondary tool is
     // the non-default choice, so it should not claim the MIME type handler.
-    openApp(m_secondaryCompatibilityToolInstaller->appId(), {m_filePath});
+    openApp(m_secondaryCompatibilityToolInstaller->appId(), {m_filePath}, fileFolder());
 }
 
 void ICompatibilityHelper::secondaryCompatibilityToolInstallFinished() const
@@ -298,5 +360,13 @@ void ICompatibilityHelper::secondaryCompatibilityToolInstallFinished() const
         return;
     }
     m_secondaryCompatibilityToolInstaller->runPostInstall();
-    openApp(m_secondaryCompatibilityToolInstaller->appId(), {m_filePath});
+    openApp(m_secondaryCompatibilityToolInstaller->appId(), {m_filePath}, fileFolder());
+}
+
+QString ICompatibilityHelper::fileFolder() const
+{
+    if (!m_filePath.isValid() || !m_filePath.isLocalFile()) {
+        return QString();
+    }
+    return QDir::cleanPath(QFileInfo(m_filePath.toLocalFile()).absolutePath());
 }
diff --git a/src/ICompatibilityHelper.h b/src/ICompatibilityHelper.h
index 947a16d..7cd6992 100644
--- a/src/ICompatibilityHelper.h
+++ b/src/ICompatibilityHelper.h
@@ -48,6 +48,9 @@ class ICompatibilityHelper : public QObject
     Q_PROPERTY(bool compatibilityToolInstalled READ compatibilityToolInstalled CONSTANT)
     // The Flatpak installer configuration for the compatibility tool.
     Q_PROPERTY(QObject *compatibilityToolInstaller READ compatibilityToolInstaller CONSTANT)
+    // Warns about the folder access the tool is given and that the file itself
+    // is unchecked. Empty means no warning is shown.
+    Q_PROPERTY(QString compatibilityToolWarning READ compatibilityToolWarning CONSTANT)
 
     // A second, optional compatibility tool offered alongside the primary one.
     // Used when a file type could be handled by either of two tools and the two
@@ -86,6 +89,7 @@ public:
     virtual QString compatibilityToolActionIcon() const;
     bool compatibilityToolInstalled() const;
     QObject *compatibilityToolInstaller() const;
+    virtual QString compatibilityToolWarning() const;
     virtual bool hasSecondaryCompatibilityTool() const;
     QString secondaryCompatibilityToolActionText() const;
     QString secondaryCompatibilityToolActionIcon() const;
@@ -148,8 +152,9 @@ protected:
     // Helper that returns the name of the default app store, e.g. "Discover" or "Bazaar".
     QString appStoreName() const;
 
-    // Helper to open an app.
-    void openApp(const QString &ref, const QList<QUrl> &urls = {}) const;
+    // Helper to open an app. A folder named by transientFolderAccess is opened
+    // for it for that launch only.
+    void openApp(const QString &ref, const QList<QUrl> &urls = {}, const QString &transientFolderAccess = {}) const;
 
     // Helper to check if an app is installed.
     bool isAppInstalled(const QString &ref) const;
@@ -160,6 +165,9 @@ protected:
     // Helper to return the distro name.
     QString distroName() const;
 
+    // The folder the opened file is in, or an empty string in --install mode.
+    QString fileFolder() const;
+
     // The file path of the executable/package being opened.
     QUrl m_filePath;
 
diff --git a/src/contents/ui/Main.qml b/src/contents/ui/Main.qml
index 7f9e3fe..2dc1cc0 100644
--- a/src/contents/ui/Main.qml
+++ b/src/contents/ui/Main.qml
@@ -78,6 +78,7 @@ Kirigami.ApplicationWindow {
     // Measures the dimensions of the install page without triggering it.
     Loader {
         id: installPageMetrics
+        active: PackageCompatibilityHelper.hasCompatibilityTool
         visible: false
         sourceComponent: installPageMetricsComponent
     }
@@ -124,6 +125,15 @@ Kirigami.ApplicationWindow {
                         wrapMode: Text.WordWrap
                         text: PackageCompatibilityHelper.description
                     }
+
+                    Kirigami.InlineMessage {
+                        visible: PackageCompatibilityHelper.compatibilityToolWarning !== "" && !PackageCompatibilityHelper.hasNativeApp
+                        Layout.fillWidth: true
+                        Layout.maximumWidth: Math.max(Kirigami.Units.gridUnit * 30, heading.implicitWidth)
+                        Layout.topMargin: Kirigami.Units.smallSpacing
+                        type: Kirigami.MessageType.Warning
+                        text: PackageCompatibilityHelper.compatibilityToolWarning
+                    }
                 }
             }
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.