From f81f0447cc8d5ce6b88cc72aede4b7324428a542 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Fri, 9 Oct 2026 13:20:34 -0300 Subject: [PATCH] fix(fs): read uncompressed Android assets from the cache copy (#3635) In plugins/fs/android/src/main/java/FsPlugin.kt getFileDescriptor, for uncompressed assets AssetManager.openFd returns an AssetFileDescriptor whose parcel descriptor is the whole APK; its startOffset/length were dropped and Rust read from offset 0, i.e. APK bytes. Only compressed assets (openFd throws) took the cache copy path. All assets now use the cache copy path. The cache file path is also checked to stay below cacheDir/_assets. Kotlin not compiled here: unverified, needs an on-device check. --- .../fix-fs-android-uncompressed-assets.md | 6 ++++ plugins/fs/android/src/main/java/FsPlugin.kt | 30 ++++++++++--------- 2 files changed, 22 insertions(+), 14 deletions(-) create mode 100644 .changes/fix-fs-android-uncompressed-assets.md diff --git a/.changes/fix-fs-android-uncompressed-assets.md b/.changes/fix-fs-android-uncompressed-assets.md new file mode 100644 index 000000000..f12f2d572 --- /dev/null +++ b/.changes/fix-fs-android-uncompressed-assets.md @@ -0,0 +1,6 @@ +--- +fs: patch +fs-js: patch +--- + +Fixed reading uncompressed Android assets (`$RESOURCE` files such as images or audio that are stored uncompressed in the APK) returning the bytes of the APK instead of the asset. Assets are now always read from a copy in the app cache, like compressed ones. diff --git a/plugins/fs/android/src/main/java/FsPlugin.kt b/plugins/fs/android/src/main/java/FsPlugin.kt index 877fbf4a6..f565ebf53 100644 --- a/plugins/fs/android/src/main/java/FsPlugin.kt +++ b/plugins/fs/android/src/main/java/FsPlugin.kt @@ -44,21 +44,23 @@ class FsPlugin(private val activity: Activity): Plugin(activity) { if (args.uri.startsWith(app.tauri.TAURI_ASSETS_DIRECTORY_URI)) { val path = args.uri.substring(app.tauri.TAURI_ASSETS_DIRECTORY_URI.length) - try { - val fd = activity.assets.openFd(path).parcelFileDescriptor?.detachFd() - res.put("fd", fd) - } catch (e: IOException) { - // if the asset is compressed, we cannot open a file descriptor directly - // so we copy it to the cache and get a fd from there - // this is a lot faster than serializing the file and sending it as invoke response - // because on the Rust side we can leverage the custom protocol IPC and read the file directly - val cacheFile = File(activity.cacheDir, "_assets/$path") - cacheFile.parentFile?.mkdirs() - copyAsset(path, cacheFile) - - val fd = ParcelFileDescriptor.open(cacheFile, ParcelFileDescriptor.parseMode(args.mode)).detachFd() - res.put("fd", fd) + // We cannot hand out a file descriptor of the asset itself: for uncompressed assets + // `AssetManager.openFd` returns the descriptor of the whole APK with a start offset + // the Rust side cannot honor, and compressed assets have no descriptor at all. + // So we copy the asset to the cache and get a fd from there. + // This is a lot faster than serializing the file and sending it as invoke response + // because on the Rust side we can leverage the custom protocol IPC and read the file directly. + val assetsCacheDir = File(activity.cacheDir, "_assets") + val cacheFile = File(assetsCacheDir, path) + if (!cacheFile.canonicalPath.startsWith(assetsCacheDir.canonicalPath + File.separator)) { + invoke.reject("invalid asset path: $path") + return } + cacheFile.parentFile?.mkdirs() + copyAsset(path, cacheFile) + + val fd = ParcelFileDescriptor.open(cacheFile, ParcelFileDescriptor.parseMode(args.mode)).detachFd() + res.put("fd", fd) } else { val fd = activity.contentResolver.openAssetFileDescriptor( Uri.parse(args.uri),