@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.
Files changed (98) hide show
  1. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +404 -84
  2. package/__tests__/check-firmware-release-protocol-v2.test.ts +199 -0
  3. package/__tests__/firmware-memory-host.test.ts +112 -0
  4. package/__tests__/firmware-update/device-update-bootloader.test.ts +13 -2
  5. package/__tests__/firmware-update/firmware-host-binding.test.ts +9 -0
  6. package/__tests__/firmware-update/firmware-prepared-host-digest.test.ts +573 -0
  7. package/__tests__/firmware-update/firmware-prepared-resource-archive.test.ts +150 -0
  8. package/__tests__/firmware-update/firmware-update-plan.test.ts +232 -70
  9. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +53 -1
  10. package/__tests__/firmware-update/firmware-update-prepared-resource-executors.test.ts +326 -0
  11. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +2 -2
  12. package/__tests__/protocol-v2-resources.test.ts +148 -232
  13. package/__tests__/protocol-v2.test.ts +1236 -516
  14. package/dist/api/CheckAllFirmwareRelease.d.ts +2 -3
  15. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  16. package/dist/api/CheckBLEFirmwareRelease.d.ts +3 -2
  17. package/dist/api/CheckBLEFirmwareRelease.d.ts.map +1 -1
  18. package/dist/api/CheckBootloaderRelease.d.ts +2 -1
  19. package/dist/api/CheckBootloaderRelease.d.ts.map +1 -1
  20. package/dist/api/CheckFirmwareRelease.d.ts +3 -2
  21. package/dist/api/CheckFirmwareRelease.d.ts.map +1 -1
  22. package/dist/api/FirmwareUpdateV2.d.ts +1 -0
  23. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  24. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  25. package/dist/api/FirmwareUpdateV4.d.ts +8 -8
  26. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  27. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  28. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  29. package/dist/api/firmware/FirmwareHostBinding.d.ts +1 -1
  30. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -1
  31. package/dist/api/firmware/FirmwareMemoryHost.d.ts +22 -0
  32. package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +1 -0
  33. package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts +11 -0
  34. package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts.map +1 -0
  35. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +48 -1
  36. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
  37. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +6 -1
  38. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
  39. package/dist/api/firmware/getBinary.d.ts +0 -1
  40. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  41. package/dist/api/firmware/protocolV2Release.d.ts +33 -0
  42. package/dist/api/firmware/protocolV2Release.d.ts.map +1 -0
  43. package/dist/data-manager/DataManager.d.ts +4 -3
  44. package/dist/data-manager/DataManager.d.ts.map +1 -1
  45. package/dist/index.d.ts +108 -114
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +1633 -967
  48. package/dist/protocols/protocol-v2/resources.d.ts +10 -28
  49. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  50. package/dist/types/api/checkAllFirmwareRelease.d.ts +20 -8
  51. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  52. package/dist/types/api/checkBLEFirmwareRelease.d.ts +2 -13
  53. package/dist/types/api/checkBLEFirmwareRelease.d.ts.map +1 -1
  54. package/dist/types/api/checkBootloaderRelease.d.ts +2 -6
  55. package/dist/types/api/checkBootloaderRelease.d.ts.map +1 -1
  56. package/dist/types/api/checkFirmwareRelease.d.ts +2 -13
  57. package/dist/types/api/checkFirmwareRelease.d.ts.map +1 -1
  58. package/dist/types/api/checkFirmwareTypeAvailable.d.ts +2 -2
  59. package/dist/types/api/checkFirmwareTypeAvailable.d.ts.map +1 -1
  60. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  61. package/dist/types/api/export.d.ts +1 -1
  62. package/dist/types/api/export.d.ts.map +1 -1
  63. package/dist/types/api/firmwareUpdate.d.ts +4 -12
  64. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  65. package/dist/types/api/firmwareUpdatePlan.d.ts +2 -2
  66. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -1
  67. package/dist/types/settings.d.ts +35 -36
  68. package/dist/types/settings.d.ts.map +1 -1
  69. package/package.json +4 -4
  70. package/src/api/CheckAllFirmwareRelease.ts +108 -80
  71. package/src/api/CheckBLEFirmwareRelease.ts +37 -5
  72. package/src/api/CheckBootloaderRelease.ts +35 -3
  73. package/src/api/CheckFirmwareRelease.ts +35 -7
  74. package/src/api/FirmwareUpdateV2.ts +108 -73
  75. package/src/api/FirmwareUpdateV3.ts +101 -56
  76. package/src/api/FirmwareUpdateV4.ts +885 -403
  77. package/src/api/UploadPortfolio.ts +18 -0
  78. package/src/api/device/DeviceUpdateBootloader.ts +37 -11
  79. package/src/api/firmware/FirmwareHostBinding.ts +16 -3
  80. package/src/api/firmware/FirmwareMemoryHost.ts +142 -0
  81. package/src/api/firmware/FirmwarePreparedResourceArchive.ts +207 -0
  82. package/src/api/firmware/FirmwareUpdatePlan.ts +282 -122
  83. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +27 -3
  84. package/src/api/firmware/protocolV2Release.ts +168 -0
  85. package/src/data-manager/DataManager.ts +35 -19
  86. package/src/index.ts +10 -0
  87. package/src/protocols/protocol-v2/resources.ts +157 -341
  88. package/src/types/api/checkAllFirmwareRelease.ts +27 -12
  89. package/src/types/api/checkBLEFirmwareRelease.ts +4 -13
  90. package/src/types/api/checkBootloaderRelease.ts +2 -6
  91. package/src/types/api/checkFirmwareRelease.ts +2 -13
  92. package/src/types/api/checkFirmwareTypeAvailable.ts +2 -2
  93. package/src/types/api/deviceUpdateBootloader.ts +1 -0
  94. package/src/types/api/export.ts +6 -1
  95. package/src/types/api/firmwareUpdate.ts +12 -17
  96. package/src/types/api/firmwareUpdatePlan.ts +2 -2
  97. package/src/types/settings.ts +35 -61
  98. 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
- PROTOCOL_V2_RESOURCE_DEVICE_PATHS,
35
- buildProtocolV2ResourceUpdatePlan,
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 { openFirmwareByteSource, writeFirmwareByteSource } from './firmware/FirmwareArtifactSource';
44
- import { resolveFirmwareUpdateHostBinding } from './firmware/FirmwareHostBinding';
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 PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID = new Map<number, FirmwareUpdateV4Target>([
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 hostBinding =
553
- payload.preparedPlan || payload.componentArtifacts || payload.resourceBundleArtifacts
554
- ? resolveFirmwareUpdateHostBinding(payload.hostBindingGeneration)
555
- : undefined;
556
- if (hostBinding) {
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: payload.preparedPlan,
661
+ preparedPlan,
559
662
  executor: 'v4',
560
663
  platform: payload.platform,
561
- scopeTargets: [
562
- 'boot',
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: payload.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
- resourceFiles: payload.resourceFiles,
608
- firmwareType: payload.firmwareType,
609
- targetsToUpdate: payload.targetsToUpdate,
610
- expectedTargetVersions: payload.expectedTargetVersions,
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: hostBinding?.artifactReader ?? payload.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
- (this.params.componentArtifacts && Object.keys(this.params.componentArtifacts).length > 0) ||
663
- this.params.resourceBundleArtifacts?.length
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 hasExplicitResourceFiles = !!this.params.resourceFiles?.length;
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 needsRemoteFirmware = !this.hasExplicitProtocolV2Payload(fwBinaryMap);
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
- (needsRemoteFirmware || needsRemoteResources || needsRemoteBootResources) &&
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 || needsRemoteResources || needsRemoteBootResources) {
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
- const bootResourceFiles = await this.prepareProtocolV2BootResources();
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 prepareProtocolV2ResourceSources(
857
- firmwareType: EFirmwareType,
858
- features: Features
859
- ): Promise<ProtocolV2ResourceBundleSource[]> {
860
- const preparedArtifacts = this.params.resourceBundleArtifacts ?? [];
861
- const resourceRequested =
862
- this.params.targetsToUpdate?.includes('resource') || preparedArtifacts.length > 0;
863
- if (!resourceRequested) {
864
- return [];
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
- const release = DataManager.getFirmwareLatestRelease(features, firmwareType);
867
- const descriptors = release?.resourceBundles ?? [];
868
- const artifactByName = new Map(
869
- preparedArtifacts.map(item => [item.name, item.artifact] as const)
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 sources: ProtocolV2ResourceBundleSource[] = [];
872
- for (const descriptor of descriptors) {
873
- const artifact = artifactByName.get(descriptor.name);
874
- if (!artifact) {
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
- `Protocol V2 resource bundle ${descriptor.name} is not prepared`,
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
- const devicePath = validateProtocolV2FilesystemPath(
885
- descriptor.devicePath,
886
- 'resourceBundles[].devicePath'
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
- if (artifactByName.size > 0) {
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 release does not contain resource bundle ${artifactByName.keys().next().value}`
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(firmwareType, features);
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.device.originalDescriptor.path
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.device.originalDescriptor?.path?.trim() || undefined;
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 hasExplicitProtocolV2Payload(fwBinaryMap: ProtocolV2TargetBinary[]) {
1112
- return (
1113
- !!this.params.resourceFiles?.length ||
1114
- !!this.params.bootloaderBinary ||
1115
- fwBinaryMap.length > 0
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(firmwareType: EFirmwareType, features: Features) {
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 remoteBinary = await this.downloadRemoteProtocolV2Component(key, component);
1237
- if (remoteBinary.kind === 'bootloader') {
1238
- bootloaderBinary = remoteBinary.binary;
1239
- installItems.push({
1240
- fileName: remoteBinary.fileName,
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: remoteBinary.fileName,
1248
- binary: remoteBinary.binary,
1249
- targetId: remoteBinary.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
- return {
1258
- bootloaderBinary,
1259
- fwBinaryMap,
1260
- installItems,
1261
- };
1262
- }
1263
-
1264
- private getProtocolV2DeviceType(): EDeviceType.Pro2 | EDeviceType.Neo {
1265
- const deviceType = this.device.getCurrentDeviceType();
1266
- if (deviceType === EDeviceType.Pro2 || deviceType === EDeviceType.Neo) return deviceType;
1267
- throw new Error(`Unsupported Protocol V2 device type: ${deviceType}`);
1268
- }
1269
-
1270
- private async prepareProtocolV2BootResources(): Promise<
1271
- ProtocolV2ResourceBundleBinary[] | undefined
1272
- > {
1273
- const wantsStableResources = !!this.params.targetsToUpdate?.includes('resource');
1274
- const wantsBootResources = !!this.params.targetsToUpdate?.includes('boot_resources');
1275
- if (!wantsStableResources && !wantsBootResources) {
1276
- return undefined;
1277
- }
1278
- if (this.params.resourceFiles?.length) {
1279
- return undefined;
1280
- }
1281
- const resource = DataManager.getProtocolV2BootResources(this.getProtocolV2DeviceType());
1282
- if (!resource) {
1283
- if (wantsBootResources) {
1284
- throw new Error('Missing Protocol V2 boot resources configuration');
1285
- }
1286
- Log.debug('[FirmwareUpdateV4] no boot resources configured; continue with stable resources');
1287
- return undefined;
1288
- }
1289
-
1290
- const files: ProtocolV2ResourceBundleBinary[] = [];
1291
- for (const file of resource.files) {
1292
- const isCurrent =
1293
- !this.params.forcedUpdateRes && (await this.isProtocolV2BootResourceCurrent(file));
1294
- if (isCurrent) {
1295
- Log.log(`[FirmwareUpdateV4] boot resource unchanged, skipping ${file.devicePath}`);
1296
- } else {
1297
- Log.log(`[FirmwareUpdateV4] downloading boot resource ${file.devicePath}`);
1298
- const { binary } = await getSysResourceBinary(file.url);
1299
- if (!isProtocolV2ResourceFileValid(binary, file)) {
1300
- throw new Error(`Boot resource file verification failed: ${file.devicePath}`);
1301
- }
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
- const bundles: ProtocolV2ResourceBundleBinary[] = [];
1441
- for (const resource of plan.resources) {
1442
- bundles.push(await this.downloadProtocolV2Resource(resource));
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
- name: `${resource.type}.okpkg`,
1457
- binary,
1458
- devicePath: PROTOCOL_V2_RESOURCE_DEVICE_PATHS[resource.type],
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 a prepared okpkg header when its manifest supplies version or hash metadata. */
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.version && !bundle.payloadHash) return false;
1950
+ if (!bundle.payloadHash || !bundle.headerHash) return false;
1522
1951
 
1523
1952
  try {
1524
- const header = await this.readProtocolV2DeviceFileHeader(bundle.devicePath);
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
- if (bundle.payloadHash) {
1532
- const expected = normalizeProtocolV2Hex(bundle.payloadHash);
1533
- if (expected && header.payloadHash !== expected) return false;
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
- if (resourceSources.length > 0) {
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
- if (await this.isProtocolV2ResourceBundleUpToDate(resource)) {
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: resource.devicePath,
2319
+ filePath: writePath,
1841
2320
  processedSize,
1842
2321
  totalSize,
1843
2322
  });
1844
- await this.verifyProtocolV2StagedFile(resource.devicePath, resource.source.size);
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 }> = [];