@onekeyfe/hd-core 1.2.0-alpha.97 → 1.2.0-alpha.98
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 +404 -84
- package/__tests__/check-firmware-release-protocol-v2.test.ts +199 -0
- package/__tests__/firmware-memory-host.test.ts +112 -0
- package/__tests__/firmware-update/device-update-bootloader.test.ts +13 -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-plan.test.ts +232 -70
- 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-resources.test.ts +148 -232
- package/__tests__/protocol-v2.test.ts +1236 -516
- 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 +8 -8
- 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/FirmwareUpdatePlan.d.ts +48 -1
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +6 -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 +1633 -967
- 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 +108 -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 +108 -73
- package/src/api/FirmwareUpdateV3.ts +101 -56
- package/src/api/FirmwareUpdateV4.ts +885 -403
- package/src/api/UploadPortfolio.ts +18 -0
- package/src/api/device/DeviceUpdateBootloader.ts +37 -11
- package/src/api/firmware/FirmwareHostBinding.ts +16 -3
- package/src/api/firmware/FirmwareMemoryHost.ts +142 -0
- package/src/api/firmware/FirmwarePreparedResourceArchive.ts +207 -0
- package/src/api/firmware/FirmwareUpdatePlan.ts +282 -122
- package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +27 -3
- 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,9 +108,34 @@ 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
141
|
params: FirmwareUpdateV4Params
|
|
@@ -148,12 +186,6 @@ type ProtocolV2TargetBinary = { fileName: string; binary: ArrayBuffer; targetId:
|
|
|
148
186
|
type ProtocolV2InstallItem = ProtocolV2TargetBinary & {
|
|
149
187
|
kind: ProtocolV2RemoteComponentTarget['kind'];
|
|
150
188
|
};
|
|
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
189
|
|
|
158
190
|
type ProtocolV2RemoteComponentBinary = ProtocolV2RemoteComponentTarget & {
|
|
159
191
|
binary: ArrayBuffer;
|
|
@@ -181,6 +213,18 @@ type ProtocolV2ResourceBundleSource = {
|
|
|
181
213
|
headerHash?: string;
|
|
182
214
|
};
|
|
183
215
|
|
|
216
|
+
type ProtocolV2LocalResourceArchive = {
|
|
217
|
+
binary: ArrayBuffer;
|
|
218
|
+
materializedEntries: FirmwareMemoryArtifactEntry[];
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
type JSZipSizedEntry = JSZip.JSZipObject & {
|
|
222
|
+
_data?: {
|
|
223
|
+
compressedSize?: unknown;
|
|
224
|
+
uncompressedSize?: unknown;
|
|
225
|
+
};
|
|
226
|
+
};
|
|
227
|
+
|
|
184
228
|
type ProtocolV2ExecutionPhaseKind =
|
|
185
229
|
| 'resource-sync'
|
|
186
230
|
| 'bootloader-install'
|
|
@@ -252,7 +296,23 @@ const PROTOCOL_V2_FIRMWARE_STAGING_PATHS = new Set(
|
|
|
252
296
|
)
|
|
253
297
|
);
|
|
254
298
|
|
|
255
|
-
const
|
|
299
|
+
const PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH = 'vol0:/loaders/bootloader/boot_resource.okpkg';
|
|
300
|
+
const PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_STAGING_PATH = `${PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH}.staging`;
|
|
301
|
+
|
|
302
|
+
const isProtocolV2BootResourcePackagePath = (devicePath: string) =>
|
|
303
|
+
typeof devicePath === 'string' &&
|
|
304
|
+
devicePath.replace(/^vol0:(?!\/)/i, 'vol0:/').toLowerCase() ===
|
|
305
|
+
PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH;
|
|
306
|
+
|
|
307
|
+
const resolveProtocolV2ResourceWritePath = (devicePath: string) =>
|
|
308
|
+
isProtocolV2BootResourcePackagePath(devicePath)
|
|
309
|
+
? PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_STAGING_PATH
|
|
310
|
+
: devicePath;
|
|
311
|
+
|
|
312
|
+
const PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID = new Map<
|
|
313
|
+
number,
|
|
314
|
+
Exclude<FirmwareUpdateV4Target, 'boot_resources'>
|
|
315
|
+
>([
|
|
256
316
|
[ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_BOOTLOADER, 'boot'],
|
|
257
317
|
[ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P1, 'app_v1'],
|
|
258
318
|
[ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P2, 'app_v2'],
|
|
@@ -263,6 +323,19 @@ const PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID = new Map<number, FirmwareUpdateV4T
|
|
|
263
323
|
[ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE04, 'se04'],
|
|
264
324
|
]);
|
|
265
325
|
|
|
326
|
+
const PROTOCOL_V2_INSTALL_TARGET_BY_UPDATE_TARGET = new Map<
|
|
327
|
+
Exclude<FirmwareUpdateV4Target, 'resource'>,
|
|
328
|
+
ProtocolV2RemoteComponentTarget
|
|
329
|
+
>(
|
|
330
|
+
Object.values(PROTOCOL_V2_REMOTE_COMPONENT_TARGETS).map(target => [
|
|
331
|
+
PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(target.targetId) as Exclude<
|
|
332
|
+
FirmwareUpdateV4Target,
|
|
333
|
+
'resource'
|
|
334
|
+
>,
|
|
335
|
+
target,
|
|
336
|
+
])
|
|
337
|
+
);
|
|
338
|
+
|
|
266
339
|
const PROTOCOL_V2_ROMLOADER_UNSUPPORTED_MESSAGE =
|
|
267
340
|
'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
341
|
|
|
@@ -494,6 +567,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
494
567
|
|
|
495
568
|
private protocolV2ExecutionInLoader = false;
|
|
496
569
|
|
|
570
|
+
private protocolV2BootResourceStagingSafe = false;
|
|
571
|
+
|
|
497
572
|
private protocolV2CompletedTargetVersions = new Map<number, number>();
|
|
498
573
|
|
|
499
574
|
private protocolV2LatestFinalFeatures?: Features;
|
|
@@ -534,11 +609,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
534
609
|
{ name: 'se02Binary', type: 'buffer' },
|
|
535
610
|
{ name: 'se03Binary', type: 'buffer' },
|
|
536
611
|
{ name: 'se04Binary', type: 'buffer' },
|
|
612
|
+
{ name: 'resourceArchiveBinary', type: 'buffer' },
|
|
537
613
|
{ name: 'firmwareType', type: 'string' },
|
|
538
614
|
{ name: 'targetsToUpdate', type: 'array', allowEmpty: true },
|
|
539
615
|
{ name: 'platform', type: 'string' },
|
|
540
616
|
{ name: 'expectedDeviceId', type: 'string' },
|
|
541
|
-
{ name: 'resourceFiles', type: 'array', allowEmpty: true },
|
|
542
617
|
]);
|
|
543
618
|
if (
|
|
544
619
|
payload.expectedDeviceId !== undefined &&
|
|
@@ -549,50 +624,101 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
549
624
|
'Protocol V2 expected device identity is invalid'
|
|
550
625
|
);
|
|
551
626
|
}
|
|
552
|
-
const
|
|
553
|
-
payload.preparedPlan
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
627
|
+
const preparedPlan = payload.preparedPlan
|
|
628
|
+
? validateFirmwareUpdatePreparedPlan(payload.preparedPlan)
|
|
629
|
+
: undefined;
|
|
630
|
+
const hasLocalArtifacts = [
|
|
631
|
+
payload.bootloaderBinary,
|
|
632
|
+
payload.romloaderBinary,
|
|
633
|
+
payload.applicationP1Binary,
|
|
634
|
+
payload.applicationP2Binary,
|
|
635
|
+
payload.coprocessorBinary,
|
|
636
|
+
payload.se01Binary,
|
|
637
|
+
payload.se02Binary,
|
|
638
|
+
payload.se03Binary,
|
|
639
|
+
payload.se04Binary,
|
|
640
|
+
payload.resourceArchiveBinary,
|
|
641
|
+
].some(Boolean);
|
|
642
|
+
if (preparedPlan && hasLocalArtifacts) {
|
|
643
|
+
throw ERRORS.TypedError(
|
|
644
|
+
HardwareErrorCode.CallMethodInvalidParameter,
|
|
645
|
+
'Prepared firmware plans cannot be combined with legacy or local firmware inputs'
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
let { artifactReader } = payload;
|
|
649
|
+
if (preparedPlan) {
|
|
650
|
+
artifactReader = resolveFirmwareUpdateHostBinding(
|
|
651
|
+
payload.hostBindingGeneration,
|
|
652
|
+
preparedPlan.preparedPlanDigest
|
|
653
|
+
).artifactReader;
|
|
654
|
+
} else if (payload.hostBindingGeneration !== undefined) {
|
|
655
|
+
artifactReader = resolveFirmwareUpdateHostBinding(
|
|
656
|
+
payload.hostBindingGeneration
|
|
657
|
+
).artifactReader;
|
|
658
|
+
}
|
|
659
|
+
if (preparedPlan) {
|
|
557
660
|
assertFirmwareUpdatePreparedPlanBinding({
|
|
558
|
-
preparedPlan
|
|
661
|
+
preparedPlan,
|
|
559
662
|
executor: 'v4',
|
|
560
663
|
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
|
-
],
|
|
664
|
+
scopeTargets: [],
|
|
665
|
+
bindings: [],
|
|
591
666
|
});
|
|
667
|
+
if (payload.componentArtifacts) {
|
|
668
|
+
const componentBindings: Array<{
|
|
669
|
+
target: Exclude<FirmwareUpdateV4Target, 'resource'>;
|
|
670
|
+
artifact: FirmwareArtifactReference;
|
|
671
|
+
}> = Object.entries(payload.componentArtifacts).flatMap(([target, artifact]) =>
|
|
672
|
+
artifact
|
|
673
|
+
? [
|
|
674
|
+
{
|
|
675
|
+
target: target as Exclude<FirmwareUpdateV4Target, 'resource'>,
|
|
676
|
+
artifact: artifact as FirmwareArtifactReference,
|
|
677
|
+
},
|
|
678
|
+
]
|
|
679
|
+
: []
|
|
680
|
+
);
|
|
681
|
+
assertFirmwareUpdatePreparedPlanBinding({
|
|
682
|
+
preparedPlan,
|
|
683
|
+
executor: 'v4',
|
|
684
|
+
platform: payload.platform,
|
|
685
|
+
scopeTargets: componentBindings.map(binding => binding.target),
|
|
686
|
+
bindings: componentBindings,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const preparedExpectedTargetVersions = preparedPlan?.artifacts.reduce<
|
|
692
|
+
NonNullable<FirmwareUpdateV4Params['expectedTargetVersions']>
|
|
693
|
+
>((result, artifact) => {
|
|
694
|
+
if (
|
|
695
|
+
artifact.role === 'component' &&
|
|
696
|
+
artifact.target !== 'resource' &&
|
|
697
|
+
artifact.targetVersion &&
|
|
698
|
+
PROTOCOL_V2_INSTALL_TARGET_BY_UPDATE_TARGET.has(
|
|
699
|
+
artifact.target as Exclude<FirmwareUpdateV4Target, 'resource'>
|
|
700
|
+
)
|
|
701
|
+
) {
|
|
702
|
+
result[artifact.target as FirmwareUpdateV4Target] = artifact.targetVersion;
|
|
703
|
+
}
|
|
704
|
+
return result;
|
|
705
|
+
}, {});
|
|
706
|
+
const localTargetsToUpdate = payload.targetsToUpdate?.map((target: FirmwareUpdateV4Target) =>
|
|
707
|
+
target === 'boot_resources' ? 'resource' : target
|
|
708
|
+
);
|
|
709
|
+
if (
|
|
710
|
+
payload.resourceArchiveBinary &&
|
|
711
|
+
localTargetsToUpdate &&
|
|
712
|
+
!localTargetsToUpdate.includes('resource')
|
|
713
|
+
) {
|
|
714
|
+
localTargetsToUpdate.push('resource');
|
|
592
715
|
}
|
|
716
|
+
// 本地 ZIP 本身就是明确的资源升级请求,不要求调用方重复声明 target。
|
|
717
|
+
const resolvedLocalTargetsToUpdate =
|
|
718
|
+
localTargetsToUpdate ?? (payload.resourceArchiveBinary ? ['resource'] : undefined);
|
|
593
719
|
|
|
594
720
|
this.params = {
|
|
595
|
-
preparedPlan
|
|
721
|
+
preparedPlan,
|
|
596
722
|
chunkSize: payload.chunkSize,
|
|
597
723
|
forcedUpdateRes: payload.forcedUpdateRes,
|
|
598
724
|
bootloaderBinary: payload.bootloaderBinary,
|
|
@@ -604,15 +730,18 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
604
730
|
se02Binary: payload.se02Binary,
|
|
605
731
|
se03Binary: payload.se03Binary,
|
|
606
732
|
se04Binary: payload.se04Binary,
|
|
607
|
-
|
|
608
|
-
firmwareType: payload.firmwareType,
|
|
609
|
-
targetsToUpdate:
|
|
610
|
-
|
|
733
|
+
resourceArchiveBinary: payload.resourceArchiveBinary,
|
|
734
|
+
firmwareType: preparedPlan?.firmwareType ?? payload.firmwareType,
|
|
735
|
+
targetsToUpdate: preparedPlan
|
|
736
|
+
? ([...preparedPlan.targetsToUpdate] as FirmwareUpdateV4Target[])
|
|
737
|
+
: resolvedLocalTargetsToUpdate,
|
|
738
|
+
expectedTargetVersions: preparedPlan
|
|
739
|
+
? preparedExpectedTargetVersions
|
|
740
|
+
: payload.expectedTargetVersions,
|
|
611
741
|
platform: payload.platform,
|
|
612
742
|
expectedDeviceId: payload.expectedDeviceId,
|
|
613
|
-
artifactReader
|
|
614
|
-
componentArtifacts: payload.componentArtifacts,
|
|
615
|
-
resourceBundleArtifacts: payload.resourceBundleArtifacts,
|
|
743
|
+
artifactReader,
|
|
744
|
+
componentArtifacts: preparedPlan ? undefined : payload.componentArtifacts,
|
|
616
745
|
};
|
|
617
746
|
}
|
|
618
747
|
|
|
@@ -659,30 +788,55 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
659
788
|
this.validateExpectedTargetVersions();
|
|
660
789
|
|
|
661
790
|
if (
|
|
662
|
-
|
|
663
|
-
this.params.
|
|
791
|
+
!this.params.preparedPlan &&
|
|
792
|
+
this.params.resourceArchiveBinary &&
|
|
793
|
+
this.params.targetsToUpdate?.includes('resource')
|
|
664
794
|
) {
|
|
795
|
+
const localMemoryHost = await this.prepareProtocolV2LocalMemoryHost({
|
|
796
|
+
features: deviceFeatures,
|
|
797
|
+
firmwareType,
|
|
798
|
+
});
|
|
799
|
+
try {
|
|
800
|
+
return await this.runProtocolV2PreparedArtifacts(deviceFeatures, firmwareType);
|
|
801
|
+
} finally {
|
|
802
|
+
localMemoryHost.release();
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const hasPreparedComponentArtifacts = Object.values(this.params.componentArtifacts ?? {}).some(
|
|
807
|
+
Boolean
|
|
808
|
+
);
|
|
809
|
+
if (this.params.preparedPlan || hasPreparedComponentArtifacts) {
|
|
665
810
|
return this.runProtocolV2PreparedArtifacts(deviceFeatures, firmwareType);
|
|
666
811
|
}
|
|
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);
|
|
812
|
+
const wantsResources = !!this.params.targetsToUpdate?.includes('resource');
|
|
673
813
|
|
|
674
814
|
let fwBinaryMap: ProtocolV2TargetBinary[] = [];
|
|
675
815
|
let bootloaderBinary: ArrayBuffer | null = null;
|
|
676
816
|
let installItems: ProtocolV2InstallItem[] | undefined;
|
|
677
|
-
let resourceBundles: ProtocolV2ResourceBundleBinary[] | undefined;
|
|
678
817
|
try {
|
|
679
818
|
this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
|
|
680
|
-
resourceBundles = this.prepareExplicitProtocolV2ResourceFiles();
|
|
681
819
|
fwBinaryMap = this.collectExplicitTargetBinaries();
|
|
682
820
|
bootloaderBinary = this.prepareBootloaderBinary();
|
|
683
|
-
const
|
|
821
|
+
const explicitInstallItems = this.buildProtocolV2InstallItems({
|
|
822
|
+
bootloaderBinary,
|
|
823
|
+
fwBinaryMap,
|
|
824
|
+
});
|
|
825
|
+
const missingFirmwareTargets = this.getMissingProtocolV2FirmwareTargets(explicitInstallItems);
|
|
826
|
+
const needsRemoteFirmware = this.params.targetsToUpdate?.length
|
|
827
|
+
? missingFirmwareTargets.length > 0
|
|
828
|
+
: explicitInstallItems.length === 0;
|
|
829
|
+
if (wantsResources) {
|
|
830
|
+
throw ERRORS.TypedError(
|
|
831
|
+
HardwareErrorCode.RuntimeError,
|
|
832
|
+
'Protocol V2 resource archive must be provided through a local or external PreparedPlan',
|
|
833
|
+
{
|
|
834
|
+
firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
|
|
835
|
+
}
|
|
836
|
+
);
|
|
837
|
+
}
|
|
684
838
|
if (
|
|
685
|
-
|
|
839
|
+
needsRemoteFirmware &&
|
|
686
840
|
(this.params.artifactReader ||
|
|
687
841
|
DataManager.getSettings('firmwareManifestMode') === 'external-only')
|
|
688
842
|
) {
|
|
@@ -694,74 +848,47 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
694
848
|
}
|
|
695
849
|
);
|
|
696
850
|
}
|
|
697
|
-
if (needsRemoteFirmware
|
|
851
|
+
if (needsRemoteFirmware) {
|
|
698
852
|
// Remote updates must use a freshly fetched config before any reboot or file write.
|
|
699
|
-
await DataManager.forceReloadData(
|
|
700
|
-
requireResources: needsRemoteResources || needsRemoteBootResources,
|
|
701
|
-
});
|
|
853
|
+
await DataManager.forceReloadData();
|
|
702
854
|
}
|
|
703
855
|
if (needsRemoteFirmware) {
|
|
704
856
|
const remoteBinaries = await this.prepareRemoteProtocolV2Binaries(
|
|
705
857
|
firmwareType,
|
|
706
|
-
deviceFeatures
|
|
858
|
+
deviceFeatures,
|
|
859
|
+
explicitInstallItems
|
|
707
860
|
);
|
|
708
861
|
bootloaderBinary = remoteBinaries.bootloaderBinary;
|
|
709
862
|
fwBinaryMap = remoteBinaries.fwBinaryMap;
|
|
710
863
|
installItems = remoteBinaries.installItems;
|
|
711
864
|
}
|
|
712
|
-
|
|
713
|
-
if (bootResourceFiles?.length) {
|
|
714
|
-
resourceBundles = this.mergeProtocolV2ResourceBundles(resourceBundles, bootResourceFiles);
|
|
715
|
-
}
|
|
716
|
-
if (!needsRemoteResources) {
|
|
717
|
-
this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
|
|
718
|
-
}
|
|
865
|
+
this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
|
|
719
866
|
} catch (err) {
|
|
867
|
+
if (
|
|
868
|
+
typeof err === 'object' &&
|
|
869
|
+
err !== null &&
|
|
870
|
+
'params' in err &&
|
|
871
|
+
(err as HardwareError).params?.firmwareUpdateCode === 'FirmwareArtifactsNotPrepared'
|
|
872
|
+
) {
|
|
873
|
+
throw err;
|
|
874
|
+
}
|
|
720
875
|
if (err instanceof HardwareError && err.errorCode === HardwareErrorCode.NetworkError) {
|
|
721
876
|
throw err;
|
|
722
877
|
}
|
|
723
878
|
throw normalizeFirmwarePreparationError(err);
|
|
724
879
|
}
|
|
725
880
|
|
|
726
|
-
if (
|
|
727
|
-
!bootloaderBinary &&
|
|
728
|
-
fwBinaryMap.length === 0 &&
|
|
729
|
-
!installItems?.length &&
|
|
730
|
-
!resourceBundles?.length &&
|
|
731
|
-
!needsRemoteResources
|
|
732
|
-
) {
|
|
881
|
+
if (!bootloaderBinary && fwBinaryMap.length === 0 && !installItems?.length) {
|
|
733
882
|
throw ERRORS.TypedError(
|
|
734
883
|
HardwareErrorCode.FirmwareUpdateDownloadFailed,
|
|
735
884
|
'No firmware to update'
|
|
736
885
|
);
|
|
737
886
|
}
|
|
738
887
|
|
|
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
888
|
return this.executeProtocolV2Update({
|
|
761
889
|
fwBinaryMap,
|
|
762
890
|
bootloaderBinary,
|
|
763
891
|
...(installItems ? { installItems } : undefined),
|
|
764
|
-
...(resourceBundles?.length ? { resourceBundles } : undefined),
|
|
765
892
|
});
|
|
766
893
|
}
|
|
767
894
|
|
|
@@ -804,6 +931,54 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
804
931
|
firmwareType: EFirmwareType,
|
|
805
932
|
features: Features
|
|
806
933
|
): Promise<ProtocolV2InstallSource[]> {
|
|
934
|
+
if (this.params.preparedPlan) {
|
|
935
|
+
const requestedTargets = new Set(
|
|
936
|
+
this.params.preparedPlan.targetsToUpdate.filter(
|
|
937
|
+
(target): target is Exclude<FirmwareUpdateV4Target, 'resource'> => target !== 'resource'
|
|
938
|
+
)
|
|
939
|
+
);
|
|
940
|
+
const installSources: ProtocolV2InstallSource[] = [];
|
|
941
|
+
const preparedTargets = new Set<Exclude<FirmwareUpdateV4Target, 'resource'>>();
|
|
942
|
+
for (const artifact of this.params.preparedPlan.artifacts) {
|
|
943
|
+
if (artifact.target !== 'resource') {
|
|
944
|
+
const target = artifact.target as Exclude<FirmwareUpdateV4Target, 'resource'>;
|
|
945
|
+
const installTarget = PROTOCOL_V2_INSTALL_TARGET_BY_UPDATE_TARGET.get(target);
|
|
946
|
+
if (
|
|
947
|
+
!requestedTargets.has(target) ||
|
|
948
|
+
artifact.role !== 'component' ||
|
|
949
|
+
artifact.container !== 'raw' ||
|
|
950
|
+
!installTarget ||
|
|
951
|
+
preparedTargets.has(target)
|
|
952
|
+
) {
|
|
953
|
+
throw ERRORS.TypedError(
|
|
954
|
+
HardwareErrorCode.RuntimeError,
|
|
955
|
+
`Protocol V2 prepared component artifact is invalid: ${artifact.artifactId}`,
|
|
956
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
installSources.push({
|
|
960
|
+
...installTarget,
|
|
961
|
+
source: await this.openProtocolV2PreparedSource(artifact.artifact),
|
|
962
|
+
});
|
|
963
|
+
preparedTargets.add(target);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
const missingTarget = Array.from(requestedTargets).find(
|
|
967
|
+
target => !preparedTargets.has(target)
|
|
968
|
+
);
|
|
969
|
+
if (missingTarget) {
|
|
970
|
+
throw ERRORS.TypedError(
|
|
971
|
+
HardwareErrorCode.RuntimeError,
|
|
972
|
+
`Protocol V2 ${missingTarget} artifact is not prepared`,
|
|
973
|
+
{
|
|
974
|
+
firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
|
|
975
|
+
artifactName: missingTarget,
|
|
976
|
+
}
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
return installSources;
|
|
980
|
+
}
|
|
981
|
+
|
|
807
982
|
const release = DataManager.getFirmwareLatestRelease(features, firmwareType);
|
|
808
983
|
if (!release) {
|
|
809
984
|
throw ERRORS.TypedError(
|
|
@@ -853,54 +1028,431 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
853
1028
|
return installSources;
|
|
854
1029
|
}
|
|
855
1030
|
|
|
856
|
-
private async
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1031
|
+
private async prepareProtocolV2LocalMemoryHost({
|
|
1032
|
+
features,
|
|
1033
|
+
firmwareType,
|
|
1034
|
+
}: {
|
|
1035
|
+
features: Features;
|
|
1036
|
+
firmwareType: EFirmwareType;
|
|
1037
|
+
}): Promise<FirmwareUpdateV4MemoryHost> {
|
|
1038
|
+
const installItems = this.buildProtocolV2InstallItems({
|
|
1039
|
+
bootloaderBinary: this.prepareBootloaderBinary(),
|
|
1040
|
+
fwBinaryMap: this.collectExplicitTargetBinaries(),
|
|
1041
|
+
});
|
|
1042
|
+
const requestedComponentTargets = new Set(
|
|
1043
|
+
(this.params.targetsToUpdate ?? []).filter(
|
|
1044
|
+
(target): target is Exclude<FirmwareUpdateV4Target, 'resource' | 'boot_resources'> =>
|
|
1045
|
+
target !== 'resource' && target !== 'boot_resources'
|
|
1046
|
+
)
|
|
1047
|
+
);
|
|
1048
|
+
const localComponentTargets = new Set(
|
|
1049
|
+
installItems.flatMap(item => {
|
|
1050
|
+
const target = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(item.targetId);
|
|
1051
|
+
return target ? [target] : [];
|
|
1052
|
+
})
|
|
1053
|
+
);
|
|
1054
|
+
const missingTarget = Array.from(requestedComponentTargets).find(
|
|
1055
|
+
target => !localComponentTargets.has(target)
|
|
1056
|
+
);
|
|
1057
|
+
if (missingTarget) {
|
|
1058
|
+
throw ERRORS.TypedError(
|
|
1059
|
+
HardwareErrorCode.RuntimeError,
|
|
1060
|
+
`Protocol V2 local update has no binary for requested target ${missingTarget}`,
|
|
1061
|
+
{
|
|
1062
|
+
firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
|
|
1063
|
+
artifactName: missingTarget,
|
|
1064
|
+
}
|
|
1065
|
+
);
|
|
865
1066
|
}
|
|
866
|
-
|
|
867
|
-
const
|
|
868
|
-
|
|
869
|
-
|
|
1067
|
+
|
|
1068
|
+
const planArtifacts: Parameters<typeof buildProtocolV2LocalFirmwareUpdatePlan>[0]['artifacts'] =
|
|
1069
|
+
[];
|
|
1070
|
+
const memoryArtifacts: FirmwareMemoryArtifact[] = [];
|
|
1071
|
+
for (const item of installItems) {
|
|
1072
|
+
const target = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(item.targetId);
|
|
1073
|
+
if (!target || item.binary.byteLength <= 0) {
|
|
1074
|
+
throw ERRORS.TypedError(
|
|
1075
|
+
HardwareErrorCode.RuntimeError,
|
|
1076
|
+
`Protocol V2 local firmware artifact is invalid: ${item.fileName}`,
|
|
1077
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
const artifactId = `component:${target}`;
|
|
1081
|
+
planArtifacts.push({
|
|
1082
|
+
artifactId,
|
|
1083
|
+
target,
|
|
1084
|
+
container: 'raw',
|
|
1085
|
+
logicalName: item.fileName,
|
|
1086
|
+
expectedSize: item.binary.byteLength,
|
|
1087
|
+
expectedSha256: bytesToHex(sha256(new Uint8Array(item.binary))),
|
|
1088
|
+
...(this.params.expectedTargetVersions?.[target]
|
|
1089
|
+
? { targetVersion: this.params.expectedTargetVersions[target] }
|
|
1090
|
+
: {}),
|
|
1091
|
+
});
|
|
1092
|
+
memoryArtifacts.push({ artifactId, binary: item.binary });
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const resourceArchive = await this.prepareProtocolV2LocalResourceArchive(
|
|
1096
|
+
this.params.resourceArchiveBinary as ArrayBuffer
|
|
870
1097
|
);
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1098
|
+
const resourceArtifactId = 'resource:archive';
|
|
1099
|
+
planArtifacts.push({
|
|
1100
|
+
artifactId: resourceArtifactId,
|
|
1101
|
+
target: 'resource',
|
|
1102
|
+
container: 'zip',
|
|
1103
|
+
logicalName: 'protocol-v2-local-resource-archive',
|
|
1104
|
+
expectedSize: resourceArchive.binary.byteLength,
|
|
1105
|
+
expectedSha256: bytesToHex(sha256(new Uint8Array(resourceArchive.binary))),
|
|
1106
|
+
});
|
|
1107
|
+
memoryArtifacts.push({
|
|
1108
|
+
artifactId: resourceArtifactId,
|
|
1109
|
+
binary: resourceArchive.binary,
|
|
1110
|
+
materializedEntries: resourceArchive.materializedEntries,
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
const plan = buildProtocolV2LocalFirmwareUpdatePlan({
|
|
1114
|
+
features,
|
|
1115
|
+
firmwareType,
|
|
1116
|
+
platform: this.params.platform,
|
|
1117
|
+
artifacts: planArtifacts,
|
|
1118
|
+
});
|
|
1119
|
+
let memoryHost: FirmwareUpdateV4MemoryHost | undefined;
|
|
1120
|
+
try {
|
|
1121
|
+
memoryHost = prepareFirmwareUpdateV4MemoryHost({
|
|
1122
|
+
sdk: {
|
|
1123
|
+
prepareFirmwareUpdatePlan,
|
|
1124
|
+
registerFirmwareUpdateHostBinding,
|
|
1125
|
+
unregisterFirmwareUpdateHostBinding,
|
|
1126
|
+
},
|
|
1127
|
+
plan,
|
|
1128
|
+
artifacts: memoryArtifacts,
|
|
1129
|
+
});
|
|
1130
|
+
const preparedPlan = validateFirmwareUpdatePreparedPlan(memoryHost.preparedPlan);
|
|
1131
|
+
assertFirmwareUpdatePreparedPlanBinding({
|
|
1132
|
+
preparedPlan,
|
|
1133
|
+
executor: 'v4',
|
|
1134
|
+
platform: this.params.platform,
|
|
1135
|
+
scopeTargets: [],
|
|
1136
|
+
bindings: [],
|
|
1137
|
+
});
|
|
1138
|
+
assertFirmwareUpdatePreparedPlanDeviceIdentity({
|
|
1139
|
+
preparedPlan,
|
|
1140
|
+
deviceIdentity: this.protocolV2ExpectedSerialNumber,
|
|
1141
|
+
deviceModel: this.getProtocolV2PreparedPlanDeviceModel(features),
|
|
1142
|
+
});
|
|
1143
|
+
const hostBinding = resolveFirmwareUpdateHostBinding(
|
|
1144
|
+
memoryHost.hostBindingGeneration,
|
|
1145
|
+
preparedPlan.preparedPlanDigest
|
|
1146
|
+
);
|
|
1147
|
+
this.params.preparedPlan = preparedPlan;
|
|
1148
|
+
this.params.targetsToUpdate = [...preparedPlan.targetsToUpdate] as FirmwareUpdateV4Target[];
|
|
1149
|
+
this.params.artifactReader = hostBinding.artifactReader;
|
|
1150
|
+
this.params.componentArtifacts = undefined;
|
|
1151
|
+
return memoryHost;
|
|
1152
|
+
} catch (error) {
|
|
1153
|
+
memoryHost?.release();
|
|
1154
|
+
throw error;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
private async prepareProtocolV2LocalResourceArchive(
|
|
1159
|
+
binary: ArrayBuffer
|
|
1160
|
+
): Promise<ProtocolV2LocalResourceArchive> {
|
|
1161
|
+
if (binary.byteLength <= 0 || binary.byteLength > PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES) {
|
|
1162
|
+
throw ERRORS.TypedError(
|
|
1163
|
+
HardwareErrorCode.RuntimeError,
|
|
1164
|
+
'Protocol V2 local resource ZIP archive size is invalid',
|
|
1165
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
const zip = await JSZip.loadAsync(binary);
|
|
1169
|
+
const zipEntries = Object.values(zip.files);
|
|
1170
|
+
if (
|
|
1171
|
+
zipEntries.some(entry => entry.unsafeOriginalName && entry.unsafeOriginalName !== entry.name)
|
|
1172
|
+
) {
|
|
1173
|
+
throw ERRORS.TypedError(
|
|
1174
|
+
HardwareErrorCode.RuntimeError,
|
|
1175
|
+
'Protocol V2 local resource ZIP contains an unsafe entry path',
|
|
1176
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
const entries = zipEntries.filter(entry => !entry.dir);
|
|
1180
|
+
if (entries.length === 0 || entries.length > PROTOCOL_V2_RESOURCE_FILE_MAX_COUNT + 1) {
|
|
1181
|
+
throw ERRORS.TypedError(
|
|
1182
|
+
HardwareErrorCode.RuntimeError,
|
|
1183
|
+
'Protocol V2 local resource ZIP entry set is invalid',
|
|
1184
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
let declaredUncompressedSize = 0;
|
|
1188
|
+
let declaredCompressedSize = 0;
|
|
1189
|
+
for (const entry of entries) {
|
|
1190
|
+
const sizes = getProtocolV2ZipEntrySizes(entry);
|
|
1191
|
+
declaredCompressedSize += sizes.compressedSize;
|
|
1192
|
+
declaredUncompressedSize += sizes.uncompressedSize;
|
|
1193
|
+
const entryLimit =
|
|
1194
|
+
entry.name === 'manifest.json'
|
|
1195
|
+
? PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES
|
|
1196
|
+
: PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES;
|
|
1197
|
+
if (
|
|
1198
|
+
sizes.uncompressedSize > entryLimit ||
|
|
1199
|
+
declaredCompressedSize > binary.byteLength ||
|
|
1200
|
+
declaredUncompressedSize >
|
|
1201
|
+
PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES + PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES
|
|
1202
|
+
) {
|
|
875
1203
|
throw ERRORS.TypedError(
|
|
876
1204
|
HardwareErrorCode.RuntimeError,
|
|
877
|
-
|
|
878
|
-
{
|
|
879
|
-
firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
|
|
880
|
-
artifactName: descriptor.name,
|
|
881
|
-
}
|
|
1205
|
+
'Protocol V2 local resource ZIP declared size exceeds the allowed limit',
|
|
1206
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
882
1207
|
);
|
|
883
1208
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1209
|
+
}
|
|
1210
|
+
const manifestEntry = zip.file('manifest.json');
|
|
1211
|
+
if (!manifestEntry) {
|
|
1212
|
+
throw ERRORS.TypedError(
|
|
1213
|
+
HardwareErrorCode.RuntimeError,
|
|
1214
|
+
'Protocol V2 local resource ZIP has no manifest.json',
|
|
1215
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
887
1216
|
);
|
|
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
1217
|
}
|
|
898
|
-
|
|
1218
|
+
const manifestBinary = await manifestEntry.async('arraybuffer');
|
|
1219
|
+
if (
|
|
1220
|
+
manifestBinary.byteLength <= 0 ||
|
|
1221
|
+
manifestBinary.byteLength > PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES
|
|
1222
|
+
) {
|
|
1223
|
+
throw ERRORS.TypedError(
|
|
1224
|
+
HardwareErrorCode.RuntimeError,
|
|
1225
|
+
'Protocol V2 local resource manifest size is invalid',
|
|
1226
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
let manifestValue: unknown;
|
|
1231
|
+
try {
|
|
1232
|
+
manifestValue = JSON.parse(new TextDecoder().decode(manifestBinary));
|
|
1233
|
+
} catch (error) {
|
|
1234
|
+
throw ERRORS.TypedError(
|
|
1235
|
+
HardwareErrorCode.RuntimeError,
|
|
1236
|
+
`Protocol V2 local resource manifest is invalid: ${String(error)}`,
|
|
1237
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
const manifest = parseProtocolV2ResourceManifest(manifestValue);
|
|
1241
|
+
const selectedFiles = selectProtocolV2ResourceManifestFiles({
|
|
1242
|
+
manifest,
|
|
1243
|
+
targetsToUpdate: this.params.targetsToUpdate ?? [],
|
|
1244
|
+
});
|
|
1245
|
+
const expectedEntryNames = new Set([
|
|
1246
|
+
'manifest.json',
|
|
1247
|
+
...selectedFiles.map(file => file.archive_path),
|
|
1248
|
+
]);
|
|
1249
|
+
if (
|
|
1250
|
+
entries.length !== expectedEntryNames.size ||
|
|
1251
|
+
entries.some(entry => !expectedEntryNames.has(entry.name))
|
|
1252
|
+
) {
|
|
1253
|
+
throw ERRORS.TypedError(
|
|
1254
|
+
HardwareErrorCode.RuntimeError,
|
|
1255
|
+
'Protocol V2 local resource ZIP contains an unexpected entry',
|
|
1256
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
let totalSize = 0;
|
|
1261
|
+
const materializedEntries: FirmwareMemoryArtifactEntry[] = [
|
|
1262
|
+
{ entryName: 'manifest.json', binary: manifestBinary },
|
|
1263
|
+
];
|
|
1264
|
+
for (const file of selectedFiles) {
|
|
1265
|
+
const entry = zip.file(file.archive_path);
|
|
1266
|
+
if (!entry) {
|
|
1267
|
+
throw ERRORS.TypedError(
|
|
1268
|
+
HardwareErrorCode.RuntimeError,
|
|
1269
|
+
`Protocol V2 local resource ZIP is missing ${file.archive_path}`,
|
|
1270
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
const { uncompressedSize } = getProtocolV2ZipEntrySizes(entry);
|
|
1274
|
+
totalSize += uncompressedSize;
|
|
1275
|
+
if (uncompressedSize !== file.size || totalSize > PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES) {
|
|
1276
|
+
throw ERRORS.TypedError(
|
|
1277
|
+
HardwareErrorCode.RuntimeError,
|
|
1278
|
+
`Protocol V2 local resource file declared size is invalid: ${file.archive_path}`,
|
|
1279
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
const fileBinary = await entry.async('arraybuffer');
|
|
1283
|
+
const digest = bytesToHex(sha256(new Uint8Array(fileBinary)));
|
|
1284
|
+
if (fileBinary.byteLength !== file.size || digest !== file.sha256.toLowerCase()) {
|
|
1285
|
+
throw ERRORS.TypedError(
|
|
1286
|
+
HardwareErrorCode.RuntimeError,
|
|
1287
|
+
`Protocol V2 local resource file does not match manifest: ${file.archive_path}`,
|
|
1288
|
+
{ firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
materializedEntries.push({ entryName: file.archive_path, binary: fileBinary });
|
|
1292
|
+
}
|
|
1293
|
+
return { binary, materializedEntries };
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
private async prepareProtocolV2ResourceSources(): Promise<ProtocolV2ResourceBundleSource[]> {
|
|
1297
|
+
const resourceRequested = this.params.targetsToUpdate?.includes('resource') ?? false;
|
|
1298
|
+
if (!resourceRequested) {
|
|
1299
|
+
return [];
|
|
1300
|
+
}
|
|
1301
|
+
const archiveSources = await this.prepareProtocolV2ResourceArchiveSources();
|
|
1302
|
+
if (archiveSources) {
|
|
1303
|
+
return archiveSources;
|
|
1304
|
+
}
|
|
1305
|
+
throw ERRORS.TypedError(
|
|
1306
|
+
HardwareErrorCode.RuntimeError,
|
|
1307
|
+
'Protocol V2 resource archive is not prepared',
|
|
1308
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
private async prepareProtocolV2ResourceArchiveSources(): Promise<
|
|
1313
|
+
ProtocolV2ResourceBundleSource[] | undefined
|
|
1314
|
+
> {
|
|
1315
|
+
const archiveArtifacts =
|
|
1316
|
+
this.params.preparedPlan?.artifacts.filter(
|
|
1317
|
+
artifact =>
|
|
1318
|
+
artifact.role === 'resourceBundle' &&
|
|
1319
|
+
artifact.target === 'resource' &&
|
|
1320
|
+
artifact.container === 'zip'
|
|
1321
|
+
) ?? [];
|
|
1322
|
+
if (archiveArtifacts.length === 0) {
|
|
1323
|
+
return undefined;
|
|
1324
|
+
}
|
|
1325
|
+
if (archiveArtifacts.length !== 1) {
|
|
1326
|
+
throw ERRORS.TypedError(
|
|
1327
|
+
HardwareErrorCode.RuntimeError,
|
|
1328
|
+
'Protocol V2 prepared plan must contain exactly one resource archive',
|
|
1329
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
const archiveArtifact = archiveArtifacts[0];
|
|
1334
|
+
const archiveSource = await this.openProtocolV2PreparedSource(archiveArtifact.artifact);
|
|
1335
|
+
if (archiveSource.size > PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES) {
|
|
1336
|
+
throw ERRORS.TypedError(
|
|
1337
|
+
HardwareErrorCode.RuntimeError,
|
|
1338
|
+
'Protocol V2 prepared resource archive exceeds the total size limit',
|
|
1339
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
let archiveBinary: ArrayBuffer;
|
|
1344
|
+
try {
|
|
1345
|
+
archiveBinary = await readFirmwareByteSourceFully(archiveSource);
|
|
1346
|
+
} finally {
|
|
1347
|
+
await archiveSource.close();
|
|
1348
|
+
}
|
|
1349
|
+
const archiveDigest = bytesToHex(sha256(new Uint8Array(archiveBinary)));
|
|
1350
|
+
if (archiveDigest !== archiveArtifact.artifact.sha256.toLowerCase()) {
|
|
1351
|
+
throw ERRORS.TypedError(
|
|
1352
|
+
HardwareErrorCode.RuntimeError,
|
|
1353
|
+
'Protocol V2 prepared resource archive does not match its approved receipt',
|
|
1354
|
+
{ firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// materializedEntries 由宿主生成,只有与获批 ZIP 的规范字节完全一致后才能使用。
|
|
1359
|
+
const verifiedArchive = await this.prepareProtocolV2LocalResourceArchive(archiveBinary);
|
|
1360
|
+
const entries = archiveArtifact.materializedEntries ?? [];
|
|
1361
|
+
const entriesByName = new Map(entries.map(entry => [entry.entryName, entry] as const));
|
|
1362
|
+
if (
|
|
1363
|
+
entries.length !== verifiedArchive.materializedEntries.length ||
|
|
1364
|
+
verifiedArchive.materializedEntries.some(entry => {
|
|
1365
|
+
const preparedEntry = entriesByName.get(entry.entryName);
|
|
1366
|
+
const digest = bytesToHex(sha256(new Uint8Array(entry.binary)));
|
|
1367
|
+
return (
|
|
1368
|
+
!preparedEntry ||
|
|
1369
|
+
preparedEntry.artifact.size !== entry.binary.byteLength ||
|
|
1370
|
+
preparedEntry.artifact.sha256.toLowerCase() !== digest
|
|
1371
|
+
);
|
|
1372
|
+
})
|
|
1373
|
+
) {
|
|
1374
|
+
throw ERRORS.TypedError(
|
|
1375
|
+
HardwareErrorCode.RuntimeError,
|
|
1376
|
+
'Protocol V2 prepared resource entries do not match the approved archive',
|
|
1377
|
+
{ firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
const verifiedEntriesByName = new Map(
|
|
1382
|
+
verifiedArchive.materializedEntries.map(entry => [entry.entryName, entry.binary] as const)
|
|
1383
|
+
);
|
|
1384
|
+
const manifestBinary = verifiedEntriesByName.get('manifest.json');
|
|
1385
|
+
if (!manifestBinary) {
|
|
1386
|
+
throw ERRORS.TypedError(
|
|
1387
|
+
HardwareErrorCode.RuntimeError,
|
|
1388
|
+
'Protocol V2 prepared resource archive has no valid manifest.json',
|
|
1389
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
let manifestValue: unknown;
|
|
1394
|
+
try {
|
|
1395
|
+
manifestValue = JSON.parse(new TextDecoder().decode(manifestBinary));
|
|
1396
|
+
} catch (error) {
|
|
899
1397
|
throw ERRORS.TypedError(
|
|
900
1398
|
HardwareErrorCode.RuntimeError,
|
|
901
|
-
`Protocol V2
|
|
1399
|
+
`Protocol V2 prepared resource manifest is invalid: ${String(error)}`,
|
|
1400
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
902
1401
|
);
|
|
903
1402
|
}
|
|
1403
|
+
const manifest = parseProtocolV2ResourceManifest(manifestValue);
|
|
1404
|
+
const selectedFiles = selectProtocolV2ResourceManifestFiles({
|
|
1405
|
+
manifest,
|
|
1406
|
+
targetsToUpdate: this.params.targetsToUpdate ?? [],
|
|
1407
|
+
});
|
|
1408
|
+
if (selectedFiles.length > PROTOCOL_V2_RESOURCE_FILE_MAX_COUNT) {
|
|
1409
|
+
throw ERRORS.TypedError(
|
|
1410
|
+
HardwareErrorCode.RuntimeError,
|
|
1411
|
+
'Protocol V2 prepared resource archive contains too many files',
|
|
1412
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
let totalSize = 0;
|
|
1417
|
+
const sources: ProtocolV2ResourceBundleSource[] = [];
|
|
1418
|
+
for (const [index, file] of selectedFiles.entries()) {
|
|
1419
|
+
const binary = verifiedEntriesByName.get(file.archive_path);
|
|
1420
|
+
if (!binary || binary.byteLength !== file.size) {
|
|
1421
|
+
throw ERRORS.TypedError(
|
|
1422
|
+
HardwareErrorCode.RuntimeError,
|
|
1423
|
+
`Protocol V2 prepared resource file does not match manifest: ${file.archive_path}`,
|
|
1424
|
+
{ firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
totalSize += binary.byteLength;
|
|
1428
|
+
if (totalSize > PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES) {
|
|
1429
|
+
throw ERRORS.TypedError(
|
|
1430
|
+
HardwareErrorCode.RuntimeError,
|
|
1431
|
+
'Protocol V2 prepared resource archive exceeds the total size limit',
|
|
1432
|
+
{ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
// 使用从获批 ZIP 中提取的规范字节,避免宿主在校验后替换 entry reader 内容。
|
|
1436
|
+
const source = await this.openProtocolV2MemorySource(binary);
|
|
1437
|
+
const header =
|
|
1438
|
+
source.size >= PROTOCOL_V2_OKPP_HEADER_SIZE
|
|
1439
|
+
? parseProtocolV2OkppHeader(
|
|
1440
|
+
new Uint8Array(await source.readAt(0, PROTOCOL_V2_OKPP_HEADER_SIZE))
|
|
1441
|
+
)
|
|
1442
|
+
: null;
|
|
1443
|
+
sources.push({
|
|
1444
|
+
name: file.original_name || `resource-${index}`,
|
|
1445
|
+
source,
|
|
1446
|
+
devicePath: file.device_path,
|
|
1447
|
+
...(header
|
|
1448
|
+
? {
|
|
1449
|
+
version: header.version,
|
|
1450
|
+
payloadHash: header.payloadHash,
|
|
1451
|
+
headerHash: header.headerHash,
|
|
1452
|
+
}
|
|
1453
|
+
: {}),
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
904
1456
|
return sources;
|
|
905
1457
|
}
|
|
906
1458
|
|
|
@@ -908,7 +1460,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
908
1460
|
try {
|
|
909
1461
|
this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
|
|
910
1462
|
const installSources = await this.prepareProtocolV2InstallSources(firmwareType, features);
|
|
911
|
-
const resourceSources = await this.prepareProtocolV2ResourceSources(
|
|
1463
|
+
const resourceSources = await this.prepareProtocolV2ResourceSources();
|
|
912
1464
|
if (installSources.length === 0 && resourceSources.length === 0) {
|
|
913
1465
|
throw ERRORS.TypedError(
|
|
914
1466
|
HardwareErrorCode.FirmwareUpdateDownloadFailed,
|
|
@@ -938,7 +1490,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
938
1490
|
for (const [target, version] of Object.entries(expected)) {
|
|
939
1491
|
if (
|
|
940
1492
|
!Array.from(PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.values()).includes(
|
|
941
|
-
target as FirmwareUpdateV4Target
|
|
1493
|
+
target as Exclude<FirmwareUpdateV4Target, 'boot_resources'>
|
|
942
1494
|
) ||
|
|
943
1495
|
typeof version !== 'string' ||
|
|
944
1496
|
!/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/u.test(version) ||
|
|
@@ -1024,13 +1576,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1024
1576
|
if (this.params.targetsToUpdate?.length) {
|
|
1025
1577
|
return [...new Set(this.params.targetsToUpdate)];
|
|
1026
1578
|
}
|
|
1579
|
+
if (this.params.preparedPlan?.targetsToUpdate.length) {
|
|
1580
|
+
return [...new Set(this.params.preparedPlan.targetsToUpdate)] as FirmwareUpdateV4Target[];
|
|
1581
|
+
}
|
|
1027
1582
|
const targets = new Set<FirmwareUpdateV4Target>();
|
|
1028
1583
|
Object.keys(this.params.componentArtifacts ?? {}).forEach(target =>
|
|
1029
1584
|
targets.add(target as FirmwareUpdateV4Target)
|
|
1030
1585
|
);
|
|
1031
|
-
if (this.params.resourceBundleArtifacts?.length || this.params.resourceFiles?.length) {
|
|
1032
|
-
targets.add('resource');
|
|
1033
|
-
}
|
|
1034
1586
|
if (this.params.bootloaderBinary) targets.add('boot');
|
|
1035
1587
|
if (this.params.applicationP1Binary) targets.add('app_v1');
|
|
1036
1588
|
if (this.params.applicationP2Binary) targets.add('app_v2');
|
|
@@ -1058,12 +1610,34 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1058
1610
|
return serialNumber || undefined;
|
|
1059
1611
|
}
|
|
1060
1612
|
|
|
1613
|
+
private getProtocolV2PreparedPlanDeviceModel(features?: Features) {
|
|
1614
|
+
const currentDeviceType = this.device.getCurrentDeviceType();
|
|
1615
|
+
const featureDeviceType = features ? getDeviceType(features) : undefined;
|
|
1616
|
+
const deviceType =
|
|
1617
|
+
currentDeviceType === EDeviceType.Pro2 || currentDeviceType === EDeviceType.Neo
|
|
1618
|
+
? currentDeviceType
|
|
1619
|
+
: featureDeviceType;
|
|
1620
|
+
return deviceType === EDeviceType.Pro2 || deviceType === EDeviceType.Neo
|
|
1621
|
+
? String(deviceType)
|
|
1622
|
+
: undefined;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
private getProtocolV2ConnectionRoute() {
|
|
1626
|
+
const descriptor = this.device.originalDescriptor;
|
|
1627
|
+
return (
|
|
1628
|
+
descriptor?.path?.trim() ||
|
|
1629
|
+
this.device.getConnectId?.()?.trim() ||
|
|
1630
|
+
descriptor?.id?.trim() ||
|
|
1631
|
+
undefined
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1061
1635
|
private assertProtocolV2DeviceInfoIdentity(deviceInfo: ProtocolV2DeviceInfo) {
|
|
1062
1636
|
assertProtocolV2ReconnectIdentity(
|
|
1063
1637
|
this.protocolV2ExpectedSerialNumber,
|
|
1064
1638
|
this.getProtocolV2SerialNumber(deviceInfo),
|
|
1065
1639
|
this.protocolV2ExpectedPath,
|
|
1066
|
-
this.
|
|
1640
|
+
this.getProtocolV2ConnectionRoute()
|
|
1067
1641
|
);
|
|
1068
1642
|
}
|
|
1069
1643
|
|
|
@@ -1083,11 +1657,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1083
1657
|
private async captureProtocolV2PhysicalIdentity() {
|
|
1084
1658
|
const deviceInfo = await this.requestProtocolV2PhysicalIdentity();
|
|
1085
1659
|
const serialNumber = this.getProtocolV2SerialNumber(deviceInfo);
|
|
1086
|
-
const path = this.
|
|
1660
|
+
const path = this.getProtocolV2ConnectionRoute();
|
|
1087
1661
|
if (this.params?.preparedPlan) {
|
|
1088
1662
|
assertFirmwareUpdatePreparedPlanDeviceIdentity({
|
|
1089
1663
|
preparedPlan: this.params.preparedPlan,
|
|
1090
1664
|
deviceIdentity: serialNumber,
|
|
1665
|
+
deviceModel: this.getProtocolV2PreparedPlanDeviceModel(this.device.features),
|
|
1091
1666
|
});
|
|
1092
1667
|
} else if (this.params?.expectedDeviceId) {
|
|
1093
1668
|
assertProtocolV2ReconnectIdentity(this.params?.expectedDeviceId, serialNumber);
|
|
@@ -1108,12 +1683,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1108
1683
|
return this.params.bootloaderBinary ?? null;
|
|
1109
1684
|
}
|
|
1110
1685
|
|
|
1111
|
-
private
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1686
|
+
private getMissingProtocolV2FirmwareTargets(installItems: ProtocolV2InstallItem[]) {
|
|
1687
|
+
const preparedTargets = new Set(
|
|
1688
|
+
installItems.flatMap(item => {
|
|
1689
|
+
const target = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(item.targetId);
|
|
1690
|
+
return target ? [target] : [];
|
|
1691
|
+
})
|
|
1116
1692
|
);
|
|
1693
|
+
return (this.params.targetsToUpdate ?? [])
|
|
1694
|
+
.filter(
|
|
1695
|
+
(target): target is Exclude<FirmwareUpdateV4Target, 'resource' | 'boot_resources'> =>
|
|
1696
|
+
target !== 'resource' && target !== 'boot_resources'
|
|
1697
|
+
)
|
|
1698
|
+
.filter(target => !preparedTargets.has(target));
|
|
1117
1699
|
}
|
|
1118
1700
|
|
|
1119
1701
|
private buildProtocolV2InstallItems({
|
|
@@ -1184,8 +1766,28 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1184
1766
|
`Missing Protocol V2 firmware component url: ${key}/${component.target}`
|
|
1185
1767
|
);
|
|
1186
1768
|
}
|
|
1769
|
+
const expectedFingerprint = normalizeProtocolV2Hex(component.fingerprint);
|
|
1770
|
+
if (
|
|
1771
|
+
!Number.isSafeInteger(component.expectedSize) ||
|
|
1772
|
+
Number(component.expectedSize) <= 0 ||
|
|
1773
|
+
!expectedFingerprint ||
|
|
1774
|
+
!/^[0-9a-f]{64}$/u.test(expectedFingerprint)
|
|
1775
|
+
) {
|
|
1776
|
+
throw ERRORS.TypedError(
|
|
1777
|
+
HardwareErrorCode.RuntimeError,
|
|
1778
|
+
`Protocol V2 firmware component integrity metadata is invalid: ${key}/${component.target}`,
|
|
1779
|
+
{ firmwareUpdateCode: 'FirmwarePlanInvalid' }
|
|
1780
|
+
);
|
|
1781
|
+
}
|
|
1187
1782
|
|
|
1188
1783
|
const { binary } = await getSysResourceBinary(component.url);
|
|
1784
|
+
if (binary.byteLength !== component.expectedSize) {
|
|
1785
|
+
throw ERRORS.TypedError(
|
|
1786
|
+
HardwareErrorCode.RuntimeError,
|
|
1787
|
+
`Protocol V2 firmware size mismatch: ${key}/${component.target}`,
|
|
1788
|
+
{ firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
|
|
1789
|
+
);
|
|
1790
|
+
}
|
|
1189
1791
|
if (!isProtocolV2FirmwareFingerprintValid(binary, component.fingerprint)) {
|
|
1190
1792
|
throw ERRORS.TypedError(
|
|
1191
1793
|
HardwareErrorCode.RuntimeError,
|
|
@@ -1208,7 +1810,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1208
1810
|
};
|
|
1209
1811
|
}
|
|
1210
1812
|
|
|
1211
|
-
private async prepareRemoteProtocolV2Binaries(
|
|
1813
|
+
private async prepareRemoteProtocolV2Binaries(
|
|
1814
|
+
firmwareType: EFirmwareType,
|
|
1815
|
+
features: Features,
|
|
1816
|
+
explicitInstallItems: ProtocolV2InstallItem[] = []
|
|
1817
|
+
) {
|
|
1212
1818
|
const release = DataManager.getFirmwareLatestRelease(features, firmwareType);
|
|
1213
1819
|
|
|
1214
1820
|
let bootloaderBinary: ArrayBuffer | null = null;
|
|
@@ -1216,6 +1822,15 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1216
1822
|
const installItems: ProtocolV2InstallItem[] = [];
|
|
1217
1823
|
|
|
1218
1824
|
if (!release) {
|
|
1825
|
+
const missingFirmwareTargets = this.getMissingProtocolV2FirmwareTargets(explicitInstallItems);
|
|
1826
|
+
if (missingFirmwareTargets.length > 0) {
|
|
1827
|
+
throw ERRORS.TypedError(
|
|
1828
|
+
HardwareErrorCode.RuntimeError,
|
|
1829
|
+
`Protocol V2 firmware release is unavailable for requested targets: ${missingFirmwareTargets.join(
|
|
1830
|
+
', '
|
|
1831
|
+
)}`
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1219
1834
|
return {
|
|
1220
1835
|
bootloaderBinary,
|
|
1221
1836
|
fwBinaryMap,
|
|
@@ -1225,6 +1840,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1225
1840
|
|
|
1226
1841
|
const entries = this.getRemoteComponentEntries(release);
|
|
1227
1842
|
const targetsToUpdate = new Set(this.params.targetsToUpdate ?? []);
|
|
1843
|
+
const explicitInstallItemByTargetId = new Map(
|
|
1844
|
+
explicitInstallItems.map(item => [item.targetId, item] as const)
|
|
1845
|
+
);
|
|
1846
|
+
const preparedTargets = new Set<FirmwareUpdateV4Target>();
|
|
1228
1847
|
|
|
1229
1848
|
for (const [key, component] of entries) {
|
|
1230
1849
|
const targetName = component.target?.toUpperCase();
|
|
@@ -1233,229 +1852,38 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1233
1852
|
? PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(target.targetId)
|
|
1234
1853
|
: undefined;
|
|
1235
1854
|
if (updateTarget && targetsToUpdate.has(updateTarget)) {
|
|
1236
|
-
const
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
binary: remoteBinary.binary,
|
|
1242
|
-
targetId: remoteBinary.targetId,
|
|
1243
|
-
kind: remoteBinary.kind,
|
|
1244
|
-
});
|
|
1855
|
+
const explicitInstallItem = explicitInstallItemByTargetId.get(target.targetId);
|
|
1856
|
+
const installItem =
|
|
1857
|
+
explicitInstallItem ?? (await this.downloadRemoteProtocolV2Component(key, component));
|
|
1858
|
+
if (installItem.kind === 'bootloader') {
|
|
1859
|
+
bootloaderBinary = installItem.binary;
|
|
1245
1860
|
} else {
|
|
1246
1861
|
const binaryEntry = {
|
|
1247
|
-
fileName:
|
|
1248
|
-
binary:
|
|
1249
|
-
targetId:
|
|
1862
|
+
fileName: installItem.fileName,
|
|
1863
|
+
binary: installItem.binary,
|
|
1864
|
+
targetId: installItem.targetId,
|
|
1250
1865
|
};
|
|
1251
1866
|
fwBinaryMap.push(binaryEntry);
|
|
1252
|
-
installItems.push({ ...binaryEntry, kind: remoteBinary.kind });
|
|
1253
1867
|
}
|
|
1868
|
+
installItems.push({ ...installItem });
|
|
1869
|
+
preparedTargets.add(updateTarget);
|
|
1254
1870
|
}
|
|
1255
1871
|
}
|
|
1256
1872
|
|
|
1257
|
-
|
|
1258
|
-
|
|
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
|
-
}
|
|
1302
|
-
files.push({
|
|
1303
|
-
name: file.name ?? file.devicePath.split('/').pop() ?? file.devicePath,
|
|
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}`);
|
|
1369
|
-
}
|
|
1370
|
-
seenPaths.add(bundle.devicePath);
|
|
1371
|
-
}
|
|
1372
|
-
return merged;
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
private prepareExplicitProtocolV2ResourceFiles(): ProtocolV2ResourceBundleBinary[] | undefined {
|
|
1376
|
-
const files = this.params.resourceFiles ?? [];
|
|
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}`
|
|
1873
|
+
const missingTarget = Array.from(targetsToUpdate).find(
|
|
1874
|
+
target => target !== 'resource' && !preparedTargets.has(target)
|
|
1438
1875
|
);
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1876
|
+
if (missingTarget) {
|
|
1877
|
+
throw ERRORS.TypedError(
|
|
1878
|
+
HardwareErrorCode.RuntimeError,
|
|
1879
|
+
`Protocol V2 release does not contain requested target ${missingTarget}`
|
|
1880
|
+
);
|
|
1443
1881
|
}
|
|
1444
|
-
return bundles;
|
|
1445
|
-
}
|
|
1446
1882
|
|
|
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
1883
|
return {
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1884
|
+
bootloaderBinary,
|
|
1885
|
+
fwBinaryMap,
|
|
1886
|
+
installItems,
|
|
1459
1887
|
};
|
|
1460
1888
|
}
|
|
1461
1889
|
|
|
@@ -1465,7 +1893,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1465
1893
|
return `vol0:/${path}`;
|
|
1466
1894
|
}
|
|
1467
1895
|
|
|
1468
|
-
private async readProtocolV2DeviceFileHeader(path: string) {
|
|
1896
|
+
private async readProtocolV2DeviceFileHeader(path: string, expectedSize?: number) {
|
|
1469
1897
|
const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
|
|
1470
1898
|
const filePath = this.getProtocolV2ResourceFilePath(path);
|
|
1471
1899
|
const pathInfoRes = await typedCall('FilesystemPathInfoQuery', 'FilesystemPathInfo', {
|
|
@@ -1476,7 +1904,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1476
1904
|
!pathInfoRes.message?.exist ||
|
|
1477
1905
|
pathInfoRes.message?.directory ||
|
|
1478
1906
|
fileSize === undefined ||
|
|
1479
|
-
fileSize < PROTOCOL_V2_OKPP_HEADER_SIZE
|
|
1907
|
+
fileSize < PROTOCOL_V2_OKPP_HEADER_SIZE ||
|
|
1908
|
+
(expectedSize !== undefined && fileSize !== expectedSize)
|
|
1480
1909
|
) {
|
|
1481
1910
|
return null;
|
|
1482
1911
|
}
|
|
@@ -1510,32 +1939,31 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1510
1939
|
return parseProtocolV2OkppHeader(headerBytes);
|
|
1511
1940
|
}
|
|
1512
1941
|
|
|
1513
|
-
/** Compare
|
|
1942
|
+
/** Compare the downloaded and installed okpkg headers before transferring a resource. */
|
|
1514
1943
|
private async isProtocolV2ResourceBundleUpToDate(
|
|
1515
1944
|
bundle: Pick<
|
|
1516
1945
|
ProtocolV2ResourceBundleSource,
|
|
1517
|
-
'name' | 'devicePath' | 'version' | 'payloadHash' | 'headerHash'
|
|
1946
|
+
'name' | 'source' | 'devicePath' | 'version' | 'payloadHash' | 'headerHash'
|
|
1518
1947
|
>
|
|
1519
1948
|
): Promise<boolean> {
|
|
1520
1949
|
if (this.params?.forcedUpdateRes) return false;
|
|
1521
|
-
if (!bundle.
|
|
1950
|
+
if (!bundle.payloadHash || !bundle.headerHash) return false;
|
|
1522
1951
|
|
|
1523
1952
|
try {
|
|
1524
|
-
const header = await this.readProtocolV2DeviceFileHeader(
|
|
1953
|
+
const header = await this.readProtocolV2DeviceFileHeader(
|
|
1954
|
+
bundle.devicePath,
|
|
1955
|
+
bundle.source.size
|
|
1956
|
+
);
|
|
1525
1957
|
if (!header) return false;
|
|
1526
1958
|
|
|
1527
1959
|
if (bundle.version) {
|
|
1528
1960
|
const cmp = compareProtocolV2Versions(header.version, bundle.version);
|
|
1529
1961
|
if (cmp === undefined || cmp !== 0) return false;
|
|
1530
1962
|
}
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
if (bundle.headerHash) {
|
|
1536
|
-
const expected = normalizeProtocolV2Hex(bundle.headerHash);
|
|
1537
|
-
if (expected && header.headerHash !== expected) return false;
|
|
1538
|
-
}
|
|
1963
|
+
const expectedPayloadHash = normalizeProtocolV2Hex(bundle.payloadHash);
|
|
1964
|
+
const expectedHeaderHash = normalizeProtocolV2Hex(bundle.headerHash);
|
|
1965
|
+
if (!expectedPayloadHash || header.payloadHash !== expectedPayloadHash) return false;
|
|
1966
|
+
if (!expectedHeaderHash || header.headerHash !== expectedHeaderHash) return false;
|
|
1539
1967
|
return true;
|
|
1540
1968
|
} catch (error) {
|
|
1541
1969
|
Log.log(`[FirmwareUpdateV4] RESC bundle ${bundle.name} header check failed: `, error);
|
|
@@ -1691,11 +2119,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1691
2119
|
resourceSources: ProtocolV2ResourceBundleSource[];
|
|
1692
2120
|
}): ProtocolV2ExecutionPhase[] {
|
|
1693
2121
|
const phases: ProtocolV2ExecutionPhase[] = [];
|
|
1694
|
-
|
|
2122
|
+
const bootResourceSources = resourceSources.filter(source =>
|
|
2123
|
+
isProtocolV2BootResourcePackagePath(source.devicePath)
|
|
2124
|
+
);
|
|
2125
|
+
const remainingResourceSources = resourceSources.filter(
|
|
2126
|
+
source => !isProtocolV2BootResourcePackagePath(source.devicePath)
|
|
2127
|
+
);
|
|
2128
|
+
// A previous interrupted run may have left a promotable staging file. Replace
|
|
2129
|
+
// it with the current approved package before any firmware-triggered reboot.
|
|
2130
|
+
if (bootResourceSources.length > 0) {
|
|
1695
2131
|
phases.push({
|
|
1696
2132
|
kind: 'resource-sync',
|
|
1697
2133
|
installSources: [],
|
|
1698
|
-
resourceSources,
|
|
2134
|
+
resourceSources: bootResourceSources,
|
|
1699
2135
|
});
|
|
1700
2136
|
}
|
|
1701
2137
|
const bootloaderSources = installSources.filter(source => source.kind === 'bootloader');
|
|
@@ -1713,6 +2149,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1713
2149
|
}
|
|
1714
2150
|
);
|
|
1715
2151
|
}
|
|
2152
|
+
if (remainingResourceSources.length > 0) {
|
|
2153
|
+
phases.push({
|
|
2154
|
+
kind: 'resource-sync',
|
|
2155
|
+
installSources: [],
|
|
2156
|
+
resourceSources: remainingResourceSources,
|
|
2157
|
+
});
|
|
2158
|
+
}
|
|
1716
2159
|
const componentSources = installSources.filter(source => source.kind !== 'bootloader');
|
|
1717
2160
|
if (componentSources.length > 0) {
|
|
1718
2161
|
phases.push({
|
|
@@ -1736,6 +2179,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1736
2179
|
installSources: ProtocolV2InstallSource[];
|
|
1737
2180
|
resourceSources: ProtocolV2ResourceBundleSource[];
|
|
1738
2181
|
}) {
|
|
2182
|
+
this.protocolV2BootResourceStagingSafe = false;
|
|
1739
2183
|
const phases = this.buildProtocolV2ExecutionPhases({
|
|
1740
2184
|
installSources,
|
|
1741
2185
|
resourceSources,
|
|
@@ -1746,9 +2190,17 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1746
2190
|
}
|
|
1747
2191
|
if (phase.kind === 'resource-sync') {
|
|
1748
2192
|
await this.enterProtocolV2BootloaderMode();
|
|
2193
|
+
if (
|
|
2194
|
+
!phase.resourceSources.some(source =>
|
|
2195
|
+
isProtocolV2BootResourcePackagePath(source.devicePath)
|
|
2196
|
+
)
|
|
2197
|
+
) {
|
|
2198
|
+
await this.ensureProtocolV2BootResourceStagingIsEmpty();
|
|
2199
|
+
}
|
|
1749
2200
|
await this.executeProtocolV2TransferPhase(phase);
|
|
1750
2201
|
} else if (phase.kind === 'bootloader-install' || phase.kind === 'component-install') {
|
|
1751
2202
|
await this.enterProtocolV2BootloaderMode();
|
|
2203
|
+
await this.ensureProtocolV2BootResourceStagingIsEmpty();
|
|
1752
2204
|
await this.executeProtocolV2TransferPhase(phase);
|
|
1753
2205
|
await this.exitProtocolV2BootloaderToNormal();
|
|
1754
2206
|
} else if (phase.kind === 'bootloader-verify') {
|
|
@@ -1762,6 +2214,35 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1762
2214
|
);
|
|
1763
2215
|
}
|
|
1764
2216
|
|
|
2217
|
+
private async ensureProtocolV2BootResourceStagingIsEmpty() {
|
|
2218
|
+
if (this.protocolV2BootResourceStagingSafe) return;
|
|
2219
|
+
const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
|
|
2220
|
+
const query = () =>
|
|
2221
|
+
typedCall('FilesystemPathInfoQuery', 'FilesystemPathInfo', {
|
|
2222
|
+
path: PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_STAGING_PATH,
|
|
2223
|
+
});
|
|
2224
|
+
const current = await query();
|
|
2225
|
+
if (current.message?.exist) {
|
|
2226
|
+
if (current.message.directory) {
|
|
2227
|
+
throw ERRORS.TypedError(
|
|
2228
|
+
HardwareErrorCode.EmmcFileWriteFirmwareError,
|
|
2229
|
+
'Protocol V2 boot resource staging path is not a file'
|
|
2230
|
+
);
|
|
2231
|
+
}
|
|
2232
|
+
await typedCall('FilesystemFileDelete', 'Success', {
|
|
2233
|
+
path: PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_STAGING_PATH,
|
|
2234
|
+
});
|
|
2235
|
+
const remaining = await query();
|
|
2236
|
+
if (remaining.message?.exist) {
|
|
2237
|
+
throw ERRORS.TypedError(
|
|
2238
|
+
HardwareErrorCode.EmmcFileWriteFirmwareError,
|
|
2239
|
+
'Protocol V2 stale boot resource staging file could not be removed'
|
|
2240
|
+
);
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
this.protocolV2BootResourceStagingSafe = true;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
1765
2246
|
private async executeProtocolV2SourceUpdate({
|
|
1766
2247
|
installSources,
|
|
1767
2248
|
resourceSources,
|
|
@@ -1779,12 +2260,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1779
2260
|
fwBinaryMap,
|
|
1780
2261
|
bootloaderBinary,
|
|
1781
2262
|
installItems,
|
|
1782
|
-
resourceBundles,
|
|
1783
2263
|
}: {
|
|
1784
2264
|
fwBinaryMap?: ProtocolV2TargetBinary[];
|
|
1785
2265
|
bootloaderBinary?: ArrayBuffer | null;
|
|
1786
2266
|
installItems?: ProtocolV2InstallItem[];
|
|
1787
|
-
resourceBundles?: ProtocolV2ResourceBundleBinary[];
|
|
1788
2267
|
}) {
|
|
1789
2268
|
const memoryInstallItems =
|
|
1790
2269
|
installItems ??
|
|
@@ -1801,16 +2280,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1801
2280
|
kind: item.kind,
|
|
1802
2281
|
}))
|
|
1803
2282
|
);
|
|
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
2283
|
return await this.executeProtocolV2Phases({
|
|
1812
2284
|
installSources,
|
|
1813
|
-
resourceSources,
|
|
2285
|
+
resourceSources: [],
|
|
1814
2286
|
});
|
|
1815
2287
|
} finally {
|
|
1816
2288
|
await this.closeProtocolV2PreparedSources();
|
|
@@ -1824,7 +2296,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1824
2296
|
let totalSize = installSources.reduce((total, item) => total + item.source.size, 0);
|
|
1825
2297
|
const resourcesToSync: ProtocolV2ResourceBundleSource[] = [];
|
|
1826
2298
|
for (const resource of resourceSources) {
|
|
1827
|
-
|
|
2299
|
+
// Early boot promotes this mounted package's staging file. Always replace any
|
|
2300
|
+
// stale staging bytes with the artifact approved by the current PreparedPlan,
|
|
2301
|
+
// even when the mounted final file itself is already current.
|
|
2302
|
+
const requiresFreshStaging = isProtocolV2BootResourcePackagePath(resource.devicePath);
|
|
2303
|
+
if (!requiresFreshStaging && (await this.isProtocolV2ResourceBundleUpToDate(resource))) {
|
|
1828
2304
|
Log.log(`[FirmwareUpdateV4] skip RESC bundle ${resource.name}; already up to date`);
|
|
1829
2305
|
} else {
|
|
1830
2306
|
resourcesToSync.push(resource);
|
|
@@ -1835,13 +2311,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1835
2311
|
this.postTipMessage(FirmwareUpdateTipMessage.StartTransferData);
|
|
1836
2312
|
let processedSize = 0;
|
|
1837
2313
|
for (const resource of resourcesToSync) {
|
|
2314
|
+
// The bootloader keeps its live resource package mounted. FatFs rejects
|
|
2315
|
+
// replacing an open file, so early boot promotes this staging file before mounting it.
|
|
2316
|
+
const writePath = resolveProtocolV2ResourceWritePath(resource.devicePath);
|
|
1838
2317
|
processedSize = await this.protocolV2SourceUpdateProcess({
|
|
1839
2318
|
source: resource.source,
|
|
1840
|
-
filePath:
|
|
2319
|
+
filePath: writePath,
|
|
1841
2320
|
processedSize,
|
|
1842
2321
|
totalSize,
|
|
1843
2322
|
});
|
|
1844
|
-
await this.verifyProtocolV2StagedFile(
|
|
2323
|
+
await this.verifyProtocolV2StagedFile(writePath, resource.source.size);
|
|
2324
|
+
if (isProtocolV2BootResourcePackagePath(resource.devicePath)) {
|
|
2325
|
+
this.protocolV2BootResourceStagingSafe = true;
|
|
2326
|
+
}
|
|
1845
2327
|
}
|
|
1846
2328
|
|
|
1847
2329
|
const stagedInstallTargets: Array<{ targetId: number; path: string }> = [];
|