From 710789b70115c043695fe515d7f2b1e8878afb99 Mon Sep 17 00:00:00 2001
From: Kenneth Chew <79120643+kthchew@users.noreply.github.com>
Date: Wed, 4 Dec 2024 01:43:28 -0500
Subject: [PATCH 1/3] Use security-scoped bookmarks to keep track of data
directory settings on macOS
This enables sandboxed apps to maintain access to user-selected items. In addition, for both sandboxed and nonsandboxed apps it can keep track of directories even if they are moved or renamed, and can remember access to directories in "sensitive" locations (such as the Documents folder or external drives).
Signed-off-by: Kenneth Chew <79120643+kthchew@users.noreply.github.com>
---
launcher/Application.cpp | 12 +-
launcher/CMakeLists.txt | 9 ++
.../macsandbox/SecurityBookmarkFileAccess.h | 83 +++++++++++
.../macsandbox/SecurityBookmarkFileAccess.mm | 138 ++++++++++++++++++
launcher/settings/SettingsObject.cpp | 114 ++++++++++++++-
launcher/settings/SettingsObject.h | 29 +++-
.../ui/widgets/MinecraftSettingsWidget.cpp | 2 +-
launcher/ui/widgets/MinecraftSettingsWidget.h | 2 +-
8 files changed, 382 insertions(+), 7 deletions(-)
create mode 100644 launcher/macsandbox/SecurityBookmarkFileAccess.h
create mode 100644 launcher/macsandbox/SecurityBookmarkFileAccess.mm
diff --git a/launcher/Application.cpp b/launcher/Application.cpp
index 2570beb3c..a532db2cf 100644
--- a/launcher/Application.cpp
+++ b/launcher/Application.cpp
@@ -709,6 +709,16 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("SkinsDir", "skins");
m_settings->registerSetting("JavaDir", "java");
+#ifdef Q_OS_MACOS
+ // Folder security-scoped bookmarks
+ m_settings->registerSetting("InstanceDirBookmark", "");
+ m_settings->registerSetting("CentralModsDirBookmark", "");
+ m_settings->registerSetting("IconsDirBookmark", "");
+ m_settings->registerSetting("DownloadsDirBookmark", "");
+ m_settings->registerSetting("SkinsDirBookmark", "");
+ m_settings->registerSetting("JavaDirBookmark", "");
+#endif
+
// Editors
m_settings->registerSetting("JsonEditor", QString());
@@ -964,7 +974,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
auto InstDirSetting = m_settings->getSetting("InstanceDir");
// instance path: check for problems with '!' in instance path and warn the user in the log
// and remember that we have to show him a dialog when the gui starts (if it does so)
- QString instDir = InstDirSetting->get().toString();
+ QString instDir = m_settings->get("InstanceDir").toString();
qInfo() << "Instance path : " << instDir;
if (FS::checkProblemticPathJava(QDir(instDir))) {
qWarning() << "Your instance path contains \'!\' and this is known to cause java problems!";
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
index 6d6e2ae7f..ecdab490e 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -1173,6 +1173,15 @@ if (NOT Apple)
)
endif()
+if (APPLE)
+ set(LAUNCHER_SOURCES
+ ${LAUNCHER_SOURCES}
+
+ macsandbox/SecurityBookmarkFileAccess.h
+ macsandbox/SecurityBookmarkFileAccess.mm
+ )
+endif()
+
if(WIN32)
set(LAUNCHER_SOURCES
console/WindowsConsole.h
diff --git a/launcher/macsandbox/SecurityBookmarkFileAccess.h b/launcher/macsandbox/SecurityBookmarkFileAccess.h
new file mode 100644
index 000000000..10d75cbd5
--- /dev/null
+++ b/launcher/macsandbox/SecurityBookmarkFileAccess.h
@@ -0,0 +1,83 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2024 Kenneth Chew <79120643+kthchew@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef FILEACCESS_H
+#define FILEACCESS_H
+
+#include
+#include
+Q_FORWARD_DECLARE_OBJC_CLASS(NSData);
+Q_FORWARD_DECLARE_OBJC_CLASS(NSURL);
+Q_FORWARD_DECLARE_OBJC_CLASS(NSString);
+Q_FORWARD_DECLARE_OBJC_CLASS(NSAutoreleasePool);
+Q_FORWARD_DECLARE_OBJC_CLASS(NSMutableDictionary);
+Q_FORWARD_DECLARE_OBJC_CLASS(NSMutableSet);
+class QString;
+class QByteArray;
+class QUrl;
+
+class SecurityBookmarkFileAccess {
+ /// The keys are bookmarks and the values are URLs.
+ NSMutableDictionary* m_bookmarks;
+ /// The keys are paths and the values are bookmarks.
+ NSMutableDictionary* m_paths;
+ /// Contains URLs that are currently being accessed.
+ NSMutableSet* m_activeURLs;
+
+ NSURL* securityScopedBookmarkToNSURL(QByteArray& bookmark, bool& isStale);
+public:
+ SecurityBookmarkFileAccess();
+ ~SecurityBookmarkFileAccess();
+
+ /// Get a security scoped bookmark from a URL.
+ ///
+ /// The URL must be accessible before calling this function. That is, call `startAccessingSecurityScopedResource()` before calling
+ /// this function. Note that this is called implicitly if the user selects the directory from a file picker.
+ /// \param url The URL to get the security scoped bookmark from.
+ /// \return A QByteArray containing the security scoped bookmark.
+ QByteArray urlToSecurityScopedBookmark(const QUrl& url);
+ /// Get a security scoped bookmark from a path.
+ ///
+ /// The path must be accessible before calling this function. That is, call `startAccessingSecurityScopedResource()` before calling
+ /// this function. Note that this is called implicitly if the user selects the directory from a file picker.
+ /// \param path The path to get the security scoped bookmark from.
+ /// \return A QByteArray containing the security scoped bookmark.
+ QByteArray pathToSecurityScopedBookmark(const QString& path);
+ /// Get a QUrl from a security scoped bookmark. If the bookmark is stale, isStale will be set to true and the bookmark will be updated.
+ ///
+ /// You must check whether the URL is valid before using it.
+ /// \param bookmark The security scoped bookmark to get the URL from.
+ /// \param isStale A boolean that will be set to true if the bookmark is stale.
+ /// \return The URL from the security scoped bookmark.
+ QUrl securityScopedBookmarkToURL(QByteArray& bookmark, bool& isStale);
+
+ /// Makes the file or directory at the path pointed to by the bookmark accessible. Unlike `startAccessingSecurityScopedResource()`, this
+ /// class ensures that only one "access" is active at a time. Calling this function again after the security-scoped resource has
+ /// already been used will do nothing, and a single call to `stopUsingSecurityScopedBookmark()` will release the resource provided that
+ /// this is the only `SecurityBookmarkFileAccess` accessing the resource.
+ ///
+ /// If the bookmark is stale, `isStale` will be set to true and the bookmark will be updated. Stored copies of the bookmark need to be
+ /// updated.
+ /// \param bookmark The security scoped bookmark to start accessing.
+ /// \param isStale A boolean that will be set to true if the bookmark is stale.
+ /// \return A boolean indicating whether the bookmark was successfully accessed.
+ bool startUsingSecurityScopedBookmark(QByteArray& bookmark, bool& isStale);
+ void stopUsingSecurityScopedBookmark(QByteArray& bookmark);
+};
+
+#endif //FILEACCESS_H
diff --git a/launcher/macsandbox/SecurityBookmarkFileAccess.mm b/launcher/macsandbox/SecurityBookmarkFileAccess.mm
new file mode 100644
index 000000000..721c3fd47
--- /dev/null
+++ b/launcher/macsandbox/SecurityBookmarkFileAccess.mm
@@ -0,0 +1,138 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2024 Kenneth Chew <79120643+kthchew@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "SecurityBookmarkFileAccess.h"
+
+#include
+#include
+#include
+
+QByteArray SecurityBookmarkFileAccess::urlToSecurityScopedBookmark(const QUrl& url)
+{
+ if (!url.isLocalFile())
+ return {};
+
+ NSError* error = nil;
+ NSURL* nsurl = [url.toNSURL() absoluteURL];
+ if ([m_paths objectForKey:[nsurl path]]) {
+ return QByteArray::fromNSData(m_paths[[nsurl path]]);
+ }
+ [m_activeURLs addObject:nsurl];
+ NSData* bookmark = [nsurl bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope
+ includingResourceValuesForKeys:nil
+ relativeToURL:nil
+ error:&error];
+ if (error) {
+ return {};
+ }
+ m_paths[[nsurl path]] = bookmark;
+ m_bookmarks[bookmark] = nsurl;
+ [m_activeURLs addObject:nsurl];
+
+ return QByteArray::fromNSData(bookmark);
+}
+
+SecurityBookmarkFileAccess::SecurityBookmarkFileAccess()
+{
+ m_bookmarks = [NSMutableDictionary new];
+ m_paths = [NSMutableDictionary new];
+ m_activeURLs = [NSMutableSet new];
+}
+
+SecurityBookmarkFileAccess::~SecurityBookmarkFileAccess()
+{
+ for (NSURL* url : m_activeURLs) {
+ [url stopAccessingSecurityScopedResource];
+ }
+ [m_bookmarks release];
+ [m_paths release];
+ [m_activeURLs release];
+}
+
+QByteArray SecurityBookmarkFileAccess::pathToSecurityScopedBookmark(const QString& path)
+{
+ return urlToSecurityScopedBookmark(QUrl::fromLocalFile(path));
+}
+
+NSURL* SecurityBookmarkFileAccess::securityScopedBookmarkToNSURL(QByteArray& bookmark, bool& isStale)
+{
+ NSError* error = nil;
+ BOOL localStale = NO;
+ NSURL* nsurl = [NSURL URLByResolvingBookmarkData:bookmark.toNSData()
+ options:NSURLBookmarkResolutionWithSecurityScope
+ relativeToURL:nil
+ bookmarkDataIsStale:&localStale
+ error:&error];
+ if (error) {
+ return nil;
+ }
+ isStale = localStale;
+ if (isStale) {
+ NSData* nsBookmark = [nsurl bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope
+ includingResourceValuesForKeys:nil
+ relativeToURL:nil
+ error:&error];
+ if (error) {
+ return nil;
+ }
+ m_paths[[nsurl path]] = nsBookmark;
+ m_bookmarks[nsBookmark] = nsurl;
+ bookmark = QByteArray::fromNSData(nsBookmark);
+ }
+
+ return nsurl;
+}
+
+QUrl SecurityBookmarkFileAccess::securityScopedBookmarkToURL(QByteArray& bookmark, bool& isStale)
+{
+ if (bookmark.isEmpty())
+ return {};
+
+ NSURL* url = securityScopedBookmarkToNSURL(bookmark, isStale);
+ if (!url)
+ return {};
+
+ return QUrl::fromNSURL(url);
+}
+
+bool SecurityBookmarkFileAccess::startUsingSecurityScopedBookmark(QByteArray& bookmark, bool& isStale)
+{
+ NSURL* url = [m_bookmarks objectForKey:bookmark.toNSData()] ? m_bookmarks[bookmark.toNSData()] : securityScopedBookmarkToNSURL(bookmark, isStale);
+ if ([m_activeURLs containsObject:url])
+ return false;
+
+ if ([url startAccessingSecurityScopedResource]) {
+ [m_activeURLs addObject:url];
+ return true;
+ }
+ return false;
+}
+
+void SecurityBookmarkFileAccess::stopUsingSecurityScopedBookmark(QByteArray& bookmark)
+{
+ if (![m_bookmarks objectForKey:bookmark.toNSData()])
+ return;
+ NSURL* url = m_bookmarks[bookmark.toNSData()];
+
+ if ([m_activeURLs containsObject:url]) {
+ [url stopAccessingSecurityScopedResource];
+ [m_activeURLs removeObject:url];
+ [m_bookmarks removeObjectForKey:bookmark.toNSData()];
+ [m_paths removeObjectForKey:[url path]];
+ }
+}
diff --git a/launcher/settings/SettingsObject.cpp b/launcher/settings/SettingsObject.cpp
index 7501d6748..8bc691f4c 100644
--- a/launcher/settings/SettingsObject.cpp
+++ b/launcher/settings/SettingsObject.cpp
@@ -20,6 +20,12 @@
#include "settings/Setting.h"
#include
+#include
+#include
+
+#ifdef Q_OS_MACOS
+#include "macsandbox/SecurityBookmarkFileAccess.h"
+#endif
SettingsObject::SettingsObject(QObject* parent) : QObject(parent) {}
@@ -78,9 +84,17 @@ std::shared_ptr SettingsObject::getSetting(const QString& id) const
return m_settings[id];
}
-QVariant SettingsObject::get(const QString& id) const
+QVariant SettingsObject::get(const QString& id)
{
auto setting = getSetting(id);
+
+#ifdef Q_OS_MACOS
+ // for macOS, use a security scoped bookmark for the paths
+ if (id.endsWith("Dir")) {
+ return { getPathFromBookmark(id) };
+ }
+#endif
+
return (setting ? setting->get() : QVariant());
}
@@ -90,11 +104,105 @@ bool SettingsObject::set(const QString& id, QVariant value)
if (!setting) {
qCritical() << QString("Error changing setting %1. Setting doesn't exist.").arg(id);
return false;
- } else {
- setting->set(value);
+ }
+
+#ifdef Q_OS_MACOS
+ // for macOS, keep a security scoped bookmark for the paths
+ if (value.userType() == QMetaType::QString && id.endsWith("Dir")) {
+ setPathWithBookmark(id, value.toString());
+ }
+#endif
+
+ setting->set(std::move(value));
+ return true;
+}
+
+#ifdef Q_OS_MACOS
+QString SettingsObject::getPathFromBookmark(const QString& id)
+{
+ auto setting = getSetting(id);
+ if (!setting) {
+ qCritical() << QString("Error changing setting %1. Setting doesn't exist.").arg(id);
+ return "";
+ }
+
+ // there is no need to use bookmarks if the default value is used or the directory is within the data directory (already can access)
+ if (setting->get() == setting->defValue() || QDir(setting->get().toString()).absolutePath().startsWith(QDir::current().absolutePath())) {
+ return setting->get().toString();
+ }
+
+ auto bookmarkId = id + "Bookmark";
+ auto bookmarkSetting = getSetting(bookmarkId);
+ if (!bookmarkSetting) {
+ qCritical() << QString("Error changing setting %1. Bookmark setting doesn't exist.").arg(id);
+ return "";
+ }
+
+ QByteArray bookmark = bookmarkSetting->get().toByteArray();
+ if (bookmark.isEmpty()) {
+ qDebug() << "Creating bookmark for" << id << "at" << setting->get().toString();
+ setPathWithBookmark(id, setting->get().toString());
+ return setting->get().toString();
+ }
+ bool stale;
+ QUrl url = m_sandboxedFileAccess.securityScopedBookmarkToURL(bookmark, stale);
+ if (url.isValid()) {
+ if (stale) {
+ setting->set(url.path());
+ bookmarkSetting->set(bookmark);
+ }
+
+ m_sandboxedFileAccess.startUsingSecurityScopedBookmark(bookmark, stale);
+ // already did a stale check, no need to do it again
+
+ // convert to relative path to current directory if `url` is a descendant of the current directory
+ QDir currentDir = QDir::current().absolutePath();
+ return url.path().startsWith(currentDir.absolutePath()) ? currentDir.relativeFilePath(url.path()) : url.path();
+ }
+
+ return setting->get().toString();
+}
+
+bool SettingsObject::setPathWithBookmark(const QString& id, const QString& path)
+{
+ auto setting = getSetting(id);
+ if (!setting) {
+ qCritical() << QString("Error changing setting %1. Setting doesn't exist.").arg(id);
+ return false;
+ }
+
+ QDir dir(path);
+ if (!dir.exists()) {
+ qCritical() << QString("Error changing setting %1. Path doesn't exist.").arg(id);
+ return false;
+ }
+ QString absolutePath = dir.absolutePath();
+ QString bookmarkId = id + "Bookmark";
+ std::shared_ptr bookmarkSetting = getSetting(bookmarkId);
+ // there is no need to use bookmarks if the default value is used or the directory is within the data directory (already can access)
+ if (path == setting->defValue().toString() || absolutePath.startsWith(QDir::current().absolutePath())) {
+ bookmarkSetting->reset();
return true;
}
+ QByteArray bytes = m_sandboxedFileAccess.pathToSecurityScopedBookmark(absolutePath);
+ if (bytes.isEmpty()) {
+ qCritical() << QString("Failed to create bookmark for %1 - no access?").arg(id);
+ // TODO: show an alert to the user asking them to reselect the directory
+ return false;
+ }
+ auto oldBookmark = bookmarkSetting->get().toByteArray();
+ m_sandboxedFileAccess.stopUsingSecurityScopedBookmark(oldBookmark);
+ if (!bytes.isEmpty() && bookmarkSetting) {
+ bookmarkSetting->set(bytes);
+ bool stale;
+ m_sandboxedFileAccess.startUsingSecurityScopedBookmark(bytes, stale);
+ // just created the bookmark, it shouldn't be stale
+ }
+
+ setting->set(path);
+ return true;
}
+#endif
void SettingsObject::reset(const QString& id) const
{
diff --git a/launcher/settings/SettingsObject.h b/launcher/settings/SettingsObject.h
index bd3f71b36..abd6c29c5 100644
--- a/launcher/settings/SettingsObject.h
+++ b/launcher/settings/SettingsObject.h
@@ -23,6 +23,10 @@
#include
#include
+#ifdef Q_OS_MACOS
+#include "macsandbox/SecurityBookmarkFileAccess.h"
+#endif
+
class Setting;
class SettingsObject;
@@ -119,7 +123,27 @@ class SettingsObject : public QObject {
* \return The setting's value as a QVariant.
* If no setting with the given ID exists, returns an invalid QVariant.
*/
- QVariant get(const QString& id) const;
+ QVariant get(const QString& id);
+
+#ifdef Q_OS_MACOS
+ /*!
+ * \brief Get the path to the file or directory represented by the bookmark stored in the associated setting.
+ * \param id The setting ID of the relevant directory - this should not include "Bookmark" at the end.
+ * \return A path to the file or directory represented by the bookmark.
+ * If a bookmark is not valid or stored, use default logic (directly return the stored path).
+ * This can attempt to create a bookmark if the path is accessible and the bookmark is not valid.
+ */
+ QString getPathFromBookmark(const QString& id);
+ /*!
+ * \brief Set a security-scoped bookmark to the provided path for the associated setting.
+ * \param id The setting ID of the relevant directory - this should not include "Bookmark" at the end.
+ * \param path The new desired path.
+ * \return A boolean indicating whether a bookmark was successfully set.
+ * The path needs to be accessible to the launcher before calling this function. For example,
+ * it could come from a user selection in an open panel.
+ */
+ bool setPathWithBookmark(const QString& id, const QString& path);
+#endif
/*!
* \brief Sets the value of the setting with the given ID.
@@ -207,6 +231,9 @@ class SettingsObject : public QObject {
private:
QMap> m_settings;
+#ifdef Q_OS_MACOS
+ SecurityBookmarkFileAccess m_sandboxedFileAccess;
+#endif
protected:
bool m_suspendSave = false;
diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.cpp b/launcher/ui/widgets/MinecraftSettingsWidget.cpp
index b738cab74..e57233b90 100644
--- a/launcher/ui/widgets/MinecraftSettingsWidget.cpp
+++ b/launcher/ui/widgets/MinecraftSettingsWidget.cpp
@@ -488,7 +488,7 @@ void MinecraftSettingsWidget::openGlobalSettings()
APPLICATION->ShowGlobalSettings(this, "minecraft-settings");
}
-void MinecraftSettingsWidget::updateAccountsMenu(const SettingsObject& settings)
+void MinecraftSettingsWidget::updateAccountsMenu(SettingsObject& settings)
{
m_ui->instanceAccountSelector->clear();
auto accounts = APPLICATION->accounts();
diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.h b/launcher/ui/widgets/MinecraftSettingsWidget.h
index 9e3e7afb2..0dd8e6ba7 100644
--- a/launcher/ui/widgets/MinecraftSettingsWidget.h
+++ b/launcher/ui/widgets/MinecraftSettingsWidget.h
@@ -54,7 +54,7 @@ class MinecraftSettingsWidget : public QWidget {
private:
void openGlobalSettings();
- void updateAccountsMenu(const SettingsObject& settings);
+ void updateAccountsMenu(SettingsObject& settings);
bool isQuickPlaySupported();
private slots:
void saveSelectedLoaders();
From efe1d71d35ebfdd210521a3c2a67acc464ddcf61 Mon Sep 17 00:00:00 2001
From: Kenneth Chew <79120643+kthchew@users.noreply.github.com>
Date: Sun, 15 Dec 2024 01:02:50 -0500
Subject: [PATCH 2/3] Generate security-scoped bookmarks on startup
Signed-off-by: Kenneth Chew <79120643+kthchew@users.noreply.github.com>
---
launcher/Application.cpp | 15 +++++++++++++++
launcher/CMakeLists.txt | 4 ++--
2 files changed, 17 insertions(+), 2 deletions(-)
diff --git a/launcher/Application.cpp b/launcher/Application.cpp
index a532db2cf..90f5a45cb 100644
--- a/launcher/Application.cpp
+++ b/launcher/Application.cpp
@@ -969,6 +969,21 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
// Themes
m_themeManager = std::make_unique();
+#ifdef Q_OS_MACOS
+ // for macOS: getting directory settings will generate URL security-scoped bookmarks if needed and not present
+ // this facilitates a smooth transition from a non-sandboxed version of the launcher, that likely can access the directory,
+ // and a sandboxed version that can't access the directory without a bookmark
+ // this section can likely be removed once the sandboxed version has been released for a while and migrations aren't done anymore
+ {
+ m_settings->get("InstanceDir");
+ m_settings->get("CentralModsDir");
+ m_settings->get("IconsDir");
+ m_settings->get("DownloadsDir");
+ m_settings->get("SkinsDir");
+ m_settings->get("JavaDir");
+ }
+#endif
+
// initialize and load all instances
{
auto InstDirSetting = m_settings->getSetting("InstanceDir");
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
index ecdab490e..b5ba3fe55 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -1177,8 +1177,8 @@ if (APPLE)
set(LAUNCHER_SOURCES
${LAUNCHER_SOURCES}
- macsandbox/SecurityBookmarkFileAccess.h
- macsandbox/SecurityBookmarkFileAccess.mm
+ macsandbox/SecurityBookmarkFileAccess.h
+ macsandbox/SecurityBookmarkFileAccess.mm
)
endif()
From d937563eadf431ac0ad0dcac97624abba4c716d4 Mon Sep 17 00:00:00 2001
From: Kenneth Chew <79120643+kthchew@users.noreply.github.com>
Date: Thu, 10 Apr 2025 17:25:59 -0400
Subject: [PATCH 3/3] Pick out additional fixes, functionality to
`SecurityBookmarkFileAccess`
Match the state of this class with the current state of the macOS sandbox PR
Signed-off-by: Kenneth Chew <79120643+kthchew@users.noreply.github.com>
---
.../macsandbox/SecurityBookmarkFileAccess.h | 8 ++-
.../macsandbox/SecurityBookmarkFileAccess.mm | 70 ++++++++++++++-----
2 files changed, 59 insertions(+), 19 deletions(-)
diff --git a/launcher/macsandbox/SecurityBookmarkFileAccess.h b/launcher/macsandbox/SecurityBookmarkFileAccess.h
index 10d75cbd5..5bddf0e31 100644
--- a/launcher/macsandbox/SecurityBookmarkFileAccess.h
+++ b/launcher/macsandbox/SecurityBookmarkFileAccess.h
@@ -39,9 +39,12 @@ class SecurityBookmarkFileAccess {
/// Contains URLs that are currently being accessed.
NSMutableSet* m_activeURLs;
+ bool m_readOnly;
+
NSURL* securityScopedBookmarkToNSURL(QByteArray& bookmark, bool& isStale);
public:
- SecurityBookmarkFileAccess();
+ /// \param readOnly A boolean indicating whether the bookmark should be read-only.
+ SecurityBookmarkFileAccess(bool readOnly = false);
~SecurityBookmarkFileAccess();
/// Get a security scoped bookmark from a URL.
@@ -78,6 +81,9 @@ public:
/// \return A boolean indicating whether the bookmark was successfully accessed.
bool startUsingSecurityScopedBookmark(QByteArray& bookmark, bool& isStale);
void stopUsingSecurityScopedBookmark(QByteArray& bookmark);
+
+ /// Returns true if access to the `path` is currently being maintained by this object.
+ bool isAccessingPath(const QString& path);
};
#endif //FILEACCESS_H
diff --git a/launcher/macsandbox/SecurityBookmarkFileAccess.mm b/launcher/macsandbox/SecurityBookmarkFileAccess.mm
index 721c3fd47..bee854abe 100644
--- a/launcher/macsandbox/SecurityBookmarkFileAccess.mm
+++ b/launcher/macsandbox/SecurityBookmarkFileAccess.mm
@@ -29,25 +29,41 @@ QByteArray SecurityBookmarkFileAccess::urlToSecurityScopedBookmark(const QUrl& u
NSError* error = nil;
NSURL* nsurl = [url.toNSURL() absoluteURL];
+ NSData* bookmark;
if ([m_paths objectForKey:[nsurl path]]) {
- return QByteArray::fromNSData(m_paths[[nsurl path]]);
+ bookmark = m_paths[[nsurl path]];
+ } else {
+ bookmark = [nsurl bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope |
+ (m_readOnly ? NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess : 0)
+ includingResourceValuesForKeys:nil
+ relativeToURL:nil
+ error:&error];
}
- [m_activeURLs addObject:nsurl];
- NSData* bookmark = [nsurl bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope
- includingResourceValuesForKeys:nil
- relativeToURL:nil
- error:&error];
if (error) {
return {};
}
+
+ // remove/reapply access to ensure that write access is immediately cut off for read-only bookmarks
+ // sometimes you need to call this twice to actually stop access (extra calls aren't harmful)
+ [nsurl stopAccessingSecurityScopedResource];
+ [nsurl stopAccessingSecurityScopedResource];
+ nsurl = [NSURL URLByResolvingBookmarkData:bookmark
+ options:NSURLBookmarkResolutionWithSecurityScope |
+ (m_readOnly ? NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess : 0)
+ relativeToURL:nil
+ bookmarkDataIsStale:nil
+ error:&error];
m_paths[[nsurl path]] = bookmark;
m_bookmarks[bookmark] = nsurl;
- [m_activeURLs addObject:nsurl];
- return QByteArray::fromNSData(bookmark);
+ QByteArray qBookmark = QByteArray::fromNSData(bookmark);
+ bool isStale = false;
+ startUsingSecurityScopedBookmark(qBookmark, isStale);
+
+ return qBookmark;
}
-SecurityBookmarkFileAccess::SecurityBookmarkFileAccess()
+SecurityBookmarkFileAccess::SecurityBookmarkFileAccess(bool readOnly) : m_readOnly(readOnly)
{
m_bookmarks = [NSMutableDictionary new];
m_paths = [NSMutableDictionary new];
@@ -59,9 +75,6 @@ SecurityBookmarkFileAccess::~SecurityBookmarkFileAccess()
for (NSURL* url : m_activeURLs) {
[url stopAccessingSecurityScopedResource];
}
- [m_bookmarks release];
- [m_paths release];
- [m_activeURLs release];
}
QByteArray SecurityBookmarkFileAccess::pathToSecurityScopedBookmark(const QString& path)
@@ -74,7 +87,8 @@ NSURL* SecurityBookmarkFileAccess::securityScopedBookmarkToNSURL(QByteArray& boo
NSError* error = nil;
BOOL localStale = NO;
NSURL* nsurl = [NSURL URLByResolvingBookmarkData:bookmark.toNSData()
- options:NSURLBookmarkResolutionWithSecurityScope
+ options:NSURLBookmarkResolutionWithSecurityScope |
+ (m_readOnly ? NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess : 0)
relativeToURL:nil
bookmarkDataIsStale:&localStale
error:&error];
@@ -83,18 +97,21 @@ NSURL* SecurityBookmarkFileAccess::securityScopedBookmarkToNSURL(QByteArray& boo
}
isStale = localStale;
if (isStale) {
- NSData* nsBookmark = [nsurl bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope
+ NSData* nsBookmark = [nsurl bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope |
+ (m_readOnly ? NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess : 0)
includingResourceValuesForKeys:nil
relativeToURL:nil
error:&error];
if (error) {
return nil;
}
- m_paths[[nsurl path]] = nsBookmark;
- m_bookmarks[nsBookmark] = nsurl;
bookmark = QByteArray::fromNSData(nsBookmark);
}
+ NSData* nsBookmark = bookmark.toNSData();
+ m_paths[[nsurl path]] = nsBookmark;
+ m_bookmarks[nsBookmark] = nsurl;
+
return nsurl;
}
@@ -112,10 +129,12 @@ QUrl SecurityBookmarkFileAccess::securityScopedBookmarkToURL(QByteArray& bookmar
bool SecurityBookmarkFileAccess::startUsingSecurityScopedBookmark(QByteArray& bookmark, bool& isStale)
{
- NSURL* url = [m_bookmarks objectForKey:bookmark.toNSData()] ? m_bookmarks[bookmark.toNSData()] : securityScopedBookmarkToNSURL(bookmark, isStale);
+ NSURL* url = [m_bookmarks objectForKey:bookmark.toNSData()] ? m_bookmarks[bookmark.toNSData()]
+ : securityScopedBookmarkToNSURL(bookmark, isStale);
if ([m_activeURLs containsObject:url])
return false;
+ [url stopAccessingSecurityScopedResource];
if ([url startAccessingSecurityScopedResource]) {
[m_activeURLs addObject:url];
return true;
@@ -131,8 +150,23 @@ void SecurityBookmarkFileAccess::stopUsingSecurityScopedBookmark(QByteArray& boo
if ([m_activeURLs containsObject:url]) {
[url stopAccessingSecurityScopedResource];
+ [url stopAccessingSecurityScopedResource];
+
[m_activeURLs removeObject:url];
- [m_bookmarks removeObjectForKey:bookmark.toNSData()];
[m_paths removeObjectForKey:[url path]];
+ [m_bookmarks removeObjectForKey:bookmark.toNSData()];
}
}
+
+bool SecurityBookmarkFileAccess::isAccessingPath(const QString& path)
+{
+ NSData* bookmark = [m_paths objectForKey:path.toNSString()];
+ if (!bookmark && path.endsWith('/')) {
+ bookmark = [m_paths objectForKey:path.left(path.length() - 1).toNSString()];
+ }
+ if (!bookmark) {
+ return false;
+ }
+ NSURL* url = [m_bookmarks objectForKey:bookmark];
+ return [m_activeURLs containsObject:url];
+}