@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
|
@@ -10,13 +10,14 @@ import {
|
|
|
10
10
|
getDeviceFirmwareVersion,
|
|
11
11
|
} from '../../utils/deviceVersionUtils';
|
|
12
12
|
|
|
13
|
+
import type { FirmwareUpdateV4Target } from '../../types/api/firmwareUpdate';
|
|
13
14
|
import type {
|
|
14
15
|
FirmwareUpdatePlan,
|
|
15
16
|
FirmwareUpdatePlanArtifact,
|
|
16
17
|
FirmwareUpdatePlanForceTarget,
|
|
17
18
|
FirmwareUpdatePlanTarget,
|
|
18
19
|
} from '../../types/api/firmwareUpdatePlan';
|
|
19
|
-
import type { Features } from '../../types';
|
|
20
|
+
import type { Features, IProtocolV2ResourceSource } from '../../types';
|
|
20
21
|
|
|
21
22
|
type FirmwareUpdatePlatform = 'native' | 'desktop' | 'ext' | 'web' | 'web-embed';
|
|
22
23
|
|
|
@@ -30,7 +31,6 @@ type ReleaseRecord = {
|
|
|
30
31
|
version?: unknown;
|
|
31
32
|
components?: unknown;
|
|
32
33
|
installOrder?: unknown;
|
|
33
|
-
resourceBundles?: unknown;
|
|
34
34
|
fingerprint?: unknown;
|
|
35
35
|
fingerprintWeb?: unknown;
|
|
36
36
|
expectedSize?: unknown;
|
|
@@ -62,6 +62,11 @@ const PROTOCOL_V2_TARGETS: Readonly<
|
|
|
62
62
|
SE04: 'se04',
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
+
const PROTOCOL_V2_FIRMWARE_UPDATE_TARGETS = new Set<FirmwareUpdateV4Target>([
|
|
66
|
+
...Object.values(PROTOCOL_V2_TARGETS),
|
|
67
|
+
'resource',
|
|
68
|
+
]);
|
|
69
|
+
|
|
65
70
|
const asRecord = (value: unknown): Record<string, unknown> | undefined =>
|
|
66
71
|
value && typeof value === 'object' && !Array.isArray(value)
|
|
67
72
|
? (value as Record<string, unknown>)
|
|
@@ -90,7 +95,7 @@ const asIntegrity = ({
|
|
|
90
95
|
}: {
|
|
91
96
|
size: unknown;
|
|
92
97
|
sha256: unknown;
|
|
93
|
-
}): Pick<FirmwareUpdatePlanArtifact, 'expectedSize' | 'expectedSha256'
|
|
98
|
+
}): Partial<Pick<FirmwareUpdatePlanArtifact, 'expectedSize' | 'expectedSha256'>> => {
|
|
94
99
|
const sha256Value =
|
|
95
100
|
typeof value === 'string' && /^[a-f0-9]{64}$/iu.test(value) ? value.toLowerCase() : undefined;
|
|
96
101
|
return {
|
|
@@ -167,6 +172,20 @@ const planError = (message: string): never => {
|
|
|
167
172
|
});
|
|
168
173
|
};
|
|
169
174
|
|
|
175
|
+
const requireArtifactIntegrity = (
|
|
176
|
+
input: Parameters<typeof asIntegrity>[0],
|
|
177
|
+
label: string
|
|
178
|
+
): Pick<FirmwareUpdatePlanArtifact, 'expectedSize' | 'expectedSha256'> => {
|
|
179
|
+
const integrity = asIntegrity(input);
|
|
180
|
+
if (integrity.expectedSize === undefined || integrity.expectedSha256 === undefined) {
|
|
181
|
+
return planError(`${label} integrity metadata is invalid`);
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
expectedSize: integrity.expectedSize,
|
|
185
|
+
expectedSha256: integrity.expectedSha256,
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
|
|
170
189
|
const FIRMWARE_UPDATE_PLAN_FORCE_TARGETS = new Set<FirmwareUpdatePlanForceTarget>([
|
|
171
190
|
'firmware',
|
|
172
191
|
'ble',
|
|
@@ -191,6 +210,28 @@ export const validateFirmwareUpdatePlanForceTargets = (
|
|
|
191
210
|
return [...value] as FirmwareUpdatePlanForceTarget[];
|
|
192
211
|
};
|
|
193
212
|
|
|
213
|
+
export const validateProtocolV2FirmwareUpdateTargets = (
|
|
214
|
+
value: unknown
|
|
215
|
+
): FirmwareUpdateV4Target[] => {
|
|
216
|
+
if (value === undefined) {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
if (!Array.isArray(value)) {
|
|
220
|
+
return planError('Protocol V2 firmware update targets are invalid');
|
|
221
|
+
}
|
|
222
|
+
const normalizedTargets = value.map(target =>
|
|
223
|
+
target === 'boot_resources' ? 'resource' : target
|
|
224
|
+
);
|
|
225
|
+
if (
|
|
226
|
+
normalizedTargets.length > PROTOCOL_V2_FIRMWARE_UPDATE_TARGETS.size ||
|
|
227
|
+
normalizedTargets.some(target => !PROTOCOL_V2_FIRMWARE_UPDATE_TARGETS.has(target)) ||
|
|
228
|
+
new Set(normalizedTargets).size !== normalizedTargets.length
|
|
229
|
+
) {
|
|
230
|
+
return planError('Protocol V2 firmware update targets are invalid');
|
|
231
|
+
}
|
|
232
|
+
return normalizedTargets as FirmwareUpdateV4Target[];
|
|
233
|
+
};
|
|
234
|
+
|
|
194
235
|
const assertExactKeys = (
|
|
195
236
|
value: Record<string, unknown>,
|
|
196
237
|
required: readonly string[],
|
|
@@ -287,8 +328,8 @@ export const assertFirmwareUpdatePlan = (value: unknown): FirmwareUpdatePlan =>
|
|
|
287
328
|
}
|
|
288
329
|
assertExactKeys(
|
|
289
330
|
artifact,
|
|
290
|
-
['artifactId', 'role', 'target', 'url', 'container'],
|
|
291
|
-
['logicalName', '
|
|
331
|
+
['artifactId', 'role', 'target', 'url', 'container', 'expectedSize', 'expectedSha256'],
|
|
332
|
+
['logicalName', 'targetVersion']
|
|
292
333
|
);
|
|
293
334
|
const artifactId = assertBoundedString(artifact.artifactId, 'artifact id', 160);
|
|
294
335
|
if (artifactIds.has(artifactId)) {
|
|
@@ -307,16 +348,12 @@ export const assertFirmwareUpdatePlan = (value: unknown): FirmwareUpdatePlan =>
|
|
|
307
348
|
if (artifact.logicalName !== undefined) {
|
|
308
349
|
assertBoundedString(artifact.logicalName, 'logical name', 256);
|
|
309
350
|
}
|
|
310
|
-
if (
|
|
311
|
-
artifact.expectedSize !== undefined &&
|
|
312
|
-
(!Number.isSafeInteger(artifact.expectedSize) || (artifact.expectedSize as number) <= 0)
|
|
313
|
-
) {
|
|
351
|
+
if (!Number.isSafeInteger(artifact.expectedSize) || (artifact.expectedSize as number) <= 0) {
|
|
314
352
|
return planError('Firmware update plan artifact size is invalid');
|
|
315
353
|
}
|
|
316
354
|
if (
|
|
317
|
-
artifact.expectedSha256 !==
|
|
318
|
-
(
|
|
319
|
-
!/^[a-f0-9]{64}$/u.test(artifact.expectedSha256))
|
|
355
|
+
typeof artifact.expectedSha256 !== 'string' ||
|
|
356
|
+
!/^[a-f0-9]{64}$/u.test(artifact.expectedSha256)
|
|
320
357
|
) {
|
|
321
358
|
return planError('Firmware update plan artifact digest is invalid');
|
|
322
359
|
}
|
|
@@ -351,13 +388,11 @@ export const assertFirmwareUpdatePlan = (value: unknown): FirmwareUpdatePlan =>
|
|
|
351
388
|
plan.targetsToUpdate.some(target =>
|
|
352
389
|
legacyOnlyTargets.has(target as FirmwareUpdatePlanTarget)
|
|
353
390
|
)) ||
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
// must always carry a real identity. Degraded v2 plans are bound to the live
|
|
357
|
-
// device at install time instead.
|
|
391
|
+
// V2 仅在 bootloader 恢复时允许缺少身份;V4 设备在尚未写入序列号时也可升级。
|
|
392
|
+
// 这类 Plan 仍通过设备型号、目标、工件摘要与 preparedPlanDigest 进行约束。
|
|
358
393
|
(plan.artifacts.length > 0 &&
|
|
359
394
|
(plan.platform === 'native' || plan.platform === 'desktop') &&
|
|
360
|
-
plan.executor
|
|
395
|
+
plan.executor === 'v3' &&
|
|
361
396
|
plan.deviceIdentity === 'unavailable')
|
|
362
397
|
) {
|
|
363
398
|
return planError('Firmware update plan executor contract is invalid');
|
|
@@ -405,7 +440,7 @@ const selectResourceUrl = ({
|
|
|
405
440
|
|
|
406
441
|
const getExecutor = (features: Features): FirmwareUpdatePlan['executor'] => {
|
|
407
442
|
const deviceType = getDeviceType(features);
|
|
408
|
-
if (deviceType === EDeviceType.Pro2) {
|
|
443
|
+
if (deviceType === EDeviceType.Pro2 || deviceType === EDeviceType.Neo) {
|
|
409
444
|
return 'v4';
|
|
410
445
|
}
|
|
411
446
|
if (
|
|
@@ -421,10 +456,10 @@ const buildProtocolV2Artifacts = (
|
|
|
421
456
|
release: ReleaseRecord,
|
|
422
457
|
{
|
|
423
458
|
includeComponents = true,
|
|
424
|
-
|
|
459
|
+
componentTargets: selectedComponentTargets,
|
|
425
460
|
}: {
|
|
426
461
|
includeComponents?: boolean;
|
|
427
|
-
|
|
462
|
+
componentTargets?: ReadonlySet<FirmwareUpdatePlanTarget>;
|
|
428
463
|
} = {}
|
|
429
464
|
): {
|
|
430
465
|
artifacts: FirmwareUpdatePlanArtifact[];
|
|
@@ -445,12 +480,21 @@ const buildProtocolV2Artifacts = (
|
|
|
445
480
|
includeComponents && components
|
|
446
481
|
? [...installOrder, ...Object.keys(components).filter(key => !installOrder.includes(key))]
|
|
447
482
|
: [];
|
|
483
|
+
const selectedComponentKeys = selectedComponentTargets
|
|
484
|
+
? componentKeys.filter(key => {
|
|
485
|
+
const component = asRecord(components?.[key]);
|
|
486
|
+
const targetName = asString(component?.target)?.toUpperCase();
|
|
487
|
+
const target = targetName ? PROTOCOL_V2_TARGETS[targetName] : undefined;
|
|
488
|
+
return !!target && selectedComponentTargets.has(target);
|
|
489
|
+
})
|
|
490
|
+
: componentKeys;
|
|
448
491
|
const artifacts: FirmwareUpdatePlanArtifact[] = [];
|
|
449
492
|
const targets: FirmwareUpdatePlanTarget[] = [];
|
|
450
|
-
const
|
|
451
|
-
for (const key of
|
|
493
|
+
const componentTargetSet = new Set<FirmwareUpdatePlanTarget>();
|
|
494
|
+
for (const key of selectedComponentKeys) {
|
|
452
495
|
const component = asRecord(components?.[key]);
|
|
453
496
|
const targetName = asString(component?.target)?.toUpperCase();
|
|
497
|
+
const target = targetName ? PROTOCOL_V2_TARGETS[targetName] : undefined;
|
|
454
498
|
if (targetName === 'ROMLOADER') {
|
|
455
499
|
throw ERRORS.TypedError(
|
|
456
500
|
HardwareErrorCode.RuntimeError,
|
|
@@ -458,7 +502,6 @@ const buildProtocolV2Artifacts = (
|
|
|
458
502
|
{ firmwareUpdateCode: 'FirmwarePlanInvalid' }
|
|
459
503
|
);
|
|
460
504
|
}
|
|
461
|
-
const target = targetName ? PROTOCOL_V2_TARGETS[targetName] : undefined;
|
|
462
505
|
if (!component || !target) {
|
|
463
506
|
throw ERRORS.TypedError(
|
|
464
507
|
HardwareErrorCode.RuntimeError,
|
|
@@ -466,14 +509,18 @@ const buildProtocolV2Artifacts = (
|
|
|
466
509
|
{ firmwareUpdateCode: 'FirmwarePlanInvalid' }
|
|
467
510
|
);
|
|
468
511
|
}
|
|
469
|
-
if (
|
|
512
|
+
if (componentTargetSet.has(target)) {
|
|
470
513
|
throw ERRORS.TypedError(
|
|
471
514
|
HardwareErrorCode.RuntimeError,
|
|
472
515
|
`Protocol V2 component ${key} duplicates target ${target}`,
|
|
473
516
|
{ firmwareUpdateCode: 'FirmwarePlanInvalid' }
|
|
474
517
|
);
|
|
475
518
|
}
|
|
476
|
-
|
|
519
|
+
componentTargetSet.add(target);
|
|
520
|
+
const integrity = requireArtifactIntegrity(
|
|
521
|
+
{ size: component.expectedSize, sha256: component.fingerprint },
|
|
522
|
+
`Protocol V2 component ${key}`
|
|
523
|
+
);
|
|
477
524
|
artifacts.push({
|
|
478
525
|
artifactId: `component:${target}`,
|
|
479
526
|
role: 'component',
|
|
@@ -481,52 +528,12 @@ const buildProtocolV2Artifacts = (
|
|
|
481
528
|
url: assertArtifactUrl(component.url, `Protocol V2 component ${key}`),
|
|
482
529
|
container: 'raw',
|
|
483
530
|
logicalName: key,
|
|
484
|
-
...
|
|
485
|
-
size: component.expectedSize,
|
|
486
|
-
sha256: component.fingerprint,
|
|
487
|
-
}),
|
|
531
|
+
...integrity,
|
|
488
532
|
...(asVersion(component.version) ? { targetVersion: asVersion(component.version) } : {}),
|
|
489
533
|
});
|
|
490
534
|
targets.push(target);
|
|
491
535
|
}
|
|
492
536
|
|
|
493
|
-
const bundles =
|
|
494
|
-
includeResources && Array.isArray(release.resourceBundles) ? release.resourceBundles : [];
|
|
495
|
-
const resourceBundleNames = new Set<string>();
|
|
496
|
-
for (const value of bundles) {
|
|
497
|
-
const bundle = asRecord(value);
|
|
498
|
-
const name = asString(bundle?.name);
|
|
499
|
-
if (!bundle || !name) {
|
|
500
|
-
throw ERRORS.TypedError(
|
|
501
|
-
HardwareErrorCode.RuntimeError,
|
|
502
|
-
'Protocol V2 resource bundle is invalid',
|
|
503
|
-
{ firmwareUpdateCode: 'FirmwarePlanInvalid' }
|
|
504
|
-
);
|
|
505
|
-
}
|
|
506
|
-
if (resourceBundleNames.has(name)) {
|
|
507
|
-
throw ERRORS.TypedError(
|
|
508
|
-
HardwareErrorCode.RuntimeError,
|
|
509
|
-
`Protocol V2 resource bundle duplicates name ${name}`,
|
|
510
|
-
{ firmwareUpdateCode: 'FirmwarePlanInvalid' }
|
|
511
|
-
);
|
|
512
|
-
}
|
|
513
|
-
resourceBundleNames.add(name);
|
|
514
|
-
artifacts.push({
|
|
515
|
-
artifactId: `resourceBundle:${name}`,
|
|
516
|
-
role: 'resourceBundle',
|
|
517
|
-
target: 'resource',
|
|
518
|
-
url: assertArtifactUrl(bundle.url, `Protocol V2 resource bundle ${name}`),
|
|
519
|
-
container: 'raw',
|
|
520
|
-
logicalName: name,
|
|
521
|
-
...asIntegrity({
|
|
522
|
-
size: bundle.expectedSize,
|
|
523
|
-
sha256: bundle.fingerprint,
|
|
524
|
-
}),
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
if (bundles.length > 0) {
|
|
528
|
-
targets.push('resource');
|
|
529
|
-
}
|
|
530
537
|
return { artifacts, targets: [...new Set(targets)] };
|
|
531
538
|
};
|
|
532
539
|
|
|
@@ -587,6 +594,176 @@ const assertForcedTargetsRepresented = ({
|
|
|
587
594
|
}
|
|
588
595
|
};
|
|
589
596
|
|
|
597
|
+
const finalizeFirmwareUpdatePlan = ({
|
|
598
|
+
features,
|
|
599
|
+
firmwareType,
|
|
600
|
+
platform,
|
|
601
|
+
artifacts,
|
|
602
|
+
targetsToUpdate,
|
|
603
|
+
}: {
|
|
604
|
+
features: Features;
|
|
605
|
+
firmwareType: EFirmwareType;
|
|
606
|
+
platform: FirmwareUpdatePlatform;
|
|
607
|
+
artifacts: FirmwareUpdatePlanArtifact[];
|
|
608
|
+
targetsToUpdate: FirmwareUpdatePlanTarget[];
|
|
609
|
+
}): FirmwareUpdatePlan => {
|
|
610
|
+
const executor = getExecutor(features);
|
|
611
|
+
const bootloaderMode = resolveDeviceBootloaderMode(features);
|
|
612
|
+
// 'unavailable' is the reserved degraded-identity sentinel; a device-reported
|
|
613
|
+
// serial must never be able to collide with it.
|
|
614
|
+
const reportedIdentity = getDeviceUUID(features);
|
|
615
|
+
const deviceIdentity = reportedIdentity === 'unavailable' ? '' : reportedIdentity;
|
|
616
|
+
// Classic 系列仅在 bootloader 恢复时允许缺少身份;部分尚未完成产线写号的
|
|
617
|
+
// Pro2/Neo(V4)在正常模式下也可能没有序列号,因此允许使用降级身份。
|
|
618
|
+
const allowsUnavailableIdentity = (bootloaderMode && executor === 'v2') || executor === 'v4';
|
|
619
|
+
if (
|
|
620
|
+
artifacts.length > 0 &&
|
|
621
|
+
(platform === 'native' || platform === 'desktop') &&
|
|
622
|
+
!deviceIdentity &&
|
|
623
|
+
!allowsUnavailableIdentity
|
|
624
|
+
) {
|
|
625
|
+
throw ERRORS.TypedError(
|
|
626
|
+
HardwareErrorCode.RuntimeError,
|
|
627
|
+
'Prepared firmware update requires a stable device identity',
|
|
628
|
+
{ firmwareUpdateCode: 'FirmwarePlanInvalid' }
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
const planWithoutDigest: Omit<FirmwareUpdatePlan, 'planDigest'> = {
|
|
632
|
+
schemaVersion: 2,
|
|
633
|
+
executor,
|
|
634
|
+
deviceIdentity: deviceIdentity || 'unavailable',
|
|
635
|
+
deviceModel: String(getDeviceType(features)),
|
|
636
|
+
firmwareType,
|
|
637
|
+
platform,
|
|
638
|
+
artifacts,
|
|
639
|
+
targetsToUpdate,
|
|
640
|
+
};
|
|
641
|
+
return assertFirmwareUpdatePlan({
|
|
642
|
+
...planWithoutDigest,
|
|
643
|
+
planDigest: digestFirmwareUpdatePlan(planWithoutDigest),
|
|
644
|
+
});
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
export type ProtocolV2LocalFirmwareUpdatePlanArtifact = {
|
|
648
|
+
artifactId: string;
|
|
649
|
+
target: Exclude<FirmwareUpdateV4Target, 'boot_resources'>;
|
|
650
|
+
container: 'raw' | 'zip';
|
|
651
|
+
logicalName: string;
|
|
652
|
+
expectedSize: number;
|
|
653
|
+
expectedSha256: string;
|
|
654
|
+
targetVersion?: string;
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
export const buildProtocolV2LocalFirmwareUpdatePlan = ({
|
|
658
|
+
features,
|
|
659
|
+
firmwareType,
|
|
660
|
+
platform,
|
|
661
|
+
artifacts,
|
|
662
|
+
}: {
|
|
663
|
+
features: Features;
|
|
664
|
+
firmwareType: EFirmwareType;
|
|
665
|
+
platform: FirmwareUpdatePlatform;
|
|
666
|
+
artifacts: ProtocolV2LocalFirmwareUpdatePlanArtifact[];
|
|
667
|
+
}): FirmwareUpdatePlan => {
|
|
668
|
+
if (artifacts.length === 0) {
|
|
669
|
+
return planError('Protocol V2 local firmware plan has no artifacts');
|
|
670
|
+
}
|
|
671
|
+
const plan = finalizeFirmwareUpdatePlan({
|
|
672
|
+
features,
|
|
673
|
+
firmwareType,
|
|
674
|
+
platform,
|
|
675
|
+
artifacts: artifacts.map(artifact => ({
|
|
676
|
+
...artifact,
|
|
677
|
+
role: artifact.target === 'resource' ? 'resourceBundle' : 'component',
|
|
678
|
+
url: `https://local-firmware.invalid/${encodeURIComponent(artifact.artifactId)}`,
|
|
679
|
+
})),
|
|
680
|
+
targetsToUpdate: artifacts.map(artifact => artifact.target),
|
|
681
|
+
});
|
|
682
|
+
if (plan.executor !== 'v4') {
|
|
683
|
+
return planError('Protocol V2 local firmware plan requires executor v4');
|
|
684
|
+
}
|
|
685
|
+
return plan;
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
export const buildProtocolV2FirmwareUpdatePlan = ({
|
|
689
|
+
features,
|
|
690
|
+
firmwareType,
|
|
691
|
+
platform,
|
|
692
|
+
release,
|
|
693
|
+
targetsToUpdate,
|
|
694
|
+
forceUpdateTargets,
|
|
695
|
+
resourceArchive,
|
|
696
|
+
}: {
|
|
697
|
+
features: Features;
|
|
698
|
+
firmwareType: EFirmwareType;
|
|
699
|
+
platform: FirmwareUpdatePlatform;
|
|
700
|
+
release: ReleaseRecord | undefined;
|
|
701
|
+
targetsToUpdate: readonly FirmwareUpdateV4Target[];
|
|
702
|
+
forceUpdateTargets?: FirmwareUpdatePlanForceTarget[];
|
|
703
|
+
resourceArchive: IProtocolV2ResourceSource | undefined;
|
|
704
|
+
}): FirmwareUpdatePlan => {
|
|
705
|
+
const validatedForceTargets = validateFirmwareUpdatePlanForceTargets(forceUpdateTargets);
|
|
706
|
+
if (validatedForceTargets.some(target => target === 'ble' || target === 'bootloader')) {
|
|
707
|
+
planError('Protocol V2 does not support forced BLE or legacy bootloader targets');
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const requestedComponentTargets = new Set<FirmwareUpdatePlanTarget>(
|
|
711
|
+
targetsToUpdate.filter(target => target !== 'resource')
|
|
712
|
+
);
|
|
713
|
+
if (
|
|
714
|
+
getDeviceType(features) === EDeviceType.Neo &&
|
|
715
|
+
(requestedComponentTargets.has('se03') || requestedComponentTargets.has('se04'))
|
|
716
|
+
) {
|
|
717
|
+
planError('Neo does not support Protocol V2 targets se03 or se04');
|
|
718
|
+
}
|
|
719
|
+
const protocolV2 = buildProtocolV2Artifacts(release ?? {}, {
|
|
720
|
+
includeComponents: requestedComponentTargets.size > 0,
|
|
721
|
+
componentTargets: requestedComponentTargets,
|
|
722
|
+
});
|
|
723
|
+
const resourceRequested = targetsToUpdate.includes('resource');
|
|
724
|
+
if (resourceRequested) {
|
|
725
|
+
const archive = resourceArchive;
|
|
726
|
+
if (!archive) {
|
|
727
|
+
return planError('Protocol V2 resource target has no resource archive');
|
|
728
|
+
}
|
|
729
|
+
const integrity = requireArtifactIntegrity(
|
|
730
|
+
{ size: archive.archiveSize, sha256: archive.archiveSha256 },
|
|
731
|
+
'Protocol V2 resource archive'
|
|
732
|
+
);
|
|
733
|
+
protocolV2.artifacts.push({
|
|
734
|
+
artifactId: 'resource:archive',
|
|
735
|
+
role: 'resourceBundle',
|
|
736
|
+
target: 'resource',
|
|
737
|
+
url: assertArtifactUrl(archive.archiveUrl, 'Protocol V2 resource archive'),
|
|
738
|
+
container: 'zip',
|
|
739
|
+
logicalName: 'protocol-v2-resource-archive',
|
|
740
|
+
...integrity,
|
|
741
|
+
});
|
|
742
|
+
protocolV2.targets.push('resource');
|
|
743
|
+
}
|
|
744
|
+
const representedTargets = new Set(protocolV2.targets);
|
|
745
|
+
const missingTarget = Array.from(requestedComponentTargets).find(
|
|
746
|
+
target => !representedTargets.has(target)
|
|
747
|
+
);
|
|
748
|
+
if (missingTarget) {
|
|
749
|
+
planError(`Protocol V2 release does not contain requested target ${missingTarget}`);
|
|
750
|
+
}
|
|
751
|
+
if (validatedForceTargets.includes('firmware') && protocolV2.targets.length === 0) {
|
|
752
|
+
planError('Forced firmware update target firmware is not represented by the plan');
|
|
753
|
+
}
|
|
754
|
+
if (validatedForceTargets.includes('resource') && !resourceArchive) {
|
|
755
|
+
planError('Forced firmware update target resource has no Protocol V2 resource archive');
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return finalizeFirmwareUpdatePlan({
|
|
759
|
+
features,
|
|
760
|
+
firmwareType,
|
|
761
|
+
platform,
|
|
762
|
+
artifacts: protocolV2.artifacts,
|
|
763
|
+
targetsToUpdate: protocolV2.targets,
|
|
764
|
+
});
|
|
765
|
+
};
|
|
766
|
+
|
|
590
767
|
export const buildFirmwareUpdatePlan = ({
|
|
591
768
|
features,
|
|
592
769
|
firmwareType,
|
|
@@ -623,7 +800,11 @@ export const buildFirmwareUpdatePlan = ({
|
|
|
623
800
|
!!asRelease(release);
|
|
624
801
|
const shouldUpdateFirmware =
|
|
625
802
|
isUpgrade(firmware) || isRecoveryInstall(firmware) || forcedTargets.has('firmware');
|
|
626
|
-
|
|
803
|
+
// V2 cannot write legacy resources once the device is already in bootloader
|
|
804
|
+
// recovery. Do not approve an artifact that its executor cannot apply.
|
|
805
|
+
const shouldUpdateResource =
|
|
806
|
+
!(executor === 'v2' && bootloaderMode) &&
|
|
807
|
+
(isUpgrade(firmware) || forcedTargets.has('resource'));
|
|
627
808
|
|
|
628
809
|
if (
|
|
629
810
|
executor === 'v4' &&
|
|
@@ -635,25 +816,29 @@ export const buildFirmwareUpdatePlan = ({
|
|
|
635
816
|
if (executor === 'v4' && (shouldUpdateFirmware || shouldUpdateResource)) {
|
|
636
817
|
const protocolV2 = buildProtocolV2Artifacts(firmwareRelease ?? {}, {
|
|
637
818
|
includeComponents: shouldUpdateFirmware,
|
|
638
|
-
includeResources: shouldUpdateResource,
|
|
639
819
|
});
|
|
640
820
|
artifacts = protocolV2.artifacts;
|
|
641
821
|
targetsToUpdate = protocolV2.targets;
|
|
642
822
|
} else {
|
|
643
823
|
if (isUpgrade(bootloader) || forcedTargets.has('bootloader')) {
|
|
824
|
+
const bootloaderRelease = asRelease(bootloader);
|
|
825
|
+
const integrity = requireArtifactIntegrity(
|
|
826
|
+
{
|
|
827
|
+
size: bootloaderRelease?.bootloaderExpectedSize,
|
|
828
|
+
sha256: bootloaderRelease?.bootloaderFingerprint,
|
|
829
|
+
},
|
|
830
|
+
'Bootloader release'
|
|
831
|
+
);
|
|
644
832
|
artifacts.push({
|
|
645
833
|
artifactId: 'bootloader',
|
|
646
834
|
role: 'bootloader',
|
|
647
835
|
target: 'bootloader',
|
|
648
|
-
url: assertArtifactUrl(
|
|
836
|
+
url: assertArtifactUrl(bootloaderRelease?.bootloaderResource, 'Bootloader release'),
|
|
649
837
|
container: 'raw',
|
|
650
|
-
...
|
|
651
|
-
|
|
652
|
-
sha256: asRelease(bootloader)?.bootloaderFingerprint,
|
|
653
|
-
}),
|
|
654
|
-
...(asVersion(asRelease(bootloader)?.bootloaderVersion)
|
|
838
|
+
...integrity,
|
|
839
|
+
...(asVersion(bootloaderRelease?.bootloaderVersion)
|
|
655
840
|
? {
|
|
656
|
-
targetVersion: asVersion(
|
|
841
|
+
targetVersion: asVersion(bootloaderRelease?.bootloaderVersion),
|
|
657
842
|
}
|
|
658
843
|
: {}),
|
|
659
844
|
});
|
|
@@ -665,16 +850,17 @@ export const buildFirmwareUpdatePlan = ({
|
|
|
665
850
|
});
|
|
666
851
|
}
|
|
667
852
|
if (shouldUpdateFirmware) {
|
|
853
|
+
const integrity = requireArtifactIntegrity(
|
|
854
|
+
{ size: firmwareRelease.expectedSize, sha256: firmwareRelease.fingerprint },
|
|
855
|
+
'Firmware release'
|
|
856
|
+
);
|
|
668
857
|
artifacts.push({
|
|
669
858
|
artifactId: 'firmware',
|
|
670
859
|
role: 'firmware',
|
|
671
860
|
target: 'firmware',
|
|
672
861
|
url: assertArtifactUrl(firmwareRelease.url, 'Firmware release'),
|
|
673
862
|
container: 'raw',
|
|
674
|
-
...
|
|
675
|
-
size: firmwareRelease.expectedSize,
|
|
676
|
-
sha256: firmwareRelease.fingerprint,
|
|
677
|
-
}),
|
|
863
|
+
...integrity,
|
|
678
864
|
...(asVersion(firmwareRelease.version)
|
|
679
865
|
? { targetVersion: asVersion(firmwareRelease.version) }
|
|
680
866
|
: {}),
|
|
@@ -687,13 +873,8 @@ export const buildFirmwareUpdatePlan = ({
|
|
|
687
873
|
platform,
|
|
688
874
|
});
|
|
689
875
|
if (resourceUrl) {
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
role: 'resource',
|
|
693
|
-
target: 'resource',
|
|
694
|
-
url: assertArtifactUrl(resourceUrl, 'Firmware resource release'),
|
|
695
|
-
container: 'zip',
|
|
696
|
-
...asIntegrity({
|
|
876
|
+
const integrity = requireArtifactIntegrity(
|
|
877
|
+
{
|
|
697
878
|
size:
|
|
698
879
|
resourceUrl === asString(firmwareRelease.fullResource)
|
|
699
880
|
? firmwareRelease.fullResourceExpectedSize
|
|
@@ -702,23 +883,36 @@ export const buildFirmwareUpdatePlan = ({
|
|
|
702
883
|
resourceUrl === asString(firmwareRelease.fullResource)
|
|
703
884
|
? firmwareRelease.fullResourceFingerprint
|
|
704
885
|
: firmwareRelease.resourceFingerprint,
|
|
705
|
-
}
|
|
886
|
+
},
|
|
887
|
+
'Legacy resource archive'
|
|
888
|
+
);
|
|
889
|
+
artifacts.push({
|
|
890
|
+
artifactId: 'resource',
|
|
891
|
+
role: 'resource',
|
|
892
|
+
target: 'resource',
|
|
893
|
+
url: assertArtifactUrl(resourceUrl, 'Firmware resource release'),
|
|
894
|
+
container: 'zip',
|
|
895
|
+
...integrity,
|
|
706
896
|
});
|
|
707
897
|
}
|
|
708
898
|
}
|
|
709
899
|
}
|
|
710
900
|
if (isUpgrade(ble) || isRecoveryInstall(ble) || forcedTargets.has('ble')) {
|
|
711
901
|
const bleRelease = asRelease(ble);
|
|
902
|
+
const integrity = requireArtifactIntegrity(
|
|
903
|
+
{
|
|
904
|
+
size: bleRelease?.expectedSize,
|
|
905
|
+
sha256: bleRelease?.fingerprintWeb ?? bleRelease?.fingerprint,
|
|
906
|
+
},
|
|
907
|
+
'BLE release'
|
|
908
|
+
);
|
|
712
909
|
artifacts.push({
|
|
713
910
|
artifactId: 'ble',
|
|
714
911
|
role: 'ble',
|
|
715
912
|
target: 'ble',
|
|
716
913
|
url: assertArtifactUrl(bleRelease?.webUpdate ?? bleRelease?.url, 'BLE release'),
|
|
717
914
|
container: 'raw',
|
|
718
|
-
...
|
|
719
|
-
size: bleRelease?.expectedSize,
|
|
720
|
-
sha256: bleRelease?.fingerprintWeb ?? bleRelease?.fingerprint,
|
|
721
|
-
}),
|
|
915
|
+
...integrity,
|
|
722
916
|
...(asVersion(bleRelease?.version)
|
|
723
917
|
? { targetVersion: asVersion(bleRelease?.version) }
|
|
724
918
|
: {}),
|
|
@@ -727,46 +921,25 @@ export const buildFirmwareUpdatePlan = ({
|
|
|
727
921
|
targetsToUpdate = [...new Set(artifacts.map(artifact => artifact.target))];
|
|
728
922
|
}
|
|
729
923
|
|
|
924
|
+
if (
|
|
925
|
+
executor !== 'v4' &&
|
|
926
|
+
targetsToUpdate.includes('resource') &&
|
|
927
|
+
!targetsToUpdate.includes('firmware')
|
|
928
|
+
) {
|
|
929
|
+
planError('Legacy resource updates require a firmware target');
|
|
930
|
+
}
|
|
931
|
+
|
|
730
932
|
assertForcedTargetsRepresented({
|
|
731
933
|
executor,
|
|
732
934
|
forcedTargets: validatedForceTargets,
|
|
733
935
|
artifacts,
|
|
734
936
|
targetsToUpdate,
|
|
735
937
|
});
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
// serial must never be able to collide with it.
|
|
739
|
-
const reportedIdentity = getDeviceUUID(features);
|
|
740
|
-
const deviceIdentity = reportedIdentity === 'unavailable' ? '' : reportedIdentity;
|
|
741
|
-
// Bootloader-mode classic-family devices (executor v2, e.g. classic1s) cannot report
|
|
742
|
-
// a serial number, so their recovery plans fall back to the degraded 'unavailable'
|
|
743
|
-
// identity. Binding is then enforced against the live device at install time instead
|
|
744
|
-
// (assertFirmwareUpdatePreparedPlanDeviceIdentity), and plan integrity by the digest.
|
|
745
|
-
// Pro (v3) and Pro2 (v4) report a serial even in bootloader mode and stay strict.
|
|
746
|
-
if (
|
|
747
|
-
artifacts.length > 0 &&
|
|
748
|
-
(platform === 'native' || platform === 'desktop') &&
|
|
749
|
-
!deviceIdentity &&
|
|
750
|
-
!(bootloaderMode && executor === 'v2')
|
|
751
|
-
) {
|
|
752
|
-
throw ERRORS.TypedError(
|
|
753
|
-
HardwareErrorCode.RuntimeError,
|
|
754
|
-
'Prepared firmware update requires a stable device identity',
|
|
755
|
-
{ firmwareUpdateCode: 'FirmwarePlanInvalid' }
|
|
756
|
-
);
|
|
757
|
-
}
|
|
758
|
-
const planWithoutDigest: Omit<FirmwareUpdatePlan, 'planDigest'> = {
|
|
759
|
-
schemaVersion: 2,
|
|
760
|
-
executor,
|
|
761
|
-
deviceIdentity: deviceIdentity || 'unavailable',
|
|
762
|
-
deviceModel: String(getDeviceType(features)),
|
|
938
|
+
return finalizeFirmwareUpdatePlan({
|
|
939
|
+
features,
|
|
763
940
|
firmwareType,
|
|
764
941
|
platform,
|
|
765
942
|
artifacts,
|
|
766
943
|
targetsToUpdate,
|
|
767
|
-
};
|
|
768
|
-
return assertFirmwareUpdatePlan({
|
|
769
|
-
...planWithoutDigest,
|
|
770
|
-
planDigest: digestFirmwareUpdatePlan(planWithoutDigest),
|
|
771
944
|
});
|
|
772
945
|
};
|