v9.0: native hardening, Zip Slip guard, aligned cross-platform progress, CI updates#356
Merged
Conversation
… CI updates - Replace raw Thread usage with ExecutorService in Android module - Close ZipFile/stream handles with try-with-resources on all paths - Extract Zip Slip validation into ZipSecurity utility and add JUnit tests - Emit progress events on the main thread - Fix TypeScript declarations (optional params, import placement) - Add iOS nullability annotations - Align Android minSdkVersion fallback with docs (23) - Update CI workflows to modern action versions and Node 20
…ndroid - Re-add Expo matrix entries to e2e.yml for iOS and Android - Rename e2e jobs to E2E iOS/Android (rn|expo) to match branch protection - Add Android build workflow for RN and Expo - Update iOS build workflow to build both RN and Expo - Disambiguate artifact names by app in matrix jobs
Expo Modules Core uses @MainActor/Sendable which require Swift 5.5+ and fail under Xcode 16 strict concurrency when compiled as Swift 5.0. Apply SWIFT_VERSION=5.5 and SWIFT_STRICT_CONCURRENCY=minimal to the iOS build commands so both RN and Expo simulator builds pass.
SWIFT_VERSION=5.5 was rejected/ignored by Xcode 16, leaving ExpoModulesCore in Swift 5 mode where @mainactor is unknown. Use 6.0 (matching the podspec) along with SWIFT_STRICT_CONCURRENCY=minimal to keep RN builds relaxed.
Swift 5 mode on the Xcode 16 / Swift 6 compiler supports @mainactor but treats strict concurrency diagnostics as warnings, avoiding the hard errors seen in react-native-screens and expo-modules-core under Swift 6 complete concurrency checking.
macos-latest uses Xcode 16.4 / Swift 6, which enforces strict concurrency as errors for Expo's dependencies. macos-14 defaults to Xcode 15.4 with Swift 5.10, avoiding those hard errors while keeping @mainactor support via SWIFT_VERSION=5.5.
- Keep iOS jobs on macos-latest (Xcode 16 / Swift 6) - Update react-native-screens 4.25.1 -> 4.25.2 in playground-expo
With react-native-screens bumped to 4.25.2, retry Swift 6.0 language mode and SWIFT_STRICT_CONCURRENCY=minimal to see if the RNScreens strict-concurrency errors are resolved.
react-native-screens 4.25.2 still fails under Swift 6 on macos-latest. Use macos-14 with Xcode 15.4 / Swift 5.10 as the pragmatic workaround until upstream dependencies are Swift-6 clean.
React Native 0.83.9 requires Xcode >= 16.1, so we must stay on macos-latest. Use a Podfile post-install hook to pin: - ExpoModulesCore -> Swift 6.0 (needs @mainactor support) - RNScreens -> Swift 5.0 (still has Swift 6 strict-concurrency issues) Remove the global SWIFT_VERSION/SWIFT_STRICT_CONCURRENCY xcodebuild overrides so the per-target settings take effect.
The per-target Swift version fix belongs on Xcode 16 / macos-latest, not macos-14. Restore the runner.
…; pin RN iOS to macos-15
…acyModule for Swift 6.2
plrthink
force-pushed
the
harden-native-modules
branch
from
June 23, 2026 17:17
c37c7ab to
5446876
Compare
…JS->MainActor bridge
plrthink
force-pushed
the
harden-native-modules
branch
from
June 23, 2026 22:30
6e0862d to
9ce1c80
Compare
…rict-concurrency errors
The GitHub Actions Android emulator (API 29, software GPU) sometimes shows a system-level "System UI isn't responding" dialog shortly after launch. The Expo dev client is heavy enough to trigger it; the RN Android E2E does not. The dialog is unrelated to react-native-zip-archive logic. A one-shot Maestro conditional can miss the dialog because it appears after the check. Replace it with an Android-only polling loop that taps "Wait" while the playground screen is not yet visible. Also give the emulator 4 cores and a 1024 MB heap to reduce UI strain.
The E2E Android (expo) job failed because a system-level "System UI isn't responding" dialog blocked Maestro from seeing the home screen. The one-shot dismiss handler never fired for two reasons: 1. The YAML used a curly apostrophe (U+2019) but Android renders a straight apostrophe (U+0027), so the visibility check was skipped. 2. The dialog often appears mid-wait, after the single pre-check runs. Replace the one-shot handler with a repeat-while loop that taps Wait whenever the ANR dialog is visible, and re-add adb settings to suppress ANR prompts on the CI emulator. Co-authored-by: Perry <plrthink@gmail.com>
fix(e2e): dismiss Android ANR dialog blocking Expo E2E on CI
Replace non-standard adb settings keys (show_anr_messages, anr_dialogs_disabled, anr_show_background) that may be no-ops on the API 29 emulator with Settings.Global.HIDE_ERROR_DIALOGS, the AOSP key used by CTS ANR tests to suppress system error dialogs. Addresses Devin review on PR #358. Co-authored-by: Perry <plrthink@gmail.com>
fix(e2e): use documented hide_error_dialogs ADB setting
Align progress semantics across platforms: unzip/unzipWithPassword now report cumulative uncompressed bytes per entry, zip/zipWithPassword report per-file counts. - Android: weight unzip progress by FileHeader uncompressed sizes via a shared totalUncompressedSize helper; add missing terminal 100% event to unzipWithPassword - iOS: extract via SSZipArchive progressHandler variant, accumulating zipInfo.uncompressed_size against payloadSizeForArchiveAtPath with a per-entry fallback; restore per-entry filePath updates - iOS: pass progress handler in zipFiles/zipFilesWithPassword so they emit intermediate progress instead of only 0% and 100% - Drop unused SSZipArchiveDelegate conformance and dead delegate method - Document progress semantics in README
Contributor
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
playground-rn has no lockfile and used floating ranges, so CI resolved react-native-screens 4.26.x (released 2026-07-16) whose codegen spec fails RN codegen validation (setToolbarMenuElementOptions). Pin react-native to exact 0.83.9 and screens/gesture-handler/ safe-area-context to the same tilde ranges as playground-expo.
- MIGRATION.md: add v8.x to v9.0 guide covering progress semantics, iOS filePath format, serialized operations, and Zip Slip rejection - CHANGELOG.md: 9.0.0 unreleased entry for the hardening PR
readableArrayToStringList called Dynamic.asString() on non-string elements, which throws UnexpectedNativeTypeException outside any try/catch and crashes the app. Validate element types up front and reject the promise with a clear message; add JUnit regression tests.
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.
Summary
Hardens the Android and iOS native modules, fixes the TypeScript declarations, modernizes CI — and aligns progress reporting across platforms. Because several behavior changes are observable to existing consumers, this is released as v9.0.0. See MIGRATION.md for upgrade guidance.
Breaking changes
unzip/unzipWithPasswordprogress is now byte-weighted per entry on both platforms (previously byte-level with within-file granularity on Android, and effectively start/end-only on iOS).zip/zipWithPasswordreport per-file counts; on iOS, the files-array variants now emit intermediate progress events at all (previously only 0% and 100%).unzipeventfilePathnow carries the zip entry name (e.g.folder/file.txt) instead of the full destination path.unzip,unzipWithPassword,unzipAssets).Hardening and fixes
Threadusage with a managedExecutorServiceshut down frominvalidate(); close all zip/stream handles with try-with-resources; close theAssetFileDescriptorinunzipAssets; flush output streams inStreamUtil.copy; non-string elements in thezipfiles array now reject the promise instead of crashing the app.ZipSecurityutility, plus JUnit regression tests forZipSecurityand the files-array conversion.RNZipArchive.h; drop the now-unusedSSZipArchiveDelegateconformance; unzip rewritten on SSZipArchive'sprogressHandlerAPI so per-entry progress andfilePathupdates actually work.index.d.tsand move thereact-nativeimport to the top level.minSdkVersionfallback with the documented API 23 minimum.hide_error_dialogs+ Maestro polling); pin playground-rn native dependencies to match playground-expo (fixes codegen failure from floatingreact-native-screens4.26.x).Verification
npm testpasses (27/27);npm run lintpasses:react-native-zip-archive:compileReleaseJavaWithJavac); Android unit tests pass (:react-native-zip-archive:testReleaseUnitTest, 8 tests)index.d.tspassesxcodebuild)Notes
getUncompressedSizecontract is preserved: it still resolves-1on error to maintain backward compatibility.package.jsonis bumped to 9.0.0 in this PR, so merging to master publishes to npm immediately.