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