diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15e9d49..b13b7b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -159,15 +159,11 @@ jobs: } >> "$GITHUB_STEP_SUMMARY" android-build: - name: Android Build (newArch=${{ matrix.new-arch }}) + name: Android New Architecture Build needs: changes if: needs.changes.outputs.android == 'true' runs-on: ubuntu-latest timeout-minutes: 45 - strategy: - fail-fast: true - matrix: - new-arch: [false, true] steps: - name: Checkout the code uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 @@ -193,14 +189,14 @@ jobs: - name: Build Android and run unit tests working-directory: example/android run: | - ./gradlew test assembleRelease --stacktrace -PnewArchEnabled=${{ matrix.new-arch }} - mv app/build/outputs/apk/release/app-release.apk app-release-${{ github.sha }}-new-arch-${{ matrix.new-arch }}.apk + ./gradlew test assembleRelease --stacktrace -PnewArchEnabled=true + mv app/build/outputs/apk/release/app-release.apk app-release-${{ github.sha }}-new-arch.apk - name: Upload Android reports if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: Android-Build-Reports-new-arch-${{ matrix.new-arch }} + name: Android-New-Architecture-Build-Reports path: example/android/**/build/reports if-no-files-found: ignore retention-days: 7 @@ -208,8 +204,8 @@ jobs: - name: Upload APK uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: app-release-${{ github.sha }}-new-arch-${{ matrix.new-arch }} - path: example/android/app-release-${{ github.sha }}-new-arch-${{ matrix.new-arch }}.apk + name: app-release-${{ github.sha }}-new-arch + path: example/android/app-release-${{ github.sha }}-new-arch.apk if-no-files-found: error retention-days: 7 @@ -223,7 +219,7 @@ jobs: fail-fast: ${{ github.event_name == 'pull_request' }} max-parallel: 2 matrix: - api-level: ${{ fromJSON(github.event_name == 'pull_request' && '[31]' || '[24,25,29,30,31]') }} + api-level: ${{ fromJSON(github.event_name == 'pull_request' && '[24,31]' || '[24,25,29,30,31]') }} steps: - name: Checkout the code uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 @@ -274,7 +270,7 @@ jobs: retention-days: 7 android-rn-compatibility: - name: Android RN API Compatibility (${{ matrix.react-native }}) + name: Android RN API Compatibility (${{ matrix.react-native }}, ${{ matrix.architecture }}) needs: changes if: needs.changes.outputs.android == 'true' runs-on: ubuntu-latest @@ -283,7 +279,13 @@ jobs: fail-fast: true max-parallel: 2 matrix: - react-native: ['0.73.11', '0.74.7', '0.86.0'] + include: + - react-native: '0.73.11' + architecture: old + - react-native: '0.73.11' + architecture: new + - react-native: '0.74.7' + architecture: new steps: - name: Checkout the code uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 @@ -297,23 +299,19 @@ jobs: - name: Set up Gradle uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6 with: + gradle-version: '8.3' cache-provider: basic cache-read-only: ${{ github.event_name == 'pull_request' }} - name: Compile library against React Native Android APIs - run: sh scripts/test-rn-android-compatibility.sh ${{ matrix.react-native }} new + run: sh scripts/test-rn-android-compatibility.sh ${{ matrix.react-native }} ${{ matrix.architecture }} ios-build-test: - name: iOS (newArch=${{ matrix.new-arch }}) + name: iOS New Architecture Runtime Test needs: changes if: needs.changes.outputs.ios == 'true' - runs-on: macos-14 + runs-on: macos-15 timeout-minutes: 45 - strategy: - fail-fast: ${{ github.event_name == 'pull_request' }} - max-parallel: 2 - matrix: - new-arch: ['0', '1'] steps: - name: Checkout the code uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 @@ -334,14 +332,14 @@ jobs: path: | example/ios/Pods ~/Library/Caches/CocoaPods - key: ${{ runner.os }}-pods-no-flipper-new-arch-${{ matrix.new-arch }}-${{ hashFiles('example/ios/Podfile.lock', 'example/Gemfile.lock') }} + key: ${{ runner.os }}-pods-rn86-new-arch-${{ hashFiles('example/ios/Podfile.lock', 'example/Gemfile.lock') }} restore-keys: | - ${{ runner.os }}-pods-no-flipper-new-arch-${{ matrix.new-arch }}- + ${{ runner.os }}-pods-rn86-new-arch- - name: Install Pods env: NO_FLIPPER: '1' - RCT_NEW_ARCH_ENABLED: ${{ matrix.new-arch }} + RCT_NEW_ARCH_ENABLED: '1' run: | cd example/ios bundle exec pod install @@ -362,6 +360,8 @@ jobs: run: | set -o pipefail cd example/ios + # Xcode expands $(inherited), not the shell. + # shellcheck disable=SC2016 xcodebuild \ -workspace BsDiffPatchExample.xcworkspace \ -scheme BsDiffPatchExample \ @@ -369,7 +369,7 @@ jobs: -sdk iphonesimulator \ -destination "platform=iOS Simulator,id=$SIMULATOR_UDID" \ -resultBundlePath "$RUNNER_TEMP/ios-tests.xcresult" \ - EXPECTED_NEW_ARCH=${{ matrix.new-arch }} \ + EXPECTED_NEW_ARCH=1 \ GCC_PREPROCESSOR_DEFINITIONS='$(inherited) EXPECTED_NEW_ARCH=$(EXPECTED_NEW_ARCH)' \ test | bundle exec xcpretty @@ -377,13 +377,13 @@ jobs: if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: iOS-Test-Results-new-arch-${{ matrix.new-arch }} + name: iOS-New-Architecture-Test-Results path: ${{ runner.temp }}/ios-tests.xcresult if-no-files-found: ignore retention-days: 7 ios-rn-compatibility: - name: iOS RN API Compatibility (${{ matrix.react-native }}) + name: iOS RN API Compatibility (${{ matrix.react-native }}, ${{ matrix.architecture }}) needs: changes if: needs.changes.outputs.ios == 'true' runs-on: ${{ matrix.runner }} @@ -394,20 +394,23 @@ jobs: matrix: include: - react-native: '0.73.11' + architecture: old + node: '18' + runner: macos-14 + - react-native: '0.73.11' + architecture: new node: '18' runner: macos-14 - react-native: '0.74.7' + architecture: new node: '18' runner: macos-14 - - react-native: '0.86.0' - node: '22' - runner: macos-15 steps: - name: Checkout the code uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Set up Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version: ${{ matrix.node }} @@ -419,7 +422,7 @@ jobs: working-directory: example - name: Compile Pod against React Native iOS APIs - run: sh scripts/test-rn-ios-compatibility.sh ${{ matrix.react-native }} + run: sh scripts/test-rn-ios-compatibility.sh ${{ matrix.react-native }} ${{ matrix.architecture }} web-test: name: Web WASM and Browser Test @@ -468,6 +471,7 @@ jobs: yarn typecheck yarn lint yarn test --runInBand + yarn test:native-operations FUZZ_RUNS=2000 yarn test:fuzz site-test: diff --git a/.github/workflows/native-benchmark.yml b/.github/workflows/native-benchmark.yml index 5d29309..aee8556 100644 --- a/.github/workflows/native-benchmark.yml +++ b/.github/workflows/native-benchmark.yml @@ -26,7 +26,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Set up Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version-file: .nvmrc diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index ce70662..0dcfc41 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -81,6 +81,7 @@ jobs: yarn typecheck yarn lint yarn test --runInBand + yarn test:native-operations FUZZ_RUNS=2000 yarn test:fuzz yarn test:web yarn test:web:browser diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 9e9555d..e6f7a83 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -2,7 +2,7 @@ name: Deploy site to GitHub Pages on: push: - branches: [main, feat/new_arch] + branches: [main] paths: - '.github/workflows/pages.yml' - 'docs/**' @@ -34,7 +34,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: - node-version: '18' + node-version-file: .nvmrc cache: yarn - name: Enable Corepack diff --git a/.github/workflows/registry-canary.yml b/.github/workflows/registry-canary.yml index 7f25ccc..2172a5a 100644 --- a/.github/workflows/registry-canary.yml +++ b/.github/workflows/registry-canary.yml @@ -32,7 +32,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Set up Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version: '22' registry-url: https://registry.npmjs.org diff --git a/.nvmrc b/.nvmrc index 3f430af..53d1c14 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v18 +v22 diff --git a/README.md b/README.md index 552294d..f94da73 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,9 @@ all use the compatible `ENDSLEY/BSDIFF43` wire format. - **Both React Native architectures:** legacy bridge and TurboModule/New Architecture. - **Responsive by default:** native work uses dedicated serial queues; Web work reuses a module Worker and cached WebAssembly instance off the page thread. -- **Bound untrusted Web work:** built-in cancellation and input/output byte - limits reject predictably with stable error codes. +- **Control expensive work:** native jobs expose progress, cooperative + cancellation, input/output limits, and atomic output; Web uses + `AbortSignal` and binary limits. - **No Web service required:** the browser implementation is the same bundled C core compiled to WebAssembly. @@ -73,6 +74,27 @@ Output paths must not exist, all paths in one call must be different, and the required input files must already exist. Both functions resolve to `0` on success. +Use the job API when the operation needs progress, cancellation, or resource +bounds: + +```ts +import { startPatch } from 'react-native-bs-diff-patch'; + +const job = startPatch(oldPath, outputPath, patchPath, { + maxInputBytes: 64 * 1024 * 1024, + maxOutputBytes: 128 * 1024 * 1024, +}); +const unsubscribe = job.onProgress(({ phase, progress }) => { + renderProgress(phase, progress); +}); + +try { + await job.result; +} finally { + unsubscribe(); +} +``` + ## React Native Web quick start ```ts @@ -108,9 +130,10 @@ with `EABORTED`; configured size limits reject with `ERESOURCE`. | ------------------------------------------ | ------- | --- | --- | | `diff(oldPath, newPath, patchPath)` | Yes | Yes | No | | `patch(oldPath, outputPath, patchPath)` | Yes | Yes | No | +| `startDiff(...)` / `startPatch(...)` | Yes | Yes | No | | `diffBytes(oldData, newData, options?)` | No | No | Yes | | `patchBytes(oldData, patchData, options?)` | No | No | Yes | -| Legacy architecture | Yes | Yes | N/A | +| Legacy architecture (when provided by RN) | Yes | Yes | N/A | | New Architecture / TurboModule | Yes | Yes | N/A | Calling an API family that is unavailable on the current platform rejects with @@ -141,6 +164,7 @@ Native peers for Web-only consumers. - [Production recipes](./docs/recipes.md) - [Platform support](./docs/platform-support.md) - [Architecture and patch format](./docs/architecture.md) +- [Controllable native operations](./docs/native-operations-v03.md) - [Troubleshooting](./docs/troubleshooting.md) - [Development and verification](./docs/development.md) diff --git a/README.zh-CN.md b/README.zh-CN.md index 39d7aa7..36e11da 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -13,7 +13,8 @@ Android、iOS 与 React Native Web 共用兼容的 `ENDSLEY/BSDIFF43` 补丁格 - **兼容 RN 两种架构:** 同时支持旧桥接架构和 TurboModule / 新架构。 - **默认不阻塞 UI:** 原生端使用专用串行队列;Web 端复用模块 Worker 和已初始化的 WebAssembly 实例,让计算离开页面线程。 -- **限制不可信 Web 任务:** 内置取消与输入/输出字节上限,并提供稳定错误码。 +- **控制高成本任务:** 原生 job 提供进度、协作式取消、输入/输出限制和原子输出; + Web 使用 `AbortSignal` 与二进制限制。 - **Web 无需后端服务:** 浏览器直接运行由同一套 C 核心编译而来的 WebAssembly。 | 运行时 | 输入方式 | 生成补丁 | 应用补丁 | @@ -68,6 +69,26 @@ export async function nativeRoundTrip({ 输出路径不能已经存在,同一次调用中的所有路径必须不同,所需输入文件必须已经 写入完成。两个函数成功时都返回 `0`。 +需要进度、取消或资源边界时使用 job API: + +```ts +import { startPatch } from 'react-native-bs-diff-patch'; + +const job = startPatch(oldPath, outputPath, patchPath, { + maxInputBytes: 64 * 1024 * 1024, + maxOutputBytes: 128 * 1024 * 1024, +}); +const unsubscribe = job.onProgress(({ phase, progress }) => { + renderProgress(phase, progress); +}); + +try { + await job.result; +} finally { + unsubscribe(); +} +``` + ## React Native Web 快速开始 ```ts @@ -103,9 +124,10 @@ export async function webRoundTrip( | ------------------------------------------ | ------- | ------ | ------ | | `diff(oldPath, newPath, patchPath)` | 支持 | 支持 | 不支持 | | `patch(oldPath, outputPath, patchPath)` | 支持 | 支持 | 不支持 | +| `startDiff(...)` / `startPatch(...)` | 支持 | 支持 | 不支持 | | `diffBytes(oldData, newData, options?)` | 不支持 | 不支持 | 支持 | | `patchBytes(oldData, patchData, options?)` | 不支持 | 不支持 | 支持 | -| 旧架构 | 支持 | 支持 | 不适用 | +| 旧架构(限 RN 仍提供时) | 支持 | 支持 | 不适用 | | 新架构 / TurboModule | 支持 | 支持 | 不适用 | 调用当前平台不可用的 API 会以 `EUNSUPPORTED` 拒绝,不会静默切换成其他行为。 @@ -133,6 +155,7 @@ CI 会直接使用 React Native 0.73.11、0.74.7 与 0.86.0 编译 Android 新 - [生产实践](./docs/zh-CN/recipes.md) - [平台支持](./docs/zh-CN/platform-support.md) - [架构与补丁格式](./docs/zh-CN/architecture.md) +- [可控制的原生操作](./docs/zh-CN/native-operations-v03.md) - [常见问题与排障](./docs/zh-CN/troubleshooting.md) - [开发与验证](./docs/zh-CN/development.md) diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt index 83a0031..3e5a881 100644 --- a/android/CMakeLists.txt +++ b/android/CMakeLists.txt @@ -4,13 +4,22 @@ project(BsDiffPatch) set (CMAKE_VERBOSE_MAKEFILE ON) set (CMAKE_CXX_STANDARD 11) -file(GLOB_RECURSE SOURCES "../cpp/*.c" "../cpp/*.cpp" "../cpp/bzlib/*.c" "../cpp/bzlib/*.cpp") +file(GLOB BZIP2_SOURCES "../cpp/bzlib/*.c") +set(SOURCES + ../cpp/bsdiff.c + ../cpp/bspatch.c + ../cpp/react-native-bs-diff-patch.cpp + ${BZIP2_SOURCES} +) add_library(react-native-bs-diff-patch SHARED ${SOURCES} cpp-adapter.cpp ) +find_library(log-lib log) +target_link_libraries(react-native-bs-diff-patch ${log-lib}) + # Specifies a path to native header files. include_directories( ../cpp diff --git a/android/cpp-adapter.cpp b/android/cpp-adapter.cpp index 40bff07..7ec2cf2 100644 --- a/android/cpp-adapter.cpp +++ b/android/cpp-adapter.cpp @@ -1,48 +1,228 @@ #include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + #include "react-native-bs-diff-patch.h" -static jint bsDiffFileJNI(JNIEnv *env, jstring oldFile_, jstring newFile_, jstring patchFile_) { - const char *oldFile = env->GetStringUTFChars(oldFile_, 0); - const char *newFile = env->GetStringUTFChars(newFile_, 0); - const char *patchFile = env->GetStringUTFChars(patchFile_, 0); +namespace { - int result = bsdiffpatch::diffFile(oldFile, newFile, patchFile); +struct OperationState { + std::atomic cancelled{false}; + JNIEnv *env = nullptr; + jclass nativeClass = nullptr; + jstring jobId = nullptr; + int lastPhase = -1; + std::chrono::steady_clock::time_point lastEmission{}; +}; - env->ReleaseStringUTFChars(oldFile_, oldFile); - env->ReleaseStringUTFChars(newFile_, newFile); - env->ReleaseStringUTFChars(patchFile_, patchFile); +std::mutex operationsMutex; +std::unordered_map> operations; +std::string javaString(JNIEnv *env, jstring value) { + const char *characters = env->GetStringUTFChars(value, nullptr); + if (characters == nullptr) { + return {}; + } + std::string result(characters); + env->ReleaseStringUTFChars(value, characters); return result; } -static jint bsPatchFileJNI(JNIEnv *env, jstring oldFile_, jstring newFile_, jstring patchFile_) { - const char *oldFile = env->GetStringUTFChars(oldFile_, 0); - const char *newFile = env->GetStringUTFChars(newFile_, 0); - const char *patchFile = env->GetStringUTFChars(patchFile_, 0); +int isCancelled(void *opaque) { + auto *state = static_cast(opaque); + return state->cancelled.load(std::memory_order_relaxed) ? 1 : 0; +} + +void emitProgress(void *opaque, int phase, double progress) { + auto *state = static_cast(opaque); + auto now = std::chrono::steady_clock::now(); + bool phaseChanged = phase != state->lastPhase; + bool intervalElapsed = state->lastEmission.time_since_epoch().count() == 0 || + now - state->lastEmission >= std::chrono::milliseconds(100); + + if (!phaseChanged && !intervalElapsed && progress < 1.0) { + return; + } + if (state->cancelled.load(std::memory_order_relaxed)) { + return; + } + + jmethodID callback = state->env->GetStaticMethodID( + state->nativeClass, + "onNativeProgress", + "(Ljava/lang/String;ID)V"); + if (callback == nullptr) { + state->env->ExceptionClear(); + return; + } + state->env->CallStaticVoidMethod( + state->nativeClass, + callback, + state->jobId, + static_cast(phase), + static_cast(progress)); + if (state->env->ExceptionCheck()) { + state->env->ExceptionClear(); + return; + } + state->lastPhase = phase; + state->lastEmission = now; +} + +template +jint runOperation( + JNIEnv *env, + jclass nativeClass, + jstring jobIdValue, + jstring oldFileValue, + jstring newFileValue, + jstring patchFileValue, + jlong maxInputBytes, + jlong maxOutputBytes, + Operation operation) { + std::string jobId = javaString(env, jobIdValue); + std::string oldFile = javaString(env, oldFileValue); + std::string newFile = javaString(env, newFileValue); + std::string patchFile = javaString(env, patchFileValue); + auto state = std::make_shared(); + bs_operation_options options{}; - int result = bsdiffpatch::patchFile(oldFile, newFile, patchFile); + if (jobId.empty() || oldFile.empty() || newFile.empty() || patchFile.empty()) { + return BS_OPERATION_ERROR; + } - env->ReleaseStringUTFChars(oldFile_, oldFile); - env->ReleaseStringUTFChars(newFile_, newFile); - env->ReleaseStringUTFChars(patchFile_, patchFile); + state->env = env; + state->nativeClass = nativeClass; + state->jobId = jobIdValue; + { + std::lock_guard lock(operationsMutex); + if (operations.find(jobId) != operations.end()) { + return BS_OPERATION_DESTINATION_EXISTS; + } + operations.emplace(jobId, state); + } + options.max_input_bytes = static_cast(maxInputBytes); + options.max_output_bytes = static_cast(maxOutputBytes); + options.opaque = state.get(); + options.is_cancelled = isCancelled; + options.progress = emitProgress; + jint result = static_cast(operation( + oldFile.c_str(), + newFile.c_str(), + patchFile.c_str(), + &options)); + + { + std::lock_guard lock(operationsMutex); + operations.erase(jobId); + } return result; } +} // namespace + extern "C" JNIEXPORT jint JNICALL -Java_com_jimmydaddy_bsdiffpatch_BsDiffPatchNative_bsDiffFile(JNIEnv *env, - jobject type, - jstring oldFile_, - jstring newFile_, - jstring patchFile_) { - return bsDiffFileJNI(env, oldFile_, newFile_, patchFile_); +Java_com_jimmydaddy_bsdiffpatch_BsDiffPatchNative_bsDiffFile( + JNIEnv *env, + jclass, + jstring oldFile, + jstring newFile, + jstring patchFile) { + jint result = static_cast(bsdiffpatch::diffFile( + javaString(env, oldFile).c_str(), + javaString(env, newFile).c_str(), + javaString(env, patchFile).c_str())); + if (result != BS_OPERATION_OK) { + const char *stage = bsdiffpatch::diffLastErrorStage(); + __android_log_print( + ANDROID_LOG_ERROR, + "BsDiffPatch", + "diff failed at %s: result=%d errno=%d (%s)", + stage == nullptr ? "unknown" : stage, + result, + errno, + std::strerror(errno)); + } + return result; } extern "C" JNIEXPORT jint JNICALL -Java_com_jimmydaddy_bsdiffpatch_BsDiffPatchNative_bsPatchFile(JNIEnv *env, - jobject type, - jstring oldFile_, - jstring newFile_, - jstring patchFile_) { - return bsPatchFileJNI(env, oldFile_, newFile_, patchFile_); +Java_com_jimmydaddy_bsdiffpatch_BsDiffPatchNative_bsPatchFile( + JNIEnv *env, + jclass, + jstring oldFile, + jstring newFile, + jstring patchFile) { + return static_cast(bsdiffpatch::patchFile( + javaString(env, oldFile).c_str(), + javaString(env, newFile).c_str(), + javaString(env, patchFile).c_str())); +} + +extern "C" JNIEXPORT jint JNICALL +Java_com_jimmydaddy_bsdiffpatch_BsDiffPatchNative_bsDiffFileWithOptions( + JNIEnv *env, + jclass nativeClass, + jstring jobId, + jstring oldFile, + jstring newFile, + jstring patchFile, + jlong maxInputBytes, + jlong maxOutputBytes) { + return runOperation( + env, + nativeClass, + jobId, + oldFile, + newFile, + patchFile, + maxInputBytes, + maxOutputBytes, + bsdiffpatch::diffFileWithOptions); +} + +extern "C" JNIEXPORT jint JNICALL +Java_com_jimmydaddy_bsdiffpatch_BsDiffPatchNative_bsPatchFileWithOptions( + JNIEnv *env, + jclass nativeClass, + jstring jobId, + jstring oldFile, + jstring newFile, + jstring patchFile, + jlong maxInputBytes, + jlong maxOutputBytes) { + return runOperation( + env, + nativeClass, + jobId, + oldFile, + newFile, + patchFile, + maxInputBytes, + maxOutputBytes, + bsdiffpatch::patchFileWithOptions); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_com_jimmydaddy_bsdiffpatch_BsDiffPatchNative_bsCancelOperation( + JNIEnv *env, + jclass, + jstring jobIdValue) { + std::string jobId = javaString(env, jobIdValue); + std::lock_guard lock(operationsMutex); + auto operation = operations.find(jobId); + if (operation == operations.end()) { + return JNI_FALSE; + } + operation->second->cancelled.store(true, std::memory_order_relaxed); + return JNI_TRUE; } diff --git a/android/newarch/java/com/jimmydaddy/bsdiffpatch/BsDiffPatchModule.kt b/android/newarch/java/com/jimmydaddy/bsdiffpatch/BsDiffPatchModule.kt index 0006c00..44076b9 100644 --- a/android/newarch/java/com/jimmydaddy/bsdiffpatch/BsDiffPatchModule.kt +++ b/android/newarch/java/com/jimmydaddy/bsdiffpatch/BsDiffPatchModule.kt @@ -7,38 +7,60 @@ import com.facebook.react.module.annotations.ReactModule @ReactModule(name = BsDiffPatchNative.NAME) class BsDiffPatchModule(reactContext: ReactApplicationContext) : NativeBsDiffPatchSpec(reactContext) { - private val taskRunner = BsDiffPatchTaskRunner() + private val support = BsDiffPatchModuleSupport(reactContext) override fun getName(): String = BsDiffPatchNative.NAME - override fun patch( + override fun patch(oldFile: String, newFile: String, patchFile: String, promise: Promise) = + support.patch(oldFile, newFile, patchFile, promise) + + override fun diff(oldFile: String, newFile: String, patchFile: String, promise: Promise) = + support.diff(oldFile, newFile, patchFile, promise) + + override fun startPatch( + jobId: String, oldFile: String, newFile: String, patchFile: String, + maxInputBytes: Double, + maxOutputBytes: Double, promise: Promise - ) { - execute(promise) { - BsDiffPatchNative.patch(oldFile, newFile, patchFile) - } - } + ) = support.startPatch( + jobId, + oldFile, + newFile, + patchFile, + maxInputBytes, + maxOutputBytes, + promise + ) - override fun diff( + override fun startDiff( + jobId: String, oldFile: String, newFile: String, patchFile: String, + maxInputBytes: Double, + maxOutputBytes: Double, promise: Promise - ) { - execute(promise) { - BsDiffPatchNative.diff(oldFile, newFile, patchFile) - } - } + ) = support.startDiff( + jobId, + oldFile, + newFile, + patchFile, + maxInputBytes, + maxOutputBytes, + promise + ) - private fun execute(promise: Promise, block: () -> Int) { - taskRunner.execute(promise, block) - } + override fun cancel(jobId: String, promise: Promise) = support.cancel(jobId, promise) + + override fun addListener(eventName: String) = support.addListener(eventName) + + override fun removeListeners(count: Double) = support.removeListeners(count) override fun invalidate() { - taskRunner.shutdown() + support.invalidate() super.invalidate() } } diff --git a/android/oldarch/java/com/jimmydaddy/bsdiffpatch/BsDiffPatchModule.kt b/android/oldarch/java/com/jimmydaddy/bsdiffpatch/BsDiffPatchModule.kt index 0d04773..38813c4 100644 --- a/android/oldarch/java/com/jimmydaddy/bsdiffpatch/BsDiffPatchModule.kt +++ b/android/oldarch/java/com/jimmydaddy/bsdiffpatch/BsDiffPatchModule.kt @@ -9,45 +9,67 @@ import com.facebook.react.module.annotations.ReactModule @ReactModule(name = BsDiffPatchNative.NAME) class BsDiffPatchModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { - private val taskRunner = BsDiffPatchTaskRunner() + private val support = BsDiffPatchModuleSupport(reactContext) override fun getName(): String = BsDiffPatchNative.NAME @ReactMethod - fun patch(oldFile: String?, newFile: String?, patchFile: String?, promise: Promise) { - execute(promise) { - BsDiffPatchNative.patch( - requireArgument(oldFile, "oldFile"), - requireArgument(newFile, "newFile"), - requireArgument(patchFile, "patchFile") - ) - } - } + fun patch(oldFile: String, newFile: String, patchFile: String, promise: Promise) = + support.patch(oldFile, newFile, patchFile, promise) @ReactMethod - fun diff(oldFile: String?, newFile: String?, patchFile: String?, promise: Promise) { - execute(promise) { - BsDiffPatchNative.diff( - requireArgument(oldFile, "oldFile"), - requireArgument(newFile, "newFile"), - requireArgument(patchFile, "patchFile") - ) - } - } + fun diff(oldFile: String, newFile: String, patchFile: String, promise: Promise) = + support.diff(oldFile, newFile, patchFile, promise) - private fun requireArgument(value: String?, fieldName: String): String { - if (value.isNullOrEmpty()) { - throw BsDiffPatchException("EINVAL", "$fieldName can not be null or empty") - } - return value - } + @ReactMethod + fun startPatch( + jobId: String, + oldFile: String, + newFile: String, + patchFile: String, + maxInputBytes: Double, + maxOutputBytes: Double, + promise: Promise + ) = support.startPatch( + jobId, + oldFile, + newFile, + patchFile, + maxInputBytes, + maxOutputBytes, + promise + ) - private fun execute(promise: Promise, block: () -> Int) { - taskRunner.execute(promise, block) - } + @ReactMethod + fun startDiff( + jobId: String, + oldFile: String, + newFile: String, + patchFile: String, + maxInputBytes: Double, + maxOutputBytes: Double, + promise: Promise + ) = support.startDiff( + jobId, + oldFile, + newFile, + patchFile, + maxInputBytes, + maxOutputBytes, + promise + ) + + @ReactMethod + fun cancel(jobId: String, promise: Promise) = support.cancel(jobId, promise) + + @ReactMethod + fun addListener(eventName: String) = support.addListener(eventName) + + @ReactMethod + fun removeListeners(count: Double) = support.removeListeners(count) override fun invalidate() { - taskRunner.shutdown() + support.invalidate() super.invalidate() } } diff --git a/android/src/main/java/com/jimmydaddy/bsdiffpatch/BsDiffPatchModuleSupport.kt b/android/src/main/java/com/jimmydaddy/bsdiffpatch/BsDiffPatchModuleSupport.kt new file mode 100644 index 0000000..0659107 --- /dev/null +++ b/android/src/main/java/com/jimmydaddy/bsdiffpatch/BsDiffPatchModuleSupport.kt @@ -0,0 +1,117 @@ +package com.jimmydaddy.bsdiffpatch + +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.modules.core.DeviceEventManagerModule +import java.util.concurrent.atomic.AtomicInteger + +internal class BsDiffPatchModuleSupport( + private val reactContext: ReactApplicationContext +) { + private val listenerCount = AtomicInteger(0) + private val taskRunner = BsDiffPatchTaskRunner() + + init { + BsDiffPatchNative.setProgressListener(::emitProgress) + } + + fun patch(oldFile: String, newFile: String, patchFile: String, promise: Promise) { + taskRunner.execute(promise) { + BsDiffPatchNative.patch(oldFile, newFile, patchFile) + } + } + + fun diff(oldFile: String, newFile: String, patchFile: String, promise: Promise) { + taskRunner.execute(promise) { + BsDiffPatchNative.diff(oldFile, newFile, patchFile) + } + } + + fun startPatch( + jobId: String, + oldFile: String, + newFile: String, + patchFile: String, + maxInputBytes: Double, + maxOutputBytes: Double, + promise: Promise + ) { + taskRunner.executeJob(jobId, promise) { + BsDiffPatchNative.patchJob( + jobId, + oldFile, + newFile, + patchFile, + maxInputBytes, + maxOutputBytes + ) + } + } + + fun startDiff( + jobId: String, + oldFile: String, + newFile: String, + patchFile: String, + maxInputBytes: Double, + maxOutputBytes: Double, + promise: Promise + ) { + taskRunner.executeJob(jobId, promise) { + BsDiffPatchNative.diffJob( + jobId, + oldFile, + newFile, + patchFile, + maxInputBytes, + maxOutputBytes + ) + } + } + + fun cancel(jobId: String, promise: Promise) { + try { + val cancelled = taskRunner.cancel(jobId) + val nativeCancelled = BsDiffPatchNative.cancel(jobId) + promise.resolve(cancelled || nativeCancelled) + } catch (error: BsDiffPatchException) { + promise.reject(error.code, error.message, error) + } + } + + fun addListener(eventName: String) { + if (eventName == BsDiffPatchNative.PROGRESS_EVENT) { + listenerCount.incrementAndGet() + } + } + + fun removeListeners(count: Double) { + val requested = count.toInt().coerceAtLeast(0) + listenerCount.updateAndGet { current -> (current - requested).coerceAtLeast(0) } + } + + fun invalidate() { + BsDiffPatchNative.setProgressListener(null) + taskRunner.shutdown().forEach(BsDiffPatchNative::cancel) + } + + private fun emitProgress(jobId: String, phase: Int, progress: Double) { + if (listenerCount.get() <= 0) return + val event = Arguments.createMap().apply { + putString("id", jobId) + putString( + "phase", + when (phase) { + 0 -> "reading" + 1 -> "processing" + else -> "writing" + } + ) + putDouble("progress", progress.coerceIn(0.0, 1.0)) + } + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit(BsDiffPatchNative.PROGRESS_EVENT, event) + } +} diff --git a/android/src/main/java/com/jimmydaddy/bsdiffpatch/BsDiffPatchNative.kt b/android/src/main/java/com/jimmydaddy/bsdiffpatch/BsDiffPatchNative.kt index e684827..8d0a730 100644 --- a/android/src/main/java/com/jimmydaddy/bsdiffpatch/BsDiffPatchNative.kt +++ b/android/src/main/java/com/jimmydaddy/bsdiffpatch/BsDiffPatchNative.kt @@ -1,74 +1,204 @@ package com.jimmydaddy.bsdiffpatch import java.io.File +import java.io.RandomAccessFile +import kotlin.math.floor internal object BsDiffPatchNative { const val NAME = "BsDiffPatch" + const val PROGRESS_EVENT = "BsDiffPatchProgress" + private const val JS_MAX_SAFE_INTEGER = 9_007_199_254_740_991.0 + + @Volatile + private var progressListener: ((String, Int, Double) -> Unit)? = null init { System.loadLibrary("react-native-bs-diff-patch") } fun patch(oldFile: String, newFile: String, patchFile: String): Int { - validateNonEmpty(oldFile, "oldFile") - validateNonEmpty(newFile, "newFile") - validateNonEmpty(patchFile, "patchFile") - validateDistinct(oldFile, newFile, patchFile) - - val oldFileObj = File(normalizePath(oldFile)) - val newFileObj = File(normalizePath(newFile)) - val patchFileObj = File(normalizePath(patchFile)) - - if (!oldFileObj.exists()) { - throw BsDiffPatchException("ENOENT", "oldFile: $oldFile does not exist") - } - if (!patchFileObj.exists()) { - throw BsDiffPatchException("ENOENT", "patchFile: $patchFile does not exist") - } - if (newFileObj.exists()) { - throw BsDiffPatchException("EEXIST", "newFile: $newFile already exists") - } + val paths = validatePatchPaths(oldFile, newFile, patchFile) + return requireSuccess( + "EPATCH", + "patch", + bsPatchFile(paths.old.absolutePath, paths.output.absolutePath, paths.input.absolutePath) + ) + } + fun diff(oldFile: String, newFile: String, patchFile: String): Int { + val paths = validateDiffPaths(oldFile, newFile, patchFile) return requireSuccess( + "EDIFF", + "diff", + bsDiffFile(paths.old.absolutePath, paths.input.absolutePath, paths.output.absolutePath) + ) + } + + fun patchJob( + jobId: String, + oldFile: String, + newFile: String, + patchFile: String, + maxInputBytes: Double, + maxOutputBytes: Double + ): Int { + validateJobId(jobId) + val inputLimit = validateLimit(maxInputBytes, "maxInputBytes") + val outputLimit = validateLimit(maxOutputBytes, "maxOutputBytes") + val paths = validatePatchPaths(oldFile, newFile, patchFile) + enforceInputLimit(inputLimit, paths.old, paths.input) + enforcePatchOutputLimit(outputLimit, paths.input) + return requireJobSuccess( "EPATCH", "patch", - bsPatchFile( - oldFileObj.absolutePath, - newFileObj.absolutePath, - patchFileObj.absolutePath + inputLimit, + outputLimit, + bsPatchFileWithOptions( + jobId, + paths.old.absolutePath, + paths.output.absolutePath, + paths.input.absolutePath, + inputLimit, + outputLimit ) ) } - fun diff(oldFile: String, newFile: String, patchFile: String): Int { + fun diffJob( + jobId: String, + oldFile: String, + newFile: String, + patchFile: String, + maxInputBytes: Double, + maxOutputBytes: Double + ): Int { + validateJobId(jobId) + val inputLimit = validateLimit(maxInputBytes, "maxInputBytes") + val outputLimit = validateLimit(maxOutputBytes, "maxOutputBytes") + val paths = validateDiffPaths(oldFile, newFile, patchFile) + enforceInputLimit(inputLimit, paths.old, paths.input) + return requireJobSuccess( + "EDIFF", + "diff", + inputLimit, + outputLimit, + bsDiffFileWithOptions( + jobId, + paths.old.absolutePath, + paths.input.absolutePath, + paths.output.absolutePath, + inputLimit, + outputLimit + ) + ) + } + + fun cancel(jobId: String): Boolean { + validateJobId(jobId) + return bsCancelOperation(jobId) + } + + fun setProgressListener(listener: ((String, Int, Double) -> Unit)?) { + progressListener = listener + } + + @JvmStatic + private fun onNativeProgress(jobId: String, phase: Int, progress: Double) { + progressListener?.invoke(jobId, phase, progress) + } + + private fun validatePatchPaths(oldFile: String, newFile: String, patchFile: String): Paths { + validatePaths(oldFile, newFile, patchFile) + val old = File(normalizePath(oldFile)) + val output = File(normalizePath(newFile)) + val input = File(normalizePath(patchFile)) + requireInput(old, "oldFile", oldFile) + requireInput(input, "patchFile", patchFile) + requireOutput(output, "newFile", newFile) + return Paths(old, input, output) + } + + private fun validateDiffPaths(oldFile: String, newFile: String, patchFile: String): Paths { + validatePaths(oldFile, newFile, patchFile) + val old = File(normalizePath(oldFile)) + val input = File(normalizePath(newFile)) + val output = File(normalizePath(patchFile)) + requireInput(old, "oldFile", oldFile) + requireInput(input, "newFile", newFile) + requireOutput(output, "patchFile", patchFile) + return Paths(old, input, output) + } + + private fun validatePaths(oldFile: String, newFile: String, patchFile: String) { validateNonEmpty(oldFile, "oldFile") validateNonEmpty(newFile, "newFile") validateNonEmpty(patchFile, "patchFile") - validateDistinct(oldFile, newFile, patchFile) + if (oldFile == newFile || oldFile == patchFile || newFile == patchFile) { + throw BsDiffPatchException( + "EINVAL", + "oldFile, newFile, patchFile can not be the same" + ) + } + } - val oldFileObj = File(normalizePath(oldFile)) - val newFileObj = File(normalizePath(newFile)) - val patchFileObj = File(normalizePath(patchFile)) + private fun validateJobId(jobId: String) { + validateNonEmpty(jobId, "jobId") + } - if (!oldFileObj.exists()) { - throw BsDiffPatchException("ENOENT", "oldFile: $oldFile does not exist") + private fun validateLimit(value: Double, fieldName: String): Long { + if (value == 0.0) return 0 + if (!value.isFinite() || value < 1 || value > JS_MAX_SAFE_INTEGER || floor(value) != value) { + throw BsDiffPatchException( + "EINVAL", + "$fieldName must be a positive safe integer" + ) } - if (!newFileObj.exists()) { - throw BsDiffPatchException("ENOENT", "newFile: $newFile does not exist") + return value.toLong() + } + + private fun enforceInputLimit(limit: Long, vararg files: File) { + if (limit == 0L) return + files.forEach { file -> + val observed = file.length() + if (observed > limit) { + throw BsDiffPatchException( + "EINPUT_TOO_LARGE", + "input is $observed bytes and exceeds the configured $limit byte limit" + ) + } } - if (patchFileObj.exists()) { - throw BsDiffPatchException("EEXIST", "patchFile: $patchFile already exists") + } + + private fun enforcePatchOutputLimit(limit: Long, patchFile: File) { + if (limit == 0L || patchFile.length() < 24) return + RandomAccessFile(patchFile, "r").use { file -> + file.seek(16) + var size = 0L + repeat(8) { index -> + val byte = file.readUnsignedByte() + if (index == 7 && byte and 0x80 != 0) return + val magnitudeByte = if (index == 7) byte and 0x7f else byte + size = size or (magnitudeByte.toLong() shl (index * 8)) + } + if (size > limit) { + throw BsDiffPatchException( + "EOUTPUT_TOO_LARGE", + "output is $size bytes and exceeds the configured $limit byte limit" + ) + } } + } - return requireSuccess( - "EDIFF", - "diff", - bsDiffFile( - oldFileObj.absolutePath, - newFileObj.absolutePath, - patchFileObj.absolutePath - ) - ) + private fun requireInput(file: File, fieldName: String, originalPath: String) { + if (!file.exists()) { + throw BsDiffPatchException("ENOENT", "$fieldName: $originalPath does not exist") + } + } + + private fun requireOutput(file: File, fieldName: String, originalPath: String) { + if (file.exists()) { + throw BsDiffPatchException("EEXIST", "$fieldName: $originalPath already exists") + } } private fun requireSuccess(code: String, operation: String, result: Int): Int { @@ -78,18 +208,34 @@ internal object BsDiffPatchNative { return result } - private fun validateNonEmpty(value: String, fieldName: String) { - if (value.isEmpty()) { - throw BsDiffPatchException("EINVAL", "$fieldName can not be null or empty") + private fun requireJobSuccess( + fallbackCode: String, + operation: String, + inputLimit: Long, + outputLimit: Long, + result: Int + ): Int { + if (result == 0) return result + val code = when (result) { + -2 -> "EINPUT_TOO_LARGE" + -3 -> "EOUTPUT_TOO_LARGE" + -4 -> "ECANCELLED" + -5 -> "EEXIST" + else -> fallbackCode + } + val detail = when (result) { + -2 -> "configured input limit: $inputLimit bytes" + -3 -> "configured output limit: $outputLimit bytes" + -4 -> "operation was cancelled" + -5 -> "destination or job already exists" + else -> "native result $result" } + throw BsDiffPatchException(code, "$operation failed: $detail") } - private fun validateDistinct(oldFile: String, newFile: String, patchFile: String) { - if (oldFile == newFile || oldFile == patchFile || newFile == patchFile) { - throw BsDiffPatchException( - "EINVAL", - "oldFile, newFile, patchFile can not be the same" - ) + private fun validateNonEmpty(value: String, fieldName: String) { + if (value.isEmpty()) { + throw BsDiffPatchException("EINVAL", "$fieldName can not be null or empty") } } @@ -102,6 +248,31 @@ internal object BsDiffPatchNative { @JvmStatic private external fun bsDiffFile(oldFile: String, newFile: String, patchFile: String): Int + + @JvmStatic + private external fun bsPatchFileWithOptions( + jobId: String, + oldFile: String, + newFile: String, + patchFile: String, + maxInputBytes: Long, + maxOutputBytes: Long + ): Int + + @JvmStatic + private external fun bsDiffFileWithOptions( + jobId: String, + oldFile: String, + newFile: String, + patchFile: String, + maxInputBytes: Long, + maxOutputBytes: Long + ): Int + + @JvmStatic + private external fun bsCancelOperation(jobId: String): Boolean + + private data class Paths(val old: File, val input: File, val output: File) } internal class BsDiffPatchException( diff --git a/android/src/main/java/com/jimmydaddy/bsdiffpatch/BsDiffPatchTaskRunner.kt b/android/src/main/java/com/jimmydaddy/bsdiffpatch/BsDiffPatchTaskRunner.kt index abfb908..9b7a6f6 100644 --- a/android/src/main/java/com/jimmydaddy/bsdiffpatch/BsDiffPatchTaskRunner.kt +++ b/android/src/main/java/com/jimmydaddy/bsdiffpatch/BsDiffPatchTaskRunner.kt @@ -1,15 +1,58 @@ package com.jimmydaddy.bsdiffpatch import com.facebook.react.bridge.Promise +import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executors import java.util.concurrent.RejectedExecutionException +import java.util.concurrent.atomic.AtomicBoolean internal class BsDiffPatchTaskRunner { + private data class Job(val cancelled: AtomicBoolean = AtomicBoolean(false)) + + private val jobs = ConcurrentHashMap() private val executor = Executors.newSingleThreadExecutor { runnable -> Thread(runnable, "BsDiffPatchWorker") } fun execute(promise: Promise, block: () -> Int) { + submit(promise) { block() } + } + + fun executeJob(jobId: String, promise: Promise, block: () -> Int) { + val job = Job() + if (jobs.putIfAbsent(jobId, job) != null) { + promise.reject("EEXIST", "jobId: $jobId already exists") + return + } + val submitted = submit(promise) { + try { + if (job.cancelled.get()) { + throw BsDiffPatchException("ECANCELLED", "operation was cancelled") + } + block() + } finally { + jobs.remove(jobId, job) + } + } + if (!submitted) { + jobs.remove(jobId, job) + } + } + + fun cancel(jobId: String): Boolean { + val job = jobs[jobId] ?: return false + job.cancelled.set(true) + return true + } + + fun shutdown(): List { + val activeJobIds = jobs.keys.toList() + jobs.values.forEach { it.cancelled.set(true) } + executor.shutdown() + return activeJobIds + } + + private fun submit(promise: Promise, block: () -> Int): Boolean { try { executor.execute { try { @@ -20,12 +63,10 @@ internal class BsDiffPatchTaskRunner { promise.reject("EUNSPECIFIED", error.message, error) } } + return true } catch (error: RejectedExecutionException) { promise.reject("EUNAVAILABLE", "BsDiffPatch module is no longer available", error) + return false } } - - fun shutdown() { - executor.shutdown() - } } diff --git a/compatibility/android-api/src/newarchStubs/kotlin/com/jimmydaddy/bsdiffpatch/NativeBsDiffPatchSpec.kt b/compatibility/android-api/src/newarchStubs/kotlin/com/jimmydaddy/bsdiffpatch/NativeBsDiffPatchSpec.kt index 54a22de..f48356c 100644 --- a/compatibility/android-api/src/newarchStubs/kotlin/com/jimmydaddy/bsdiffpatch/NativeBsDiffPatchSpec.kt +++ b/compatibility/android-api/src/newarchStubs/kotlin/com/jimmydaddy/bsdiffpatch/NativeBsDiffPatchSpec.kt @@ -20,4 +20,30 @@ abstract class NativeBsDiffPatchSpec( patchFile: String, promise: Promise ) + + abstract fun startPatch( + jobId: String, + oldFile: String, + newFile: String, + patchFile: String, + maxInputBytes: Double, + maxOutputBytes: Double, + promise: Promise + ) + + abstract fun startDiff( + jobId: String, + oldFile: String, + newFile: String, + patchFile: String, + maxInputBytes: Double, + maxOutputBytes: Double, + promise: Promise + ) + + abstract fun cancel(jobId: String, promise: Promise) + + abstract fun addListener(eventName: String) + + abstract fun removeListeners(count: Double) } diff --git a/cpp/bsdiff.c b/cpp/bsdiff.c index d35f30f..471703f 100644 --- a/cpp/bsdiff.c +++ b/cpp/bsdiff.c @@ -25,19 +25,54 @@ * POSSIBILITY OF SUCH DAMAGE. */ +#if defined(__linux__) && !defined(_GNU_SOURCE) +#define _GNU_SOURCE +#endif + #include "bsdiff.h" +#include "bsdiffpatch_operation.h" +#include #include #include +#if defined(__APPLE__) +#include +#elif defined(__linux__) +#include +#include +#endif + #define MIN(x,y) (((x)<(y)) ? (x) : (y)) +#define BSDIFF_IO_CHUNK (64 * 1024) + +static __thread const char *bsdiffLastErrorStage = NULL; + +const char *bsDiffLastErrorStage(void) +{ + return bsdiffLastErrorStage; +} + +static int bsdiff_cancelled(struct bsdiff_stream *stream) +{ + return stream->is_cancelled != NULL && stream->is_cancelled(stream); +} + +static void bsdiff_progress(struct bsdiff_stream *stream, double progress) +{ + if (stream->progress != NULL) + stream->progress(stream, progress); +} -static void split(int64_t *I,int64_t *V,int64_t start,int64_t len,int64_t h) +static int split(int64_t *I,int64_t *V,int64_t start,int64_t len,int64_t h, + struct bsdiff_stream *stream) { int64_t i,j,k,x,tmp,jj,kk; + if (bsdiff_cancelled(stream)) return -1; if(len<16) { for(k=start;kstart) split(I,V,start,jj-start,h); + if(jj>start && split(I,V,start,jj-start,h,stream)) return -1; - for(i=0;ikk) split(I,V,kk,start+len-kk,h); + if(start+len>kk && split(I,V,kk,start+len-kk,h,stream)) return -1; + return 0; } -static void qsufsort(int64_t *I,int64_t *V,const uint8_t *old,int64_t oldsize) +static int qsufsort(int64_t *I,int64_t *V,const uint8_t *old,int64_t oldsize, + struct bsdiff_stream *stream) { int64_t buckets[256]; int64_t i,h,len; for(i=0;i<256;i++) buckets[i]=0; - for(i=0;i0;i--) buckets[i]=buckets[i-1]; buckets[0]=0; for(i=0;i 0) { - const int smallsize = (int)MIN(length, INT_MAX); + const int smallsize = (int)MIN(length, BSDIFF_IO_CHUNK); + if (bsdiff_cancelled(stream)) + return -1; const int writeresult = stream->write(stream, buffer, smallsize); if (writeresult == -1) { @@ -234,8 +290,13 @@ static int bsdiff_internal(const struct bsdiff_request req) if((V=req.stream->malloc((req.oldsize+1)*sizeof(int64_t)))==NULL) return -1; I = req.I; - qsufsort(I,V,req.old,req.oldsize); + bsdiff_progress(req.stream, 0.0); + if (qsufsort(I,V,req.old,req.oldsize,req.stream)) { + req.stream->free(V); + return -1; + } req.stream->free(V); + bsdiff_progress(req.stream, 0.25); buffer = req.buffer; @@ -243,16 +304,24 @@ static int bsdiff_internal(const struct bsdiff_request req) scan=0;len=0;pos=0; lastscan=0;lastpos=0;lastoffset=0; while(scan 0) + bsdiff_progress(req.stream, 0.25 + 0.75 * ((double)scan / (double)req.newsize)); oldscore=0; for(scsc=scan+=len;scanoldscore+8)) break; @@ -265,6 +334,7 @@ static int bsdiff_internal(const struct bsdiff_request req) if((len!=oldscore) || (scan==req.newsize)) { s=0;Sf=0;lenf=0; for(i=0;(lastscan+iSf*2-lenf) { Sf=s; lenf=i; }; @@ -274,6 +344,7 @@ static int bsdiff_internal(const struct bsdiff_request req) if(scan=lastscan+i)&&(pos>=i);i++) { + if ((i & 0x3fff) == 0 && bsdiff_cancelled(req.stream)) return -1; if(req.old[pos-i]==req.new[scan-i]) s++; if(s*2-i>Sb*2-lenb) { Sb=s; lenb=i; }; }; @@ -283,6 +354,7 @@ static int bsdiff_internal(const struct bsdiff_request req) overlap=(lastscan+lenf)-(scan-lenb); s=0;Ss=0;lens=0; for(i=0;iis_cancelled != NULL && + options->is_cancelled(options->opaque); +} + +static void operation_progress( + const struct bs_operation_options *options, + int phase, + double progress) +{ + if (options != NULL && options->progress != NULL) + options->progress(options->opaque, phase, progress); +} + +static int input_limit_result( + const struct bs_operation_options *options, + int64_t size) +{ + if (options != NULL && options->max_input_bytes > 0 && + size > options->max_input_bytes) + return BS_OPERATION_INPUT_TOO_LARGE; + return BS_OPERATION_OK; +} + +static off_t readFileToBuffer( + int fd, + uint8_t *buffer, + off_t bufferSize, + const struct bs_operation_options *options) { off_t bytesRead = 0; - int ret; - while (bytesRead < bufferSize) - { - ret = read(fd, buffer + bytesRead, bufferSize - bytesRead); - if (ret > 0) - { - bytesRead += ret; - } - else - { + while (bytesRead < bufferSize) { + size_t remaining = (size_t)(bufferSize - bytesRead); + size_t chunk = MIN(remaining, BSDIFF_IO_CHUNK); + ssize_t ret; + + if (operation_cancelled(options)) break; - } + ret = read(fd, buffer + bytesRead, chunk); + if (ret <= 0) + break; + bytesRead += ret; } return bytesRead; } -static int bz2_write(struct bsdiff_stream* stream, const void* buffer, int size) +struct bsdiff_file_stream_context { + BZFILE *bz2; + FILE *file; + const struct bs_operation_options *options; + int result; +}; + +static int bsdiff_file_cancelled(struct bsdiff_stream *stream) +{ + struct bsdiff_file_stream_context *context = stream->opaque; + return operation_cancelled(context->options); +} + +static void bsdiff_file_progress(struct bsdiff_stream *stream, double progress) { - int bz2err; - BZFILE* bz2; + struct bsdiff_file_stream_context *context = stream->opaque; + operation_progress( + context->options, + BS_OPERATION_PROCESSING, + 0.15 + progress * 0.70); +} - bz2 = (BZFILE*)stream->opaque; - BZ2_bzWrite(&bz2err, bz2, (void*)buffer, size); - if (bz2err != BZ_STREAM_END && bz2err != BZ_OK) - return -1; +static int bz2_write(struct bsdiff_stream *stream, const void *buffer, int size) +{ + struct bsdiff_file_stream_context *context = stream->opaque; + int bz2err; - return 0; + if (operation_cancelled(context->options)) { + context->result = BS_OPERATION_CANCELLED; + return -1; + } + + BZ2_bzWrite(&bz2err, context->bz2, (void *)buffer, size); + if (bz2err != BZ_STREAM_END && bz2err != BZ_OK) { + context->result = BS_OPERATION_ERROR; + return -1; + } + + if (context->options != NULL && context->options->max_output_bytes > 0) { + long position = ftell(context->file); + if (position >= 0 && position > context->options->max_output_bytes) { + context->result = BS_OPERATION_OUTPUT_TOO_LARGE; + return -1; + } + } + return 0; } -int bsDiffFile(const char* oldFile, const char* newFile, const char* patchFile) +static int bsDiffFileInternal( + const char *oldFile, + const char *newFile, + const char *patchFile, + int outputFd, + const struct bs_operation_options *options) { int fd = -1; int bz2err; int closeResult; - int outputCreated = 0; - int result = -1; + int outputCreated = outputFd >= 0; + int result = BS_OPERATION_ERROR; uint8_t *old = NULL, *new = NULL; int64_t oldsize = 0, newsize = 0; off_t measuredSize; uint8_t buf[8]; - FILE * pf = NULL; + FILE *pf = NULL; struct bsdiff_stream stream; - BZFILE* bz2 = NULL; + BZFILE *bz2 = NULL; + struct bsdiff_file_stream_context context; + const char *errorStage = "validate"; + memset(&stream, 0, sizeof(stream)); + memset(&context, 0, sizeof(context)); stream.malloc = malloc; stream.free = free; stream.write = bz2_write; + stream.is_cancelled = bsdiff_file_cancelled; + stream.progress = bsdiff_file_progress; + stream.opaque = &context; + context.file = NULL; + context.options = options; + context.result = BS_OPERATION_ERROR; if (oldFile == NULL || newFile == NULL || patchFile == NULL) goto cleanup; + if (operation_cancelled(options)) { + result = BS_OPERATION_CANCELLED; + goto cleanup; + } + operation_progress(options, BS_OPERATION_READING, 0.0); + errorStage = "read-old"; fd = open(oldFile, O_RDONLY, 0); if (fd < 0) goto cleanup; @@ -419,15 +580,23 @@ int bsDiffFile(const char* oldFile, const char* newFile, const char* patchFile) if (measuredSize < 0 || (uint64_t)measuredSize > SIZE_MAX - 1) goto cleanup; oldsize = (int64_t)measuredSize; + if (input_limit_result(options, oldsize) != BS_OPERATION_OK) { + result = BS_OPERATION_INPUT_TOO_LARGE; + goto cleanup; + } old = malloc((size_t)oldsize + 1); if (old == NULL || lseek(fd, 0, SEEK_SET) != 0 || - readFileToBuffer(fd, old, (off_t)oldsize) != (off_t)oldsize) + readFileToBuffer(fd, old, (off_t)oldsize, options) != (off_t)oldsize) { + if (operation_cancelled(options)) result = BS_OPERATION_CANCELLED; goto cleanup; + } closeResult = close(fd); fd = -1; if (closeResult != 0) goto cleanup; + operation_progress(options, BS_OPERATION_READING, 0.075); + errorStage = "read-new"; fd = open(newFile, O_RDONLY, 0); if (fd < 0) goto cleanup; @@ -435,49 +604,88 @@ int bsDiffFile(const char* oldFile, const char* newFile, const char* patchFile) if (measuredSize < 0 || (uint64_t)measuredSize > SIZE_MAX - 1) goto cleanup; newsize = (int64_t)measuredSize; + if (input_limit_result(options, newsize) != BS_OPERATION_OK) { + result = BS_OPERATION_INPUT_TOO_LARGE; + goto cleanup; + } new = malloc((size_t)newsize + 1); if (new == NULL || lseek(fd, 0, SEEK_SET) != 0 || - readFileToBuffer(fd, new, (off_t)newsize) != (off_t)newsize) + readFileToBuffer(fd, new, (off_t)newsize, options) != (off_t)newsize) { + if (operation_cancelled(options)) result = BS_OPERATION_CANCELLED; goto cleanup; + } closeResult = close(fd); fd = -1; if (closeResult != 0) goto cleanup; + operation_progress(options, BS_OPERATION_READING, 0.15); - /* Create the patch file */ - fd = open(patchFile, O_CREAT|O_EXCL|O_WRONLY, 0666); - if (fd < 0) + if (options != NULL && options->max_output_bytes > 0 && + options->max_output_bytes < 24) { + result = BS_OPERATION_OUTPUT_TOO_LARGE; goto cleanup; + } + + errorStage = "open-output"; + fd = outputFd >= 0 ? outputFd : open(patchFile, O_CREAT|O_EXCL|O_WRONLY, 0666); + outputFd = -1; + if (fd < 0) { + if (errno == EEXIST) result = BS_OPERATION_DESTINATION_EXISTS; + goto cleanup; + } outputCreated = 1; pf = fdopen(fd, "wb"); if (pf == NULL) goto cleanup; fd = -1; + context.file = pf; - /* Write header (signature+newsize)*/ + errorStage = "write-header"; offtout(newsize, buf); if (fwrite("ENDSLEY/BSDIFF43", 16, 1, pf) != 1 || fwrite(buf, sizeof(buf), 1, pf) != 1) goto cleanup; + errorStage = "open-compressor"; bz2 = BZ2_bzWriteOpen(&bz2err, pf, 9, 0, 0); if (bz2 == NULL || bz2err != BZ_OK) goto cleanup; + context.bz2 = bz2; - stream.opaque = bz2; - if (bsdiff(old, oldsize, new, newsize, &stream)) + errorStage = "generate-diff"; + if (bsdiff(old, oldsize, new, newsize, &stream)) { + result = context.result; + if (operation_cancelled(options)) result = BS_OPERATION_CANCELLED; goto cleanup; + } + errorStage = "close-compressor"; BZ2_bzWriteClose(&bz2err, bz2, 0, NULL, NULL); bz2 = NULL; + context.bz2 = NULL; if (bz2err != BZ_OK) goto cleanup; - + if (options != NULL && options->max_output_bytes > 0) { + long position = ftell(pf); + if (position < 0 || position > options->max_output_bytes) { + result = BS_OPERATION_OUTPUT_TOO_LARGE; + goto cleanup; + } + } + if (operation_cancelled(options)) { + result = BS_OPERATION_CANCELLED; + goto cleanup; + } + operation_progress(options, BS_OPERATION_WRITING, 0.95); + errorStage = "sync-output"; + if (options != NULL && (fflush(pf) != 0 || fsync(fileno(pf)) != 0)) + goto cleanup; + errorStage = "close-output"; closeResult = fclose(pf); pf = NULL; if (closeResult != 0) goto cleanup; - result = 0; + result = BS_OPERATION_OK; cleanup: if (bz2 != NULL) @@ -486,9 +694,144 @@ int bsDiffFile(const char* oldFile, const char* newFile, const char* patchFile) fclose(pf); if (fd >= 0) close(fd); - if (result != 0 && outputCreated && patchFile != NULL) + if (outputFd >= 0) + close(outputFd); + if (result != BS_OPERATION_OK && outputCreated && patchFile != NULL) unlink(patchFile); free(old); free(new); + bsdiffLastErrorStage = result == BS_OPERATION_OK ? NULL : errorStage; + return result; +} + +static int create_sibling_temp(const char *destination, char **temporaryPath) +{ + size_t length; + int fd; + + if (destination == NULL || temporaryPath == NULL) + return -1; + length = strlen(destination) + sizeof(".bsdiffpatch.XXXXXX"); + *temporaryPath = malloc(length); + if (*temporaryPath == NULL) + return -1; + snprintf(*temporaryPath, length, "%s.bsdiffpatch.XXXXXX", destination); + fd = mkstemp(*temporaryPath); + if (fd < 0) { + free(*temporaryPath); + *temporaryPath = NULL; + } + return fd; +} + +static int reserve_and_rename_temp( + const char *temporaryPath, + const char *destination) +{ + int placeholderFd; + int savedErrno; + + /* Android 7's kernel does not provide renameat2 and its SELinux policy + * rejects hard links in the app data directory. Reserve the destination + * name exclusively, then atomically replace our private placeholder. */ + placeholderFd = open(destination, O_CREAT | O_EXCL | O_WRONLY, 0000); + if (placeholderFd < 0) { + if (errno == EEXIST) + return BS_OPERATION_DESTINATION_EXISTS; + return BS_OPERATION_ERROR; + } + if (close(placeholderFd) != 0) { + savedErrno = errno; + unlink(destination); + errno = savedErrno; + return BS_OPERATION_ERROR; + } + if (rename(temporaryPath, destination) == 0) + return BS_OPERATION_OK; + + savedErrno = errno; + unlink(destination); + errno = savedErrno; + return BS_OPERATION_ERROR; +} + +static int commit_sibling_temp(const char *temporaryPath, const char *destination) +{ +#if defined(__APPLE__) + if (renamex_np(temporaryPath, destination, RENAME_EXCL) == 0) + return BS_OPERATION_OK; + if (errno == EEXIST) + return BS_OPERATION_DESTINATION_EXISTS; + if (errno != ENOTSUP && errno != EINVAL) + return BS_OPERATION_ERROR; +#elif defined(__linux__) && defined(SYS_renameat2) + if (syscall( + SYS_renameat2, + AT_FDCWD, + temporaryPath, + AT_FDCWD, + destination, + RENAME_NOREPLACE) == 0) + return BS_OPERATION_OK; + if (errno == EEXIST) + return BS_OPERATION_DESTINATION_EXISTS; + if (errno != ENOSYS && errno != ENOTSUP && errno != EOPNOTSUPP && + errno != EINVAL) + return BS_OPERATION_ERROR; +#endif + + if (link(temporaryPath, destination) != 0) { + if (errno == EEXIST) + return BS_OPERATION_DESTINATION_EXISTS; + return reserve_and_rename_temp(temporaryPath, destination); + } + + /* The destination now names the fully-written inode. Removing the private + * sibling name cannot expose a partial destination or replace another file. */ + unlink(temporaryPath); + return BS_OPERATION_OK; +} + +int bsDiffFile(const char *oldFile, const char *newFile, const char *patchFile) +{ + return bsDiffFileInternal(oldFile, newFile, patchFile, -1, NULL); +} + +int bsDiffFileWithOptions( + const char *oldFile, + const char *newFile, + const char *patchFile, + const struct bs_operation_options *options) +{ + char *temporaryPath = NULL; + int temporaryFd = -1; + int result; + + if (patchFile == NULL) + return BS_OPERATION_ERROR; + if (access(patchFile, F_OK) == 0) + return BS_OPERATION_DESTINATION_EXISTS; + if (errno != ENOENT) + return BS_OPERATION_ERROR; + + temporaryFd = create_sibling_temp(patchFile, &temporaryPath); + if (temporaryFd < 0) + return BS_OPERATION_ERROR; + result = bsDiffFileInternal(oldFile, newFile, temporaryPath, temporaryFd, options); + temporaryFd = -1; + if (result == BS_OPERATION_OK) { + if (operation_cancelled(options)) { + result = BS_OPERATION_CANCELLED; + } else { + result = commit_sibling_temp(temporaryPath, patchFile); + } + } + if (result != BS_OPERATION_OK) + unlink(temporaryPath); + else + operation_progress(options, BS_OPERATION_WRITING, 1.0); + if (result != BS_OPERATION_OK && bsdiffLastErrorStage == NULL) + bsdiffLastErrorStage = "commit-output"; + free(temporaryPath); return result; } diff --git a/cpp/bsdiff.h b/cpp/bsdiff.h index 9809e1a..a72f54b 100644 --- a/cpp/bsdiff.h +++ b/cpp/bsdiff.h @@ -31,11 +31,7 @@ #include #include -#if __has_include() -#include -#else #include "bzlib/bzlib.h" -#endif #include #include @@ -50,9 +46,12 @@ struct bsdiff_stream void* (*malloc)(size_t size); void (*free)(void* ptr); int (*write)(struct bsdiff_stream* stream, const void* buffer, int size); + int (*is_cancelled)(struct bsdiff_stream* stream); + void (*progress)(struct bsdiff_stream* stream, double progress); }; int bsdiff(const uint8_t* oldBuf, int64_t oldsize, const uint8_t* newBuf, int64_t newsize, struct bsdiff_stream* stream); int bsDiffFile(const char* oldFile, const char* newFile, const char* patchFile); +const char* bsDiffLastErrorStage(void); #endif diff --git a/cpp/bsdiffpatch_operation.h b/cpp/bsdiffpatch_operation.h new file mode 100644 index 0000000..1906353 --- /dev/null +++ b/cpp/bsdiffpatch_operation.h @@ -0,0 +1,49 @@ +#ifndef BSDIFFPATCH_OPERATION_H +#define BSDIFFPATCH_OPERATION_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +enum bs_operation_result { + BS_OPERATION_OK = 0, + BS_OPERATION_ERROR = -1, + BS_OPERATION_INPUT_TOO_LARGE = -2, + BS_OPERATION_OUTPUT_TOO_LARGE = -3, + BS_OPERATION_CANCELLED = -4, + BS_OPERATION_DESTINATION_EXISTS = -5 +}; + +enum bs_operation_phase { + BS_OPERATION_READING = 0, + BS_OPERATION_PROCESSING = 1, + BS_OPERATION_WRITING = 2 +}; + +struct bs_operation_options { + int64_t max_input_bytes; + int64_t max_output_bytes; + void *opaque; + int (*is_cancelled)(void *opaque); + void (*progress)(void *opaque, int phase, double progress); +}; + +int bsDiffFileWithOptions( + const char *old_file, + const char *new_file, + const char *patch_file, + const struct bs_operation_options *options); + +int bsPatchFileWithOptions( + const char *old_file, + const char *new_file, + const char *patch_file, + const struct bs_operation_options *options); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/cpp/bspatch.c b/cpp/bspatch.c index bb9040b..b6b6764 100644 --- a/cpp/bspatch.c +++ b/cpp/bspatch.c @@ -25,8 +25,35 @@ * POSSIBILITY OF SUCH DAMAGE. */ +#if defined(__linux__) && !defined(_GNU_SOURCE) +#define _GNU_SOURCE +#endif + #include #include "bspatch.h" +#include "bsdiffpatch_operation.h" + +#include + +#if defined(__APPLE__) +#include +#elif defined(__linux__) +#include +#include +#endif + +#define BSPATCH_IO_CHUNK (64 * 1024) + +static int bspatch_cancelled(const struct bspatch_stream *stream) +{ + return stream->is_cancelled != NULL && stream->is_cancelled(stream); +} + +static void bspatch_progress(const struct bspatch_stream *stream, double progress) +{ + if (stream->progress != NULL) + stream->progress(stream, progress); +} static int64_t offtin(uint8_t *buf) { @@ -68,7 +95,10 @@ int bspatch(const uint8_t* oldbuf, int64_t oldsize, uint8_t* newbuf, int64_t new return -1; oldpos=0;newpos=0; + bspatch_progress(stream, 0.0); while(newposread(stream, buf, 8)) @@ -88,9 +118,12 @@ int bspatch(const uint8_t* oldbuf, int64_t oldsize, uint8_t* newbuf, int64_t new return -1; /* Add old data to diff string */ - for(i=0;i=0) && (oldpos+i 0) + bspatch_progress(stream, (double)newpos / (double)newsize); }; + bspatch_progress(stream, 1.0); return 0; } -static int bz2_read(const struct bspatch_stream* stream, void* buffer, int length) +static int operation_cancelled(const struct bs_operation_options *options) { - int n; - int bz2err; - BZFILE* bz2; + return options != NULL && options->is_cancelled != NULL && + options->is_cancelled(options->opaque); +} - bz2 = (BZFILE*)stream->opaque; - n = BZ2_bzRead(&bz2err, bz2, buffer, length); - if (n != length) - return -1; +static void operation_progress( + const struct bs_operation_options *options, + int phase, + double progress) +{ + if (options != NULL && options->progress != NULL) + options->progress(options->opaque, phase, progress); +} + +static int input_limit_result( + const struct bs_operation_options *options, + int64_t size) +{ + if (options != NULL && options->max_input_bytes > 0 && + size > options->max_input_bytes) + return BS_OPERATION_INPUT_TOO_LARGE; + return BS_OPERATION_OK; +} + +struct bspatch_file_stream_context { + BZFILE *bz2; + const struct bs_operation_options *options; + int result; +}; + +static int bspatch_file_cancelled(const struct bspatch_stream *stream) +{ + struct bspatch_file_stream_context *context = stream->opaque; + return operation_cancelled(context->options); +} +static void bspatch_file_progress(const struct bspatch_stream *stream, double progress) +{ + struct bspatch_file_stream_context *context = stream->opaque; + operation_progress( + context->options, + BS_OPERATION_PROCESSING, + 0.15 + progress * 0.70); +} + +static int bz2_read(const struct bspatch_stream *stream, void *buffer, int length) +{ + struct bspatch_file_stream_context *context = stream->opaque; + int offset = 0; + + while (offset < length) { + int bz2err; + int chunk = length - offset > BSPATCH_IO_CHUNK + ? BSPATCH_IO_CHUNK + : length - offset; + int count; + + if (operation_cancelled(context->options)) { + context->result = BS_OPERATION_CANCELLED; + return -1; + } + count = BZ2_bzRead(&bz2err, context->bz2, (uint8_t *)buffer + offset, chunk); + if (count != chunk) { + context->result = BS_OPERATION_ERROR; + return -1; + } + offset += count; + } return 0; } -static off_t readFileToBuffer(int fd, uint8_t* buffer, off_t bufferSize) +static off_t readFileToBuffer( + int fd, + uint8_t *buffer, + off_t bufferSize, + const struct bs_operation_options *options) { off_t bytesRead = 0; - int ret; - while (bytesRead < bufferSize) - { - ret = read(fd, buffer + bytesRead, bufferSize - bytesRead); - if (ret > 0) - { - bytesRead += ret; - } - else - { + while (bytesRead < bufferSize) { + size_t remaining = (size_t)(bufferSize - bytesRead); + size_t chunk = remaining > BSPATCH_IO_CHUNK ? BSPATCH_IO_CHUNK : remaining; + ssize_t count; + + if (operation_cancelled(options)) break; - } + count = read(fd, buffer + bytesRead, chunk); + if (count <= 0) + break; + bytesRead += count; } return bytesRead; } -static off_t writeFileFromBuffer(int fd, uint8_t* buffer, off_t bufferSize) +static off_t writeFileFromBuffer( + int fd, + uint8_t *buffer, + off_t bufferSize, + const struct bs_operation_options *options) { off_t bytesWritten = 0; - int ret; - while (bytesWritten < bufferSize) - { - ret = write(fd, buffer + bytesWritten, bufferSize - bytesWritten); - if (ret > 0) - { - bytesWritten += ret; - } - else - { + while (bytesWritten < bufferSize) { + size_t remaining = (size_t)(bufferSize - bytesWritten); + size_t chunk = remaining > BSPATCH_IO_CHUNK ? BSPATCH_IO_CHUNK : remaining; + ssize_t count; + + if (operation_cancelled(options)) break; - } + count = write(fd, buffer + bytesWritten, chunk); + if (count <= 0) + break; + bytesWritten += count; + operation_progress( + options, + BS_OPERATION_WRITING, + bufferSize > 0 + ? 0.85 + 0.15 * ((double)bytesWritten / (double)bufferSize) + : 1.0); } return bytesWritten; } -int bsPatchFile(const char* oldFile, const char* newFile, const char* patchFile) +static int bsPatchFileInternal( + const char *oldFile, + const char *newFile, + const char *patchFile, + int outputFd, + const struct bs_operation_options *options) { - FILE * f = NULL; - int fd = -1; - int bz2err; - int closeResult; - int result = -1; - int outputCreated = 0; - uint8_t header[24]; - uint8_t *old = NULL, *new = NULL; - int64_t oldsize = 0, newsize = 0; - off_t measuredSize; - BZFILE* bz2 = NULL; - struct bspatch_stream stream; - - if (oldFile == NULL || newFile == NULL || patchFile == NULL) - goto cleanup; - - /* Open patch file */ - f = fopen(patchFile, "rb"); - if (f == NULL) - goto cleanup; - - /* Read header */ - if (fread(header, 1, 24, f) != 24) - goto cleanup; - - /* Check for appropriate magic */ - if (memcmp(header, "ENDSLEY/BSDIFF43", 16) != 0) - goto cleanup; - - /* Read lengths from header */ - newsize=offtin(header+16); - if(newsize < 0 || (uint64_t)newsize > SIZE_MAX - 1) - goto cleanup; - - /* Close patch file and re-open it via libbzip2 at the right places */ - fd = open(oldFile, O_RDONLY, 0); - if (fd < 0) - goto cleanup; - measuredSize = lseek(fd, 0, SEEK_END); - if (measuredSize < 0 || (uint64_t)measuredSize > SIZE_MAX - 1) - goto cleanup; - oldsize = (int64_t)measuredSize; - old = malloc((size_t)oldsize + 1); - if (old == NULL || lseek(fd, 0, SEEK_SET) != 0 || - readFileToBuffer(fd, old, (off_t)oldsize) != (off_t)oldsize) - goto cleanup; - closeResult = close(fd); - fd = -1; - if (closeResult != 0) - goto cleanup; - - new = malloc((size_t)newsize + 1); - if (new == NULL) - goto cleanup; - bz2 = BZ2_bzReadOpen(&bz2err, f, 0, 1, NULL, 0); - if (bz2 == NULL || bz2err != BZ_OK) - goto cleanup; - - stream.read = bz2_read; - stream.opaque = bz2; - if (bspatch(old, oldsize, new, newsize, &stream)) - goto cleanup; - - /* Clean up the bzip2 reads */ - BZ2_bzReadClose(&bz2err, bz2); - bz2 = NULL; - closeResult = fclose(f); - f = NULL; - if (closeResult != 0) - goto cleanup; - - /* Write the new file */ - fd = open(newFile, O_CREAT|O_EXCL|O_WRONLY, 0666); - if (fd < 0) - goto cleanup; - outputCreated = 1; - if (writeFileFromBuffer(fd, new, (off_t)newsize) != (off_t)newsize) - goto cleanup; - closeResult = close(fd); - fd = -1; - if (closeResult != 0) - goto cleanup; - result = 0; + FILE *f = NULL; + int fd = -1; + int bz2err; + int closeResult; + int result = BS_OPERATION_ERROR; + int outputCreated = outputFd >= 0; + uint8_t header[24]; + uint8_t *old = NULL, *new = NULL; + int64_t oldsize = 0, newsize = 0; + off_t measuredSize; + BZFILE *bz2 = NULL; + struct bspatch_stream stream; + struct bspatch_file_stream_context context; + struct stat patchStat; + + memset(&stream, 0, sizeof(stream)); + memset(&context, 0, sizeof(context)); + stream.read = bz2_read; + stream.is_cancelled = bspatch_file_cancelled; + stream.progress = bspatch_file_progress; + stream.opaque = &context; + context.options = options; + context.result = BS_OPERATION_ERROR; + + if (oldFile == NULL || newFile == NULL || patchFile == NULL) + goto cleanup; + if (operation_cancelled(options)) { + result = BS_OPERATION_CANCELLED; + goto cleanup; + } + + operation_progress(options, BS_OPERATION_READING, 0.0); + f = fopen(patchFile, "rb"); + if (f == NULL) + goto cleanup; + if (fstat(fileno(f), &patchStat) != 0 || patchStat.st_size < 0) + goto cleanup; + if (input_limit_result(options, (int64_t)patchStat.st_size) != BS_OPERATION_OK) { + result = BS_OPERATION_INPUT_TOO_LARGE; + goto cleanup; + } + if (fread(header, 1, 24, f) != 24) + goto cleanup; + if (memcmp(header, "ENDSLEY/BSDIFF43", 16) != 0) + goto cleanup; + newsize = offtin(header + 16); + if (newsize < 0 || (uint64_t)newsize > SIZE_MAX - 1) + goto cleanup; + if (options != NULL && options->max_output_bytes > 0 && + newsize > options->max_output_bytes) { + result = BS_OPERATION_OUTPUT_TOO_LARGE; + goto cleanup; + } + operation_progress(options, BS_OPERATION_READING, 0.05); + + fd = open(oldFile, O_RDONLY, 0); + if (fd < 0) + goto cleanup; + measuredSize = lseek(fd, 0, SEEK_END); + if (measuredSize < 0 || (uint64_t)measuredSize > SIZE_MAX - 1) + goto cleanup; + oldsize = (int64_t)measuredSize; + if (input_limit_result(options, oldsize) != BS_OPERATION_OK) { + result = BS_OPERATION_INPUT_TOO_LARGE; + goto cleanup; + } + old = malloc((size_t)oldsize + 1); + if (old == NULL || lseek(fd, 0, SEEK_SET) != 0 || + readFileToBuffer(fd, old, (off_t)oldsize, options) != (off_t)oldsize) { + if (operation_cancelled(options)) result = BS_OPERATION_CANCELLED; + goto cleanup; + } + closeResult = close(fd); + fd = -1; + if (closeResult != 0) + goto cleanup; + operation_progress(options, BS_OPERATION_READING, 0.15); + + new = malloc((size_t)newsize + 1); + if (new == NULL) + goto cleanup; + bz2 = BZ2_bzReadOpen(&bz2err, f, 0, 1, NULL, 0); + if (bz2 == NULL || bz2err != BZ_OK) + goto cleanup; + context.bz2 = bz2; + if (bspatch(old, oldsize, new, newsize, &stream)) { + result = context.result; + if (operation_cancelled(options)) result = BS_OPERATION_CANCELLED; + goto cleanup; + } + + BZ2_bzReadClose(&bz2err, bz2); + bz2 = NULL; + context.bz2 = NULL; + closeResult = fclose(f); + f = NULL; + if (closeResult != 0) + goto cleanup; + + fd = outputFd >= 0 ? outputFd : open(newFile, O_CREAT|O_EXCL|O_WRONLY, 0666); + outputFd = -1; + if (fd < 0) { + if (errno == EEXIST) result = BS_OPERATION_DESTINATION_EXISTS; + goto cleanup; + } + outputCreated = 1; + if (writeFileFromBuffer(fd, new, (off_t)newsize, options) != (off_t)newsize) { + if (operation_cancelled(options)) result = BS_OPERATION_CANCELLED; + goto cleanup; + } + if (options != NULL && fsync(fd) != 0) + goto cleanup; + closeResult = close(fd); + fd = -1; + if (closeResult != 0) + goto cleanup; + result = BS_OPERATION_OK; cleanup: - if (bz2 != NULL) - BZ2_bzReadClose(&bz2err, bz2); - if (f != NULL) - fclose(f); - if (fd >= 0) - close(fd); - if (result != 0 && outputCreated && newFile != NULL) - unlink(newFile); - free(new); - free(old); - return result; + if (bz2 != NULL) + BZ2_bzReadClose(&bz2err, bz2); + if (f != NULL) + fclose(f); + if (fd >= 0) + close(fd); + if (outputFd >= 0) + close(outputFd); + if (result != BS_OPERATION_OK && outputCreated && newFile != NULL) + unlink(newFile); + free(new); + free(old); + return result; +} + +static int create_sibling_temp(const char *destination, char **temporaryPath) +{ + size_t length; + int fd; + + if (destination == NULL || temporaryPath == NULL) + return -1; + length = strlen(destination) + sizeof(".bsdiffpatch.XXXXXX"); + *temporaryPath = malloc(length); + if (*temporaryPath == NULL) + return -1; + snprintf(*temporaryPath, length, "%s.bsdiffpatch.XXXXXX", destination); + fd = mkstemp(*temporaryPath); + if (fd < 0) { + free(*temporaryPath); + *temporaryPath = NULL; + } + return fd; +} + +static int reserve_and_rename_temp( + const char *temporaryPath, + const char *destination) +{ + int placeholderFd; + int savedErrno; + + /* Android 7's kernel does not provide renameat2 and its SELinux policy + * rejects hard links in the app data directory. Reserve the destination + * name exclusively, then atomically replace our private placeholder. */ + placeholderFd = open(destination, O_CREAT | O_EXCL | O_WRONLY, 0000); + if (placeholderFd < 0) { + if (errno == EEXIST) + return BS_OPERATION_DESTINATION_EXISTS; + return BS_OPERATION_ERROR; + } + if (close(placeholderFd) != 0) { + savedErrno = errno; + unlink(destination); + errno = savedErrno; + return BS_OPERATION_ERROR; + } + if (rename(temporaryPath, destination) == 0) + return BS_OPERATION_OK; + + savedErrno = errno; + unlink(destination); + errno = savedErrno; + return BS_OPERATION_ERROR; +} + +static int commit_sibling_temp(const char *temporaryPath, const char *destination) +{ +#if defined(__APPLE__) + if (renamex_np(temporaryPath, destination, RENAME_EXCL) == 0) + return BS_OPERATION_OK; + if (errno == EEXIST) + return BS_OPERATION_DESTINATION_EXISTS; + if (errno != ENOTSUP && errno != EINVAL) + return BS_OPERATION_ERROR; +#elif defined(__linux__) && defined(SYS_renameat2) + if (syscall( + SYS_renameat2, + AT_FDCWD, + temporaryPath, + AT_FDCWD, + destination, + RENAME_NOREPLACE) == 0) + return BS_OPERATION_OK; + if (errno == EEXIST) + return BS_OPERATION_DESTINATION_EXISTS; + if (errno != ENOSYS && errno != ENOTSUP && errno != EOPNOTSUPP && + errno != EINVAL) + return BS_OPERATION_ERROR; +#endif + + if (link(temporaryPath, destination) != 0) { + if (errno == EEXIST) + return BS_OPERATION_DESTINATION_EXISTS; + return reserve_and_rename_temp(temporaryPath, destination); + } + + /* The destination now names the fully-written inode. Removing the private + * sibling name cannot expose a partial destination or replace another file. */ + unlink(temporaryPath); + return BS_OPERATION_OK; +} + +int bsPatchFile(const char *oldFile, const char *newFile, const char *patchFile) +{ + return bsPatchFileInternal(oldFile, newFile, patchFile, -1, NULL); +} + +int bsPatchFileWithOptions( + const char *oldFile, + const char *newFile, + const char *patchFile, + const struct bs_operation_options *options) +{ + char *temporaryPath = NULL; + int temporaryFd; + int result; + + if (newFile == NULL) + return BS_OPERATION_ERROR; + if (access(newFile, F_OK) == 0) + return BS_OPERATION_DESTINATION_EXISTS; + if (errno != ENOENT) + return BS_OPERATION_ERROR; + + temporaryFd = create_sibling_temp(newFile, &temporaryPath); + if (temporaryFd < 0) + return BS_OPERATION_ERROR; + result = bsPatchFileInternal(oldFile, temporaryPath, patchFile, temporaryFd, options); + if (result == BS_OPERATION_OK) { + if (operation_cancelled(options)) { + result = BS_OPERATION_CANCELLED; + } else { + result = commit_sibling_temp(temporaryPath, newFile); + } + } + if (result != BS_OPERATION_OK) + unlink(temporaryPath); + else + operation_progress(options, BS_OPERATION_WRITING, 1.0); + free(temporaryPath); + return result; } diff --git a/cpp/bspatch.h b/cpp/bspatch.h index 04b8c75..070783f 100644 --- a/cpp/bspatch.h +++ b/cpp/bspatch.h @@ -28,11 +28,7 @@ #ifndef BSPATCH_H #define BSPATCH_H -#if __has_include() -#include -#else #include "bzlib/bzlib.h" -#endif #include #include @@ -47,6 +43,8 @@ struct bspatch_stream { void* opaque; int (*read)(const struct bspatch_stream* stream, void* buffer, int length); + int (*is_cancelled)(const struct bspatch_stream* stream); + void (*progress)(const struct bspatch_stream* stream, double progress); }; int bspatch(const uint8_t* oldbuf, int64_t oldsize, uint8_t* newbuf, int64_t newsize, struct bspatch_stream* stream); diff --git a/cpp/react-native-bs-diff-patch.cpp b/cpp/react-native-bs-diff-patch.cpp index 38a76dd..82d3d89 100644 --- a/cpp/react-native-bs-diff-patch.cpp +++ b/cpp/react-native-bs-diff-patch.cpp @@ -4,6 +4,8 @@ extern "C" { #include "bspatch.h" } +#include "react-native-bs-diff-patch.h" + namespace bsdiffpatch { int diffFile(const char* oldFile, const char* newFile, const char* patchFile) { @@ -13,4 +15,24 @@ namespace bsdiffpatch { int patchFile(const char* oldFile, const char* newFile, const char* patchFile) { return bsPatchFile(oldFile, newFile, patchFile); } + + int diffFileWithOptions( + const char* oldFile, + const char* newFile, + const char* patchFile, + const bs_operation_options* options) { + return bsDiffFileWithOptions(oldFile, newFile, patchFile, options); + } + + int patchFileWithOptions( + const char* oldFile, + const char* newFile, + const char* patchFile, + const bs_operation_options* options) { + return bsPatchFileWithOptions(oldFile, newFile, patchFile, options); + } + + const char* diffLastErrorStage() { + return bsDiffLastErrorStage(); + } } diff --git a/cpp/react-native-bs-diff-patch.h b/cpp/react-native-bs-diff-patch.h index e7aa24e..130d7ed 100644 --- a/cpp/react-native-bs-diff-patch.h +++ b/cpp/react-native-bs-diff-patch.h @@ -1,9 +1,22 @@ #ifndef BSDIFFPATCH_H #define BSDIFFPATCH_H +#include "bsdiffpatch_operation.h" + namespace bsdiffpatch { int diffFile(const char* oldFile, const char* newFile, const char* patchFile); int patchFile(const char* oldFile, const char* newFile, const char* patchFile); + int diffFileWithOptions( + const char* oldFile, + const char* newFile, + const char* patchFile, + const bs_operation_options* options); + int patchFileWithOptions( + const char* oldFile, + const char* newFile, + const char* patchFile, + const bs_operation_options* options); + const char* diffLastErrorStage(); } #endif /* BSDIFFPATCH_H */ diff --git a/cpp/tests/native_operations_test.c b/cpp/tests/native_operations_test.c new file mode 100644 index 0000000..7968f78 --- /dev/null +++ b/cpp/tests/native_operations_test.c @@ -0,0 +1,279 @@ +#ifdef __APPLE__ +#ifndef _DARWIN_C_SOURCE +#define _DARWIN_C_SOURCE +#endif +#else +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L +#endif +#endif + +#include "bsdiff.h" +#include "bsdiffpatch_operation.h" +#include "bspatch.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#define FIXTURE_SIZE (256 * 1024) + +struct callback_state { + int cancelled; + int cancel_during_processing; + int callback_count; + int last_phase; + double last_progress; + int monotonic; + const char *concurrent_destination; +}; + +static int fail(const char *message, int line) +{ + fprintf(stderr, "native operation test failed at line %d: %s\n", line, message); + return 1; +} + +#define CHECK(condition, message) do { if (!(condition)) return fail((message), __LINE__); } while (0) + +static int is_cancelled(void *opaque) +{ + struct callback_state *state = opaque; + return state->cancelled; +} + +static void on_progress(void *opaque, int phase, double progress) +{ + struct callback_state *state = opaque; + if (state->callback_count > 0 && + (phase < state->last_phase || progress < state->last_progress)) + state->monotonic = 0; + state->callback_count++; + state->last_phase = phase; + state->last_progress = progress; + if (state->cancel_during_processing && phase == BS_OPERATION_PROCESSING) + state->cancelled = 1; + if (state->concurrent_destination != NULL && + phase == BS_OPERATION_WRITING && progress >= 0.95) { + FILE *file = fopen(state->concurrent_destination, "wb"); + const char marker[] = "concurrent destination"; + if (file != NULL) { + fwrite(marker, 1, sizeof(marker), file); + fclose(file); + } + state->concurrent_destination = NULL; + } +} + +static int write_fixture(const char *path, int modified) +{ + FILE *file = fopen(path, "wb"); + uint8_t buffer[4096]; + size_t offset; + if (file == NULL) + return -1; + for (offset = 0; offset < FIXTURE_SIZE; offset += sizeof(buffer)) { + size_t index; + for (index = 0; index < sizeof(buffer); index++) { + size_t absolute = offset + index; + uint8_t value = (uint8_t)((absolute * 31 + (absolute >> 8)) & 0xff); + if (modified && absolute % 4096 == 0) + value ^= 0x5a; + buffer[index] = value; + } + if (fwrite(buffer, 1, sizeof(buffer), file) != sizeof(buffer)) { + fclose(file); + return -1; + } + } + return fclose(file); +} + +static int files_equal(const char *leftPath, const char *rightPath) +{ + FILE *left = fopen(leftPath, "rb"); + FILE *right = fopen(rightPath, "rb"); + uint8_t leftBuffer[4096]; + uint8_t rightBuffer[4096]; + int equal = 0; + if (left == NULL || right == NULL) + goto cleanup; + for (;;) { + size_t leftCount = fread(leftBuffer, 1, sizeof(leftBuffer), left); + size_t rightCount = fread(rightBuffer, 1, sizeof(rightBuffer), right); + if (leftCount != rightCount || + memcmp(leftBuffer, rightBuffer, leftCount) != 0) + goto cleanup; + if (leftCount == 0) { + equal = feof(left) && feof(right); + break; + } + } +cleanup: + if (left != NULL) fclose(left); + if (right != NULL) fclose(right); + return equal; +} + +static int has_temporary_output(const char *directory) +{ + DIR *entries = opendir(directory); + struct dirent *entry; + int found = 0; + if (entries == NULL) + return 1; + while ((entry = readdir(entries)) != NULL) { + if (strstr(entry->d_name, ".bsdiffpatch.") != NULL) { + found = 1; + break; + } + } + closedir(entries); + return found; +} + +static struct bs_operation_options options_for(struct callback_state *state) +{ + struct bs_operation_options options; + memset(&options, 0, sizeof(options)); + options.max_input_bytes = FIXTURE_SIZE * 2; + options.max_output_bytes = FIXTURE_SIZE * 2; + options.opaque = state; + options.is_cancelled = is_cancelled; + options.progress = on_progress; + return options; +} + +int main(void) +{ + char directoryTemplate[] = "/tmp/bsdiffpatch-operations-XXXXXX"; + char oldPath[512]; + char newPath[512]; + char patchPath[512]; + char restoredPath[512]; + char limitedPath[512]; + char cancelledPath[512]; + char corruptPatchPath[512]; + char corruptOutputPath[512]; + char racedPath[512]; + char legacyPatchPath[512]; + char legacyRestoredPath[512]; + char *directory = mkdtemp(directoryTemplate); + struct callback_state state; + struct bs_operation_options options; + int result; + + CHECK(directory != NULL, "mkdtemp failed"); + snprintf(oldPath, sizeof(oldPath), "%s/old.bin", directory); + snprintf(newPath, sizeof(newPath), "%s/new.bin", directory); + snprintf(patchPath, sizeof(patchPath), "%s/change.patch", directory); + snprintf(restoredPath, sizeof(restoredPath), "%s/restored.bin", directory); + snprintf(limitedPath, sizeof(limitedPath), "%s/limited.bin", directory); + snprintf(cancelledPath, sizeof(cancelledPath), "%s/cancelled.patch", directory); + snprintf(corruptPatchPath, sizeof(corruptPatchPath), "%s/corrupt.patch", directory); + snprintf(corruptOutputPath, sizeof(corruptOutputPath), "%s/corrupt-output.bin", directory); + snprintf(racedPath, sizeof(racedPath), "%s/raced.patch", directory); + snprintf(legacyPatchPath, sizeof(legacyPatchPath), "%s/legacy.patch", directory); + snprintf(legacyRestoredPath, sizeof(legacyRestoredPath), "%s/legacy-restored.bin", directory); + CHECK(write_fixture(oldPath, 0) == 0, "old fixture creation failed"); + CHECK(write_fixture(newPath, 1) == 0, "new fixture creation failed"); + + { + FILE *corruptPatch = fopen(corruptPatchPath, "wb"); + const char invalidPatch[] = "not a bsdiff patch"; + CHECK(corruptPatch != NULL, "corrupt patch creation failed"); + CHECK(fwrite(invalidPatch, 1, sizeof(invalidPatch), corruptPatch) == sizeof(invalidPatch), + "corrupt patch write failed"); + CHECK(fclose(corruptPatch) == 0, "corrupt patch close failed"); + } + CHECK(bsPatchFile(oldPath, corruptOutputPath, corruptPatchPath) != BS_OPERATION_OK, + "legacy patch accepted malformed input"); + CHECK(access(corruptOutputPath, F_OK) != 0, + "legacy malformed patch committed an output"); + CHECK(bsDiffFile(oldPath, newPath, legacyPatchPath) == BS_OPERATION_OK, + "legacy diff failed"); + CHECK(bsPatchFile(oldPath, legacyRestoredPath, legacyPatchPath) == BS_OPERATION_OK, + "legacy patch failed"); + CHECK(files_equal(newPath, legacyRestoredPath), + "legacy round trip differs from fixture"); + + memset(&state, 0, sizeof(state)); + state.last_phase = -1; + state.monotonic = 1; + options = options_for(&state); + result = bsDiffFileWithOptions(oldPath, newPath, patchPath, &options); + CHECK(result == BS_OPERATION_OK, "limited diff did not succeed"); + CHECK(state.callback_count > 2 && state.monotonic, "diff progress was not monotonic"); + CHECK(access(patchPath, F_OK) == 0, "diff output was not committed"); + + memset(&state, 0, sizeof(state)); + state.last_phase = -1; + state.monotonic = 1; + options = options_for(&state); + result = bsPatchFileWithOptions(oldPath, restoredPath, patchPath, &options); + CHECK(result == BS_OPERATION_OK, "limited patch did not succeed"); + CHECK(files_equal(newPath, restoredPath), "patched output differs from fixture"); + CHECK(state.callback_count > 2 && state.monotonic, "patch progress was not monotonic"); + + memset(&state, 0, sizeof(state)); + options = options_for(&state); + options.max_input_bytes = 1024; + result = bsDiffFileWithOptions(oldPath, newPath, limitedPath, &options); + CHECK(result == BS_OPERATION_INPUT_TOO_LARGE, "input limit returned the wrong status"); + CHECK(access(limitedPath, F_OK) != 0, "input limit committed an output"); + + memset(&state, 0, sizeof(state)); + options = options_for(&state); + options.max_output_bytes = FIXTURE_SIZE - 1; + result = bsPatchFileWithOptions(oldPath, limitedPath, patchPath, &options); + CHECK(result == BS_OPERATION_OUTPUT_TOO_LARGE, "output limit returned the wrong status"); + CHECK(access(limitedPath, F_OK) != 0, "output limit committed an output"); + + memset(&state, 0, sizeof(state)); + state.cancel_during_processing = 1; + state.last_phase = -1; + state.monotonic = 1; + options = options_for(&state); + result = bsDiffFileWithOptions(oldPath, newPath, cancelledPath, &options); + CHECK(result == BS_OPERATION_CANCELLED, "cancellation returned the wrong status"); + CHECK(access(cancelledPath, F_OK) != 0, "cancelled operation committed an output"); + CHECK(!has_temporary_output(directory), "cancelled operation leaked a temporary file"); + + CHECK(write_fixture(limitedPath, 0) == 0, "destination fixture creation failed"); + memset(&state, 0, sizeof(state)); + options = options_for(&state); + result = bsPatchFileWithOptions(oldPath, limitedPath, patchPath, &options); + CHECK(result == BS_OPERATION_DESTINATION_EXISTS, "existing destination returned the wrong status"); + CHECK(files_equal(oldPath, limitedPath), "existing destination was modified"); + + memset(&state, 0, sizeof(state)); + state.last_phase = -1; + state.monotonic = 1; + state.concurrent_destination = racedPath; + options = options_for(&state); + result = bsDiffFileWithOptions(oldPath, newPath, racedPath, &options); + CHECK(result == BS_OPERATION_DESTINATION_EXISTS, + "concurrent destination returned the wrong status"); + CHECK(access(racedPath, F_OK) == 0, + "concurrent destination disappeared during commit"); + CHECK(!has_temporary_output(directory), + "concurrent destination commit leaked a temporary file"); + + unlink(limitedPath); + unlink(restoredPath); + unlink(patchPath); + unlink(newPath); + unlink(oldPath); + unlink(corruptPatchPath); + unlink(racedPath); + unlink(legacyPatchPath); + unlink(legacyRestoredPath); + CHECK(rmdir(directory) == 0, "temporary directory cleanup failed"); + printf("native operation controls: ok\n"); + return 0; +} diff --git a/docs/README.md b/docs/README.md index d0907ae..eff93af 100644 --- a/docs/README.md +++ b/docs/README.md @@ -23,7 +23,7 @@ The [Chinese documentation](./zh-CN/README.md) mirrors the same public guides. - [Production recipes](./recipes.md) — integrity, cleanup, downloads, and cross-runtime workflows. - [Platform support](./platform-support.md) — architecture and bundler behavior. - [Architecture](./architecture.md) — execution paths and patch compatibility. -- [Native operations 0.3](./native-operations-v03.md) — resource limits, +- [Controllable native operations](./native-operations-v03.md) — resource limits, cancellation, progress, and atomic output contract. - [Troubleshooting](./troubleshooting.md) — common integration failures. - [Development](./development.md) — local builds, tests, WebAssembly, and release checks. diff --git a/docs/api-reference.md b/docs/api-reference.md index 825e6e0..c9853f2 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -7,6 +7,8 @@ path. Native runtimes use absolute paths; Web uses in-memory binary values. import { diff, patch, + startDiff, + startPatch, diffBytes, patchBytes, type BinaryInput, @@ -52,6 +54,54 @@ Reconstructs the target file at `outputFile`. Available on Android and iOS. - Resolves to `0` on success. - Rejects rather than overwriting an existing `outputFile`. +## `startDiff` and `startPatch` + +```ts +interface NativeOperationOptions { + maxInputBytes?: number; + maxOutputBytes?: number; +} + +interface NativeOperationProgress { + id: string; + operation: 'diff' | 'patch'; + phase: 'reading' | 'processing' | 'writing'; + progress: number; +} + +interface NativeOperationJob { + id: string; + result: Promise; + cancel(): Promise; + onProgress(listener: (event: NativeOperationProgress) => void): () => void; +} + +function startDiff( + oldFile: string, + newFile: string, + patchFile: string, + options?: NativeOperationOptions +): NativeOperationJob; + +function startPatch( + oldFile: string, + outputFile: string, + patchFile: string, + options?: NativeOperationOptions +): NativeOperationJob; +``` + +The job API is available on Android and iOS when progress, cancellation, or +resource bounds are required. + +- `result` resolves to `0` or rejects with a classified native error. +- `cancel()` requests cooperative cancellation; a cancelled result rejects + with `ECANCELLED`. +- `onProgress()` filters events to this job and returns an unsubscribe function. +- Native limits must be positive safe integers when supplied. +- Failed job operations remove their sibling temporary output and never + overwrite an existing destination. + ## `diffBytes` ```ts @@ -108,13 +158,14 @@ bytes. Available on Web. The binary APIs accept the options argument on native only to keep shared wrappers source-compatible, then reject with `EUNSUPPORTED` as usual. Native -resource policy remains the application's filesystem/workflow responsibility. +path operations use `startDiff` or `startPatch` for equivalent controls. ## Availability behavior -All four functions remain exported so shared code has one stable import shape. +All functions remain exported so shared code has one stable import shape. Calling `diffBytes` or `patchBytes` on native rejects with `EUNSUPPORTED`. -Calling `diff` or `patch` on Web behaves the same way. +Calling `diff`, `patch`, `startDiff`, or `startPatch` on Web behaves the same +way. Importing the Web entry during server-side rendering does not start a Worker. Calling a binary API without browser Worker support rejects with @@ -136,6 +187,9 @@ type PatchError = Error & { code?: string }; | `EEXIST` | A native output path already exists. | | `EUNSUPPORTED` | The selected API is not available on the current platform. | | `EUNAVAILABLE` | The native module worker has already shut down. | +| `ECANCELLED` | A native job was cooperatively cancelled. | +| `EINPUT_TOO_LARGE` | A native input exceeded `maxInputBytes`. | +| `EOUTPUT_TOO_LARGE` | Native generated/restored output exceeded its limit. | | `EABORTED` | The Web operation was cancelled through its signal. | | `ERESOURCE` | A configured Web input or output byte limit was exceeded. | | `EDIFF` | The native diff core rejected or could not write the input. | @@ -152,7 +206,9 @@ Worker startup, patch validation, or WebAssembly execution use ## Concurrency and ordering -Each native platform uses a serial library-owned queue. Web calls without a +Each native platform uses a serial library-owned queue shared by promise and +job operations. Cancelling a queued job prevents it from entering the C core; +cancelling an active job is observed cooperatively. Web calls without a signal share one module Worker, a serialized request queue, and a cached WebAssembly module. Calls with a signal use a dedicated Worker so cancellation is operation-local. Apply an application-level concurrency and memory budget @@ -160,5 +216,5 @@ for large browser inputs. ## Patch format -All four operations read or write `ENDSLEY/BSDIFF43` patches. Other bsdiff +All operations read or write `ENDSLEY/BSDIFF43` patches. Other bsdiff variants, such as patches beginning with `BSDIFF40`, are not interchangeable. diff --git a/docs/architecture.md b/docs/architecture.md index 7cba9fb..b1965ba 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -9,7 +9,7 @@ adapters. React Native JavaScript -> typed public API -> TurboModule or legacy bridge - -> platform-owned serial worker queue + -> platform job registry and serial operation boundary -> JNI / Objective-C++ -> shared bsdiff + bzip2 C sources @@ -24,6 +24,11 @@ The worker boundaries keep expensive binary work away from the JavaScript/UI thread. They do not make the algorithm free: callers remain responsible for product-specific input-size and time limits. +Native jobs carry cancellation and progress callbacks into the C streams. +Outputs are written to an exclusive sibling temporary file and committed only +after the operation has flushed successfully. The legacy promise API shares the +same serial operation boundary but does not add implicit limits. + Web calls without an `AbortSignal` reuse one Worker and a cached Emscripten module, avoiding repeated Worker and WebAssembly initialization. Calls with a signal use a dedicated Worker; aborting the signal terminates only that Worker. @@ -114,9 +119,9 @@ output size. The native reference reaches roughly nineteen times the input size for this highly similar 50 MiB fixture, primarily because of the suffix array and simultaneous file buffers. -For very large updates, enforce an application limit before calling the -library, and consider a server-side or streaming update strategy when the full -files cannot safely fit in memory. +For very large updates, configure native/Web operation limits (or check before +calling the legacy API), and consider a server-side or streaming update +strategy when the full files cannot safely fit in memory. ## Ownership boundaries @@ -126,7 +131,7 @@ owns: - file selection, storage permissions, and temporary-file cleanup; - patch transport and cache policy; - authentication and cryptographic integrity checks; -- concurrency, size, and time limits; +- choosing concurrency, size, and time policies and passing supported limits; - verification and atomic replacement of the restored output. Keeping these responsibilities outside the patch engine lets applications use diff --git a/docs/development.md b/docs/development.md index dd41afd..ae0bc29 100644 --- a/docs/development.md +++ b/docs/development.md @@ -2,9 +2,10 @@ ## Prerequisites -- Node.js 18 or newer. +- Node.js 20.19.4 or newer (the repository uses Node 22 in CI). - Yarn 3.6.1 through the repository's checked-in Yarn release. -- Android Studio/JDK 17 for Android work. +- Android Studio/JDK 17 for Android work; the RN 0.73/0.74 compatibility fixture + intentionally uses Gradle 8.3 (`GRADLE_EXECUTABLE` can select it locally). - Xcode and CocoaPods for iOS work. - Emscripten only when regenerating the checked-in WebAssembly bundle. @@ -23,6 +24,7 @@ yarn prepare yarn typecheck yarn lint yarn test --runInBand +yarn test:native-operations ``` ## Web gates @@ -45,12 +47,14 @@ yarn test:package ```sh FUZZ_RUNS=2000 yarn test:fuzz +scripts/test-rn-android-compatibility.sh 0.73.11 old scripts/test-rn-android-compatibility.sh 0.73.11 new scripts/test-rn-android-compatibility.sh 0.74.7 new scripts/test-rn-android-compatibility.sh 0.86.0 new -scripts/test-rn-ios-compatibility.sh 0.73.11 -scripts/test-rn-ios-compatibility.sh 0.74.7 -scripts/test-rn-ios-compatibility.sh 0.86.0 +scripts/test-rn-ios-compatibility.sh 0.73.11 old +scripts/test-rn-ios-compatibility.sh 0.73.11 new +scripts/test-rn-ios-compatibility.sh 0.74.7 new +scripts/test-rn-ios-compatibility.sh 0.86.0 new ``` The fuzz gate uses libFuzzer with AddressSanitizer and UndefinedBehaviorSanitizer @@ -58,6 +62,8 @@ when the local Clang runtime provides it, otherwise it runs a deterministic sanitizer corpus. The compatibility fixture compiles the actual Android module sources against the selected React Native artifact instead of relying on source-pattern assertions. +`test:native-operations` deterministically covers job progress, cancellation, +limits, malformed patches, atomic destination behavior, and temporary cleanup. Run the repeatable Web performance baseline with: @@ -80,13 +86,15 @@ are optional peers. Dependabot groups routine npm, Ruby, and Actions updates to keep review volume bounded. The lockfile also pins patched leaf versions where their APIs remain compatible. -The legacy development toolchain currently retains three development-only -transitive families that cannot be safely forced to their advertised fixes: -`fast-xml-parser` requires a major upgrade outside CLI 12's range, `tar` requires -a major upgrade outside its parents' ranges, and `ip` has an open advisory with -no patched version. Keep these visible in GitHub alerts and remove them when the -0.73 example and Jest toolchains are retired or their parents publish -compatible upgrades. +```sh +yarn npm audit --all --recursive +``` + +The example and root toolchains track React Native 0.86, CLI 20.2, and +release-it 20. The upgrades remove the vulnerable `tmp` and `ip` chains; the +lockfile pins patched `tar`, `fast-xml-parser`, `socks`, and compatible leaf +overrides reported by the audit. Recheck GitHub Dependabot alerts after +dependency changes instead of assuming a lockfile override closes an advisory. ## Site and documentation @@ -115,13 +123,17 @@ Commit the regenerated `web/bsdiffpatch.mjs` with the C source change. ## Native verification -Android CI builds both architecture modes, directly compiles New Architecture -sources against React Native 0.73.11, 0.74.7, and 0.86.0, and runs the New -Architecture device round trip on its emulator matrix. iOS CI compiles the Pod -against the same three React Native versions, then uses the CocoaPods version -locked in the example Gemfile to test both legacy and New Architecture modes. -The simulator asserts the active architecture in addition to cross-platform -golden patches and malformed-patch rejection. +Android CI compiles the legacy boundary and New Architecture sources, then runs +the public API through RN 0.86 New Architecture on API 24 and 31 for pull +requests. iOS compiles the Pod compatibility fixtures and runs RN 0.86 New +Architecture on Simulator; React Native 0.82 and newer no longer provide a +legacy runtime. Device tests assert the active architecture, cross-platform +golden patches, malformed-patch rejection, job progress, cancellation, limits, +and output cleanup. + +`native-benchmark.yml` is manual and scheduled infrastructure. It uploads +Linux/macOS JSON baselines but is intentionally not a pull-request blocker +because shared-runner performance is noisy. For local example commands, see [CONTRIBUTING.md](../CONTRIBUTING.md). diff --git a/docs/getting-started.md b/docs/getting-started.md index 80f585d..3196649 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -23,7 +23,7 @@ native modules inside an already-installed binary. | Runtime | Use | Do not use | | ------------ | ---------------------------- | ---------------- | -| Android, iOS | `diff` and `patch` | Binary-data APIs | +| Android, iOS | `diff`/`patch` or native jobs | Binary-data APIs | | Web | `diffBytes` and `patchBytes` | File-path APIs | The unavailable family rejects with `EUNSUPPORTED`, which helps catch imports @@ -76,6 +76,38 @@ Use a content hash or byte comparison from your filesystem layer to verify that `restoredPath` matches `newFilePath`. Clean the patch and restored file when they are no longer needed. +## Add native progress, cancellation, and limits + +Use a job when the operation is user-controlled or processes untrusted input: + +```ts +import { startPatch } from 'react-native-bs-diff-patch'; + +const job = startPatch(oldFilePath, restoredPath, patchPath, { + maxInputBytes: 64 * 1024 * 1024, + maxOutputBytes: 128 * 1024 * 1024, +}); + +const unsubscribe = job.onProgress(({ phase, progress }) => { + setOperationState({ phase, percent: Math.round(progress * 100) }); +}); + +try { + await job.result; +} catch (error) { + if ((error as { code?: string }).code !== 'ECANCELLED') throw error; +} finally { + unsubscribe(); +} + +// Wire this to the screen's Cancel action while result is pending. +cancelButton.onPress = () => void job.cancel(); +``` + +`startDiff` accepts the same options. Native jobs write through a sibling +temporary file, so cancellation or a limit failure does not expose a partial +destination. + ## Web binary workflow React Native Web uses binary values instead of filesystem paths: @@ -138,5 +170,6 @@ but call it only after a browser `Worker` is available. - Copy a recovery pattern from [Production recipes](./recipes.md). - Review all signatures and error codes in the [API reference](./api-reference.md). +- Review [controllable native operations](./native-operations-v03.md). - Check [platform and bundler support](./platform-support.md). - Try the [live Playground](https://bs-dff-patch.corerobin.com/#playground). diff --git a/docs/native-operations-v03.md b/docs/native-operations-v03.md index e5d0674..072be28 100644 --- a/docs/native-operations-v03.md +++ b/docs/native-operations-v03.md @@ -1,88 +1,85 @@ -# Native operations roadmap for 0.3 +# Controllable native operations -Version 0.2 established background execution, deterministic cross-platform -patches, malformed-input cleanup, and browser-side cancellation and limits. -The 0.3 native work will add the same production controls without changing the -existing `diff` and `patch` signatures. +Version 0.3 adds job-based Android and iOS operations for progress, cooperative +cancellation, resource limits, and atomic output. The original `diff` and +`patch` promise APIs remain source compatible and do not acquire implicit +limits. -This is an implementation contract. Public names remain provisional until the -0.3 beta, but the behavior and error model below are the acceptance criteria. +## Public API -## Public shape +```ts +import { startPatch } from 'react-native-bs-diff-patch'; -The existing promise APIs stay source compatible. A job API is added for -callers that need limits, cancellation, or progress: +const job = startPatch(oldPath, outputPath, patchPath, { + maxInputBytes: 64 * 1024 * 1024, + maxOutputBytes: 128 * 1024 * 1024, +}); -```ts -type NativeOperationOptions = { - maxInputBytes?: number; - maxOutputBytes?: number; -}; - -type NativeOperationProgress = { - id: string; - operation: 'diff' | 'patch'; - phase: 'reading' | 'processing' | 'writing'; - progress: number; // monotonic, from 0 through 1 -}; - -const job = startPatch(oldFile, newFile, patchFile, options); -const unsubscribe = job.onProgress((event) => updateUi(event.progress)); +const unsubscribe = job.onProgress(({ phase, progress }) => { + updateProgress({ phase, percent: Math.round(progress * 100) }); +}); + +try { + await job.result; +} finally { + unsubscribe(); +} + +// From a separate UI action: await job.cancel(); -await job.result; -unsubscribe(); ``` -`diff` and `patch` continue to use the same serialized native worker. They do -not acquire implicit size limits, so upgrading does not reject an operation -that previously succeeded. +`startDiff(oldPath, newPath, patchPath, options?)` has the same job shape. +`job.result` resolves to `0`; `job.cancel()` is idempotent from the caller's +perspective; and `job.onProgress()` returns an unsubscribe function. ## Resource limits -- Validate every numeric limit in JavaScript and native code as a positive, - safe integer. -- Check input file sizes before allocating operation buffers. -- Check the patch header before allocating restored output. -- Enforce generated-output limits while writing, not only after completion. -- Reject with `EINPUT_TOO_LARGE` or `EOUTPUT_TOO_LARGE`; include the configured - limit and observed byte count in native error metadata. +`maxInputBytes` and `maxOutputBytes` must be positive safe integers when +provided. Limits are per operation and have no library default because safe +values depend on device class and the host application's memory budget. -Limits are per operation. The package will not choose a universal default -because safe values depend on device class and the host application's memory -budget. +- `maxInputBytes` checks each native input before allocating operation buffers. +- `maxOutputBytes` checks a patch's declared restored size before allocation. +- Patch generation also checks its compressed output while it is being written. +- Limit failures reject with `EINPUT_TOO_LARGE` or `EOUTPUT_TOO_LARGE`. ## Cancellation and progress -Cancellation is cooperative. The shared C core receives a callback context and -checks it during file reads, suffix processing, compression/decompression, and -output writes. A cancelled operation rejects with `ECANCELLED` and never emits -another progress event. +Cancellation is cooperative. The shared C core checks it during file reads, +suffix processing, compression/decompression, and output writes. A cancelled +operation rejects with `ECANCELLED`, removes its temporary output, and emits no +later progress events. -Progress is phase based and monotonic. It is not an ETA: suffix sorting and -compression are data dependent. Native code rate-limits events to avoid -crossing the React Native bridge more than ten times per second. +Progress is phase based and monotonic, not an ETA. Events contain the job `id`, +the `diff` or `patch` operation, a `reading`, `processing`, or `writing` phase, +and a normalized value from 0 through 1. Native delivery is rate-limited to at +most ten events per second except at phase boundaries and completion. ## Atomic output -0.2 already removes an output created by a failed operation. In 0.3, job-based -operations strengthen this to an atomic commit: +Job operations write to an exclusively created sibling temporary file, flush +and validate it, then commit it at the destination. The destination must not +already exist, and failed, limited, or cancelled jobs do not expose a partial +output. Existing `diff` and `patch` keep their established behavior. + +## Platform behavior -1. create a unique sibling temporary file with exclusive creation; -2. write, flush, close, and validate the result; -3. rename the temporary file to the requested output path; -4. remove the temporary file on error or cancellation. +The job API is available on Android and iOS. React Native Web uses the binary +`diffBytes` and `patchBytes` APIs with an `AbortSignal` and byte limits instead; +calling `startDiff` or `startPatch` on Web rejects with `EUNSUPPORTED`. -The destination must not exist. Rename must stay on the same filesystem; the -library will not silently fall back to a copy. +The patch wire format remains `ENDSLEY/BSDIFF43`. Operation control changes +execution behavior, not patch compatibility. -## Delivery sequence +## Verification -1. Add cancellable/limited C stream callbacks and deterministic C tests. -2. Add Android and iOS job registries, event delivery, and cleanup tests. -3. Expose the TypeScript job facade while keeping `diff` and `patch` unchanged. -4. Run API 24 Android and iOS simulator cancellation/resource-limit tests. -5. Publish a 0.3 beta, validate registry consumers, then promote the stable - release without changing patch bytes. +The repository verifies the controls at three levels: -The patch wire format remains `ENDSLEY/BSDIFF43`; 0.3 changes operation control, -not compatibility. +1. deterministic C tests cover progress, limits, cancellation, destination + preservation, malformed input, and temporary-file cleanup; +2. Android API 24/31 and iOS Simulator tests invoke the public JavaScript job + facade through the New Architecture runtime; +3. React Native 0.73.11/0.74.7 compatibility fixtures compile the packaged + native APIs, while the full RN 0.86 example supplies the current-version + build and runtime gate. diff --git a/docs/platform-support.md b/docs/platform-support.md index fd6a8b7..7b22b14 100644 --- a/docs/platform-support.md +++ b/docs/platform-support.md @@ -6,16 +6,20 @@ | ------------------------------ | ------------------ | --------------------- | ------------------ | | File-path APIs | Yes | Yes | No | | Binary-data APIs | No | No | Yes | +| Progress / cooperative cancel | Native jobs | Native jobs | `AbortSignal` | +| Input / output limits | Native jobs | Native jobs | Binary options | | Legacy bridge | Yes | Yes | N/A | | TurboModule / New Architecture | Yes | Yes | N/A | -| Background execution | Serial executor | Serial dispatch queue | Module Web Worker | +| Background execution | Serial executor | Serialized worker | Module Web Worker | | Patch format | `ENDSLEY/BSDIFF43` | `ENDSLEY/BSDIFF43` | `ENDSLEY/BSDIFF43` | -The example application continuously exercises React Native 0.73.2. A direct -Android source-compatibility matrix compiles the New Architecture integration -against React Native 0.73.11, 0.74.7, and 0.86.0. The regular Android build also -compiles the 0.73 legacy architecture. These are tested versions, not a promise -that every intermediate or future React Native release is compatible. +The example application uses React Native 0.86.0 and exercises the New +Architecture on Android API 24/31 and iOS Simulator. Direct compatibility +fixtures compile the package against React Native 0.73.11 and 0.74.7; the 0.73 +fixture also preserves the legacy-architecture boundary, while the full example +provides the current RN 0.86 build gate. React Native 0.82 and newer are New +Architecture only. These are tested versions, not a promise that every +intermediate or future release is compatible. ## Android @@ -26,8 +30,9 @@ Native minor version: - React Native 0.74 and newer use `BaseReactPackage`. - Legacy architecture builds use the classic `ReactPackage` implementation. -Native operations run on a module-owned single-thread executor. The packaged C -code is built with CMake and invoked through JNI. +Native operations run on a module-owned single-thread executor. Jobs add a +registry for queued/active cancellation, rate-limited progress events, limits, +and cleanup. The packaged C code is built with CMake and invoked through JNI. ## iOS @@ -35,7 +40,11 @@ iOS autolinking registers `BsDiffPatch` for both architectures. New Architecture codegen maps the module through `modulesProvider`, and the module returns a generated TurboModule instance when `RCT_NEW_ARCH_ENABLED` is set. -Operations run on a dedicated serial dispatch queue rather than the main queue. +Module methods use a serial dispatch queue so a job is registered before a +following cancellation request is handled. Patch work runs asynchronously on a +separate serial worker queue, keeping the bridge responsive while preserving the +shared C library's single-operation boundary. The job registry is cancelled and +cleaned when the module is invalidated. ## React Native Web @@ -59,6 +68,8 @@ Web setup must preserve module-worker URLs in its Web serializer. The Web entry is browser-oriented rather than a Node.js filesystem adapter. It does not make the native file-path APIs available in Node.js. +Native job functions remain exported for a stable import shape but reject with +`EUNSUPPORTED` on Web. Calls without an `AbortSignal` share a module Worker and initialized WebAssembly module. Calls with a signal receive a dedicated Worker so diff --git a/docs/recipes.md b/docs/recipes.md index d7c6058..4a621db 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -75,6 +75,7 @@ the input or output size. Before starting an operation: - reject input larger than the product's tested limit; - confirm sufficient local storage for native temporary outputs; - prevent unbounded simultaneous calls from user actions; +- use `startDiff`/`startPatch` to enforce native byte limits and expose Cancel; - pass an `AbortSignal` to Web operations when cancellation is appropriate; - move very large update generation to controlled backend infrastructure. @@ -104,6 +105,32 @@ try { } ``` +The equivalent native control flow uses a job: + +```ts +const job = startPatch(oldPath, outputPath, patchPath, { + maxInputBytes: 64 * 1024 * 1024, + maxOutputBytes: 128 * 1024 * 1024, +}); +const unsubscribe = job.onProgress(renderProgress); + +try { + await job.result; +} catch (error) { + if (isPatchError(error) && error.code === 'ECANCELLED') return; + if ( + isPatchError(error) && + ['EINPUT_TOO_LARGE', 'EOUTPUT_TOO_LARGE'].includes(error.code || '') + ) { + // Show the native size-limit guidance. + return; + } + throw error; +} finally { + unsubscribe(); +} +``` + ## Download a Web patch Create an object URL, trigger the download, and revoke the URL after use: diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index c117416..0e40da9 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -8,6 +8,7 @@ Start with the error `code`, then confirm the runtime and input model: | `ENOENT`, `EEXIST`, `EINVAL` | Inspect path state before starting native work | | `EUNSUPPORTED` | Confirm that the correct API family was selected | | `EABORTED`, `ERESOURCE` | Check Web cancellation and byte limits | +| `ECANCELLED`, native limit codes | Check native job cancellation and configured bounds | | `EDIFF`, `EPATCH` | Check native I/O and patch integrity | | Worker or `EWEBASSEMBLY` failure | Check emitted Web assets, CSP, and patch magic | | High memory use | Enforce input and concurrency limits | @@ -55,6 +56,13 @@ means an input, generated patch, or declared restored output exceeded the configured byte limit. Both are expected control-flow errors rather than a WebAssembly failure. +## `ECANCELLED`, `EINPUT_TOO_LARGE`, or `EOUTPUT_TOO_LARGE` + +These are expected native job outcomes. `ECANCELLED` confirms the cooperative +cancel request reached a queued or active job. The two limit codes identify +which configured native boundary was exceeded. Use a fresh destination before +retrying; a failed job does not commit a partial output. + ## Worker failed to load Confirm the bundler emits module-worker assets and that the deployed server @@ -78,9 +86,10 @@ remove any partial output owned by that operation. ## High memory use -The algorithm and adapters operate on complete in-memory buffers. Add a size -check before calling the library and avoid accepting arbitrary large untrusted -files. Web execution is off-main-thread but still consumes the tab's memory. +The algorithm and adapters operate on complete in-memory buffers. Use native job +limits or add an application check before calling the legacy API, and avoid +accepting arbitrary large untrusted files. Web execution is off-main-thread but +still consumes the tab's memory. Unsignalled Web operations queue through a shared Worker. Signalled operations use dedicated Workers for isolated cancellation. Debounce repeated user actions diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md index f1d6b75..4a9d7d7 100644 --- a/docs/zh-CN/README.md +++ b/docs/zh-CN/README.md @@ -23,7 +23,7 @@ - [生产实践](/docs/zh-CN/recipes/) — 完整性、清理、下载与跨运行时流程。 - [平台支持](/docs/zh-CN/platform-support/) — 架构与打包器行为。 - [架构](/docs/zh-CN/architecture/) — 执行路径与补丁兼容性。 -- [0.3 原生操作](/docs/zh-CN/native-operations-v03/) — 资源限制、取消、进度与 +- [可控制的原生操作](/docs/zh-CN/native-operations-v03/) — 资源限制、取消、进度与 原子输出约定。 - [常见问题与排障](/docs/zh-CN/troubleshooting/) — 常见集成失败。 - [开发与验证](/docs/zh-CN/development/) — 本地构建、测试、WASM 与发布检查。 diff --git a/docs/zh-CN/api-reference.md b/docs/zh-CN/api-reference.md index c912c01..0ae7c03 100644 --- a/docs/zh-CN/api-reference.md +++ b/docs/zh-CN/api-reference.md @@ -7,6 +7,8 @@ import { diff, patch, + startDiff, + startPatch, diffBytes, patchBytes, type BinaryInput, @@ -49,6 +51,51 @@ function patch( - `patchFile`:已存在且兼容的补丁路径。 - 成功时返回 `0`,不会覆盖已有输出文件。 +## `startDiff` 与 `startPatch` + +```ts +interface NativeOperationOptions { + maxInputBytes?: number; + maxOutputBytes?: number; +} + +interface NativeOperationProgress { + id: string; + operation: 'diff' | 'patch'; + phase: 'reading' | 'processing' | 'writing'; + progress: number; +} + +interface NativeOperationJob { + id: string; + result: Promise; + cancel(): Promise; + onProgress(listener: (event: NativeOperationProgress) => void): () => void; +} + +function startDiff( + oldFile: string, + newFile: string, + patchFile: string, + options?: NativeOperationOptions +): NativeOperationJob; + +function startPatch( + oldFile: string, + outputFile: string, + patchFile: string, + options?: NativeOperationOptions +): NativeOperationJob; +``` + +Android、iOS 需要进度、取消或资源边界时使用 job API。 + +- `result` 成功时返回 `0`,失败时按原生错误码拒绝。 +- `cancel()` 请求协作式取消;取消后的结果以 `ECANCELLED` 拒绝。 +- `onProgress()` 只转发当前 job 的事件,并返回取消订阅函数。 +- 原生限制在传入时必须是正安全整数。 +- job 失败会清理同目录临时输出,且不会覆盖已有目标文件。 + ## `diffBytes` ```ts @@ -102,12 +149,13 @@ function patchBytes( 拒绝。 原生端的二进制 API 接受 options 参数只是为了让共享封装保持源码兼容,随后仍会以 -`EUNSUPPORTED` 拒绝。原生资源策略由应用的文件系统与外围流程负责。 +`EUNSUPPORTED` 拒绝。原生路径操作通过 `startDiff`、`startPatch` 获得同类控制。 ## 平台不可用时的行为 -四个函数始终导出,以便共享代码保持稳定导入形式。在原生端调用 `diffBytes` 或 -`patchBytes`、在 Web 调用 `diff` 或 `patch`,都会以 `EUNSUPPORTED` 拒绝。 +所有函数始终导出,以便共享代码保持稳定导入形式。在原生端调用 `diffBytes` 或 +`patchBytes`,以及在 Web 调用 `diff`、`patch`、`startDiff` 或 `startPatch`, +都会以 `EUNSUPPORTED` 拒绝。 SSR 阶段导入 Web 入口不会启动 Worker;在没有浏览器 Worker 的环境调用二进制 API 会以 `EUNSUPPORTED` 拒绝。 @@ -127,6 +175,9 @@ type PatchError = Error & { code?: string }; | `EEXIST` | 原生端输出路径已经存在。 | | `EUNSUPPORTED` | 当前平台不支持所选 API。 | | `EUNAVAILABLE` | 原生模块工作队列已经关闭。 | +| `ECANCELLED` | 原生 job 被协作式取消。 | +| `EINPUT_TOO_LARGE` | 原生输入超过 `maxInputBytes`。 | +| `EOUTPUT_TOO_LARGE` | 原生生成或还原输出超过配置上限。 | | `EABORTED` | Web 操作被传入的 signal 取消。 | | `ERESOURCE` | 超过配置的 Web 输入或输出字节上限。 | | `EDIFF` | 原生 diff 核心拒绝输入或无法写入补丁。 | @@ -138,11 +189,12 @@ type PatchError = Error & { code?: string }; ## 并发与顺序 -每个原生平台使用库内部的串行队列。不带 signal 的 Web 调用共用一个模块 Worker、 +每个原生平台的 Promise 与 job 操作共用库内部串行队列。取消排队任务会阻止它进入 +C 核心;运行中的任务会协作式观察取消。不带 signal 的 Web 调用共用一个模块 Worker、 串行请求队列和已缓存的 WebAssembly 模块;带 signal 的调用使用专用 Worker,确保 取消仅影响当前操作。对大输入仍应设置应用级并发和内存预算。 ## 补丁格式 -四个操作都读写 `ENDSLEY/BSDIFF43` 补丁。以 `BSDIFF40` 开头的其他 bsdiff +所有操作都读写 `ENDSLEY/BSDIFF43` 补丁。以 `BSDIFF40` 开头的其他 bsdiff 变体不能互换。 diff --git a/docs/zh-CN/architecture.md b/docs/zh-CN/architecture.md index 9657f0d..417091d 100644 --- a/docs/zh-CN/architecture.md +++ b/docs/zh-CN/architecture.md @@ -8,7 +8,7 @@ React Native JavaScript -> 强类型公开 API -> TurboModule 或旧桥接 - -> 平台自有串行工作队列 + -> 平台 job registry 与串行操作边界 -> JNI / Objective-C++ -> 共用 bsdiff + bzip2 C 源码 @@ -22,6 +22,9 @@ React Native Web Worker 边界让高开销二进制计算离开 JavaScript / UI 线程,但不会消除算法成本。 调用方仍需设置符合产品场景的输入大小和时间限制。 +原生 job 会把取消与进度回调传入 C stream。输出先写入独占创建的同目录临时文件, +只有操作成功刷新后才提交。旧 Promise API 共用相同串行边界,但不会自动增加限制。 + 未传 `AbortSignal` 的 Web 调用复用一个 Worker 和已缓存的 Emscripten 模块,避免 重复初始化 Worker 与 WebAssembly。带 signal 的调用使用专用 Worker,取消 signal 只会终止该 Worker。每个 Worker 在自己的队列内串行执行,并在每次操作后删除 @@ -100,8 +103,8 @@ bsdiff 和 bzip2 源码,从而保持跨平台兼容。 的 50 MiB fixture 中,原生参考峰值约为输入的十九倍,主要来自后缀数组和同时存在的 文件缓冲区。 -对于大更新,应在调用前执行应用级大小限制。完整文件无法安全放入内存时,应考虑 -服务端或流式更新策略。 +对于大更新,应配置原生/Web 操作限制(或在调用旧 API 前检查)。完整文件无法安全 +放入内存时,应考虑服务端或流式更新策略。 ## 职责边界 @@ -110,7 +113,7 @@ bsdiff 和 bzip2 源码,从而保持跨平台兼容。 - 文件选择、存储权限和临时文件清理; - 补丁传输与缓存策略; - 来源认证与密码学完整性校验; -- 并发、大小和时间限制; +- 选择并发、大小与时间策略,并传入平台支持的限制; - 还原结果验证和原子替换。 这些职责保留在补丁引擎外,便于应用复用已有文件系统和发布信任模型。 diff --git a/docs/zh-CN/development.md b/docs/zh-CN/development.md index 8f6f41b..3dd3894 100644 --- a/docs/zh-CN/development.md +++ b/docs/zh-CN/development.md @@ -2,9 +2,10 @@ ## 前置条件 -- Node.js 18 或更高版本。 +- Node.js 20.19.4 或更高版本(CI 使用 Node 22)。 - 通过仓库检入版本使用 Yarn 3.6.1。 -- Android 开发需要 Android Studio / JDK 17。 +- Android 开发需要 Android Studio / JDK 17;RN 0.73/0.74 兼容 fixture 固定使用 + Gradle 8.3(本地可通过 `GRADLE_EXECUTABLE` 指定)。 - iOS 开发需要 Xcode 和 CocoaPods。 - 只有重新生成已检入 WebAssembly bundle 时才需要 Emscripten。 @@ -23,6 +24,7 @@ yarn prepare yarn typecheck yarn lint yarn test --runInBand +yarn test:native-operations ``` ## Web 门禁 @@ -44,17 +46,21 @@ yarn test:package ```sh FUZZ_RUNS=2000 yarn test:fuzz +scripts/test-rn-android-compatibility.sh 0.73.11 old scripts/test-rn-android-compatibility.sh 0.73.11 new scripts/test-rn-android-compatibility.sh 0.74.7 new scripts/test-rn-android-compatibility.sh 0.86.0 new -scripts/test-rn-ios-compatibility.sh 0.73.11 -scripts/test-rn-ios-compatibility.sh 0.74.7 -scripts/test-rn-ios-compatibility.sh 0.86.0 +scripts/test-rn-ios-compatibility.sh 0.73.11 old +scripts/test-rn-ios-compatibility.sh 0.73.11 new +scripts/test-rn-ios-compatibility.sh 0.74.7 new +scripts/test-rn-ios-compatibility.sh 0.86.0 new ``` 本地 Clang runtime 支持时,fuzz 门禁使用 libFuzzer、AddressSanitizer 与 UndefinedBehaviorSanitizer;否则运行确定性 sanitizer 语料。兼容 fixture 会使用所选 React Native artifact 直接编译真实 Android 模块源码,不依赖源码文本断言。 +`test:native-operations` 会确定性覆盖 job 进度、取消、限制、畸形补丁、原子目标行为 +和临时文件清理。 可复现的 Web 性能基准命令: @@ -75,10 +81,14 @@ CI,不是发版门禁。可用 `yarn test:registry:vite` 和 `yarn test:regist Dependabot 会把常规 npm、Ruby 与 Actions 更新分组,控制评审数量;对于 API 兼容 的叶子依赖,锁文件会直接固定到已修复版本。 -旧版开发工具链目前仍有三类仅开发期使用、不能安全强制升级的传递依赖: -`fast-xml-parser` 的修复版本超出 CLI 12 的 major 范围,`tar` 的修复版本超出父依赖 -的 major 范围,而 `ip` 的一个告警尚无已修复版本。它们应继续保留在 GitHub alerts -中;待 0.73 示例与 Jest 工具链退役,或父依赖提供兼容升级后移除。 +```sh +yarn npm audit --all --recursive +``` + +示例与根工具链已升级到 React Native 0.86、CLI 20.2 与 release-it 20。升级后有 +漏洞的 `tmp`、`ip` 链已不再安装;锁文件固定了已修复的 `tar`、 +`fast-xml-parser`、`socks` 以及审计指出且 API 兼容的叶子覆盖。依赖变更后仍应 +检查 GitHub Dependabot alerts,不能只根据 lockfile override 推断告警已经关闭。 ## 站点与文档 @@ -106,11 +116,13 @@ yarn test:web:browser ## 原生验证 -Android CI 构建两种架构模式,使用 React Native 0.73.11、0.74.7 与 0.86.0 -直接编译新架构源码,并在模拟器矩阵执行新架构设备级往返测试。iOS CI 也针对三 -个 React Native 版本编译 Pod,再使用示例 Gemfile 锁定的 CocoaPods 版本测试旧 -架构和新架构。模拟器除跨平台 golden patch 与损坏补丁拒绝外,还会断言实际启用 -的架构模式。 +Android CI 编译旧架构边界与新架构源码,并在 pull request 中通过 RN 0.86 新架构 +运行 API 24、31 设备测试。iOS 会编译 Pod 兼容 fixture,并在 Simulator 运行 +RN 0.86 新架构;React Native 0.82 及以上已不再提供旧架构运行时。设备测试会断言 +实际架构、跨平台 golden patch、损坏补丁拒绝、job 进度、取消、限制和输出清理。 + +`native-benchmark.yml` 属于手动与定时基础设施,只上传 Linux/macOS JSON 基线; +共享 Runner 波动较大,因此不会阻塞 pull request。 本地示例命令见仓库 [CONTRIBUTING.md](https://github.com/JimmyDaddy/react-native-bs-diff-patch/blob/main/CONTRIBUTING.md)。 diff --git a/docs/zh-CN/getting-started.md b/docs/zh-CN/getting-started.md index e7d68e6..da55899 100644 --- a/docs/zh-CN/getting-started.md +++ b/docs/zh-CN/getting-started.md @@ -21,7 +21,7 @@ React Native autolinking 会完成 Android 与 iOS 注册。安装后必须重 | 运行时 | 应使用 | 不应使用 | | ------------ | --------------------------- | -------------- | -| Android、iOS | `diff` 和 `patch` | 二进制数据 API | +| Android、iOS | `diff`/`patch` 或原生 job | 二进制数据 API | | Web | `diffBytes` 和 `patchBytes` | 文件路径 API | 不可用的 API 族会以 `EUNSUPPORTED` 拒绝,便于发现入口解析到了错误平台。 @@ -71,6 +71,37 @@ export async function nativeRoundTrip({ 使用文件系统层提供的内容哈希或字节比较验证 `restoredPath` 与 `newFilePath` 一致。 补丁和还原文件不再需要时应及时清理。 +## 添加原生进度、取消与限制 + +由用户控制操作,或处理不可信输入时,应使用 job: + +```ts +import { startPatch } from 'react-native-bs-diff-patch'; + +const job = startPatch(oldFilePath, restoredPath, patchPath, { + maxInputBytes: 64 * 1024 * 1024, + maxOutputBytes: 128 * 1024 * 1024, +}); + +const unsubscribe = job.onProgress(({ phase, progress }) => { + setOperationState({ phase, percent: Math.round(progress * 100) }); +}); + +try { + await job.result; +} catch (error) { + if ((error as { code?: string }).code !== 'ECANCELLED') throw error; +} finally { + unsubscribe(); +} + +// 在 result 等待期间,将其绑定到页面的“取消”操作。 +cancelButton.onPress = () => void job.cancel(); +``` + +`startDiff` 接受相同选项。原生 job 通过同目录临时文件写入,因此取消或限制失败 +不会暴露不完整的目标文件。 + ## Web 二进制流程 React Native Web 使用二进制值而不是文件路径: @@ -132,5 +163,6 @@ Web API 只能在客户端调用。在 SSR 阶段导入不会创建 Worker,但 - 从[生产实践](/docs/zh-CN/recipes/)复制错误恢复模式。 - 在 [API 参考](/docs/zh-CN/api-reference/)中查看全部签名和错误码。 +- 阅读[可控制的原生操作](/docs/zh-CN/native-operations-v03/)。 - 确认[平台与打包器支持](/docs/zh-CN/platform-support/)。 - 尝试[在线 Playground](https://bs-dff-patch.corerobin.com/#playground)。 diff --git a/docs/zh-CN/native-operations-v03.md b/docs/zh-CN/native-operations-v03.md index e13ceda..1b5451c 100644 --- a/docs/zh-CN/native-operations-v03.md +++ b/docs/zh-CN/native-operations-v03.md @@ -1,73 +1,76 @@ -# 0.3 原生操作路线图 +# 可控制的原生操作 -0.2 已完成后台执行、跨平台确定性补丁、畸形输入清理,以及浏览器端的取消和资源 -限制。0.3 会在不改变现有 `diff`、`patch` 签名的前提下,为原生端补齐同类生产控制。 +0.3 为 Android 与 iOS 增加了 job API,用于监听进度、协作式取消、限制资源并 +原子提交输出。原有 `diff`、`patch` Promise API 保持源码兼容,也不会自动增加 +默认限制。 -本文是实现约定。公开命名在 0.3 beta 前仍可调整,但以下行为和错误模型是验收条件。 +## 公共 API -## 公开接口形态 +```ts +import { startPatch } from 'react-native-bs-diff-patch'; -现有 Promise API 保持源码兼容;需要限制、取消或进度的调用方使用新增 job API: +const job = startPatch(oldPath, outputPath, patchPath, { + maxInputBytes: 64 * 1024 * 1024, + maxOutputBytes: 128 * 1024 * 1024, +}); -```ts -type NativeOperationOptions = { - maxInputBytes?: number; - maxOutputBytes?: number; -}; - -type NativeOperationProgress = { - id: string; - operation: 'diff' | 'patch'; - phase: 'reading' | 'processing' | 'writing'; - progress: number; // 从 0 到 1,单调递增 -}; - -const job = startPatch(oldFile, newFile, patchFile, options); -const unsubscribe = job.onProgress((event) => updateUi(event.progress)); +const unsubscribe = job.onProgress(({ phase, progress }) => { + updateProgress({ phase, percent: Math.round(progress * 100) }); +}); + +try { + await job.result; +} finally { + unsubscribe(); +} + +// 可在单独的 UI 操作中调用: await job.cancel(); -await job.result; -unsubscribe(); ``` -`diff` 和 `patch` 继续使用同一个串行原生 worker,且不会隐式增加大小上限,因此升级 -不会让原本成功的操作突然被拒绝。 +`startDiff(oldPath, newPath, patchPath, options?)` 返回相同结构。`job.result` +成功时返回 `0`,`job.cancel()` 对调用方可重复执行,`job.onProgress()` 返回取消 +订阅函数。 ## 资源限制 -- JavaScript 与原生端都要把每个限制校验为正的安全整数。 -- 分配操作缓冲区前检查输入文件大小。 -- 分配还原输出前检查补丁头声明的长度。 -- 生成输出时持续执行限制,而不是完成后才检查。 -- 超限分别拒绝为 `EINPUT_TOO_LARGE` 或 `EOUTPUT_TOO_LARGE`,原生错误元数据包含 - 配置上限和实际字节数。 +传入的 `maxInputBytes`、`maxOutputBytes` 必须是正安全整数。限制只作用于当前任务; +库不设置统一默认值,因为安全范围取决于设备级别和宿主应用的内存预算。 -限制按操作设置。库不提供通用默认值,因为安全值取决于设备档位和宿主应用的内存预算。 +- `maxInputBytes` 会在分配操作缓冲区前检查每个原生输入。 +- `maxOutputBytes` 会在分配还原缓冲区前检查补丁声明的输出大小。 +- 生成补丁时也会在写入压缩结果的过程中检查输出上限。 +- 超过限制分别以 `EINPUT_TOO_LARGE`、`EOUTPUT_TOO_LARGE` 拒绝。 ## 取消与进度 -取消采用协作式检查。共用 C 核心接收回调上下文,并在文件读取、后缀处理、压缩与 -解压、输出写入阶段检查取消状态。取消后以 `ECANCELLED` 拒绝,且不再发送进度事件。 +取消采用协作式机制。共用 C 核心会在读取文件、后缀处理、压缩/解压和输出写入时 +检查取消状态。取消后的操作以 `ECANCELLED` 拒绝、删除临时输出,并且不会继续发送 +进度事件。 -进度按阶段计算并单调递增,但不是 ETA:后缀排序和压缩耗时受数据影响。原生端把 -事件频率限制为每秒最多十次,避免频繁跨越 React Native 边界。 +进度按阶段单调递增,并不代表 ETA。事件包含任务 `id`、`diff` 或 `patch` 操作、 +`reading`、`processing`、`writing` 阶段,以及 0 到 1 的归一化进度。除阶段切换与 +完成事件外,原生端最多每秒发送十次。 ## 原子输出 -0.2 已保证失败时删除本次创建的输出。0.3 的 job 操作进一步使用原子提交: +job 操作会独占创建同目录临时文件,完成写入、刷新与校验后再提交到目标路径。目标 +文件不能已经存在;失败、命中限制或取消的任务都不会暴露半成品。原有 `diff`、 +`patch` 保持既有行为。 + +## 平台差异 -1. 在目标文件同目录以排他模式创建唯一临时文件; -2. 写入、flush、关闭并验证结果; -3. 将临时文件 rename 为目标路径; -4. 出错或取消时删除临时文件。 +job API 仅用于 Android 与 iOS。React Native Web 应使用二进制 `diffBytes`、 +`patchBytes` API,并通过 `AbortSignal` 与字节限制控制任务;Web 调用 `startDiff` +或 `startPatch` 会以 `EUNSUPPORTED` 拒绝。 -目标文件必须不存在;rename 必须发生在同一文件系统,库不会静默退化成复制。 +补丁格式仍是 `ENDSLEY/BSDIFF43`。0.3 改变的是操作控制,不是补丁兼容性。 -## 交付顺序 +## 验证范围 -1. 为 C stream 增加可取消、可限制的回调和确定性 C 测试。 -2. 增加 Android/iOS job registry、事件发送与清理测试。 -3. 暴露 TypeScript job facade,同时保持 `diff`、`patch` 不变。 -4. 在 Android API 24 与 iOS 模拟器执行取消和资源限制运行时测试。 -5. 发布 0.3 beta,验证 registry 消费者,再发布稳定版且不改变补丁字节。 +仓库从三个层级验证这些能力: -补丁格式仍是 `ENDSLEY/BSDIFF43`;0.3 改变的是操作控制,不是兼容性。 +1. 确定性 C 测试覆盖进度、限制、取消、目标文件保护、畸形输入与临时文件清理; +2. Android API 24/31 与 iOS Simulator 通过新架构运行时调用公共 JavaScript job API; +3. React Native 0.73.11/0.74.7 兼容 fixture 编译发布包原生 API,完整 RN 0.86 + 示例负责当前版本的构建与运行时门禁。 diff --git a/docs/zh-CN/platform-support.md b/docs/zh-CN/platform-support.md index 6632d46..0ecb210 100644 --- a/docs/zh-CN/platform-support.md +++ b/docs/zh-CN/platform-support.md @@ -6,14 +6,18 @@ | -------------------- | ------------------ | -------- | ---------------- | | 文件路径 API | 支持 | 支持 | 不支持 | | 二进制数据 API | 不支持 | 不支持 | 支持 | +| 进度 / 协作式取消 | 原生 job | 原生 job | `AbortSignal` | +| 输入 / 输出限制 | 原生 job | 原生 job | 二进制 options | | 旧桥接架构 | 支持 | 支持 | 不适用 | | TurboModule / 新架构 | 支持 | 支持 | 不适用 | -| 后台执行 | 串行 executor | 串行队列 | 模块 Web Worker | +| 后台执行 | 串行 executor | 串行 worker | 模块 Web Worker | | 补丁格式 | `ENDSLEY/BSDIFF43` | 同左 | 同左 | -示例应用持续验证 React Native 0.73.2。Android 源码兼容矩阵会直接使用 React -Native 0.73.11、0.74.7 与 0.86.0 编译新架构集成;常规 Android 构建还会编译 -0.73 旧架构。这些是已测试版本,并不承诺所有中间版本或未来版本必然兼容。 +示例应用使用 React Native 0.86.0,并在 Android API 24/31 与 iOS Simulator +验证新架构运行时。直接兼容 fixture 会针对 React Native 0.73.11、0.74.7 编译 +发布包,其中 0.73 fixture 还保留旧架构边界;完整示例负责当前 RN 0.86 构建门禁。 +React Native 0.82 及以上只支持新架构。这些是已测试版本,并不承诺所有中间版本或 +未来版本必然兼容。 ## Android @@ -23,8 +27,8 @@ Android 根据 React Native 次版本选择新架构包实现: - React Native 0.74 及以上使用 `BaseReactPackage`。 - 旧架构构建使用传统 `ReactPackage` 实现。 -原生操作运行在模块自有的单线程 executor 上。项目内置 C 代码通过 CMake 构建, -并由 JNI 调用。 +原生操作运行在模块自有的单线程 executor 上。job registry 负责排队/运行中取消、 +限频进度、资源限制和清理。项目内置 C 代码通过 CMake 构建,并由 JNI 调用。 ## iOS @@ -32,7 +36,9 @@ iOS autolinking 会为两种架构注册 `BsDiffPatch`。新架构 codegen 通 `modulesProvider` 映射模块;设置 `RCT_NEW_ARCH_ENABLED` 后,模块返回生成的 TurboModule 实例。 -操作在专用串行 dispatch queue 中运行,不占用主队列。 +模块方法使用串行 dispatch queue,确保后续取消请求到达时 job 已经登记。耗时补丁 +操作会异步进入独立的串行工作队列,在保持桥接响应的同时维护共享 C 核心的单操作 +边界。模块失效时会取消并清理 job registry。 ## React Native Web @@ -56,6 +62,7 @@ Webpack 与 Vite 能识别标准的 Web 入口面向浏览器,不是 Node.js 文件系统适配器;它不会在 Node.js 中提供原生 文件路径 API。 +原生 job 函数仍会导出以保持统一导入形式,但在 Web 上以 `EUNSUPPORTED` 拒绝。 未传 `AbortSignal` 的调用共用模块 Worker 与已初始化的 WebAssembly 模块;带 signal 的调用使用专用 Worker,保证取消只影响当前任务。两种路径都会在各自 Worker diff --git a/docs/zh-CN/recipes.md b/docs/zh-CN/recipes.md index 2f6b192..4d779d4 100644 --- a/docs/zh-CN/recipes.md +++ b/docs/zh-CN/recipes.md @@ -66,6 +66,7 @@ try { - 拒绝超过产品验证上限的输入; - 确认原生端临时输出所需的本地空间; - 防止用户操作触发无限并发; +- 使用 `startDiff`/`startPatch` 限制原生字节数并提供取消入口; - Web 操作需要取消时传入 `AbortSignal`; - 将超大更新的生成工作放到受控后端基础设施。 @@ -94,6 +95,32 @@ try { } ``` +原生端对应流程使用 job: + +```ts +const job = startPatch(oldPath, outputPath, patchPath, { + maxInputBytes: 64 * 1024 * 1024, + maxOutputBytes: 128 * 1024 * 1024, +}); +const unsubscribe = job.onProgress(renderProgress); + +try { + await job.result; +} catch (error) { + if (isPatchError(error) && error.code === 'ECANCELLED') return; + if ( + isPatchError(error) && + ['EINPUT_TOO_LARGE', 'EOUTPUT_TOO_LARGE'].includes(error.code || '') + ) { + // 展示原生端大小限制说明。 + return; + } + throw error; +} finally { + unsubscribe(); +} +``` + ## 下载 Web 补丁 创建对象 URL、触发下载,并在使用后释放 URL: diff --git a/docs/zh-CN/troubleshooting.md b/docs/zh-CN/troubleshooting.md index b6cf2c2..cd66fc8 100644 --- a/docs/zh-CN/troubleshooting.md +++ b/docs/zh-CN/troubleshooting.md @@ -8,6 +8,7 @@ | `ENOENT`、`EEXIST`、`EINVAL` | 在原生任务开始前检查路径状态 | | `EUNSUPPORTED` | 确认选择了当前平台对应的 API 族 | | `EABORTED`、`ERESOURCE` | 检查 Web 取消与配置的字节上限 | +| `ECANCELLED`、原生限制错误码 | 检查原生 job 取消与配置边界 | | `EDIFF`、`EPATCH` | 检查原生 I/O 与补丁完整性 | | Worker 或 `EWEBASSEMBLY` 失败 | 检查 Web 资源、CSP 和补丁 magic | | 内存占用过高 | 设置输入大小和并发限制 | @@ -50,6 +51,12 @@ `ERESOURCE` 表示输入、生成补丁或补丁声明的还原输出超过配置的字节上限。二者是预期 控制流错误,并不表示 WebAssembly 损坏。 +## `ECANCELLED`、`EINPUT_TOO_LARGE` 或 `EOUTPUT_TOO_LARGE` + +这些是原生 job 的预期结果。`ECANCELLED` 表示排队中或运行中的任务已经收到协作式 +取消;两个限制错误码用于区分命中的原生边界。重试前使用新的目标路径;失败 job 不会 +提交半成品输出。 + ## Worker 加载失败 确认打包器输出了模块 Worker 资源,并且服务器将 `.mjs` 作为 JavaScript 提供。 @@ -69,8 +76,8 @@ ## 内存占用过高 -算法和适配器处理完整内存缓冲区。调用前添加大小限制,不要接受任意大的不可信文件。 -Web 虽在主线程外执行,仍会消耗当前标签页内存。 +算法和适配器处理完整内存缓冲区。使用原生 job 限制,或在调用旧 API 前自行检查, +不要接受任意大的不可信文件。Web 虽在主线程外执行,仍会消耗当前标签页内存。 不带 signal 的 Web 操作在共享 Worker 中排队;带 signal 的操作使用专用 Worker, 以实现隔离取消。对重复用户操作进行防抖;大任务可能重叠时设置应用级预算。 diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index ce2f32a..5e3fa92 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -27,6 +27,9 @@ react { // A list containing the node command and its flags. Default is just 'node'. // nodeExecutableAndArgs = ["node"] // + + /* Autolinking */ + autolinkLibrariesWithApp() // The command to run when bundling. By default is 'bundle' // bundleCommand = "ram-bundle" // @@ -68,7 +71,7 @@ def enableProguardInReleaseBuilds = false * give correct results when using with locales other than en-US. Note that * this variant is about 6MiB larger per architecture than default. */ -def jscFlavor = 'org.webkit:android-jsc:+' +def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+' android { ndkVersion rootProject.ext.ndkVersion @@ -110,8 +113,6 @@ android { dependencies { // The version of react-native is set by the React Native Gradle Plugin implementation("com.facebook.react:react-android") - implementation("com.facebook.react:flipper-integration") - androidTestImplementation("androidx.test:runner:1.5.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") @@ -122,5 +123,3 @@ dependencies { implementation jscFlavor } } - -apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) diff --git a/example/android/app/src/androidTest/java/com/bsdiffpatchexample/NewArchitectureRuntimeTest.kt b/example/android/app/src/androidTest/java/com/bsdiffpatchexample/NewArchitectureRuntimeTest.kt index eafb8f0..8cdefe0 100644 --- a/example/android/app/src/androidTest/java/com/bsdiffpatchexample/NewArchitectureRuntimeTest.kt +++ b/example/android/app/src/androidTest/java/com/bsdiffpatchexample/NewArchitectureRuntimeTest.kt @@ -33,6 +33,7 @@ class NewArchitectureRuntimeTest { while (SystemClock.uptimeMillis() < deadline) { try { onView(withText("Runtime: success")).check(matches(isDisplayed())) + onView(withText("Controls: success")).check(matches(isDisplayed())) return } catch (error: NoMatchingViewException) { lastFailure = error @@ -44,7 +45,7 @@ class NewArchitectureRuntimeTest { } throw AssertionError( - "The React Native diff/patch round trip did not complete successfully", + "The React Native round trip and native operation controls did not complete successfully", lastFailure ) } diff --git a/example/android/app/src/main/java/com/bsdiffpatchexample/MainApplication.kt b/example/android/app/src/main/java/com/bsdiffpatchexample/MainApplication.kt index b2573f9..5e1dff6 100644 --- a/example/android/app/src/main/java/com/bsdiffpatchexample/MainApplication.kt +++ b/example/android/app/src/main/java/com/bsdiffpatchexample/MainApplication.kt @@ -1,45 +1,22 @@ package com.bsdiffpatchexample -import android.content.res.Configuration import android.app.Application import com.facebook.react.PackageList import com.facebook.react.ReactApplication import com.facebook.react.ReactHost -import com.facebook.react.ReactNativeHost -import com.facebook.react.ReactPackage -import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load +import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost -import com.facebook.react.defaults.DefaultReactNativeHost -import com.facebook.react.flipper.ReactNativeFlipper -import com.facebook.soloader.SoLoader class MainApplication : Application(), ReactApplication { - override val reactNativeHost: ReactNativeHost = - object : DefaultReactNativeHost(this) { - override fun getPackages(): List = - PackageList(this).packages.apply { - // Packages that cannot be autolinked yet can be added manually here, for example: - // add(MyReactNativePackage()) - } - - override fun getJSMainModuleName(): String = "index" - - override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG - - override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED - override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED - } - - override val reactHost: ReactHost - get() = getDefaultReactHost(this.applicationContext, reactNativeHost) + override val reactHost: ReactHost by lazy { + getDefaultReactHost( + context = applicationContext, + packageList = PackageList(this).packages, + ) + } override fun onCreate() { super.onCreate() - SoLoader.init(this, false) - if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { - // If you opted-in for the New Architecture, we load the native entry point for this app. - load() - } - ReactNativeFlipper.initializeFlipper(this, reactNativeHost.reactInstanceManager) + loadReactNative(this) } } diff --git a/example/android/build.gradle b/example/android/build.gradle index cb9d623..dad99b0 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,11 +1,11 @@ buildscript { ext { - buildToolsVersion = "34.0.0" - minSdkVersion = 21 - compileSdkVersion = 34 - targetSdkVersion = 34 - ndkVersion = "25.1.8937393" - kotlinVersion = "1.8.0" + buildToolsVersion = "36.0.0" + minSdkVersion = 24 + compileSdkVersion = 36 + targetSdkVersion = 36 + ndkVersion = "27.1.12297006" + kotlinVersion = "2.1.20" } repositories { google() diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 9d1070c..0f404d9 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -21,9 +21,6 @@ org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m # Android operating system, and which are packaged with your app's APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true -# Automatically convert third-party libraries to use AndroidX -android.enableJetifier=true - # Use this property to specify which architecture you want to build. # You can also override it from the CLI using # ./gradlew -PreactNativeArchitectures=x86_64 @@ -34,8 +31,11 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 # your application. You should enable this flag either if you want # to write custom TurboModules/Fabric components OR use libraries that # are providing them. -newArchEnabled=false +newArchEnabled=true # Use this property to enable or disable the Hermes JS engine. # If set to false, you will be using JSC instead. -hermesEnabled=false +hermesEnabled=true + +# Keep edge-to-edge rendering opt-in for the compatibility example. +edgeToEdgeEnabled=false diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index d11cdd9..37f78a6 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/example/android/settings.gradle b/example/android/settings.gradle index 8755ef9..67405ba 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -1,4 +1,9 @@ +pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") } +plugins { id("com.facebook.react.settings") } +extensions.configure(com.facebook.react.ReactSettingsExtension) { extension -> + def cli = file("../node_modules/@react-native-community/cli/build/bin.js").absolutePath + extension.autolinkLibrariesFromCommand(["node", cli, "config"]) +} rootProject.name = 'BsDiffPatchExample' -apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) include ':app' includeBuild('../node_modules/@react-native/gradle-plugin') diff --git a/example/ios/BsDiffPatchExample.xcodeproj/project.pbxproj b/example/ios/BsDiffPatchExample.xcodeproj/project.pbxproj index fcf4e99..5870f2f 100644 --- a/example/ios/BsDiffPatchExample.xcodeproj/project.pbxproj +++ b/example/ios/BsDiffPatchExample.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 7699B88040F8A987B510C191 /* libPods-BsDiffPatchExample-BsDiffPatchExampleTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19F6CBCC0A4E27FBF8BF4A61 /* libPods-BsDiffPatchExample-BsDiffPatchExampleTests.a */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; + 94053906BC4F26249834AFB7 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3543B981BA751DF36D049EEE /* PrivacyInfo.xcprivacy */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -37,6 +38,7 @@ 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = BsDiffPatchExample/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = BsDiffPatchExample/main.m; sourceTree = ""; }; 19F6CBCC0A4E27FBF8BF4A61 /* libPods-BsDiffPatchExample-BsDiffPatchExampleTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-BsDiffPatchExample-BsDiffPatchExampleTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3543B981BA751DF36D049EEE /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = BsDiffPatchExample/PrivacyInfo.xcprivacy; sourceTree = ""; }; 3B4392A12AC88292D35C810B /* Pods-BsDiffPatchExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BsDiffPatchExample.debug.xcconfig"; path = "Target Support Files/Pods-BsDiffPatchExample/Pods-BsDiffPatchExample.debug.xcconfig"; sourceTree = ""; }; 5709B34CF0A7D63546082F79 /* Pods-BsDiffPatchExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BsDiffPatchExample.release.xcconfig"; path = "Target Support Files/Pods-BsDiffPatchExample/Pods-BsDiffPatchExample.release.xcconfig"; sourceTree = ""; }; 5B7EB9410499542E8C5724F5 /* Pods-BsDiffPatchExample-BsDiffPatchExampleTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BsDiffPatchExample-BsDiffPatchExampleTests.debug.xcconfig"; path = "Target Support Files/Pods-BsDiffPatchExample-BsDiffPatchExampleTests/Pods-BsDiffPatchExample-BsDiffPatchExampleTests.debug.xcconfig"; sourceTree = ""; }; @@ -92,6 +94,7 @@ 13B07FB61A68108700A75B9A /* Info.plist */, 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */, 13B07FB71A68108700A75B9A /* main.m */, + 3543B981BA751DF36D049EEE /* PrivacyInfo.xcprivacy */, ); name = BsDiffPatchExample; sourceTree = ""; @@ -160,6 +163,7 @@ 00E356EB1AD99517003FC87E /* Frameworks */, 00E356EC1AD99517003FC87E /* Resources */, F6A41C54EA430FDDC6A6ED99 /* [CP] Copy Pods Resources */, + 527F97F1F5D732C3F56E6DB4 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -181,6 +185,7 @@ 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, E235C05ADACE081382539298 /* [CP] Copy Pods Resources */, + D23758E65EED2F53790556F3 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -241,6 +246,7 @@ files = ( 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, + 94053906BC4F26249834AFB7 /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -263,6 +269,23 @@ shellPath = /bin/sh; shellScript = "set -e\n\nWITH_ENVIRONMENT=\"../node_modules/react-native/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"../node_modules/react-native/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n"; }; + 527F97F1F5D732C3F56E6DB4 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-BsDiffPatchExample-BsDiffPatchExampleTests/Pods-BsDiffPatchExample-BsDiffPatchExampleTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-BsDiffPatchExample-BsDiffPatchExampleTests/Pods-BsDiffPatchExample-BsDiffPatchExampleTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-BsDiffPatchExample-BsDiffPatchExampleTests/Pods-BsDiffPatchExample-BsDiffPatchExampleTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; A55EABD7B0C7F3A422A6CC61 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -307,6 +330,23 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + D23758E65EED2F53790556F3 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-BsDiffPatchExample/Pods-BsDiffPatchExample-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-BsDiffPatchExample/Pods-BsDiffPatchExample-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-BsDiffPatchExample/Pods-BsDiffPatchExample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; E235C05ADACE081382539298 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -540,22 +580,29 @@ ); MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; - OTHER_CFLAGS = "$(inherited)"; + OTHER_CFLAGS = ( + "$(inherited)", + "-DRCT_REMOVE_LEGACY_ARCH=1", + ); OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "-DFOLLY_NO_CONFIG", "-DFOLLY_MOBILE=1", "-DFOLLY_USE_LIBCPP=1", "-DFOLLY_CFG_NO_COROUTINES=1", + "-DRCT_REMOVE_LEGACY_ARCH=1", ); OTHER_LDFLAGS = ( "$(inherited)", "-Wl", "-ld_classic", ); + PODFILE_DIR = "$(SRCROOT)"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; - USE_HERMES = false; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; + SWIFT_ENABLE_EXPLICIT_MODULES = NO; + USE_HERMES = true; }; name = Debug; }; @@ -612,22 +659,28 @@ "\"$(inherited)\"", ); MTL_ENABLE_DEBUG_INFO = NO; - OTHER_CFLAGS = "$(inherited)"; + OTHER_CFLAGS = ( + "$(inherited)", + "-DRCT_REMOVE_LEGACY_ARCH=1", + ); OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "-DFOLLY_NO_CONFIG", "-DFOLLY_MOBILE=1", "-DFOLLY_USE_LIBCPP=1", "-DFOLLY_CFG_NO_COROUTINES=1", + "-DRCT_REMOVE_LEGACY_ARCH=1", ); OTHER_LDFLAGS = ( "$(inherited)", "-Wl", "-ld_classic", ); + PODFILE_DIR = "$(SRCROOT)"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; - USE_HERMES = false; + SWIFT_ENABLE_EXPLICIT_MODULES = NO; + USE_HERMES = true; VALIDATE_PRODUCT = YES; }; name = Release; diff --git a/example/ios/BsDiffPatchExample/AppDelegate.mm b/example/ios/BsDiffPatchExample/AppDelegate.mm index 9da5cfd..2da6eb9 100644 --- a/example/ios/BsDiffPatchExample/AppDelegate.mm +++ b/example/ios/BsDiffPatchExample/AppDelegate.mm @@ -16,10 +16,10 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { - return [self getBundleURL]; + return [self bundleURL]; } -- (NSURL *)getBundleURL +- (NSURL *)bundleURL { #if DEBUG return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; @@ -28,6 +28,12 @@ - (NSURL *)getBundleURL #endif } +// Kept for React Native versions whose RCTAppDelegate still calls this name. +- (NSURL *)getBundleURL +{ + return [self bundleURL]; +} + #ifdef RCT_NEW_ARCH_ENABLED - (BOOL)bridgelessEnabled { diff --git a/example/ios/BsDiffPatchExample/Info.plist b/example/ios/BsDiffPatchExample/Info.plist index c09cb14..f9a9857 100644 --- a/example/ios/BsDiffPatchExample/Info.plist +++ b/example/ios/BsDiffPatchExample/Info.plist @@ -33,6 +33,8 @@ NSLocationWhenInUseUsageDescription + RCTNewArchEnabled + UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities diff --git a/example/ios/BsDiffPatchExample/PrivacyInfo.xcprivacy b/example/ios/BsDiffPatchExample/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..41b8317 --- /dev/null +++ b/example/ios/BsDiffPatchExample/PrivacyInfo.xcprivacy @@ -0,0 +1,37 @@ + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategorySystemBootTime + NSPrivacyAccessedAPITypeReasons + + 35F9.1 + + + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/example/ios/BsDiffPatchExampleTests/BsDiffPatchExampleTests.m b/example/ios/BsDiffPatchExampleTests/BsDiffPatchExampleTests.m index ee3463d..95cca3e 100644 --- a/example/ios/BsDiffPatchExampleTests/BsDiffPatchExampleTests.m +++ b/example/ios/BsDiffPatchExampleTests/BsDiffPatchExampleTests.m @@ -7,7 +7,9 @@ #define TIMEOUT_SECONDS 120 #define RUNTIME_STATUS_ID @"runtime-status" #define ARCHITECTURE_STATUS_ID @"architecture-status" +#define CONTROLS_STATUS_ID @"controls-status" #define SUCCESS_STATUS @"Runtime: success" +#define CONTROLS_SUCCESS_STATUS @"Controls: success" #define ERROR_STATUS_PREFIX @"Runtime: error:" #ifndef EXPECTED_NEW_ARCH #error "EXPECTED_NEW_ARCH must be provided by the iOS runtime test" @@ -42,6 +44,7 @@ - (void)testCompletesNativeDiffPatchRoundTrip UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; __block NSString *runtimeStatus = nil; + __block NSString *controlsStatus = nil; __block NSString *architectureStatus = nil; __block NSString *redboxError = nil; @@ -54,7 +57,7 @@ - (void)testCompletesNativeDiffPatchRoundTrip }); #endif - while ([date timeIntervalSinceNow] > 0 && !runtimeStatus && !redboxError) { + while ([date timeIntervalSinceNow] > 0 && (!runtimeStatus || !controlsStatus) && !redboxError) { [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; @@ -65,12 +68,18 @@ - (void)testCompletesNativeDiffPatchRoundTrip if ([status isEqualToString:SUCCESS_STATUS] || [status hasPrefix:ERROR_STATUS_PREFIX]) { runtimeStatus = status; - return YES; } } if ([view.accessibilityIdentifier isEqualToString:ARCHITECTURE_STATUS_ID]) { architectureStatus = view.accessibilityLabel; } + if ([view.accessibilityIdentifier isEqualToString:CONTROLS_STATUS_ID]) { + NSString *status = view.accessibilityLabel; + if ([status isEqualToString:CONTROLS_SUCCESS_STATUS] || + [status hasPrefix:@"Controls: error:"]) { + controlsStatus = status; + } + } return NO; }]; } @@ -84,6 +93,10 @@ - (void)testCompletesNativeDiffPatchRoundTrip SUCCESS_STATUS, @"Expected a successful native diff/patch round trip, got '%@'", runtimeStatus); + XCTAssertEqualObjects(controlsStatus, + CONTROLS_SUCCESS_STATUS, + @"Expected progress, cancellation, and resource limit assertions to pass, got '%@'", + controlsStatus); NSString *expectedArchitectureStatus = [NSString stringWithFormat:@"Architecture: %@", EXPECTED_ARCHITECTURE]; diff --git a/example/ios/Podfile b/example/ios/Podfile index e0e1dc7..53959b4 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -19,11 +19,8 @@ target 'BsDiffPatchExample' do use_react_native!( :path => config[:reactNativePath], - # Flipper is unnecessary in CI and is incompatible with current Xcode releases. - :flipper_configuration => FlipperConfiguration.disabled, # An absolute path to your application root. - :app_path => "#{Pod::Config.instance.installation_root}/..", - :hermes_enabled => false + :app_path => "#{Pod::Config.instance.installation_root}/.." ) target 'BsDiffPatchExampleTests' do diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index ff2a9b8..f38e979 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,1128 +1,1988 @@ PODS: - - boost (1.83.0) - - DoubleConversion (1.1.6) - - FBLazyVector (0.73.2) - - FBReactNativeSpec (0.73.2): - - RCT-Folly (= 2022.05.16.00) - - RCTRequired (= 0.73.2) - - RCTTypeSafety (= 0.73.2) - - React-Core (= 0.73.2) - - React-jsi (= 0.73.2) - - ReactCommon/turbomodule/core (= 0.73.2) - - fmt (6.2.1) - - glog (0.3.5) - - RCT-Folly (2022.05.16.00): - - boost - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Default (= 2022.05.16.00) - - RCT-Folly/Default (2022.05.16.00): - - boost - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Fabric (2022.05.16.00): - - boost - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCTRequired (0.73.2) - - RCTTypeSafety (0.73.2): - - FBLazyVector (= 0.73.2) - - RCTRequired (= 0.73.2) - - React-Core (= 0.73.2) - - React (0.73.2): - - React-Core (= 0.73.2) - - React-Core/DevSupport (= 0.73.2) - - React-Core/RCTWebSocket (= 0.73.2) - - React-RCTActionSheet (= 0.73.2) - - React-RCTAnimation (= 0.73.2) - - React-RCTBlob (= 0.73.2) - - React-RCTImage (= 0.73.2) - - React-RCTLinking (= 0.73.2) - - React-RCTNetwork (= 0.73.2) - - React-RCTSettings (= 0.73.2) - - React-RCTText (= 0.73.2) - - React-RCTVibration (= 0.73.2) - - React-callinvoker (0.73.2) - - React-Codegen (0.73.2): - - DoubleConversion - - FBReactNativeSpec - - glog - - RCT-Folly - - RCTRequired - - RCTTypeSafety - - React-Core - - React-jsc - - React-jsi - - React-jsiexecutor - - React-NativeModulesApple - - React-rncore - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - React-Core (0.73.2): - - glog - - RCT-Folly (= 2022.05.16.00) - - React-Core/Default (= 0.73.2) + - FBLazyVector (0.86.0) + - hermes-engine (250829098.0.14): + - hermes-engine/Pre-built (= 250829098.0.14) + - hermes-engine/Pre-built (250829098.0.14) + - RCTDeprecation (0.86.0) + - RCTRequired (0.86.0) + - RCTSwiftUI (0.86.0) + - RCTSwiftUIWrapper (0.86.0): + - RCTSwiftUI + - RCTTypeSafety (0.86.0): + - FBLazyVector (= 0.86.0) + - RCTRequired (= 0.86.0) + - React-Core (= 0.86.0) + - React (0.86.0): + - React-Core (= 0.86.0) + - React-Core/DevSupport (= 0.86.0) + - React-Core/RCTWebSocket (= 0.86.0) + - React-RCTActionSheet (= 0.86.0) + - React-RCTAnimation (= 0.86.0) + - React-RCTBlob (= 0.86.0) + - React-RCTImage (= 0.86.0) + - React-RCTLinking (= 0.86.0) + - React-RCTNetwork (= 0.86.0) + - React-RCTSettings (= 0.86.0) + - React-RCTText (= 0.86.0) + - React-RCTVibration (= 0.86.0) + - React-callinvoker (0.86.0) + - React-Core (0.86.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default (= 0.86.0) - React-cxxreact - - React-jsc + - React-featureflags + - React-hermes - React-jsi - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling - React-perflogger + - React-runtimeexecutor - React-runtimescheduler - React-utils - - SocketRocket (= 0.6.1) + - ReactNativeDependencies - Yoga - - React-Core/CoreModulesHeaders (0.73.2): - - glog - - RCT-Folly (= 2022.05.16.00) + - React-Core-prebuilt (0.86.0): + - ReactNativeDependencies + - React-Core/CoreModulesHeaders (0.86.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt - React-Core/Default - React-cxxreact - - React-jsc + - React-featureflags + - React-hermes - React-jsi - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling - React-perflogger + - React-runtimeexecutor - React-runtimescheduler - React-utils - - SocketRocket (= 0.6.1) + - ReactNativeDependencies - Yoga - - React-Core/Default (0.73.2): - - glog - - RCT-Folly (= 2022.05.16.00) + - React-Core/Default (0.86.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt - React-cxxreact - - React-jsc + - React-featureflags + - React-hermes - React-jsi - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling - React-perflogger + - React-runtimeexecutor - React-runtimescheduler - React-utils - - SocketRocket (= 0.6.1) + - ReactNativeDependencies - Yoga - - React-Core/DevSupport (0.73.2): - - glog - - RCT-Folly (= 2022.05.16.00) - - React-Core/Default (= 0.73.2) - - React-Core/RCTWebSocket (= 0.73.2) + - React-Core/DevSupport (0.86.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default (= 0.86.0) + - React-Core/RCTWebSocket (= 0.86.0) - React-cxxreact - - React-jsc + - React-featureflags + - React-hermes - React-jsi - React-jsiexecutor - - React-jsinspector (= 0.73.2) + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling - React-perflogger + - React-runtimeexecutor - React-runtimescheduler - React-utils - - SocketRocket (= 0.6.1) + - ReactNativeDependencies - Yoga - - React-Core/RCTActionSheetHeaders (0.73.2): - - glog - - RCT-Folly (= 2022.05.16.00) + - React-Core/RCTActionSheetHeaders (0.86.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt - React-Core/Default - React-cxxreact - - React-jsc + - React-featureflags + - React-hermes - React-jsi - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling - React-perflogger + - React-runtimeexecutor - React-runtimescheduler - React-utils - - SocketRocket (= 0.6.1) + - ReactNativeDependencies - Yoga - - React-Core/RCTAnimationHeaders (0.73.2): - - glog - - RCT-Folly (= 2022.05.16.00) + - React-Core/RCTAnimationHeaders (0.86.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt - React-Core/Default - React-cxxreact - - React-jsc + - React-featureflags + - React-hermes - React-jsi - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling - React-perflogger + - React-runtimeexecutor - React-runtimescheduler - React-utils - - SocketRocket (= 0.6.1) + - ReactNativeDependencies - Yoga - - React-Core/RCTBlobHeaders (0.73.2): - - glog - - RCT-Folly (= 2022.05.16.00) + - React-Core/RCTBlobHeaders (0.86.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt - React-Core/Default - React-cxxreact - - React-jsc + - React-featureflags + - React-hermes - React-jsi - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling - React-perflogger + - React-runtimeexecutor - React-runtimescheduler - React-utils - - SocketRocket (= 0.6.1) + - ReactNativeDependencies - Yoga - - React-Core/RCTImageHeaders (0.73.2): - - glog - - RCT-Folly (= 2022.05.16.00) + - React-Core/RCTImageHeaders (0.86.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt - React-Core/Default - React-cxxreact - - React-jsc + - React-featureflags + - React-hermes - React-jsi - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling - React-perflogger + - React-runtimeexecutor - React-runtimescheduler - React-utils - - SocketRocket (= 0.6.1) + - ReactNativeDependencies - Yoga - - React-Core/RCTLinkingHeaders (0.73.2): - - glog - - RCT-Folly (= 2022.05.16.00) + - React-Core/RCTLinkingHeaders (0.86.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt - React-Core/Default - React-cxxreact - - React-jsc + - React-featureflags + - React-hermes - React-jsi - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling - React-perflogger + - React-runtimeexecutor - React-runtimescheduler - React-utils - - SocketRocket (= 0.6.1) + - ReactNativeDependencies - Yoga - - React-Core/RCTNetworkHeaders (0.73.2): - - glog - - RCT-Folly (= 2022.05.16.00) + - React-Core/RCTNetworkHeaders (0.86.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt - React-Core/Default - React-cxxreact - - React-jsc + - React-featureflags + - React-hermes - React-jsi - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling - React-perflogger + - React-runtimeexecutor - React-runtimescheduler - React-utils - - SocketRocket (= 0.6.1) + - ReactNativeDependencies - Yoga - - React-Core/RCTSettingsHeaders (0.73.2): - - glog - - RCT-Folly (= 2022.05.16.00) + - React-Core/RCTSettingsHeaders (0.86.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt - React-Core/Default - React-cxxreact - - React-jsc + - React-featureflags + - React-hermes - React-jsi - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling - React-perflogger + - React-runtimeexecutor - React-runtimescheduler - React-utils - - SocketRocket (= 0.6.1) + - ReactNativeDependencies - Yoga - - React-Core/RCTTextHeaders (0.73.2): - - glog - - RCT-Folly (= 2022.05.16.00) + - React-Core/RCTTextHeaders (0.86.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt - React-Core/Default - React-cxxreact - - React-jsc + - React-featureflags + - React-hermes - React-jsi - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling - React-perflogger + - React-runtimeexecutor - React-runtimescheduler - React-utils - - SocketRocket (= 0.6.1) + - ReactNativeDependencies - Yoga - - React-Core/RCTVibrationHeaders (0.73.2): - - glog - - RCT-Folly (= 2022.05.16.00) + - React-Core/RCTVibrationHeaders (0.86.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt - React-Core/Default - React-cxxreact - - React-jsc + - React-featureflags + - React-hermes - React-jsi - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling - React-perflogger + - React-runtimeexecutor - React-runtimescheduler - React-utils - - SocketRocket (= 0.6.1) + - ReactNativeDependencies - Yoga - - React-Core/RCTWebSocket (0.73.2): - - glog - - RCT-Folly (= 2022.05.16.00) - - React-Core/Default (= 0.73.2) + - React-Core/RCTWebSocket (0.86.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default (= 0.86.0) - React-cxxreact - - React-jsc + - React-featureflags + - React-hermes - React-jsi - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling - React-perflogger + - React-runtimeexecutor - React-runtimescheduler - React-utils - - SocketRocket (= 0.6.1) + - ReactNativeDependencies - Yoga - - React-CoreModules (0.73.2): - - RCT-Folly (= 2022.05.16.00) - - RCTTypeSafety (= 0.73.2) - - React-Codegen - - React-Core/CoreModulesHeaders (= 0.73.2) - - React-jsi (= 0.73.2) + - React-CoreModules (0.86.0): + - RCTTypeSafety (= 0.86.0) + - React-Core-prebuilt + - React-Core/CoreModulesHeaders (= 0.86.0) + - React-debug + - React-featureflags + - React-jsi (= 0.86.0) + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing - React-NativeModulesApple - React-RCTBlob - - React-RCTImage (= 0.73.2) + - React-RCTFBReactNativeSpec + - React-RCTImage (= 0.86.0) + - React-runtimeexecutor + - React-utils - ReactCommon - - SocketRocket (= 0.6.1) - - React-cxxreact (0.73.2): - - boost (= 1.83.0) - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly (= 2022.05.16.00) - - React-callinvoker (= 0.73.2) - - React-debug (= 0.73.2) - - React-jsi (= 0.73.2) - - React-jsinspector (= 0.73.2) - - React-logger (= 0.73.2) - - React-perflogger (= 0.73.2) - - React-runtimeexecutor (= 0.73.2) - - React-debug (0.73.2) - - React-Fabric (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) + - ReactNativeDependencies + - React-cxxreact (0.86.0): + - hermes-engine + - React-callinvoker (= 0.86.0) + - React-Core-prebuilt + - React-debug (= 0.86.0) + - React-jsi (= 0.86.0) + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-logger (= 0.86.0) + - React-perflogger (= 0.86.0) + - React-runtimeexecutor + - React-timing (= 0.86.0) + - React-utils + - ReactNativeDependencies + - React-debug (0.86.0): + - React-debug/redbox (= 0.86.0) + - React-debug/redbox (0.86.0) + - React-defaultsnativemodule (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-domnativemodule + - React-Fabric/animated + - React-featureflags + - React-featureflagsnativemodule + - React-idlecallbacksnativemodule + - React-intersectionobservernativemodule + - React-jsi + - React-jsiexecutor + - React-microtasksnativemodule + - React-mutationobservernativemodule + - React-RCTFBReactNativeSpec + - React-viewtransitionnativemodule + - React-webperformancenativemodule + - ReactNativeDependencies + - Yoga + - React-domnativemodule (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-Fabric + - React-Fabric/bridging + - React-FabricComponents + - React-graphics + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - React-runtimeexecutor + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-Fabric (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric/animated (= 0.86.0) + - React-Fabric/animationbackend (= 0.86.0) + - React-Fabric/animations (= 0.86.0) + - React-Fabric/attributedstring (= 0.86.0) + - React-Fabric/bridging (= 0.86.0) + - React-Fabric/componentregistry (= 0.86.0) + - React-Fabric/componentregistrynative (= 0.86.0) + - React-Fabric/components (= 0.86.0) + - React-Fabric/consistency (= 0.86.0) + - React-Fabric/core (= 0.86.0) + - React-Fabric/dom (= 0.86.0) + - React-Fabric/imagemanager (= 0.86.0) + - React-Fabric/leakchecker (= 0.86.0) + - React-Fabric/mounting (= 0.86.0) + - React-Fabric/observers (= 0.86.0) + - React-Fabric/scheduler (= 0.86.0) + - React-Fabric/telemetry (= 0.86.0) + - React-Fabric/uimanager (= 0.86.0) + - React-Fabric/viewtransition (= 0.86.0) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/animated (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric/animationbackend + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/animationbackend (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/animations (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/attributedstring (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/bridging (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/componentregistry (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/componentregistrynative (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/components (0.86.0): + - hermes-engine - RCTRequired - RCTTypeSafety - React-Core + - React-Core-prebuilt - React-cxxreact - React-debug - - React-Fabric/animations (= 0.73.2) - - React-Fabric/attributedstring (= 0.73.2) - - React-Fabric/componentregistry (= 0.73.2) - - React-Fabric/componentregistrynative (= 0.73.2) - - React-Fabric/components (= 0.73.2) - - React-Fabric/core (= 0.73.2) - - React-Fabric/imagemanager (= 0.73.2) - - React-Fabric/leakchecker (= 0.73.2) - - React-Fabric/mounting (= 0.73.2) - - React-Fabric/scheduler (= 0.73.2) - - React-Fabric/telemetry (= 0.73.2) - - React-Fabric/templateprocessor (= 0.73.2) - - React-Fabric/textlayoutmanager (= 0.73.2) - - React-Fabric/uimanager (= 0.73.2) + - React-Fabric/components/legacyviewmanagerinterop (= 0.86.0) + - React-Fabric/components/root (= 0.86.0) + - React-Fabric/components/scrollview (= 0.86.0) + - React-Fabric/components/view (= 0.86.0) + - React-featureflags - React-graphics - - React-jsc - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/animations (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) + - ReactNativeDependencies + - React-Fabric/components/legacyviewmanagerinterop (0.86.0): + - hermes-engine - RCTRequired - RCTTypeSafety - React-Core + - React-Core-prebuilt - React-cxxreact - React-debug + - React-featureflags - React-graphics - - React-jsc - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/attributedstring (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) + - ReactNativeDependencies + - React-Fabric/components/root (0.86.0): + - hermes-engine - RCTRequired - RCTTypeSafety - React-Core + - React-Core-prebuilt - React-cxxreact - React-debug + - React-featureflags - React-graphics - - React-jsc - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/componentregistry (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) + - ReactNativeDependencies + - React-Fabric/components/scrollview (0.86.0): + - hermes-engine - RCTRequired - RCTTypeSafety - React-Core + - React-Core-prebuilt - React-cxxreact - React-debug + - React-featureflags - React-graphics - - React-jsc - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/componentregistrynative (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) + - ReactNativeDependencies + - React-Fabric/components/view (0.86.0): + - hermes-engine - RCTRequired - RCTTypeSafety - React-Core + - React-Core-prebuilt - React-cxxreact - React-debug + - React-featureflags - React-graphics - - React-jsc - React-jsi - React-jsiexecutor - React-logger + - React-renderercss - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/components (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) + - ReactNativeDependencies + - Yoga + - React-Fabric/consistency (0.86.0): + - hermes-engine - RCTRequired - RCTTypeSafety - React-Core + - React-Core-prebuilt - React-cxxreact - React-debug - - React-Fabric/components/inputaccessory (= 0.73.2) - - React-Fabric/components/legacyviewmanagerinterop (= 0.73.2) - - React-Fabric/components/modal (= 0.73.2) - - React-Fabric/components/rncore (= 0.73.2) - - React-Fabric/components/root (= 0.73.2) - - React-Fabric/components/safeareaview (= 0.73.2) - - React-Fabric/components/scrollview (= 0.73.2) - - React-Fabric/components/text (= 0.73.2) - - React-Fabric/components/textinput (= 0.73.2) - - React-Fabric/components/unimplementedview (= 0.73.2) - - React-Fabric/components/view (= 0.73.2) + - React-featureflags - React-graphics - - React-jsc - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/components/inputaccessory (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) + - ReactNativeDependencies + - React-Fabric/core (0.86.0): + - hermes-engine - RCTRequired - RCTTypeSafety - React-Core + - React-Core-prebuilt - React-cxxreact - React-debug + - React-featureflags - React-graphics - - React-jsc - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/components/legacyviewmanagerinterop (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) + - ReactNativeDependencies + - React-Fabric/dom (0.86.0): + - hermes-engine - RCTRequired - RCTTypeSafety - React-Core + - React-Core-prebuilt - React-cxxreact - React-debug + - React-featureflags - React-graphics - - React-jsc - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/components/modal (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) + - ReactNativeDependencies + - React-Fabric/imagemanager (0.86.0): + - hermes-engine - RCTRequired - RCTTypeSafety - React-Core + - React-Core-prebuilt - React-cxxreact - React-debug + - React-featureflags - React-graphics - - React-jsc - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/components/rncore (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) + - ReactNativeDependencies + - React-Fabric/leakchecker (0.86.0): + - hermes-engine - RCTRequired - RCTTypeSafety - React-Core + - React-Core-prebuilt - React-cxxreact - React-debug + - React-featureflags - React-graphics - - React-jsc - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/components/root (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) + - ReactNativeDependencies + - React-Fabric/mounting (0.86.0): + - hermes-engine - RCTRequired - RCTTypeSafety - React-Core + - React-Core-prebuilt - React-cxxreact - React-debug + - React-featureflags - React-graphics - - React-jsc - React-jsi - React-jsiexecutor + - React-jsinspectortracing - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/components/safeareaview (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) + - ReactNativeDependencies + - React-Fabric/observers (0.86.0): + - hermes-engine - RCTRequired - RCTTypeSafety - React-Core + - React-Core-prebuilt - React-cxxreact - React-debug + - React-Fabric/observers/events (= 0.86.0) + - React-Fabric/observers/intersection (= 0.86.0) + - React-Fabric/observers/mutation (= 0.86.0) + - React-featureflags - React-graphics - - React-jsc - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/components/scrollview (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) + - ReactNativeDependencies + - React-Fabric/observers/events (0.86.0): + - hermes-engine - RCTRequired - RCTTypeSafety - React-Core + - React-Core-prebuilt - React-cxxreact - React-debug + - React-featureflags - React-graphics - - React-jsc - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/components/text (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) + - ReactNativeDependencies + - React-Fabric/observers/intersection (0.86.0): + - hermes-engine - RCTRequired - RCTTypeSafety - React-Core + - React-Core-prebuilt - React-cxxreact - React-debug + - React-featureflags - React-graphics - - React-jsc - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/components/textinput (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) + - ReactNativeDependencies + - React-Fabric/observers/mutation (0.86.0): + - hermes-engine - RCTRequired - RCTTypeSafety - React-Core + - React-Core-prebuilt - React-cxxreact - React-debug + - React-featureflags - React-graphics - - React-jsc - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/components/unimplementedview (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) + - ReactNativeDependencies + - React-Fabric/scheduler (0.86.0): + - hermes-engine - RCTRequired - RCTTypeSafety - React-Core + - React-Core-prebuilt - React-cxxreact - React-debug + - React-Fabric/animationbackend + - React-Fabric/observers/events + - React-Fabric/viewtransition + - React-featureflags - React-graphics - - React-jsc - React-jsi - React-jsiexecutor - React-logger + - React-performancecdpmetrics + - React-performancetimeline - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/components/view (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) + - ReactNativeDependencies + - React-Fabric/telemetry (0.86.0): + - hermes-engine - RCTRequired - RCTTypeSafety - React-Core + - React-Core-prebuilt - React-cxxreact - React-debug + - React-featureflags - React-graphics - - React-jsc - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/uimanager (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric/uimanager/consistency (= 0.86.0) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererconsistency + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/uimanager/consistency (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererconsistency + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/viewtransition (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-FabricComponents (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-FabricComponents/components (= 0.86.0) + - React-FabricComponents/textlayoutmanager (= 0.86.0) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies - Yoga - - React-Fabric/core (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) + - React-FabricComponents/components (0.86.0): + - hermes-engine - RCTRequired - RCTTypeSafety - React-Core + - React-Core-prebuilt - React-cxxreact - React-debug + - React-Fabric + - React-FabricComponents/components/inputaccessory (= 0.86.0) + - React-FabricComponents/components/iostextinput (= 0.86.0) + - React-FabricComponents/components/modal (= 0.86.0) + - React-FabricComponents/components/rncore (= 0.86.0) + - React-FabricComponents/components/safeareaview (= 0.86.0) + - React-FabricComponents/components/scrollview (= 0.86.0) + - React-FabricComponents/components/switch (= 0.86.0) + - React-FabricComponents/components/text (= 0.86.0) + - React-FabricComponents/components/textinput (= 0.86.0) + - React-FabricComponents/components/unimplementedview (= 0.86.0) + - React-FabricComponents/components/virtualview (= 0.86.0) + - React-featureflags - React-graphics - - React-jsc - React-jsi - React-jsiexecutor - React-logger + - React-RCTFBReactNativeSpec - React-rendererdebug - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/imagemanager (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/inputaccessory (0.86.0): + - hermes-engine - RCTRequired - RCTTypeSafety - React-Core + - React-Core-prebuilt - React-cxxreact - React-debug + - React-Fabric + - React-featureflags - React-graphics - - React-jsc - React-jsi - React-jsiexecutor - React-logger + - React-RCTFBReactNativeSpec - React-rendererdebug - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/leakchecker (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/iostextinput (0.86.0): + - hermes-engine - RCTRequired - RCTTypeSafety - React-Core + - React-Core-prebuilt - React-cxxreact - React-debug + - React-Fabric + - React-featureflags - React-graphics - - React-jsc - React-jsi - React-jsiexecutor - React-logger + - React-RCTFBReactNativeSpec - React-rendererdebug - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/mounting (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/modal (0.86.0): + - hermes-engine - RCTRequired - RCTTypeSafety - React-Core + - React-Core-prebuilt - React-cxxreact - React-debug + - React-Fabric + - React-featureflags - React-graphics - - React-jsc - React-jsi - React-jsiexecutor - React-logger + - React-RCTFBReactNativeSpec - React-rendererdebug - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/scheduler (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/rncore (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/safeareaview (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/scrollview (0.86.0): + - hermes-engine - RCTRequired - RCTTypeSafety - React-Core + - React-Core-prebuilt - React-cxxreact - React-debug + - React-Fabric + - React-featureflags - React-graphics - - React-jsc - React-jsi - React-jsiexecutor - React-logger + - React-RCTFBReactNativeSpec - React-rendererdebug - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/telemetry (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/switch (0.86.0): + - hermes-engine - RCTRequired - RCTTypeSafety - React-Core + - React-Core-prebuilt - React-cxxreact - React-debug + - React-Fabric + - React-featureflags - React-graphics - - React-jsc - React-jsi - React-jsiexecutor - React-logger + - React-RCTFBReactNativeSpec - React-rendererdebug - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/templateprocessor (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/text (0.86.0): + - hermes-engine - RCTRequired - RCTTypeSafety - React-Core + - React-Core-prebuilt - React-cxxreact - React-debug + - React-Fabric + - React-featureflags - React-graphics - - React-jsc - React-jsi - React-jsiexecutor - React-logger + - React-RCTFBReactNativeSpec - React-rendererdebug - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/textlayoutmanager (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/textinput (0.86.0): + - hermes-engine - RCTRequired - RCTTypeSafety - React-Core + - React-Core-prebuilt - React-cxxreact - React-debug - - React-Fabric/uimanager + - React-Fabric + - React-featureflags - React-graphics - - React-jsc - React-jsi - React-jsiexecutor - React-logger + - React-RCTFBReactNativeSpec - React-rendererdebug - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/uimanager (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/unimplementedview (0.86.0): + - hermes-engine - RCTRequired - RCTTypeSafety - React-Core + - React-Core-prebuilt - React-cxxreact - React-debug + - React-Fabric + - React-featureflags - React-graphics - - React-jsc - React-jsi - React-jsiexecutor - React-logger + - React-RCTFBReactNativeSpec - React-rendererdebug - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-FabricImage (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) - - RCTRequired (= 0.73.2) - - RCTTypeSafety (= 0.73.2) + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/virtualview (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/textlayoutmanager (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricImage (0.86.0): + - hermes-engine + - RCTRequired (= 0.86.0) + - RCTTypeSafety (= 0.86.0) + - React-Core-prebuilt + - React-Fabric + - React-featureflags - React-graphics - React-ImageManager - - React-jsc - React-jsi - - React-jsiexecutor (= 0.73.2) + - React-jsiexecutor (= 0.86.0) - React-logger - React-rendererdebug - React-utils - ReactCommon + - ReactNativeDependencies - Yoga - - React-graphics (0.73.2): - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) - - React-Core/Default (= 0.73.2) - - React-utils - - React-ImageManager (0.73.2): - - glog - - RCT-Folly/Fabric + - React-featureflags (0.86.0): + - React-Core-prebuilt + - ReactNativeDependencies + - React-featureflagsnativemodule (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-featureflags + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-graphics (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-featureflags + - React-jsi + - React-jsiexecutor + - React-rendererdebug + - React-utils + - ReactNativeDependencies + - React-hermes (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact (= 0.86.0) + - React-jsi + - React-jsiexecutor (= 0.86.0) + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-jsitooling + - React-oscompat + - React-perflogger (= 0.86.0) + - React-runtimeexecutor + - ReactNativeDependencies + - React-idlecallbacksnativemodule (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - React-runtimeexecutor + - React-runtimescheduler + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-ImageManager (0.86.0): + - React-Core-prebuilt - React-Core/Default - React-debug - React-Fabric - React-graphics - React-rendererdebug - React-utils - - React-jsc (0.73.2): - - React-jsc/Fabric (= 0.73.2) - - React-jsi (= 0.73.2) - - React-jsc/Fabric (0.73.2): - - React-jsi (= 0.73.2) - - React-jserrorhandler (0.73.2): - - RCT-Folly/Fabric (= 2022.05.16.00) + - ReactNativeDependencies + - React-intersectionobservernativemodule (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact + - React-Fabric + - React-Fabric/bridging + - React-graphics + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - React-runtimeexecutor + - React-runtimescheduler + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-jserrorhandler (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact - React-debug + - React-featureflags - React-jsi - - React-Mapbuffer - - React-jsi (0.73.2): - - boost (= 1.83.0) - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly (= 2022.05.16.00) - - React-jsiexecutor (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly (= 2022.05.16.00) - - React-cxxreact (= 0.73.2) - - React-jsi (= 0.73.2) - - React-perflogger (= 0.73.2) - - React-jsinspector (0.73.2) - - React-logger (0.73.2): - - glog - - React-Mapbuffer (0.73.2): - - glog + - ReactCommon/turbomodule/bridging + - ReactNativeDependencies + - React-jsi (0.86.0): + - hermes-engine + - React-Core-prebuilt + - ReactNativeDependencies + - React-jsiexecutor (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-jserrorhandler + - React-jsi + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-utils + - ReactNativeDependencies + - React-jsinspector (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-featureflags + - React-jsi + - React-jsinspectorcdp + - React-jsinspectornetwork + - React-jsinspectortracing + - React-oscompat + - React-perflogger (= 0.86.0) + - React-runtimeexecutor + - React-utils + - ReactNativeDependencies + - React-jsinspectorcdp (0.86.0): + - React-Core-prebuilt + - ReactNativeDependencies + - React-jsinspectornetwork (0.86.0): + - React-Core-prebuilt + - React-jsinspectorcdp + - ReactNativeDependencies + - React-jsinspectortracing (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-jsi + - React-jsinspectornetwork + - React-oscompat + - React-timing + - React-utils + - ReactNativeDependencies + - React-jsitooling (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact (= 0.86.0) - React-debug + - React-jsi (= 0.86.0) + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-runtimeexecutor + - React-utils + - ReactNativeDependencies + - React-jsitracing (0.86.0): + - React-jsi + - React-logger (0.86.0): + - React-Core-prebuilt + - ReactNativeDependencies + - React-Mapbuffer (0.86.0): + - React-Core-prebuilt + - React-debug + - ReactNativeDependencies + - React-microtasksnativemodule (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-mutationobservernativemodule (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact + - React-Fabric + - React-Fabric/bridging + - React-Fabric/observers/mutation + - React-featureflags + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - React-runtimeexecutor + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga - react-native-bs-diff-patch (0.2.0): - - glog - - RCT-Folly (= 2022.05.16.00) + - hermes-engine + - RCTRequired + - RCTTypeSafety - React-Core - - React-nativeconfig (0.73.2) - - React-NativeModulesApple (0.73.2): - - glog + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-NativeModulesApple (0.86.0): + - hermes-engine - React-callinvoker - React-Core + - React-Core-prebuilt - React-cxxreact - - React-jsc + - React-debug + - React-featureflags - React-jsi + - React-jsinspector + - React-jsinspectorcdp - React-runtimeexecutor - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - React-perflogger (0.73.2) - - React-RCTActionSheet (0.73.2): - - React-Core/RCTActionSheetHeaders (= 0.73.2) - - React-RCTAnimation (0.73.2): - - RCT-Folly (= 2022.05.16.00) + - ReactNativeDependencies + - React-networking (0.86.0): + - React-Core-prebuilt + - React-jsinspectornetwork + - React-jsinspectortracing + - React-performancetimeline + - React-timing + - ReactNativeDependencies + - React-oscompat (0.86.0) + - React-perflogger (0.86.0): + - React-Core-prebuilt + - ReactNativeDependencies + - React-performancecdpmetrics (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-jsi + - React-performancetimeline + - React-runtimeexecutor + - React-timing + - ReactNativeDependencies + - React-performancetimeline (0.86.0): + - React-Core-prebuilt + - React-featureflags + - React-jsinspector + - React-jsinspectortracing + - React-perflogger + - React-timing + - ReactNativeDependencies + - React-RCTActionSheet (0.86.0): + - React-Core/RCTActionSheetHeaders (= 0.86.0) + - React-RCTAnimation (0.86.0): - RCTTypeSafety - - React-Codegen + - React-Core-prebuilt - React-Core/RCTAnimationHeaders + - React-debug + - React-featureflags - React-jsi - React-NativeModulesApple + - React-RCTFBReactNativeSpec - ReactCommon - - React-RCTAppDelegate (0.73.2): - - RCT-Folly + - ReactNativeDependencies + - React-RCTAppDelegate (0.86.0): + - hermes-engine - RCTRequired - RCTTypeSafety - React-Core + - React-Core-prebuilt - React-CoreModules - - React-jsc - - React-nativeconfig + - React-debug + - React-defaultsnativemodule + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-jsitooling - React-NativeModulesApple - React-RCTFabric + - React-RCTFBReactNativeSpec - React-RCTImage - React-RCTNetwork + - React-RCTRuntime + - React-rendererdebug + - React-RuntimeApple + - React-RuntimeCore + - React-runtimeexecutor - React-runtimescheduler + - React-utils - ReactCommon - - React-RCTBlob (0.73.2): - - RCT-Folly (= 2022.05.16.00) - - React-Codegen + - ReactNativeDependencies + - React-RCTBlob (0.86.0): + - hermes-engine + - React-Core-prebuilt - React-Core/RCTBlobHeaders - React-Core/RCTWebSocket - React-jsi + - React-jsinspector + - React-jsinspectorcdp - React-NativeModulesApple + - React-RCTFBReactNativeSpec - React-RCTNetwork - ReactCommon - - React-RCTFabric (0.73.2): - - glog - - RCT-Folly/Fabric (= 2022.05.16.00) + - ReactNativeDependencies + - React-RCTFabric (0.86.0): + - hermes-engine + - RCTSwiftUIWrapper - React-Core + - React-Core-prebuilt - React-debug - React-Fabric + - React-FabricComponents - React-FabricImage + - React-featureflags - React-graphics - React-ImageManager - - React-jsc - React-jsi - - React-nativeconfig + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-networking + - React-performancecdpmetrics + - React-performancetimeline + - React-RCTAnimation + - React-RCTFBReactNativeSpec - React-RCTImage - React-RCTText + - React-rendererconsistency + - React-renderercss - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils + - ReactNativeDependencies + - Yoga + - React-RCTFBReactNativeSpec (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-jsi + - React-NativeModulesApple + - React-RCTFBReactNativeSpec/components (= 0.86.0) + - ReactCommon + - ReactNativeDependencies + - React-RCTFBReactNativeSpec/components (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-NativeModulesApple + - React-rendererdebug + - React-utils + - ReactCommon + - ReactNativeDependencies - Yoga - - React-RCTImage (0.73.2): - - RCT-Folly (= 2022.05.16.00) + - React-RCTImage (0.86.0): - RCTTypeSafety - - React-Codegen + - React-Core-prebuilt - React-Core/RCTImageHeaders - React-jsi - React-NativeModulesApple + - React-RCTFBReactNativeSpec - React-RCTNetwork - ReactCommon - - React-RCTLinking (0.73.2): - - React-Codegen - - React-Core/RCTLinkingHeaders (= 0.73.2) - - React-jsi (= 0.73.2) + - ReactNativeDependencies + - React-RCTLinking (0.86.0): + - React-Core/RCTLinkingHeaders (= 0.86.0) + - React-jsi (= 0.86.0) - React-NativeModulesApple + - React-RCTFBReactNativeSpec - ReactCommon - - ReactCommon/turbomodule/core (= 0.73.2) - - React-RCTNetwork (0.73.2): - - RCT-Folly (= 2022.05.16.00) + - ReactCommon/turbomodule/core (= 0.86.0) + - React-RCTNetwork (0.86.0): - RCTTypeSafety - - React-Codegen + - React-Core-prebuilt - React-Core/RCTNetworkHeaders + - React-debug + - React-featureflags - React-jsi + - React-jsinspectorcdp + - React-jsinspectornetwork - React-NativeModulesApple + - React-networking + - React-RCTFBReactNativeSpec - ReactCommon - - React-RCTSettings (0.73.2): - - RCT-Folly (= 2022.05.16.00) + - ReactNativeDependencies + - React-RCTRuntime (0.86.0): + - hermes-engine + - React-Core + - React-Core-prebuilt + - React-debug + - React-jsi + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-jsitooling + - React-RuntimeApple + - React-RuntimeCore + - React-runtimeexecutor + - React-RuntimeHermes + - React-utils + - ReactNativeDependencies + - React-RCTSettings (0.86.0): - RCTTypeSafety - - React-Codegen + - React-Core-prebuilt - React-Core/RCTSettingsHeaders - React-jsi - React-NativeModulesApple + - React-RCTFBReactNativeSpec - ReactCommon - - React-RCTText (0.73.2): - - React-Core/RCTTextHeaders (= 0.73.2) + - ReactNativeDependencies + - React-RCTText (0.86.0): + - React-Core/RCTTextHeaders (= 0.86.0) - Yoga - - React-RCTVibration (0.73.2): - - RCT-Folly (= 2022.05.16.00) - - React-Codegen + - React-RCTVibration (0.86.0): + - React-Core-prebuilt - React-Core/RCTVibrationHeaders - React-jsi - React-NativeModulesApple + - React-RCTFBReactNativeSpec - ReactCommon - - React-rendererdebug (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - RCT-Folly (= 2022.05.16.00) + - ReactNativeDependencies + - React-rendererconsistency (0.86.0) + - React-renderercss (0.86.0): - React-debug - - React-rncore (0.73.2) - - React-runtimeexecutor (0.73.2): - - React-jsi (= 0.73.2) - - React-runtimescheduler (0.73.2): - - glog - - RCT-Folly (= 2022.05.16.00) + - React-utils + - React-rendererdebug (0.86.0): + - React-Core-prebuilt + - React-debug + - ReactNativeDependencies + - React-RuntimeApple (0.86.0): + - hermes-engine - React-callinvoker + - React-Core-prebuilt + - React-Core/Default + - React-CoreModules + - React-cxxreact + - React-featureflags + - React-jserrorhandler + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsitooling + - React-Mapbuffer + - React-NativeModulesApple + - React-RCTFabric + - React-RCTFBReactNativeSpec + - React-RuntimeCore + - React-runtimeexecutor + - React-RuntimeHermes + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - React-RuntimeCore (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact + - React-Fabric + - React-featureflags + - React-jserrorhandler + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsitooling + - React-performancetimeline + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - React-runtimeexecutor (0.86.0): + - React-Core-prebuilt + - React-debug + - React-featureflags + - React-jsi (= 0.86.0) + - React-utils + - ReactNativeDependencies + - React-RuntimeHermes (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-featureflags + - React-hermes + - React-jsi + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-jsitooling + - React-jsitracing + - React-RuntimeCore + - React-runtimeexecutor + - React-utils + - ReactNativeDependencies + - React-runtimescheduler (0.86.0): + - hermes-engine + - React-callinvoker + - React-Core-prebuilt - React-cxxreact - React-debug - - React-jsc + - React-featureflags - React-jsi + - React-jsinspectortracing + - React-performancetimeline + - React-rendererconsistency - React-rendererdebug - React-runtimeexecutor + - React-timing - React-utils - - React-utils (0.73.2): - - glog - - RCT-Folly (= 2022.05.16.00) + - ReactNativeDependencies + - React-timing (0.86.0): - React-debug - - ReactCommon (0.73.2): - - React-logger (= 0.73.2) - - ReactCommon/turbomodule (= 0.73.2) - - ReactCommon/turbomodule (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly (= 2022.05.16.00) - - React-callinvoker (= 0.73.2) - - React-cxxreact (= 0.73.2) - - React-jsi (= 0.73.2) - - React-logger (= 0.73.2) - - React-perflogger (= 0.73.2) - - ReactCommon/turbomodule/bridging (= 0.73.2) - - ReactCommon/turbomodule/core (= 0.73.2) - - ReactCommon/turbomodule/bridging (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly (= 2022.05.16.00) - - React-callinvoker (= 0.73.2) - - React-cxxreact (= 0.73.2) - - React-jsi (= 0.73.2) - - React-logger (= 0.73.2) - - React-perflogger (= 0.73.2) - - ReactCommon/turbomodule/core (0.73.2): - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly (= 2022.05.16.00) - - React-callinvoker (= 0.73.2) - - React-cxxreact (= 0.73.2) - - React-jsi (= 0.73.2) - - React-logger (= 0.73.2) - - React-perflogger (= 0.73.2) + - React-utils (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-debug + - React-jsi (= 0.86.0) + - ReactNativeDependencies + - React-viewtransitionnativemodule (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-Fabric + - React-Fabric/bridging + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - React-runtimeexecutor + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-webperformancenativemodule (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact + - React-jsi + - React-jsiexecutor + - React-performancetimeline + - React-RCTFBReactNativeSpec + - React-runtimeexecutor + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - ReactAppDependencyProvider (0.86.0): + - ReactCodegen + - ReactCodegen (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-FabricImage + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-NativeModulesApple + - React-RCTAppDelegate + - React-rendererdebug + - React-utils + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - ReactCommon (0.86.0): + - React-Core-prebuilt + - ReactCommon/turbomodule (= 0.86.0) + - ReactNativeDependencies + - ReactCommon/turbomodule (0.86.0): + - hermes-engine + - React-callinvoker (= 0.86.0) + - React-Core-prebuilt + - React-cxxreact (= 0.86.0) + - React-jsi (= 0.86.0) + - React-logger (= 0.86.0) + - React-perflogger (= 0.86.0) + - ReactCommon/turbomodule/bridging (= 0.86.0) + - ReactCommon/turbomodule/core (= 0.86.0) + - ReactNativeDependencies + - ReactCommon/turbomodule/bridging (0.86.0): + - hermes-engine + - React-callinvoker (= 0.86.0) + - React-Core-prebuilt + - React-cxxreact (= 0.86.0) + - React-jsi (= 0.86.0) + - React-logger (= 0.86.0) + - React-perflogger (= 0.86.0) + - ReactNativeDependencies + - ReactCommon/turbomodule/core (0.86.0): + - hermes-engine + - React-callinvoker (= 0.86.0) + - React-Core-prebuilt + - React-cxxreact (= 0.86.0) + - React-debug (= 0.86.0) + - React-featureflags (= 0.86.0) + - React-jsi (= 0.86.0) + - React-logger (= 0.86.0) + - React-perflogger (= 0.86.0) + - React-utils (= 0.86.0) + - ReactNativeDependencies + - ReactNativeDependencies (0.86.0) - RNFS (2.20.0): - React-Core - - SocketRocket (0.6.1) - - Yoga (1.14.0) + - Yoga (0.0.0) DEPENDENCIES: - - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) - - FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`) - - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - - RCT-Folly/Fabric (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - - RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`) + - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) + - RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`) + - RCTRequired (from `../node_modules/react-native/Libraries/Required`) + - RCTSwiftUI (from `../node_modules/react-native/ReactApple/RCTSwiftUI`) + - RCTSwiftUIWrapper (from `../node_modules/react-native/ReactApple/RCTSwiftUIWrapper`) - RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`) - React (from `../node_modules/react-native/`) - React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`) - - React-Codegen (from `build/generated/ios`) - React-Core (from `../node_modules/react-native/`) + - React-Core-prebuilt (from `../node_modules/react-native/React-Core-prebuilt.podspec`) - React-Core/RCTWebSocket (from `../node_modules/react-native/`) - React-CoreModules (from `../node_modules/react-native/React/CoreModules`) - React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`) - React-debug (from `../node_modules/react-native/ReactCommon/react/debug`) + - React-defaultsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/defaults`) + - React-domnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/dom`) - React-Fabric (from `../node_modules/react-native/ReactCommon`) + - React-FabricComponents (from `../node_modules/react-native/ReactCommon`) - React-FabricImage (from `../node_modules/react-native/ReactCommon`) + - React-featureflags (from `../node_modules/react-native/ReactCommon/react/featureflags`) + - React-featureflagsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/featureflags`) - React-graphics (from `../node_modules/react-native/ReactCommon/react/renderer/graphics`) + - React-hermes (from `../node_modules/react-native/ReactCommon/hermes`) + - React-idlecallbacksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`) - React-ImageManager (from `../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`) - - React-jsc (from `../node_modules/react-native/ReactCommon/jsc`) + - React-intersectionobservernativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/intersectionobserver`) - React-jserrorhandler (from `../node_modules/react-native/ReactCommon/jserrorhandler`) - React-jsi (from `../node_modules/react-native/ReactCommon/jsi`) - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector-modern`) + - React-jsinspectorcdp (from `../node_modules/react-native/ReactCommon/jsinspector-modern/cdp`) + - React-jsinspectornetwork (from `../node_modules/react-native/ReactCommon/jsinspector-modern/network`) + - React-jsinspectortracing (from `../node_modules/react-native/ReactCommon/jsinspector-modern/tracing`) + - React-jsitooling (from `../node_modules/react-native/ReactCommon/jsitooling`) + - React-jsitracing (from `../node_modules/react-native/ReactCommon/hermes/executor/`) - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) + - React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) + - React-mutationobservernativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/mutationobserver`) - react-native-bs-diff-patch (from `../..`) - - React-nativeconfig (from `../node_modules/react-native/ReactCommon`) - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) + - React-networking (from `../node_modules/react-native/ReactCommon/react/networking`) + - React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`) - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) + - React-performancecdpmetrics (from `../node_modules/react-native/ReactCommon/react/performance/cdpmetrics`) + - React-performancetimeline (from `../node_modules/react-native/ReactCommon/react/performance/timeline`) - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) - React-RCTAppDelegate (from `../node_modules/react-native/Libraries/AppDelegate`) - React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`) - React-RCTFabric (from `../node_modules/react-native/React`) + - React-RCTFBReactNativeSpec (from `../node_modules/react-native/React`) - React-RCTImage (from `../node_modules/react-native/Libraries/Image`) - React-RCTLinking (from `../node_modules/react-native/Libraries/LinkingIOS`) - React-RCTNetwork (from `../node_modules/react-native/Libraries/Network`) + - React-RCTRuntime (from `../node_modules/react-native/React/Runtime`) - React-RCTSettings (from `../node_modules/react-native/Libraries/Settings`) - React-RCTText (from `../node_modules/react-native/Libraries/Text`) - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) + - React-rendererconsistency (from `../node_modules/react-native/ReactCommon/react/renderer/consistency`) + - React-renderercss (from `../node_modules/react-native/ReactCommon/react/renderer/css`) - React-rendererdebug (from `../node_modules/react-native/ReactCommon/react/renderer/debug`) - - React-rncore (from `../node_modules/react-native/ReactCommon`) + - React-RuntimeApple (from `../node_modules/react-native/ReactCommon/react/runtime/platform/ios`) + - React-RuntimeCore (from `../node_modules/react-native/ReactCommon/react/runtime`) - React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`) + - React-RuntimeHermes (from `../node_modules/react-native/ReactCommon/react/runtime`) - React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`) + - React-timing (from `../node_modules/react-native/ReactCommon/react/timing`) - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) + - React-viewtransitionnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/viewtransition`) + - React-webperformancenativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/webperformance`) + - ReactAppDependencyProvider (from `build/generated/ios/ReactAppDependencyProvider`) + - ReactCodegen (from `build/generated/ios/ReactCodegen`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - ReactNativeDependencies (from `../node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec`) - RNFS (from `../node_modules/react-native-fs`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) -SPEC REPOS: - trunk: - - fmt - - SocketRocket - EXTERNAL SOURCES: - boost: - :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" - DoubleConversion: - :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" FBLazyVector: :path: "../node_modules/react-native/Libraries/FBLazyVector" - FBReactNativeSpec: - :path: "../node_modules/react-native/React/FBReactNativeSpec" - glog: - :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" - RCT-Folly: - :podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" + hermes-engine: + :podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" + :tag: hermes-v250829098.0.14 + RCTDeprecation: + :path: "../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation" RCTRequired: - :path: "../node_modules/react-native/Libraries/RCTRequired" + :path: "../node_modules/react-native/Libraries/Required" + RCTSwiftUI: + :path: "../node_modules/react-native/ReactApple/RCTSwiftUI" + RCTSwiftUIWrapper: + :path: "../node_modules/react-native/ReactApple/RCTSwiftUIWrapper" RCTTypeSafety: :path: "../node_modules/react-native/Libraries/TypeSafety" React: :path: "../node_modules/react-native/" React-callinvoker: :path: "../node_modules/react-native/ReactCommon/callinvoker" - React-Codegen: - :path: build/generated/ios React-Core: :path: "../node_modules/react-native/" + React-Core-prebuilt: + :podspec: "../node_modules/react-native/React-Core-prebuilt.podspec" React-CoreModules: :path: "../node_modules/react-native/React/CoreModules" React-cxxreact: :path: "../node_modules/react-native/ReactCommon/cxxreact" React-debug: :path: "../node_modules/react-native/ReactCommon/react/debug" + React-defaultsnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/defaults" + React-domnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/dom" React-Fabric: :path: "../node_modules/react-native/ReactCommon" + React-FabricComponents: + :path: "../node_modules/react-native/ReactCommon" React-FabricImage: :path: "../node_modules/react-native/ReactCommon" + React-featureflags: + :path: "../node_modules/react-native/ReactCommon/react/featureflags" + React-featureflagsnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/featureflags" React-graphics: :path: "../node_modules/react-native/ReactCommon/react/renderer/graphics" + React-hermes: + :path: "../node_modules/react-native/ReactCommon/hermes" + React-idlecallbacksnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks" React-ImageManager: :path: "../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios" - React-jsc: - :path: "../node_modules/react-native/ReactCommon/jsc" + React-intersectionobservernativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/intersectionobserver" React-jserrorhandler: :path: "../node_modules/react-native/ReactCommon/jserrorhandler" React-jsi: @@ -1131,18 +1991,38 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/jsiexecutor" React-jsinspector: :path: "../node_modules/react-native/ReactCommon/jsinspector-modern" + React-jsinspectorcdp: + :path: "../node_modules/react-native/ReactCommon/jsinspector-modern/cdp" + React-jsinspectornetwork: + :path: "../node_modules/react-native/ReactCommon/jsinspector-modern/network" + React-jsinspectortracing: + :path: "../node_modules/react-native/ReactCommon/jsinspector-modern/tracing" + React-jsitooling: + :path: "../node_modules/react-native/ReactCommon/jsitooling" + React-jsitracing: + :path: "../node_modules/react-native/ReactCommon/hermes/executor/" React-logger: :path: "../node_modules/react-native/ReactCommon/logger" React-Mapbuffer: :path: "../node_modules/react-native/ReactCommon" + React-microtasksnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" + React-mutationobservernativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/mutationobserver" react-native-bs-diff-patch: :path: "../.." - React-nativeconfig: - :path: "../node_modules/react-native/ReactCommon" React-NativeModulesApple: :path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios" + React-networking: + :path: "../node_modules/react-native/ReactCommon/react/networking" + React-oscompat: + :path: "../node_modules/react-native/ReactCommon/oscompat" React-perflogger: :path: "../node_modules/react-native/ReactCommon/reactperflogger" + React-performancecdpmetrics: + :path: "../node_modules/react-native/ReactCommon/react/performance/cdpmetrics" + React-performancetimeline: + :path: "../node_modules/react-native/ReactCommon/react/performance/timeline" React-RCTActionSheet: :path: "../node_modules/react-native/Libraries/ActionSheetIOS" React-RCTAnimation: @@ -1153,88 +2033,138 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/Libraries/Blob" React-RCTFabric: :path: "../node_modules/react-native/React" + React-RCTFBReactNativeSpec: + :path: "../node_modules/react-native/React" React-RCTImage: :path: "../node_modules/react-native/Libraries/Image" React-RCTLinking: :path: "../node_modules/react-native/Libraries/LinkingIOS" React-RCTNetwork: :path: "../node_modules/react-native/Libraries/Network" + React-RCTRuntime: + :path: "../node_modules/react-native/React/Runtime" React-RCTSettings: :path: "../node_modules/react-native/Libraries/Settings" React-RCTText: :path: "../node_modules/react-native/Libraries/Text" React-RCTVibration: :path: "../node_modules/react-native/Libraries/Vibration" + React-rendererconsistency: + :path: "../node_modules/react-native/ReactCommon/react/renderer/consistency" + React-renderercss: + :path: "../node_modules/react-native/ReactCommon/react/renderer/css" React-rendererdebug: :path: "../node_modules/react-native/ReactCommon/react/renderer/debug" - React-rncore: - :path: "../node_modules/react-native/ReactCommon" + React-RuntimeApple: + :path: "../node_modules/react-native/ReactCommon/react/runtime/platform/ios" + React-RuntimeCore: + :path: "../node_modules/react-native/ReactCommon/react/runtime" React-runtimeexecutor: :path: "../node_modules/react-native/ReactCommon/runtimeexecutor" + React-RuntimeHermes: + :path: "../node_modules/react-native/ReactCommon/react/runtime" React-runtimescheduler: :path: "../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler" + React-timing: + :path: "../node_modules/react-native/ReactCommon/react/timing" React-utils: :path: "../node_modules/react-native/ReactCommon/react/utils" + React-viewtransitionnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/viewtransition" + React-webperformancenativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/webperformance" + ReactAppDependencyProvider: + :path: build/generated/ios/ReactAppDependencyProvider + ReactCodegen: + :path: build/generated/ios/ReactCodegen ReactCommon: :path: "../node_modules/react-native/ReactCommon" + ReactNativeDependencies: + :podspec: "../node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec" RNFS: :path: "../node_modules/react-native-fs" Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - boost: d3f49c53809116a5d38da093a8aa78bf551aed09 - DoubleConversion: fea03f2699887d960129cc54bba7e52542b6f953 - FBLazyVector: fbc4957d9aa695250b55d879c1d86f79d7e69ab4 - FBReactNativeSpec: 86de768f89901ef6ed3207cd686362189d64ac88 - fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 - glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2 - RCT-Folly: 7169b2b1c44399c76a47b5deaaba715eeeb476c0 - RCTRequired: 9b1e7e262745fb671e33c51c1078d093bd30e322 - RCTTypeSafety: a759e3b086eccf3e2cbf2493d22f28e082f958e6 - React: 805f5dd55bbdb92c36b4914c64aaae4c97d358dc - React-callinvoker: 6a697867607c990c2c2c085296ee32cfb5e47c01 - React-Codegen: ea685412377744d2e2108adb131fce2f040ddd14 - React-Core: d9a7e296b2e7b57f27d81b62a7d293d06527268a - React-CoreModules: 399f72f0892befe0fde5d415703c0b9b356762e7 - React-cxxreact: beb7cb8adddd4b25d4262f55927dcd1d3577e99d - React-debug: f1637bce73342b2f6eee4982508fdfb088667a87 - React-Fabric: cb2eba1fd764229f9a5737fa7b339695d4949848 - React-FabricImage: b86bf7e1c6560bf82ade361dac6f3281e68453ee - React-graphics: 87ba141b72379824c7835224c1a87993683151a8 - React-ImageManager: 1bc92d558d4d5de07c6c1a7244d33ad2a872728e - React-jsc: 94234736a90ea29f017f2ee76e5f358a6ba076a9 - React-jserrorhandler: 0b1476485be6d79f09a8f0548c355b1cc14e8f21 - React-jsi: dbfd3bab7712367d4c2aced271d794dde76f0d68 - React-jsiexecutor: 368e562638c31174479c00434d37fd67b761c15b - React-jsinspector: 03644c063fc3621c9a4e8bf263a8150909129618 - React-logger: b42a493ae72922d8d6a497038359240ffd457119 - React-Mapbuffer: 2cd1af67c3754dfb934948448125f0e051586db7 - react-native-bs-diff-patch: c053bbc33ece9b98a38292e413ab5829687c10b4 - React-nativeconfig: d753fbbc8cecc8ae413d615599ac378bbf6999bb - React-NativeModulesApple: 0c22e17930a2de06bbd4d49a149351a5151283dc - React-perflogger: 29efe63b7ef5fbaaa50ef6eaa92482f98a24b97e - React-RCTActionSheet: 69134c62aefd362027b20da01cd5d14ffd39db3f - React-RCTAnimation: ed774e28e707ce47b1e2dc6aa7f8f3267b815061 - React-RCTAppDelegate: b3312577f20a1c3aaf58ff6d561f3119c74b95b4 - React-RCTBlob: 8c173ce722daff128efb38f29a390984800f9302 - React-RCTFabric: a0a76ccfa863b02382cb74e04450e9f69cd1cf49 - React-RCTImage: 1f75f5d0539b381f70981386b1459fbb2c21eb9b - React-RCTLinking: febd566d57a9cb05a02f39771ccc8c20f8432e89 - React-RCTNetwork: d427de729372fd50d7cb601db64d0fcd9ea9a514 - React-RCTSettings: 73594c6c8c334c7d958cf98ac72335f3e4df9bf5 - React-RCTText: f1079c24f45cec6ddb6363c12ad87f9a940b2ddb - React-RCTVibration: 62420b57a47482d1b88dde64ba88d333f2625aab - React-rendererdebug: a474ec4cdfed75211dce6c3828de8391cc5c4280 - React-rncore: 74030de0ffef7b1a3fb77941168624534cc9ae7f - React-runtimeexecutor: 2d1f64f58193f00a3ad71d3f89c2bfbfe11cf5a5 - React-runtimescheduler: 60e37202d804ff618723c9d01d194fed91a44265 - React-utils: dd1e04ea7105dc58637939b50f1a74a5dd634ec4 - ReactCommon: 95988d0d48646507f3e952a02cab66e8d7f08872 + FBLazyVector: b3e7ad108f0d882e30445c5527d774e3fd432f3d + hermes-engine: 210cd97484a7ca3f831c1457a6a0351d1b2aaf9a + RCTDeprecation: 2a74a2c57675e64419bd89078efde81f7c1de90b + RCTRequired: 30451112e6fef4e6f31b4e7eee0845156e35e4b0 + RCTSwiftUI: 5aaf0b07e747ba749dc6acc94d8bd41eea4b570f + RCTSwiftUIWrapper: ab2ca548be15d63afa95103afc8685a7c3eab78b + RCTTypeSafety: 3eaed17dbddb0b989208b062ea14c44d412b9780 + React: 2574546f2d017abd14d0c9b48cf2b6a0547c2591 + React-callinvoker: 03cd4b931d1d583d87aae99b8f7b6fe26bf571ee + React-Core: 1c824d9c7dd8aa760b5f1b50d5a54c2a3f598f87 + React-Core-prebuilt: 13924a267683b3d6fa4bde9c80380becf83a9c5c + React-CoreModules: 2f9ed75bca7f6dea2b70e8a1f4a5ca9b6be52d76 + React-cxxreact: 7103d5ba69848c039e11079e74ceede05efe795e + React-debug: 8cc8d99ccc664ef9e873027f7fc3fb0a50496012 + React-defaultsnativemodule: 603c108411d39d7bb5ccb8c270f1f50cb6207e10 + React-domnativemodule: c49a502edc85f515a029c341fe5fba155fa6675c + React-Fabric: acc4915d719db793c5853e5c74b54ea5aa48e865 + React-FabricComponents: aebebc98914a4a8fc962fa7b5b98ebaf250acb68 + React-FabricImage: 42e99e48ff73c8b20cee229e622977d0b97b6247 + React-featureflags: c6d8d8d52acf3a956485726076b73eeff7935340 + React-featureflagsnativemodule: c992de92dcf30dcf09efdec17752abe4128ec55f + React-graphics: 239fd9b0512e539d3563f0825618f4e49795eefd + React-hermes: ae4685ca9fa5f47003bc594d3f146e29284136d1 + React-idlecallbacksnativemodule: 10a5be842ab181953c772a3f28cdf94572833eb0 + React-ImageManager: c36a56c3b13eba92e1d0d30da35d0a1ccf29cd6c + React-intersectionobservernativemodule: 71404bfa47d31e4143cc9db3e26178b5cfcd07e1 + React-jserrorhandler: ca2eeb03c1a77bbfab1b5425b943e3e8d92295f2 + React-jsi: c4b6daf8a31ac54f2db49cd6cce29720fec1f3a8 + React-jsiexecutor: acdc1a217b7ea29bcf6315b770d01e6b1c2fca14 + React-jsinspector: 95d6394efe9fb4a64ed33afc076b32a6384c8514 + React-jsinspectorcdp: 6ce51378a552feaaaac1a7b0d995fa0c49fa4f8c + React-jsinspectornetwork: 0db435c9264f200635fdf294a3b643dd3946d4cf + React-jsinspectortracing: 2e4470e1f301ff597bd65299aa95da4bae6e5c4f + React-jsitooling: 796bc991cdce68e2cc059d0271a28e0d4f8b0891 + React-jsitracing: f3008e7b5e1d9de8d9f2ca1b85824ed86b0cea00 + React-logger: fff73f4ceecad968c97baafdc77dcf84befc38b6 + React-Mapbuffer: ce449ccdf3b80384415b925606be8a4bdcfc65d3 + React-microtasksnativemodule: 2eb3f49d0d8e77b5343455eccd057010b8d38b6b + React-mutationobservernativemodule: f0a0d5ae9b51caf7becbeabf836d716cfedb6bf2 + react-native-bs-diff-patch: 327540677938edbd1b335b0685865c97ee3c9c6c + React-NativeModulesApple: a092d89b58f635ebfab88048b0eda9fb516819fd + React-networking: 968bbbe73590149feb1e72b2af4f6a68e4796ece + React-oscompat: 0b72a7e926954a0415ccd83e0748b6561fe45367 + React-perflogger: 865984e492514aa6e5279fc3e663132cfa4d5022 + React-performancecdpmetrics: 2efbf9bdb48c8d8446f4dd10e8bf0dc5d711772f + React-performancetimeline: 9d256484bff1513481be9f234baba694dc3b52e2 + React-RCTActionSheet: 902c79deec52f99cc48b1051b59bcbe86787d339 + React-RCTAnimation: 09cf722039ae30ff5d64e2b011ff054ae651c3b6 + React-RCTAppDelegate: a47de6fddb7eb0c028abba83138a5ce283bd7e77 + React-RCTBlob: 38c418d067e0c61818223503063310122e44c588 + React-RCTFabric: 480ca2a105730e1790cfd13291d086cb53725825 + React-RCTFBReactNativeSpec: 63131378510a5191515a4adfc308e65b465106f4 + React-RCTImage: 3ce36f82441b76b715818ee7ee95f6f5b34f9ee1 + React-RCTLinking: 2f7b5ed4983122e5115732d25a4360960eab583c + React-RCTNetwork: dcd3b180f33da86f5dc5e928a816eb5464fa7f16 + React-RCTRuntime: 6ef8e778fbab426b12eb5a9b5f7a0e86313c5a10 + React-RCTSettings: b97727ec8c55c35bd284457a647c938c040b942b + React-RCTText: 4d1b88f6d3e1a43afe46706d956ec6664c87b984 + React-RCTVibration: 415d14d6a1a64bf947fcef6b193915a494431168 + React-rendererconsistency: ef8519bdd9931261c6561bfad6506356b8108387 + React-renderercss: 1aa1bf99fa2ace143eb87d5190fd43609fc1e832 + React-rendererdebug: 40a4fb3dea21a7ac1759ca0eb6c88a924aecb075 + React-RuntimeApple: 84fadbb4fe8ca531e15e29a22af05911f17569c6 + React-RuntimeCore: f4d9af5f16d37e63308cb03b36c89d01e7f68a06 + React-runtimeexecutor: 4d59410f66af529d04c84c8b152ced07dabc471e + React-RuntimeHermes: fd719d8f4d9ce79636fe2e09e94b0ac31b7b263b + React-runtimescheduler: 784033620aa5515e2f45a60369eed4d32f9400b8 + React-timing: a16df9ae98f950396d9ce3abf43cb0cb2f21194c + React-utils: b68ee619aef28d8f9b85532d98db0327f7bdf743 + React-viewtransitionnativemodule: 11fe091101d381451b1542c37b2745900254f096 + React-webperformancenativemodule: b5d249419f1546663845c82f24de4e18a3180997 + ReactAppDependencyProvider: dcdd0e1b9559a6d8d8aea05286f4ed085091978e + ReactCodegen: afe0d436ee089d7bea6418429d8312b1d23a6801 + ReactCommon: d5c1bb4427bf51c443de5926aac332c89ddd9363 + ReactNativeDependencies: fa0a54b3f5319ae0e3b9aff32bfee7a424b88e66 RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8 - SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 - Yoga: 13c8ef87792450193e117976337b8527b49e8c03 + Yoga: fe50ab299e578f397fef753cf309c6703a4db29b -PODFILE CHECKSUM: 4abad8f5d0039c55400fe3dc57af4b12719ceb23 +PODFILE CHECKSUM: 84bfd8c0093bc661920962baf14e6677cb405e3e COCOAPODS: 1.17.0 diff --git a/example/metro.config.js b/example/metro.config.js index b823214..de00cc7 100644 --- a/example/metro.config.js +++ b/example/metro.config.js @@ -1,7 +1,6 @@ const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); const path = require('path'); const escape = require('escape-string-regexp'); -const exclusionList = require('metro-config/src/defaults/exclusionList'); const pak = require('../package.json'); const root = path.resolve(__dirname, '..'); @@ -19,11 +18,11 @@ const config = { // We need to make sure that only one version is loaded for peerDependencies // So we block them at the root, and alias them to the versions in example's node_modules resolver: { - blacklistRE: exclusionList( - modules.map( - (m) => - new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`) - ) + blockList: modules.map( + (moduleName) => + new RegExp( + `^${escape(path.join(root, 'node_modules', moduleName))}\\/.*$` + ) ), extraNodeModules: modules.reduce((acc, name) => { diff --git a/example/package.json b/example/package.json index c04cea2..2003204 100644 --- a/example/package.json +++ b/example/package.json @@ -11,21 +11,24 @@ "build:ios": "cd ios && xcodebuild -workspace BsDiffPatchExample.xcworkspace -scheme BsDiffPatchExample -configuration Debug -sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO" }, "dependencies": { - "react": "18.2.0", - "react-native": "0.73.2", + "react": "19.2.3", + "react-native": "0.86.0", "react-native-fs": "^2.20.0" }, "devDependencies": { - "@babel/core": "^7.20.0", - "@babel/preset-env": "^7.20.0", - "@babel/runtime": "^7.20.0", - "@react-native/babel-preset": "0.73.19", - "@react-native/metro-config": "0.73.3", - "@react-native/typescript-config": "0.73.1", + "@babel/core": "^7.25.2", + "@babel/preset-env": "^7.25.3", + "@babel/runtime": "^7.25.0", + "@react-native-community/cli": "20.2.0", + "@react-native-community/cli-platform-android": "20.2.0", + "@react-native-community/cli-platform-ios": "20.2.0", + "@react-native/babel-preset": "0.86.0", + "@react-native/metro-config": "0.86.0", + "@react-native/typescript-config": "0.86.0", "babel-plugin-module-resolver": "^5.0.0", "pod-install": "^0.1.0" }, "engines": { - "node": ">=18" + "node": ">=20.19.4" } } diff --git a/example/src/App.tsx b/example/src/App.tsx index 471854a..a0316af 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,7 +1,13 @@ import * as React from 'react'; import { StyleSheet, View, Text } from 'react-native'; -import { diff, patch } from 'react-native-bs-diff-patch'; +import { + diff, + patch, + startDiff, + startPatch, + type NativeOperationProgress, +} from 'react-native-bs-diff-patch'; import * as FS from 'react-native-fs'; import crossPlatformFixture from '../../fixtures/cross-platform.json'; @@ -24,10 +30,19 @@ export default function App() { FS.DocumentDirectoryPath + '/golden-generated.patch'; const corruptPatchFile = FS.DocumentDirectoryPath + '/corrupt.patch'; const corruptOutputFile = FS.DocumentDirectoryPath + '/corrupt-output.bin'; + const controlledOldFile = FS.DocumentDirectoryPath + '/controlled-old.bin'; + const controlledNewFile = FS.DocumentDirectoryPath + '/controlled-new.bin'; + const controlledPatchFile = FS.DocumentDirectoryPath + '/controlled.patch'; + const controlledOutputFile = + FS.DocumentDirectoryPath + '/controlled-output.bin'; + const cancelledPatchFile = FS.DocumentDirectoryPath + '/cancelled.patch'; + const limitedPatchFile = FS.DocumentDirectoryPath + '/limited.patch'; + const limitedOutputFile = FS.DocumentDirectoryPath + '/limited-output.bin'; const [textLength, setTextLength] = React.useState(); const [patchFileUri, setPatchFileUri] = React.useState(); const [runtimeStatus, setRuntimeStatus] = React.useState('running'); + const [controlsStatus, setControlsStatus] = React.useState('running'); React.useEffect(() => { const oldContent = new Array(1000).fill('Hello World').join(' | '); @@ -54,6 +69,13 @@ export default function App() { generatedGoldenPatchFile, corruptPatchFile, corruptOutputFile, + controlledOldFile, + controlledNewFile, + controlledPatchFile, + controlledOutputFile, + cancelledPatchFile, + limitedPatchFile, + limitedOutputFile, ]) { if (await FS.exists(file)) { await FS.unlink(file); @@ -132,16 +154,121 @@ export default function App() { throw new Error('corrupt patch left a partial output file'); } + const controlledOldContent = new Array(65_536) + .fill('native-controls-old') + .join('|'); + const controlledNewContent = `${controlledOldContent.slice( + 0, + -64 + )}native-controls-new-${Date.now()}`; + await FS.writeFile(controlledOldFile, controlledOldContent); + await FS.writeFile(controlledNewFile, controlledNewContent); + + const progressEvents: NativeOperationProgress[] = []; + const controlledDiff = startDiff( + controlledOldFile, + controlledNewFile, + controlledPatchFile, + { maxInputBytes: controlledOldContent.length * 2 } + ); + const unsubscribe = controlledDiff.onProgress((event) => { + progressEvents.push(event); + }); + try { + await controlledDiff.result; + } finally { + unsubscribe(); + } + if ( + progressEvents.length === 0 || + progressEvents.some( + (event, index) => + index > 0 && event.progress < progressEvents[index - 1]!.progress + ) + ) { + throw new Error('native progress was missing or non-monotonic'); + } + + const controlledPatch = startPatch( + controlledOldFile, + controlledOutputFile, + controlledPatchFile, + { maxOutputBytes: controlledNewContent.length * 2 } + ); + await controlledPatch.result; + if ( + (await FS.readFile(controlledOutputFile)) !== controlledNewContent + ) { + throw new Error('controlled patch did not restore the expected file'); + } + + const inputLimitCode = await startDiff( + controlledOldFile, + controlledNewFile, + limitedPatchFile, + { maxInputBytes: 1 } + ).result.then( + () => undefined, + (error: { code?: string }) => error.code + ); + if (inputLimitCode !== 'EINPUT_TOO_LARGE') { + throw new Error( + `input limit should reject with EINPUT_TOO_LARGE, got ${String( + inputLimitCode + )}` + ); + } + + const outputLimitCode = await startPatch( + controlledOldFile, + limitedOutputFile, + controlledPatchFile, + { maxOutputBytes: 1 } + ).result.then( + () => undefined, + (error: { code?: string }) => error.code + ); + if (outputLimitCode !== 'EOUTPUT_TOO_LARGE') { + throw new Error( + `output limit should reject with EOUTPUT_TOO_LARGE, got ${String( + outputLimitCode + )}` + ); + } + + const cancelledDiff = startDiff( + controlledOldFile, + controlledNewFile, + cancelledPatchFile + ); + const cancellationCode = cancelledDiff.result.then( + () => undefined, + (error: { code?: string }) => error.code + ); + await cancelledDiff.cancel(); + if ((await cancellationCode) !== 'ECANCELLED') { + throw new Error('cancelled operation should reject with ECANCELLED'); + } + if ( + (await FS.exists(cancelledPatchFile)) || + (await FS.exists(limitedPatchFile)) || + (await FS.exists(limitedOutputFile)) + ) { + throw new Error('cancelled or limited operation left an output file'); + } + if (!cancelled) { setPatchFileUri(patchFileInfo.path); setTextLength(patchedContent.length); setRuntimeStatus('success'); + setControlsStatus('success'); } } catch (error) { if (!cancelled) { const message = error instanceof Error ? error.message : String(error); setRuntimeStatus(`error: ${message}`); + setControlsStatus(`error: ${message}`); } } } @@ -164,11 +291,18 @@ export default function App() { }, [ corruptOutputFile, corruptPatchFile, + controlledNewFile, + controlledOldFile, + controlledOutputFile, + controlledPatchFile, + cancelledPatchFile, generatedGoldenPatchFile, goldenNewFile, goldenOldFile, goldenOutputFile, goldenPatchFile, + limitedOutputFile, + limitedPatchFile, newFile, newFile1, oldFile, @@ -181,6 +315,7 @@ export default function App() { Patch: {patchFileUri} Architecture: {architecture} Runtime: {runtimeStatus} + Controls: {controlsStatus} ); } diff --git a/ios/BsDiffPatch.h b/ios/BsDiffPatch.h index cc7ba6f..a053642 100644 --- a/ios/BsDiffPatch.h +++ b/ios/BsDiffPatch.h @@ -4,12 +4,13 @@ #ifdef RCT_NEW_ARCH_ENABLED #import "RNBsDiffPatchSpec.h" +#import -@interface BsDiffPatch : NSObject +@interface BsDiffPatch : RCTEventEmitter #else -#import +#import -@interface BsDiffPatch : NSObject +@interface BsDiffPatch : RCTEventEmitter #endif @end diff --git a/ios/BsDiffPatch.mm b/ios/BsDiffPatch.mm index bcc29a6..88567e1 100644 --- a/ios/BsDiffPatch.mm +++ b/ios/BsDiffPatch.mm @@ -1,10 +1,129 @@ #import "BsDiffPatch.h" -#ifdef RCT_NEW_ARCH_ENABLED +#import +#import +#import #import +#import +#import +#import + +#ifdef RCT_NEW_ARCH_ENABLED #import #endif +static NSString *const BsDiffPatchProgressEvent = @"BsDiffPatchProgress"; + +@class BsDiffPatch; + +namespace { + +struct OperationState { + std::atomic cancelled{false}; + __strong BsDiffPatch *module = nil; + __strong NSString *jobId = nil; + int lastPhase = -1; + std::chrono::steady_clock::time_point lastEmission{}; +}; + +std::mutex operationsMutex; +std::unordered_map> operations; + +dispatch_queue_t operationQueue() +{ + static dispatch_queue_t queue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + queue = dispatch_queue_create( + "com.jimmydaddy.bsdiffpatch.worker", + DISPATCH_QUEUE_SERIAL); + }); + return queue; +} + +std::string operationKey(NSString *jobId) +{ + return std::string([jobId UTF8String] ?: ""); +} + +int isCancelled(void *opaque) +{ + auto *state = static_cast(opaque); + return state->cancelled.load(std::memory_order_relaxed) ? 1 : 0; +} + +} // namespace + +@interface BsDiffPatch () + +@property(nonatomic, assign) BOOL hasProgressListeners; + +- (void)emitProgressForState:(OperationState *)state + phase:(int)phase + progress:(double)progress; + +@end + +namespace { + +void emitProgress(void *opaque, int phase, double progress) +{ + auto *state = static_cast(opaque); + auto now = std::chrono::steady_clock::now(); + bool phaseChanged = phase != state->lastPhase; + bool intervalElapsed = state->lastEmission.time_since_epoch().count() == 0 || + now - state->lastEmission >= std::chrono::milliseconds(100); + + if ((!phaseChanged && !intervalElapsed && progress < 1.0) || + state->cancelled.load(std::memory_order_relaxed)) { + return; + } + [state->module emitProgressForState:state phase:phase progress:progress]; + state->lastPhase = phase; + state->lastEmission = now; +} + +std::shared_ptr registerOperation(BsDiffPatch *module, NSString *jobId) +{ + std::string key = operationKey(jobId); + auto state = std::make_shared(); + state->module = module; + state->jobId = jobId; + std::lock_guard lock(operationsMutex); + if (key.empty() || operations.find(key) != operations.end()) { + return nullptr; + } + operations.emplace(key, state); + return state; +} + +void removeOperation(NSString *jobId) +{ + std::lock_guard lock(operationsMutex); + operations.erase(operationKey(jobId)); +} + +bool cancelOperation(NSString *jobId) +{ + std::lock_guard lock(operationsMutex); + auto operation = operations.find(operationKey(jobId)); + if (operation == operations.end()) { + return false; + } + operation->second->cancelled.store(true, std::memory_order_relaxed); + return true; +} + +void cancelAllOperations() +{ + std::lock_guard lock(operationsMutex); + for (const auto &operation : operations) { + operation.second->cancelled.store(true, std::memory_order_relaxed); + } +} + +} // namespace + @implementation BsDiffPatch RCT_EXPORT_MODULE() @@ -18,105 +137,333 @@ - (dispatch_queue_t)methodQueue static dispatch_queue_t queue; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - queue = dispatch_queue_create("com.jimmydaddy.bsdiffpatch.worker", DISPATCH_QUEUE_SERIAL); + queue = dispatch_queue_create( + "com.jimmydaddy.bsdiffpatch.module", + DISPATCH_QUEUE_SERIAL); }); return queue; } -// Example method -// See // https://reactnative.dev/docs/native-modules-ios -RCT_EXPORT_METHOD(patch:(NSString*) oldFile - newFile:(NSString*) newFile - patchFile:(NSString*) patchFile - resolve:(RCTPromiseResolveBlock)resolve - reject:(RCTPromiseRejectBlock)reject) +- (NSArray *)supportedEvents { - if (oldFile == nil || oldFile.length == 0 || newFile == nil || newFile.length == 0 || patchFile == nil || patchFile.length == 0) { - NSString *message = @"oldFile, newFile, and patchFile should not be nil or empty"; - reject(@"EINVAL", message, nil); - return; + return @[ BsDiffPatchProgressEvent ]; +} + +- (void)startObserving +{ + self.hasProgressListeners = YES; +} + +- (void)stopObserving +{ + self.hasProgressListeners = NO; +} + +- (void)invalidate +{ + cancelAllOperations(); + [super invalidate]; +} + +- (NSString *)normalizedPath:(NSString *)path +{ + return [path hasPrefix:@"file://"] ? [path substringFromIndex:7] : path; +} + +- (BOOL)validateOldFile:(NSString *)oldFile + newFile:(NSString *)newFile + patchFile:(NSString *)patchFile + reject:(RCTPromiseRejectBlock)reject +{ + if (oldFile == nil || oldFile.length == 0 || + newFile == nil || newFile.length == 0 || + patchFile == nil || patchFile.length == 0) { + reject(@"EINVAL", @"oldFile, newFile, and patchFile should not be nil or empty", nil); + return NO; } - if ([oldFile isEqualToString:newFile] || [oldFile isEqualToString:patchFile] || [newFile isEqualToString:patchFile]) { - NSString *message = @"oldFile, newFile, and patchFile should not be the same"; - reject(@"EINVAL", message, nil); - return; + if ([oldFile isEqualToString:newFile] || + [oldFile isEqualToString:patchFile] || + [newFile isEqualToString:patchFile]) { + reject(@"EINVAL", @"oldFile, newFile, and patchFile should not be the same", nil); + return NO; + } + return YES; +} + +- (BOOL)validateLimit:(double)value + name:(NSString *)name + reject:(RCTPromiseRejectBlock)reject +{ + if (value == 0) { + return YES; + } + if (!std::isfinite(value) || value < 1 || value > 9007199254740991.0 || + std::floor(value) != value) { + reject( + @"EINVAL", + [NSString stringWithFormat:@"%@ must be a positive safe integer", name], + nil); + return NO; } + return YES; +} +- (BOOL)validateFilesForOperation:(NSString *)operation + oldFile:(NSString *)oldFile + newFile:(NSString *)newFile + patchFile:(NSString *)patchFile + reject:(RCTPromiseRejectBlock)reject +{ NSFileManager *fileManager = [NSFileManager defaultManager]; + BOOL patch = [operation isEqualToString:@"patch"]; + NSString *secondInput = patch ? patchFile : newFile; + NSString *output = patch ? newFile : patchFile; + NSString *secondInputName = patch ? @"patchFile" : @"newFile"; + NSString *outputName = patch ? @"newFile" : @"patchFile"; + if (![fileManager fileExistsAtPath:oldFile]) { - NSString *message = [NSString stringWithFormat:@"oldFile: %@! does not exist", oldFile]; - reject(@"ENOENT", message, nil); - return; + reject(@"ENOENT", [NSString stringWithFormat:@"oldFile: %@ does not exist", oldFile], nil); + return NO; } - if ([fileManager fileExistsAtPath:newFile]) { - NSString *message = [NSString stringWithFormat:@"newFile: %@! already exists", newFile]; - reject(@"ENOENT", message, nil); - return; + if (![fileManager fileExistsAtPath:secondInput]) { + reject( + @"ENOENT", + [NSString stringWithFormat:@"%@: %@ does not exist", secondInputName, secondInput], + nil); + return NO; } - if (![fileManager fileExistsAtPath:patchFile]) { - NSString *message = [NSString stringWithFormat:@"patchFile: %@! does not exist", newFile]; - reject(@"ENOENT", message, nil); - return; + if ([fileManager fileExistsAtPath:output]) { + reject( + @"EEXIST", + [NSString stringWithFormat:@"%@: %@ already exists", outputName, output], + nil); + return NO; } + return YES; +} - const char *oldFileCString = [oldFile UTF8String]; - const char *newFileCString = [newFile UTF8String]; - const char *patchFileCString = [patchFile UTF8String]; - - int result = bsdiffpatch::patchFile(oldFileCString, newFileCString, patchFileCString); - if (result != 0) { - NSString *message = [NSString stringWithFormat:@"patch failed with native result %d", result]; - reject(@"EPATCH", message, nil); - return; +- (void)rejectOperation:(NSString *)operation + result:(int)result + maxInputBytes:(int64_t)maxInputBytes + maxOutputBytes:(int64_t)maxOutputBytes + reject:(RCTPromiseRejectBlock)reject +{ + NSString *code; + NSString *detail; + switch (result) { + case BS_OPERATION_INPUT_TOO_LARGE: + code = @"EINPUT_TOO_LARGE"; + detail = [NSString stringWithFormat: + @"configured input limit: %lld bytes", (long long)maxInputBytes]; + break; + case BS_OPERATION_OUTPUT_TOO_LARGE: + code = @"EOUTPUT_TOO_LARGE"; + detail = [NSString stringWithFormat: + @"configured output limit: %lld bytes", (long long)maxOutputBytes]; + break; + case BS_OPERATION_CANCELLED: + code = @"ECANCELLED"; + detail = @"operation was cancelled"; + break; + case BS_OPERATION_DESTINATION_EXISTS: + code = @"EEXIST"; + detail = @"destination or job already exists"; + break; + default: + code = [operation isEqualToString:@"patch"] ? @"EPATCH" : @"EDIFF"; + detail = [NSString stringWithFormat:@"native result %d", result]; + break; } - resolve(@(result)); + reject(code, [NSString stringWithFormat:@"%@ failed: %@", operation, detail], nil); } -RCT_EXPORT_METHOD(diff:(NSString*) oldFile - newFile:(NSString*) newFile - patchFile:(NSString*) patchFile - resolve:(RCTPromiseResolveBlock)resolve - reject:(RCTPromiseRejectBlock)reject) +- (void)runJob:(NSString *)operation + jobId:(NSString *)jobId + oldFile:(NSString *)oldFile + newFile:(NSString *)newFile + patchFile:(NSString *)patchFile + maxInputBytes:(double)maxInputBytes + maxOutputBytes:(double)maxOutputBytes + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { - if (oldFile == nil || oldFile.length == 0 || newFile == nil || newFile.length == 0 || patchFile == nil || patchFile.length == 0) { - NSString *message = @"oldFile, newFile, and patchFile should not be nil or empty"; - reject(@"EINVAL", message, nil); + if (jobId == nil || jobId.length == 0) { + reject(@"EINVAL", @"jobId should not be nil or empty", nil); return; } - if ([oldFile isEqualToString:newFile] || [oldFile isEqualToString:patchFile] || [newFile isEqualToString:patchFile]) { - NSString *message = @"oldFile, newFile, and patchFile should not be the same"; - reject(@"EINVAL", message, nil); + if (![self validateOldFile:oldFile newFile:newFile patchFile:patchFile reject:reject] || + ![self validateLimit:maxInputBytes name:@"maxInputBytes" reject:reject] || + ![self validateLimit:maxOutputBytes name:@"maxOutputBytes" reject:reject]) { return; } - NSFileManager *fileManager = [NSFileManager defaultManager]; - if (![fileManager fileExistsAtPath:oldFile]) { - NSString *message = [NSString stringWithFormat:@"oldFile: %@! does not exist", oldFile]; - reject(@"ENOENT", message, nil); - return; - } - if (![fileManager fileExistsAtPath:newFile]) { - NSString *message = [NSString stringWithFormat:@"newFile: %@! does not exist", newFile]; - reject(@"ENOENT", message, nil); + oldFile = [self normalizedPath:oldFile]; + newFile = [self normalizedPath:newFile]; + patchFile = [self normalizedPath:patchFile]; + if (![self validateFilesForOperation:operation + oldFile:oldFile + newFile:newFile + patchFile:patchFile + reject:reject]) { return; } - if ([fileManager fileExistsAtPath:patchFile]) { - NSString *message = [NSString stringWithFormat:@"patchFile: %@! already exists", newFile]; - reject(@"ENOENT", message, nil); + + auto state = registerOperation(self, jobId); + if (state == nullptr) { + reject(@"EEXIST", [NSString stringWithFormat:@"jobId: %@ already exists", jobId], nil); return; } - const char *oldFileCString = [oldFile UTF8String]; - const char *newFileCString = [newFile UTF8String]; - const char *patchFileCString = [patchFile UTF8String]; + dispatch_async(operationQueue(), ^{ + int result; + if (state->cancelled.load(std::memory_order_relaxed)) { + result = BS_OPERATION_CANCELLED; + } else { + bs_operation_options options{}; + options.max_input_bytes = (int64_t)maxInputBytes; + options.max_output_bytes = (int64_t)maxOutputBytes; + options.opaque = state.get(); + options.is_cancelled = isCancelled; + options.progress = emitProgress; + result = [operation isEqualToString:@"patch"] + ? bsdiffpatch::patchFileWithOptions( + [oldFile UTF8String], + [newFile UTF8String], + [patchFile UTF8String], + &options) + : bsdiffpatch::diffFileWithOptions( + [oldFile UTF8String], + [newFile UTF8String], + [patchFile UTF8String], + &options); + } + removeOperation(jobId); + + if (result == BS_OPERATION_OK) { + resolve(@(result)); + } else { + [self rejectOperation:operation + result:result + maxInputBytes:(int64_t)maxInputBytes + maxOutputBytes:(int64_t)maxOutputBytes + reject:reject]; + } + }); +} + +RCT_EXPORT_METHOD(patch:(NSString *)oldFile + newFile:(NSString *)newFile + patchFile:(NSString *)patchFile + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + if (![self validateOldFile:oldFile newFile:newFile patchFile:patchFile reject:reject]) return; + oldFile = [self normalizedPath:oldFile]; + newFile = [self normalizedPath:newFile]; + patchFile = [self normalizedPath:patchFile]; + if (![self validateFilesForOperation:@"patch" + oldFile:oldFile + newFile:newFile + patchFile:patchFile + reject:reject]) return; + + dispatch_async(operationQueue(), ^{ + int result = bsdiffpatch::patchFile( + [oldFile UTF8String], [newFile UTF8String], [patchFile UTF8String]); + if (result == 0) resolve(@(result)); + else reject(@"EPATCH", [NSString stringWithFormat:@"patch failed with native result %d", result], nil); + }); +} + +RCT_EXPORT_METHOD(diff:(NSString *)oldFile + newFile:(NSString *)newFile + patchFile:(NSString *)patchFile + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + if (![self validateOldFile:oldFile newFile:newFile patchFile:patchFile reject:reject]) return; + oldFile = [self normalizedPath:oldFile]; + newFile = [self normalizedPath:newFile]; + patchFile = [self normalizedPath:patchFile]; + if (![self validateFilesForOperation:@"diff" + oldFile:oldFile + newFile:newFile + patchFile:patchFile + reject:reject]) return; + + dispatch_async(operationQueue(), ^{ + int result = bsdiffpatch::diffFile( + [oldFile UTF8String], [newFile UTF8String], [patchFile UTF8String]); + if (result == 0) resolve(@(result)); + else reject(@"EDIFF", [NSString stringWithFormat:@"diff failed with native result %d", result], nil); + }); +} - int result = bsdiffpatch::diffFile(oldFileCString, newFileCString, patchFileCString); - if (result != 0) { - NSString *message = [NSString stringWithFormat:@"diff failed with native result %d", result]; - reject(@"EDIFF", message, nil); +RCT_EXPORT_METHOD(startPatch:(NSString *)jobId + oldFile:(NSString *)oldFile + newFile:(NSString *)newFile + patchFile:(NSString *)patchFile + maxInputBytes:(double)maxInputBytes + maxOutputBytes:(double)maxOutputBytes + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + [self runJob:@"patch" + jobId:jobId + oldFile:oldFile + newFile:newFile + patchFile:patchFile + maxInputBytes:maxInputBytes + maxOutputBytes:maxOutputBytes + resolve:resolve + reject:reject]; +} + +RCT_EXPORT_METHOD(startDiff:(NSString *)jobId + oldFile:(NSString *)oldFile + newFile:(NSString *)newFile + patchFile:(NSString *)patchFile + maxInputBytes:(double)maxInputBytes + maxOutputBytes:(double)maxOutputBytes + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + [self runJob:@"diff" + jobId:jobId + oldFile:oldFile + newFile:newFile + patchFile:patchFile + maxInputBytes:maxInputBytes + maxOutputBytes:maxOutputBytes + resolve:resolve + reject:reject]; +} + +RCT_EXPORT_METHOD(cancel:(NSString *)jobId + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + if (jobId == nil || jobId.length == 0) { + reject(@"EINVAL", @"jobId should not be nil or empty", nil); return; } - resolve(@(result)); + resolve(@(cancelOperation(jobId))); +} + +- (void)emitProgressForState:(OperationState *)state + phase:(int)phase + progress:(double)progress +{ + if (!self.hasProgressListeners) return; + NSString *phaseName = phase == BS_OPERATION_READING + ? @"reading" + : (phase == BS_OPERATION_PROCESSING ? @"processing" : @"writing"); + [self sendEventWithName:BsDiffPatchProgressEvent + body:@{ + @"id" : state->jobId, + @"phase" : phaseName, + @"progress" : @(MAX(0.0, MIN(1.0, progress))) + }]; } #ifdef RCT_NEW_ARCH_ENABLED diff --git a/lefthook.yml b/lefthook.yml index b22aaa7..32bb01d 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -3,12 +3,12 @@ pre-commit: commands: lint: glob: "*.{js,ts,jsx,tsx}" - run: npx eslint {staged_files} + run: node node_modules/eslint/bin/eslint.js {staged_files} types: - glob: "*.{js,ts, jsx, tsx}" - run: npx tsc --noEmit + glob: "*.{js,ts,jsx,tsx}" + run: node node_modules/typescript/bin/tsc --noEmit commit-msg: parallel: true commands: commitlint: - run: npx commitlint --edit + run: node node_modules/@commitlint/cli/cli.js --edit diff --git a/package.json b/package.json index 784e215..6d71ae5 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "!**/__mocks__", "!cpp/benchmark", "!cpp/fuzz", + "!cpp/tests", "!**/.*" ], "scripts": { @@ -53,6 +54,7 @@ "test:registry:vite": "node scripts/test-registry-consumers.mjs vite", "test:registry:expo": "node scripts/test-registry-consumers.mjs expo", "test:fuzz": "sh scripts/test-native-fuzz.sh", + "test:native-operations": "sh scripts/test-native-operations.sh", "benchmark:web": "node scripts/benchmark-web.mjs", "benchmark:native": "node scripts/benchmark-native.mjs", "site:build": "node scripts/build-site.mjs", @@ -87,34 +89,50 @@ "registry": "https://registry.npmjs.org/" }, "devDependencies": { - "@commitlint/config-conventional": "^17.0.2", + "@babel/core": "^7.25.2", + "@commitlint/config-conventional": "^20.5.0", "@evilmartians/lefthook": "^1.5.0", - "@react-native/eslint-config": "^0.72.2", - "@release-it/conventional-changelog": "^8.0.2", - "@types/jest": "^28.1.2", - "@types/react": "~17.0.21", - "@types/react-native": "0.70.0", - "commitlint": "^17.0.2", + "@react-native-community/cli": "20.2.0", + "@react-native-community/cli-platform-android": "20.2.0", + "@react-native-community/cli-platform-ios": "20.2.0", + "@react-native/babel-preset": "0.86.0", + "@react-native/eslint-config": "0.86.0", + "@react-native/jest-preset": "0.86.0", + "@release-it/conventional-changelog": "^11.0.1", + "@types/jest": "^29.5.14", + "@types/react": "^19.2.0", + "commitlint": "^20.5.0", "del-cli": "^5.0.0", - "eslint": "^8.4.1", + "eslint": "8.57.1", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.0.0", - "jest": "^28.1.1", + "jest": "^29.7.0", "pod-install": "^0.1.0", "prettier": "^2.0.5", "puppeteer-core": "^22.15.0", - "react": "18.2.0", - "react-native": "0.73.2", + "react": "19.2.3", + "react-native": "0.86.0", "react-native-builder-bob": "^0.20.0", - "release-it": "^17.11.0", + "release-it": "^20.2.1", "turbo": "^2.10.5", - "typescript": "^5.0.2" + "typescript": "^5.8.3" }, "resolutions": { - "@types/react": "17.0.21", + "@types/react": "19.2.7", + "@commitlint/config-validator/ajv": "8.20.0", + "@eslint/eslintrc/ajv": "6.15.0", + "@react-native-community/cli-config/joi": "17.13.4", + "@react-native-community/cli-doctor/yaml": "2.9.0", + "@react-native-community/cli-types/joi": "17.13.4", + "cosmiconfig/yaml": "1.10.3", + "eslint/ajv": "6.15.0", + "fast-xml-parser": "5.10.1", + "metro/image-size": "1.2.1", + "node-gyp": "13.0.1", "lodash": "4.18.1", "on-headers": "1.1.0", - "tmp": "0.2.6" + "socks": "2.8.9", + "regenerator-transform/@babel/runtime": "7.29.7" }, "peerDependencies": { "react": "*", @@ -133,7 +151,7 @@ ], "packageManager": "yarn@3.6.1", "engines": { - "node": ">= 18.0.0" + "node": ">= 20.19.4" }, "jest": { "preset": "react-native", @@ -171,6 +189,9 @@ "@react-native", "prettier" ], + "plugins": [ + "prettier" + ], "rules": { "prettier/prettier": [ "error", diff --git a/react-native-bs-diff-patch.podspec b/react-native-bs-diff-patch.podspec index 945b5da..aa81982 100644 --- a/react-native-bs-diff-patch.podspec +++ b/react-native-bs-diff-patch.podspec @@ -15,7 +15,7 @@ Pod::Spec.new do |s| s.source = { :git => "https://github.com/JimmyDaddy/react-native-bs-diff-patch.git", :tag => "#{s.version}" } s.source_files = "ios/**/*.{h,m,mm}", "cpp/**/*.{hpp,cpp,c,h}" - s.exclude_files = "cpp/benchmark/**/*", "cpp/fuzz/**/*" + s.exclude_files = "cpp/benchmark/**/*", "cpp/fuzz/**/*", "cpp/tests/**/*" # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79. diff --git a/scripts/build-site.mjs b/scripts/build-site.mjs index 7956fa0..0792b48 100644 --- a/scripts/build-site.mjs +++ b/scripts/build-site.mjs @@ -46,9 +46,9 @@ const pages = [ }, { slug: 'native-operations-v03', - title: 'Native operations 0.3', + title: 'Controllable native operations', description: - 'Planned resource limits, cancellation, progress, and atomic output contract.', + 'Native resource limits, cancellation, progress, and atomic output behavior.', file: 'native-operations-v03.md', }, { @@ -100,8 +100,8 @@ const chinesePages = [ }, { slug: 'native-operations-v03', - title: '0.3 原生操作', - description: '规划中的资源限制、取消、进度与原子输出约定。', + title: '可控制的原生操作', + description: '原生资源限制、取消、进度与原子输出行为。', file: 'native-operations-v03.md', }, { @@ -128,7 +128,7 @@ function escapeHtml(value) { } function normalizeDocumentationLink(href) { - const markdownMatch = href.match(/^\.\/([a-z-]+)\.md(#[\w-]+)?$/); + const markdownMatch = href.match(/^\.\/([a-z0-9-]+)\.md(#[\w-]+)?$/); if (markdownMatch) { return `/docs/${markdownMatch[1]}/${markdownMatch[2] || ''}`; } diff --git a/scripts/test-native-operations.sh b/scripts/test-native-operations.sh new file mode 100755 index 0000000..7bb1510 --- /dev/null +++ b/scripts/test-native-operations.sh @@ -0,0 +1,29 @@ +#!/bin/sh +set -eu + +repository_directory=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +temporary_directory=$(mktemp -d) +trap 'rm -rf "$temporary_directory"' EXIT INT TERM + +feature_test_macro=-D_POSIX_C_SOURCE=200809L +if [ "$(uname -s)" = "Darwin" ]; then + feature_test_macro=-D_DARWIN_C_SOURCE +fi + +cc -std=c11 "$feature_test_macro" -O2 -Wall -Wextra -Werror \ + -Wno-implicit-fallthrough \ + -Wno-unused-parameter \ + -I "$repository_directory/cpp" \ + "$repository_directory/cpp/tests/native_operations_test.c" \ + "$repository_directory/cpp/bsdiff.c" \ + "$repository_directory/cpp/bspatch.c" \ + "$repository_directory/cpp/bzlib/blocksort.c" \ + "$repository_directory/cpp/bzlib/bzlib.c" \ + "$repository_directory/cpp/bzlib/compress.c" \ + "$repository_directory/cpp/bzlib/crctable.c" \ + "$repository_directory/cpp/bzlib/decompress.c" \ + "$repository_directory/cpp/bzlib/huffman.c" \ + "$repository_directory/cpp/bzlib/randtable.c" \ + -o "$temporary_directory/native-operations-test" + +"$temporary_directory/native-operations-test" diff --git a/scripts/test-package-consumers.mjs b/scripts/test-package-consumers.mjs index c9b72d9..e993855 100644 --- a/scripts/test-package-consumers.mjs +++ b/scripts/test-package-consumers.mjs @@ -167,11 +167,11 @@ try { 'const moduleValue = { diff: async () => 0, patch: async () => 0 };\n'; await writeFile( path.join(fakeReactNativeDirectory, 'index.mjs'), - `${fakeRegistry}export const TurboModuleRegistry = { getEnforcing: () => moduleValue };\n` + `${fakeRegistry}export class NativeEventEmitter { addListener() { return { remove() {} }; } }\nexport const TurboModuleRegistry = { getEnforcing: () => moduleValue };\n` ); await writeFile( path.join(fakeReactNativeDirectory, 'index.cjs'), - `${fakeRegistry}exports.TurboModuleRegistry = { getEnforcing: () => moduleValue };\n` + `${fakeRegistry}exports.NativeEventEmitter = class NativeEventEmitter { addListener() { return { remove() {} }; } };\nexports.TurboModuleRegistry = { getEnforcing: () => moduleValue };\n` ); const installedPackageDirectory = path.join( diff --git a/scripts/test-rn-android-compatibility.sh b/scripts/test-rn-android-compatibility.sh index 9c0e5bb..031d4ec 100755 --- a/scripts/test-rn-android-compatibility.sh +++ b/scripts/test-rn-android-compatibility.sh @@ -18,7 +18,13 @@ else kotlin_version=1.9.22 fi -"$repository_directory/example/android/gradlew" \ +gradle_executable="${GRADLE_EXECUTABLE:-gradle}" +if ! command -v "$gradle_executable" >/dev/null 2>&1; then + echo "Gradle 8.3 is required; set GRADLE_EXECUTABLE or install Gradle 8.3" >&2 + exit 1 +fi + +"$gradle_executable" \ -p "$repository_directory/compatibility/android-api" \ --no-daemon \ --stacktrace \ diff --git a/scripts/test-rn-ios-compatibility.sh b/scripts/test-rn-ios-compatibility.sh index e5aa6c7..3922c80 100755 --- a/scripts/test-rn-ios-compatibility.sh +++ b/scripts/test-rn-ios-compatibility.sh @@ -2,12 +2,21 @@ set -eu -if [ "$#" -ne 1 ]; then - echo "Usage: $0 " >&2 +if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then + echo "Usage: $0 [new|old]" >&2 exit 2 fi react_native_version="$1" +architecture="${2:-new}" +case "$architecture" in + new) new_arch_enabled=1 ;; + old) new_arch_enabled=0 ;; + *) + echo "Unsupported iOS architecture fixture: $architecture" >&2 + exit 2 + ;; +esac repository_directory=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) temporary_directory=$(mktemp -d) consumer_directory="$temporary_directory/consumer" @@ -73,7 +82,7 @@ npm install \ ( cd "$consumer_directory/ios" - RCT_NEW_ARCH_ENABLED=1 \ + RCT_NEW_ARCH_ENABLED="$new_arch_enabled" \ BUNDLE_GEMFILE="$repository_directory/example/Gemfile" \ bundle exec pod install diff --git a/scripts/test-site-browser.mjs b/scripts/test-site-browser.mjs index 0344d9e..5a8293f 100644 --- a/scripts/test-site-browser.mjs +++ b/scripts/test-site-browser.mjs @@ -96,6 +96,31 @@ try { assert.match(result.status || '', /verified byte-for-byte/); assert.equal(result.evidenceRows, 3); + await page.select('#max-input-bytes', '64'); + await page.click('#generate-patch'); + await page.waitForSelector('#playground-status[data-state="error"]'); + assert.equal( + await page.$eval('#error-code', (element) => element.textContent), + 'ERESOURCE' + ); + + await page.select('#max-input-bytes', ''); + await page.$eval('#old-payload', (element) => { + element.value = 'a'.repeat(4 * 1024 * 1024); + element.dispatchEvent(new Event('input', { bubbles: true })); + }); + await page.$eval('#new-payload', (element) => { + element.value = `${'a'.repeat(4 * 1024 * 1024 - 1)}b`; + element.dispatchEvent(new Event('input', { bubbles: true })); + }); + await page.click('#generate-patch'); + await page.waitForSelector('#cancel-operation:not([disabled])'); + await page.click('#cancel-operation'); + await page.waitForFunction( + () => document.querySelector('#error-code')?.textContent === 'EABORTED', + { timeout: 30_000 } + ); + await page.setViewport({ width: 390, height: 844, deviceScaleFactor: 1 }); await page.reload({ waitUntil: 'networkidle0' }); const mobile = await page.evaluate(() => ({ diff --git a/scripts/test-site.mjs b/scripts/test-site.mjs index 3db4514..8d5a45a 100644 --- a/scripts/test-site.mjs +++ b/scripts/test-site.mjs @@ -155,10 +155,16 @@ const homepage = await readFile( ); assert.match(homepage, /id="playground"/); assert.match(homepage, /id="generate-patch"/); +assert.match(homepage, /id="cancel-operation"/); +assert.match(homepage, /id="max-input-bytes"/); +assert.match(homepage, /id="max-output-bytes"/); +assert.match(homepage, /startPatch/); +assert.match(homepage, /EINPUT_TOO_LARGE/); assert.match(homepage, /id="evidence"/); assert.match(homepage, /RN 0\.73\.11/); assert.match(homepage, /RN 0\.86\.0/); -assert.match(homepage, /111 KiB packed/); +assert.match(homepage, /122 KiB packed/); +assert.match(homepage, /452 KiB unpacked · 58 files/); assert.match(homepage, /30,697\.5 ms/); assert.match(homepage, /assets\/playground\.js/); diff --git a/scripts/test-web-metro.mjs b/scripts/test-web-metro.mjs index b406458..01f9bd4 100644 --- a/scripts/test-web-metro.mjs +++ b/scripts/test-web-metro.mjs @@ -14,12 +14,13 @@ const bundleBase = path.join( const bundlePath = `${bundleBase}.js`; const metroPath = path.join( repositoryDirectory, - 'example/node_modules/.bin/metro' + 'example/node_modules/metro/src/cli.js' ); const result = spawnSync( - metroPath, + process.execPath, [ + metroPath, 'build', '../scripts/web-metro-entry.js', '--platform', @@ -39,7 +40,9 @@ const result = spawnSync( if (result.status !== 0) { throw new Error( - `Metro Web build failed:\n${result.stdout || ''}\n${result.stderr || ''}` + `Metro Web build failed:\n${result.error || ''}\n${ + result.stdout || '' + }\n${result.stderr || ''}` ); } diff --git a/site/assets/playground.js b/site/assets/playground.js index 6580505..ce86848 100644 --- a/site/assets/playground.js +++ b/site/assets/playground.js @@ -10,12 +10,20 @@ const newSize = document.querySelector('#new-size'); const patchSize = document.querySelector('#patch-size'); const transferSaved = document.querySelector('#transfer-saved'); const runtimeMs = document.querySelector('#runtime-ms'); +const errorCode = document.querySelector('#error-code'); const runtimeState = document.querySelector('#runtime-state'); const status = document.querySelector('#playground-status'); const generateButton = document.querySelector('#generate-patch'); const downloadButton = document.querySelector('#download-patch'); +const cancelButton = document.querySelector('#cancel-operation'); +const maxInputBytes = document.querySelector('#max-input-bytes'); +const maxOutputBytes = document.querySelector('#max-output-bytes'); +const operationPhase = document.querySelector('#operation-phase'); +const operationPercent = document.querySelector('#operation-percent'); +const operationProgress = document.querySelector('#operation-progress'); let currentPatch; +let activeController; function formatBytes(bytes) { if (bytes < 1024) { @@ -34,6 +42,17 @@ function setStatus(state, message) { status.lastChild.textContent = message; } +function setProgress(phase, progress) { + operationPhase.textContent = phase; + operationPercent.value = `${Math.round(progress * 100)}%`; + operationProgress.value = progress; + operationProgress.textContent = operationPercent.value; +} + +function selectedLimit(select) { + return select.value ? Number(select.value) : undefined; +} + function bytesEqual(left, right) { return ( left.byteLength === right.byteLength && @@ -46,15 +65,25 @@ async function generatePatch() { const newData = encoder.encode(newPayload.value); generateButton.disabled = true; + cancelButton.disabled = false; downloadButton.disabled = true; generateButton.classList.add('is-running'); + errorCode.textContent = '—'; + activeController = new AbortController(); + const options = { + signal: activeController.signal, + maxInputBytes: selectedLimit(maxInputBytes), + maxOutputBytes: selectedLimit(maxOutputBytes), + }; + setProgress('Diffing', 0.2); setStatus('running', 'Generating patch in the Web Worker…'); const startedAt = performance.now(); try { - const patchData = await diffBytes(oldData, newData); - const restoredData = await patchBytes(oldData, patchData); + const patchData = await diffBytes(oldData, newData, options); + setProgress('Verifying', 0.65); + const restoredData = await patchBytes(oldData, patchData, options); if (!bytesEqual(restoredData, newData)) { throw new Error('The reconstructed payload did not match the target'); @@ -73,23 +102,42 @@ async function generatePatch() { : `${Math.abs(saved).toFixed(1)}% overhead`; transferSaved.classList.toggle('negative', saved < 0); runtimeMs.textContent = `${elapsed.toFixed(2)} ms`; + errorCode.textContent = '—'; downloadButton.disabled = false; + setProgress('Complete', 1); setStatus('success', 'Round trip verified byte-for-byte'); } catch (error) { currentPatch = undefined; patchSize.textContent = '—'; transferSaved.textContent = '—'; runtimeMs.textContent = '—'; + const code = + typeof error === 'object' && error !== null && 'code' in error + ? String(error.code) + : 'EUNKNOWN'; + errorCode.textContent = code; + setProgress(code === 'EABORTED' ? 'Cancelled' : 'Rejected', 0); setStatus( 'error', - error instanceof Error ? error.message : 'The patch operation failed' + error instanceof Error + ? `[${code}] ${error.message}` + : `[${code}] The patch operation failed` ); } finally { + activeController = undefined; generateButton.disabled = false; + cancelButton.disabled = true; generateButton.classList.remove('is-running'); } } +function cancelOperation() { + if (!activeController) return; + activeController.abort(); + cancelButton.disabled = true; + setStatus('running', 'Cancellation requested…'); +} + function downloadPatch() { if (!currentPatch) { return; @@ -108,6 +156,7 @@ function downloadPatch() { oldPayload.addEventListener('input', updateInputSizes); newPayload.addEventListener('input', updateInputSizes); generateButton.addEventListener('click', generatePatch); +cancelButton.addEventListener('click', cancelOperation); downloadButton.addEventListener('click', downloadPatch); updateInputSizes(); diff --git a/site/assets/site.css b/site/assets/site.css index 0808a0b..a939db5 100644 --- a/site/assets/site.css +++ b/site/assets/site.css @@ -502,13 +502,84 @@ code { .play-actions { min-height: 90px; display: grid; - grid-template-columns: auto 1fr auto; + grid-template-columns: auto auto 1fr auto; align-items: center; gap: 20px; padding: 18px 20px; border-bottom: 1px solid var(--line); } +.danger-button:hover:not(:disabled) { + color: var(--danger); + border-color: var(--danger); +} + +.operation-controls { + display: grid; + grid-template-columns: minmax(150px, 0.55fr) minmax(150px, 0.55fr) minmax(240px, 1fr); + gap: 12px; + padding: 16px 20px; + border-bottom: 1px solid var(--line); +} + +.operation-controls label, +.operation-progress { + min-width: 0; + display: grid; + gap: 9px; + color: var(--muted); + font: 700 9px/1 var(--mono); + letter-spacing: 0.07em; + text-transform: uppercase; +} + +.operation-controls select { + width: 100%; + min-height: 38px; + padding: 0 10px; + color: #c6d1d4; + background: var(--panel-black); + border: 1px solid var(--line-bright); + border-radius: 0; + font: 600 10px/1 var(--mono); +} + +.operation-controls select:focus { + border-color: var(--cyan); + outline: none; +} + +.operation-progress > span { + display: flex; + justify-content: space-between; + gap: 16px; +} + +.operation-progress strong { + color: var(--cyan); +} + +.operation-progress progress { + width: 100%; + height: 10px; + overflow: hidden; + appearance: none; + background: var(--panel-black); + border: 1px solid var(--line-bright); +} + +.operation-progress progress::-webkit-progress-bar { + background: var(--panel-black); +} + +.operation-progress progress::-webkit-progress-value { + background: var(--cyan); +} + +.operation-progress progress::-moz-progress-bar { + background: var(--cyan); +} + .primary-button, .primary-link { min-height: 48px; @@ -595,12 +666,106 @@ code { .metrics { display: grid; - grid-template-columns: repeat(3, 1fr); + grid-template-columns: repeat(4, 1fr); gap: 12px; padding: 18px 20px; border-bottom: 1px solid var(--line); } +.control-showcase { + display: grid; + grid-template-columns: minmax(260px, 0.7fr) minmax(0, 1.3fr); + border-bottom: 1px solid var(--line); +} + +.control-showcase > header { + padding: 52px 34px; + border-right: 1px solid var(--line); +} + +.control-showcase h2 { + margin: 0; + font-size: clamp(28px, 3vw, 42px); + line-height: 1.02; + letter-spacing: -0.045em; +} + +.control-showcase header > p:last-child { + max-width: 520px; + margin: 24px 0 0; + color: var(--muted); + line-height: 1.7; +} + +.control-code-grid { + display: grid; + grid-template-columns: 1fr 1fr; + min-width: 0; +} + +.control-code-grid article { + min-width: 0; + background: var(--panel-black); + border-right: 1px solid var(--line); +} + +.control-code-grid article:last-child { + border-right: 0; +} + +.code-label { + min-height: 58px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 0 18px; + color: var(--muted); + border-bottom: 1px solid var(--line); + font: 700 9px/1 var(--mono); + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.code-label strong { + color: var(--lime); +} + +.control-code-grid pre { + min-height: 300px; + margin: 0; + padding: 24px 18px; + overflow: auto; + color: #c6d1d4; + font: 500 10px/1.7 var(--mono); +} + +.error-contract { + grid-column: 1 / -1; + display: grid; + grid-template-columns: repeat(3, 1fr); + border-top: 1px solid var(--line); +} + +.error-contract span { + min-width: 0; + padding: 14px 18px; + color: var(--muted); + border-right: 1px solid var(--line); + border-bottom: 1px solid var(--line); + font: 600 9px/1.45 var(--mono); +} + +.error-contract span:nth-child(3n) { + border-right: 0; +} + +.error-contract code { + display: block; + margin-bottom: 5px; + color: var(--orange); +} + .metric { min-width: 0; min-height: 88px; @@ -632,6 +797,11 @@ code { font-size: 14px; } +.metric #error-code { + overflow-wrap: anywhere; + font-size: 13px; +} + .playground-footer { min-height: 48px; display: flex; @@ -1288,6 +1458,15 @@ code { grid-template-columns: 1fr; } + .control-showcase { + grid-template-columns: 1fr; + } + + .control-showcase > header { + border-right: 0; + border-bottom: 1px solid var(--line); + } + .architecture-intro { border-right: 0; border-bottom: 1px solid var(--line); @@ -1404,6 +1583,16 @@ code { grid-template-columns: 1fr 1fr; } + .operation-controls, + .control-code-grid { + grid-template-columns: 1fr; + } + + .control-code-grid article { + border-right: 0; + border-bottom: 1px solid var(--line); + } + .worker-note { order: 3; grid-column: 1 / -1; @@ -1557,6 +1746,16 @@ code { .metrics { padding-inline: 14px; + grid-template-columns: repeat(2, 1fr); + } + + .error-contract { + grid-template-columns: 1fr; + } + + .error-contract span, + .error-contract span:nth-child(3n) { + border-right: 0; } .playground-footer { diff --git a/site/index.html b/site/index.html index 031be57..8044471 100644 --- a/site/index.html +++ b/site/index.html @@ -152,6 +152,14 @@

Patch playground

+
Worker isolated No payload data leaves this page. @@ -166,6 +174,31 @@

Patch playground

+
+ + +
+ Idle0% + 0% +
+
+
Patch size @@ -179,6 +212,10 @@

Patch playground

Runtime
+
+ Error code + +