react-native-update 10.53.1 → 10.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README-CN.md +31 -31
  2. package/README.md +31 -31
  3. package/android/jni/HDiffPatch/.travis.yml +33 -0
  4. package/android/jni/HDiffPatch/builds/xcode/bestParams.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  5. package/android/jni/HDiffPatch/builds/xcode/hdiffz.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  6. package/android/jni/HDiffPatch/builds/xcode/hdiffz.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  7. package/android/jni/HDiffPatch/builds/xcode/hpatchz.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  8. package/android/jni/HDiffPatch/builds/xcode/lz4.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  9. package/android/jni/HDiffPatch/builds/xcode/lzma.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  10. package/android/jni/HDiffPatch/builds/xcode/testHashClash.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  11. package/android/jni/HDiffPatch/builds/xcode/testHashClash.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  12. package/android/jni/HDiffPatch/builds/xcode/unitTest.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  13. package/android/jni/HDiffPatch/builds/xcode/zstd.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  14. package/android/jni/lzma/DOC/7zC.txt +187 -0
  15. package/android/jni/lzma/DOC/7zFormat.txt +469 -0
  16. package/android/jni/lzma/DOC/Methods.txt +173 -0
  17. package/android/jni/lzma/DOC/installer.txt +166 -0
  18. package/android/jni/lzma/DOC/lzma-history.txt +446 -0
  19. package/android/jni/lzma/DOC/lzma-sdk.txt +357 -0
  20. package/android/jni/lzma/DOC/lzma-specification.txt +1176 -0
  21. package/android/jni/lzma/DOC/lzma.txt +328 -0
  22. package/android/jni/lzma/bin/7zS2.sfx +0 -0
  23. package/android/jni/lzma/bin/7zS2con.sfx +0 -0
  24. package/android/jni/lzma/bin/7zSD.sfx +0 -0
  25. package/android/jni/lzma/bin/7zdec.exe +0 -0
  26. package/android/jni/lzma/bin/7zr.exe +0 -0
  27. package/android/jni/lzma/bin/installer/config.txt +5 -0
  28. package/android/jni/lzma/bin/installer/cr.bat +5 -0
  29. package/android/jni/lzma/bin/lzma.exe +0 -0
  30. package/android/jni/lzma/bin/x64/7zr.exe +0 -0
  31. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  32. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  33. package/android/lib/x86/librnupdate.so +0 -0
  34. package/android/lib/x86_64/librnupdate.so +0 -0
  35. package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +44 -0
  36. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +124 -40
  37. package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +125 -0
  38. package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +43 -11
  39. package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateFlow.java +2 -0
  40. package/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java +90 -0
  41. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +79 -19
  42. package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +21 -0
  43. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +33 -2
  44. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleSupport.java +1 -0
  45. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +5 -0
  46. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +5 -0
  47. package/cpp/patch_core/archive_limits.h +38 -0
  48. package/cpp/patch_core/install_record.h +36 -0
  49. package/cpp/update_flow_core/flow_json.h +1 -0
  50. package/cpp/update_flow_core/update_flow_core.cpp +41 -7
  51. package/cpp/update_flow_core/update_flow_core.h +9 -0
  52. package/cpp/update_flow_core/update_flow_jni.cpp +12 -0
  53. package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +2 -0
  54. package/harmony/pushy/src/main/cpp/pushy.cpp +37 -0
  55. package/harmony/pushy/src/main/ets/ArchiveLimits.ts +83 -0
  56. package/harmony/pushy/src/main/ets/DownloadTask.ts +126 -30
  57. package/harmony/pushy/src/main/ets/InstallRecord.ts +129 -0
  58. package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +24 -2
  59. package/harmony/pushy/src/main/ets/NativePatchCore.ts +5 -0
  60. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +12 -0
  61. package/harmony/pushy/src/main/ets/UpdateContext.ts +78 -15
  62. package/harmony/pushy.har +0 -0
  63. package/ios/RCTPushy/RCTPushy.mm +384 -25
  64. package/ios/RCTPushy/RCTPushyDownloader.mm +32 -0
  65. package/package.json +25 -2
  66. package/src/NativePushy.ts +13 -0
  67. package/src/client.ts +160 -43
  68. package/src/core.ts +17 -3
  69. package/src/error.ts +3 -0
  70. package/src/index.ts +8 -0
  71. package/src/locales/en.ts +3 -0
  72. package/src/locales/zh.ts +2 -0
  73. package/src/metadata.ts +166 -0
  74. package/src/provider.tsx +52 -17
  75. package/src/type.ts +22 -0
  76. package/src/updateFlowCore.ts +95 -4
  77. package/src/utils.ts +31 -19
  78. package/.gitmodules +0 -6
  79. package/BUNDLEHASH_MIGRATION.md +0 -355
  80. package/NATIVE_CHECKUPDATE_DESIGN.md +0 -539
  81. package/NATIVE_CHECK_FOLLOWUPS.md +0 -514
  82. package/android/jni/HDiffPatch/test/_private_searchBestParams.cpp +0 -301
  83. package/android/jni/HDiffPatch/test/hpatchz_test.cpp +0 -292
  84. package/android/jni/HDiffPatch/test/testHashClash.cpp +0 -436
  85. package/android/jni/HDiffPatch/test/unit_test.cpp +0 -417
  86. package/biome.json +0 -59
  87. package/cpp/patch_core/tests/fixtures/rawstored.lzma2label.patch.bin +0 -0
  88. package/cpp/patch_core/tests/fixtures/rawstored.new.bin +0 -0
  89. package/cpp/patch_core/tests/fixtures/rawstored.old.bin +0 -0
  90. package/cpp/patch_core/tests/fixtures/v96.hbc +0 -0
  91. package/cpp/patch_core/tests/fixtures/v96.meta.json +0 -1
  92. package/cpp/patch_core/tests/fixtures/v96.streampatch.bin +0 -0
  93. package/cpp/patch_core/tests/fixtures/v96.t.hbc +0 -0
  94. package/cpp/patch_core/tests/fixtures/v96.tpatch.bin +0 -0
  95. package/cpp/patch_core/tests/fixtures/v96.tstreampatch.bin +0 -0
  96. package/cpp/patch_core/tests/fixtures/v96b.hbc +0 -0
  97. package/cpp/patch_core/tests/fixtures/v98.hbc +0 -0
  98. package/cpp/patch_core/tests/fixtures/v98.t.hbc +0 -0
  99. package/cpp/patch_core/tests/fixtures/v98b.hbc +0 -0
  100. package/cpp/patch_core/tests/fixtures/v98b.t.hbc +0 -0
  101. package/cpp/patch_core/tests/hbc_transform_test.cpp +0 -384
  102. package/cpp/patch_core/tests/patch_core_test.cpp +0 -970
  103. package/cpp/update_flow_core/tests/flow_vectors.json +0 -1401
  104. package/cpp/update_flow_core/tests/update_flow_core_test.cpp +0 -317
  105. package/scripts/check-expo-version.js +0 -92
@@ -0,0 +1,83 @@
1
+ import fileIo from '@ohos.file.fs';
2
+ import statvfs from '@ohos.file.statvfs';
3
+
4
+ // ArkTS mirror of cpp/patch_core/archive_limits.h — keep in sync by hand.
5
+ // 损坏或恶意更新包的伤害上限:最多耗费有限的磁盘/内存/时间,绝不撑爆磁盘。
6
+ export const MAX_ARCHIVE_BYTES = 512 * 1024 * 1024;
7
+ export const MAX_TOTAL_UNCOMPRESSED_BYTES = 2048 * 1024 * 1024;
8
+ export const MAX_ENTRY_BYTES = 512 * 1024 * 1024;
9
+ export const MAX_ENTRIES = 20000;
10
+ export const MAX_COMPRESSION_RATIO = 100;
11
+ export const RATIO_CHECK_MIN_BYTES = 1 * 1024 * 1024;
12
+ export const MAX_MANIFEST_BYTES = 16 * 1024 * 1024;
13
+ export const FREE_DISK_MARGIN_BYTES = 64 * 1024 * 1024;
14
+
15
+ /**
16
+ * 目标所在文件系统必须还能再写 bytesToWrite + 安全余量。取不到可用空间
17
+ * (返回 0/异常)视为未知,放行。
18
+ */
19
+ export async function ensureFreeSpace(
20
+ target: string,
21
+ bytesToWrite: number,
22
+ ): Promise<void> {
23
+ let probe = target;
24
+ while (probe.length > 1 && !fileIo.accessSync(probe)) {
25
+ const slash = probe.lastIndexOf('/');
26
+ if (slash <= 0) {
27
+ return;
28
+ }
29
+ probe = probe.substring(0, slash);
30
+ }
31
+ let free = 0;
32
+ try {
33
+ free = await statvfs.getFreeSize(probe);
34
+ } catch (e) {
35
+ return;
36
+ }
37
+ if (free <= 0) {
38
+ return;
39
+ }
40
+ const needed = Math.max(0, bytesToWrite) + FREE_DISK_MARGIN_BYTES;
41
+ if (free < needed) {
42
+ throw Error(`insufficient disk space: need ${needed} bytes, have ${free}`);
43
+ }
44
+ }
45
+
46
+ export interface DirectoryMeasure {
47
+ entries: number;
48
+ bytes: number;
49
+ }
50
+
51
+ /**
52
+ * 递归统计目录里的条目数与总字节数;两者任一超过上限立即抛错(不必扫完)。
53
+ */
54
+ export async function measureExtractedDirectory(
55
+ directory: string,
56
+ acc: DirectoryMeasure = { entries: 0, bytes: 0 },
57
+ ): Promise<DirectoryMeasure> {
58
+ const names = (await fileIo.listFile(directory)).filter(
59
+ name => name !== '.' && name !== '..',
60
+ );
61
+ for (const name of names) {
62
+ const path = `${directory}/${name}`;
63
+ const stat = await fileIo.stat(path);
64
+ acc.entries += 1;
65
+ if (acc.entries > MAX_ENTRIES) {
66
+ throw Error(`archive has too many entries (> ${MAX_ENTRIES})`);
67
+ }
68
+ if (stat.isDirectory()) {
69
+ await measureExtractedDirectory(path, acc);
70
+ continue;
71
+ }
72
+ if (stat.size > MAX_ENTRY_BYTES) {
73
+ throw Error(`archive entry too large: ${name} (${stat.size} bytes)`);
74
+ }
75
+ acc.bytes += stat.size;
76
+ if (acc.bytes > MAX_TOTAL_UNCOMPRESSED_BYTES) {
77
+ throw Error(
78
+ `archive expands beyond ${MAX_TOTAL_UNCOMPRESSED_BYTES} bytes`,
79
+ );
80
+ }
81
+ }
82
+ return acc;
83
+ }
@@ -12,6 +12,18 @@ import NativePatchCore, {
12
12
  CopyGroupResult,
13
13
  } from './NativePatchCore';
14
14
  import { monotonicNowMs } from './MonotonicClock';
15
+ import {
16
+ MAX_ARCHIVE_BYTES,
17
+ MAX_MANIFEST_BYTES,
18
+ ensureFreeSpace,
19
+ measureExtractedDirectory,
20
+ } from './ArchiveLimits';
21
+ import {
22
+ HARMONY_BUNDLE_FILE_NAME,
23
+ buildInstallRecord,
24
+ stagingDirectoryFor,
25
+ writeInstallRecord,
26
+ } from './InstallRecord';
15
27
 
16
28
  export const VERSION_COMPLETE_FILE_NAME = '.pushy-complete';
17
29
 
@@ -144,6 +156,32 @@ export class DownloadTask {
144
156
  // 一经 downloadFile 正常返回即置位:之后的失败是 patch 应用失败,归档
145
157
  // 已被判定有毒,清理时必须连 sidecar 一起删,绝不能续传进同一个失败。
146
158
  private downloadPhaseCompleted = false;
159
+ // 下载归档的 SHA-256,解压前计算,写进完成记录。
160
+ private artifactSha256 = '';
161
+
162
+ // 两阶段安装(cpp/patch_core/install_record.h):解压/打补丁全部在
163
+ // <hash>.staging 里进行,写完完成记录后一次 rename 成 <hash>;失败或崩溃
164
+ // 都不会留下一个"看起来像版本"的半成品目录。
165
+ private stagingDirectory(params: DownloadTaskParams): string {
166
+ return stagingDirectoryFor(params.unzipDirectory);
167
+ }
168
+
169
+ private async promoteStaging(params: DownloadTaskParams): Promise<void> {
170
+ const work = this.stagingDirectory(params);
171
+ const bundlePath = `${work}/${HARMONY_BUNDLE_FILE_NAME}`;
172
+ if (!fileIo.accessSync(bundlePath)) {
173
+ throw Error(`bundle missing after install: ${bundlePath}`);
174
+ }
175
+ const bundleSha256 = NativePatchCore.sha256HexFile(bundlePath);
176
+ await writeInstallRecord(
177
+ work,
178
+ buildInstallRecord(params.hash, bundleSha256, this.artifactSha256),
179
+ );
180
+ if (fileIo.accessSync(params.unzipDirectory)) {
181
+ await this.removeDirectory(params.unzipDirectory);
182
+ }
183
+ await fileIo.rename(work, params.unzipDirectory);
184
+ }
147
185
 
148
186
  constructor(context: common.Context) {
149
187
  this.context = context;
@@ -267,6 +305,49 @@ export class DownloadTask {
267
305
  await this.ensureDirectory(parentPath);
268
306
  }
269
307
 
308
+ /**
309
+ * The completion marker is written by this SDK only, after the whole
310
+ * install succeeded. zlib.decompressFile offers no per-entry filter, so an
311
+ * archive that ships its own marker is detected right after extraction:
312
+ * the marker is removed (so the failure cleanup below cannot mistake the
313
+ * directory for a finished install) and the package is rejected.
314
+ */
315
+ private async rejectReservedEntries(unzipDirectory: string): Promise<void> {
316
+ const marker = `${unzipDirectory}/${VERSION_COMPLETE_FILE_NAME}`;
317
+ if (!fileIo.accessSync(marker)) {
318
+ return;
319
+ }
320
+ try {
321
+ await fileIo.unlink(marker);
322
+ } catch (e) {
323
+ // The directory is removed by the failure cleanup either way.
324
+ }
325
+ const error: Error & { code?: string } = Error(
326
+ `archive contains reserved entry ${VERSION_COMPLETE_FILE_NAME}`,
327
+ );
328
+ error.code = 'PATCH_FAILED';
329
+ throw error;
330
+ }
331
+
332
+ /**
333
+ * 解压 + 资源上限(cpp/patch_core/archive_limits.h)。zlib.decompressFile
334
+ * 没有逐条钩子:解压前按归档大小与 2 倍启发式查磁盘,解压后统计条目数与
335
+ * 总字节数(超限即失败,目录由失败清理删除),再拒绝保留条目。
336
+ */
337
+ private async extractArchive(
338
+ archiveFile: string,
339
+ unzipDirectory: string,
340
+ ): Promise<void> {
341
+ const archiveStat = await fileIo.stat(archiveFile);
342
+ if (archiveStat.size > MAX_ARCHIVE_BYTES) {
343
+ throw Error(`archive too large: ${archiveStat.size} bytes`);
344
+ }
345
+ await ensureFreeSpace(unzipDirectory, archiveStat.size * 2);
346
+ await zlib.decompressFile(archiveFile, unzipDirectory);
347
+ await measureExtractedDirectory(unzipDirectory);
348
+ await this.rejectReservedEntries(unzipDirectory);
349
+ }
350
+
270
351
  private async recreateDirectory(path: string): Promise<void> {
271
352
  await this.removeDirectory(path);
272
353
  await this.ensureDirectory(path);
@@ -353,6 +434,10 @@ export class DownloadTask {
353
434
  };
354
435
  }
355
436
 
437
+ const manifestStat = await fileIo.stat(manifestPath);
438
+ if (manifestStat.size > MAX_MANIFEST_BYTES) {
439
+ throw Error(`patch manifest too large: ${manifestStat.size} bytes`);
440
+ }
356
441
  return parseManifestToArrays(
357
442
  this.parseJsonEntry(await this.readFileContent(manifestPath)),
358
443
  normalizeResourceCopies,
@@ -553,6 +638,10 @@ export class DownloadTask {
553
638
 
554
639
  const enqueueWrite = (data: ArrayBuffer) => {
555
640
  received += data.byteLength;
641
+ if (!writeError && baseOffset + received > MAX_ARCHIVE_BYTES) {
642
+ // 未知长度/分块传输的兜底:超过上限即停写,请求在下面结算时失败。
643
+ writeError = Error(`archive too large: exceeded ${MAX_ARCHIVE_BYTES}`);
644
+ }
556
645
  writeQueue = writeQueue.then(async () => {
557
646
  if (!writer || writeError) {
558
647
  return;
@@ -771,6 +860,14 @@ export class DownloadTask {
771
860
  fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY,
772
861
  );
773
862
  }
863
+ if (totalAll > MAX_ARCHIVE_BYTES) {
864
+ writeError = Error(`archive too large: ${totalAll} bytes`);
865
+ throw writeError;
866
+ }
867
+ await ensureFreeSpace(
868
+ params.targetFile,
869
+ totalAll > 0 ? totalAll - baseOffset : 0,
870
+ );
774
871
  // 先落 sidecar 再放行写入,流中崩溃才有得续。
775
872
  const meta: ResumeMeta = { url: params.url };
776
873
  const etag = etagHeader || resumeMeta?.etag;
@@ -874,8 +971,10 @@ export class DownloadTask {
874
971
 
875
972
  private async doFullPatch(params: DownloadTaskParams): Promise<void> {
876
973
  await this.downloadFile(params);
877
- await this.recreateDirectory(params.unzipDirectory);
878
- await zlib.decompressFile(params.targetFile, params.unzipDirectory);
974
+ const work = this.stagingDirectory(params);
975
+ await this.recreateDirectory(work);
976
+ this.artifactSha256 = NativePatchCore.sha256HexFile(params.targetFile);
977
+ await this.extractArchive(params.targetFile, work);
879
978
  await this.deleteArchiveAndSidecar(params.targetFile);
880
979
  }
881
980
 
@@ -895,12 +994,14 @@ export class DownloadTask {
895
994
  private async doPatchFromAppAfterDownload(
896
995
  params: DownloadTaskParams,
897
996
  ): Promise<void> {
898
- await this.recreateDirectory(params.unzipDirectory);
997
+ const work = this.stagingDirectory(params);
998
+ await this.recreateDirectory(work);
999
+ this.artifactSha256 = NativePatchCore.sha256HexFile(params.targetFile);
899
1000
 
900
- await zlib.decompressFile(params.targetFile, params.unzipDirectory);
1001
+ await this.extractArchive(params.targetFile, work);
901
1002
  const [entryNames, manifestArrays] = await Promise.all([
902
- this.listEntryNames(params.unzipDirectory),
903
- this.readManifestArrays(params.unzipDirectory, true),
1003
+ this.listEntryNames(work),
1004
+ this.readManifestArrays(work, true),
904
1005
  ]);
905
1006
 
906
1007
  NativePatchCore.buildArchivePatchPlan(
@@ -912,7 +1013,7 @@ export class DownloadTask {
912
1013
  HARMONY_BUNDLE_PATCH_ENTRY,
913
1014
  );
914
1015
 
915
- const bundlePatchPath = `${params.unzipDirectory}/${HARMONY_BUNDLE_PATCH_ENTRY}`;
1016
+ const bundlePatchPath = `${work}/${HARMONY_BUNDLE_PATCH_ENTRY}`;
916
1017
  if (!fileIo.accessSync(bundlePatchPath)) {
917
1018
  throw Error('bundle patch not found');
918
1019
  }
@@ -922,9 +1023,9 @@ export class DownloadTask {
922
1023
  );
923
1024
  await this.applyBundlePatchFromFileSource(
924
1025
  originContent.buffer as ArrayBuffer,
925
- params.unzipDirectory,
1026
+ work,
926
1027
  bundlePatchPath,
927
- `${params.unzipDirectory}/bundle.harmony.js`,
1028
+ `${work}/${HARMONY_BUNDLE_FILE_NAME}`,
928
1029
  manifestArrays.hbcTransformMeta,
929
1030
  );
930
1031
  // 组按 from 聚合;同一 from 的内容必然一致,所以每组一个 CRC 即可
@@ -941,7 +1042,7 @@ export class DownloadTask {
941
1042
  manifestArrays.copyFroms,
942
1043
  manifestArrays.copyTos,
943
1044
  ),
944
- params.unzipDirectory,
1045
+ work,
945
1046
  crcByFrom,
946
1047
  );
947
1048
  await this.deleteArchiveAndSidecar(params.targetFile);
@@ -961,12 +1062,14 @@ export class DownloadTask {
961
1062
  private async doPatchFromPpkAfterDownload(
962
1063
  params: DownloadTaskParams,
963
1064
  ): Promise<void> {
964
- await this.recreateDirectory(params.unzipDirectory);
1065
+ const work = this.stagingDirectory(params);
1066
+ await this.recreateDirectory(work);
1067
+ this.artifactSha256 = NativePatchCore.sha256HexFile(params.targetFile);
965
1068
 
966
- await zlib.decompressFile(params.targetFile, params.unzipDirectory);
1069
+ await this.extractArchive(params.targetFile, work);
967
1070
  const [entryNames, manifestArrays] = await Promise.all([
968
- this.listEntryNames(params.unzipDirectory),
969
- this.readManifestArrays(params.unzipDirectory, false),
1071
+ this.listEntryNames(work),
1072
+ this.readManifestArrays(work, false),
970
1073
  ]);
971
1074
 
972
1075
  const plan = NativePatchCore.buildArchivePatchPlan(
@@ -982,10 +1085,10 @@ export class DownloadTask {
982
1085
  copyTos: manifestArrays.copyTos,
983
1086
  deletes: manifestArrays.deletes,
984
1087
  sourceRoot: params.originDirectory,
985
- targetRoot: params.unzipDirectory,
1088
+ targetRoot: work,
986
1089
  originBundlePath: `${params.originDirectory}/bundle.harmony.js`,
987
- bundlePatchPath: `${params.unzipDirectory}/${HARMONY_BUNDLE_PATCH_ENTRY}`,
988
- bundleOutputPath: `${params.unzipDirectory}/bundle.harmony.js`,
1090
+ bundlePatchPath: `${work}/${HARMONY_BUNDLE_PATCH_ENTRY}`,
1091
+ bundleOutputPath: `${work}/${HARMONY_BUNDLE_FILE_NAME}`,
989
1092
  mergeSourceSubdir: plan.mergeSourceSubdir,
990
1093
  enableMerge: plan.enableMerge,
991
1094
  bundleHbcTransformMeta: manifestArrays.hbcTransformMeta,
@@ -1147,10 +1250,7 @@ export class DownloadTask {
1147
1250
  throw Error(`Unknown task type: ${params.type}`);
1148
1251
  }
1149
1252
  if (isPatchTask) {
1150
- await this.writeFileContent(
1151
- `${params.unzipDirectory}/${VERSION_COMPLETE_FILE_NAME}`,
1152
- new Uint8Array(0),
1153
- );
1253
+ await this.promoteStaging(params);
1154
1254
  }
1155
1255
  } catch (error: any) {
1156
1256
  console.error('Task execution failed:', error.message);
@@ -1159,15 +1259,11 @@ export class DownloadTask {
1159
1259
  if (params.type === DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD) {
1160
1260
  await fileIo.unlink(params.targetFile);
1161
1261
  await this.deleteResumeSidecar(params.targetFile);
1162
- } else if (
1163
- !fileIo.accessSync(
1164
- `${params.unzipDirectory}/${VERSION_COMPLETE_FILE_NAME}`,
1165
- ) ||
1166
- !fileIo.accessSync(`${params.unzipDirectory}/bundle.harmony.js`)
1167
- ) {
1168
- // Never let a failed duplicate task remove an install already
1169
- // handed off by an earlier task via marker + bundle.
1170
- await this.removeDirectory(params.unzipDirectory);
1262
+ } else {
1263
+ // Only the staging directory is ours to drop: the final version
1264
+ // directory is never touched by a failed install, so a failed
1265
+ // duplicate task cannot remove an earlier task's install.
1266
+ await this.removeDirectory(this.stagingDirectory(params));
1171
1267
  if (this.downloadPhaseCompleted) {
1172
1268
  // 收齐后才失败的是解压/patch 失败:归档有毒,续传只会撞上
1173
1269
  // 同一个失败。下载阶段的失败则保留 partial + sidecar——那
@@ -0,0 +1,129 @@
1
+ import fileIo from '@ohos.file.fs';
2
+ import { util } from '@kit.ArkTS';
3
+ import NativePatchCore from './NativePatchCore';
4
+
5
+ // ArkTS mirror of cpp/patch_core/install_record.h — keep in sync by hand.
6
+ // 两阶段安装的完成记录:作为安装最后一步写进 staging 目录,随后 staging
7
+ // 原子 rename 成版本目录;激活前重新校验 bundle 摘要。空文件 = 旧版 SDK
8
+ // 写的历史标记,仍视为完整安装(无摘要可验)。
9
+ export const INSTALL_RECORD_SCHEMA = 1;
10
+ export const INSTALL_RECORD_FILE_NAME = '.pushy-complete';
11
+ export const STAGING_SUFFIX = '.staging';
12
+ export const HARMONY_BUNDLE_FILE_NAME = 'bundle.harmony.js';
13
+
14
+ export interface InstallRecordData {
15
+ schema?: number;
16
+ versionHash?: string;
17
+ bundleSha256?: string;
18
+ artifactSha256?: string;
19
+ }
20
+
21
+ export function stagingDirectoryFor(versionDir: string): string {
22
+ return `${versionDir}${STAGING_SUFFIX}`;
23
+ }
24
+
25
+ export function buildInstallRecord(
26
+ versionHash: string,
27
+ bundleSha256: string,
28
+ artifactSha256: string,
29
+ ): string {
30
+ const record: InstallRecordData = {
31
+ schema: INSTALL_RECORD_SCHEMA,
32
+ versionHash,
33
+ };
34
+ if (bundleSha256) {
35
+ record.bundleSha256 = bundleSha256;
36
+ }
37
+ if (artifactSha256) {
38
+ record.artifactSha256 = artifactSha256;
39
+ }
40
+ return JSON.stringify(record);
41
+ }
42
+
43
+ /** 写入并 fsync:紧随其后的 rename 必须能在盘上看到它。 */
44
+ export async function writeInstallRecord(
45
+ versionDir: string,
46
+ recordJson: string,
47
+ ): Promise<void> {
48
+ const path = `${versionDir}/${INSTALL_RECORD_FILE_NAME}`;
49
+ if (fileIo.accessSync(path)) {
50
+ await fileIo.unlink(path);
51
+ }
52
+ const file = await fileIo.open(
53
+ path,
54
+ fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY,
55
+ );
56
+ try {
57
+ const bytes = new util.TextEncoder().encodeInto(recordJson);
58
+ await fileIo.write(file.fd, bytes.buffer as ArrayBuffer);
59
+ await fileIo.fsync(file.fd);
60
+ } finally {
61
+ await fileIo.close(file);
62
+ }
63
+ }
64
+
65
+ /**
66
+ * 解析记录:文件缺失/畸形返回 null;空文件(历史标记)返回 {}。
67
+ */
68
+ export function readInstallRecord(versionDir: string): InstallRecordData | null {
69
+ const path = `${versionDir}/${INSTALL_RECORD_FILE_NAME}`;
70
+ if (!fileIo.accessSync(path)) {
71
+ return null;
72
+ }
73
+ try {
74
+ const stat = fileIo.statSync(path);
75
+ if (stat.size === 0) {
76
+ return {};
77
+ }
78
+ if (stat.size > 64 * 1024) {
79
+ return null;
80
+ }
81
+ const text = fileIo.readTextSync(path);
82
+ const parsed = JSON.parse(text) as InstallRecordData | null;
83
+ return parsed !== null && typeof parsed === 'object' ? parsed : null;
84
+ } catch (e) {
85
+ return null;
86
+ }
87
+ }
88
+
89
+ /** 存在性判定(启动/去重路径,不算摘要):记录存在且(非历史空文件时)指向本版本。 */
90
+ export function isInstallComplete(versionDir: string, versionHash: string): boolean {
91
+ const record = readInstallRecord(versionDir);
92
+ if (record === null) {
93
+ return false;
94
+ }
95
+ if (Object.keys(record).length === 0) {
96
+ return true;
97
+ }
98
+ return (
99
+ record.schema === INSTALL_RECORD_SCHEMA && record.versionHash === versionHash
100
+ );
101
+ }
102
+
103
+ /** 激活前校验:记录带摘要时重新计算 bundle 摘要;不通过则抛错说明原因。 */
104
+ export function verifyInstallForActivation(
105
+ versionDir: string,
106
+ versionHash: string,
107
+ bundlePath: string,
108
+ ): void {
109
+ const record = readInstallRecord(versionDir);
110
+ if (record === null) {
111
+ throw Error(`Bundle version ${versionHash} has no valid completion record.`);
112
+ }
113
+ if (Object.keys(record).length === 0) {
114
+ return;
115
+ }
116
+ if (
117
+ record.schema !== INSTALL_RECORD_SCHEMA ||
118
+ record.versionHash !== versionHash
119
+ ) {
120
+ throw Error(`Bundle version ${versionHash} completion record mismatch.`);
121
+ }
122
+ if (!record.bundleSha256) {
123
+ return;
124
+ }
125
+ const actual = NativePatchCore.sha256HexFile(bundlePath);
126
+ if (actual.toLowerCase() !== record.bundleSha256.toLowerCase()) {
127
+ throw Error(`Bundle version ${versionHash} bundle digest mismatch.`);
128
+ }
129
+ }
@@ -92,6 +92,22 @@ interface RespCacheEntry {
92
92
  }
93
93
 
94
94
  let scheduled = false;
95
+ // JS 在本进程内已拿到有效检查响应时对应的配置 JSON(markJsCheckCompleted)。
96
+ // 进程级:下次启动无信号,冷启动轮次照常运行。
97
+ let jsCompletedConfig: string | undefined;
98
+
99
+ export function markJsCheckCompleted(config: string): void {
100
+ jsCompletedConfig = config;
101
+ }
102
+
103
+ // JS 已用与原生完全相同的配置拿到有效响应(§10.3):延迟轮次就是一次重复请求。
104
+ // 只有计划内的轮次会问这个问题——JS 没起来、启动即崩、检查失败时都不会有信号。
105
+ function isJsCheckCompleted(context: UpdateContext): boolean {
106
+ return (
107
+ jsCompletedConfig !== undefined &&
108
+ jsCompletedConfig === context.getKv(KEY_CONFIG)
109
+ );
110
+ }
95
111
 
96
112
  export function scheduleNativeCheck(
97
113
  context: UpdateContext,
@@ -105,6 +121,13 @@ export function scheduleNativeCheck(
105
121
  // 除非上个进程死于轮中(残留标记),那时每一秒启动时间都要用来续传。
106
122
  const delayMs = context.getKv(KEY_ROUND_INCOMPLETE) ? 0 : START_DELAY_MS;
107
123
  setTimeout(() => {
124
+ if (isJsCheckCompleted(context)) {
125
+ logger.info(
126
+ TAG,
127
+ 'native check skipped: JS check completed in this process',
128
+ );
129
+ return;
130
+ }
108
131
  runOnce(context, launchRolledBackVersion).catch((e: Object) => {
109
132
  // 救援路径自身绝不能把应用拖垮。
110
133
  logger.error(TAG, `native check failed: ${e}`);
@@ -315,8 +338,7 @@ function isValidCheckResponse(responseText: string | undefined): boolean {
315
338
  return false;
316
339
  }
317
340
  try {
318
- const parsed = JSON.parse(responseText) as Object | null;
319
- return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed);
341
+ return NativePatchCore.isValidCheckResponse(responseText);
320
342
  } catch (e) {
321
343
  return false;
322
344
  }
@@ -82,6 +82,8 @@ interface NativePatchCoreBindings {
82
82
  ): Promise<void>;
83
83
  /** sha256(小写 hex)。同步:输入是已在内存的 rawfile bundle,哈希毫秒级 */
84
84
  sha256Hex(data: Uint8Array): string;
85
+ // 文件流式 SHA-256(小写 hex;不可读时空串),供安装记录使用。
86
+ sha256HexFile(path: string): string;
85
87
  /** CRC32(zip/zlib 多项式)。pdiff 拷贝前与 copiesCrc 比对用 */
86
88
  crc32(data: Uint8Array | ArrayBuffer): number;
87
89
  /** 原生 patch 内核可消费的 diff 轨道版本(2 = hdiffv2 轨道) */
@@ -100,6 +102,9 @@ interface NativePatchCoreBindings {
100
102
  identityJson: string,
101
103
  afterDownload: string,
102
104
  ): string | undefined;
105
+ // 响应 schema 门槛(update_flow_core::IsValidCheckResponse):200 但只带
106
+ // `{"error":...}` 的节点算失败,不得阻止继续 fallback。
107
+ isValidCheckResponse(responseText: string): boolean;
103
108
  }
104
109
 
105
110
  export default NativeUpdateCore as unknown as NativePatchCoreBindings;
@@ -10,6 +10,7 @@ import { UpdateContext } from './UpdateContext';
10
10
  import { EventHub } from './EventHub';
11
11
  import {
12
12
  KEY_CONFIG,
13
+ markJsCheckCompleted,
13
14
  KEY_RESP_CACHE,
14
15
  } from './NativeCheckOrchestrator';
15
16
 
@@ -136,6 +137,7 @@ export class PushyTurboModule extends UITurboModule {
136
137
  const currentVersionInfo = currentVersion
137
138
  ? this.context.getKv(`hash_${currentVersion}`)
138
139
  : '';
140
+ const currentBundleSha256 = this.context.currentBundleSha256(currentVersion);
139
141
  const isFirstTime = this.context.consumeFirstLoadMarker();
140
142
  const rolledBackVersion = this.context.rolledBackVersion();
141
143
  const uuid = this.context.getKv('uuid');
@@ -148,6 +150,7 @@ export class PushyTurboModule extends UITurboModule {
148
150
  const result = {
149
151
  downloadRootDir: `${this.mUiCtx.filesDir}/_update`,
150
152
  currentVersionInfo,
153
+ currentBundleSha256,
151
154
  packageVersion,
152
155
  currentVersion,
153
156
  buildTime,
@@ -201,6 +204,15 @@ export class PushyTurboModule extends UITurboModule {
201
204
  this.context.setKv(KEY_CONFIG, config);
202
205
  }
203
206
 
207
+ // JS 本进程已拿到有效检查响应:延迟的原生轮次据此跳过重复请求(§10.3)。
208
+ async markJsCheckCompleted(config: string): Promise<void> {
209
+ logger.debug(TAG, ',call markJsCheckCompleted');
210
+ if (typeof config !== 'string' || config.length === 0) {
211
+ throw Error('config must be a non-empty string');
212
+ }
213
+ markJsCheckCompleted(config);
214
+ }
215
+
204
216
  // 原生冷启动检测落盘的原始响应缓存,JS 侧新鲜期内直接复用免二次请求
205
217
  // (NATIVE_CHECKUPDATE_DESIGN §10.3)。缺省空串,永不 reject。
206
218
  async getNativeCheckCache(): Promise<string> {