From 44d21b6975e6a6d6183819d6a622360088b364a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sun, 8 Nov 2026 15:08:01 +0100 Subject: [PATCH 1/3] Android storage: Don't treat a failed content URI open as a handle Opening a content URI now fails with a StorageError (-2 for a file that isn't there) rather than always -1, and DirectoryFileHandle::Open only checked for -1. So opening a missing file on the memory stick succeeded, with fd -2. Seeking it fails, and on 32-bit ReadEntireFile took that as a 4 GB file and the resize threw, which is how saving new savedata crashed. ReadEntireFile now also catches a failed seek on 32-bit, where it isn't negative as an s64, and sizes the vector can't hold. Co-Authored-By: Claude Opus 5.5 --- Core/FileSystems/DirectoryFileSystem.cpp | 3 ++- Core/FileSystems/MetaFileSystem.cpp | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Core/FileSystems/DirectoryFileSystem.cpp b/Core/FileSystems/DirectoryFileSystem.cpp index 989407da1c..2ccb34770a 100644 --- a/Core/FileSystems/DirectoryFileSystem.cpp +++ b/Core/FileSystems/DirectoryFileSystem.cpp @@ -282,7 +282,8 @@ bool DirectoryFileHandle::Open(const Path &basePath, std::string &fileName, File inGameDir_ = true; } hFile = fd; - if (fd != -1) { + // Content URI opens fail with any negative StorageError, not just -1. + if (fd >= 0) { // Success return true; } else { diff --git a/Core/FileSystems/MetaFileSystem.cpp b/Core/FileSystems/MetaFileSystem.cpp index 37bc651953..f04149cee4 100644 --- a/Core/FileSystems/MetaFileSystem.cpp +++ b/Core/FileSystems/MetaFileSystem.cpp @@ -626,9 +626,10 @@ int MetaFileSystem::ReadEntireFile(const std::string &filename, std::vector return handle; SeekFile(handle, 0, FILEMOVE_END); - const s64 fileSize = GetSeekPos(handle); + // A failed seek is (size_t)-1, which an s64 only sees as negative on 64-bit. + const size_t fileSize = GetSeekPos(handle); SeekFile(handle, 0, FILEMOVE_BEGIN); - if (fileSize < 0 || (u64)fileSize > maxSize) { + if (fileSize == (size_t)-1 || fileSize > maxSize || fileSize > data.max_size()) { CloseFile(handle); return SCE_KERNEL_ERROR_ERROR; } From c2612c143b122c70b3e0b7aea19eb851740f7135 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sun, 8 Nov 2026 17:09:15 +0100 Subject: [PATCH 2/3] Android storage: CreateDir succeeds when the folder is already there Creating a taken name on a content URI now fails with ALREADY_EXISTS (instead of making "name (1)"), so CreateDir failed whenever its existence check missed the folder, by a race or a failed query. It promises true for a folder that's already there. Co-Authored-By: Claude Opus 5.5 --- Common/File/FileUtil.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Common/File/FileUtil.cpp b/Common/File/FileUtil.cpp index 9736ac161f..745291b05b 100644 --- a/Common/File/FileUtil.cpp +++ b/Common/File/FileUtil.cpp @@ -640,8 +640,8 @@ bool CreateDir(const Path &path, bool quiet) { break; // OK case PathType::CONTENT_URI: { - // NOTE: The Android storage API will simply create a renamed directory (append a number) if it already exists. - // We want to avoid that, so let's just return true if the directory already is there. + // Given a taken name, the storage API creates "name (1)", which the Java side then deletes again. + // Checking first saves that. if (File::Exists(path)) { return true; } @@ -654,7 +654,9 @@ bool CreateDir(const Path &path, bool quiet) { if (!quiet) { INFO_LOG(Log::IO, "Calling Android_CreateDirectory(%s, %s)", uri.ToString().c_str(), newDirName.c_str()); } - return Android_CreateDirectory(uri.ToString(), newDirName) == StorageError::SUCCESS; + // ALREADY_EXISTS if it showed up since the check, or the check failed. + const StorageError created = Android_CreateDirectory(uri.ToString(), newDirName); + return created == StorageError::SUCCESS || created == StorageError::ALREADY_EXISTS; } else { // Bad path - can't create this directory. if (!quiet) { From be50e764718f82b248ae2624c435b0a95960fd58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sun, 8 Nov 2026 18:09:21 +0100 Subject: [PATCH 3/3] Android storage: Open content URIs through the cached provider client Like the queries. Opening a file through the content resolver takes and drops a stable reference to the provider for each call, and is a few ms slower: opening for a read or an overwrite went from about 25 and 12 ms to 22 and 10.5 ms in the storage test, on a phone's SD card. If the provider has died, it falls back to the resolver. Co-Authored-By: Claude Opus 5.5 --- android/src/org/ppsspp/ppsspp/ContentUri.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/android/src/org/ppsspp/ppsspp/ContentUri.java b/android/src/org/ppsspp/ppsspp/ContentUri.java index 855e65bc11..c3ce3cbff9 100644 --- a/android/src/org/ppsspp/ppsspp/ContentUri.java +++ b/android/src/org/ppsspp/ppsspp/ContentUri.java @@ -175,12 +175,27 @@ public class ContentUri { return context.getContentResolver().query(uri, projection, null, null, null); } + // Like query. The resolver's own open is slower: it takes a stable reference to the provider for each + // call, and drops it after. + private static ParcelFileDescriptor openFile(Context context, Uri uri, String mode) throws Exception { + final ContentProviderClient c = getClient(context, uri); + if (c != null) { + try { + return c.openFile(uri, mode); + } catch (android.os.DeadObjectException e) { + Log.w(TAG, "Storage provider died, acquiring it again"); + dropClient(uri.getAuthority(), c); + } + } + return context.getContentResolver().openFileDescriptor(uri, mode); + } + @Keep @SuppressWarnings("unused") public static int openContentUri(Activity activity, String uriString, String mode) { try { Uri uri = Uri.parse(uriString); - try (ParcelFileDescriptor filePfd = activity.getContentResolver().openFileDescriptor(uri, mode)) { + try (ParcelFileDescriptor filePfd = openFile(activity, uri, mode)) { if (filePfd == null) { // I'd expect an exception to happen before we get here, so this is probably // never reached.