@onekeyfe/hd-core 1.2.0-alpha.97 → 1.2.0-alpha.99
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/__tests__/check-all-firmware-release-protocol-v2.test.ts +411 -86
- package/__tests__/check-firmware-release-protocol-v2.test.ts +199 -0
- package/__tests__/firmware-memory-host.test.ts +126 -0
- package/__tests__/firmware-update/device-update-bootloader.test.ts +14 -2
- package/__tests__/firmware-update/firmware-host-binding.test.ts +9 -0
- package/__tests__/firmware-update/firmware-prepared-host-digest.test.ts +573 -0
- package/__tests__/firmware-update/firmware-prepared-resource-archive.test.ts +150 -0
- package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +65 -2
- package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +80 -0
- package/__tests__/firmware-update/firmware-update-plan.test.ts +293 -71
- package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +53 -1
- package/__tests__/firmware-update/firmware-update-prepared-resource-executors.test.ts +326 -0
- package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +2 -2
- package/__tests__/protocol-v2-firmware-targets.test.ts +16 -0
- package/__tests__/protocol-v2-resources.test.ts +148 -232
- package/__tests__/protocol-v2.test.ts +1326 -510
- package/dist/api/CheckAllFirmwareRelease.d.ts +2 -3
- package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
- package/dist/api/CheckBLEFirmwareRelease.d.ts +3 -2
- package/dist/api/CheckBLEFirmwareRelease.d.ts.map +1 -1
- package/dist/api/CheckBootloaderRelease.d.ts +2 -1
- package/dist/api/CheckBootloaderRelease.d.ts.map +1 -1
- package/dist/api/CheckFirmwareRelease.d.ts +3 -2
- package/dist/api/CheckFirmwareRelease.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV2.d.ts +1 -0
- package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +11 -9
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/UploadPortfolio.d.ts.map +1 -1
- package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareHostBinding.d.ts +1 -1
- package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareMemoryHost.d.ts +22 -0
- package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +1 -0
- package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts +11 -0
- package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts +48 -1
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +7 -1
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
- package/dist/api/firmware/getBinary.d.ts +0 -1
- package/dist/api/firmware/getBinary.d.ts.map +1 -1
- package/dist/api/firmware/protocolV2Release.d.ts +33 -0
- package/dist/api/firmware/protocolV2Release.d.ts.map +1 -0
- package/dist/data-manager/DataManager.d.ts +4 -3
- package/dist/data-manager/DataManager.d.ts.map +1 -1
- package/dist/index.d.ts +108 -114
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1825 -1006
- package/dist/protocols/protocol-v2/resources.d.ts +10 -28
- package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
- package/dist/types/api/checkAllFirmwareRelease.d.ts +20 -8
- package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
- package/dist/types/api/checkBLEFirmwareRelease.d.ts +2 -13
- package/dist/types/api/checkBLEFirmwareRelease.d.ts.map +1 -1
- package/dist/types/api/checkBootloaderRelease.d.ts +2 -6
- package/dist/types/api/checkBootloaderRelease.d.ts.map +1 -1
- package/dist/types/api/checkFirmwareRelease.d.ts +2 -13
- package/dist/types/api/checkFirmwareRelease.d.ts.map +1 -1
- package/dist/types/api/checkFirmwareTypeAvailable.d.ts +2 -2
- package/dist/types/api/checkFirmwareTypeAvailable.d.ts.map +1 -1
- package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
- package/dist/types/api/export.d.ts +1 -1
- package/dist/types/api/export.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdate.d.ts +4 -12
- package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdatePlan.d.ts +2 -2
- package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -1
- package/dist/types/settings.d.ts +35 -36
- package/dist/types/settings.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/CheckAllFirmwareRelease.ts +113 -80
- package/src/api/CheckBLEFirmwareRelease.ts +37 -5
- package/src/api/CheckBootloaderRelease.ts +35 -3
- package/src/api/CheckFirmwareRelease.ts +35 -7
- package/src/api/FirmwareUpdateV2.ts +231 -138
- package/src/api/FirmwareUpdateV3.ts +101 -56
- package/src/api/FirmwareUpdateV4.ts +936 -408
- package/src/api/UploadPortfolio.ts +18 -0
- package/src/api/device/DeviceUpdateBootloader.ts +37 -11
- package/src/api/firmware/FirmwareHostBinding.ts +26 -3
- package/src/api/firmware/FirmwareMemoryHost.ts +143 -0
- package/src/api/firmware/FirmwarePreparedResourceArchive.ts +207 -0
- package/src/api/firmware/FirmwareUpdateBaseMethod.ts +122 -73
- package/src/api/firmware/FirmwareUpdatePlan.ts +295 -122
- package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +49 -12
- package/src/api/firmware/protocolV2Release.ts +168 -0
- package/src/data-manager/DataManager.ts +35 -19
- package/src/index.ts +10 -0
- package/src/protocols/protocol-v2/resources.ts +157 -341
- package/src/types/api/checkAllFirmwareRelease.ts +27 -12
- package/src/types/api/checkBLEFirmwareRelease.ts +4 -13
- package/src/types/api/checkBootloaderRelease.ts +2 -6
- package/src/types/api/checkFirmwareRelease.ts +2 -13
- package/src/types/api/checkFirmwareTypeAvailable.ts +2 -2
- package/src/types/api/deviceUpdateBootloader.ts +1 -0
- package/src/types/api/export.ts +6 -1
- package/src/types/api/firmwareUpdate.ts +12 -17
- package/src/types/api/firmwareUpdatePlan.ts +2 -2
- package/src/types/settings.ts +35 -61
- package/src/utils/deviceFeaturesUtils.ts +2 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EDeviceType, ERRORS, HardwareError, HardwareErrorCode, wait } from '@onekeyfe/hd-shared';
|
|
2
|
+
import JSZip from 'jszip';
|
|
2
3
|
import {
|
|
3
4
|
DeviceRebootType,
|
|
4
5
|
PROTOCOL_V2_BLE_FILE_CHUNK_SIZE,
|
|
@@ -9,7 +10,6 @@ import {
|
|
|
9
10
|
import { sha256 } from '@noble/hashes/sha256';
|
|
10
11
|
|
|
11
12
|
import { FirmwareUpdateTipMessage, UI_REQUEST } from '../events/ui-request';
|
|
12
|
-
import { validateProtocolV2FilesystemPath } from './helpers/filesystemValidation';
|
|
13
13
|
import { validateParams } from './helpers/paramsValidator';
|
|
14
14
|
import {
|
|
15
15
|
LoggerNames,
|
|
@@ -31,21 +31,31 @@ import {
|
|
|
31
31
|
} from '../protocols/protocol-v2';
|
|
32
32
|
import { requestProtocolV2DeviceInfo } from '../protocols/protocol-v2/features';
|
|
33
33
|
import {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
isProtocolV2ResourceFileValid,
|
|
37
|
-
readProtocolV2ResourceInventory,
|
|
34
|
+
parseProtocolV2ResourceManifest,
|
|
35
|
+
selectProtocolV2ResourceManifestFiles,
|
|
38
36
|
} from '../protocols/protocol-v2/resources';
|
|
39
37
|
import {
|
|
40
38
|
getProtocolV2UnknownErrorText,
|
|
41
39
|
isProtocolV2DeviceDisconnectedError,
|
|
42
40
|
} from './protocol-v2/helpers';
|
|
43
|
-
import {
|
|
44
|
-
|
|
41
|
+
import {
|
|
42
|
+
openFirmwareByteSource,
|
|
43
|
+
readFirmwareByteSourceFully,
|
|
44
|
+
writeFirmwareByteSource,
|
|
45
|
+
} from './firmware/FirmwareArtifactSource';
|
|
46
|
+
import {
|
|
47
|
+
registerFirmwareUpdateHostBinding,
|
|
48
|
+
resolveFirmwareUpdateHostBinding,
|
|
49
|
+
unregisterFirmwareUpdateHostBinding,
|
|
50
|
+
} from './firmware/FirmwareHostBinding';
|
|
45
51
|
import {
|
|
46
52
|
assertFirmwareUpdatePreparedPlanBinding,
|
|
47
53
|
assertFirmwareUpdatePreparedPlanDeviceIdentity,
|
|
54
|
+
prepareFirmwareUpdatePlan,
|
|
55
|
+
validateFirmwareUpdatePreparedPlan,
|
|
48
56
|
} from './firmware/FirmwareUpdatePreparedPlan';
|
|
57
|
+
import { prepareFirmwareUpdateV4MemoryHost } from './firmware/FirmwareMemoryHost';
|
|
58
|
+
import { buildProtocolV2LocalFirmwareUpdatePlan } from './firmware/FirmwareUpdatePlan';
|
|
49
59
|
|
|
50
60
|
import type {
|
|
51
61
|
FirmwareArtifactReference,
|
|
@@ -59,11 +69,14 @@ import type {
|
|
|
59
69
|
Features,
|
|
60
70
|
IFirmwareReleaseInfo,
|
|
61
71
|
IProtocolV2FirmwareComponent,
|
|
62
|
-
IProtocolV2Resource,
|
|
63
|
-
IProtocolV2ResourceFile,
|
|
64
72
|
IVersionArray,
|
|
65
73
|
} from '../types';
|
|
66
74
|
import type { FirmwareByteSource } from './firmware/FirmwareArtifactSource';
|
|
75
|
+
import type {
|
|
76
|
+
FirmwareMemoryArtifact,
|
|
77
|
+
FirmwareMemoryArtifactEntry,
|
|
78
|
+
FirmwareUpdateV4MemoryHost,
|
|
79
|
+
} from './firmware/FirmwareMemoryHost';
|
|
67
80
|
|
|
68
81
|
const Log = getLogger(LoggerNames.Method);
|
|
69
82
|
|
|
@@ -95,18 +108,49 @@ const PROTOCOL_V2_OKPP_HEADER_SIZE = 0x52a0;
|
|
|
95
108
|
const PROTOCOL_V2_OKPP_PAYLOAD_HASH_OFFSET = 0x200;
|
|
96
109
|
const PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET = 0x240;
|
|
97
110
|
const PROTOCOL_V2_OKPP_HASH_SIZE = 64;
|
|
111
|
+
const PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES = 1024 * 1024;
|
|
112
|
+
const PROTOCOL_V2_RESOURCE_FILE_MAX_COUNT = 512;
|
|
113
|
+
const PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES = 256 * 1024 * 1024;
|
|
98
114
|
|
|
99
115
|
const PROTOCOL_V2_NEO_UNSUPPORTED_TARGETS = new Set<FirmwareUpdateV4Target>(['se03', 'se04']);
|
|
100
116
|
|
|
117
|
+
const getProtocolV2ZipEntrySizes = (entry: JSZip.JSZipObject) => {
|
|
118
|
+
// loadAsync records central-directory sizes on JSZip's documented private data object.
|
|
119
|
+
// Validate those bounds before calling async(), which allocates the decompressed entry.
|
|
120
|
+
const { compressedSize, uncompressedSize } = (entry as JSZipSizedEntry)._data ?? {};
|
|
121
|
+
if (
|
|
122
|
+
!Number.isSafeInteger(compressedSize) ||
|
|
123
|
+
Number(compressedSize) < 0 ||
|
|
124
|
+
!Number.isSafeInteger(uncompressedSize) ||
|
|
125
|
+
Number(uncompressedSize) <= 0
|
|
126
|
+
) {
|
|
127
|
+
throw ERRORS.TypedError(
|
|
128
|
+
HardwareErrorCode.RuntimeError,
|
|
129
|
+
`Protocol V2 local resource ZIP entry size is invalid: ${entry.name}`,
|
|
130
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
compressedSize: Number(compressedSize),
|
|
135
|
+
uncompressedSize: Number(uncompressedSize),
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
|
|
101
139
|
export function assertProtocolV2FirmwareTargetsSupported(
|
|
102
140
|
deviceType: EDeviceType | string | undefined,
|
|
103
|
-
params: FirmwareUpdateV4Params
|
|
141
|
+
params: FirmwareUpdateV4Params,
|
|
142
|
+
hasExplicitTargetSelection = false
|
|
104
143
|
) {
|
|
144
|
+
const requestedTargets = new Set(params.targetsToUpdate ?? []);
|
|
105
145
|
const unsupportedTargets = new Set(
|
|
106
|
-
(
|
|
146
|
+
Array.from(requestedTargets).filter(target => PROTOCOL_V2_NEO_UNSUPPORTED_TARGETS.has(target))
|
|
107
147
|
);
|
|
108
|
-
if (params.se03Binary
|
|
109
|
-
|
|
148
|
+
if (params.se03Binary && (!hasExplicitTargetSelection || requestedTargets.has('se03'))) {
|
|
149
|
+
unsupportedTargets.add('se03');
|
|
150
|
+
}
|
|
151
|
+
if (params.se04Binary && (!hasExplicitTargetSelection || requestedTargets.has('se04'))) {
|
|
152
|
+
unsupportedTargets.add('se04');
|
|
153
|
+
}
|
|
110
154
|
|
|
111
155
|
if (!unsupportedTargets.size || deviceType === EDeviceType.Pro2) return;
|
|
112
156
|
|
|
@@ -148,12 +192,6 @@ type ProtocolV2TargetBinary = { fileName: string; binary: ArrayBuffer; targetId:
|
|
|
148
192
|
type ProtocolV2InstallItem = ProtocolV2TargetBinary & {
|
|
149
193
|
kind: ProtocolV2RemoteComponentTarget['kind'];
|
|
150
194
|
};
|
|
151
|
-
/** Protocol V2 resource file written independently to devicePath through FileWrite. */
|
|
152
|
-
type ProtocolV2ResourceBundleBinary = {
|
|
153
|
-
name: string;
|
|
154
|
-
binary: ArrayBuffer;
|
|
155
|
-
devicePath: string;
|
|
156
|
-
};
|
|
157
195
|
|
|
158
196
|
type ProtocolV2RemoteComponentBinary = ProtocolV2RemoteComponentTarget & {
|
|
159
197
|
binary: ArrayBuffer;
|
|
@@ -181,6 +219,18 @@ type ProtocolV2ResourceBundleSource = {
|
|
|
181
219
|
headerHash?: string;
|
|
182
220
|
};
|
|
183
221
|
|
|
222
|
+
type ProtocolV2LocalResourceArchive = {
|
|
223
|
+
binary: ArrayBuffer;
|
|
224
|
+
materializedEntries: FirmwareMemoryArtifactEntry[];
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
type JSZipSizedEntry = JSZip.JSZipObject & {
|
|
228
|
+
_data?: {
|
|
229
|
+
compressedSize?: unknown;
|
|
230
|
+
uncompressedSize?: unknown;
|
|
231
|
+
};
|
|
232
|
+
};
|
|
233
|
+
|
|
184
234
|
type ProtocolV2ExecutionPhaseKind =
|
|
185
235
|
| 'resource-sync'
|
|
186
236
|
| 'bootloader-install'
|
|
@@ -252,7 +302,23 @@ const PROTOCOL_V2_FIRMWARE_STAGING_PATHS = new Set(
|
|
|
252
302
|
)
|
|
253
303
|
);
|
|
254
304
|
|
|
255
|
-
const
|
|
305
|
+
const PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH = 'vol0:/loaders/bootloader/boot_resource.okpkg';
|
|
306
|
+
const PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_STAGING_PATH = `${PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH}.staging`;
|
|
307
|
+
|
|
308
|
+
const isProtocolV2BootResourcePackagePath = (devicePath: string) =>
|
|
309
|
+
typeof devicePath === 'string' &&
|
|
310
|
+
devicePath.replace(/^vol0:(?!\/)/i, 'vol0:/').toLowerCase() ===
|
|
311
|
+
PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH;
|
|
312
|
+
|
|
313
|
+
const resolveProtocolV2ResourceWritePath = (devicePath: string) =>
|
|
314
|
+
isProtocolV2BootResourcePackagePath(devicePath)
|
|
315
|
+
? PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_STAGING_PATH
|
|
316
|
+
: devicePath;
|
|
317
|
+
|
|
318
|
+
const PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID = new Map<
|
|
319
|
+
number,
|
|
320
|
+
Exclude<FirmwareUpdateV4Target, 'boot_resources'>
|
|
321
|
+
>([
|
|
256
322
|
[ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_BOOTLOADER, 'boot'],
|
|
257
323
|
[ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P1, 'app_v1'],
|
|
258
324
|
[ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P2, 'app_v2'],
|
|
@@ -263,6 +329,19 @@ const PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID = new Map<number, FirmwareUpdateV4T
|
|
|
263
329
|
[ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE04, 'se04'],
|
|
264
330
|
]);
|
|
265
331
|
|
|
332
|
+
const PROTOCOL_V2_INSTALL_TARGET_BY_UPDATE_TARGET = new Map<
|
|
333
|
+
Exclude<FirmwareUpdateV4Target, 'resource'>,
|
|
334
|
+
ProtocolV2RemoteComponentTarget
|
|
335
|
+
>(
|
|
336
|
+
Object.values(PROTOCOL_V2_REMOTE_COMPONENT_TARGETS).map(target => [
|
|
337
|
+
PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(target.targetId) as Exclude<
|
|
338
|
+
FirmwareUpdateV4Target,
|
|
339
|
+
'resource'
|
|
340
|
+
>,
|
|
341
|
+
target,
|
|
342
|
+
])
|
|
343
|
+
);
|
|
344
|
+
|
|
266
345
|
const PROTOCOL_V2_ROMLOADER_UNSUPPORTED_MESSAGE =
|
|
267
346
|
'FW_MGMT_TARGET_ROMLOADER is not accepted by the current Pro2 bootloader update request. Flash romloader with the loader-specific flow instead of firmwareUpdateV4.';
|
|
268
347
|
|
|
@@ -486,6 +565,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
486
565
|
|
|
487
566
|
private protocolV2ExpectedPath?: string;
|
|
488
567
|
|
|
568
|
+
private protocolV2HasExplicitTargetSelection = false;
|
|
569
|
+
|
|
489
570
|
getSupportedProtocols() {
|
|
490
571
|
return ['V2'] as const;
|
|
491
572
|
}
|
|
@@ -494,6 +575,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
494
575
|
|
|
495
576
|
private protocolV2ExecutionInLoader = false;
|
|
496
577
|
|
|
578
|
+
private protocolV2BootResourceStagingSafe = false;
|
|
579
|
+
|
|
497
580
|
private protocolV2CompletedTargetVersions = new Map<number, number>();
|
|
498
581
|
|
|
499
582
|
private protocolV2LatestFinalFeatures?: Features;
|
|
@@ -509,6 +592,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
509
592
|
|
|
510
593
|
const { payload } = this;
|
|
511
594
|
|
|
595
|
+
this.protocolV2HasExplicitTargetSelection = payload.targetsToUpdate !== undefined;
|
|
596
|
+
|
|
512
597
|
if (typeof payload.retryCount !== 'number') {
|
|
513
598
|
payload.retryCount = PROTOCOL_V2_CONNECT_RETRY_COUNT;
|
|
514
599
|
}
|
|
@@ -534,11 +619,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
534
619
|
{ name: 'se02Binary', type: 'buffer' },
|
|
535
620
|
{ name: 'se03Binary', type: 'buffer' },
|
|
536
621
|
{ name: 'se04Binary', type: 'buffer' },
|
|
622
|
+
{ name: 'resourceArchiveBinary', type: 'buffer' },
|
|
537
623
|
{ name: 'firmwareType', type: 'string' },
|
|
538
624
|
{ name: 'targetsToUpdate', type: 'array', allowEmpty: true },
|
|
539
625
|
{ name: 'platform', type: 'string' },
|
|
540
626
|
{ name: 'expectedDeviceId', type: 'string' },
|
|
541
|
-
{ name: 'resourceFiles', type: 'array', allowEmpty: true },
|
|
542
627
|
]);
|
|
543
628
|
if (
|
|
544
629
|
payload.expectedDeviceId !== undefined &&
|
|
@@ -549,50 +634,101 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
549
634
|
'Protocol V2 expected device identity is invalid'
|
|
550
635
|
);
|
|
551
636
|
}
|
|
552
|
-
const
|
|
553
|
-
payload.preparedPlan
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
637
|
+
const preparedPlan = payload.preparedPlan
|
|
638
|
+
? validateFirmwareUpdatePreparedPlan(payload.preparedPlan)
|
|
639
|
+
: undefined;
|
|
640
|
+
const hasLocalArtifacts = [
|
|
641
|
+
payload.bootloaderBinary,
|
|
642
|
+
payload.romloaderBinary,
|
|
643
|
+
payload.applicationP1Binary,
|
|
644
|
+
payload.applicationP2Binary,
|
|
645
|
+
payload.coprocessorBinary,
|
|
646
|
+
payload.se01Binary,
|
|
647
|
+
payload.se02Binary,
|
|
648
|
+
payload.se03Binary,
|
|
649
|
+
payload.se04Binary,
|
|
650
|
+
payload.resourceArchiveBinary,
|
|
651
|
+
].some(Boolean);
|
|
652
|
+
if (preparedPlan && hasLocalArtifacts) {
|
|
653
|
+
throw ERRORS.TypedError(
|
|
654
|
+
HardwareErrorCode.CallMethodInvalidParameter,
|
|
655
|
+
'Prepared firmware plans cannot be combined with legacy or local firmware inputs'
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
let { artifactReader } = payload;
|
|
659
|
+
if (preparedPlan) {
|
|
660
|
+
artifactReader = resolveFirmwareUpdateHostBinding(
|
|
661
|
+
payload.hostBindingGeneration,
|
|
662
|
+
preparedPlan.preparedPlanDigest
|
|
663
|
+
).artifactReader;
|
|
664
|
+
} else if (payload.hostBindingGeneration !== undefined) {
|
|
665
|
+
artifactReader = resolveFirmwareUpdateHostBinding(
|
|
666
|
+
payload.hostBindingGeneration
|
|
667
|
+
).artifactReader;
|
|
668
|
+
}
|
|
669
|
+
if (preparedPlan) {
|
|
557
670
|
assertFirmwareUpdatePreparedPlanBinding({
|
|
558
|
-
preparedPlan
|
|
671
|
+
preparedPlan,
|
|
559
672
|
executor: 'v4',
|
|
560
673
|
platform: payload.platform,
|
|
561
|
-
scopeTargets: [
|
|
562
|
-
|
|
563
|
-
'app_v1',
|
|
564
|
-
'app_v2',
|
|
565
|
-
'coprocessor',
|
|
566
|
-
'resource',
|
|
567
|
-
'se01',
|
|
568
|
-
'se02',
|
|
569
|
-
'se03',
|
|
570
|
-
'se04',
|
|
571
|
-
],
|
|
572
|
-
bindings: [
|
|
573
|
-
...Object.entries(payload.componentArtifacts ?? {}).flatMap(([target, artifact]) =>
|
|
574
|
-
artifact
|
|
575
|
-
? [
|
|
576
|
-
{
|
|
577
|
-
target: target as Exclude<FirmwareUpdateV4Target, 'resource'>,
|
|
578
|
-
artifact,
|
|
579
|
-
},
|
|
580
|
-
]
|
|
581
|
-
: []
|
|
582
|
-
),
|
|
583
|
-
...(payload.resourceBundleArtifacts ?? []).map(
|
|
584
|
-
(entry: { name: string; artifact: FirmwareArtifactReference }) => ({
|
|
585
|
-
target: 'resource' as const,
|
|
586
|
-
logicalName: entry.name,
|
|
587
|
-
artifact: entry.artifact,
|
|
588
|
-
})
|
|
589
|
-
),
|
|
590
|
-
],
|
|
674
|
+
scopeTargets: [],
|
|
675
|
+
bindings: [],
|
|
591
676
|
});
|
|
677
|
+
if (payload.componentArtifacts) {
|
|
678
|
+
const componentBindings: Array<{
|
|
679
|
+
target: Exclude<FirmwareUpdateV4Target, 'resource'>;
|
|
680
|
+
artifact: FirmwareArtifactReference;
|
|
681
|
+
}> = Object.entries(payload.componentArtifacts).flatMap(([target, artifact]) =>
|
|
682
|
+
artifact
|
|
683
|
+
? [
|
|
684
|
+
{
|
|
685
|
+
target: target as Exclude<FirmwareUpdateV4Target, 'resource'>,
|
|
686
|
+
artifact: artifact as FirmwareArtifactReference,
|
|
687
|
+
},
|
|
688
|
+
]
|
|
689
|
+
: []
|
|
690
|
+
);
|
|
691
|
+
assertFirmwareUpdatePreparedPlanBinding({
|
|
692
|
+
preparedPlan,
|
|
693
|
+
executor: 'v4',
|
|
694
|
+
platform: payload.platform,
|
|
695
|
+
scopeTargets: componentBindings.map(binding => binding.target),
|
|
696
|
+
bindings: componentBindings,
|
|
697
|
+
});
|
|
698
|
+
}
|
|
592
699
|
}
|
|
593
700
|
|
|
701
|
+
const preparedExpectedTargetVersions = preparedPlan?.artifacts.reduce<
|
|
702
|
+
NonNullable<FirmwareUpdateV4Params['expectedTargetVersions']>
|
|
703
|
+
>((result, artifact) => {
|
|
704
|
+
if (
|
|
705
|
+
artifact.role === 'component' &&
|
|
706
|
+
artifact.target !== 'resource' &&
|
|
707
|
+
artifact.targetVersion &&
|
|
708
|
+
PROTOCOL_V2_INSTALL_TARGET_BY_UPDATE_TARGET.has(
|
|
709
|
+
artifact.target as Exclude<FirmwareUpdateV4Target, 'resource'>
|
|
710
|
+
)
|
|
711
|
+
) {
|
|
712
|
+
result[artifact.target as FirmwareUpdateV4Target] = artifact.targetVersion;
|
|
713
|
+
}
|
|
714
|
+
return result;
|
|
715
|
+
}, {});
|
|
716
|
+
const localTargetsToUpdate = payload.targetsToUpdate?.map((target: FirmwareUpdateV4Target) =>
|
|
717
|
+
target === 'boot_resources' ? 'resource' : target
|
|
718
|
+
);
|
|
719
|
+
if (
|
|
720
|
+
payload.resourceArchiveBinary &&
|
|
721
|
+
localTargetsToUpdate &&
|
|
722
|
+
!localTargetsToUpdate.includes('resource')
|
|
723
|
+
) {
|
|
724
|
+
localTargetsToUpdate.push('resource');
|
|
725
|
+
}
|
|
726
|
+
// 本地 ZIP 本身就是明确的资源升级请求,不要求调用方重复声明 target。
|
|
727
|
+
const resolvedLocalTargetsToUpdate =
|
|
728
|
+
localTargetsToUpdate ?? (payload.resourceArchiveBinary ? ['resource'] : undefined);
|
|
729
|
+
|
|
594
730
|
this.params = {
|
|
595
|
-
preparedPlan
|
|
731
|
+
preparedPlan,
|
|
596
732
|
chunkSize: payload.chunkSize,
|
|
597
733
|
forcedUpdateRes: payload.forcedUpdateRes,
|
|
598
734
|
bootloaderBinary: payload.bootloaderBinary,
|
|
@@ -604,15 +740,18 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
604
740
|
se02Binary: payload.se02Binary,
|
|
605
741
|
se03Binary: payload.se03Binary,
|
|
606
742
|
se04Binary: payload.se04Binary,
|
|
607
|
-
|
|
608
|
-
firmwareType: payload.firmwareType,
|
|
609
|
-
targetsToUpdate:
|
|
610
|
-
|
|
743
|
+
resourceArchiveBinary: payload.resourceArchiveBinary,
|
|
744
|
+
firmwareType: preparedPlan?.firmwareType ?? payload.firmwareType,
|
|
745
|
+
targetsToUpdate: preparedPlan
|
|
746
|
+
? ([...preparedPlan.targetsToUpdate] as FirmwareUpdateV4Target[])
|
|
747
|
+
: resolvedLocalTargetsToUpdate,
|
|
748
|
+
expectedTargetVersions: preparedPlan
|
|
749
|
+
? preparedExpectedTargetVersions
|
|
750
|
+
: payload.expectedTargetVersions,
|
|
611
751
|
platform: payload.platform,
|
|
612
752
|
expectedDeviceId: payload.expectedDeviceId,
|
|
613
|
-
artifactReader
|
|
614
|
-
componentArtifacts: payload.componentArtifacts,
|
|
615
|
-
resourceBundleArtifacts: payload.resourceBundleArtifacts,
|
|
753
|
+
artifactReader,
|
|
754
|
+
componentArtifacts: preparedPlan ? undefined : payload.componentArtifacts,
|
|
616
755
|
};
|
|
617
756
|
}
|
|
618
757
|
|
|
@@ -653,36 +792,65 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
653
792
|
currentDeviceType === EDeviceType.Pro2 || currentDeviceType === EDeviceType.Neo
|
|
654
793
|
? currentDeviceType
|
|
655
794
|
: getDeviceType(deviceFeatures);
|
|
656
|
-
assertProtocolV2FirmwareTargetsSupported(
|
|
795
|
+
assertProtocolV2FirmwareTargetsSupported(
|
|
796
|
+
capabilityDeviceType,
|
|
797
|
+
this.params,
|
|
798
|
+
this.protocolV2HasExplicitTargetSelection
|
|
799
|
+
);
|
|
657
800
|
const deviceFirmwareType = getFirmwareType(deviceFeatures);
|
|
658
801
|
const firmwareType = this.params.firmwareType ?? deviceFirmwareType;
|
|
659
802
|
this.validateExpectedTargetVersions();
|
|
660
803
|
|
|
661
804
|
if (
|
|
662
|
-
|
|
663
|
-
this.params.
|
|
805
|
+
!this.params.preparedPlan &&
|
|
806
|
+
this.params.resourceArchiveBinary &&
|
|
807
|
+
this.params.targetsToUpdate?.includes('resource')
|
|
664
808
|
) {
|
|
809
|
+
const localMemoryHost = await this.prepareProtocolV2LocalMemoryHost({
|
|
810
|
+
features: deviceFeatures,
|
|
811
|
+
firmwareType,
|
|
812
|
+
});
|
|
813
|
+
try {
|
|
814
|
+
return await this.runProtocolV2PreparedArtifacts(deviceFeatures, firmwareType);
|
|
815
|
+
} finally {
|
|
816
|
+
localMemoryHost.release();
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const hasPreparedComponentArtifacts = Object.values(this.params.componentArtifacts ?? {}).some(
|
|
821
|
+
Boolean
|
|
822
|
+
);
|
|
823
|
+
if (this.params.preparedPlan || hasPreparedComponentArtifacts) {
|
|
665
824
|
return this.runProtocolV2PreparedArtifacts(deviceFeatures, firmwareType);
|
|
666
825
|
}
|
|
667
|
-
const
|
|
668
|
-
const wantsStableResources = !!this.params.targetsToUpdate?.includes('resource');
|
|
669
|
-
const wantsBootResources = !!this.params.targetsToUpdate?.includes('boot_resources');
|
|
670
|
-
const needsRemoteResources = !hasExplicitResourceFiles && wantsStableResources;
|
|
671
|
-
const needsRemoteBootResources =
|
|
672
|
-
!hasExplicitResourceFiles && (wantsBootResources || wantsStableResources);
|
|
826
|
+
const wantsResources = !!this.params.targetsToUpdate?.includes('resource');
|
|
673
827
|
|
|
674
828
|
let fwBinaryMap: ProtocolV2TargetBinary[] = [];
|
|
675
829
|
let bootloaderBinary: ArrayBuffer | null = null;
|
|
676
830
|
let installItems: ProtocolV2InstallItem[] | undefined;
|
|
677
|
-
let resourceBundles: ProtocolV2ResourceBundleBinary[] | undefined;
|
|
678
831
|
try {
|
|
679
832
|
this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
|
|
680
|
-
resourceBundles = this.prepareExplicitProtocolV2ResourceFiles();
|
|
681
833
|
fwBinaryMap = this.collectExplicitTargetBinaries();
|
|
682
834
|
bootloaderBinary = this.prepareBootloaderBinary();
|
|
683
|
-
const
|
|
835
|
+
const explicitInstallItems = this.buildProtocolV2InstallItems({
|
|
836
|
+
bootloaderBinary,
|
|
837
|
+
fwBinaryMap,
|
|
838
|
+
});
|
|
839
|
+
const missingFirmwareTargets = this.getMissingProtocolV2FirmwareTargets(explicitInstallItems);
|
|
840
|
+
const needsRemoteFirmware = this.params.targetsToUpdate?.length
|
|
841
|
+
? missingFirmwareTargets.length > 0
|
|
842
|
+
: explicitInstallItems.length === 0;
|
|
843
|
+
if (wantsResources) {
|
|
844
|
+
throw ERRORS.TypedError(
|
|
845
|
+
HardwareErrorCode.RuntimeError,
|
|
846
|
+
'Protocol V2 resource archive must be provided through a local or external PreparedPlan',
|
|
847
|
+
{
|
|
848
|
+
firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
|
|
849
|
+
}
|
|
850
|
+
);
|
|
851
|
+
}
|
|
684
852
|
if (
|
|
685
|
-
|
|
853
|
+
needsRemoteFirmware &&
|
|
686
854
|
(this.params.artifactReader ||
|
|
687
855
|
DataManager.getSettings('firmwareManifestMode') === 'external-only')
|
|
688
856
|
) {
|
|
@@ -694,74 +862,58 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
694
862
|
}
|
|
695
863
|
);
|
|
696
864
|
}
|
|
697
|
-
if (needsRemoteFirmware
|
|
865
|
+
if (needsRemoteFirmware) {
|
|
698
866
|
// Remote updates must use a freshly fetched config before any reboot or file write.
|
|
699
|
-
await DataManager.forceReloadData(
|
|
700
|
-
requireResources: needsRemoteResources || needsRemoteBootResources,
|
|
701
|
-
});
|
|
867
|
+
await DataManager.forceReloadData();
|
|
702
868
|
}
|
|
703
869
|
if (needsRemoteFirmware) {
|
|
704
870
|
const remoteBinaries = await this.prepareRemoteProtocolV2Binaries(
|
|
705
871
|
firmwareType,
|
|
706
|
-
deviceFeatures
|
|
872
|
+
deviceFeatures,
|
|
873
|
+
explicitInstallItems
|
|
707
874
|
);
|
|
708
875
|
bootloaderBinary = remoteBinaries.bootloaderBinary;
|
|
709
876
|
fwBinaryMap = remoteBinaries.fwBinaryMap;
|
|
710
877
|
installItems = remoteBinaries.installItems;
|
|
878
|
+
} else {
|
|
879
|
+
const selectedInstallItems = this.filterProtocolV2LocalInstallItems(explicitInstallItems);
|
|
880
|
+
bootloaderBinary =
|
|
881
|
+
selectedInstallItems.find(item => item.kind === 'bootloader')?.binary ?? null;
|
|
882
|
+
fwBinaryMap = selectedInstallItems
|
|
883
|
+
.filter(item => item.kind === 'firmware')
|
|
884
|
+
.map(item => ({
|
|
885
|
+
fileName: item.fileName,
|
|
886
|
+
binary: item.binary,
|
|
887
|
+
targetId: item.targetId,
|
|
888
|
+
}));
|
|
711
889
|
}
|
|
712
|
-
|
|
713
|
-
if (bootResourceFiles?.length) {
|
|
714
|
-
resourceBundles = this.mergeProtocolV2ResourceBundles(resourceBundles, bootResourceFiles);
|
|
715
|
-
}
|
|
716
|
-
if (!needsRemoteResources) {
|
|
717
|
-
this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
|
|
718
|
-
}
|
|
890
|
+
this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
|
|
719
891
|
} catch (err) {
|
|
892
|
+
if (
|
|
893
|
+
typeof err === 'object' &&
|
|
894
|
+
err !== null &&
|
|
895
|
+
'params' in err &&
|
|
896
|
+
(err as HardwareError).params?.firmwareUpdateCode === 'FirmwareArtifactsNotPrepared'
|
|
897
|
+
) {
|
|
898
|
+
throw err;
|
|
899
|
+
}
|
|
720
900
|
if (err instanceof HardwareError && err.errorCode === HardwareErrorCode.NetworkError) {
|
|
721
901
|
throw err;
|
|
722
902
|
}
|
|
723
903
|
throw normalizeFirmwarePreparationError(err);
|
|
724
904
|
}
|
|
725
905
|
|
|
726
|
-
if (
|
|
727
|
-
!bootloaderBinary &&
|
|
728
|
-
fwBinaryMap.length === 0 &&
|
|
729
|
-
!installItems?.length &&
|
|
730
|
-
!resourceBundles?.length &&
|
|
731
|
-
!needsRemoteResources
|
|
732
|
-
) {
|
|
906
|
+
if (!bootloaderBinary && fwBinaryMap.length === 0 && !installItems?.length) {
|
|
733
907
|
throw ERRORS.TypedError(
|
|
734
908
|
HardwareErrorCode.FirmwareUpdateDownloadFailed,
|
|
735
909
|
'No firmware to update'
|
|
736
910
|
);
|
|
737
911
|
}
|
|
738
912
|
|
|
739
|
-
if (needsRemoteResources) {
|
|
740
|
-
const enteredBootloader = await this.enterProtocolV2BootloaderMode();
|
|
741
|
-
try {
|
|
742
|
-
const stableResources = await this.prepareProtocolV2ResourceBundles();
|
|
743
|
-
resourceBundles = this.mergeProtocolV2ResourceBundles(resourceBundles, stableResources);
|
|
744
|
-
this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
|
|
745
|
-
} catch (err) {
|
|
746
|
-
if (enteredBootloader) {
|
|
747
|
-
try {
|
|
748
|
-
await this.exitProtocolV2BootloaderToNormal();
|
|
749
|
-
} catch (restoreError) {
|
|
750
|
-
Log.warn(
|
|
751
|
-
'[FirmwareUpdateV4] failed to restore App mode after resource preparation error:',
|
|
752
|
-
restoreError
|
|
753
|
-
);
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
throw normalizeFirmwarePreparationError(err);
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
|
|
760
913
|
return this.executeProtocolV2Update({
|
|
761
914
|
fwBinaryMap,
|
|
762
915
|
bootloaderBinary,
|
|
763
916
|
...(installItems ? { installItems } : undefined),
|
|
764
|
-
...(resourceBundles?.length ? { resourceBundles } : undefined),
|
|
765
917
|
});
|
|
766
918
|
}
|
|
767
919
|
|
|
@@ -804,6 +956,54 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
804
956
|
firmwareType: EFirmwareType,
|
|
805
957
|
features: Features
|
|
806
958
|
): Promise<ProtocolV2InstallSource[]> {
|
|
959
|
+
if (this.params.preparedPlan) {
|
|
960
|
+
const requestedTargets = new Set(
|
|
961
|
+
this.params.preparedPlan.targetsToUpdate.filter(
|
|
962
|
+
(target): target is Exclude<FirmwareUpdateV4Target, 'resource'> => target !== 'resource'
|
|
963
|
+
)
|
|
964
|
+
);
|
|
965
|
+
const installSources: ProtocolV2InstallSource[] = [];
|
|
966
|
+
const preparedTargets = new Set<Exclude<FirmwareUpdateV4Target, 'resource'>>();
|
|
967
|
+
for (const artifact of this.params.preparedPlan.artifacts) {
|
|
968
|
+
if (artifact.target !== 'resource') {
|
|
969
|
+
const target = artifact.target as Exclude<FirmwareUpdateV4Target, 'resource'>;
|
|
970
|
+
const installTarget = PROTOCOL_V2_INSTALL_TARGET_BY_UPDATE_TARGET.get(target);
|
|
971
|
+
if (
|
|
972
|
+
!requestedTargets.has(target) ||
|
|
973
|
+
artifact.role !== 'component' ||
|
|
974
|
+
artifact.container !== 'raw' ||
|
|
975
|
+
!installTarget ||
|
|
976
|
+
preparedTargets.has(target)
|
|
977
|
+
) {
|
|
978
|
+
throw ERRORS.TypedError(
|
|
979
|
+
HardwareErrorCode.RuntimeError,
|
|
980
|
+
`Protocol V2 prepared component artifact is invalid: ${artifact.artifactId}`,
|
|
981
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
installSources.push({
|
|
985
|
+
...installTarget,
|
|
986
|
+
source: await this.openProtocolV2PreparedSource(artifact.artifact),
|
|
987
|
+
});
|
|
988
|
+
preparedTargets.add(target);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
const missingTarget = Array.from(requestedTargets).find(
|
|
992
|
+
target => !preparedTargets.has(target)
|
|
993
|
+
);
|
|
994
|
+
if (missingTarget) {
|
|
995
|
+
throw ERRORS.TypedError(
|
|
996
|
+
HardwareErrorCode.RuntimeError,
|
|
997
|
+
`Protocol V2 ${missingTarget} artifact is not prepared`,
|
|
998
|
+
{
|
|
999
|
+
firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
|
|
1000
|
+
artifactName: missingTarget,
|
|
1001
|
+
}
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
return installSources;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
807
1007
|
const release = DataManager.getFirmwareLatestRelease(features, firmwareType);
|
|
808
1008
|
if (!release) {
|
|
809
1009
|
throw ERRORS.TypedError(
|
|
@@ -853,54 +1053,441 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
853
1053
|
return installSources;
|
|
854
1054
|
}
|
|
855
1055
|
|
|
856
|
-
private async
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1056
|
+
private async prepareProtocolV2LocalMemoryHost({
|
|
1057
|
+
features,
|
|
1058
|
+
firmwareType,
|
|
1059
|
+
}: {
|
|
1060
|
+
features: Features;
|
|
1061
|
+
firmwareType: EFirmwareType;
|
|
1062
|
+
}): Promise<FirmwareUpdateV4MemoryHost> {
|
|
1063
|
+
const availableInstallItems = this.buildProtocolV2InstallItems({
|
|
1064
|
+
bootloaderBinary: this.prepareBootloaderBinary(),
|
|
1065
|
+
fwBinaryMap: this.collectExplicitTargetBinaries(),
|
|
1066
|
+
});
|
|
1067
|
+
const requestedComponentTargets = new Set(
|
|
1068
|
+
(this.params.targetsToUpdate ?? []).filter(
|
|
1069
|
+
(target): target is Exclude<FirmwareUpdateV4Target, 'resource' | 'boot_resources'> =>
|
|
1070
|
+
target !== 'resource' && target !== 'boot_resources'
|
|
1071
|
+
)
|
|
1072
|
+
);
|
|
1073
|
+
const localComponentTargets = new Set(
|
|
1074
|
+
availableInstallItems.flatMap(item => {
|
|
1075
|
+
const target = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(item.targetId);
|
|
1076
|
+
return target ? [target] : [];
|
|
1077
|
+
})
|
|
1078
|
+
);
|
|
1079
|
+
const missingTarget = Array.from(requestedComponentTargets).find(
|
|
1080
|
+
target => !localComponentTargets.has(target)
|
|
1081
|
+
);
|
|
1082
|
+
if (missingTarget) {
|
|
1083
|
+
throw ERRORS.TypedError(
|
|
1084
|
+
HardwareErrorCode.RuntimeError,
|
|
1085
|
+
`Protocol V2 local update has no binary for requested target ${missingTarget}`,
|
|
1086
|
+
{
|
|
1087
|
+
firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
|
|
1088
|
+
artifactName: missingTarget,
|
|
1089
|
+
}
|
|
1090
|
+
);
|
|
865
1091
|
}
|
|
866
|
-
const
|
|
867
|
-
|
|
868
|
-
const
|
|
869
|
-
|
|
1092
|
+
const installItems = this.filterProtocolV2LocalInstallItems(availableInstallItems);
|
|
1093
|
+
|
|
1094
|
+
const planArtifacts: Parameters<typeof buildProtocolV2LocalFirmwareUpdatePlan>[0]['artifacts'] =
|
|
1095
|
+
[];
|
|
1096
|
+
const memoryArtifacts: FirmwareMemoryArtifact[] = [];
|
|
1097
|
+
for (const item of installItems) {
|
|
1098
|
+
const target = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(item.targetId);
|
|
1099
|
+
if (!target || item.binary.byteLength <= 0) {
|
|
1100
|
+
throw ERRORS.TypedError(
|
|
1101
|
+
HardwareErrorCode.RuntimeError,
|
|
1102
|
+
`Protocol V2 local firmware artifact is invalid: ${item.fileName}`,
|
|
1103
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
const artifactId = `component:${target}`;
|
|
1107
|
+
planArtifacts.push({
|
|
1108
|
+
artifactId,
|
|
1109
|
+
target,
|
|
1110
|
+
container: 'raw',
|
|
1111
|
+
logicalName: item.fileName,
|
|
1112
|
+
expectedSize: item.binary.byteLength,
|
|
1113
|
+
expectedSha256: bytesToHex(sha256(new Uint8Array(item.binary))),
|
|
1114
|
+
...(this.params.expectedTargetVersions?.[target]
|
|
1115
|
+
? { targetVersion: this.params.expectedTargetVersions[target] }
|
|
1116
|
+
: {}),
|
|
1117
|
+
});
|
|
1118
|
+
memoryArtifacts.push({ artifactId, binary: item.binary });
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
const resourceArchive = await this.prepareProtocolV2LocalResourceArchive(
|
|
1122
|
+
this.params.resourceArchiveBinary as ArrayBuffer
|
|
870
1123
|
);
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1124
|
+
const resourceArtifactId = 'resource:archive';
|
|
1125
|
+
planArtifacts.push({
|
|
1126
|
+
artifactId: resourceArtifactId,
|
|
1127
|
+
target: 'resource',
|
|
1128
|
+
container: 'zip',
|
|
1129
|
+
logicalName: 'protocol-v2-local-resource-archive',
|
|
1130
|
+
expectedSize: resourceArchive.binary.byteLength,
|
|
1131
|
+
expectedSha256: bytesToHex(sha256(new Uint8Array(resourceArchive.binary))),
|
|
1132
|
+
});
|
|
1133
|
+
memoryArtifacts.push({
|
|
1134
|
+
artifactId: resourceArtifactId,
|
|
1135
|
+
binary: resourceArchive.binary,
|
|
1136
|
+
materializedEntries: resourceArchive.materializedEntries,
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
const plan = buildProtocolV2LocalFirmwareUpdatePlan({
|
|
1140
|
+
features,
|
|
1141
|
+
firmwareType,
|
|
1142
|
+
platform: this.params.platform,
|
|
1143
|
+
artifacts: planArtifacts,
|
|
1144
|
+
});
|
|
1145
|
+
let memoryHost: FirmwareUpdateV4MemoryHost | undefined;
|
|
1146
|
+
try {
|
|
1147
|
+
memoryHost = prepareFirmwareUpdateV4MemoryHost({
|
|
1148
|
+
sdk: {
|
|
1149
|
+
prepareFirmwareUpdatePlan,
|
|
1150
|
+
registerFirmwareUpdateHostBinding,
|
|
1151
|
+
unregisterFirmwareUpdateHostBinding,
|
|
1152
|
+
},
|
|
1153
|
+
plan,
|
|
1154
|
+
artifacts: memoryArtifacts,
|
|
1155
|
+
});
|
|
1156
|
+
const preparedPlan = validateFirmwareUpdatePreparedPlan(memoryHost.preparedPlan);
|
|
1157
|
+
assertFirmwareUpdatePreparedPlanBinding({
|
|
1158
|
+
preparedPlan,
|
|
1159
|
+
executor: 'v4',
|
|
1160
|
+
platform: this.params.platform,
|
|
1161
|
+
scopeTargets: [],
|
|
1162
|
+
bindings: [],
|
|
1163
|
+
});
|
|
1164
|
+
assertFirmwareUpdatePreparedPlanDeviceIdentity({
|
|
1165
|
+
preparedPlan,
|
|
1166
|
+
deviceIdentity: this.protocolV2ExpectedSerialNumber,
|
|
1167
|
+
deviceModel: this.getProtocolV2PreparedPlanDeviceModel(features),
|
|
1168
|
+
});
|
|
1169
|
+
const hostBinding = resolveFirmwareUpdateHostBinding(
|
|
1170
|
+
memoryHost.hostBindingGeneration,
|
|
1171
|
+
preparedPlan.preparedPlanDigest
|
|
1172
|
+
);
|
|
1173
|
+
this.params.preparedPlan = preparedPlan;
|
|
1174
|
+
this.params.targetsToUpdate = [...preparedPlan.targetsToUpdate] as FirmwareUpdateV4Target[];
|
|
1175
|
+
this.params.artifactReader = hostBinding.artifactReader;
|
|
1176
|
+
this.params.componentArtifacts = undefined;
|
|
1177
|
+
return memoryHost;
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
memoryHost?.release();
|
|
1180
|
+
throw error;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
private async prepareProtocolV2LocalResourceArchive(
|
|
1185
|
+
binary: ArrayBuffer
|
|
1186
|
+
): Promise<ProtocolV2LocalResourceArchive> {
|
|
1187
|
+
if (binary.byteLength <= 0 || binary.byteLength > PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES) {
|
|
1188
|
+
throw ERRORS.TypedError(
|
|
1189
|
+
HardwareErrorCode.RuntimeError,
|
|
1190
|
+
'Protocol V2 local resource ZIP archive size is invalid',
|
|
1191
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
let zip: JSZip;
|
|
1195
|
+
try {
|
|
1196
|
+
zip = await JSZip.loadAsync(binary);
|
|
1197
|
+
} catch {
|
|
1198
|
+
throw ERRORS.TypedError(
|
|
1199
|
+
HardwareErrorCode.RuntimeError,
|
|
1200
|
+
'Protocol V2 resource ZIP cannot be parsed',
|
|
1201
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
const zipEntries = Object.values(zip.files);
|
|
1205
|
+
if (
|
|
1206
|
+
zipEntries.some(entry => entry.unsafeOriginalName && entry.unsafeOriginalName !== entry.name)
|
|
1207
|
+
) {
|
|
1208
|
+
throw ERRORS.TypedError(
|
|
1209
|
+
HardwareErrorCode.RuntimeError,
|
|
1210
|
+
'Protocol V2 local resource ZIP contains an unsafe entry path',
|
|
1211
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
const entries = zipEntries.filter(entry => !entry.dir);
|
|
1215
|
+
if (entries.length === 0 || entries.length > PROTOCOL_V2_RESOURCE_FILE_MAX_COUNT + 1) {
|
|
1216
|
+
throw ERRORS.TypedError(
|
|
1217
|
+
HardwareErrorCode.RuntimeError,
|
|
1218
|
+
'Protocol V2 local resource ZIP entry set is invalid',
|
|
1219
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
let declaredUncompressedSize = 0;
|
|
1223
|
+
let declaredCompressedSize = 0;
|
|
1224
|
+
for (const entry of entries) {
|
|
1225
|
+
const sizes = getProtocolV2ZipEntrySizes(entry);
|
|
1226
|
+
declaredCompressedSize += sizes.compressedSize;
|
|
1227
|
+
declaredUncompressedSize += sizes.uncompressedSize;
|
|
1228
|
+
const entryLimit =
|
|
1229
|
+
entry.name === 'manifest.json'
|
|
1230
|
+
? PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES
|
|
1231
|
+
: PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES;
|
|
1232
|
+
if (
|
|
1233
|
+
sizes.uncompressedSize > entryLimit ||
|
|
1234
|
+
declaredCompressedSize > binary.byteLength ||
|
|
1235
|
+
declaredUncompressedSize >
|
|
1236
|
+
PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES + PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES
|
|
1237
|
+
) {
|
|
875
1238
|
throw ERRORS.TypedError(
|
|
876
1239
|
HardwareErrorCode.RuntimeError,
|
|
877
|
-
|
|
878
|
-
{
|
|
879
|
-
firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
|
|
880
|
-
artifactName: descriptor.name,
|
|
881
|
-
}
|
|
1240
|
+
'Protocol V2 local resource ZIP declared size exceeds the allowed limit',
|
|
1241
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
882
1242
|
);
|
|
883
1243
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1244
|
+
}
|
|
1245
|
+
const manifestEntry = zip.file('manifest.json');
|
|
1246
|
+
if (!manifestEntry) {
|
|
1247
|
+
throw ERRORS.TypedError(
|
|
1248
|
+
HardwareErrorCode.RuntimeError,
|
|
1249
|
+
'Protocol V2 local resource ZIP has no manifest.json',
|
|
1250
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
887
1251
|
);
|
|
888
|
-
sources.push({
|
|
889
|
-
name: descriptor.name,
|
|
890
|
-
source: await this.openProtocolV2PreparedSource(artifact),
|
|
891
|
-
devicePath,
|
|
892
|
-
version: descriptor.version,
|
|
893
|
-
payloadHash: descriptor.payloadHash,
|
|
894
|
-
headerHash: descriptor.headerHash,
|
|
895
|
-
});
|
|
896
|
-
artifactByName.delete(descriptor.name);
|
|
897
1252
|
}
|
|
898
|
-
|
|
1253
|
+
const manifestBinary = await manifestEntry.async('arraybuffer');
|
|
1254
|
+
if (
|
|
1255
|
+
manifestBinary.byteLength <= 0 ||
|
|
1256
|
+
manifestBinary.byteLength > PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES
|
|
1257
|
+
) {
|
|
899
1258
|
throw ERRORS.TypedError(
|
|
900
1259
|
HardwareErrorCode.RuntimeError,
|
|
901
|
-
|
|
1260
|
+
'Protocol V2 local resource manifest size is invalid',
|
|
1261
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
902
1262
|
);
|
|
903
1263
|
}
|
|
1264
|
+
|
|
1265
|
+
let manifestValue: unknown;
|
|
1266
|
+
try {
|
|
1267
|
+
manifestValue = JSON.parse(new TextDecoder().decode(manifestBinary));
|
|
1268
|
+
} catch (error) {
|
|
1269
|
+
throw ERRORS.TypedError(
|
|
1270
|
+
HardwareErrorCode.RuntimeError,
|
|
1271
|
+
`Protocol V2 local resource manifest is invalid: ${String(error)}`,
|
|
1272
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
const manifest = parseProtocolV2ResourceManifest(manifestValue);
|
|
1276
|
+
const selectedFiles = selectProtocolV2ResourceManifestFiles({
|
|
1277
|
+
manifest,
|
|
1278
|
+
targetsToUpdate: this.params.targetsToUpdate ?? [],
|
|
1279
|
+
});
|
|
1280
|
+
const expectedEntryNames = new Set([
|
|
1281
|
+
'manifest.json',
|
|
1282
|
+
...selectedFiles.map(file => file.archive_path),
|
|
1283
|
+
]);
|
|
1284
|
+
if (
|
|
1285
|
+
entries.length !== expectedEntryNames.size ||
|
|
1286
|
+
entries.some(entry => !expectedEntryNames.has(entry.name))
|
|
1287
|
+
) {
|
|
1288
|
+
throw ERRORS.TypedError(
|
|
1289
|
+
HardwareErrorCode.RuntimeError,
|
|
1290
|
+
'Protocol V2 local resource ZIP contains an unexpected entry',
|
|
1291
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
let totalSize = 0;
|
|
1296
|
+
const materializedEntries: FirmwareMemoryArtifactEntry[] = [
|
|
1297
|
+
{ entryName: 'manifest.json', binary: manifestBinary },
|
|
1298
|
+
];
|
|
1299
|
+
for (const file of selectedFiles) {
|
|
1300
|
+
const entry = zip.file(file.archive_path);
|
|
1301
|
+
if (!entry) {
|
|
1302
|
+
throw ERRORS.TypedError(
|
|
1303
|
+
HardwareErrorCode.RuntimeError,
|
|
1304
|
+
`Protocol V2 local resource ZIP is missing ${file.archive_path}`,
|
|
1305
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
const { uncompressedSize } = getProtocolV2ZipEntrySizes(entry);
|
|
1309
|
+
totalSize += uncompressedSize;
|
|
1310
|
+
if (uncompressedSize !== file.size || totalSize > PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES) {
|
|
1311
|
+
throw ERRORS.TypedError(
|
|
1312
|
+
HardwareErrorCode.RuntimeError,
|
|
1313
|
+
`Protocol V2 local resource file declared size is invalid: ${file.archive_path}`,
|
|
1314
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
const fileBinary = await entry.async('arraybuffer');
|
|
1318
|
+
const digest = bytesToHex(sha256(new Uint8Array(fileBinary)));
|
|
1319
|
+
if (fileBinary.byteLength !== file.size || digest !== file.sha256.toLowerCase()) {
|
|
1320
|
+
throw ERRORS.TypedError(
|
|
1321
|
+
HardwareErrorCode.RuntimeError,
|
|
1322
|
+
`Protocol V2 local resource file does not match manifest: ${file.archive_path}`,
|
|
1323
|
+
{ firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
materializedEntries.push({ entryName: file.archive_path, binary: fileBinary });
|
|
1327
|
+
}
|
|
1328
|
+
return { binary, materializedEntries };
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
private async prepareProtocolV2ResourceSources(): Promise<ProtocolV2ResourceBundleSource[]> {
|
|
1332
|
+
const resourceRequested = this.params.targetsToUpdate?.includes('resource') ?? false;
|
|
1333
|
+
if (!resourceRequested) {
|
|
1334
|
+
return [];
|
|
1335
|
+
}
|
|
1336
|
+
const archiveSources = await this.prepareProtocolV2ResourceArchiveSources();
|
|
1337
|
+
if (archiveSources) {
|
|
1338
|
+
return archiveSources;
|
|
1339
|
+
}
|
|
1340
|
+
throw ERRORS.TypedError(
|
|
1341
|
+
HardwareErrorCode.RuntimeError,
|
|
1342
|
+
'Protocol V2 resource archive is not prepared',
|
|
1343
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
private async prepareProtocolV2ResourceArchiveSources(): Promise<
|
|
1348
|
+
ProtocolV2ResourceBundleSource[] | undefined
|
|
1349
|
+
> {
|
|
1350
|
+
const archiveArtifacts =
|
|
1351
|
+
this.params.preparedPlan?.artifacts.filter(
|
|
1352
|
+
artifact =>
|
|
1353
|
+
artifact.role === 'resourceBundle' &&
|
|
1354
|
+
artifact.target === 'resource' &&
|
|
1355
|
+
artifact.container === 'zip'
|
|
1356
|
+
) ?? [];
|
|
1357
|
+
if (archiveArtifacts.length === 0) {
|
|
1358
|
+
return undefined;
|
|
1359
|
+
}
|
|
1360
|
+
if (archiveArtifacts.length !== 1) {
|
|
1361
|
+
throw ERRORS.TypedError(
|
|
1362
|
+
HardwareErrorCode.RuntimeError,
|
|
1363
|
+
'Protocol V2 prepared plan must contain exactly one resource archive',
|
|
1364
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
const archiveArtifact = archiveArtifacts[0];
|
|
1369
|
+
const archiveSource = await this.openProtocolV2PreparedSource(archiveArtifact.artifact);
|
|
1370
|
+
if (archiveSource.size > PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES) {
|
|
1371
|
+
throw ERRORS.TypedError(
|
|
1372
|
+
HardwareErrorCode.RuntimeError,
|
|
1373
|
+
'Protocol V2 prepared resource archive exceeds the total size limit',
|
|
1374
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
let archiveBinary: ArrayBuffer;
|
|
1379
|
+
try {
|
|
1380
|
+
archiveBinary = await readFirmwareByteSourceFully(archiveSource);
|
|
1381
|
+
} finally {
|
|
1382
|
+
await archiveSource.close();
|
|
1383
|
+
}
|
|
1384
|
+
const archiveDigest = bytesToHex(sha256(new Uint8Array(archiveBinary)));
|
|
1385
|
+
if (archiveDigest !== archiveArtifact.artifact.sha256.toLowerCase()) {
|
|
1386
|
+
throw ERRORS.TypedError(
|
|
1387
|
+
HardwareErrorCode.RuntimeError,
|
|
1388
|
+
'Protocol V2 prepared resource archive does not match its approved receipt',
|
|
1389
|
+
{ firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// materializedEntries 由宿主生成,只有与获批 ZIP 的规范字节完全一致后才能使用。
|
|
1394
|
+
const verifiedArchive = await this.prepareProtocolV2LocalResourceArchive(archiveBinary);
|
|
1395
|
+
const entries = archiveArtifact.materializedEntries ?? [];
|
|
1396
|
+
const entriesByName = new Map(entries.map(entry => [entry.entryName, entry] as const));
|
|
1397
|
+
if (
|
|
1398
|
+
entries.length !== verifiedArchive.materializedEntries.length ||
|
|
1399
|
+
verifiedArchive.materializedEntries.some(entry => {
|
|
1400
|
+
const preparedEntry = entriesByName.get(entry.entryName);
|
|
1401
|
+
const digest = bytesToHex(sha256(new Uint8Array(entry.binary)));
|
|
1402
|
+
return (
|
|
1403
|
+
!preparedEntry ||
|
|
1404
|
+
preparedEntry.artifact.size !== entry.binary.byteLength ||
|
|
1405
|
+
preparedEntry.artifact.sha256.toLowerCase() !== digest
|
|
1406
|
+
);
|
|
1407
|
+
})
|
|
1408
|
+
) {
|
|
1409
|
+
throw ERRORS.TypedError(
|
|
1410
|
+
HardwareErrorCode.RuntimeError,
|
|
1411
|
+
'Protocol V2 prepared resource entries do not match the approved archive',
|
|
1412
|
+
{ firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
const verifiedEntriesByName = new Map(
|
|
1417
|
+
verifiedArchive.materializedEntries.map(entry => [entry.entryName, entry.binary] as const)
|
|
1418
|
+
);
|
|
1419
|
+
const manifestBinary = verifiedEntriesByName.get('manifest.json');
|
|
1420
|
+
if (!manifestBinary) {
|
|
1421
|
+
throw ERRORS.TypedError(
|
|
1422
|
+
HardwareErrorCode.RuntimeError,
|
|
1423
|
+
'Protocol V2 prepared resource archive has no valid manifest.json',
|
|
1424
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
let manifestValue: unknown;
|
|
1429
|
+
try {
|
|
1430
|
+
manifestValue = JSON.parse(new TextDecoder().decode(manifestBinary));
|
|
1431
|
+
} catch (error) {
|
|
1432
|
+
throw ERRORS.TypedError(
|
|
1433
|
+
HardwareErrorCode.RuntimeError,
|
|
1434
|
+
`Protocol V2 prepared resource manifest is invalid: ${String(error)}`,
|
|
1435
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
const manifest = parseProtocolV2ResourceManifest(manifestValue);
|
|
1439
|
+
const selectedFiles = selectProtocolV2ResourceManifestFiles({
|
|
1440
|
+
manifest,
|
|
1441
|
+
targetsToUpdate: this.params.targetsToUpdate ?? [],
|
|
1442
|
+
});
|
|
1443
|
+
if (selectedFiles.length > PROTOCOL_V2_RESOURCE_FILE_MAX_COUNT) {
|
|
1444
|
+
throw ERRORS.TypedError(
|
|
1445
|
+
HardwareErrorCode.RuntimeError,
|
|
1446
|
+
'Protocol V2 prepared resource archive contains too many files',
|
|
1447
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
let totalSize = 0;
|
|
1452
|
+
const sources: ProtocolV2ResourceBundleSource[] = [];
|
|
1453
|
+
for (const [index, file] of selectedFiles.entries()) {
|
|
1454
|
+
const binary = verifiedEntriesByName.get(file.archive_path);
|
|
1455
|
+
if (!binary || binary.byteLength !== file.size) {
|
|
1456
|
+
throw ERRORS.TypedError(
|
|
1457
|
+
HardwareErrorCode.RuntimeError,
|
|
1458
|
+
`Protocol V2 prepared resource file does not match manifest: ${file.archive_path}`,
|
|
1459
|
+
{ firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
totalSize += binary.byteLength;
|
|
1463
|
+
if (totalSize > PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES) {
|
|
1464
|
+
throw ERRORS.TypedError(
|
|
1465
|
+
HardwareErrorCode.RuntimeError,
|
|
1466
|
+
'Protocol V2 prepared resource archive exceeds the total size limit',
|
|
1467
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
// 使用从获批 ZIP 中提取的规范字节,避免宿主在校验后替换 entry reader 内容。
|
|
1471
|
+
const source = await this.openProtocolV2MemorySource(binary);
|
|
1472
|
+
const header =
|
|
1473
|
+
source.size >= PROTOCOL_V2_OKPP_HEADER_SIZE
|
|
1474
|
+
? parseProtocolV2OkppHeader(
|
|
1475
|
+
new Uint8Array(await source.readAt(0, PROTOCOL_V2_OKPP_HEADER_SIZE))
|
|
1476
|
+
)
|
|
1477
|
+
: null;
|
|
1478
|
+
sources.push({
|
|
1479
|
+
name: file.original_name || `resource-${index}`,
|
|
1480
|
+
source,
|
|
1481
|
+
devicePath: file.device_path,
|
|
1482
|
+
...(header
|
|
1483
|
+
? {
|
|
1484
|
+
version: header.version,
|
|
1485
|
+
payloadHash: header.payloadHash,
|
|
1486
|
+
headerHash: header.headerHash,
|
|
1487
|
+
}
|
|
1488
|
+
: {}),
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
904
1491
|
return sources;
|
|
905
1492
|
}
|
|
906
1493
|
|
|
@@ -908,7 +1495,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
908
1495
|
try {
|
|
909
1496
|
this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
|
|
910
1497
|
const installSources = await this.prepareProtocolV2InstallSources(firmwareType, features);
|
|
911
|
-
const resourceSources = await this.prepareProtocolV2ResourceSources(
|
|
1498
|
+
const resourceSources = await this.prepareProtocolV2ResourceSources();
|
|
912
1499
|
if (installSources.length === 0 && resourceSources.length === 0) {
|
|
913
1500
|
throw ERRORS.TypedError(
|
|
914
1501
|
HardwareErrorCode.FirmwareUpdateDownloadFailed,
|
|
@@ -938,7 +1525,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
938
1525
|
for (const [target, version] of Object.entries(expected)) {
|
|
939
1526
|
if (
|
|
940
1527
|
!Array.from(PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.values()).includes(
|
|
941
|
-
target as FirmwareUpdateV4Target
|
|
1528
|
+
target as Exclude<FirmwareUpdateV4Target, 'boot_resources'>
|
|
942
1529
|
) ||
|
|
943
1530
|
typeof version !== 'string' ||
|
|
944
1531
|
!/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/u.test(version) ||
|
|
@@ -1024,13 +1611,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1024
1611
|
if (this.params.targetsToUpdate?.length) {
|
|
1025
1612
|
return [...new Set(this.params.targetsToUpdate)];
|
|
1026
1613
|
}
|
|
1614
|
+
if (this.params.preparedPlan?.targetsToUpdate.length) {
|
|
1615
|
+
return [...new Set(this.params.preparedPlan.targetsToUpdate)] as FirmwareUpdateV4Target[];
|
|
1616
|
+
}
|
|
1027
1617
|
const targets = new Set<FirmwareUpdateV4Target>();
|
|
1028
1618
|
Object.keys(this.params.componentArtifacts ?? {}).forEach(target =>
|
|
1029
1619
|
targets.add(target as FirmwareUpdateV4Target)
|
|
1030
1620
|
);
|
|
1031
|
-
if (this.params.resourceBundleArtifacts?.length || this.params.resourceFiles?.length) {
|
|
1032
|
-
targets.add('resource');
|
|
1033
|
-
}
|
|
1034
1621
|
if (this.params.bootloaderBinary) targets.add('boot');
|
|
1035
1622
|
if (this.params.applicationP1Binary) targets.add('app_v1');
|
|
1036
1623
|
if (this.params.applicationP2Binary) targets.add('app_v2');
|
|
@@ -1058,12 +1645,34 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1058
1645
|
return serialNumber || undefined;
|
|
1059
1646
|
}
|
|
1060
1647
|
|
|
1648
|
+
private getProtocolV2PreparedPlanDeviceModel(features?: Features) {
|
|
1649
|
+
const currentDeviceType = this.device.getCurrentDeviceType();
|
|
1650
|
+
const featureDeviceType = features ? getDeviceType(features) : undefined;
|
|
1651
|
+
const deviceType =
|
|
1652
|
+
currentDeviceType === EDeviceType.Pro2 || currentDeviceType === EDeviceType.Neo
|
|
1653
|
+
? currentDeviceType
|
|
1654
|
+
: featureDeviceType;
|
|
1655
|
+
return deviceType === EDeviceType.Pro2 || deviceType === EDeviceType.Neo
|
|
1656
|
+
? String(deviceType)
|
|
1657
|
+
: undefined;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
private getProtocolV2ConnectionRoute() {
|
|
1661
|
+
const descriptor = this.device.originalDescriptor;
|
|
1662
|
+
return (
|
|
1663
|
+
descriptor?.path?.trim() ||
|
|
1664
|
+
this.device.getConnectId?.()?.trim() ||
|
|
1665
|
+
descriptor?.id?.trim() ||
|
|
1666
|
+
undefined
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1061
1670
|
private assertProtocolV2DeviceInfoIdentity(deviceInfo: ProtocolV2DeviceInfo) {
|
|
1062
1671
|
assertProtocolV2ReconnectIdentity(
|
|
1063
1672
|
this.protocolV2ExpectedSerialNumber,
|
|
1064
1673
|
this.getProtocolV2SerialNumber(deviceInfo),
|
|
1065
1674
|
this.protocolV2ExpectedPath,
|
|
1066
|
-
this.
|
|
1675
|
+
this.getProtocolV2ConnectionRoute()
|
|
1067
1676
|
);
|
|
1068
1677
|
}
|
|
1069
1678
|
|
|
@@ -1083,11 +1692,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1083
1692
|
private async captureProtocolV2PhysicalIdentity() {
|
|
1084
1693
|
const deviceInfo = await this.requestProtocolV2PhysicalIdentity();
|
|
1085
1694
|
const serialNumber = this.getProtocolV2SerialNumber(deviceInfo);
|
|
1086
|
-
const path = this.
|
|
1695
|
+
const path = this.getProtocolV2ConnectionRoute();
|
|
1087
1696
|
if (this.params?.preparedPlan) {
|
|
1088
1697
|
assertFirmwareUpdatePreparedPlanDeviceIdentity({
|
|
1089
1698
|
preparedPlan: this.params.preparedPlan,
|
|
1090
1699
|
deviceIdentity: serialNumber,
|
|
1700
|
+
deviceModel: this.getProtocolV2PreparedPlanDeviceModel(this.device.features),
|
|
1091
1701
|
});
|
|
1092
1702
|
} else if (this.params?.expectedDeviceId) {
|
|
1093
1703
|
assertProtocolV2ReconnectIdentity(this.params?.expectedDeviceId, serialNumber);
|
|
@@ -1108,12 +1718,30 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1108
1718
|
return this.params.bootloaderBinary ?? null;
|
|
1109
1719
|
}
|
|
1110
1720
|
|
|
1111
|
-
private
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1721
|
+
private getMissingProtocolV2FirmwareTargets(installItems: ProtocolV2InstallItem[]) {
|
|
1722
|
+
const preparedTargets = new Set(
|
|
1723
|
+
installItems.flatMap(item => {
|
|
1724
|
+
const target = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(item.targetId);
|
|
1725
|
+
return target ? [target] : [];
|
|
1726
|
+
})
|
|
1116
1727
|
);
|
|
1728
|
+
return (this.params.targetsToUpdate ?? [])
|
|
1729
|
+
.filter(
|
|
1730
|
+
(target): target is Exclude<FirmwareUpdateV4Target, 'resource' | 'boot_resources'> =>
|
|
1731
|
+
target !== 'resource' && target !== 'boot_resources'
|
|
1732
|
+
)
|
|
1733
|
+
.filter(target => !preparedTargets.has(target));
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
private filterProtocolV2LocalInstallItems(installItems: ProtocolV2InstallItem[]) {
|
|
1737
|
+
if (!this.protocolV2HasExplicitTargetSelection) {
|
|
1738
|
+
return installItems;
|
|
1739
|
+
}
|
|
1740
|
+
const requestedTargets = new Set(this.params.targetsToUpdate ?? []);
|
|
1741
|
+
return installItems.filter(item => {
|
|
1742
|
+
const target = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(item.targetId);
|
|
1743
|
+
return target !== undefined && requestedTargets.has(target);
|
|
1744
|
+
});
|
|
1117
1745
|
}
|
|
1118
1746
|
|
|
1119
1747
|
private buildProtocolV2InstallItems({
|
|
@@ -1184,8 +1812,28 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1184
1812
|
`Missing Protocol V2 firmware component url: ${key}/${component.target}`
|
|
1185
1813
|
);
|
|
1186
1814
|
}
|
|
1815
|
+
const expectedFingerprint = normalizeProtocolV2Hex(component.fingerprint);
|
|
1816
|
+
if (
|
|
1817
|
+
!Number.isSafeInteger(component.expectedSize) ||
|
|
1818
|
+
Number(component.expectedSize) <= 0 ||
|
|
1819
|
+
!expectedFingerprint ||
|
|
1820
|
+
!/^[0-9a-f]{64}$/u.test(expectedFingerprint)
|
|
1821
|
+
) {
|
|
1822
|
+
throw ERRORS.TypedError(
|
|
1823
|
+
HardwareErrorCode.RuntimeError,
|
|
1824
|
+
`Protocol V2 firmware component integrity metadata is invalid: ${key}/${component.target}`,
|
|
1825
|
+
{ firmwareUpdateCode: 'FirmwarePlanInvalid' }
|
|
1826
|
+
);
|
|
1827
|
+
}
|
|
1187
1828
|
|
|
1188
1829
|
const { binary } = await getSysResourceBinary(component.url);
|
|
1830
|
+
if (binary.byteLength !== component.expectedSize) {
|
|
1831
|
+
throw ERRORS.TypedError(
|
|
1832
|
+
HardwareErrorCode.RuntimeError,
|
|
1833
|
+
`Protocol V2 firmware size mismatch: ${key}/${component.target}`,
|
|
1834
|
+
{ firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
|
|
1835
|
+
);
|
|
1836
|
+
}
|
|
1189
1837
|
if (!isProtocolV2FirmwareFingerprintValid(binary, component.fingerprint)) {
|
|
1190
1838
|
throw ERRORS.TypedError(
|
|
1191
1839
|
HardwareErrorCode.RuntimeError,
|
|
@@ -1208,7 +1856,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1208
1856
|
};
|
|
1209
1857
|
}
|
|
1210
1858
|
|
|
1211
|
-
private async prepareRemoteProtocolV2Binaries(
|
|
1859
|
+
private async prepareRemoteProtocolV2Binaries(
|
|
1860
|
+
firmwareType: EFirmwareType,
|
|
1861
|
+
features: Features,
|
|
1862
|
+
explicitInstallItems: ProtocolV2InstallItem[] = []
|
|
1863
|
+
) {
|
|
1212
1864
|
const release = DataManager.getFirmwareLatestRelease(features, firmwareType);
|
|
1213
1865
|
|
|
1214
1866
|
let bootloaderBinary: ArrayBuffer | null = null;
|
|
@@ -1216,6 +1868,15 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1216
1868
|
const installItems: ProtocolV2InstallItem[] = [];
|
|
1217
1869
|
|
|
1218
1870
|
if (!release) {
|
|
1871
|
+
const missingFirmwareTargets = this.getMissingProtocolV2FirmwareTargets(explicitInstallItems);
|
|
1872
|
+
if (missingFirmwareTargets.length > 0) {
|
|
1873
|
+
throw ERRORS.TypedError(
|
|
1874
|
+
HardwareErrorCode.RuntimeError,
|
|
1875
|
+
`Protocol V2 firmware release is unavailable for requested targets: ${missingFirmwareTargets.join(
|
|
1876
|
+
', '
|
|
1877
|
+
)}`
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1219
1880
|
return {
|
|
1220
1881
|
bootloaderBinary,
|
|
1221
1882
|
fwBinaryMap,
|
|
@@ -1225,6 +1886,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1225
1886
|
|
|
1226
1887
|
const entries = this.getRemoteComponentEntries(release);
|
|
1227
1888
|
const targetsToUpdate = new Set(this.params.targetsToUpdate ?? []);
|
|
1889
|
+
const explicitInstallItemByTargetId = new Map(
|
|
1890
|
+
explicitInstallItems.map(item => [item.targetId, item] as const)
|
|
1891
|
+
);
|
|
1892
|
+
const preparedTargets = new Set<FirmwareUpdateV4Target>();
|
|
1228
1893
|
|
|
1229
1894
|
for (const [key, component] of entries) {
|
|
1230
1895
|
const targetName = component.target?.toUpperCase();
|
|
@@ -1233,229 +1898,38 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1233
1898
|
? PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(target.targetId)
|
|
1234
1899
|
: undefined;
|
|
1235
1900
|
if (updateTarget && targetsToUpdate.has(updateTarget)) {
|
|
1236
|
-
const
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
binary: remoteBinary.binary,
|
|
1242
|
-
targetId: remoteBinary.targetId,
|
|
1243
|
-
kind: remoteBinary.kind,
|
|
1244
|
-
});
|
|
1901
|
+
const explicitInstallItem = explicitInstallItemByTargetId.get(target.targetId);
|
|
1902
|
+
const installItem =
|
|
1903
|
+
explicitInstallItem ?? (await this.downloadRemoteProtocolV2Component(key, component));
|
|
1904
|
+
if (installItem.kind === 'bootloader') {
|
|
1905
|
+
bootloaderBinary = installItem.binary;
|
|
1245
1906
|
} else {
|
|
1246
1907
|
const binaryEntry = {
|
|
1247
|
-
fileName:
|
|
1248
|
-
binary:
|
|
1249
|
-
targetId:
|
|
1908
|
+
fileName: installItem.fileName,
|
|
1909
|
+
binary: installItem.binary,
|
|
1910
|
+
targetId: installItem.targetId,
|
|
1250
1911
|
};
|
|
1251
1912
|
fwBinaryMap.push(binaryEntry);
|
|
1252
|
-
installItems.push({ ...binaryEntry, kind: remoteBinary.kind });
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
return {
|
|
1258
|
-
bootloaderBinary,
|
|
1259
|
-
fwBinaryMap,
|
|
1260
|
-
installItems,
|
|
1261
|
-
};
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
private getProtocolV2DeviceType(): EDeviceType.Pro2 | EDeviceType.Neo {
|
|
1265
|
-
const deviceType = this.device.getCurrentDeviceType();
|
|
1266
|
-
if (deviceType === EDeviceType.Pro2 || deviceType === EDeviceType.Neo) return deviceType;
|
|
1267
|
-
throw new Error(`Unsupported Protocol V2 device type: ${deviceType}`);
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
private async prepareProtocolV2BootResources(): Promise<
|
|
1271
|
-
ProtocolV2ResourceBundleBinary[] | undefined
|
|
1272
|
-
> {
|
|
1273
|
-
const wantsStableResources = !!this.params.targetsToUpdate?.includes('resource');
|
|
1274
|
-
const wantsBootResources = !!this.params.targetsToUpdate?.includes('boot_resources');
|
|
1275
|
-
if (!wantsStableResources && !wantsBootResources) {
|
|
1276
|
-
return undefined;
|
|
1277
|
-
}
|
|
1278
|
-
if (this.params.resourceFiles?.length) {
|
|
1279
|
-
return undefined;
|
|
1280
|
-
}
|
|
1281
|
-
const resource = DataManager.getProtocolV2BootResources(this.getProtocolV2DeviceType());
|
|
1282
|
-
if (!resource) {
|
|
1283
|
-
if (wantsBootResources) {
|
|
1284
|
-
throw new Error('Missing Protocol V2 boot resources configuration');
|
|
1285
|
-
}
|
|
1286
|
-
Log.debug('[FirmwareUpdateV4] no boot resources configured; continue with stable resources');
|
|
1287
|
-
return undefined;
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
const files: ProtocolV2ResourceBundleBinary[] = [];
|
|
1291
|
-
for (const file of resource.files) {
|
|
1292
|
-
const isCurrent =
|
|
1293
|
-
!this.params.forcedUpdateRes && (await this.isProtocolV2BootResourceCurrent(file));
|
|
1294
|
-
if (isCurrent) {
|
|
1295
|
-
Log.log(`[FirmwareUpdateV4] boot resource unchanged, skipping ${file.devicePath}`);
|
|
1296
|
-
} else {
|
|
1297
|
-
Log.log(`[FirmwareUpdateV4] downloading boot resource ${file.devicePath}`);
|
|
1298
|
-
const { binary } = await getSysResourceBinary(file.url);
|
|
1299
|
-
if (!isProtocolV2ResourceFileValid(binary, file)) {
|
|
1300
|
-
throw new Error(`Boot resource file verification failed: ${file.devicePath}`);
|
|
1301
1913
|
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
binary,
|
|
1305
|
-
devicePath: file.devicePath,
|
|
1306
|
-
});
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
return files;
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
private async isProtocolV2BootResourceCurrent(file: IProtocolV2ResourceFile) {
|
|
1313
|
-
try {
|
|
1314
|
-
const commands = this.device.getCommands();
|
|
1315
|
-
const pathInfo = await commands.typedCall(
|
|
1316
|
-
'FilesystemPathInfoQuery',
|
|
1317
|
-
'FilesystemPathInfo',
|
|
1318
|
-
{ path: file.devicePath },
|
|
1319
|
-
{ timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT }
|
|
1320
|
-
);
|
|
1321
|
-
const size = toProtocolV2FiniteNumber(pathInfo.message?.size);
|
|
1322
|
-
if (
|
|
1323
|
-
!pathInfo.message?.exist ||
|
|
1324
|
-
pathInfo.message?.directory ||
|
|
1325
|
-
!Number.isSafeInteger(size) ||
|
|
1326
|
-
size !== file.size
|
|
1327
|
-
) {
|
|
1328
|
-
return false;
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
const digest = sha256.create();
|
|
1332
|
-
const chunkSize = this.getProtocolV2FirmwareChunkSize('write');
|
|
1333
|
-
let offset = 0;
|
|
1334
|
-
while (offset < file.size) {
|
|
1335
|
-
const response = await commands.typedCall(
|
|
1336
|
-
'FilesystemFileRead',
|
|
1337
|
-
'FilesystemFile',
|
|
1338
|
-
{
|
|
1339
|
-
file: { path: file.devicePath, offset, total_size: 0 },
|
|
1340
|
-
chunk_len: Math.min(chunkSize, file.size - offset),
|
|
1341
|
-
},
|
|
1342
|
-
{ timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT }
|
|
1343
|
-
);
|
|
1344
|
-
const data = toProtocolV2Bytes(response.message?.data);
|
|
1345
|
-
if (data.byteLength === 0) return false;
|
|
1346
|
-
const consumed = data.subarray(0, Math.min(data.byteLength, file.size - offset));
|
|
1347
|
-
digest.update(consumed);
|
|
1348
|
-
offset += consumed.byteLength;
|
|
1349
|
-
}
|
|
1350
|
-
return bytesToHex(digest.digest()) === normalizeProtocolV2Hex(file.fileHash);
|
|
1351
|
-
} catch (error) {
|
|
1352
|
-
Log.debug(
|
|
1353
|
-
`[FirmwareUpdateV4] unable to compare boot resource ${file.devicePath}; scheduling rewrite`,
|
|
1354
|
-
error
|
|
1355
|
-
);
|
|
1356
|
-
return false;
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
private mergeProtocolV2ResourceBundles(
|
|
1361
|
-
...groups: Array<ProtocolV2ResourceBundleBinary[] | undefined>
|
|
1362
|
-
): ProtocolV2ResourceBundleBinary[] | undefined {
|
|
1363
|
-
const merged = groups.flatMap(group => group ?? []);
|
|
1364
|
-
if (!merged.length) return undefined;
|
|
1365
|
-
const seenPaths = new Set<string>();
|
|
1366
|
-
for (const bundle of merged) {
|
|
1367
|
-
if (seenPaths.has(bundle.devicePath)) {
|
|
1368
|
-
throw new Error(`Duplicate Protocol V2 resource devicePath: ${bundle.devicePath}`);
|
|
1914
|
+
installItems.push({ ...installItem });
|
|
1915
|
+
preparedTargets.add(updateTarget);
|
|
1369
1916
|
}
|
|
1370
|
-
seenPaths.add(bundle.devicePath);
|
|
1371
1917
|
}
|
|
1372
|
-
return merged;
|
|
1373
|
-
}
|
|
1374
1918
|
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
if (!files?.length) return undefined;
|
|
1378
|
-
|
|
1379
|
-
const prepared = files.map((file, index) => {
|
|
1380
|
-
const devicePath = validateProtocolV2FilesystemPath(
|
|
1381
|
-
file.devicePath,
|
|
1382
|
-
`resourceFiles[${index}].devicePath`
|
|
1383
|
-
);
|
|
1384
|
-
const descriptor = file as typeof file &
|
|
1385
|
-
Pick<Partial<IProtocolV2ResourceFile>, 'size' | 'fileHash'>;
|
|
1386
|
-
if (descriptor.size !== undefined && descriptor.size !== file.binary.byteLength) {
|
|
1387
|
-
throw new Error(`resourceFiles[${index}] size mismatch`);
|
|
1388
|
-
}
|
|
1389
|
-
if (
|
|
1390
|
-
descriptor.fileHash &&
|
|
1391
|
-
!isProtocolV2ResourceFileValid(file.binary, {
|
|
1392
|
-
size: file.binary.byteLength,
|
|
1393
|
-
fileHash: descriptor.fileHash,
|
|
1394
|
-
})
|
|
1395
|
-
) {
|
|
1396
|
-
throw new Error(`resourceFiles[${index}] SHA-256 mismatch`);
|
|
1397
|
-
}
|
|
1398
|
-
return {
|
|
1399
|
-
name: devicePath.split('/').pop() ?? devicePath,
|
|
1400
|
-
binary: file.binary,
|
|
1401
|
-
devicePath,
|
|
1402
|
-
};
|
|
1403
|
-
});
|
|
1404
|
-
if (new Set(prepared.map(file => file.devicePath)).size !== prepared.length) {
|
|
1405
|
-
throw new Error('resourceFiles contain duplicate devicePath values');
|
|
1406
|
-
}
|
|
1407
|
-
return prepared;
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
private async prepareProtocolV2ResourceBundles(): Promise<
|
|
1411
|
-
ProtocolV2ResourceBundleBinary[] | undefined
|
|
1412
|
-
> {
|
|
1413
|
-
if (!this.params.targetsToUpdate?.includes('resource')) {
|
|
1414
|
-
return undefined;
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
const resources = DataManager.getProtocolV2Resources(this.getProtocolV2DeviceType());
|
|
1418
|
-
if (!resources?.length) {
|
|
1419
|
-
throw new Error('Missing Pro2 stable resource configuration');
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
const inventory = this.params.forcedUpdateRes
|
|
1423
|
-
? undefined
|
|
1424
|
-
: await readProtocolV2ResourceInventory({
|
|
1425
|
-
commands: this.device.getCommands(),
|
|
1426
|
-
resources,
|
|
1427
|
-
chunkSize: this.getProtocolV2FirmwareChunkSize('write'),
|
|
1428
|
-
timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
|
|
1429
|
-
});
|
|
1430
|
-
const plan = buildProtocolV2ResourceUpdatePlan({
|
|
1431
|
-
resources,
|
|
1432
|
-
inventory,
|
|
1433
|
-
mode: 'bootloader-recovery',
|
|
1434
|
-
forced: this.params.forcedUpdateRes,
|
|
1435
|
-
});
|
|
1436
|
-
Log.log(
|
|
1437
|
-
`[FirmwareUpdateV4] Protocol V2 resource plan mode=bootloader-recovery status=${plan.status} count=${plan.resources.length}`
|
|
1919
|
+
const missingTarget = Array.from(targetsToUpdate).find(
|
|
1920
|
+
target => target !== 'resource' && !preparedTargets.has(target)
|
|
1438
1921
|
);
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1922
|
+
if (missingTarget) {
|
|
1923
|
+
throw ERRORS.TypedError(
|
|
1924
|
+
HardwareErrorCode.RuntimeError,
|
|
1925
|
+
`Protocol V2 release does not contain requested target ${missingTarget}`
|
|
1926
|
+
);
|
|
1443
1927
|
}
|
|
1444
|
-
return bundles;
|
|
1445
|
-
}
|
|
1446
1928
|
|
|
1447
|
-
private async downloadProtocolV2Resource(
|
|
1448
|
-
resource: IProtocolV2Resource
|
|
1449
|
-
): Promise<ProtocolV2ResourceBundleBinary> {
|
|
1450
|
-
Log.log(`[FirmwareUpdateV4] downloading Pro2 resource ${resource.type}`);
|
|
1451
|
-
const { binary } = await getSysResourceBinary(resource.url);
|
|
1452
|
-
if (!isProtocolV2ResourceFileValid(binary, resource)) {
|
|
1453
|
-
throw new Error(`Pro2 resource file verification failed: ${resource.type}`);
|
|
1454
|
-
}
|
|
1455
1929
|
return {
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1930
|
+
bootloaderBinary,
|
|
1931
|
+
fwBinaryMap,
|
|
1932
|
+
installItems,
|
|
1459
1933
|
};
|
|
1460
1934
|
}
|
|
1461
1935
|
|
|
@@ -1465,7 +1939,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1465
1939
|
return `vol0:/${path}`;
|
|
1466
1940
|
}
|
|
1467
1941
|
|
|
1468
|
-
private async readProtocolV2DeviceFileHeader(path: string) {
|
|
1942
|
+
private async readProtocolV2DeviceFileHeader(path: string, expectedSize?: number) {
|
|
1469
1943
|
const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
|
|
1470
1944
|
const filePath = this.getProtocolV2ResourceFilePath(path);
|
|
1471
1945
|
const pathInfoRes = await typedCall('FilesystemPathInfoQuery', 'FilesystemPathInfo', {
|
|
@@ -1476,7 +1950,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1476
1950
|
!pathInfoRes.message?.exist ||
|
|
1477
1951
|
pathInfoRes.message?.directory ||
|
|
1478
1952
|
fileSize === undefined ||
|
|
1479
|
-
fileSize < PROTOCOL_V2_OKPP_HEADER_SIZE
|
|
1953
|
+
fileSize < PROTOCOL_V2_OKPP_HEADER_SIZE ||
|
|
1954
|
+
(expectedSize !== undefined && fileSize !== expectedSize)
|
|
1480
1955
|
) {
|
|
1481
1956
|
return null;
|
|
1482
1957
|
}
|
|
@@ -1510,32 +1985,31 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1510
1985
|
return parseProtocolV2OkppHeader(headerBytes);
|
|
1511
1986
|
}
|
|
1512
1987
|
|
|
1513
|
-
/** Compare
|
|
1988
|
+
/** Compare the downloaded and installed okpkg headers before transferring a resource. */
|
|
1514
1989
|
private async isProtocolV2ResourceBundleUpToDate(
|
|
1515
1990
|
bundle: Pick<
|
|
1516
1991
|
ProtocolV2ResourceBundleSource,
|
|
1517
|
-
'name' | 'devicePath' | 'version' | 'payloadHash' | 'headerHash'
|
|
1992
|
+
'name' | 'source' | 'devicePath' | 'version' | 'payloadHash' | 'headerHash'
|
|
1518
1993
|
>
|
|
1519
1994
|
): Promise<boolean> {
|
|
1520
1995
|
if (this.params?.forcedUpdateRes) return false;
|
|
1521
|
-
if (!bundle.
|
|
1996
|
+
if (!bundle.payloadHash || !bundle.headerHash) return false;
|
|
1522
1997
|
|
|
1523
1998
|
try {
|
|
1524
|
-
const header = await this.readProtocolV2DeviceFileHeader(
|
|
1999
|
+
const header = await this.readProtocolV2DeviceFileHeader(
|
|
2000
|
+
bundle.devicePath,
|
|
2001
|
+
bundle.source.size
|
|
2002
|
+
);
|
|
1525
2003
|
if (!header) return false;
|
|
1526
2004
|
|
|
1527
2005
|
if (bundle.version) {
|
|
1528
2006
|
const cmp = compareProtocolV2Versions(header.version, bundle.version);
|
|
1529
2007
|
if (cmp === undefined || cmp !== 0) return false;
|
|
1530
2008
|
}
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
if (bundle.headerHash) {
|
|
1536
|
-
const expected = normalizeProtocolV2Hex(bundle.headerHash);
|
|
1537
|
-
if (expected && header.headerHash !== expected) return false;
|
|
1538
|
-
}
|
|
2009
|
+
const expectedPayloadHash = normalizeProtocolV2Hex(bundle.payloadHash);
|
|
2010
|
+
const expectedHeaderHash = normalizeProtocolV2Hex(bundle.headerHash);
|
|
2011
|
+
if (!expectedPayloadHash || header.payloadHash !== expectedPayloadHash) return false;
|
|
2012
|
+
if (!expectedHeaderHash || header.headerHash !== expectedHeaderHash) return false;
|
|
1539
2013
|
return true;
|
|
1540
2014
|
} catch (error) {
|
|
1541
2015
|
Log.log(`[FirmwareUpdateV4] RESC bundle ${bundle.name} header check failed: `, error);
|
|
@@ -1691,11 +2165,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1691
2165
|
resourceSources: ProtocolV2ResourceBundleSource[];
|
|
1692
2166
|
}): ProtocolV2ExecutionPhase[] {
|
|
1693
2167
|
const phases: ProtocolV2ExecutionPhase[] = [];
|
|
1694
|
-
|
|
2168
|
+
const bootResourceSources = resourceSources.filter(source =>
|
|
2169
|
+
isProtocolV2BootResourcePackagePath(source.devicePath)
|
|
2170
|
+
);
|
|
2171
|
+
const remainingResourceSources = resourceSources.filter(
|
|
2172
|
+
source => !isProtocolV2BootResourcePackagePath(source.devicePath)
|
|
2173
|
+
);
|
|
2174
|
+
// A previous interrupted run may have left a promotable staging file. Replace
|
|
2175
|
+
// it with the current approved package before any firmware-triggered reboot.
|
|
2176
|
+
if (bootResourceSources.length > 0) {
|
|
1695
2177
|
phases.push({
|
|
1696
2178
|
kind: 'resource-sync',
|
|
1697
2179
|
installSources: [],
|
|
1698
|
-
resourceSources,
|
|
2180
|
+
resourceSources: bootResourceSources,
|
|
1699
2181
|
});
|
|
1700
2182
|
}
|
|
1701
2183
|
const bootloaderSources = installSources.filter(source => source.kind === 'bootloader');
|
|
@@ -1713,6 +2195,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1713
2195
|
}
|
|
1714
2196
|
);
|
|
1715
2197
|
}
|
|
2198
|
+
if (remainingResourceSources.length > 0) {
|
|
2199
|
+
phases.push({
|
|
2200
|
+
kind: 'resource-sync',
|
|
2201
|
+
installSources: [],
|
|
2202
|
+
resourceSources: remainingResourceSources,
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
1716
2205
|
const componentSources = installSources.filter(source => source.kind !== 'bootloader');
|
|
1717
2206
|
if (componentSources.length > 0) {
|
|
1718
2207
|
phases.push({
|
|
@@ -1736,6 +2225,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1736
2225
|
installSources: ProtocolV2InstallSource[];
|
|
1737
2226
|
resourceSources: ProtocolV2ResourceBundleSource[];
|
|
1738
2227
|
}) {
|
|
2228
|
+
this.protocolV2BootResourceStagingSafe = false;
|
|
1739
2229
|
const phases = this.buildProtocolV2ExecutionPhases({
|
|
1740
2230
|
installSources,
|
|
1741
2231
|
resourceSources,
|
|
@@ -1746,9 +2236,17 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1746
2236
|
}
|
|
1747
2237
|
if (phase.kind === 'resource-sync') {
|
|
1748
2238
|
await this.enterProtocolV2BootloaderMode();
|
|
2239
|
+
if (
|
|
2240
|
+
!phase.resourceSources.some(source =>
|
|
2241
|
+
isProtocolV2BootResourcePackagePath(source.devicePath)
|
|
2242
|
+
)
|
|
2243
|
+
) {
|
|
2244
|
+
await this.ensureProtocolV2BootResourceStagingIsEmpty();
|
|
2245
|
+
}
|
|
1749
2246
|
await this.executeProtocolV2TransferPhase(phase);
|
|
1750
2247
|
} else if (phase.kind === 'bootloader-install' || phase.kind === 'component-install') {
|
|
1751
2248
|
await this.enterProtocolV2BootloaderMode();
|
|
2249
|
+
await this.ensureProtocolV2BootResourceStagingIsEmpty();
|
|
1752
2250
|
await this.executeProtocolV2TransferPhase(phase);
|
|
1753
2251
|
await this.exitProtocolV2BootloaderToNormal();
|
|
1754
2252
|
} else if (phase.kind === 'bootloader-verify') {
|
|
@@ -1762,6 +2260,35 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1762
2260
|
);
|
|
1763
2261
|
}
|
|
1764
2262
|
|
|
2263
|
+
private async ensureProtocolV2BootResourceStagingIsEmpty() {
|
|
2264
|
+
if (this.protocolV2BootResourceStagingSafe) return;
|
|
2265
|
+
const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
|
|
2266
|
+
const query = () =>
|
|
2267
|
+
typedCall('FilesystemPathInfoQuery', 'FilesystemPathInfo', {
|
|
2268
|
+
path: PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_STAGING_PATH,
|
|
2269
|
+
});
|
|
2270
|
+
const current = await query();
|
|
2271
|
+
if (current.message?.exist) {
|
|
2272
|
+
if (current.message.directory) {
|
|
2273
|
+
throw ERRORS.TypedError(
|
|
2274
|
+
HardwareErrorCode.EmmcFileWriteFirmwareError,
|
|
2275
|
+
'Protocol V2 boot resource staging path is not a file'
|
|
2276
|
+
);
|
|
2277
|
+
}
|
|
2278
|
+
await typedCall('FilesystemFileDelete', 'Success', {
|
|
2279
|
+
path: PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_STAGING_PATH,
|
|
2280
|
+
});
|
|
2281
|
+
const remaining = await query();
|
|
2282
|
+
if (remaining.message?.exist) {
|
|
2283
|
+
throw ERRORS.TypedError(
|
|
2284
|
+
HardwareErrorCode.EmmcFileWriteFirmwareError,
|
|
2285
|
+
'Protocol V2 stale boot resource staging file could not be removed'
|
|
2286
|
+
);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
this.protocolV2BootResourceStagingSafe = true;
|
|
2290
|
+
}
|
|
2291
|
+
|
|
1765
2292
|
private async executeProtocolV2SourceUpdate({
|
|
1766
2293
|
installSources,
|
|
1767
2294
|
resourceSources,
|
|
@@ -1779,12 +2306,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1779
2306
|
fwBinaryMap,
|
|
1780
2307
|
bootloaderBinary,
|
|
1781
2308
|
installItems,
|
|
1782
|
-
resourceBundles,
|
|
1783
2309
|
}: {
|
|
1784
2310
|
fwBinaryMap?: ProtocolV2TargetBinary[];
|
|
1785
2311
|
bootloaderBinary?: ArrayBuffer | null;
|
|
1786
2312
|
installItems?: ProtocolV2InstallItem[];
|
|
1787
|
-
resourceBundles?: ProtocolV2ResourceBundleBinary[];
|
|
1788
2313
|
}) {
|
|
1789
2314
|
const memoryInstallItems =
|
|
1790
2315
|
installItems ??
|
|
@@ -1801,16 +2326,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1801
2326
|
kind: item.kind,
|
|
1802
2327
|
}))
|
|
1803
2328
|
);
|
|
1804
|
-
const resourceSources = await Promise.all(
|
|
1805
|
-
(resourceBundles ?? []).map(async bundle => ({
|
|
1806
|
-
name: bundle.name,
|
|
1807
|
-
source: await this.openProtocolV2MemorySource(bundle.binary),
|
|
1808
|
-
devicePath: bundle.devicePath,
|
|
1809
|
-
}))
|
|
1810
|
-
);
|
|
1811
2329
|
return await this.executeProtocolV2Phases({
|
|
1812
2330
|
installSources,
|
|
1813
|
-
resourceSources,
|
|
2331
|
+
resourceSources: [],
|
|
1814
2332
|
});
|
|
1815
2333
|
} finally {
|
|
1816
2334
|
await this.closeProtocolV2PreparedSources();
|
|
@@ -1824,7 +2342,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1824
2342
|
let totalSize = installSources.reduce((total, item) => total + item.source.size, 0);
|
|
1825
2343
|
const resourcesToSync: ProtocolV2ResourceBundleSource[] = [];
|
|
1826
2344
|
for (const resource of resourceSources) {
|
|
1827
|
-
|
|
2345
|
+
// Early boot promotes this mounted package's staging file. Always replace any
|
|
2346
|
+
// stale staging bytes with the artifact approved by the current PreparedPlan,
|
|
2347
|
+
// even when the mounted final file itself is already current.
|
|
2348
|
+
const requiresFreshStaging = isProtocolV2BootResourcePackagePath(resource.devicePath);
|
|
2349
|
+
if (!requiresFreshStaging && (await this.isProtocolV2ResourceBundleUpToDate(resource))) {
|
|
1828
2350
|
Log.log(`[FirmwareUpdateV4] skip RESC bundle ${resource.name}; already up to date`);
|
|
1829
2351
|
} else {
|
|
1830
2352
|
resourcesToSync.push(resource);
|
|
@@ -1835,13 +2357,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1835
2357
|
this.postTipMessage(FirmwareUpdateTipMessage.StartTransferData);
|
|
1836
2358
|
let processedSize = 0;
|
|
1837
2359
|
for (const resource of resourcesToSync) {
|
|
2360
|
+
// The bootloader keeps its live resource package mounted. FatFs rejects
|
|
2361
|
+
// replacing an open file, so early boot promotes this staging file before mounting it.
|
|
2362
|
+
const writePath = resolveProtocolV2ResourceWritePath(resource.devicePath);
|
|
1838
2363
|
processedSize = await this.protocolV2SourceUpdateProcess({
|
|
1839
2364
|
source: resource.source,
|
|
1840
|
-
filePath:
|
|
2365
|
+
filePath: writePath,
|
|
1841
2366
|
processedSize,
|
|
1842
2367
|
totalSize,
|
|
1843
2368
|
});
|
|
1844
|
-
await this.verifyProtocolV2StagedFile(
|
|
2369
|
+
await this.verifyProtocolV2StagedFile(writePath, resource.source.size);
|
|
2370
|
+
if (isProtocolV2BootResourcePackagePath(resource.devicePath)) {
|
|
2371
|
+
this.protocolV2BootResourceStagingSafe = true;
|
|
2372
|
+
}
|
|
1845
2373
|
}
|
|
1846
2374
|
|
|
1847
2375
|
const stagedInstallTargets: Array<{ targetId: number; path: string }> = [];
|