react-native-update 10.59.2 → 10.60.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 (125) hide show
  1. package/README-CN.md +7 -7
  2. package/README.md +7 -6
  3. package/android/build.gradle +25 -33
  4. package/android/jni/Android.mk +16 -24
  5. package/android/lib/arm64-v8a/librnpushy.so +0 -0
  6. package/android/lib/armeabi-v7a/librnpushy.so +0 -0
  7. package/android/lib/x86/librnpushy.so +0 -0
  8. package/android/lib/x86_64/librnpushy.so +0 -0
  9. package/android/proguard.pro +9 -7
  10. package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +2 -7
  11. package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +23 -6
  12. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +50 -51
  13. package/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +12 -13
  14. package/android/src/main/java/cn/reactnative/modules/update/CopyGroupResult.java +3 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/CrashHold.java +22 -30
  16. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +181 -332
  17. package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -1
  18. package/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java +27 -0
  19. package/android/src/main/java/cn/reactnative/modules/update/FileOperationException.java +1 -1
  20. package/android/src/main/java/cn/reactnative/modules/update/FlowBridge.java +32 -8
  21. package/android/src/main/java/cn/reactnative/modules/update/HttpClients.java +63 -0
  22. package/android/src/main/java/cn/reactnative/modules/update/HttpUtils.java +0 -11
  23. package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +66 -116
  24. package/android/src/main/java/cn/reactnative/modules/update/NativeCore.java +9 -1
  25. package/android/src/main/java/cn/reactnative/modules/update/PatchFailedException.java +1 -1
  26. package/android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java +32 -60
  27. package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +8 -23
  28. package/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java +42 -80
  29. package/android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java +22 -39
  30. package/android/src/main/java/cn/reactnative/modules/update/SyncCoordinator.java +176 -299
  31. package/android/src/main/java/cn/reactnative/modules/update/Threads.java +35 -0
  32. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +242 -174
  33. package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +1 -1
  34. package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +75 -17
  35. package/android/src/main/java/cn/reactnative/modules/update/{UpdateModuleImpl.java → UpdateModule.java} +113 -95
  36. package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +16 -19
  37. package/android/src/oldarch/cn/reactnative/modules/update/NativePushySpec.java +69 -0
  38. package/cpp/android_jni/archive_limits_jni.cpp +83 -0
  39. package/cpp/android_jni/install_record_jni.cpp +89 -0
  40. package/cpp/android_jni/jni_log.h +31 -0
  41. package/cpp/android_jni/jni_natives.h +40 -0
  42. package/cpp/android_jni/jni_registration.cpp +133 -0
  43. package/cpp/android_jni/jni_util.h +160 -0
  44. package/cpp/android_jni/update_core_android.cpp +450 -0
  45. package/cpp/android_jni/update_flow_jni.cpp +133 -0
  46. package/{android/jni → cpp/hpatch}/hpatch.c +5 -5
  47. package/{android/jni → cpp/hpatch}/hpatch.h +4 -1
  48. package/cpp/patch_core/archive_limits.cpp +194 -0
  49. package/cpp/patch_core/archive_limits.h +99 -10
  50. package/cpp/patch_core/archive_patch_core.cpp +96 -12
  51. package/cpp/patch_core/archive_patch_core.h +49 -0
  52. package/cpp/patch_core/digest.h +5 -4
  53. package/cpp/patch_core/error_codes.h +5 -3
  54. package/cpp/patch_core/install_record.cpp +430 -0
  55. package/cpp/patch_core/install_record.h +163 -9
  56. package/cpp/patch_core/patch_core.cpp +62 -16
  57. package/cpp/patch_core/patch_core.h +27 -7
  58. package/cpp/patch_core/patch_manifest.cpp +201 -0
  59. package/cpp/patch_core/patch_manifest.h +74 -0
  60. package/cpp/patch_core/state_ops.h +3 -3
  61. package/cpp/update_flow_core/flow_json.cpp +82 -8
  62. package/cpp/update_flow_core/flow_json.h +21 -0
  63. package/cpp/update_flow_core/native_config.cpp +456 -0
  64. package/cpp/update_flow_core/native_config.h +84 -0
  65. package/cpp/update_flow_core/native_round.cpp +326 -0
  66. package/cpp/update_flow_core/native_round.h +90 -0
  67. package/cpp/update_flow_core/update_flow_core.h +2 -2
  68. package/harmony/hvigor-plugin.ts +11 -19
  69. package/harmony/pushy/src/main/cpp/CMakeLists.txt +17 -26
  70. package/harmony/pushy/src/main/cpp/PushyPackage.h +2 -2
  71. package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +55 -94
  72. package/harmony/pushy/src/main/cpp/pushy.cpp +694 -517
  73. package/harmony/pushy/src/main/ets/ArchiveLimits.ts +48 -72
  74. package/harmony/pushy/src/main/ets/DownloadTask.ts +164 -404
  75. package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +1 -3
  76. package/harmony/pushy/src/main/ets/ErrorCodes.ts +1 -14
  77. package/harmony/pushy/src/main/ets/EventHub.ts +1 -1
  78. package/harmony/pushy/src/main/ets/InstallRecord.ts +52 -151
  79. package/harmony/pushy/src/main/ets/Logger.ts +20 -34
  80. package/harmony/pushy/src/main/ets/NativePatchCore.ts +116 -42
  81. package/harmony/pushy/src/main/ets/PushyConfiguration.ts +8 -85
  82. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +11 -25
  83. package/harmony/pushy/src/main/ets/SaveFile.ts +99 -18
  84. package/harmony/pushy/src/main/ets/SyncCoordinator.ts +136 -335
  85. package/harmony/pushy/src/main/ets/Texts.ts +0 -4
  86. package/harmony/pushy/src/main/ets/UpdateContext.ts +106 -119
  87. package/harmony/pushy.har +0 -0
  88. package/ios/RCTPushy/RCTPushy+Launch.mm +185 -0
  89. package/ios/RCTPushy/RCTPushy.h +12 -0
  90. package/ios/RCTPushy/RCTPushy.mm +87 -2634
  91. package/ios/RCTPushy/RCTPushyCommon.h +34 -0
  92. package/ios/RCTPushy/RCTPushyConfiguration.h +0 -8
  93. package/ios/RCTPushy/RCTPushyConfiguration.mm +40 -121
  94. package/ios/RCTPushy/RCTPushyDownloader.mm +40 -37
  95. package/ios/RCTPushy/RCTPushyInstallRecord.mm +61 -0
  96. package/ios/RCTPushy/RCTPushyInstaller.mm +689 -0
  97. package/ios/RCTPushy/RCTPushyInternal.h +144 -0
  98. package/ios/RCTPushy/RCTPushyOrchestrator.mm +1001 -0
  99. package/ios/RCTPushy/RCTPushyState.mm +314 -0
  100. package/package.json +25 -23
  101. package/react-native-update.podspec +73 -82
  102. package/src/client.ts +361 -451
  103. package/src/context.ts +1 -1
  104. package/src/core.ts +35 -8
  105. package/src/endpoint.ts +0 -2
  106. package/src/error.ts +13 -32
  107. package/src/i18n.ts +18 -83
  108. package/src/locales/en.ts +0 -4
  109. package/src/locales/zh.ts +0 -4
  110. package/src/provider.tsx +60 -53
  111. package/src/telemetry.ts +0 -13
  112. package/src/type.ts +0 -15
  113. package/src/updateFlowCore.ts +116 -10
  114. package/src/utils.ts +49 -42
  115. package/android/src/main/java/cn/reactnative/modules/update/ArchivePatchPlanResult.java +0 -6
  116. package/android/src/main/java/cn/reactnative/modules/update/PushyConfiguration.java +0 -104
  117. package/android/src/main/java/cn/reactnative/modules/update/UiThreadRunner.java +0 -37
  118. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +0 -121
  119. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +0 -125
  120. package/cpp/patch_core/jni_natives.h +0 -27
  121. package/cpp/patch_core/jni_registration.cpp +0 -103
  122. package/cpp/patch_core/jni_util.h +0 -60
  123. package/cpp/patch_core/patch_core_android.cpp +0 -97
  124. package/cpp/patch_core/update_core_android.cpp +0 -485
  125. package/cpp/update_flow_core/update_flow_jni.cpp +0 -150
@@ -4,7 +4,12 @@ import common from '@ohos.app.ability.common';
4
4
  import { zlib } from '@kit.BasicServicesKit';
5
5
  import { EventHub } from './EventHub';
6
6
  import { DownloadTaskParams } from './DownloadTaskParams';
7
- import { saveFileToSandbox } from './SaveFile';
7
+ import {
8
+ saveFileToSandbox,
9
+ unlinkIfExists,
10
+ withRawFd,
11
+ writeFully,
12
+ } from './SaveFile';
8
13
  import { util } from '@kit.ArkTS';
9
14
  import NativePatchCore, {
10
15
  ARCHIVE_PATCH_TYPE_FROM_PACKAGE,
@@ -16,16 +21,16 @@ import logger from './Logger';
16
21
  import {
17
22
  MAX_ARCHIVE_BYTES,
18
23
  MAX_MANIFEST_BYTES,
19
- UNKNOWN_LENGTH_FREE_SPACE_PROBE_BYTES,
24
+ checkExtractedDirectory,
25
+ checkOriginalSize,
20
26
  checkUncompressedSize,
21
27
  ensureFreeSpace,
22
- measureExtractedDirectory,
23
28
  } from './ArchiveLimits';
24
29
  import {
25
30
  HARMONY_BUNDLE_FILE_NAME,
26
- buildInstallRecord,
31
+ promoteStaging,
32
+ sealInstallRecord,
27
33
  stagingDirectoryFor,
28
- writeInstallRecord,
29
34
  } from './InstallRecord';
30
35
  import {
31
36
  ERROR_DOWNLOAD_FAILED,
@@ -40,94 +45,18 @@ import { BUNDLE_DELTA_ENTRY } from './Texts';
40
45
 
41
46
  const TAG = 'DownloadTask';
42
47
 
43
- export interface PatchManifestArrays {
44
- copyFroms: string[];
45
- copyTos: string[];
46
- // 与 copyFroms/copyTos 逐位对齐的 copiesCrc(CLI >= 2.21.2 的 pdiff
47
- // manifest 携带,键是 to);无声明时为 null。拷贝前用它校验包内资源内容,
48
- // 不符则整次 patch 失败落 full——重打包二进制不能静默拷出漂移的资源。
49
- copyCrcs: (number | null)[];
50
- deletes: string[];
51
- // __diff.json 中对应 bundle patch 条目的 hbcTransform 元数据(原始 JSON
52
- // 字符串);为空时 native 走现状路径
53
- hbcTransformMeta: string;
54
- }
55
-
56
- export function parseManifestToArrays(
57
- manifest: Record<string, any>,
58
- normalizeResourceCopies: boolean,
59
- ): PatchManifestArrays {
60
- const copyFroms: string[] = [];
61
- const copyTos: string[] = [];
62
- const copyCrcs: (number | null)[] = [];
63
- const deletesValue = manifest.deletes;
64
- const deletes = Array.isArray(deletesValue)
65
- ? deletesValue.map(item => String(item))
66
- : deletesValue && typeof deletesValue === 'object'
67
- ? Object.keys(deletesValue)
68
- : [];
69
-
70
- const copies = (manifest.copies || {}) as Record<string, string>;
71
- const copiesCrcValue = manifest.copiesCrc;
72
- const copiesCrc =
73
- copiesCrcValue && typeof copiesCrcValue === 'object'
74
- ? (copiesCrcValue as Record<string, number>)
75
- : ({} as Record<string, number>);
76
- for (const to of Object.keys(copies)) {
77
- const rawFrom = copies[to];
78
- let from = String(rawFrom || '');
79
- if (normalizeResourceCopies) {
80
- from = from.replace('resources/rawfile/', '');
81
- if (!from) {
82
- from = to;
83
- }
84
- }
85
- copyFroms.push(from);
86
- copyTos.push(to);
87
- const crc = copiesCrc[to];
88
- copyCrcs.push(typeof crc === 'number' && Number.isFinite(crc) ? crc : null);
89
- }
90
-
91
- const hbcTransform = manifest.hbcTransform as
92
- | Record<string, object>
93
- | undefined;
94
- const hbcTransformEntry =
95
- hbcTransform && typeof hbcTransform === 'object'
96
- ? hbcTransform[BUNDLE_DELTA_ENTRY]
97
- : undefined;
98
- const hbcTransformMeta =
99
- hbcTransformEntry && typeof hbcTransformEntry === 'object'
100
- ? JSON.stringify(hbcTransformEntry)
101
- : '';
102
-
103
- return {
104
- copyFroms,
105
- copyTos,
106
- copyCrcs,
107
- deletes,
108
- hbcTransformMeta,
109
- };
110
- }
111
-
48
+ // staging 目录里的条目名 + __diff.json 原文:由原生内核统一校验与解析
49
+ // (cpp/patch_core/patch_manifest.h),这里不解析 manifest。
112
50
  interface DeltaInputs {
113
51
  entryNames: string[];
114
- manifestArrays: PatchManifestArrays;
115
- }
116
-
117
- function toArrayBufferSlice(
118
- payload: Uint8Array,
119
- offset: number,
120
- length: number,
121
- ): ArrayBuffer {
122
- return (payload.buffer as ArrayBuffer).slice(
123
- payload.byteOffset + offset,
124
- payload.byteOffset + offset + length,
125
- );
52
+ manifestJson: string;
126
53
  }
127
54
 
55
+ // 包内 rawfile 在 HAP 中的路径前缀:pdiff 的拷贝源按 HAP 路径命名,
56
+ // resourceManager 按去掉前缀后的路径读取。原生解析时只剥开头一次。
57
+ const RAWFILE_COPY_SOURCE_PREFIX = 'resources/rawfile/';
128
58
  const DIFF_MANIFEST_ENTRY = '__diff.json';
129
- const TEMP_ORIGIN_BUNDLE_ENTRY = '.origin.bundle.harmony.js';
130
- const FILE_COPY_BUFFER_SIZE = 64 * 1024;
59
+ const TEMP_ORIGIN_BUNDLE_ENTRY = `.origin.${HARMONY_BUNDLE_FILE_NAME}`;
131
60
  const DOWNLOAD_CALL_TIMEOUT_MS = 10 * 60 * 1000;
132
61
 
133
62
  // 断点续传 sidecar(NATIVE_CHECKUPDATE_DESIGN §11.4):记录 partial 属于
@@ -194,23 +123,12 @@ export class DownloadTask {
194
123
 
195
124
  private async promoteStaging(params: DownloadTaskParams): Promise<void> {
196
125
  const work = this.stagingDirectory(params);
197
- const bundlePath = `${work}/${HARMONY_BUNDLE_FILE_NAME}`;
198
- if (!fileIo.accessSync(bundlePath)) {
199
- throw createPushyError(
200
- ERROR_DELTA_FAILED,
201
- `bundle missing after install: ${bundlePath}`,
202
- );
203
- }
204
- // 整 bundle 几十 MB:摘要在 native 工作线程算,不冻结 UI 线程。
205
- const bundleSha256 = await NativePatchCore.sha256HexFileAsync(bundlePath);
206
- await writeInstallRecord(
207
- work,
208
- buildInstallRecord(params.hash, bundleSha256, this.artifactSha256),
209
- );
126
+ // 整 bundle 几十 MB:摘要与记录写入在 native 工作线程完成,不冻结 UI 线程。
127
+ await sealInstallRecord(work, params.hash, this.artifactSha256);
210
128
  if (fileIo.accessSync(params.unzipDirectory)) {
211
129
  await this.removeDirectory(params.unzipDirectory);
212
130
  }
213
- await fileIo.rename(work, params.unzipDirectory);
131
+ await promoteStaging(work, params.unzipDirectory);
214
132
  }
215
133
 
216
134
  constructor(context: common.Context) {
@@ -258,10 +176,7 @@ export class DownloadTask {
258
176
 
259
177
  private async deleteResumeSidecar(targetFile: string): Promise<void> {
260
178
  try {
261
- const sidecar = this.resumeSidecarPath(targetFile);
262
- if (fileIo.accessSync(sidecar)) {
263
- await fileIo.unlink(sidecar);
264
- }
179
+ await unlinkIfExists(this.resumeSidecarPath(targetFile));
265
180
  } catch (e) {
266
181
  logger.error(TAG, `Failed to delete resume sidecar: ${getErrorMessage(e)}`);
267
182
  }
@@ -270,9 +185,7 @@ export class DownloadTask {
270
185
  // 归档连同 sidecar 一起删除(被消费或被判定有毒)。
271
186
  private async deleteArchiveAndSidecar(targetFile: string): Promise<void> {
272
187
  try {
273
- if (fileIo.accessSync(targetFile)) {
274
- await fileIo.unlink(targetFile);
275
- }
188
+ await unlinkIfExists(targetFile);
276
189
  } catch (e) {
277
190
  logger.error(TAG, `Failed to delete archive: ${getErrorMessage(e)}`);
278
191
  }
@@ -328,35 +241,37 @@ export class DownloadTask {
328
241
 
329
242
  /**
330
243
  * 解压前读取归档解压后的总字节数(zlib.getOriginalSize,API 12——RNOH 的
331
- * 最低 API 已覆盖)。读不到就拒绝归档:zlib.decompressFile 没有逐条钩子,
332
- * 不知道展开量就放行等于允许一个 20MB 的归档在 measureExtractedDirectory
333
- * 跑到之前先把磁盘写满。
244
+ * 最低 API 已覆盖)。读不到就拒绝归档(checkOriginalSize):zlib.decompressFile
245
+ * 没有逐条钩子,不知道展开量就放行等于允许一个 20MB 的归档在
246
+ * checkExtractedDirectory 跑到之前先把磁盘写满。拒绝带专属错误细节
247
+ * (ORIGINAL_SIZE_UNAVAILABLE)并单独记日志,与"超限"拒绝可区分。
334
248
  */
335
249
  private async readOriginalSize(archiveFile: string): Promise<number> {
336
250
  let size: number | undefined;
251
+ let failure: string | undefined;
337
252
  try {
338
253
  size = await zlib.getOriginalSize(archiveFile);
339
254
  } catch (e) {
340
- throw createPushyError(
341
- ERROR_DELTA_FAILED,
342
- `cannot determine archive expansion size: ${getErrorMessage(e)}`,
343
- );
255
+ failure = getErrorMessage(e);
344
256
  }
345
- if (typeof size !== 'number' || !Number.isFinite(size) || size < 0) {
346
- throw createPushyError(
347
- ERROR_DELTA_FAILED,
348
- `cannot determine archive expansion size: ${String(size)}`,
257
+ try {
258
+ return checkOriginalSize(size, failure);
259
+ } catch (error) {
260
+ logger.error(
261
+ TAG,
262
+ `Refusing archive, expansion size unknown: ${getErrorMessage(error)}`,
349
263
  );
264
+ throw error;
350
265
  }
351
- return size;
352
266
  }
353
267
 
354
268
  /**
355
269
  * 解压 + 资源上限(cpp/patch_core/archive_limits.h)。zlib.decompressFile
356
270
  * 没有逐条钩子:解压前用 getOriginalSize 把总解压量与整包压缩比挡在
357
271
  * 上限内(20MB 归档 100:1 的 2GB 载荷不会先解出来再量),按已知解压量查
358
- * 磁盘,解压量未知直接拒绝;解压后再统计条目数/单条大小/总字节数并拒绝
359
- * 任意深度的 `.pushy-` 保留条目(超限即失败,staging 目录由失败清理删除)。
272
+ * 磁盘,解压量未知直接拒绝;解压后再逐条核对条目数/单条大小/总字节数,
273
+ * 拒绝任意深度的 `.pushy-` 保留条目与符号链接(规则见
274
+ * cpp/patch_core/archive_limits.h;超限即失败,staging 目录由失败清理删除)。
360
275
  */
361
276
  private async extractArchive(
362
277
  archiveFile: string,
@@ -377,7 +292,7 @@ export class DownloadTask {
377
292
  } catch (e) {
378
293
  throw toPushyError(e, ERROR_DELTA_FAILED);
379
294
  }
380
- await measureExtractedDirectory(unzipDirectory);
295
+ await checkExtractedDirectory(unzipDirectory, archiveStat.size);
381
296
  }
382
297
 
383
298
  private async recreateDirectory(path: string): Promise<void> {
@@ -415,33 +330,26 @@ export class DownloadTask {
415
330
  ): Promise<void> {
416
331
  const payload =
417
332
  content instanceof Uint8Array ? content : new Uint8Array(content);
333
+ // writeFully writes from the start of its buffer: a view into a larger
334
+ // buffer at a non-zero offset needs its own copy.
335
+ const bytes =
336
+ payload.byteOffset === 0
337
+ ? (payload.buffer as ArrayBuffer)
338
+ : (payload.buffer as ArrayBuffer).slice(
339
+ payload.byteOffset,
340
+ payload.byteOffset + payload.byteLength,
341
+ );
418
342
  await this.ensureParentDirectory(targetFile);
419
- if (fileIo.accessSync(targetFile)) {
420
- await fileIo.unlink(targetFile);
421
- }
343
+ await unlinkIfExists(targetFile);
422
344
 
423
- let writer: fileIo.File | null = null;
345
+ const writer = await fileIo.open(
346
+ targetFile,
347
+ fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY,
348
+ );
424
349
  try {
425
- writer = await fileIo.open(
426
- targetFile,
427
- fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY,
428
- );
429
- const chunkSize = FILE_COPY_BUFFER_SIZE;
430
- let bytesWritten = 0;
431
-
432
- while (bytesWritten < payload.byteLength) {
433
- const chunkLength = Math.min(
434
- chunkSize,
435
- payload.byteLength - bytesWritten,
436
- );
437
- const chunk = toArrayBufferSlice(payload, bytesWritten, chunkLength);
438
- await fileIo.write(writer.fd, chunk);
439
- bytesWritten += chunkLength;
440
- }
350
+ await writeFully(writer.fd, bytes, payload.byteLength);
441
351
  } finally {
442
- if (writer) {
443
- await fileIo.close(writer);
444
- }
352
+ await fileIo.close(writer);
445
353
  }
446
354
  }
447
355
 
@@ -451,19 +359,11 @@ export class DownloadTask {
451
359
  ) as Record<string, any>;
452
360
  }
453
361
 
454
- private async readManifestArrays(
455
- directory: string,
456
- normalizeResourceCopies: boolean,
457
- ): Promise<PatchManifestArrays> {
362
+ // __diff.json 原文;归档里没有时为空串(原生内核报 "diff.json not found")。
363
+ private async readManifestJson(directory: string): Promise<string> {
458
364
  const manifestPath = `${directory}/${DIFF_MANIFEST_ENTRY}`;
459
365
  if (!fileIo.accessSync(manifestPath)) {
460
- return {
461
- copyFroms: [],
462
- copyTos: [],
463
- copyCrcs: [],
464
- deletes: [],
465
- hbcTransformMeta: '',
466
- };
366
+ return '';
467
367
  }
468
368
 
469
369
  const manifestStat = await fileIo.stat(manifestPath);
@@ -473,34 +373,39 @@ export class DownloadTask {
473
373
  `delta manifest too large: ${manifestStat.size} bytes`,
474
374
  );
475
375
  }
476
- return parseManifestToArrays(
477
- this.parseJsonEntry(await this.readFileContent(manifestPath)),
478
- normalizeResourceCopies,
376
+ return new util.TextDecoder().decodeToString(
377
+ new Uint8Array(await this.readFileContent(manifestPath)),
479
378
  );
480
379
  }
481
380
 
482
-
483
- private async applyBundlePatchFromFileSource(
484
- originContent: ArrayBuffer,
485
- workingDirectory: string,
486
- bundleDeltaPath: string,
487
- outputFile: string,
488
- hbcTransformMeta = '',
489
- ): Promise<void> {
490
- const originBundlePath = `${workingDirectory}/${TEMP_ORIGIN_BUNDLE_ENTRY}`;
381
+ /**
382
+ * pdiff:hpatch 需要文件源,先把二进制内嵌的 bundle 从 rawfile descriptor
383
+ * 异步流式拷到 staging 里的临时文件(不整包读进内存、不在 UI 线程同步
384
+ * IO),原生内核只重建 bundle,拷贝分组交回由 copyFromResource 完成。
385
+ */
386
+ private async applyPatchFromEmbeddedBundle(
387
+ work: string,
388
+ inputs: DeltaInputs,
389
+ ): Promise<CopyGroupResult[]> {
390
+ const originBundlePath = `${work}/${TEMP_ORIGIN_BUNDLE_ENTRY}`;
491
391
  try {
492
- await this.writeFileContent(originBundlePath, originContent);
493
- await NativePatchCore.applyDeltaFromSource({
494
- copyFroms: [],
495
- copyTos: [],
496
- deletes: [],
497
- sourceRoot: workingDirectory,
498
- targetRoot: workingDirectory,
392
+ await withRawFd(
393
+ this.context.resourceManager,
394
+ HARMONY_BUNDLE_FILE_NAME,
395
+ descriptor => saveFileToSandbox(descriptor, originBundlePath),
396
+ );
397
+ return await NativePatchCore.applyArchiveDelta({
398
+ archiveType: ARCHIVE_PATCH_TYPE_FROM_PACKAGE,
399
+ entryNames: inputs.entryNames,
400
+ manifestJson: inputs.manifestJson,
401
+ bundleDeltaEntry: BUNDLE_DELTA_ENTRY,
402
+ copySourcePrefix: RAWFILE_COPY_SOURCE_PREFIX,
403
+ sourceRoot: work,
404
+ targetRoot: work,
499
405
  originBundlePath,
500
- bundleDeltaPath,
501
- bundleOutputPath: outputFile,
502
- enableMerge: false,
503
- bundleHbcTransformMeta: hbcTransformMeta,
406
+ bundleDeltaPath: `${work}/${BUNDLE_DELTA_ENTRY}`,
407
+ bundleOutputPath: `${work}/${HARMONY_BUNDLE_FILE_NAME}`,
408
+ hostCopiesResources: true,
504
409
  });
505
410
  } catch (error) {
506
411
  throw createPushyError(
@@ -508,56 +413,18 @@ export class DownloadTask {
508
413
  `Failed to process bundle delta: ${getErrorMessage(error)}`,
509
414
  );
510
415
  } finally {
511
- if (fileIo.accessSync(originBundlePath)) {
512
- await fileIo.unlink(originBundlePath);
513
- }
416
+ await unlinkIfExists(originBundlePath);
514
417
  }
515
418
  }
516
419
 
420
+ // 沙箱内文件复制(同一 rawfile 落到多个目标时,首个之外的目标从首个复制)。
421
+ // fileIo.copyFile 在工作线程整文件复制并覆盖目标,无需逐块搬运。
517
422
  private async copySandboxFile(
518
423
  sourceFile: string,
519
424
  targetFile: string,
520
425
  ): Promise<void> {
521
- let reader: fileIo.File | null = null;
522
- let writer: fileIo.File | null = null;
523
- const buffer = new ArrayBuffer(FILE_COPY_BUFFER_SIZE);
524
- let offset = 0;
525
-
526
- try {
527
- reader = await fileIo.open(sourceFile, fileIo.OpenMode.READ_ONLY);
528
- await this.ensureParentDirectory(targetFile);
529
- if (fileIo.accessSync(targetFile)) {
530
- await fileIo.unlink(targetFile);
531
- }
532
- writer = await fileIo.open(
533
- targetFile,
534
- fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY,
535
- );
536
-
537
- while (true) {
538
- const readLength = await fileIo.read(reader.fd, buffer, {
539
- offset,
540
- length: FILE_COPY_BUFFER_SIZE,
541
- });
542
- if (readLength <= 0) {
543
- break;
544
- }
545
-
546
- await fileIo.write(writer.fd, buffer.slice(0, readLength));
547
- offset += readLength;
548
-
549
- if (readLength < FILE_COPY_BUFFER_SIZE) {
550
- break;
551
- }
552
- }
553
- } finally {
554
- if (reader) {
555
- await fileIo.close(reader);
556
- }
557
- if (writer) {
558
- await fileIo.close(writer);
559
- }
560
- }
426
+ await this.ensureParentDirectory(targetFile);
427
+ await fileIo.copyFile(sourceFile, targetFile);
561
428
  }
562
429
 
563
430
  private async downloadFile(params: DownloadTaskParams): Promise<void> {
@@ -651,6 +518,24 @@ export class DownloadTask {
651
518
  authorize = resolve;
652
519
  });
653
520
 
521
+ // 本 url 的 sidecar 内容:验证器优先取本次响应头,缺失时沿用上次的;
522
+ // 总长未知(0)时不写。
523
+ const buildResumeMeta = (total: number): ResumeMeta => {
524
+ const meta: ResumeMeta = { url: params.url };
525
+ const etag = etagHeader || resumeMeta?.etag;
526
+ const lastModified = lastModifiedHeader || resumeMeta?.lastModified;
527
+ if (etag) {
528
+ meta.etag = etag;
529
+ }
530
+ if (lastModified) {
531
+ meta.lastModified = lastModified;
532
+ }
533
+ if (total > 0) {
534
+ meta.total = total;
535
+ }
536
+ return meta;
537
+ };
538
+
654
539
  const reportProgress = () => {
655
540
  const overall = baseOffset + received;
656
541
  const overallTotal = totalAll > 0
@@ -689,19 +574,18 @@ export class DownloadTask {
689
574
  `archive too large: exceeded ${MAX_ARCHIVE_BYTES}`,
690
575
  );
691
576
  }
692
- // 未知长度:响应到达时只能预留安全余量,所以任何会写过已预留字节数的
693
- // 写入之前先探测,每次至少预留接下来的 PROBE 字节(单个 chunk 更大时按
694
- // chunk 算)——任何写入都吃不到余量(归档上限本身远大于余量)。失败保留
695
- // partial 供下次续传。
696
- const writtenBefore = received - data.byteLength;
697
- const probeFreeSpace = totalAll <= 0 && received > freeSpaceReservedUntil;
698
- const reserve = Math.max(
699
- UNKNOWN_LENGTH_FREE_SPACE_PROBE_BYTES,
577
+ // 未知长度:响应到达时只能预留安全余量,任何会写过已预留位置的写入
578
+ // 之前都要探测(archive_limits::PlanUnknownLengthFreeSpaceProbe)。
579
+ // 失败保留 partial 供下次续传。
580
+ const plan = NativePatchCore.planFreeSpaceProbe(
581
+ totalAll,
582
+ received - data.byteLength,
700
583
  data.byteLength,
584
+ freeSpaceReservedUntil,
701
585
  );
702
- if (probeFreeSpace) {
703
- freeSpaceReservedUntil = writtenBefore + reserve;
704
- }
586
+ freeSpaceReservedUntil = plan.reservedUntil;
587
+ const probeFreeSpace = plan.probe;
588
+ const reserve = plan.reserve;
705
589
  writeQueue = writeQueue.then(async () => {
706
590
  if (!writer || writeError) {
707
591
  return;
@@ -934,9 +818,7 @@ export class DownloadTask {
934
818
  );
935
819
  } else {
936
820
  // 服务端忽略了 Range(或本就没发):从零开始。
937
- if (fileIo.accessSync(params.targetFile)) {
938
- await fileIo.unlink(params.targetFile);
939
- }
821
+ await unlinkIfExists(params.targetFile);
940
822
  baseOffset = 0;
941
823
  totalAll = contentLength > 0 ? contentLength : 0;
942
824
  writer = await fileIo.open(
@@ -956,19 +838,7 @@ export class DownloadTask {
956
838
  totalAll > 0 ? totalAll - baseOffset : 0,
957
839
  );
958
840
  // 先落 sidecar 再放行写入,流中崩溃才有得续。
959
- const meta: ResumeMeta = { url: params.url };
960
- const etag = etagHeader || resumeMeta?.etag;
961
- const lastModified = lastModifiedHeader || resumeMeta?.lastModified;
962
- if (etag) {
963
- meta.etag = etag;
964
- }
965
- if (lastModified) {
966
- meta.lastModified = lastModified;
967
- }
968
- if (totalAll > 0) {
969
- meta.total = totalAll;
970
- }
971
- await this.writeResumeMeta(params.targetFile, meta);
841
+ await this.writeResumeMeta(params.targetFile, buildResumeMeta(totalAll));
972
842
 
973
843
  // 鉴权通过:回放缓冲,之后的数据直写。
974
844
  const buffered = pendingChunks ?? [];
@@ -1010,15 +880,10 @@ export class DownloadTask {
1010
880
  }
1011
881
  // 用最终观测值刷新 sidecar:首次写入可能因头部竞态缺 total/验证器,
1012
882
  // 补齐后“下载完、解压前死”的窗口才能被 complete 检测覆盖。
1013
- const finalMeta: ResumeMeta = { url: params.url, total: stats.size };
1014
- if (etagHeader || resumeMeta?.etag) {
1015
- finalMeta.etag = etagHeader || resumeMeta?.etag;
1016
- }
1017
- if (lastModifiedHeader || resumeMeta?.lastModified) {
1018
- finalMeta.lastModified =
1019
- lastModifiedHeader || resumeMeta?.lastModified;
1020
- }
1021
- await this.writeResumeMeta(params.targetFile, finalMeta);
883
+ await this.writeResumeMeta(
884
+ params.targetFile,
885
+ buildResumeMeta(stats.size),
886
+ );
1022
887
  } else {
1023
888
  // 编码传输不可续传:盘上是解码后的字节,编码域的长度与偏移都失效。
1024
889
  await this.deleteResumeSidecar(params.targetFile);
@@ -1074,16 +939,12 @@ export class DownloadTask {
1074
939
  return work;
1075
940
  }
1076
941
 
1077
- /** 解压目录里的条目名 + __diff.json 解析结果,供 patch plan 使用。 */
1078
- private async readDeltaInputs(
1079
- work: string,
1080
- normalizeResourceCopies: boolean,
1081
- ): Promise<DeltaInputs> {
942
+ private async readDeltaInputs(work: string): Promise<DeltaInputs> {
1082
943
  const results = await Promise.all([
1083
944
  this.listEntryNames(work),
1084
- this.readManifestArrays(work, normalizeResourceCopies),
945
+ this.readManifestJson(work),
1085
946
  ]);
1086
- return { entryNames: results[0], manifestArrays: results[1] };
947
+ return { entryNames: results[0], manifestJson: results[1] };
1087
948
  }
1088
949
 
1089
950
  private async doFullPatch(params: DownloadTaskParams): Promise<void> {
@@ -1093,78 +954,27 @@ export class DownloadTask {
1093
954
 
1094
955
  private async doPatchFromApp(params: DownloadTaskParams): Promise<void> {
1095
956
  const work = await this.downloadAndExtract(params);
1096
- const inputs = await this.readDeltaInputs(work, true);
1097
- const manifestArrays = inputs.manifestArrays;
1098
-
1099
- NativePatchCore.buildArchivePlan(
1100
- ARCHIVE_PATCH_TYPE_FROM_PACKAGE,
1101
- inputs.entryNames,
1102
- manifestArrays.copyFroms,
1103
- manifestArrays.copyTos,
1104
- manifestArrays.deletes,
1105
- BUNDLE_DELTA_ENTRY,
1106
- );
1107
-
1108
- const bundleDeltaPath = `${work}/${BUNDLE_DELTA_ENTRY}`;
1109
- if (!fileIo.accessSync(bundleDeltaPath)) {
1110
- throw createPushyError(ERROR_DELTA_FAILED, 'bundle delta not found');
1111
- }
1112
- const resourceManager = this.context.resourceManager;
1113
- const originContent = await resourceManager.getRawFileContent(
1114
- 'bundle.harmony.js',
1115
- );
1116
- await this.applyBundlePatchFromFileSource(
1117
- originContent.buffer as ArrayBuffer,
957
+ const copyGroups = await this.applyPatchFromEmbeddedBundle(
1118
958
  work,
1119
- bundleDeltaPath,
1120
- `${work}/${HARMONY_BUNDLE_FILE_NAME}`,
1121
- manifestArrays.hbcTransformMeta,
1122
- );
1123
- // 组按 from 聚合;同一 from 的内容必然一致,所以每组一个 CRC 即可
1124
- // (与 manifest 逐位对齐的 copyCrcs 里取第一个非空值)。
1125
- const crcByFrom = new Map<string, number>();
1126
- manifestArrays.copyFroms.forEach((from, index) => {
1127
- const crc = manifestArrays.copyCrcs[index];
1128
- if (crc !== null && !crcByFrom.has(from)) {
1129
- crcByFrom.set(from, crc);
1130
- }
1131
- });
1132
- await this.copyFromResource(
1133
- NativePatchCore.buildCopyGroups(
1134
- manifestArrays.copyFroms,
1135
- manifestArrays.copyTos,
1136
- ),
1137
- work,
1138
- crcByFrom,
959
+ await this.readDeltaInputs(work),
1139
960
  );
961
+ await this.copyFromResource(copyGroups, work);
1140
962
  await this.deleteArchiveAndSidecar(params.targetFile);
1141
963
  }
1142
964
 
1143
965
  private async doPatchFromPpk(params: DownloadTaskParams): Promise<void> {
1144
966
  const work = await this.downloadAndExtract(params);
1145
- const inputs = await this.readDeltaInputs(work, false);
1146
- const manifestArrays = inputs.manifestArrays;
1147
-
1148
- const plan = NativePatchCore.buildArchivePlan(
1149
- ARCHIVE_PATCH_TYPE_FROM_PPK,
1150
- inputs.entryNames,
1151
- manifestArrays.copyFroms,
1152
- manifestArrays.copyTos,
1153
- manifestArrays.deletes,
1154
- BUNDLE_DELTA_ENTRY,
1155
- );
1156
- await NativePatchCore.applyDeltaFromSource({
1157
- copyFroms: manifestArrays.copyFroms,
1158
- copyTos: manifestArrays.copyTos,
1159
- deletes: manifestArrays.deletes,
967
+ const inputs = await this.readDeltaInputs(work);
968
+ await NativePatchCore.applyArchiveDelta({
969
+ archiveType: ARCHIVE_PATCH_TYPE_FROM_PPK,
970
+ entryNames: inputs.entryNames,
971
+ manifestJson: inputs.manifestJson,
972
+ bundleDeltaEntry: BUNDLE_DELTA_ENTRY,
1160
973
  sourceRoot: params.originDirectory,
1161
974
  targetRoot: work,
1162
- originBundlePath: `${params.originDirectory}/bundle.harmony.js`,
975
+ originBundlePath: `${params.originDirectory}/${HARMONY_BUNDLE_FILE_NAME}`,
1163
976
  bundleDeltaPath: `${work}/${BUNDLE_DELTA_ENTRY}`,
1164
977
  bundleOutputPath: `${work}/${HARMONY_BUNDLE_FILE_NAME}`,
1165
- mergeSourceSubdir: plan.mergeSourceSubdir,
1166
- enableMerge: plan.enableMerge,
1167
- bundleHbcTransformMeta: manifestArrays.hbcTransformMeta,
1168
978
  });
1169
979
  logger.info(TAG, 'Delta from PPK completed');
1170
980
  await this.deleteArchiveAndSidecar(params.targetFile);
@@ -1173,7 +983,6 @@ export class DownloadTask {
1173
983
  private async copyFromResource(
1174
984
  copyGroups: CopyGroupResult[],
1175
985
  targetRoot: string,
1176
- crcByFrom?: Map<string, number>,
1177
986
  ): Promise<void> {
1178
987
  let currentFrom = '';
1179
988
  try {
@@ -1185,7 +994,7 @@ export class DownloadTask {
1185
994
  if (targets.length === 0) {
1186
995
  continue;
1187
996
  }
1188
- const expectedCrc = crcByFrom?.get(currentFrom);
997
+ const expectedCrc = group.expectedCrc;
1189
998
 
1190
999
  if (currentFrom.startsWith('resources/base/media/')) {
1191
1000
  // Strip only the final extension: 'icon.round.png' -> 'icon.round',
@@ -1218,28 +1027,15 @@ export class DownloadTask {
1218
1027
  continue;
1219
1028
  }
1220
1029
 
1221
- const fromContent = await resourceManager.getRawFd(currentFrom);
1222
- try {
1223
- const firstTarget = targets[0];
1224
- const restTargets = targets.slice(1);
1225
- await this.ensureParentDirectories(targets);
1226
- if (fileIo.accessSync(firstTarget)) {
1227
- await fileIo.unlink(firstTarget);
1228
- }
1229
- saveFileToSandbox(fromContent, firstTarget);
1230
- await Promise.all(
1231
- restTargets.map(target => this.copySandboxFile(firstTarget, target)),
1232
- );
1233
- } finally {
1234
- try {
1235
- await resourceManager.closeRawFd(currentFrom);
1236
- } catch (closeError) {
1237
- logger.error(
1238
- TAG,
1239
- `Failed to close raw fd for ${currentFrom}: ${getErrorMessage(closeError)}`,
1240
- );
1241
- }
1242
- }
1030
+ const firstTarget = targets[0];
1031
+ const restTargets = targets.slice(1);
1032
+ await this.ensureParentDirectories(targets);
1033
+ await withRawFd(resourceManager, currentFrom, descriptor =>
1034
+ saveFileToSandbox(descriptor, firstTarget),
1035
+ );
1036
+ await Promise.all(
1037
+ restTargets.map(target => this.copySandboxFile(firstTarget, target)),
1038
+ );
1243
1039
  }
1244
1040
  } catch (error) {
1245
1041
  const coded = toPushyError(error, ERROR_DELTA_FAILED);
@@ -1282,25 +1078,7 @@ export class DownloadTask {
1282
1078
  }
1283
1079
  }
1284
1080
 
1285
- private async doCleanUp(params: DownloadTaskParams): Promise<void> {
1286
- try {
1287
- await NativePatchCore.cleanupOldEntries(
1288
- params.unzipDirectory,
1289
- [params.hash, params.originHash].filter(name => !!name),
1290
- 3,
1291
- );
1292
- } catch (error) {
1293
- const message = `Cleanup failed: ${getErrorMessage(error)}`;
1294
- logger.error(TAG, message);
1295
- throw createPushyError(ERROR_FILE_OPERATION_FAILED, message);
1296
- }
1297
- }
1298
-
1299
1081
  public async execute(params: DownloadTaskParams): Promise<void> {
1300
- const isPatchTask =
1301
- params.type === DownloadTaskParams.TASK_TYPE_PATCH_FULL ||
1302
- params.type === DownloadTaskParams.TASK_TYPE_PATCH_FROM_APP ||
1303
- params.type === DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK;
1304
1082
  try {
1305
1083
  switch (params.type) {
1306
1084
  case DownloadTaskParams.TASK_TYPE_PATCH_FULL:
@@ -1312,24 +1090,13 @@ export class DownloadTask {
1312
1090
  case DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK:
1313
1091
  await this.doPatchFromPpk(params);
1314
1092
  break;
1315
- case DownloadTaskParams.TASK_TYPE_CLEANUP:
1316
- await this.doCleanUp(params);
1317
- break;
1318
- case DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD:
1319
- await this.downloadFile(params);
1320
- // 文件在完成时即是最终产物——残留的 sidecar 会让下次同 URL 的
1321
- // 下载不发请求直接返回这份旧字节。
1322
- await this.deleteResumeSidecar(params.targetFile);
1323
- break;
1324
1093
  default:
1325
1094
  throw createPushyError(
1326
1095
  ERROR_INVALID_OPTIONS,
1327
1096
  `Unknown task type: ${params.type}`,
1328
1097
  );
1329
1098
  }
1330
- if (isPatchTask) {
1331
- await this.promoteStaging(params);
1332
- }
1099
+ await this.promoteStaging(params);
1333
1100
  } catch (rawError) {
1334
1101
  // 稳定错误码:下载阶段的失败是 DOWNLOAD_FAILED;归档收齐之后的失败
1335
1102
  // (解压/hpatch/资源拷贝含 copiesCrc 校验/完成记录)一律 PATCH_FAILED,
@@ -1340,29 +1107,22 @@ export class DownloadTask {
1340
1107
  this.downloadPhaseCompleted ? ERROR_DELTA_FAILED : ERROR_DOWNLOAD_FAILED,
1341
1108
  );
1342
1109
  logger.error(TAG, `Task execution failed: ${error.message}`);
1343
- if (params.type !== DownloadTaskParams.TASK_TYPE_CLEANUP) {
1344
- try {
1345
- if (params.type === DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD) {
1346
- await fileIo.unlink(params.targetFile);
1347
- await this.deleteResumeSidecar(params.targetFile);
1348
- } else {
1349
- // Only the staging directory is ours to drop: the final version
1350
- // directory is never touched by a failed install, so a failed
1351
- // duplicate task cannot remove an earlier task's install.
1352
- await this.removeDirectory(this.stagingDirectory(params));
1353
- if (this.downloadPhaseCompleted) {
1354
- // 收齐后才失败的是解压/patch 失败:归档有毒,续传只会撞上
1355
- // 同一个失败。下载阶段的失败则保留 partial + sidecar——那
1356
- // 就是续传状态。
1357
- await this.deleteArchiveAndSidecar(params.targetFile);
1358
- }
1359
- }
1360
- } catch (cleanupError) {
1361
- logger.error(
1362
- TAG,
1363
- `Cleanup after error failed: ${getErrorMessage(cleanupError)}`,
1364
- );
1110
+ try {
1111
+ // Only the staging directory is ours to drop: the final version
1112
+ // directory is never touched by a failed install, so a failed
1113
+ // duplicate task cannot remove an earlier task's install.
1114
+ await this.removeDirectory(this.stagingDirectory(params));
1115
+ if (this.downloadPhaseCompleted) {
1116
+ // 收齐后才失败的是解压/patch 失败:归档有毒,续传只会撞上
1117
+ // 同一个失败。下载阶段的失败则保留 partial + sidecar——那
1118
+ // 就是续传状态。
1119
+ await this.deleteArchiveAndSidecar(params.targetFile);
1365
1120
  }
1121
+ } catch (cleanupError) {
1122
+ logger.error(
1123
+ TAG,
1124
+ `Cleanup after error failed: ${getErrorMessage(cleanupError)}`,
1125
+ );
1366
1126
  }
1367
1127
  throw error;
1368
1128
  }