Bug Report
Capacitor Version
Capacitor iOS 8.x; the proposed fix was verified with CocoaPods and Swift Package Manager.
Plugin Version
@capgo/capacitor-updater 8.51.15; CLI 8.47.1. Reproducing lookup code is also present on current main (a0f85588).
Context
Automatic signed v2 delta updates (autoUpdate: 'atInstall', publicKey configured). Brotli is enabled during a --delta-only upload with --key-data-v2.
Platform
iOS; occurs in Debug and Release. Validated the fix on a physical iPhone 17 Pro Max running iOS 26.6 and on the iOS simulator.
Current Behavior
For a compressed manifest entry such as assets/app.js.br, downloadManifest removes .br for the destination but looks for builtin assets/app.js.br. The native bundle contains the decoded assets/app.js, so the builtin reuse check cannot find it and downloads an otherwise identical file.
The optional getMissingBundleFiles path has the same filename mismatch.
This is a path issue, not a checksum-format incompatibility: CLI 8.47.1 hashes/signs the uncompressed bytes, then applies Brotli and AES. iOS already decrypts, decompresses, and compares the decoded file hash correctly when it downloads the entry.
Expected Behavior
An unchanged compressed manifest entry should reuse the corresponding uncompressed builtin asset after verifying its hash. A mismatching builtin must still be downloaded and checked normally.
Reproduction
- Put a compressible file at
public/assets/app.js in the native app.
- Upload a signed delta with Brotli enabled and identical
assets/app.js content. Confirm the manifest entry is named assets/app.js.br.
- Run the automatic update on a fresh install with an empty OTA cache.
- Observe a download instead of builtin reuse.
--disable-brotli avoids the filename mismatch, but sacrifices the compression benefit.
A native unit reproduction can write a builtin file into an overridden builtinFolderURL, provide its hash under a .br manifest filename, and use an invalid download URL. Successful assembly must then come from verified builtin reuse, without network access.
Proposed Fix
Use the existing resolveManifestTargetPath helper for the builtin lookups as well. It removes one transport .br suffix and retains directory-boundary/path validation. Keep SHA-256 checks unchanged.
Validation
Two regression tests using temporary synthetic builtin files fail against unmodified upstream: the missing-files query reports the .br entry missing, and signed manifest assembly attempts the deliberately invalid download URL. With the path fix, both pass. The signed test uses the existing upstream RSA contract fixture for an empty file.
The full 17-test cache/reuse/path suite passes with the fix, including a modified-content checksum check and rejection of a path outside builtin. Android's builtin resolver already strips .br and does not share this specific lookup mismatch. This fix requires a native plugin release; an OTA cannot patch already-installed Swift code.
Bug Report
Capacitor Version
Capacitor iOS 8.x; the proposed fix was verified with CocoaPods and Swift Package Manager.
Plugin Version
@capgo/capacitor-updater8.51.15; CLI 8.47.1. Reproducing lookup code is also present on current main (a0f85588).Context
Automatic signed v2 delta updates (
autoUpdate: 'atInstall',publicKeyconfigured). Brotli is enabled during a--delta-onlyupload with--key-data-v2.Platform
iOS; occurs in Debug and Release. Validated the fix on a physical iPhone 17 Pro Max running iOS 26.6 and on the iOS simulator.
Current Behavior
For a compressed manifest entry such as
assets/app.js.br,downloadManifestremoves.brfor the destination but looks for builtinassets/app.js.br. The native bundle contains the decodedassets/app.js, so the builtin reuse check cannot find it and downloads an otherwise identical file.The optional
getMissingBundleFilespath has the same filename mismatch.This is a path issue, not a checksum-format incompatibility: CLI 8.47.1 hashes/signs the uncompressed bytes, then applies Brotli and AES. iOS already decrypts, decompresses, and compares the decoded file hash correctly when it downloads the entry.
Expected Behavior
An unchanged compressed manifest entry should reuse the corresponding uncompressed builtin asset after verifying its hash. A mismatching builtin must still be downloaded and checked normally.
Reproduction
public/assets/app.jsin the native app.assets/app.jscontent. Confirm the manifest entry is namedassets/app.js.br.--disable-brotliavoids the filename mismatch, but sacrifices the compression benefit.A native unit reproduction can write a builtin file into an overridden
builtinFolderURL, provide its hash under a.brmanifest filename, and use an invalid download URL. Successful assembly must then come from verified builtin reuse, without network access.Proposed Fix
Use the existing
resolveManifestTargetPathhelper for the builtin lookups as well. It removes one transport.brsuffix and retains directory-boundary/path validation. Keep SHA-256 checks unchanged.Validation
Two regression tests using temporary synthetic builtin files fail against unmodified upstream: the missing-files query reports the
.brentry missing, and signed manifest assembly attempts the deliberately invalid download URL. With the path fix, both pass. The signed test uses the existing upstream RSA contract fixture for an empty file.The full 17-test cache/reuse/path suite passes with the fix, including a modified-content checksum check and rejection of a path outside builtin. Android's builtin resolver already strips
.brand does not share this specific lookup mismatch. This fix requires a native plugin release; an OTA cannot patch already-installed Swift code.