From 14ef83f06a6a95ec35ba49d669d598aff4d974e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 7 Mar 2026 15:03:58 +0100 Subject: [PATCH] Implement iOS "bookmark" support, to preserve permissions to recent files from iCloud --- Core/Loaders.cpp | 10 ++ Core/Util/DarwinFileSystemServices.h | 5 +- Core/Util/DarwinFileSystemServices.mm | 136 +++++++++++++++++++++++++- Core/Util/PathUtil.cpp | 23 +++-- Core/Util/RecentFiles.cpp | 1 + UI/MainScreen.cpp | 4 +- UI/NativeApp.cpp | 4 + 7 files changed, 173 insertions(+), 10 deletions(-) diff --git a/Core/Loaders.cpp b/Core/Loaders.cpp index c7f28b6567..a67bee12d7 100644 --- a/Core/Loaders.cpp +++ b/Core/Loaders.cpp @@ -30,6 +30,7 @@ #include "Core/FileSystems/MetaFileSystem.h" #include "Core/FileSystems/BlockDevices.h" #include "Core/FileSystems/ISOFileSystem.h" +#include "Core/Util/DarwinFileSystemServices.h" #include "Core/PSPLoaders.h" #include "Core/MemMap.h" #include "Core/Loaders.h" @@ -57,6 +58,15 @@ FileLoader *ConstructFileLoader(const Path &filename) { } return new CachingFileLoader(baseLoader); } + #if PPSSPP_PLATFORM(IOS) + if (!File::Exists(filename)) { + // Try to "unlock" the path before the file loader hits it + Path newFilename = DarwinFileSystemServices::reauthorizeBookmarkByPath(filename); + if (!newFilename.empty()) { + return new LocalFileLoader(newFilename); + } + } + #endif return new LocalFileLoader(filename); } diff --git a/Core/Util/DarwinFileSystemServices.h b/Core/Util/DarwinFileSystemServices.h index 5c5102f71a..85acc99c03 100644 --- a/Core/Util/DarwinFileSystemServices.h +++ b/Core/Util/DarwinFileSystemServices.h @@ -31,8 +31,11 @@ public: static Path appropriateMemoryStickDirectoryToUse(); static void setUserPreferredMemoryStickDirectory(Path); static Path defaultMemoryStickPath(); - static void ClearDelegate(); + + static Path reauthorizeBookmarkByPath(const Path &pathStr); + static void stopAccessingPath(const Path &pathStr); + static void terminate(); private: #if PPSSPP_PLATFORM(IOS) // iOS only, needed for UIDocumentPickerViewController diff --git a/Core/Util/DarwinFileSystemServices.mm b/Core/Util/DarwinFileSystemServices.mm index 2d49e5ba38..ddc8952750 100644 --- a/Core/Util/DarwinFileSystemServices.mm +++ b/Core/Util/DarwinFileSystemServices.mm @@ -14,6 +14,7 @@ #include #include +#include #include "ppsspp_config.h" @@ -36,6 +37,8 @@ @property DarwinDirectoryPanelCallback panelCallback; @end +static std::map activeAccessTokens; + void *DarwinFileSystemServices::__pickerDelegate = nullptr; void DarwinFileSystemServices::ClearDelegate() { @@ -52,6 +55,8 @@ void DarwinFileSystemServices::ClearDelegate() { return self; } +static void addBookmark(NSURL *url); + - (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray *)urls { NSURL *url = urls.firstObject; if (!url) { @@ -59,6 +64,8 @@ void DarwinFileSystemServices::ClearDelegate() { return; } + addBookmark(url); + // 1. Start accessing the resource BOOL success = [url startAccessingSecurityScopedResource]; if (success) { @@ -82,9 +89,136 @@ void DarwinFileSystemServices::ClearDelegate() { INFO_LOG(Log::System, "Picker cancelled, pre-emptively hide keyboard"); [sharedViewController hideKeyboard]; } - @end +// Internal helper to generate and store a bookmark for a given URL. +// Also activates the security scope for the current session. +static void addBookmark(NSURL *url) { + if (!url) return; + + // IMPORTANT: You must start accessing BEFORE creating the bookmark, + // otherwise the OS denies the 'read' required to seal the bookmark. + BOOL accessStarted = [url startAccessingSecurityScopedResource]; + + INFO_LOG(Log::System, "Darwin: Creating bookmark (Access: %s) for: %s", + accessStarted ? "YES" : "NO", url.path.UTF8String); + + NSError *error = nil; + #if PPSSPP_PLATFORM(IOS) + NSURLBookmarkCreationOptions options = 0; + #else + NSURLBookmarkCreationOptions options = NSURLBookmarkCreationWithSecurityScope; + #endif + + NSData *bookmark = [url bookmarkDataWithOptions:options + includingResourceValuesForKeys:nil + relativeToURL:nil + error:&error]; + + if (bookmark) { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSMutableDictionary *dict = [[defaults dictionaryForKey:@"SecureBookmarks"] mutableCopy] ?: [NSMutableDictionary dictionary]; + + [dict setObject:bookmark forKey:url.path]; + [defaults setObject:dict forKey:@"SecureBookmarks"]; + [defaults synchronize]; + + // If we started access specifically for this function, we keep the token. + // If we didn't have it in our map yet, store this URL. + std::string pathKey = url.path.UTF8String; + if (accessStarted && activeAccessTokens.find(pathKey) == activeAccessTokens.end()) { + activeAccessTokens[pathKey] = (void *)CFBridgingRetain(url); + } else if (accessStarted) { + // If it's already in the map, we don't need a second retain. + [url stopAccessingSecurityScopedResource]; + } + } else { + ERROR_LOG(Log::System, "Darwin: Bookmark failed. Error 260 often means the file isn't 'coordinated' yet. Detail: %s", + error.localizedDescription.UTF8String); + + if (accessStarted) [url stopAccessingSecurityScopedResource]; + } +} +Path DarwinFileSystemServices::reauthorizeBookmarkByPath(const Path &pathStr) { + INFO_LOG(Log::System, "Darwin: Reauthorizing [%s]", pathStr.c_str()); + + // 1. Return immediately if already active + if (activeAccessTokens.count(pathStr.ToString())) { + return pathStr; + } + + // 2. Retrieve bookmark from disk + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSDictionary *dict = [defaults dictionaryForKey:@"SecureBookmarks"]; + NSData *bookmarkData = dict[@(pathStr.c_str())]; + + if (!bookmarkData) { + WARN_LOG(Log::System, "Darwin: No bookmark data found for path (%s)", pathStr.c_str()); + return Path(); + } + + // 3. Resolve + BOOL isStale = NO; + NSError *error = nil; +#if PPSSPP_PLATFORM(IOS) + NSURLBookmarkResolutionOptions resOptions = 0; +#else + NSURLBookmarkResolutionOptions resOptions = NSURLBookmarkResolutionWithSecurityScope; +#endif + + NSURL *url = [NSURL URLByResolvingBookmarkData:bookmarkData + options:resOptions + relativeToURL:nil + bookmarkDataIsStale:&isStale + error:&error]; + + if (url && [url startAccessingSecurityScopedResource]) { + Path resolvedPath(url.path.UTF8String); + + // 4. Handle stale/moved files + if (isStale || resolvedPath != pathStr) { + INFO_LOG(Log::System, "Darwin: File moved! Updating [%s] -> [%s]", pathStr.c_str(), resolvedPath.c_str()); + + // Clean up the old key from defaults + NSMutableDictionary *mutableDict = [dict mutableCopy]; + [mutableDict removeObjectForKey:@(pathStr.c_str())]; + [defaults setObject:mutableDict forKey:@"SecureBookmarks"]; + + // Re-save under the new path + addBookmark(url); + } else { + // Path is the same, just cache the token + activeAccessTokens[resolvedPath.ToString()] = (void *)CFBridgingRetain(url); + } + + return resolvedPath; + } + + ERROR_LOG(Log::System, "Darwin: Resolution failed for %s", pathStr.c_str()); + return Path(); +} + +void DarwinFileSystemServices::stopAccessingPath(const Path &pathStr) { + auto it = activeAccessTokens.find(pathStr.ToString()); + if (it != activeAccessTokens.end()) { + INFO_LOG(Log::System, "Darwin: Stopping access for: %s", pathStr.c_str()); + + NSURL *url = (__bridge_transfer NSURL *)it->second; + [url stopAccessingSecurityScopedResource]; + + activeAccessTokens.erase(it); + } +} + +void DarwinFileSystemServices::terminate() { + INFO_LOG(Log::System, "Darwin: Terminating all security-scoped access."); + for (auto const& [path, token] : activeAccessTokens) { + NSURL *url = (__bridge_transfer NSURL *)token; + [url stopAccessingSecurityScopedResource]; + } + activeAccessTokens.clear(); +} + #else #include #endif // __has_include() diff --git a/Core/Util/PathUtil.cpp b/Core/Util/PathUtil.cpp index a4e790a599..9124da2277 100644 --- a/Core/Util/PathUtil.cpp +++ b/Core/Util/PathUtil.cpp @@ -6,6 +6,7 @@ #include "Common/System/System.h" #include "Common/Log.h" #include "Core/Util/PathUtil.h" +#include "Core/Util/DarwinFileSystemServices.h" #include "Core/Config.h" #include "Common/VR/PPSSPPVR.h" @@ -169,14 +170,24 @@ bool TryUpdateSavedPath(Path *path) { std::string memstick = g_Config.memStickDirectory.ToString(); size_t memstickDocumentsPos = memstick.find("/Documents"); // Note: No trailing slash, or we won't find it. *path = Path(memstick.substr(0, memstickDocumentsPos) + pathStr.substr(documentsPos)); - return true; - } else { - // Path can't be auto-updated. - return false; } -#else - return false; + + if (File::Exists(*path)) { + return true; + } + + // Still doesn't exist? Maybe got de-authorized + // Try to "unlock" the path before the file loader hits it + Path newFilename = DarwinFileSystemServices::reauthorizeBookmarkByPath(*path); + if (!newFilename.empty()) { + INFO_LOG(Log::UI, "Bookmark rename: %s -> %s", path->c_str(), newFilename.c_str()); + *path = newFilename; + return true; + } + + // Path can't be auto-updated. #endif + return false; } Path GetFailedBackendsDir() { diff --git a/Core/Util/RecentFiles.cpp b/Core/Util/RecentFiles.cpp index 1f96626e8f..339b125eff 100644 --- a/Core/Util/RecentFiles.cpp +++ b/Core/Util/RecentFiles.cpp @@ -10,6 +10,7 @@ #include "Core/Loaders.h" #include "Core/Util/RecentFiles.h" #include "Core/Util/PathUtil.h" +#include "Core/Util/DarwinFileSystemServices.h" #include "Core/Config.h" RecentFilesManager g_recentFiles; diff --git a/UI/MainScreen.cpp b/UI/MainScreen.cpp index 9e908f6d9a..387c97007f 100644 --- a/UI/MainScreen.cpp +++ b/UI/MainScreen.cpp @@ -1455,11 +1455,11 @@ void MainScreen::CreateViews() { auto gr = GetI18NCategory(I18NCat::GRAPHICS); Button *fullscreenButton = logo->Add(new Button("", ImageID(), new AnchorLayoutParams(48, 48, NONE, 0, 0, NONE, Centering::None))); fullscreenButton->SetIgnoreText(true); - fullscreenButton->OnClick.Add([this](UI::EventParams &e) { + fullscreenButton->OnClick.Add([](UI::EventParams &e) { g_Config.bFullScreen = !g_Config.bFullScreen; System_ApplyFullscreenState(); }); - fullscreenButton->SetImageIDFunc([this]() { + fullscreenButton->SetImageIDFunc([]() { return g_Config.bFullScreen ? ImageID("I_RESTORE") : ImageID("I_FULLSCREEN"); }); #endif diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index d5d64d0483..f324b2511e 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -930,6 +930,10 @@ void NativeShutdownGraphics() { } #endif +#if PPSSPP_PLATFORM(IOS) + DarwinFileSystemServices::terminate(); +#endif + if (g_audioBackend) { delete g_audioBackend; g_audioBackend = nullptr;