Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
e4f0a66
harden: managed executor, resource cleanup, Zip Slip guard, TS types,…
plrthink Jun 22, 2026
bffbff4
ci: add iOS build validation workflow for playground-rn
plrthink Jun 22, 2026
d8802a8
fix: close assetsInputStream in unzipAssets finally block
plrthink Jun 22, 2026
f693328
ci: restore expo e2e checks and add build checks for rn/expo on ios/a…
plrthink Jun 23, 2026
b6a2b2a
ci: bump Swift version and relax concurrency for Expo iOS builds
plrthink Jun 23, 2026
dae2503
ci: use Swift 6.0 for iOS builds to satisfy expo-modules-core
plrthink Jun 23, 2026
5b78cd9
ci: use Swift 5 language mode for iOS builds
plrthink Jun 23, 2026
bedbedc
ci: run iOS jobs on macos-14 with Xcode 15.4
plrthink Jun 23, 2026
6f92285
ci: revert macos-14 and update react-native-screens to 4.25.2
plrthink Jun 23, 2026
be90155
ci: restore Swift 6.0 + minimal concurrency with updated screens
plrthink Jun 23, 2026
fa785a0
ci: move iOS jobs back to macos-14 (Xcode 15.4)
plrthink Jun 23, 2026
47d8db5
ci: per-target Swift versions for Expo iOS Xcode 16 build
plrthink Jun 23, 2026
c8d62e9
ci: restore macos-latest for iOS jobs
plrthink Jun 23, 2026
375065a
chore: force Swift 6.0 + minimal concurrency for Expo iOS
plrthink Jun 23, 2026
adcb4bb
chore: patch generated xcconfigs for Swift 6.0 + minimal concurrency
plrthink Jun 23, 2026
0a299ac
chore: keep RNScreens on Swift 5.10 to avoid Swift 6 concurrency errors
plrthink Jun 23, 2026
c54ea1e
chore: use Swift 5.10 across Expo iOS workspace
plrthink Jun 23, 2026
ae28323
chore: Swift 6.0 targeted for Expo pods, Swift 5.0 for RNScreens
plrthink Jun 23, 2026
39ef33d
chore: enable Swift 6.2 isolated-conformance experimental features fo…
plrthink Jun 23, 2026
f0883c1
ci: use macos-26 runner for Expo iOS builds
plrthink Jun 23, 2026
d8b46e2
chore: enable InferIsolatedConformances for Expo iOS pods
plrthink Jun 23, 2026
0ae1737
chore: set Swift 6 strict concurrency to minimal for Expo pods
plrthink Jun 23, 2026
644bc48
chore: enable main-actor-by-default for Expo iOS workspace
plrthink Jun 23, 2026
6ea1bb4
fix(ios): revert main-actor-by-default, use InferIsolatedConformances…
plrthink Jun 23, 2026
f6f7aa1
fix(ios): patch expo-router isolated conformances for Swift 6.2
plrthink Jun 23, 2026
967f644
fix(ios): patch expo-linking Sendable singleton for Swift 6.2
plrthink Jun 23, 2026
2ffb9f0
fix(ios): patch ExpoHeadModule global mutable state for Swift 6.2
plrthink Jun 23, 2026
8782f40
fix(ios): patch expo-router main-actor isolation for Swift 6.2
plrthink Jun 23, 2026
60be459
fix(ios): use actual newlines in @MainActor pod patches
plrthink Jun 23, 2026
b825bc8
fix(ios): patch expo-font global mutable state for Swift 6.2
plrthink Jun 23, 2026
661ab4f
fix(ios): use glob to locate expo-font source for patching
plrthink Jun 23, 2026
8715e55
fix(ios): patch expo-file-system isolated conformance for Swift 6.2
plrthink Jun 23, 2026
6ba99bb
fix(ios): isolate EXSessionHandlerProtocol conformance to main actor
plrthink Jun 23, 2026
e42aa3c
fix(ios): patch expo-file-system FilePickingHandler and FileSystemLeg…
plrthink Jun 23, 2026
5446876
fix(expo-ios): patch expo-file-system for Swift 6.2 main-actor/Sendab…
plrthink Jun 23, 2026
9ce1c80
fix(expo-ios): patch updates-interface, dom-webview, and dom-webview …
plrthink Jun 23, 2026
a4a5fda
fix(expo-ios): compile Expo SDK pods in Swift 5 to avoid Swift 6.2 st…
plrthink Jun 30, 2026
6d08d14
test(maestro): dismiss Android system UI ANR dialog during setup
plrthink Jun 30, 2026
a14d725
ci(e2e): increase Android emulator RAM and disable system ANR dialogs
plrthink Jun 30, 2026
760f77a
ci(e2e): keep 4GB emulator RAM and add Maestro fallback for system UI…
plrthink Jun 30, 2026
e539212
fix(e2e): poll for Android system-UI ANR dialog on Expo builds
plrthink Jun 30, 2026
6bde190
fix(e2e): dismiss Android ANR dialog during home-screen wait
cursoragent Jun 30, 2026
b23895e
Merge pull request #358 from mockingbot/cursor/fix-e2e-android-anr-5cc7
cursor[bot] Jun 30, 2026
84a4678
fix(e2e): use documented hide_error_dialogs setting on API 29
cursoragent Jun 30, 2026
1a7b678
Merge pull request #359 from mockingbot/cursor/fix-adb-anr-settings-5cc7
cursor[bot] Jun 30, 2026
b3e5183
feat(progress): byte-weighted unzip progress on Android and iOS
plrthink Jul 17, 2026
813aa10
fix(playground-rn): pin native deps to match playground-expo
plrthink Jul 17, 2026
bb48f4e
chore(release): bump to 9.0.0 and document breaking changes
plrthink Jul 17, 2026
b94eb37
fix(android): reject non-string zip file entries instead of crashing
plrthink Jul 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions .github/workflows/android-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Android Build

on:
pull_request:
branches: [master]
push:
branches: [master]
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build-android:
name: Build Android (${{ matrix.app }})
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- app: rn
dir: playground-rn
- app: expo
dir: playground-expo
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'

- name: Cache node_modules
uses: actions/cache@v4
with:
path: ${{ matrix.dir }}/node_modules
key: ${{ runner.os }}-${{ matrix.app }}-node-${{ hashFiles(format('{0}/package.json', matrix.dir)) }}

- name: Install dependencies
working-directory: ${{ matrix.dir }}
run: |
if [ -f "pnpm-lock.yaml" ]; then
npm install -g pnpm@9
pnpm install
else
npm install
fi

- name: Cache Gradle
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-${{ matrix.app }}-gradle-${{ hashFiles(format('{0}/android/gradle/wrapper/gradle-wrapper.properties', matrix.dir), format('{0}/android/build.gradle', matrix.dir)) }}
restore-keys: |
${{ runner.os }}-${{ matrix.app }}-gradle-

- name: Build Android app
working-directory: ${{ matrix.dir }}/android
run: ./gradlew :app:assembleRelease
29 changes: 25 additions & 4 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ concurrency:
jobs:
e2e-ios:
name: E2E iOS (${{ matrix.app }})
runs-on: macos-latest
runs-on: ${{ matrix.runner }}
timeout-minutes: 60
strategy:
fail-fast: false
Expand All @@ -27,6 +27,14 @@ jobs:
scheme: PlaygroundRN
app_name: PlaygroundRN
bundle_id: com.rnziparchive.playground.rn
runner: macos-15
- app: expo
dir: playground-expo
workspace: RNZipArchivePlayground.xcworkspace
scheme: RNZipArchivePlayground
app_name: RNZipArchivePlayground
bundle_id: com.rnziparchive.playground.expo
runner: macos-26
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down Expand Up @@ -102,7 +110,7 @@ jobs:
if: failure()
uses: actions/upload-artifact@v4
with:
name: maestro-debug-ios
name: maestro-debug-ios-${{ matrix.app }}
path: ~/.maestro/tests/

- name: Upload iOS crash logs
Expand All @@ -117,7 +125,7 @@ jobs:
if: failure()
uses: actions/upload-artifact@v4
with:
name: ios-crash-logs
name: ios-crash-logs-${{ matrix.app }}
path: crash-logs/

e2e-android:
Expand All @@ -131,6 +139,9 @@ jobs:
- app: rn
dir: playground-rn
bundle_id: com.rnziparchive.playground.rn
- app: expo
dir: playground-expo
bundle_id: com.rnziparchive.playground.expo
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down Expand Up @@ -186,15 +197,25 @@ jobs:
with:
api-level: 29
arch: x86_64
cores: 4
ram-size: 4096M
heap-size: 1024M
disk-size: 4096M
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: |
# Expo's heavier UI can trigger a system-level ANR dialog on the
# low-memory GitHub Actions emulator. hide_error_dialogs is the
# documented AOSP global setting (Settings.Global.HIDE_ERROR_DIALOGS)
# used by CTS/Input ANR tests to suppress error dialogs on API 29+.
# Maestro still dismisses any dialog that slips through (_setup.yaml).
adb shell settings put global hide_error_dialogs 1
adb install ${{ matrix.dir }}/android/app/build/outputs/apk/release/app-release.apk
maestro test --env APP_ID="${{ matrix.bundle_id }}" .maestro/flows/ci-master.yaml

- name: Upload Maestro debug artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: maestro-debug-android
name: maestro-debug-android-${{ matrix.app }}
path: ~/.maestro/tests/
80 changes: 80 additions & 0 deletions .github/workflows/ios-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: iOS Build

on:
pull_request:
branches: [master]
push:
branches: [master]
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build-ios:
name: Build iOS (${{ matrix.app }})
runs-on: ${{ matrix.runner }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- app: rn
dir: playground-rn
workspace: PlaygroundRN.xcworkspace
scheme: PlaygroundRN
runner: macos-15
- app: expo
dir: playground-expo
workspace: RNZipArchivePlayground.xcworkspace
scheme: RNZipArchivePlayground
runner: macos-26
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- name: Cache node_modules
uses: actions/cache@v4
with:
path: ${{ matrix.dir }}/node_modules
key: ${{ runner.os }}-${{ matrix.app }}-node-${{ hashFiles(format('{0}/package.json', matrix.dir)) }}

- name: Install dependencies
working-directory: ${{ matrix.dir }}
run: |
if [ -f "pnpm-lock.yaml" ]; then
npm install -g pnpm@9
pnpm install
else
npm install
fi

- name: Cache CocoaPods
uses: actions/cache@v4
with:
path: ${{ matrix.dir }}/ios/Pods
key: ${{ runner.os }}-${{ matrix.app }}-pods-${{ hashFiles(format('{0}/ios/Podfile.lock', matrix.dir)) }}

- name: Install CocoaPods
working-directory: ${{ matrix.dir }}/ios
run: |
gem install cocoapods -v '>= 1.13'
pod install

- name: Build iOS app
working-directory: ${{ matrix.dir }}/ios
run: |
xcodebuild \
-workspace ${{ matrix.workspace }} \
-scheme ${{ matrix.scheme }} \
-configuration Release \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath build \
CODE_SIGNING_ALLOWED=NO \
COMPILER_INDEX_STORE_ENABLE=NO
8 changes: 4 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@master
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@master
uses: actions/setup-node@v4
with:
node-version: 10.0.0
node-version: 20
- name: Publish if version has been updated
uses: pascalgn/npm-publish-action@4f4bf159e299f65d21cd1cbd96fc5d53228036df
uses: pascalgn/npm-publish-action@1.3.9
with: # All of theses inputs are optional
tag_name: "v%s"
tag_message: "v%s"
Expand Down
24 changes: 23 additions & 1 deletion .maestro/flows/_setup.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,31 @@ appId: ${APP_ID}
commands:
- tapOn: "Reload"

# The GitHub Actions Android emulator (API 29, software GPU) occasionally shows
# a system-level "System UI isn't responding" ANR dialog that is unrelated to
# the app. The dialog uses a straight ASCII apostrophe; match that exactly.
# Poll for the dialog on Android while the playground screen is still hidden,
# and tap "Wait" so the loaded app stays in focus.
- runFlow:
when:
platform: Android
commands:
- repeat:
times: 60
while:
notVisible: "RNZipArchive Playground"
commands:
- runFlow:
when:
visible: "System UI isn't responding"
commands:
- tapOn: "Wait"
- waitForAnimationToEnd:
timeout: 2000

- extendedWaitUntil:
visible: "RNZipArchive Playground"
timeout: 180000
timeout: 10000

- runFlow:
when:
Expand Down
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
# Changelog

## [9.0.0] - Unreleased

### Changed (Breaking)
- Progress events are now byte-weighted per entry for `unzip`/`unzipWithPassword` on both platforms (previously byte-level with within-file granularity on Android, effectively start/end-only on iOS)
- iOS `unzip` progress events now report the zip entry name in `filePath` instead of the full destination path
- iOS `zip`/`zipWithPassword` with a files array now emits per-file progress events (previously only 0% and 100%)
- Android operations are serialized on a managed single-thread executor instead of one raw thread per call
- Android progress events are posted on the main thread; the final 100% event may now arrive after the promise resolves
- See [MIGRATION.md](./MIGRATION.md) for upgrade guidance

### Added
- Android: Zip Slip protection — entries escaping the destination directory are rejected during extraction (`unzip`, `unzipWithPassword`, `unzipAssets`)
- Android: `ZipSecurity` utility with JUnit regression tests
- CI: Android/iOS build workflows for both playground apps; Expo added to the E2E matrix

### Fixed
- Android: close all zip/stream handles via try-with-resources (previously leaked on error paths)
- Android: emit progress events on the main thread and flush output streams in `StreamUtil.copy`
- Android: `minSdkVersion` fallback now matches the documented API 23 minimum
- TypeScript: fix invalid default-parameter syntax in `index.d.ts`; move the `react-native` import to the top level
- iOS: nullability annotations in `RNZipArchive.h`
- iOS: per-entry `filePath` updates during unzip (previously dispatched a stale progress value)
- playground-rn: pin `react-native`, `react-native-screens`, `react-native-gesture-handler`, `react-native-safe-area-context` to match playground-expo (fixes CI codegen failure from floating `react-native-screens` 4.26.x)

## [8.0.1] - 2026-05-19

### Fixed
Expand Down
43 changes: 43 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,48 @@
# Migration Guide

## v8.x to v9.0

### What's Changed

v9.0 hardens the native implementations and aligns progress reporting across platforms. The **JavaScript API is unchanged** — all breaking changes are in native behavior that existing code may observe.

### Breaking Changes

| | v8.x | v9.0 |
|---|---|---|
| `unzip` progress (Android) | Byte-level, within-file granularity | Byte-weighted, per-entry granularity |
| `unzip` progress (iOS) | Effectively start/end events only | Byte-weighted, per-entry granularity |
| `unzip` event `filePath` (iOS) | Full destination path | Zip entry name (e.g. `folder/file.txt`) |
| `zip` progress with files array (iOS) | Only 0% and 100% events | Per-file progress events |
| Concurrent operations (Android) | Ran in parallel | Serialized on a single worker thread |
| Malicious/traversal zip entries (Android) | Extracted outside destination | Rejected (Zip Slip protection) |

### Migration Steps

#### Step 1: Check your progress-event handling

If you subscribe to `zipArchiveProgressEvent`:

- **Don't parse `filePath` on iOS** expecting a filesystem path for `unzip` operations — it is now the archive entry name. On Android it remains the source zip path.
- **Don't assume the final 100% event arrives before the promise resolves.** On Android, events are now posted to the main thread and the last event may land after `.then()` runs. Gate completion logic on the promise, not the event.
- Progress is byte-weighted per entry for `unzip`: with archives containing one very large file, expect the bar to jump rather than advance smoothly within that file.

#### Step 2: Check concurrent usage (Android)

If you kick off multiple zip/unzip operations simultaneously, they now execute one at a time in call order. Await them sequentially, or expect later calls to take longer.

#### Step 3: Rebuild

```bash
npm install react-native-zip-archive@^9.0.0
cd ios && pod install && cd ..
```

### Need Help?

- Check the playground apps (`playground-rn/`, `playground-expo/`) for working examples
- Open an issue on GitHub

## v7.x to v8.0

### What's Changed
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,13 @@ getUncompressedSize(sourcePath)
Subscribe to progress events. Useful for showing a progress bar.

- `progress` — value from 0 to 1 (1 = completed)
- `filePath` — the zip file path (empty on iOS for zip operations)
- `filePath` — the zip file path (on iOS, the entry being processed for unzip operations; empty for zip operations)

Progress is reported monotonically from 0 to 1, with explicit 0% and 100% events at the start and end of each operation. The granularity depends on the operation:

- `unzip` / `unzipWithPassword` — byte-weighted: progress reflects uncompressed bytes extracted so far, updated after each entry completes.
- `zip` / `zipWithPassword` — per-file: progress reflects the number of files compressed so far.
- `unzipAssets` (Android only) — approximate: compares bytes read to the compressed archive size.

> The event is global — check `filePath` in your callback to ensure it matches the operation you care about. Remember to call `.remove()` on the returned subscription when done.

Expand Down
4 changes: 3 additions & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ android {
}

defaultConfig {
minSdkVersion safeExtGet('minSdkVersion', 21)
minSdkVersion safeExtGet('minSdkVersion', 23)
targetSdkVersion safeExtGet('targetSdkVersion', 33)
versionCode 1
versionName "1.0"
Expand Down Expand Up @@ -64,4 +64,6 @@ repositories {
dependencies {
implementation 'com.facebook.react:react-native'
implementation group: 'net.lingala.zip4j', name: 'zip4j', version: '2.11.5'

testImplementation 'junit:junit:4.13.2'
}
Loading
Loading