feat(live-update): support file:// URIs in downloadBundle (Android + iOS)#846
Open
cevcode wants to merge 3 commits intocapawesome-team:mainfrom
Open
feat(live-update): support file:// URIs in downloadBundle (Android + iOS)#846cevcode wants to merge 3 commits intocapawesome-team:mainfrom
cevcode wants to merge 3 commits intocapawesome-team:mainfrom
Conversation
…iOS)
Adds `file://` URL handling to `downloadBundle({ url, bundleId })` on
both platforms. Detects the scheme at the top of `downloadBundleOfTypeZip`
and dispatches to a new private helper that copies the local file to the
plugin's temporary directory, runs the existing checksum/signature
verification, and registers the bundle through the unchanged
`addBundleOfTypeZip` path. The HTTPS/OkHttp/Alamofire branch is
untouched.
Why
---
Sideloaded content delivery: an MDM-managed device receives a bundle
zip on its filesystem (e.g. via Android Enterprise managed
configuration) and needs to register it through the existing plugin
API without re-introducing a separate ingestion path. With file://
support, the consumer only ever calls `LiveUpdate.downloadBundle({ url,
bundleId })` regardless of source.
What changed
------------
- `LiveUpdate.java` / `LiveUpdate.swift`: scheme guard at the top of
`downloadBundleOfTypeZip` + new private
`copyFromFileSchemeAndAddBundle` method that reuses the existing
`verifyFile` + `addBundleOfTypeZip` + cleanup pipeline.
- New `LiveUpdateFileScheme.java` (Android) and
`Classes/LiveUpdateFileScheme.swift` (iOS): pure-Java / pure-Swift
static helper for the byte-copy + progress reporting. No platform
Context dependency, so unit tests run on the JVM (no Robolectric)
and via XCTest without instantiating the full plugin.
- JUnit (3 cases) and XCTest (3 cases) covering: identical-bytes copy
+ final progress event, FileNotFound when source missing, null/nil
progress callback accepted.
Tests
-----
- Android: `./gradlew clean build test` → 3/3 LiveUpdateFileSchemeTest
passes (new test class), existing tests unchanged.
- iOS: `xcodebuild build` succeeds with the new helper. The existing
`PluginTests/LiveUpdateTests.swift` `testEcho` is broken on `main`
(`LiveUpdate()` is missing required `config:` and `plugin:`
arguments since the public init signature changed) — that's
pre-existing and out of scope here. The new
`LiveUpdateFileSchemeTests.swift` compiles cleanly on its own.
API surface
-----------
No public TS API change. `downloadBundle({ url, bundleId })` already
accepts a free-form string; this adds a recognised scheme. If the
maintainers prefer an explicit method (`addBundleFromFile`) over the
overload, happy to refactor — both platforms + tests would be a small
follow-up commit.
Downstream
----------
Already shipped as a `yarn patch` over the npm release in our
consumer repo (validated runtime on Android emulator and iOS
simulator: file:// happy path + missing-file + checksum-mismatch +
HTTPS regression all behave correctly).
Source fixes: iOS — LiveUpdateFileScheme.swift: - Move output.write inside the withUnsafeBufferPointer closure so the pointer never escapes its valid scope (fixes Swift undefined behaviour) - Add resolveFileUrl(_:allowedPrefixes:) — rejects non-file schemes via isFileURL, malformed URIs, and any path outside the caller-supplied sandbox prefixes after standardizedFileURL resolution - Make all FileSchemeError.errorDescription strings generic (no path) - Add invalidFileUri and sourceOutsideSandbox cases so the helper owns the full file-scheme error vocabulary iOS — LiveUpdate.swift: - Log entry to the file:// branch and the error in the catch via CAPLog - Throw FileSchemeError.invalidFileUri (not CustomError.downloadFailed) when the URI is rejected - Replace `try? FileManager.default.removeItem(...)` with a removeTemporaryFile() helper that logs cleanup failures - Add sandboxPrefixes() helper that collects documents/library/caches/ applicationSupport and the temp dir, and pass it to resolveFileUrl Android — LiveUpdateFileScheme.java: - Add resolveSandboxedFile(uri, allowedCanonicalPrefixes...) — parses the URI, asserts file scheme, canonicalises the candidate, and rejects paths that escape the supplied prefixes. Pure JVM (no Context) so it is unit-testable without Robolectric/Mockito - Drop the absolute path from FileNotFoundException message Android — LiveUpdate.java: - Logger.debug entry log on the file:// branch and Logger.error in catch - Route through resolveSandboxedFile, passing canonical paths for filesDir, cacheDir, and noBackupFilesDir - Replace bare destination.delete() with a removeTemporaryFile() helper that warns when the delete returns false Tests: - Android: 9 new pure-JVM tests covering scheme rejection, sandbox enforcement, parent-traversal, prefix collision, and the error-message generic-string contract - iOS: 7 new XCTests covering the same surface against LiveUpdateFileScheme.resolveFileUrl - All 12 Android tests pass locally (gradle :testDebugUnitTest).
Per review feedback — for the file:// branch the source is already on local disk, so the buffered loop with per-chunk progress events was overkill. Reduce to a single copy call. Android — LiveUpdate.java: - Replace LiveUpdateFileScheme.copyAndReportProgress(...) call with java.nio.file.Files.copy(source, destination, REPLACE_EXISTING). - Drop the DownloadBundleProgressEvent emission for the file:// branch (the HTTPS path's progress events are unchanged). Android — LiveUpdateFileScheme.java: - Drop ProgressListener interface and copyAndReportProgress method. - Keep resolveSandboxedFile (still required for path validation). iOS — LiveUpdate.swift: - Replace LiveUpdateFileScheme.copyAndReportProgress(...) call with FileManager.default.copyItem(at:to:), preceded by a removeItem when the destination already exists. iOS — LiveUpdateFileScheme.swift: - Drop ProgressCallback typealias and copyAndReportProgress method. - Drop sourceNotFound, streamOpenFailed, streamReadFailed, streamWriteFailed FileSchemeError cases (no longer raised by the helper — Files.copy/copyItem throw their own IO errors). - Keep resolveFileUrl (still required for path validation). Tests: - Android: drop the three copyAndReportProgress tests; keep the eight resolveSandboxedFile tests (8/8 passing locally). - iOS: drop the four copyAndReportProgress XCTests; keep the six resolveFileUrl XCTests.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Sideloaded content delivery: a managed device (e.g. Android Enterprise via MDM, iOS via offline package install) receives a bundle zip on its filesystem and needs to register it through the existing plugin API without introducing a parallel ingestion path. With
file://support, the consumer only ever callsLiveUpdate.downloadBundle({ url, bundleId })regardless of source — same code path for HTTPS in production andfile://in offline/sideload scenarios.What changed
LiveUpdate.java/LiveUpdate.swift: scheme guard at the top ofdownloadBundleOfTypeZip. Ifurlstarts withfile://, dispatches to a new privatecopyFromFileSchemeAndAddBundlemethod. The new method copies the source to the existing temporary-zip path, runs the unchangedverifyFile+addBundleOfTypeZippipeline, and cleans up on both success and error. The HTTPS branch (OkHttp on Android, Alamofire on iOS) is untouched.LiveUpdateFileScheme.java(Android): new pure-Java static helper for byte-copy with progress callback. NoContextdependency — JVM unit tests run without Robolectric.Classes/LiveUpdateFileScheme.swift(iOS): same idea in Swift.enum LiveUpdateFileSchemewith a staticcopyAndReportProgressmethod. No platformContextdependency.Tests
Android — JUnit 4 (3 cases, run via
./gradlew test)copyAndReportProgress_copiesBytesIdenticallycopied == total == source.lengthcopyAndReportProgress_throwsWhenSourceMissingFileNotFoundExceptionwhen source absentcopyAndReportProgress_acceptsNullListenernullprogress callback`./gradlew clean build test` → all 3 pass on my machine. Output: `tests="3" skipped="0" failures="0" errors="0"`.
iOS — XCTest (3 cases, mirror of Android)
testCopyAndReportProgress_copiesBytesIdenticallytestCopyAndReportProgress_throwsWhenSourceMissingFileSchemeError.sourceNotFoundtestCopyAndReportProgress_acceptsNilProgressnilprogress callbackxcodebuild -workspace Plugin.xcworkspace -scheme Plugin build→ succeeds with the new helper.xcodebuild test: the existingPluginTests/LiveUpdateTests.swift::testEchodoesn't compile onmain—LiveUpdate()requiresconfig:andplugin:arguments since the publicinitsignature changed at some point. That's pre-existing and out of scope here, but it does block running the full test target. The newLiveUpdateFileSchemeTests.swiftcompiles cleanly when isolated. Happy to fix thetestEchoregression in a follow-up if useful.API design
No public TS API change.
downloadBundle({ url, bundleId })already accepts a free-form string; this just adds a recognised scheme that's dispatched at the top of the platform implementation. If you'd prefer an explicit method (e.g.addBundleFromFile) instead of the scheme-overload, happy to refactor — both platforms + tests would be a small follow-up.