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.
- package/README-CN.md +31 -31
- package/README.md +31 -31
- package/android/jni/HDiffPatch/.travis.yml +33 -0
- package/android/jni/HDiffPatch/builds/xcode/bestParams.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/android/jni/HDiffPatch/builds/xcode/hdiffz.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/android/jni/HDiffPatch/builds/xcode/hdiffz.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/android/jni/HDiffPatch/builds/xcode/hpatchz.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/android/jni/HDiffPatch/builds/xcode/lz4.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/android/jni/HDiffPatch/builds/xcode/lzma.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/android/jni/HDiffPatch/builds/xcode/testHashClash.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/android/jni/HDiffPatch/builds/xcode/testHashClash.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/android/jni/HDiffPatch/builds/xcode/unitTest.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/android/jni/HDiffPatch/builds/xcode/zstd.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/android/jni/lzma/DOC/7zC.txt +187 -0
- package/android/jni/lzma/DOC/7zFormat.txt +469 -0
- package/android/jni/lzma/DOC/Methods.txt +173 -0
- package/android/jni/lzma/DOC/installer.txt +166 -0
- package/android/jni/lzma/DOC/lzma-history.txt +446 -0
- package/android/jni/lzma/DOC/lzma-sdk.txt +357 -0
- package/android/jni/lzma/DOC/lzma-specification.txt +1176 -0
- package/android/jni/lzma/DOC/lzma.txt +328 -0
- package/android/jni/lzma/bin/7zS2.sfx +0 -0
- package/android/jni/lzma/bin/7zS2con.sfx +0 -0
- package/android/jni/lzma/bin/7zSD.sfx +0 -0
- package/android/jni/lzma/bin/7zdec.exe +0 -0
- package/android/jni/lzma/bin/7zr.exe +0 -0
- package/android/jni/lzma/bin/installer/config.txt +5 -0
- package/android/jni/lzma/bin/installer/cr.bat +5 -0
- package/android/jni/lzma/bin/lzma.exe +0 -0
- package/android/jni/lzma/bin/x64/7zr.exe +0 -0
- package/android/lib/arm64-v8a/librnupdate.so +0 -0
- package/android/lib/armeabi-v7a/librnupdate.so +0 -0
- package/android/lib/x86/librnupdate.so +0 -0
- package/android/lib/x86_64/librnupdate.so +0 -0
- package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +44 -0
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +124 -40
- package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +125 -0
- package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +43 -11
- package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateFlow.java +2 -0
- package/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java +90 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +79 -19
- package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +21 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +33 -2
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleSupport.java +1 -0
- package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +5 -0
- package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +5 -0
- package/cpp/patch_core/archive_limits.h +38 -0
- package/cpp/patch_core/install_record.h +36 -0
- package/cpp/update_flow_core/flow_json.h +1 -0
- package/cpp/update_flow_core/update_flow_core.cpp +41 -7
- package/cpp/update_flow_core/update_flow_core.h +9 -0
- package/cpp/update_flow_core/update_flow_jni.cpp +12 -0
- package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +2 -0
- package/harmony/pushy/src/main/cpp/pushy.cpp +37 -0
- package/harmony/pushy/src/main/ets/ArchiveLimits.ts +83 -0
- package/harmony/pushy/src/main/ets/DownloadTask.ts +126 -30
- package/harmony/pushy/src/main/ets/InstallRecord.ts +129 -0
- package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +24 -2
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +5 -0
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +12 -0
- package/harmony/pushy/src/main/ets/UpdateContext.ts +78 -15
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.mm +384 -25
- package/ios/RCTPushy/RCTPushyDownloader.mm +32 -0
- package/package.json +25 -2
- package/src/NativePushy.ts +13 -0
- package/src/client.ts +160 -43
- package/src/core.ts +17 -3
- package/src/error.ts +3 -0
- package/src/index.ts +8 -0
- package/src/locales/en.ts +3 -0
- package/src/locales/zh.ts +2 -0
- package/src/metadata.ts +166 -0
- package/src/provider.tsx +52 -17
- package/src/type.ts +22 -0
- package/src/updateFlowCore.ts +95 -4
- package/src/utils.ts +31 -19
- package/.gitmodules +0 -6
- package/BUNDLEHASH_MIGRATION.md +0 -355
- package/NATIVE_CHECKUPDATE_DESIGN.md +0 -539
- package/NATIVE_CHECK_FOLLOWUPS.md +0 -514
- package/android/jni/HDiffPatch/test/_private_searchBestParams.cpp +0 -301
- package/android/jni/HDiffPatch/test/hpatchz_test.cpp +0 -292
- package/android/jni/HDiffPatch/test/testHashClash.cpp +0 -436
- package/android/jni/HDiffPatch/test/unit_test.cpp +0 -417
- package/biome.json +0 -59
- package/cpp/patch_core/tests/fixtures/rawstored.lzma2label.patch.bin +0 -0
- package/cpp/patch_core/tests/fixtures/rawstored.new.bin +0 -0
- package/cpp/patch_core/tests/fixtures/rawstored.old.bin +0 -0
- package/cpp/patch_core/tests/fixtures/v96.hbc +0 -0
- package/cpp/patch_core/tests/fixtures/v96.meta.json +0 -1
- package/cpp/patch_core/tests/fixtures/v96.streampatch.bin +0 -0
- package/cpp/patch_core/tests/fixtures/v96.t.hbc +0 -0
- package/cpp/patch_core/tests/fixtures/v96.tpatch.bin +0 -0
- package/cpp/patch_core/tests/fixtures/v96.tstreampatch.bin +0 -0
- package/cpp/patch_core/tests/fixtures/v96b.hbc +0 -0
- package/cpp/patch_core/tests/fixtures/v98.hbc +0 -0
- package/cpp/patch_core/tests/fixtures/v98.t.hbc +0 -0
- package/cpp/patch_core/tests/fixtures/v98b.hbc +0 -0
- package/cpp/patch_core/tests/fixtures/v98b.t.hbc +0 -0
- package/cpp/patch_core/tests/hbc_transform_test.cpp +0 -384
- package/cpp/patch_core/tests/patch_core_test.cpp +0 -970
- package/cpp/update_flow_core/tests/flow_vectors.json +0 -1401
- package/cpp/update_flow_core/tests/update_flow_core_test.cpp +0 -317
- 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
|
-
|
|
878
|
-
await
|
|
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
|
-
|
|
997
|
+
const work = this.stagingDirectory(params);
|
|
998
|
+
await this.recreateDirectory(work);
|
|
999
|
+
this.artifactSha256 = NativePatchCore.sha256HexFile(params.targetFile);
|
|
899
1000
|
|
|
900
|
-
await
|
|
1001
|
+
await this.extractArchive(params.targetFile, work);
|
|
901
1002
|
const [entryNames, manifestArrays] = await Promise.all([
|
|
902
|
-
this.listEntryNames(
|
|
903
|
-
this.readManifestArrays(
|
|
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 = `${
|
|
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
|
-
|
|
1026
|
+
work,
|
|
926
1027
|
bundlePatchPath,
|
|
927
|
-
`${
|
|
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
|
-
|
|
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
|
-
|
|
1065
|
+
const work = this.stagingDirectory(params);
|
|
1066
|
+
await this.recreateDirectory(work);
|
|
1067
|
+
this.artifactSha256 = NativePatchCore.sha256HexFile(params.targetFile);
|
|
965
1068
|
|
|
966
|
-
await
|
|
1069
|
+
await this.extractArchive(params.targetFile, work);
|
|
967
1070
|
const [entryNames, manifestArrays] = await Promise.all([
|
|
968
|
-
this.listEntryNames(
|
|
969
|
-
this.readManifestArrays(
|
|
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:
|
|
1088
|
+
targetRoot: work,
|
|
986
1089
|
originBundlePath: `${params.originDirectory}/bundle.harmony.js`,
|
|
987
|
-
bundlePatchPath: `${
|
|
988
|
-
bundleOutputPath: `${
|
|
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.
|
|
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
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
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
|
-
|
|
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> {
|