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>
This commit is contained in:
Kenneth Chew
2024-12-04 01:43:28 -05:00
parent 7e8cf628e8
commit 710789b701
8 changed files with 382 additions and 7 deletions

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
#ifndef FILEACCESS_H
#define FILEACCESS_H
#include <QtCore/QMap>
#include <QtCore/QSet>
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

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
#include "SecurityBookmarkFileAccess.h"
#include <Foundation/Foundation.h>
#include <QByteArray>
#include <QUrl>
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]];
}
}