Repository navigation
Commit 1260b11
refactor: cross-layer cleanup and sink duplicated native logic into the shared C++ cores (#657)
* chore(lint): check C++ constant mirrors on Android and JS too
Move check-constant-parity.js from harmony/pushy/src/test to scripts/ and
extend it to compare archive_limits.h / install_record.h / error_codes.h
against ArchiveLimits.java / InstallRecord.java / ErrorCodes.java and the
error codes known to src/error.ts. Runs as part of bun lint.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(flow-core): drop Object.hasOwn, missing from JSC before iOS 15.4
jsonDeepEqual (native config equivalence, C1) called Object.hasOwn, which
throws on iOS < 15.4 JSC. Use Object.prototype.hasOwnProperty.call and add a
test that runs the comparison with Object.hasOwn removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor: single jsonDeepEqual in updateFlowCore
client.ts carried its own deep-equal (jsonValuesEqual) for the native
check-cache request comparison; the only difference to updateFlowCore's
jsonDeepEqual was skipping undefined-valued members. Fold that rule into
jsonDeepEqual (a no-op for the JSON-parsed native configs it already
compared; flow vectors unchanged) and export it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(error): derive UpdateErrorCode from one code list
The union type and KNOWN_CODES listed every code twice; keep a single
'as const' array and derive both from it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(harmony): stop retrying mirrors after a patch fails to apply
The native cold-start orchestrator retried every mirror of a strategy even
when the artifact downloaded fine but could not be applied (PATCH_FAILED).
Every mirror serves the same bytes, so match JS client.ts and the C++
IsMirrorRetryableCode: break out to the next strategy instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(core): web-safe nativeMethod helper for optional native methods
On web PushyModule is a noop Proxy, so every 'typeof PushyModule.x ===
"function"' feature detect was true and each call site carried its own
Platform.OS === 'web' guard plus try/catch to turn a synchronous bridge
throw into a rejection. nativeMethod(name) returns undefined on web and for
older natives, and always returns a promise; the per-call guards and the
dead web branch of ping() (web never reaches a download) are gone.
The VM-evaluated client harness in nativeConfiguration.test.ts depended on
client.ts's free identifiers; its assertions now live in client.test.ts
(native ownership suppresses constructor/setOptions writes until handed
back), so it is removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(harmony): drop dead code and generated files
- UpdateContext isFirstTime()/clearFirstTime() and the ArkTS
STATE_OP_CLEAR_FIRST_TIME mirror (the C++ enum stays).
- DownloadTask types that are never created (TASK_TYPE_CLEANUP,
TASK_TYPE_PLAIN_DOWNLOAD): doCleanUp, their switch cases, the plain
download failure branch and the now always-true isPatchTask checks. The
params default type becomes TASK_TYPE_PATCH_FULL.
- Test-only ErrorCodes.parseErrorCodePrefix, unused
ARCHIVE_PATCH_TYPE_FULL, an empty extern "C" block in pushy.cpp.
- PushyPackage.h used a copy-pasted GEO_LOCATION_PACKAGE_H guard.
- BuildProfile.ets is regenerated by hvigor on every build (nothing
imports it): untrack and ignore it. OAT.xml was an unused template.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(client): trim endpoint plumbing
- getRemoteEndpoints no longer filters out configured endpoints: the
endpoint fallback already dedupes candidates and drops failed ones.
- getConfiguredCheckEndpoints returns server.main as is (deduplicated on
assignment and again by the endpoint plan).
- Inline the single-caller getCheckUrl; the per-endpoint failure log moves
from fetchCheckResult's tryEndpoint wrapper into requestCheckResult.
- endpoint.ts no longer re-exports dedupeEndpoints; fetchWithTimeout drops
the unused params.signal fallback; drop explicit default timeouts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* build(android): drop pre-7.5 Gradle exec path, dedupe gradle and ndk flags
Expo SDK 50+ (the only caller of the node fallback) needs Gradle 8, so
the Project.exec branch is dead. Set version once, drop the commented
useExpoPublishing, and declare Java 8 explicitly so lambdas compile on
AGP < 4.2. Android.mk repeated -std=c++17 and --exclude-libs that
Application.mk already applies to every module.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(android-jni): build Java strings from UTF-8 with NewString, not NewStringUTF
update_core_android.cpp passed std::string values to NewStringUTF, which
expects JNI modified UTF-8 and mangles supplementary (4-byte) code points.
Move update_flow_jni.cpp's correct UTF-8 -> UTF-16 ToJString into the shared
jni_util.h and use it for every Java string the patch/state glue returns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(client): one telemetry POST for events and exceptions
reportExceptionToServer and reportToServer each built the same POST to
{endpoint}/report/{appKey}; both now go through postReport. The event
shape {type,message,code,data} is a single ReportEvent type, the
single-caller send closure is inlined, and captureException's guard absorbs
the duplicated opt-out checks (plus the no-hot-update check).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(android-jni): dedupe manifest building and exception guards
- delta::BuildPatchManifest (patch_core.h): one pure builder for the
parallel copy_froms/copy_tos lists with the length-mismatch check, so the
Harmony bridge can reuse it too; jni_util::ReadPatchManifest wraps it for
the three JNI entry points that built it by hand.
- jni_util::GuardJni replaces the six copies of the
catch(std::exception)/catch(...) -> RuntimeException block.
- Drop the pure forwarders ToArchivePatchType and MakeStateResult
(NewStateCoreResult takes the default arguments now).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(client): one fail() exit for caught update-flow errors
The toUpdateError -> emitError -> throw/throwIfEnabled sequence was
spelled out at every catch site; fail() does the first two and returns the
error. runBeforeReload now wraps a beforeReload throw as USER_HOOK_ERROR
itself (event type/data derived from the context) instead of switchVersion
and restartApp each doing it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(android): stop retrying mirrors after PATCH_FAILED in the native round
The cold-start orchestrator re-downloaded the same artifact from every
mirror even when the failure was a patch failure, which no mirror can
change. Break out to the next strategy like update_flow_core's
IsMirrorRetryableCode and the JS client do. The download-failure code
mapping moves to ErrorCodes so the module and the orchestrator share it,
and the attempt loop takes a DownloadStarter so it is JVM-testable.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(android): keep https-configured native rounds off plaintext
Same rule as Harmony's SyncCoordinator (isHttpsOnly): when every
configured endpoint is https, endpoints fetched from queryUrls and
artifact URLs in the decision must not downgrade the round to http.
Non-https remote endpoints and cleartext artifact URLs are skipped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(client): simplify performDownload bookkeeping
- patchFailed/patchErrorMessages are exactly the exhausted strategies
(only PATCH_FAILED exhausts one, and an exhausted strategy never runs
again), so derive them instead of tracking them in parallel.
- Drop the unreachable 'failed without lastError' branch: every attempt
runs at least one download, so a failed download always has an error.
- Progress listeners are local subscriptions released in finally (also on
an unexpected throw) instead of sharedState.progressHandlers entries.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(harmony): share the NAPI async-work plumbing in pushy.cpp
The four promise-returning bindings (applyDeltaFromSource,
cleanupOldEntries, sha256HexFileAsync, sha256HexRawFdAsync) each repeated
~70 lines of create-promise / create-async-work / queue / settle / free.
QueueWork<W>(env, name, data, execute, settle) now owns that lifecycle;
every failure path (promise creation, work creation, queueing, aborted
work) still settles the promise exactly once and frees the call.
NewStateResult takes a StateResultExtras struct with defaults instead of
five positional extras, and NewString absorbs MakeUtf8String (it now
throws on failure like the latter did).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(harmony): single X-macro method table in PushyTurboModule.cpp
Method names were written twice (host-function macro + registerMethod)
and could drift. PUSHY_METHODS(X) now lists (name, argc, SYNC|ASYNC) once
and generates both the host functions and the registrations.
check-native-spec-parity.js parses that table: a spec method missing from
it, a Promise method registered SYNC, a SYNC entry wrapping an async ArkTS
method, or a duplicated row all still fail the check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(test): share the cpp test scripts' SANITIZE/FUZZ plumbing
scripts/lib/cpp-test-common.sh holds CORE_WARNINGS, the SANITIZE=1 flags
and the libFuzzer probe that test-patch-core.sh and test-update-flow-core.sh
each carried a copy of. The probe also tries Homebrew LLVM (Apple clang has
no libFuzzer runtime) and honours FUZZ_CXX. test.yml installs clang with the
other build dependencies instead of a second apt-get update.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(provider): dedupe catch blocks and context actions
- checkUpdate/downloadUpdate share surfaceCaughtError for errors that
bypassed the client pipeline (user hooks).
- switchVersion/switchVersionLater come from one forUpdateHash factory.
- restartApp/resetToPackagedBundle are the client's own stable arrow
methods, like markSuccess.
- downloadAndInstallApk drops the Android check the client already does.
- Linking cleanup prefers subscription.remove() and only falls back to
removeEventListener on RN < 0.65 (no deprecation warning on 0.65-0.69).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(packlist): derive the whitelist from package.json files
The allowed prefixes were a hand copy of package.json `files`. Build them
from `files` instead (globs included) and also flag a packed path that a
negated `files` entry should have excluded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(harmony): dedupe DownloadTask file helpers
- writeFileContent writes through SaveFile.writeFully (now exported, and
checks short writes) instead of its own 64KB chunk loop;
toArrayBufferSlice and FILE_COPY_BUFFER_SIZE go away.
- SaveFile.unlinkIfExists replaces the access-then-unlink pattern in
DownloadTask and InstallRecord.
- One buildResumeMeta closure builds the resume sidecar for both the
initial and the final write.
- 'bundle.harmony.js' literals use HARMONY_BUNDLE_FILE_NAME.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(harmony): trim UpdateContext wrappers and the getAllSync probe
downloadFullUpdate/downloadPatchFromPackage wrapped executeTask in a
try/log/rethrow that duplicated DownloadTask's own failure log
(downloadPatchFromPpk never had it). Preferences.getAllSync exists since
API 10 and the HAR targets API 12, so reset calls it directly; only the
API 14 flushSync keeps its structural probe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: remove dead JS exports and locale keys
- i18n: getLocale/addTranslations and the I18n class export were only
used by tests; t() resolves its template in one expression; the stale
usage comment is gone. Tests use the real singleton instead.
- locales: checking_update / download_progress were only referenced by
the i18n test and that comment.
- telemetry ServerReportPayload, type.ts Up-to-date/UpdateAvailable/
Expired CheckResult variants (not exported from index), core
isFirstTimeDebug, the utils joinUrls re-export (tests import it from
updateFlowCore) and the defaultContext export.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(flow-core): drop the unused uuid input of buildCheckFingerprint
No caller ever passed it (the client uuid is inside the request body's
cInfo). The fingerprint is in-memory only and not part of the flow
vectors (unchanged).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(harmony): dedupe SyncCoordinator query and commit paths
runQueryRequest walked configured and remotely discovered endpoint bases
with two copies of the normalize / dedupe / budget / request / validate
sequence; one tryBase closure (plus claimAttempt for the shared 8-request
budget) now serves both. The three commitSyncResult calls share one
commit helper that also maps "reset during the round" to cancelled/reset,
and the response cache JSON is built once.
harmonySyncQuery.test.ts pins the fallback order, dedupe, cleartext
filter, single remote list and request budget (it passes against the
previous implementation too).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(harmony): one Logger.log for all levels; log constants minus uuid
The four hilog levels shared one body, now Logger.log(fn, args).
getConstants logged a hand-copied subset of its result to keep the
install uuid out of hilog; it now logs the whole result without uuid
(plus uuidSet), so new constants are not silently left out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(harmony): share the install record predicate
isRecordForVersion (presence) and expectedBundleSha256ForActivation
(activation) each spelled out the legacy-empty and schema/version rules.
Activation now reuses isRecordForVersion and only adds the mandatory
bundle digest (#655) on top, so the two cannot drift.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(harmony): call applicationContext.restartApp directly
restartApp is API 12, the HAR's minimum (compatibleSdkVersion 12), so
the structural typeof probe is dead. A throwing restartApp still falls
back to startAbility/terminateSelf exactly as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(android): one UpdateModule for both architectures
src/oldarch now only holds a hand-written NativePushySpec mirroring the
codegen spec (name, constants hook, abstract methods), so UpdateModule
in src/main absorbs UpdateModuleImpl and extends NativePushySpec under
either architecture. Every spec method carries @ReactMethod for the old
bridge, which reads the annotations from the module class itself. The
unused UpdateModuleImpl(ReactApplicationContext) constructor goes with
it; removeListeners takes the codegen's double on the old bridge too.
scripts/check-native-spec-parity.js still reads the deleted per-arch
UpdateModule files and needs to follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(harmony): require hvigor task registration in the build-meta plugin
The plugin fell back to writing meta.json at apply time (every hvigor
invocation) when the node had no registerTask or registration threw. The
hvigor versions RNOH supports all register tasks, so a missing
registerTask now fails loudly instead of degrading. The project-level
hvigorfile -> entry/ module-dir fallback is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ci(e2e): share the e2e setup between Android and iOS, dedupe config
- .github/actions/e2e-common: Node/Bun + Bun cache, e2e project deps, the
local react-native-update link and react-native-update-cli, used by
e2e-android-setup and the e2e-ios build job (which carried a copy).
The iOS jobs move from Node 22 to 24 like every other workflow (22 was
never bumped when Android moved to 24; no recorded reason).
- e2e_android.yml: DETOX_ANDROID_ARCHS/DETOX_AVD_NAME set once at workflow
level; the three emulator-runner configs share one YAML anchor, each step
passing its commands via EMULATOR_SCRIPT.
- e2e_ios.yml: the Xcode version (also part of every cache key) is one env.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ci(publish): move the shared publish steps into a composite action
publish_with_harmony and publish_without_harmony ran the same toolchain,
native-library, verification and npm publish steps and differed only in
where harmony/pushy.har comes from. .github/actions/publish-npm runs them
with harmony-har: build | previous; both jobs stay (the HAR build needs the
HarmonyOS container). The release tag now reaches the publish script via
env instead of inline expression interpolation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(scripts): drop the stale debug e2e npm scripts
test:ios-debug / test:android-debug ran the update-flow suites against a
__DEV__ build, where the client skips update checks, and build:ios-debug
only fed them; nothing (CI, docs, Example) references the three. The
debug boot smoke pair (build:android-debug, smoke:android-debug) stays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(android): lambdas and one thread helper
Threads.single(name, daemon) replaces the four hand-written executor
factories and Threads.startDaemon the three hand-rolled daemon threads.
UiThreadRunner folds into StateSerialRunner.run(Executor, ...) with a
UI_THREAD executor; its promise == null branches were dead (every
caller passes one). Anonymous single-method classes become lambdas.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* build(harmony): compile shared native sources in place, drop the staging copy
build-harmony-har.js copied android/jni, cpp/patch_core and
cpp/update_flow_core into harmony/pushy/src/main/cpp/android-generated
for CMakeLists.txt's STAGED fallback, then deleted it again. CMake always
resolved the repo-root sources first, and the HAR only ships the compiled
librnpushy.so (no C++ sources), so the copy was never used: a release HAR
built without it has the same file list (minus OAT.xml), identical
non-ArkTS files and the same exported symbols. Hosts compiling
PushyTurboModule.cpp from node_modules are unaffected (npm ships
android/jni and cpp/).
Also: the six source checks are one table, and the eval-based JSON5
dependency probe is gone (the module always has RNOH to install, the
wrapper project never has dependencies).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(android): share install presence and activation checks
InstallRecord.isInstalled (bundle + matching record) replaces
UpdateContext.hasCompletedVersion's and DownloadTask.hasCompletedDelta-
Directory's copies; InstallRecord.verifyInstalled (bundle + digest)
backs both verifySwitchTarget and ensureNotReinstallingRunningVersion.
isComplete and verifyForActivation share one schema/hash check, and the
bundle file name is a constant. A switch to a version without a valid
record now reports the record's own reason instead of 'is incomplete'.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(android): tidy UpdateContext state ops, downloads and cleanup
runStateOp folds the four snapshot -> state core calls without launch
flags; the three no-deadline public download wrappers go (only the
module called them; it now passes deadline 0) and every download shares
one params builder; the two queued-cleanup blocks become
cleanUp(maxAgeDays). Also drops the unused VERSION_COMPLETE_FILE,
commented-out code, a stacked stale javadoc and the one-line
applyEditor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* build(harmony): move the CI SDK discovery into check-harmony-types.js
The harmony-build type-check step carried ~20 lines of shell that walked
the container's install roots for openharmony/ets/api. The script now
does that walk itself (first, and only when HARMONY_TYPECHECK_REQUIRED=1,
in the same root order and depth) and logs the SDK it uses. The
oh_modules gate is gone: the type check only covers the .ts sources and
resolves @rnoh and the NAPI module through harmony/types stubs. The
header comment no longer claims the check skips on CI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ci(harmony): dedupe trigger paths and drop checks bun lint already runs
push and pull_request share one paths list through a YAML anchor. The
JS <-> native spec parity step already runs in `bun lint` (lint.yml, every
PR), and the constant-parity check moves into `bun lint` as well, so both
steps and their src/NativePushy.ts / check-native-spec-parity.js path
triggers go. The type-check step is a plain invocation now that the
script finds the container SDK itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(android): share the patch-task prelude in DownloadTask
doPatchFromApk/doPatchFromPpk share stagePatchArchive (download, stage,
native plan) and PatchArchiveContents hands out arrays instead of seven
inline toArray calls. discardPartial replaces three delete-or-throw
blocks, the plain-download failure path reuses deleteConsumedArchive,
and rejectProtocolDowngrade uses HttpUtils.isHttpsUrl.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: condense the native-check followups and bundleHash migration
- NATIVE_CHECK_FOLLOWUPS.md: keep only the still-open items (Harmony
native-check e2e, automated crash-on-launch e2e, Harmony real-device
rescue, iOS main-thread hold budget) plus a short record of what was
settled; the full four-round history stays in git.
- BUNDLEHASH_MIGRATION.md: Phase 0-2 narration compressed to what shipped
and the decisions that overturned the original design; Phase 3 (§5.1
SyncBinaryVersion migration, §5.2 digests) kept in full and marked not
started (state_core.cpp still compares buildTime).
- NATIVE_CHECKUPDATE_DESIGN.md: the status header now reflects the
10.51.0/10.52.0/10.52.1 releases and the current orchestrator names.
- Dangling references to the never-committed BUNDLEHASH_DESIGN.md /
REMOTE_RESET_DESIGN.md now say so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test: one loader for native sources and Harmony VM modules
nativeHostApi, nativeConfiguration, configEquivalenceVectors and
harmonyInstallRecord each stripped imports/exports from a Harmony .ts file,
transpiled it and ran it in a VM; nativeHostApiNaming and
nativeTextEncoding each defined root/source/shippedNativeSources/
stripComments. Both now come from nativeSources.ts (evalHarmonyModule
caches the transpiled script per file).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test: shared client test helpers, real client in provider render tests
- helpers.ts: createJsonResponse, flush, __DEV__ save/restore
(setDevForFile / restoreDevAfterEach), mockPlatform, mockI18n, mockCore
and importFreshClient/createClient with an auto-incrementing module key
(no more hand-picked cache keys).
- Every module mock now passes the real namespace through: mockCore spreads
the real ../core (nativeMethod bound to the mocked native module), the
react-native stand-in is setup.ts's baseReactNative with overrides, the
download tests' ../utils mock spreads the real utils, and the file-scope
partial mocks in utils.test.ts are gone.
- client.test.ts: setupAndroidApkMocks folds into setupClientMocks
({ platform: 'android' }); 104 import+construct pairs become createClient;
no-op resets of a fresh module's sharedState are dropped.
- provider.render.test.tsx renders the real client with spied update-flow
methods instead of a hand-written fake (provider.contract.test.tsx no
longer has to keep that fake honest).
Verified per file in isolation, in reversed/shuffled file order and with
bun test --randomize.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* style(client): keep UpdateErrorListener's doc comment on its type
The ReportEvent interface had been inserted between them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(android): one set of IO and HTTP helpers
HttpClients.withDeadline replaces the deadline -> callTimeout code
duplicated in DownloadTask and SyncCoordinator, and hosts
rejectProtocolDowngrade (SyncCoordinator no longer reaches into
DownloadTask for it). UpdateFileUtils gains readBytes/readSmallUtf8
(resume sidecar, install record), sha256Hex(InputStream) (bundled asset
hash), a public copy used by ApkInstaller, and the single 64KB
BUFFER_SIZE; SafeZipFile creates parents via ensureParentDirectory.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ci(lint): mention the constant parity check in the lint step comment
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* style: biome-format the harmony sync tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(harmony): run the pure-logic tests under bun, drop the hypium suite
The hypium suite in harmony/pushy/src/test was run by no workflow. Its
pure-logic cases now run in `bun test` (every PR), evaluating the real
ArkTS sources the same way harmonyInstallRecord.test.ts does (direct
import where a module has no @ohos imports, otherwise imports stripped
and the source evaluated in a VM):
- harmonyDownloadTask: Content-Range parsing, __diff.json manifest
parsing (plus copiesCrc / hbcTransform), DownloadTaskParams defaults
- harmonyInstallRecordLogic: record build/parse/match/activation rules
- harmonyArchiveLimits: free-space probe plan, original-size and
uncompressed-size checks
- harmonyErrorCodes, harmonyValidation: error codes, path components,
hash info JSON, EventHub
NativeConfig.test.ets duplicated configEquivalenceVectors.test.ts and is
dropped. The @ohos/hypium devDependency goes with the suite;
check-constant-parity.js stays where it is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(android): one endpoint attempt path in SyncCoordinator
QueryRun.tryEndpoint carries the normalize -> dedupe -> https filter ->
budget -> request -> validate sequence both endpoint loops repeated. The
response cache entry is built once instead of in three branches, the
startRound javadoc sits on startRound again, and HttpUtils.
stripTrailingSlashes is the single trailing-slash rule for endpoint
bases, ConfigEquivalence and PushyConfiguration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(android): one entry map in BundledResourceCopier
The parallel entry/archive maps become HashMap<String, ZipSource>, which
makes the baseZipFile fallback (never reached: every indexed entry has
its archive) go away. collectApkPaths reads the split APKs from
context.getApplicationInfo() instead of a PackageManager round trip.
Map.putIfAbsent (API 24+, below the module's minSdk 21) is replaced by
containsKey checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(android): small cleanups across the module
- the three hardcoded "pushy" log tags use Texts.LOG_TAG
- BuildConfig.DEBUG everywhere; UpdateContext.DEBUG was the same value
and reading it from another class forced UpdateContext's native load
- UpdateContext's static initializer is the only native library load:
JNI_OnLoad registers every class's natives, and DownloadTask,
FlowBridge and supportedDiffVersion are only reached through an
UpdateContext instance
- ReactReloadManager.prepareRestart is inlined into the restart hop
- the ReactHostDelegate bundle loader field is found by trying
jsBundleLoader, then Expo's _jsBundleLoader, instead of matching the
Expo class name
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(audit): compress fixed CODE_AUDIT items, keep the open ones in full
Every item was re-checked against the current code. Fixed items shrink to
one line with the master commit that closed them (section numbers kept:
the Android sources cite CODE_AUDIT 2.x); partially fixed or open items
(1.10, 2.6, 2.13, 3.10, 3.13, 3.14, 4.4, 4.12, the Harmony NAPI null-field
P4, 5.7, 5.8, 5.15, 6.4) keep a full description of what remains. The
executed §7 ordering and the §8 status table fold into the header.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* build(android): drop two proguard rules nothing needs any more
ReactReloadManager no longer matches the Expo host delegate by class
name, so its -keepnames rule goes; the field names it probes stay
covered by the ReactHostDelegate member rule. UpdatePackage is always
constructed directly by the host, so it may be renamed.
Verified with an R8-minified release build of Example/e2etest (RN 0.85):
dexdump shows the JNI result classes with <init>()V and every field, all
native methods under their names, UpdateModule/NativePushySpec with
every spec method, DefaultReactHostDelegate.jsBundleLoader; the app
started on an emulator, read the module constants, reset and checked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(ios): Foundation-only common header for shared internals
- RCTPushyCommon.h (private, Foundation-only, so RCTPushyConfiguration.mm
and RCTPushyDownloader.mm can use it without React): PUSHY_HIDDEN, one
error domain + code key constant instead of three "cn.reactnative.pushy"
literals, and the shared helpers below.
- RCTPushyRevealText now wraps pushy::text::Reveal
(cpp/patch_core/obscured_text.h) instead of duplicating the decoder; it
and RCTPushyQueryPath move from the public RCTPushyConfiguration.h to the
private header (pod-internal, hidden).
- Configuration errors use error_codes.h kInvalidOptions.
- RCTPushyIsHttpsDowngrade: one redirect rule for the check session and the
downloader. RCTPushyDiscardArchive: archive + resume sidecar removal;
the downloader's file-handle close is one private method.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ios): align the native round's download and config rules with JS/Harmony
- Mirror retry: a failed attempt whose code is not mirror-retryable
(flowcore::IsMirrorRetryableCode, i.e. PATCH_FAILED) moves on to the next
strategy instead of downloading the same unappliable artifact from every
mirror, like client.ts does.
- https-only (Harmony SyncCoordinator isHttpsOnly, SR-1): when every
configured endpoint is https, endpoints from a fetched remote list must be
https and plaintext http artifact URLs are skipped.
- The runnable-config gate trims appKey like Android and Harmony (the
trimmed key is the one used in the request URL).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(harmony): load Harmony sources through the shared evalHarmonyModule
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(text-encoding): drop stale PushyTurboModule.cpp allowlist entries
The method table is now an X-macro and the names are stringified with
#name, so they no longer appear as string literals.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(harmony): drop the stale hypium suite mention from harmonyInstallRecord
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(harmony): point comments at scripts/check-constant-parity.js, drop hypium mentions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(package): drop the files exclusion for the removed harmony/pushy/src/test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(e2e): remove the one-off iOS crash-rescue experiment scaffolding
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(ios): split RCTPushy.mm by responsibility, RCTPushyInstaller
RCTPushy.mm (~2160 lines) is split into
- RCTPushyState.mm: defaults keys, state lock, State <-> defaults, version
switch under the lock, string/error/JSON helpers, embedded bundle hash;
- RCTPushyInstallRecord.mm: version/staging dirs, completion record (read,
presence, activation verification, the staging promotion write);
- RCTPushyInstaller.mm: an RCTPushyInstaller class (class methods) owning
the process-wide file queue, the archive guard, the in-flight download
registry, fetch/unzip/patch, switchVersion and cleanup. Progress is a
callback, so the orchestrator no longer borrows a bridgeless RCTPushy
instance (+engine is gone);
- RCTPushy+Launch.mm: launch bundle resolution, download dir, binary
identity (packageVersion/buildTime);
- RCTPushy.mm keeps the RN exports, constants and the public host API
(bundleURL/configure/prepareBundle stay in the primary @implementation:
methods declared in RCTPushy.h must be implemented there).
Cross-file statics become PUSHY_HIDDEN externs in RCTPushyInternal.h;
keyNativeConfig/keyNativeCheckIncomplete move into the orchestrator.
Also:
- dispatch_once -> function-local statics (thread-safe C++ statics in
ObjC++); PushyRevealOnce is gone.
- PushyNSErrorFromStatus / PushyDownloadDeadlineExpiredError ->
PushyErrorWithCode; PushyHostResult defaults hash/activated.
- Helpers: PushyJsonDictionary/PushyJsonString, PushyRejectException,
PushyVersionDir/PushyStagingDir, PushyWaitSeconds for the timed
semaphore waits, one response-only commit block in the round.
- The archive digest travels through a __block variable on the serial
file queue instead of a global table under @synchronized.
- Unused +rollback removed; the completion record name comes from
install_record.h (kFileName).
- The [bridge reload] fallback for React Native without RCTReloadCommand
(< 0.62) is now an #error: incomplete integration fails the build.
- SSZipArchive is imported through its public header
SSZipArchive/SSZipArchive.h, which resolves with and without
use_frameworks!.
- Unused private declarations dropped from RCTPushyInternal.h.
scripts/tests/ios-purge-restore: SOURCES lists the new files (plus the
header holding PushyFileStamp); switchVersion is extracted as an
RCTPushyInstaller class method into installer.inc, PushyWaitSeconds ahead
of restorePurgedLaunch so the launch-wait seams still apply.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(podspec): drop dead fallbacks and search paths
- Remove the RN 0.68-0.70 new-arch fallback used when
install_modules_dependencies is missing (and the new_arch_enabled
variable it needed); React Native >= 0.71 wires codegen and the new
architecture itself.
- USER_HEADER_SEARCH_PATHS: drop Headers/Public/React-Codegen/RCTPushySpec
(the Pods directory is ReactCodegen, so it never existed) and
Headers/Public/SSZipArchive (the sources import SSZipArchive's public
header framework-style now).
- One node call (printing JSON) answers expo's version and
expo-modules-core's location and version, instead of three.
- Drop the pre-SDK-50 `s.platform = :ios` regex fallback for
ExpoModulesCore.podspec (SDK < 50 is rejected earlier anyway).
- Drop `s.dependency 'React'`, a duplicate of React-Core.
Verified with pod install + Release simulator builds of Example/e2etest,
both static (default) and use_frameworks! :linkage => :dynamic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(client): release applyingUpdate before emitting a beforeReload hook error
The JS cleanup moved the USER_HOOK_ERROR emit into runBeforeReload, so
onError listeners ran while applyingUpdate was still held and a listener
that retried switchVersion synchronously was ignored. Emit from the
callers again, after the release.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(core): let a synchronous getBundleHash throw fail the module load again
Routing the prefetch through nativeMethod turned a synchronous native
throw (a broken integration) into a logged rejection. Call it directly
behind the same feature detect so it propagates as before; async failures
still only leave the hash unknown.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(harmony): build patch manifests with the shared delta::BuildPatchManifest
The three NAPI entry points checked the copy list lengths and assembled
the PatchManifest by hand; use the patch_core builder the Android JNI glue
already uses. The mismatch error now reads 'copy_froms and copy_tos length
mismatch' (nothing matches on the old wording).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(parity): check the merged Android UpdateModule and the oldarch NativePushySpec
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ios): include hpatch.h by relative path so use_frameworks! builds resolve it
Also point stale header/core.ts comments at the split iOS files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(android): move the JNI glue out of the shared cores into cpp/android_jni
cpp/patch_core and cpp/update_flow_core now hold only platform-neutral code;
the Android-only JNI translation units and headers live in cpp/android_jni and
include the cores by file-relative path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(android): fold patch_core_android.cpp into update_core_android.cpp
The two remaining delta natives (applyDeltaFromSource, cleanupOldEntries)
join the other patch/state natives, so cpp/android_jni has one core glue
unit plus the flow glue and the registration table.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(cpp): move the hpatch wrapper from android/jni to cpp/hpatch
hpatch.{c,h} is compiled by all three platforms, so it moves next to the
other shared cores. The HDiffPatch and lzma submodules stay under
android/jni; hpatch.{c,h} reach them by file-relative path, which keeps the
use_frameworks! builds (no header search path into the pod) resolving the
real headers, and patch_core.cpp includes ../hpatch/hpatch.h the same way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* build: pick the shared C++ sources by directory on every platform
Android.mk ($(wildcard)), the Harmony CMakeLists (file(GLOB
CONFIGURE_DEPENDS)), the podspec Core subspec and the core test scripts now
compile cpp/patch_core, cpp/update_flow_core and cpp/hpatch (plus
cpp/android_jni on Android) by directory instead of hand-copied file lists,
so adding a platform-neutral source no longer needs five edits. The globs
are non-recursive, so tests/ stays out everywhere.
All includes are file-relative now, so Android.mk drops its include paths
and the Harmony target keeps only the ones pushy.cpp needs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(package): ship cpp/ as a whole and scan it in the native source tests
package.json ships cpp/ (minus tests) instead of listing each core;
check-packlist requires cpp/hpatch and, for releases, the HDiffPatch/lzma
sources iOS and Harmony compile from the package. The native wording/encoding
tests scan all of cpp/, which now includes cpp/android_jni and cpp/hpatch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(flow-core): shared native config gate, equivalence and normalizer
cpp/update_flow_core/native_config.{h,cpp} hosts the three rule sets the
Android, iOS and Harmony orchestrators each carried their own copy of:
ParseRunnableConfig/RunnableConfigJson (may the persisted config run a
round), ConfigsEquivalent (contract C1) and NormalizeConfiguration (the
host configure() validation). flowjson gains DeepEquals.
The gate takes JS semantics: disabled is JS-truthy (Android used to
accept only true/"true") and appKey is trimmed like
String.prototype.trim (Android's String.trim also ate C0 controls and kept
NBSP/U+3000). The C++ test replays the shared C1 vectors and the new
native-config-vectors.json, and the fuzz target covers the new entry
points.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(android): use the shared config gate, equivalence and normalizer
FlowBridge gains configsEquivalent / runnableConfig /
normalizeConfiguration (JNI over cpp/update_flow_core/native_config.h).
ConfigEquivalence.java, PushyConfiguration.java and their JUnit test are
gone; the C1 and native-config vectors are replayed by the C++ test.
SyncCoordinator.parseRunnableConfig keeps its shape but asks the core for
the verdict, so disabled now follows JS truthiness (disabled:1 or
"false" skips) and appKey is trimmed like String.prototype.trim.
PushyRuntime.configure loads the native library itself, since it
validates before touching UpdateContext.
The default service addresses now live encoded in the shared core only;
nativeTextEncoding checks them there and the request path per platform.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(ios): use the shared config gate, equivalence and normalizer
PushyConfigsEquivalent is now a nil-guarding binding over
flowcore::ConfigsEquivalent (the NSDictionary-based canonicalizer is gone),
PushyLoadRunnableConfig hands back the host result built from
flowcore::ParseRunnableConfig (appKey now trimmed like
String.prototype.trim instead of NSCharacterSet whitespace), and
RCTPushyNormalizeConfiguration serializes the options and defers to
flowcore::NormalizeConfiguration. The purge-restore harness compiles the
flow core and keeps replaying the C1 vectors through the binding.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(harmony): use the shared config gate, equivalence and normalizer
pushy.cpp exports configsEquivalent / runnableConfig /
normalizeConfiguration over cpp/update_flow_core/native_config.h.
UpdateContext's ArkTS canonicalizer, SyncCoordinator's hand-rolled gate
and PushyConfiguration's validator (with the default addresses and
POLICY_NEXT_LAUNCH) are gone; the public PushyConfiguration type stays.
The bun tests no longer evaluate those ArkTS copies: the C1 vectors run
against the JS client only (the core replays them in C++), the
normalization cases moved to native-config-vectors.json, and the
evaluated Harmony modules get a NativePatchCore stand-in whose gate is
checked against the same vectors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(flow-core): caller-chosen parse limits and linear object parsing
flowjson::Parse gains a ParseLimits overload (input bytes, node count) for
documents bounded by something other than a checkUpdate response; the
patch core's __diff.json parser needs its 16 MB manifest cap. Objects
with more than 16 members are deduplicated through a hash index while
parsing instead of Value::Set's linear scan, so a manifest's copies map
of thousands of members no longer parses in quadratic time. Duplicate
keys keep the first position and the last value as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(patch-core): one __diff.json parser and an ApplyArchivePatch entry point
cpp/patch_core/patch_manifest.h: ParsePatchManifest(text, options) is the
single parser of a patch archive's manifest, taking the shape pushy-go and
the CLI emit (copies to->from, copiesCrc per target, deletes object,
hbcTransform per bundle patch entry) under these rules, where the three
platform parsers used to differ:
- deletes: an object's keys or an array of strings (Android ignored
arrays); any other type, or a non-string element, is an error;
- copiesCrc: keyed by copy target, an integer in [0, 2^32) or null.
Android keyed it by source with the last target winning; now two
targets of one source with different CRCs fail the manifest, and a
string/fractional/oversized/boolean CRC fails instead of being coerced
or skipping the content check;
- types: copies must be an object of strings (Harmony coerced with
String(), Android threw, iOS rejected); a null or empty source means the
target's own path on every platform (Harmony ppk kept "");
- copy_source_prefix (HarmonyOS rawfiles) is stripped only at the start
(Harmony replaced its first occurrence anywhere);
- hbcTransform and its bundle entry must be objects when present; the
platforms silently dropped other types and applied the patch untransformed.
Every path still goes through ValidateManifest. The text is capped at
kMaxManifestBytes and 4 * kMaxEntries values.
PatchManifest now carries bundle_hbc_transform_meta (moved from
FileSourcePatchOptions). archive_patch gains PlanArchivePatch (entry check,
parse, merge plan) and ApplyArchivePatch (plan + apply; with
host_copies_resources it only rebuilds the bundle and hands the copies
back as CopyGroups, which now carry the source's CRC). BuildCopyGroups is
linear in the number of copies.
The test replays tests/patch-manifest-vectors.json (server shapes, the
drift cases, hostile paths) and covers the caps, the plan and both apply
modes, including a transform-domain patch driven by the manifest; FUZZ=1
adds a libFuzzer target for the parser.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(android): hand __diff.json to the shared core as bytes
DownloadTask no longer parses the manifest: extractPatchArchive keeps the
raw __diff.json bytes and both patch tasks call one native,
applyArchiveDelta (JNI over archive_patch::ApplyArchivePatch), which
checks the entries, parses and validates the manifest, rebuilds the
bundle and, for ppk patches, copies/deletes/merges. The APK task passes
hostCopiesResources and gets back the copy groups, now with the CRC each
source must match, for BundledResourceCopier.
The manifest crosses JNI as byte[] rather than a jstring, so paths with
supplementary characters are no longer re-encoded as modified UTF-8.
appendManifestEntries, the buildArchivePlan/buildCopyGroups/
applyDeltaFromSource natives, ArchivePatchPlanResult and buildCopyList's
canonical-path check are gone: ValidateManifest rejects every lexical
escape (absolute, '..', '.', empty segment, backslash, control bytes), and
the staging directory holds only what SafeZipFile extracted, which never
creates symlinks. SafeZipFile's own entry checks stay, since zip entry
names never reach the C++ validation.
Behaviour changes, from the shared rules: copiesCrc is read per target
(conflicting CRCs for one source fail the patch instead of the last one
winning), a deletes array is honoured, and type errors are patch failures.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(ios): hand __diff.json to the shared core as text
applyDeltaForHash reads the manifest (size-capped as before) and calls
archive_patch::ApplyArchivePatch with it, so PushyPatchManifestFromJson,
the NSJSONSerialization round trip and the hbcTransform extraction are
gone. The rules are the shared ones: a CRC must be an integer in
[0, 2^32) (true, 1.5 or 2^32 are no longer read as 1, 1 or 0) and
non-object hbcTransform metadata fails the patch instead of being skipped.
A missing manifest is now reported as "diff.json not found".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(harmony): hand __diff.json to the shared core as text
pushy.cpp exports applyArchiveDelta (async, over
archive_patch::ApplyArchivePatch) in place of buildArchivePlan,
buildCopyGroups and applyDeltaFromSource; the manifest is parsed on the
worker thread with the rest of the patch. DownloadTask reads the manifest
text and passes it with the entry names: the ppk task lets the core copy,
delete and merge, the package task passes hostCopiesResources and the
"resources/rawfile/" prefix and copies the returned groups, each with the
CRC its source must match.
parseManifestToArrays and its bun tests are gone; the cases moved to
cpp/patch_core/tests/patch-manifest-vectors.json. Behaviour changes, from
the shared rules: the rawfile prefix is stripped only at the start of a
source, an empty source means the target's path in ppk patches too, and
values of the wrong type fail the patch instead of being coerced with
String() or ignored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(patch-core): drop BuildPatchManifest
No binding assembles a manifest from parallel copy lists any more; every
platform hands __diff.json to ApplyArchivePatch. CODE_AUDIT 3.14 records
the manifest parser as sunk into the shared core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(patch-core): one completion record implementation in install_record.cpp
cpp/patch_core/install_record.h was a header of constants with the logic
kept three times (InstallRecord.java, RCTPushyInstallRecord.mm,
InstallRecord.ts). It now carries the whole contract on POSIX IO and
digest::Sha256File:
- Parse / Read / Names / HasRecordFile / RecordedBundleSha256;
- Build (refuses an empty bundleSha256) and Write: temp file + fsync,
rename over the record, directory fsync (best-effort). Android wrote in
place with fsyncs, iOS atomically without fsync, Harmony in place with a
file fsync only;
- Seal (hash the staged bundle unless the digest is known, build, write;
tells a file-system failure from a bundle failure) and Promote (rename
the staging directory, fsync the versions root, which only Android did);
- IsComplete (bundle is a regular file + legacy marker or a schema-1
record naming the version) for launch/dedup;
- CheckActivation, the switchVersion gate: bundle present; no record file
is grandfathered only for the current/last version; the zero-byte
legacy marker passes; otherwise schema 1, versionHash and a non-empty
bundleSha256 matching the bundle, which must not change while hashed
(iOS's stamp check, now everywhere). Unchanged re-checks cheaply that
the directory still passes on the same basis (iOS's under-lock check).
Where the platforms disagreed, the stricter reading wins: only a zero-byte
file is the legacy marker (all three trusted "{}" too), "schema" must be
the number 1 (Android took "1" and 1.5, iOS true and 1.5), versionHash and
bundleSha256 must be strings (Android coerced numbers), the bundle must be
a regular file, and a grandfathered version still needs its bundle (iOS
skipped that check).
tests/install-record-vectors.json holds 31 record shapes with what each
means for IsComplete, CheckActivation and RecordedBundleSha256; the C++
test replays them and covers the size cap, Build, atomic Write, Seal,
Promote and Unchanged. check-constant-parity.js learns `only`, so the
mirrors are held to the constants they still use.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(android): use the shared completion record core
InstallRecord.java keeps the paths (bundleFile, stagingDirectoryFor) and
wraps five natives (JNI over cpp/patch_core/install_record.h, in
cpp/android_jni/install_record_jni.cpp): isInstalled, verifyInstalled
(CheckActivation, now taking the grandfathering decision), seal, promote
and recordedBundleSha256. build/write/read/isComplete/verifyForActivation/
syncDirectory and the org.json parsing are gone.
- DownloadTask.promoteStaging seals the staging directory (reusing the
digest taken while extracting a full package) and promotes it; the
record is now written atomically (temp + rename) and the directory
fsync no longer needs API 26.
- verifySwitchTarget hands current/last to the core, which grandfathers
only a directory without a record, as before; a markerless directory
outside current/last now fails with "is incomplete." like iOS/Harmony.
- The bundle hash moves from MessageDigest to the core's SHA-256 on the
same threads as before (state serial thread, sync coordinator, download
tasks); nothing on the UI thread hashes. Launch-time callers only stat
and read the record.
InstallRecordTest keeps the path helpers; the record logic is covered by
the C++ vectors (the JVM cannot load the library).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(patch-core): define Status::Ok/Error inline
install_record.cpp reports a delta::Status but needs nothing else of
patch_core.cpp; with the two factories in the header it links on its own,
which lets the iOS purge-restore harness compile the real record core
without hpatch, HDiffPatch and lzma.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(ios): use the shared completion record core
RCTPushyInstallRecord.mm keeps the paths and becomes a thin layer over
cpp/patch_core/install_record.h: PushyHasCompletedVersionAtPath is
IsComplete, PushyRecordedBundleSha256 (constants, crash attribution) is
RecordedBundleSha256, and PushyPromoteStagingDir seals the staging
directory (Seal: bundle failures stay PATCH_FAILED, a record that cannot
be written is FILE_OPERATION_FAILED instead of a raw Cocoa error) and
promotes it (Promote, which now also fsyncs the record and the versions
root; NSDataWritingAtomic did neither).
switchVersion:error: calls CheckActivation outside the state lock with
the current/last decision taken under it, and re-checks with Unchanged
under the lock (same grandfathering, record text and bundle stamp) before
committing. PushyReadInstallRecord, PushyInstallRecordMatches,
PushyHasInstallRecordFile, PushyStampFile, PushyFileStamp and
PushyVerifyInstallForActivation are gone. A grandfathered version now
needs its bundle like on Android/Harmony.
The purge-restore harness compiles the extractor's copy of
install_record.cpp next to the extracted switchVersion:error:, so
install_record_activation and switch_version_records exercise the shared
core; the trust-digestless and grandfather-recorded mutants now alter that
copy and are still caught by their named assertions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(ios-purge-restore): drop the stale PushyFileStamp note
The struct moved into the shared record core (install_record::FileStamp).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(harmony): use the shared completion record core
pushy.cpp exports the record core (cpp/patch_core/install_record.h):
isInstallComplete and recordedBundleSha256 synchronously (a stat and a
read of the record, as before), sealInstallRecord, promoteStaging and
checkActivation as promises on libuv workers, so neither the bundle hash
nor the fsyncs run on the JS thread.
InstallRecord.ts keeps the paths and the reserved-entry rule and maps
rejections to the stable codes (install steps PATCH_FAILED, activation
SWITCH_VERSION_FAILED). parse/read/build/write, the record matching and
assertActivatable are gone; UpdateContext.verifyActivation hands
current/last to the core, DownloadTask seals and promotes. The record is
now written atomically with a directory fsync (it was unlinked and
rewritten in place) and the versions root is fsynced after the rename.
The bun tests now cover only the binding: paths, the arguments handed to
the NAPI module, the error-code mapping, and that NativePatchCore.ts
declares exactly the functions pushy.cpp exports. The record logic is the
C++ vectors'. check-constant-parity holds both mirrors to kStagingSuffix
alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(flow-core): one native round core for request, query walk and response
cpp/update_flow_core/native_round.{h,cpp} takes over the pure parts the
Android, iOS and Harmony orchestrators each carried around the shared
decision layer:
- PrepareRound(config, env, sample): the checkUpdate request body (identity,
cInfo and input assembled from the persisted config and the launch env,
diffV from this core) plus the first endpoint-query state.
- NextQueryStep(state, text): the sequential endpoint fallback — configured
endpoints in sampled order, then the first queryUrls list that parses,
https-only for remote bases, the 8-request budget and the response schema
gate. The request path now lives here, encoded.
- HandleRoundResponse(...): the decision with attempts planned for the device
(no diff without a running version, no cleartext URL under an https-only
config), the hashInfo to persist (incl. forceBootRescue) and the
response-cache text.
tests/native-round-vectors.json pins it and records, per case, where the
three platforms disagreed: non-string packageVersion/rnu/rn overrides,
absent rnu/rn on iOS, Android's optBoolean forceBoot marker and lenient
remote-list JSON, non-string endpoint/queryUrls handling, and Harmony's
failures on non-array endpoints or a non-string afterDownload. The core
takes the JS-equivalent or strictest behaviour. The fuzz target walks the
new entry points.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(android): run the cold-start round through the shared round core
SyncCoordinator hands the config text and a launch env to
FlowBridge.prepareRound / nextQueryStep / handleResponse (JNI over
cpp/update_flow_core/native_round.h) instead of assembling identity, cInfo
and the request input, walking the endpoints (QueryRun, isHttpsOnly), building
hashInfo and the response-cache JSON, and filtering attempts itself. The
buildRequestBody / orderEndpointCandidates / isValidResponse natives are gone;
the config gate returns just the rejection (configRejection), since the core
reads the appKey itself. HttpUtils loses QUERY_PATH, stripTrailingSlashes and
isCleartextUrl; the request path lives encoded in the core.
Behaviour aligned with iOS/Harmony/JS: a truthy non-boolean forceBoot (1,
"false") now marks forceBootRescue like the activation it already caused;
non-string packageVersion/rnu/rn are no longer coerced to strings;
non-string endpoints and queryUrls entries no longer spend a request; a
remote endpoint list must be strict JSON; a config with duplicate keys is no
longer rejected by org.json after the shared gate accepted it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(ios): run the cold-start round through the shared round core
RCTPushyOrchestrator passes the config text and a launch env to
flowcore::PrepareRound / NextQueryStep / HandleRoundResponse
(cpp/update_flow_core/native_round.h) instead of assembling identity, cInfo
and the request input, walking the endpoints with its own tried-set and
budget, projecting the version info and filtering attempts.
commitRoundWithGeneration takes the core's response-cache text (cache:)
in place of the four fields it serialized itself; nil still marks the crash
handler's activation-only commit. PushyLoadRunnableConfig only hands back
the config text, RCTPushyQueryPath and the https/endpoint helpers are gone.
Behaviour aligned with Android/Harmony/JS: absent rnu/rn are sent as ""
(cInfo used to omit them) and non-string ones as "", like the core's C1
defaults.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(harmony): run the cold-start round through the shared round core
SyncCoordinator passes the config text and a launch env to the new NAPI
prepareRound / nextQueryStep / handleResponse (cpp/update_flow_core/
native_round.h) instead of assembling identity, cInfo and the request
input, walking the endpoints itself, projecting hashInfo, building the
response-cache JSON and filtering attempts. The buildRequestBody /
orderEndpointCandidates / isValidResponse exports and Texts.QUERY_PATH are
gone; the request path now lives only in the core.
harmonySyncQuery checks the glue follows the core's steps (the walk's
policy moved to native-round-vectors.json); harmonySyncAttempts and the
host-API harness use the new signatures, and nativeTextEncoding expects the
request path in cpp/ only.
Behaviour aligned with Android/iOS: non-array endpoints still try the list
discovery (the round used to stop), a non-string endpoint is skipped (it
used to throw and fail the round), a non-string afterDownload never
activates (it used to fail the round as invalid_response), non-string
queryUrls entries no longer spend a request, and non-string
packageVersion/rn overrides are ignored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(audit): the https-only filter and round helpers live in the shared core
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(patch-core): one archive extraction accounting in archive_limits.cpp
cpp/patch_core/archive_limits.h was a header of constants with the rules
kept three times (SafeZipFile.inspect, PushyUnzipGuard, ArchiveLimits.ts).
It now carries them:
- EntryAccumulator(archiveBytes).Accept(size, compressed, symlink, name):
entry count, reserved name, symlink, per-entry cap, running total and
whole-archive ratio, per-entry ratio; AcceptExtracted for an entry already
on disk whose compressed size was never seen (no per-entry ratio);
- CheckEntryCount, CheckUncompressedSize(archiveBytes, uncompressedBytes)
and IsReservedEntryName (install_record::kReservedPrefix, new);
- CheckExtractedDirectory: an iterative lstat walk of a freshly unpacked
tree through the accumulator (Harmony, whose zlib exposes no entries);
- PlanUnknownLengthFreeSpaceProbe: the chunked-download disk probe plan
all three platforms implemented identically.
Where the platforms disagreed, the strictest reading wins:
- the ratio is size > compressed * 100 (Android/iOS divided, so up to
101:1 minus one byte passed), and a zero compressed size past the 1 MiB
threshold is an unbounded ratio (Android/iOS skipped the check);
- the whole-archive ratio (Harmony only) now applies on every platform;
- a reserved `.pushy-` prefix is refused in any path component (Android
checked the base name, iOS the last component of each unpacked path);
- symlinks are refused everywhere (iOS only; Harmony's walk followed them);
- an entry of unknown (negative) size is refused (Android skipped it);
- a non-positive archive size bounds nothing (Harmony skipped the ratio).
tests/archive-limits-vectors.json pins the accumulator, the whole-archive
check, the probe plan and the reserved names (ported from the Harmony bun
cases and SafeZipFileTest, with `was` notes); the C++ test also walks real
trees (reserved directory, symlink, 20000/20001 entries).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(ios): run the unzip guard and the download probe through archive_limits
PushyUnzipGuard feeds an archive_limits::EntryAccumulator (sized by the
archive file, for the whole-archive ratio) from the should-unzip callback
and checks the entry count up front with CheckEntryCount; its own size,
total and ratio arithmetic is gone. The reserved-name check in the
did-unzip callback now covers every component of the path below the
destination (IsReservedEntryName), not just the last one; an unpacked path
that is not below the destination is a violation. The downloader's
unknown-length disk probe is PlanUnknownLengthFreeSpaceProbe.
Behaviour aligned with the shared rules: ratio as size > compressed * 100
(was integer division), a zero compressed size past 1 MiB is refused, the
whole-archive ratio applies, a `.pushy-` directory component is refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(android): run the archive accounting through archive_limits over JNI
SafeZipFile.inspect(archiveBytes) collects the central directory once and
hands names, declared and compressed sizes to ArchiveLimits.checkEntries
(one coarse JNI call per archive, cpp/android_jni/archive_limits_jni.cpp,
over archive_limits::EntryAccumulator), which returns the total for the
free-space check or throws the first violation. The Inspection class,
the Java count/size/ratio arithmetic and isReservedEntryName are gone; the
reserved-name rule now comes from the core during inspect, which both
extraction paths run before any entry is written (the iterator keeps the
traversal check, unzipToPath the canonical-path check). The download loop
asks ArchiveLimits.planFreeSpaceProbe (JNI over
PlanUnknownLengthFreeSpaceProbe) before each chunk.
Behaviour aligned with the shared rules: ratio as size > compressed * 100,
a zero compressed size past 1 MiB is refused, the whole-archive ratio
applies, a `.pushy-` directory component is refused, an entry of unknown
size is refused instead of skipped.
ArchiveLimits.java keeps the five caps it still compares against itself;
check-constant-parity holds it to exactly those. SafeZipFileTest keeps the
traversal and extraction cases; the accounting cases moved to
cpp/patch_core/tests/archive-limits-vectors.json (the JVM cannot load the
library). The JNI registration test accepts a throws clause on a native.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(harmony): run the archive checks through archive_limits over NAPI
pushy.cpp exports the extraction accounting (cpp/patch_core/archive_limits.h):
checkUncompressedSize (whole-archive expansion and ratio, synchronous,
returns the reason), planFreeSpaceProbe (the unknown-length download probe
plan, per chunk) and checkExtractedDirectory (the post-unpack walk, a promise
on a libuv worker). zlib exposes no entries, so the accumulator runs over
the unpacked tree there.
ArchiveLimits.ts keeps checkOriginalSize (the getOriginalSize gate, Harmony
only) and ensureFreeSpace, and maps refusals to PATCH_FAILED.
planUnknownLengthFreeSpaceProbe and measureExtractedDirectory are gone, and
so are InstallRecord.ts's RESERVED_ENTRY_PREFIX / isReservedEntryName. The
walk no longer runs on the JS thread and uses lstat: a symlink in the
unpacked tree is refused instead of followed.
ArchiveLimits.ts mirrors only the caps it still compares against
(M…1 parent 4ca8972 commit 1260b11
206 files changed
Lines changed: 14981 additions & 12406 deletions
File tree
- .github
- actions
- e2e-android-setup
- e2e-common
- publish-npm
- workflows
- Example/e2etest
- e2e
- native
- scripts
- android
- jni
- lib
- arm64-v8a
- armeabi-v7a
- x86_64
- x86
- src
- main/java/cn/reactnative/modules/update
- newarch/cn/reactnative/modules/update
- oldarch/cn/reactnative/modules/update
- test/java/cn/reactnative/modules/update
- cpp
- android_jni
- hpatch
- patch_core
- tests
- update_flow_core
- tests
- docs
- harmony
- pushy
- src
- main
- cpp
- ets
- test
- ios/RCTPushy
- scripts
- lib
- tests/ios-purge-restore
- src
- __tests__
- locales
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
4 | | - | |
5 | | - | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
6 | 7 | | |
7 | 8 | | |
8 | 9 | | |
| |||
32 | 33 | | |
33 | 34 | | |
34 | 35 | | |
35 | | - | |
36 | | - | |
37 | | - | |
38 | | - | |
39 | | - | |
40 | | - | |
41 | | - | |
42 | | - | |
43 | | - | |
44 | | - | |
45 | | - | |
46 | | - | |
47 | | - | |
48 | | - | |
49 | | - | |
50 | | - | |
51 | | - | |
52 | | - | |
53 | | - | |
54 | | - | |
55 | | - | |
56 | | - | |
57 | | - | |
58 | | - | |
59 | | - | |
60 | | - | |
61 | 36 | | |
62 | 37 | | |
63 | 38 | | |
| |||
73 | 48 | | |
74 | 49 | | |
75 | 50 | | |
76 | | - | |
77 | | - | |
78 | | - | |
79 | | - | |
80 | | - | |
81 | | - | |
82 | | - | |
83 | | - | |
84 | | - | |
85 | | - | |
86 | | - | |
87 | | - | |
88 | | - | |
89 | | - | |
90 | | - | |
91 | | - | |
92 | | - | |
93 | | - | |
94 | | - | |
95 | | - | |
96 | | - | |
97 | | - | |
98 | | - | |
99 | | - | |
100 | | - | |
101 | | - | |
102 | | - | |
103 | | - | |
104 | | - | |
105 | | - | |
106 | | - | |
107 | | - | |
108 | | - | |
109 | | - | |
110 | | - | |
111 | | - | |
112 | | - | |
113 | | - | |
114 | | - | |
115 | | - | |
116 | | - | |
117 | | - | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
0 commit comments