Skip to content

feat: OCDS v1.1 conformance — concurrent downloader (iOS + Android) + standard#70

Merged
huhuanming merged 24 commits into
mainfrom
feat/ocds-download-conformance
Jun 24, 2026
Merged

feat: OCDS v1.1 conformance — concurrent downloader (iOS + Android) + standard#70
huhuanming merged 24 commits into
mainfrom
feat/ocds-download-conformance

Conversation

@huhuanming

@huhuanming huhuanming commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Defines the OneKey Concurrent Download Standard (OCDS v1.1) and brings the iOS + Android concurrent (multi-range) downloaders into conformance.

Standard

  • native-modules/react-native-range-downloader/SPEC.md — normative, platform-agnostic standard: segment-file data model, permanent-vs-transient failure classification, resume / retry / integrity / finalization / fallback / termination, trust boundary, conformance scenarios.

iOS (range-downloader core + bundle-update caller)

  • Explicit typed permanent/transient failure class (no side-effect inference), forwarded across the Nitro JS bridge (RangeDownloadOutcome + RangeFallbackKind + tunable params)
  • 429/5xx keep .segN — no data loss; in-place per-segment retry + stall watchdog + jittered backoff + Retry-After
  • Per-hop HTTPS redirect guard; multipart/total rejection; per-destination single-flight; monotonic progress floor
  • 416 transient with mandatory size re-evaluation

Android (concurrent core + bundle-update caller)

  • Wire CancelHandle from the bundle caller (cancel-then-delete)
  • Exhaustive HTTP-status classification (416 transient); per-segment backoff; multipart/total rejection; fsync before rename; per-destination single-flight

Validation

  • Compile-verified: iOS full-workspace xcodebuild BUILD SUCCEEDED; Android gradle compileDebugKotlin BUILD SUCCESSFUL (both modules)
  • All four platform classifiers verified consistent against the spec (416 transient everywhere)

Follow-ups (not in this PR)

  • JVM classifier unit test (Android module has no test dir; JS side covered)
  • Android shutdownNow + awaitTermination (small resurrection window)
  • Device stress testing (lock/background/network/throttle) — compilation ≠ runtime validation, especially the iOS per-segment retry state machine

Pairs with the app-monorepo PR (desktop + shared orchestration).

Companion PR: app-monorepo #12121 — desktop/Node downloader + shared AppUpdate orchestration.


✅ Verification record

How every platform's downloader is verified against the standard, and how each
test was proven load-bearing (a green test that survives its own regression
proves nothing). Full methodology + runnable harness:
react-native-range-downloader/conformance/.

Approach

flowchart TD
    SPEC["OCDS SPEC v1.2<br/>§6 — 11 conformance scenarios"]
    SPEC --> AND["Android<br/>pure-JVM + OkHttp MockWebServer<br/>real fault injection"]
    SPEC --> NODE["Node / Desktop<br/>jest + real local HTTP/HTTPS server"]
    SPEC --> IOSU["iOS · unit<br/>SwiftPM over the real RangeDownloadLogic"]
    SPEC --> IOSE["iOS · end-to-end<br/>real Release .app on Simulator<br/>+ local fault server (silent auto-download)"]
    AND  --> MUT["Mutation verification<br/>inject the regression each test targets<br/>→ the test MUST go red"]
    NODE --> MUT
    IOSE --> EVID["evidence: server log + app-latest.log<br/>+ on-disk .segN + final SHA"]
    EVID --> ADV["adversarial verify<br/>(default FAIL unless 3 sources agree)"]
    MUT  --> OK(["load-bearing ✔"])
    ADV  --> OK
Loading

SPEC §6 coverage matrix

# Scenario iOS (sim) Android Node
1 transient error → backoff retry, no restart
2 killed mid-run → resume from completed segments ✅¹ ✅¹
3 200 to a Range → permanent → single-stream
4 416 / 5xx transient (re-evaluate, retry)
5 short / over-long / mis-aligned / bad-total 206
6 corrupted assembly → terminal (v1.2: no retry-once)
7 flap / background → progress never resets to 0 ◑²
8 user cancel mid-run ⚪³
9 persistent failure → bounded give-up caller⁴
10 stalled socket → stall watchdog
11 two concurrent download() → single-flight ⚪³

¹ resume tested via a persisted-manifest proxy — a true SIGKILL-mid-write isn't reproducible in-process; covered by the manifest-resume + intra-call-resume tests.
² Android progress: the clamp/guard math is unit-tested, but the monotonic CAS primitive stays inline in the JNI adapter (not unit-testable).
³ iOS #8/#11 need a temp shim (the iOS app has no cancel trigger at all — #8 would test dead code); both behaviors are fully covered on Android + Node.
⁴ Node #9 give-up budget lives in the shared kit caller (useAppUpdate.test.ts), not the downloader.

Test counts & strength

Platform Tests Mutation-proven Driver
Android 64 9 OkHttp MockWebServer (real sockets, real .segN, real SHA)
Node / Desktop 18 3 real local HTTP/HTTPS + positioned writes + .progress.json
iOS · unit 30 n/a SwiftPM, source-symlinked to the shipping logic
iOS · e2e 9/11 §6 adversarial real Release .app on a booted Simulator

Mutants planted & killed (load-bearing proof)

flowchart LR
    subgraph Android
      a1["503 → permanent"] --> ka(["🔴 caught"])
      a2["416 → permanent"] --> ka
      a3["bypass multipart"] --> ka
      a4["drop total check"] --> ka
      a5["501 → transient"] --> ka
      a6["retry budget +2"] --> ka
      a7["registry identity-drop"] --> ka
      a8["progress clamp removed"] --> ka
      a9["seg-glob narrowed"] --> ka
    end
    subgraph Node
      n1["416 → permanent"] --> kn(["🔴 caught"])
      n2["break single-stream 416"] --> kn
      n3["isConcurrentFallback→false"] --> kn
    end
Loading

Every mutant produced a real test failure (not a compile error) and was reverted.

Honesty / scope

  • iOS e2e is a fake update task running through real download code. The app's own native downloader, its silent-auto-download trigger, segmentation, resume, retry, fallback and SHA-256 are all real; the update server, manifest, version and the bundle payload are synthetic (filler bytes, not a signed zip). It verifies the download layer only — the post-download unzip → signature-verify → install chain is out of OCDS scope.
  • SPEC §6 fix: fix EXC_BAD_ACCESS in Skeleton on iOS #6 previously mandated a single-stream retry-once on a checksum mismatch. All three reference implementations terminate on mismatch instead (re-fetching the same object would re-corrupt), so v1.2 removed it#6 is now conformant everywhere.
  • The behavior-preserving extractions (Android RangeDownloadLogic.kt, iOS RangeDownloadLogic.swift) are pure moves — proven by the pre-existing real-path suites staying green.

🔩 Android APK updater — audit + fixes

The Android APK download path (react-native-app-update) shares the OCDS core, but its ~1346-line caller was never audited and had zero tests. An adversarial audit found 4 issues.

flowchart LR
  Q["'is the APK update missing changes?'"] --> AUDIT["adversarial OCDS audit<br/>of react-native-app-update"]
  AUDIT --> B1["#1 cancel race (HIGH)"] --> FIX["✅ fixed"]
  AUDIT --> B2["#2a/#2b spurious update/error"] --> FIX
  AUDIT --> B34["#3/#4 single-flight · promote-before-verify"] --> BL["📋 backlog (documented, mitigated)"]
  AUDIT --> COV["AppUpdateLogic extraction<br/>17 JVM tests · 10 mutants killed"]
Loading

Issues

Bug Severity Status What
#1 cancel-then-delete race (OCDS §5.8) HIGH ✅ fixed (213acf0f3) concurrent download had NO cancelHandleclearCache deleted .segN while 8 workers were still writing. Wired CancelHandle + cancel-before-delete (mirrors the bundle caller).
#2a spurious update/error on cancel MED ✅ fixed (24d5973cb) suppress update/error when the run was intentionally cancelled.
#2b spurious update/error when ASC offline MED ✅ fixed (011c24a58) a deferred (offline) signature check was mis-reported as a download error on every retry; suppressed (the Promise still rejects → JS still retries).
#3 process-global single-flight / #4 promote-before-verify MED / LOW 📋 backlog documented; doesn't corrupt, mitigated.

Coverage (b9caf71f9)

Extracted the dependency-free pure logic into AppUpdateLogic.kt (segment names + count, 416/206 Content-Range parse, completeness/alignment predicates); the adapter now delegates (pure move). Added junit + 17 JVM unit tests, with all 10 planted mutants killed (mutation-proven). Also wired a previously-untested inline 206-parse duplicate through the helper.

Honesty / scope

Still device/Robolectric-only: the JNI download loop, the actual .segN deletion + §5.8 cancel-race execution, and the promote/verify/install File-I/O are not exercised by these JVM tests.

Normative, platform-agnostic standard for the concurrent multi-range
downloader (iOS/Android/Desktop). Defines the segment-file data model,
permanent-vs-transient failure classification, and the resume / retry /
integrity / finalization / fallback / termination requirements, plus a
trust boundary and conformance scenarios. README links to it.
iOS (range-downloader core + bundle-update caller):
- explicit typed permanent/transient failure class (no side-effect inference)
- 429/5xx keep .segN — no data loss; in-place per-segment retry + stall
  watchdog + jittered backoff + Retry-After
- per-hop HTTPS redirect guard; multipart/byteranges + total rejection
- per-destination single-flight; monotonic progress floor
- 416 transient with mandatory size re-evaluation

Android (concurrent core + bundle-update caller):
- wire CancelHandle from the bundle caller (cancel-then-delete)
- exhaustive HTTP-status classification (416 transient); per-segment backoff
- multipart/byteranges + total rejection; fsync before rename
- per-destination single-flight

All four platform classifiers verified consistent against OCDS v1.1.
NOTE: nitrogen codegen regen still required to forward the typed outcome and
tunable params across the JS bridge; iOS/Android need device/build validation.
…bridge

nitrogen regenerated RangeDownloadOutcome (completed|fallbackTransient|
fallbackPermanent), new RangeFallbackKind, RangeDownloadResult.fallbackKind,
and RangeDownloadParams.{maxSegmentAttempts,requestTimeoutSeconds,
stallTimeoutSeconds,overallDeadlineSeconds}. Shims now map the in-process
typed failure class onto the real wire cases (no legacy collapse) and forward
the tunable params JS->native. Removes the 'codegen regen required' deferral.

Compile-validated: iOS full-workspace xcodebuild BUILD SUCCEEDED; Android
gradle compileDebugKotlin BUILD SUCCESSFUL (both modules).
…§5.8)

CancelHandle.cancel() called shutdownNow() (interrupt-only) and returned
immediately; a worker blocked in a native write() could straggle past the
caller's subsequent .segN delete and resurrect it. Add a bounded
awaitTermination(3s) so cancel-then-delete is race-free.

Verified: gradle compileDebugKotlin BUILD SUCCESSFUL.
Extract the pure status->permanent/transient decision into a module-visible
isPermanentHttpStatus(code) (was buried inside classifyHttpFailure behind an
okhttp Response), add junit test infra to the module, and add a matrix test:
401/403/404/410/501/505/other-4xx/unknown -> permanent vs 408/429/416/other-5xx
-> transient. Keeps the Kotlin classifier in lockstep with the JS taxonomy and
guards the 416-must-be-transient regression.

Verified: gradle testDebugUnitTest — 4 tests, 0 failures.
…r (MockWebServer)

A Range-aware OkHttp MockWebServer FaultDispatcher harness drives the REAL
ConcurrentRangeDownloader.download() over a real socket and asserts on recorded
ranges + on-disk .segN/.partial + assembled SHA-256:
  T1 drop-segment→only-missing refetch · T2 full + resume(seeded segs) ·
  T3 200→FALLBACK+wipe · T4 429/Retry-After→retry-in-place ·
  T5 misaligned/over-long/short 206 → reject/clamp no corruption ·
  T8 cancel→stop+artifacts · T10 stall→read-timeout→transient retry.
T6 (whole-file SHA) and T11 (per-dest single-flight) are caller-level (n/a to the
core); T9 (cross-restart budget) is the shared-JS layer. Adds mockwebserver
testImplementation. 16/16 green via gradle testDebugUnitTest.
…ct pure logic)

Extracts the deterministic, dependency-free logic (planRanges, classifyStatus,
parseContentRange{Bounds,Total}, parseRetryAfterSeconds, backoffDelay,
calculateSHA256 + the RangeDownloadClass/RangeFallbackClass enums) verbatim from
ReactNativeRangeDownloader.swift into ios/RangeDownloadLogic.swift (Foundation +
CommonCrypto only). Pure move — bodies byte-identical; the module delegates via
RangeDownloadLogic.<fn> and keeps the Nitro wire projections as extensions.

A SwiftPM test package (tests/swiftpm, source symlinked to the production file so
no copy can drift) runs 30 XCTests via `swift test`, covering OCDS §4
classification (incl. 416-transient / 501,505-permanent guard), §5.5 Content-Range
parsing, §5.2/§5.3 range planning, §5.4 Retry-After + jittered backoff, and SHA-256.

DEVICE-ONLY (documented in the test header, not faked): the concurrent BACKGROUND
URLSession download + stash/concat (T1/T2), resume across suspend/kill (T8), and
handleEventsForBackgroundURLSession delivery (T10) — they need a real device + the
app-latest.log checklist. Module compile verified (example pod build SUCCEEDED;
swiftc -parse clean). 30/30 green.
…ignore them

The earlier 'git add tests' swept tests/swiftpm/.build/ (2298 ModuleCache
.pcm binaries) into the commit. Remove from tracking and ignore the SwiftPM
build dir; the test sources (Package.swift, symlinked source, tests) stay.
…OS) + iOS sim harness

Adds conformance/ documenting how each platform is verified against the OCDS
SPEC and housing the runnable code:
- conformance/README.md: per-platform method/code/how-to-run table, the latest
  iOS on-simulator §6 result (9/11 pass, #6 deviation, #8/#11 need a shim), and
  a 're-test after a code change' guide.
- conformance/ios-simulator/: the end-to-end harness (HTTP fault server, simctl
  driver, per-scenario capture, multi-agent verify workflow) + a README with the
  temporary local-verify patches needed to drive a Release .app from localhost.
Android JVM tests (android/src/test) and iOS SwiftPM tests (tests/swiftpm)
already live in this module and are referenced; the Node e2e suite lives in the
app-monorepo desktop module and is pointed to.

Action item recorded: confirm whether iOS #6 (no single-stream retry-once after
a corrupted concurrent assembly) is intentional.
…sim harness

Make explicit what is real (the app's native downloader, the silent auto-download
trigger, segmentation/resume/retry/fallback/SHA-256) vs synthetic (the localhost
update server, manifest, version, and the 16MB filler payload), and that the
post-download unzip/signature-verify/install chain is NOT exercised (the
synthetic payload fails verifyASC after a successful download — expected, out of
OCDS scope), plus how to extend coverage with a real signed test bundle.
A whole-file checksum/signature mismatch after assembly is now simply Permanent
-> discard + terminal failure (no automatic re-download of the same object).
Re-fetching the same CDN object would re-corrupt, so the retry added cost with
no integrity benefit; all three reference implementations (iOS/Android/Node)
already terminate on mismatch, confirming it was over-specified.

- SPEC.md: bump to 1.2; rewrite the §4 failure-table row + §6 scenario 6; changelog.
- conformance/README.md: #6 is now PASS (conformant), not a deviation; resolve the action item.
- ios-simulator workflow: judge #6 as conformant under 1.2 (terminating without single-stream retry is expected).
…total/read-only-fs)

5 new JVM tests (9 cases) driving the real ConcurrentRangeDownloader via the
MockWebServer RangeFaultServer, arming the previously-dead fault modes:
- OcdsTransient5xxTest: 503 transient retry-in-place + budget-exhaustion-throws + 501-permanent-bypass (#4)
- Ocds416ResumeTest: 416-on-resume is transient, never discards seeded segments (#4/#6)
- OcdsMultipartRejectTest: multipart/byteranges body -> FALLBACK + wipe (#5)
- OcdsBadTotalRejectTest: disagreeing Content-Range total -> FALLBACK + wipe (#5)
- OcdsReadOnlyFsTest: read-only/near-full FS -> clean fail, no corrupt artifact, idempotent re-concat (the segment-file design's raison d'etre, §5.2)
Full module suite green (25 tests). 1/9 mutation-proven so far (503->permanent);
the rest are green characterization guards pending mutation verification.
Adapter layer (single-flight/progress-CAS/cancel) stays uncovered — needs Robolectric.
OcdsBadTotalRejectTest referenced RangeFaultServer.FaultMode.BAD_TOTAL_206 but
the mode was never added (the coverage workflow's harness step didn't persist to
source), so the test suite did not compile. Add the enum value + a dispatcher
branch that serves the correct window/body with a Content-Range total that
disagrees with the probe total, so the core's total-mismatch check is exercised.
Full module unit suite now compiles and passes.
…verification

- Android: 9 new tests (5xx/416/multipart/bad-total/read-only-fs), 6 mutation-proven
  load-bearing; 2 (never-discards, read-only-fs) green but only break under
  insertion-type regressions; adapter layer needs Robolectric.
- Node: 3 new tests (416 concurrent+single-stream, concurrent->single-stream
  handoff), all 3 mutation-proven.
- Document why true cross-restart resume (#2) and #9 give-up budget are out of the
  downloader test layer, and fix the Android run command (example-host gradle).
…ng extraction

The Nitro adapter (HybridReactNativeRangeDownloaderSpec / JNI) can't be JVM-unit-
tested directly. Extract its dependency-free pieces verbatim into a new
RangeDownloadLogic.kt (same package/source set, no build.gradle change) and have
the adapter DELEGATE — a pure move (adapter diff +11/-21, all delegation):
- RunRegistry: the channel|taskId single-flight registry (identity-checked finish preserved)
- progressPercent: the total>0 clamp/guard math feeding the (unchanged) CAS gate
- sweepPartialArtifacts + runKey: the .segN/.partial artifact sweep
39 new pure-JVM tests (RangeDownloadLogic/RunRegistry/MonotonicProgressGate/
SegmentArtifactSweep); full module suite green at 64 tests. 3 mutants planted +
killed (sweep glob-narrow, registry identity-drop, progressPercent clamp-removal).
No download/network/file behavior changed — proven by the pre-existing OCDS/HTTP/
Smoke suite (which drives the real paths) staying green.
… extraction

The adapter's single-flight / progress-clamp / artifact-sweep logic was extracted
to RangeDownloadLogic.kt and unit-tested (39 tests, 3 mutation-proven). Update the
coverage doc: the adapter layer is no longer 'needs Robolectric' — only the CAS
primitive + JNI/Promise boundary remain genuinely not JVM-unit-testable.
…ght workers (OCDS §5.8)

The APK concurrent download invoked ConcurrentRangeDownloader.download(url, path)
with NO cancelHandle, so clearCache/clearApkCache deleted the .segN files while
the 8 worker threads were still streaming into them — the cancel-then-delete
resurrection race OCDS §5.8 forbids (a live worker re-creates a just-deleted
segment / writes to a deleted FD → corruption / EROFS). The JS-bundle caller
fixed this during OCDS; the APK caller never adopted the machinery.

Mirror the bundle wiring: create a CancelHandle, store it in activeDownload,
pass it to .download(...), clear it in the finally, and cancel()
(shutdownNow + awaitTermination) at the top of wipeApkCacheFiles — the single
sweep both external cleanup paths (clearCache / clearApkCache) call — BEFORE any
.segN is deleted. wipeApkCacheFiles is never invoked mid-download, so this only
cancels a genuinely-orphaned run. Compiles clean.

Found by auditing the APK path against OCDS (it shares the conformant core but
its ~1346-line caller was never audited and has zero tests).
…nally cancelled

Completes the prior cancel-wiring fix: now that clearCache/clearApkCache abort the
in-flight core workers, that abort surfaces in downloadAPK's catch as an exception.
Without suppression it emitted a spurious update/error for an intentional cancel.
Hoist cancelHandle so the catch can detect cancelHandle.aborted and skip update/error
on an intentional cancel (mirrors react-native-bundle-update's intentionallyCancelled
guard). Compiles clean.
…rely deferred (offline)

OCDS audit BUG #2b. When SHA256SUMS.asc is temporarily unreachable (device
offline) verifyExistingApk/tryPromoteAndVerify return Indeterminate/Deferred and
throw ApkVerificationDeferredException, which the downloadAPK catch surfaced as
update/error on every retry — a benign 'verify later, bytes preserved' condition
mis-reported as a download error with no native bound.

Suppress update/error for ApkVerificationDeferredException (type-based, not string
match), alongside the intentional-cancel suppression; downgrade its log to info.
The Promise still rejects (throw e preserved) so the awaited JS caller
(auto-update/index.native.ts:119) still retries on the next online pass, and the
byte-preservation (rollback to .partial) is untouched — only the spurious
user-facing error event is withheld. Compiles clean.
…overage APK updater

The APK updater (HybridObject/JNI) can't be JVM-instantiated and had ZERO tests.
Extract its dependency-free pure pieces verbatim into AppUpdateLogic.kt (segment-
name template + CONCURRENT_SEGMENT_COUNT, 416 total parse + is416Complete, 206
Content-Range parse + is206StartAligned) and have the adapter DELEGATE (pure move,
diff 23/23). Add junit testImplementation + 17 JVM unit tests (segment names,
206/416 parse + whitespace tolerance, completeness/alignment predicates,
segmentCount==8); all 10 planted mutants killed (mutation-proven load-bearing).

Also wire the adapter's inline 206-Content-Range parse through the new
parse206ContentRange (it was an identical inline duplicate — the helper was
tested but unused, i.e. coverage theater). No download/file behavior changed
(regexes byte-identical; File-I/O untouched). Behavior preservation rests on the
pure-move + the new pins, since this module has no pre-existing suite.

STILL device/Robolectric-only: the JNI download loop, segment deletion + §5.8
cancel race, promote/verify/install File-I/O.
…raction tests

react-native-app-update shares the OCDS core but its ~1346-line caller was never
audited and had zero tests. Audit found 4 issues; 3 fixed (#1 HIGH cancel-then-
delete §5.8 race, #2a/#2b spurious update/error), pure logic extracted to
AppUpdateLogic.kt + 17 mutation-proven JVM tests. #3/#4 recorded as backlog;
JNI download/cancel/promote File-I/O noted as device/Robolectric-only.
- BUG #4: clarify verifyAPK does package-name + signing-cert verification
  (anti-tamper), not a whole-file SHA/GPG check against the manifest.
- Add an explicit out-of-OCDS-scope note: the desktop full-installer
  (DMG/exe/AppImage) is downloaded by electron-updater, not the OCDS core;
  only the desktop OTA JS bundle is OCDS-conformant.
…g them

verifyExistingApk's catch-all mapped every exception to Indeterminate, which
flows to ApkVerificationDeferredException and is suppressed from update/error.
A genuine local fault (corrupt APK, path/security rejection, runtime error)
was therefore mis-reported as a benign 'verify later' and retried until the
JS give-up budget was spent. Narrow the deferral to IOException (offline /
disk-I/O transient) only; rethrow other exceptions so they surface to the
caller. Verified: app-update module unit tests green.
@huhuanming

Copy link
Copy Markdown
Contributor Author

OCDS conformance — adversarial review summary (native: iOS + Android)

Independent multi-agent audit of this PR against OCDS SPEC v1.2, with each high-severity finding re-checked by a separate skeptic. No blockers — every download path has a SHA256/signature backstop, so a corrupt artifact cannot be installed. Coverage was independently confirmed real (Swift 30/30, Android range 12 suites + APK 17, mutation-proven by injecting regressions).

Landed in this PR (just pushed)

  • fix(android-apk)verifyExistingApk's catch-all collapsed every exception to Indeterminate → ApkVerificationDeferredException, which is suppressed from update/error. A genuine local fault (corrupt APK, path/security rejection, runtime error) was mis-reported as a benign "verify later" and retried until the JS give-up budget drained. Now only IOException (offline / disk-I/O transient) defers; other exceptions rethrow and surface. APK unit tests green.
  • docs(conformance) — corrected two overstatements: (1) verifyAPK does package-name + signing-cert verification (anti-tamper), not a whole-file SHA/GPG check vs the manifest; (2) added an explicit out-of-OCDS-scope note that the desktop full-installer (DMG/exe/AppImage) is downloaded by electron-updater, not the OCDS core.

Confirmed correct

  • Android cancel-then-delete is bounded (awaitTermination(3s), catches InterruptedException) — no deadlock; worst case a 3s block on a Dispatchers.Default worker.
  • The three "pure-move" extractions (iOS/Android core/APK *Logic) are genuinely behavior-preserving; the prior parse206ContentRange "tested-but-not-wired" coverage theater is now actually closed.

Backlog — real but accepted (not fixed here)

Item Why deferred
iOS finalizePermanentFallback deletes .segN before sibling cancels run (§5.8 inversion) Leaves orphan artifacts; whole-file SHA backstops. Real race, low impact.
Android drops all 4 §5.4/§5.11 tuning knobs incl. overall deadline (iOS honors them) Latent — no production caller passes these today, and iOS's deadline also defaults to none; JS-layer give-up budget bounds the real flows.
iOS checksumMismatch → .fallbackPermanent (= single-stream re-download, contradicts SPEC 1.2) Dead in prod — sole caller passes expectedSha256: nil. Landmine for any future caller; needs a distinct terminal class.
BUG #3 (process-global single-flight) / BUG #4 (promote-before-verify) Mitigated by cert verify + installAPK TOCTOU; documented.
clearCache comment/commit "never invoked mid-download" is inaccurate; new-run TOCTOU after isDownloading=false Rare; cancel-handle covers the in-flight-worker race.

Full report available on request.

@huhuanming huhuanming merged commit 377d3b8 into main Jun 24, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants