@onekeyfe/hd-core 1.2.0-alpha.96 → 1.2.0-alpha.97

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 (89) hide show
  1. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +84 -404
  2. package/__tests__/firmware-update/device-update-bootloader.test.ts +2 -6
  3. package/__tests__/firmware-update/firmware-host-binding.test.ts +0 -9
  4. package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +112 -0
  5. package/__tests__/firmware-update/firmware-update-plan.test.ts +70 -131
  6. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +1 -53
  7. package/__tests__/protocol-v2-resources.test.ts +232 -148
  8. package/__tests__/protocol-v2.test.ts +517 -1106
  9. package/dist/api/CheckAllFirmwareRelease.d.ts +3 -2
  10. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  11. package/dist/api/CheckBLEFirmwareRelease.d.ts +2 -3
  12. package/dist/api/CheckBLEFirmwareRelease.d.ts.map +1 -1
  13. package/dist/api/CheckBootloaderRelease.d.ts +1 -2
  14. package/dist/api/CheckBootloaderRelease.d.ts.map +1 -1
  15. package/dist/api/CheckFirmwareRelease.d.ts +2 -3
  16. package/dist/api/CheckFirmwareRelease.d.ts.map +1 -1
  17. package/dist/api/FirmwareUpdateV2.d.ts +0 -1
  18. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  19. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  20. package/dist/api/FirmwareUpdateV4.d.ts +8 -6
  21. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  22. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  23. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  24. package/dist/api/firmware/FirmwareHostBinding.d.ts +1 -1
  25. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -1
  26. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  27. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +1 -48
  28. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
  29. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
  30. package/dist/api/firmware/getBinary.d.ts +1 -0
  31. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  32. package/dist/data-manager/DataManager.d.ts +3 -4
  33. package/dist/data-manager/DataManager.d.ts.map +1 -1
  34. package/dist/index.d.ts +111 -103
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +849 -1313
  37. package/dist/protocols/protocol-v2/resources.d.ts +28 -10
  38. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  39. package/dist/types/api/checkAllFirmwareRelease.d.ts +8 -20
  40. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  41. package/dist/types/api/checkBLEFirmwareRelease.d.ts +13 -2
  42. package/dist/types/api/checkBLEFirmwareRelease.d.ts.map +1 -1
  43. package/dist/types/api/checkBootloaderRelease.d.ts +6 -2
  44. package/dist/types/api/checkBootloaderRelease.d.ts.map +1 -1
  45. package/dist/types/api/checkFirmwareRelease.d.ts +13 -2
  46. package/dist/types/api/checkFirmwareRelease.d.ts.map +1 -1
  47. package/dist/types/api/checkFirmwareTypeAvailable.d.ts +2 -2
  48. package/dist/types/api/checkFirmwareTypeAvailable.d.ts.map +1 -1
  49. package/dist/types/api/export.d.ts +1 -1
  50. package/dist/types/api/export.d.ts.map +1 -1
  51. package/dist/types/api/firmwareUpdate.d.ts +11 -3
  52. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  53. package/dist/types/settings.d.ts +36 -35
  54. package/dist/types/settings.d.ts.map +1 -1
  55. package/package.json +4 -4
  56. package/src/api/CheckAllFirmwareRelease.ts +80 -108
  57. package/src/api/CheckBLEFirmwareRelease.ts +5 -37
  58. package/src/api/CheckBootloaderRelease.ts +3 -35
  59. package/src/api/CheckFirmwareRelease.ts +7 -35
  60. package/src/api/FirmwareUpdateV2.ts +3 -15
  61. package/src/api/FirmwareUpdateV3.ts +7 -29
  62. package/src/api/FirmwareUpdateV4.ts +407 -824
  63. package/src/api/UploadPortfolio.ts +0 -18
  64. package/src/api/device/DeviceUpdateBootloader.ts +5 -26
  65. package/src/api/firmware/FirmwareHostBinding.ts +3 -16
  66. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +2 -0
  67. package/src/api/firmware/FirmwareUpdatePlan.ts +88 -226
  68. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +3 -10
  69. package/src/data-manager/DataManager.ts +19 -35
  70. package/src/index.ts +0 -10
  71. package/src/protocols/protocol-v2/resources.ts +341 -157
  72. package/src/types/api/checkAllFirmwareRelease.ts +12 -27
  73. package/src/types/api/checkBLEFirmwareRelease.ts +13 -4
  74. package/src/types/api/checkBootloaderRelease.ts +6 -2
  75. package/src/types/api/checkFirmwareRelease.ts +13 -2
  76. package/src/types/api/checkFirmwareTypeAvailable.ts +2 -2
  77. package/src/types/api/export.ts +1 -6
  78. package/src/types/api/firmwareUpdate.ts +16 -10
  79. package/src/types/settings.ts +61 -35
  80. package/src/utils/deviceFeaturesUtils.ts +2 -2
  81. package/__tests__/check-firmware-release-protocol-v2.test.ts +0 -199
  82. package/__tests__/firmware-memory-host.test.ts +0 -112
  83. package/__tests__/firmware-update/firmware-prepared-host-digest.test.ts +0 -469
  84. package/dist/api/firmware/FirmwareMemoryHost.d.ts +0 -22
  85. package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +0 -1
  86. package/dist/api/firmware/protocolV2Release.d.ts +0 -33
  87. package/dist/api/firmware/protocolV2Release.d.ts.map +0 -1
  88. package/src/api/firmware/FirmwareMemoryHost.ts +0 -142
  89. package/src/api/firmware/protocolV2Release.ts +0 -168
@@ -1,5 +1,4 @@
1
1
  import { EDeviceType, ERRORS, HardwareError, HardwareErrorCode, wait } from '@onekeyfe/hd-shared';
2
- import JSZip from 'jszip';
3
2
  import {
4
3
  DeviceRebootType,
5
4
  PROTOCOL_V2_BLE_FILE_CHUNK_SIZE,
@@ -10,6 +9,7 @@ import {
10
9
  import { sha256 } from '@noble/hashes/sha256';
11
10
 
12
11
  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,31 +31,21 @@ import {
31
31
  } from '../protocols/protocol-v2';
32
32
  import { requestProtocolV2DeviceInfo } from '../protocols/protocol-v2/features';
33
33
  import {
34
- parseProtocolV2ResourceManifest,
35
- selectProtocolV2ResourceManifestFiles,
34
+ PROTOCOL_V2_RESOURCE_DEVICE_PATHS,
35
+ buildProtocolV2ResourceUpdatePlan,
36
+ isProtocolV2ResourceFileValid,
37
+ readProtocolV2ResourceInventory,
36
38
  } from '../protocols/protocol-v2/resources';
37
39
  import {
38
40
  getProtocolV2UnknownErrorText,
39
41
  isProtocolV2DeviceDisconnectedError,
40
42
  } from './protocol-v2/helpers';
41
- import {
42
- openFirmwareByteSource,
43
- readFirmwareByteSourceFully,
44
- writeFirmwareByteSource,
45
- } from './firmware/FirmwareArtifactSource';
46
- import {
47
- registerFirmwareUpdateHostBinding,
48
- resolveFirmwareUpdateHostBinding,
49
- unregisterFirmwareUpdateHostBinding,
50
- } from './firmware/FirmwareHostBinding';
43
+ import { openFirmwareByteSource, writeFirmwareByteSource } from './firmware/FirmwareArtifactSource';
44
+ import { resolveFirmwareUpdateHostBinding } from './firmware/FirmwareHostBinding';
51
45
  import {
52
46
  assertFirmwareUpdatePreparedPlanBinding,
53
47
  assertFirmwareUpdatePreparedPlanDeviceIdentity,
54
- prepareFirmwareUpdatePlan,
55
- validateFirmwareUpdatePreparedPlan,
56
48
  } from './firmware/FirmwareUpdatePreparedPlan';
57
- import { prepareFirmwareUpdateV4MemoryHost } from './firmware/FirmwareMemoryHost';
58
- import { buildProtocolV2LocalFirmwareUpdatePlan } from './firmware/FirmwareUpdatePlan';
59
49
 
60
50
  import type {
61
51
  FirmwareArtifactReference,
@@ -69,14 +59,11 @@ import type {
69
59
  Features,
70
60
  IFirmwareReleaseInfo,
71
61
  IProtocolV2FirmwareComponent,
62
+ IProtocolV2Resource,
63
+ IProtocolV2ResourceFile,
72
64
  IVersionArray,
73
65
  } from '../types';
74
66
  import type { FirmwareByteSource } from './firmware/FirmwareArtifactSource';
75
- import type {
76
- FirmwareMemoryArtifact,
77
- FirmwareMemoryArtifactEntry,
78
- FirmwareUpdateV4MemoryHost,
79
- } from './firmware/FirmwareMemoryHost';
80
67
 
81
68
  const Log = getLogger(LoggerNames.Method);
82
69
 
@@ -108,34 +95,9 @@ const PROTOCOL_V2_OKPP_HEADER_SIZE = 0x52a0;
108
95
  const PROTOCOL_V2_OKPP_PAYLOAD_HASH_OFFSET = 0x200;
109
96
  const PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET = 0x240;
110
97
  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;
114
98
 
115
99
  const PROTOCOL_V2_NEO_UNSUPPORTED_TARGETS = new Set<FirmwareUpdateV4Target>(['se03', 'se04']);
116
100
 
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
-
139
101
  export function assertProtocolV2FirmwareTargetsSupported(
140
102
  deviceType: EDeviceType | string | undefined,
141
103
  params: FirmwareUpdateV4Params
@@ -186,6 +148,12 @@ type ProtocolV2TargetBinary = { fileName: string; binary: ArrayBuffer; targetId:
186
148
  type ProtocolV2InstallItem = ProtocolV2TargetBinary & {
187
149
  kind: ProtocolV2RemoteComponentTarget['kind'];
188
150
  };
151
+ /** Protocol V2 resource file written independently to devicePath through FileWrite. */
152
+ type ProtocolV2ResourceBundleBinary = {
153
+ name: string;
154
+ binary: ArrayBuffer;
155
+ devicePath: string;
156
+ };
189
157
 
190
158
  type ProtocolV2RemoteComponentBinary = ProtocolV2RemoteComponentTarget & {
191
159
  binary: ArrayBuffer;
@@ -213,18 +181,6 @@ type ProtocolV2ResourceBundleSource = {
213
181
  headerHash?: string;
214
182
  };
215
183
 
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
-
228
184
  type ProtocolV2ExecutionPhaseKind =
229
185
  | 'resource-sync'
230
186
  | 'bootloader-install'
@@ -296,20 +252,7 @@ const PROTOCOL_V2_FIRMWARE_STAGING_PATHS = new Set(
296
252
  )
297
253
  );
298
254
 
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 resolveProtocolV2ResourceWritePath = (devicePath: string) => {
303
- const normalizedPath = devicePath.replace(/^vol0:(?!\/)/i, 'vol0:/').toLowerCase();
304
- return normalizedPath === PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH
305
- ? PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_STAGING_PATH
306
- : devicePath;
307
- };
308
-
309
- const PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID = new Map<
310
- number,
311
- Exclude<FirmwareUpdateV4Target, 'boot_resources'>
312
- >([
255
+ const PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID = new Map<number, FirmwareUpdateV4Target>([
313
256
  [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_BOOTLOADER, 'boot'],
314
257
  [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P1, 'app_v1'],
315
258
  [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P2, 'app_v2'],
@@ -320,19 +263,6 @@ const PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID = new Map<
320
263
  [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE04, 'se04'],
321
264
  ]);
322
265
 
323
- const PROTOCOL_V2_INSTALL_TARGET_BY_UPDATE_TARGET = new Map<
324
- Exclude<FirmwareUpdateV4Target, 'resource'>,
325
- ProtocolV2RemoteComponentTarget
326
- >(
327
- Object.values(PROTOCOL_V2_REMOTE_COMPONENT_TARGETS).map(target => [
328
- PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(target.targetId) as Exclude<
329
- FirmwareUpdateV4Target,
330
- 'resource'
331
- >,
332
- target,
333
- ])
334
- );
335
-
336
266
  const PROTOCOL_V2_ROMLOADER_UNSUPPORTED_MESSAGE =
337
267
  'FW_MGMT_TARGET_ROMLOADER is not accepted by the current Pro2 bootloader update request. Flash romloader with the loader-specific flow instead of firmwareUpdateV4.';
338
268
 
@@ -604,11 +534,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
604
534
  { name: 'se02Binary', type: 'buffer' },
605
535
  { name: 'se03Binary', type: 'buffer' },
606
536
  { name: 'se04Binary', type: 'buffer' },
607
- { name: 'resourceArchiveBinary', type: 'buffer' },
608
537
  { name: 'firmwareType', type: 'string' },
609
538
  { name: 'targetsToUpdate', type: 'array', allowEmpty: true },
610
539
  { name: 'platform', type: 'string' },
611
540
  { name: 'expectedDeviceId', type: 'string' },
541
+ { name: 'resourceFiles', type: 'array', allowEmpty: true },
612
542
  ]);
613
543
  if (
614
544
  payload.expectedDeviceId !== undefined &&
@@ -619,101 +549,50 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
619
549
  'Protocol V2 expected device identity is invalid'
620
550
  );
621
551
  }
622
- const preparedPlan = payload.preparedPlan
623
- ? validateFirmwareUpdatePreparedPlan(payload.preparedPlan)
624
- : undefined;
625
- const hasLocalArtifacts = [
626
- payload.bootloaderBinary,
627
- payload.romloaderBinary,
628
- payload.applicationP1Binary,
629
- payload.applicationP2Binary,
630
- payload.coprocessorBinary,
631
- payload.se01Binary,
632
- payload.se02Binary,
633
- payload.se03Binary,
634
- payload.se04Binary,
635
- payload.resourceArchiveBinary,
636
- ].some(Boolean);
637
- if (preparedPlan && hasLocalArtifacts) {
638
- throw ERRORS.TypedError(
639
- HardwareErrorCode.CallMethodInvalidParameter,
640
- 'Prepared firmware plans cannot be combined with legacy or local firmware inputs'
641
- );
642
- }
643
- let { artifactReader } = payload;
644
- if (preparedPlan) {
645
- artifactReader = resolveFirmwareUpdateHostBinding(
646
- payload.hostBindingGeneration,
647
- preparedPlan.preparedPlanDigest
648
- ).artifactReader;
649
- } else if (payload.hostBindingGeneration !== undefined) {
650
- artifactReader = resolveFirmwareUpdateHostBinding(
651
- payload.hostBindingGeneration
652
- ).artifactReader;
653
- }
654
- if (preparedPlan) {
552
+ const hostBinding =
553
+ payload.preparedPlan || payload.componentArtifacts || payload.resourceBundleArtifacts
554
+ ? resolveFirmwareUpdateHostBinding(payload.hostBindingGeneration)
555
+ : undefined;
556
+ if (hostBinding) {
655
557
  assertFirmwareUpdatePreparedPlanBinding({
656
- preparedPlan,
558
+ preparedPlan: payload.preparedPlan,
657
559
  executor: 'v4',
658
560
  platform: payload.platform,
659
- scopeTargets: [],
660
- bindings: [],
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
+ ],
661
591
  });
662
- if (payload.componentArtifacts) {
663
- const componentBindings: Array<{
664
- target: Exclude<FirmwareUpdateV4Target, 'resource'>;
665
- artifact: FirmwareArtifactReference;
666
- }> = Object.entries(payload.componentArtifacts).flatMap(([target, artifact]) =>
667
- artifact
668
- ? [
669
- {
670
- target: target as Exclude<FirmwareUpdateV4Target, 'resource'>,
671
- artifact: artifact as FirmwareArtifactReference,
672
- },
673
- ]
674
- : []
675
- );
676
- assertFirmwareUpdatePreparedPlanBinding({
677
- preparedPlan,
678
- executor: 'v4',
679
- platform: payload.platform,
680
- scopeTargets: componentBindings.map(binding => binding.target),
681
- bindings: componentBindings,
682
- });
683
- }
684
592
  }
685
593
 
686
- const preparedExpectedTargetVersions = preparedPlan?.artifacts.reduce<
687
- NonNullable<FirmwareUpdateV4Params['expectedTargetVersions']>
688
- >((result, artifact) => {
689
- if (
690
- artifact.role === 'component' &&
691
- artifact.target !== 'resource' &&
692
- artifact.targetVersion &&
693
- PROTOCOL_V2_INSTALL_TARGET_BY_UPDATE_TARGET.has(
694
- artifact.target as Exclude<FirmwareUpdateV4Target, 'resource'>
695
- )
696
- ) {
697
- result[artifact.target as FirmwareUpdateV4Target] = artifact.targetVersion;
698
- }
699
- return result;
700
- }, {});
701
- const localTargetsToUpdate = payload.targetsToUpdate?.map((target: FirmwareUpdateV4Target) =>
702
- target === 'boot_resources' ? 'resource' : target
703
- );
704
- if (
705
- payload.resourceArchiveBinary &&
706
- localTargetsToUpdate &&
707
- !localTargetsToUpdate.includes('resource')
708
- ) {
709
- localTargetsToUpdate.push('resource');
710
- }
711
- // 本地 ZIP 本身就是明确的资源升级请求,不要求调用方重复声明 target。
712
- const resolvedLocalTargetsToUpdate =
713
- localTargetsToUpdate ?? (payload.resourceArchiveBinary ? ['resource'] : undefined);
714
-
715
594
  this.params = {
716
- preparedPlan,
595
+ preparedPlan: payload.preparedPlan,
717
596
  chunkSize: payload.chunkSize,
718
597
  forcedUpdateRes: payload.forcedUpdateRes,
719
598
  bootloaderBinary: payload.bootloaderBinary,
@@ -725,18 +604,15 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
725
604
  se02Binary: payload.se02Binary,
726
605
  se03Binary: payload.se03Binary,
727
606
  se04Binary: payload.se04Binary,
728
- resourceArchiveBinary: payload.resourceArchiveBinary,
729
- firmwareType: preparedPlan?.firmwareType ?? payload.firmwareType,
730
- targetsToUpdate: preparedPlan
731
- ? ([...preparedPlan.targetsToUpdate] as FirmwareUpdateV4Target[])
732
- : resolvedLocalTargetsToUpdate,
733
- expectedTargetVersions: preparedPlan
734
- ? preparedExpectedTargetVersions
735
- : payload.expectedTargetVersions,
607
+ resourceFiles: payload.resourceFiles,
608
+ firmwareType: payload.firmwareType,
609
+ targetsToUpdate: payload.targetsToUpdate,
610
+ expectedTargetVersions: payload.expectedTargetVersions,
736
611
  platform: payload.platform,
737
612
  expectedDeviceId: payload.expectedDeviceId,
738
- artifactReader,
739
- componentArtifacts: preparedPlan ? undefined : payload.componentArtifacts,
613
+ artifactReader: hostBinding?.artifactReader ?? payload.artifactReader,
614
+ componentArtifacts: payload.componentArtifacts,
615
+ resourceBundleArtifacts: payload.resourceBundleArtifacts,
740
616
  };
741
617
  }
742
618
 
@@ -783,55 +659,30 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
783
659
  this.validateExpectedTargetVersions();
784
660
 
785
661
  if (
786
- !this.params.preparedPlan &&
787
- this.params.resourceArchiveBinary &&
788
- this.params.targetsToUpdate?.includes('resource')
662
+ (this.params.componentArtifacts && Object.keys(this.params.componentArtifacts).length > 0) ||
663
+ this.params.resourceBundleArtifacts?.length
789
664
  ) {
790
- const localMemoryHost = await this.prepareProtocolV2LocalMemoryHost({
791
- features: deviceFeatures,
792
- firmwareType,
793
- });
794
- try {
795
- return await this.runProtocolV2PreparedArtifacts(deviceFeatures, firmwareType);
796
- } finally {
797
- localMemoryHost.release();
798
- }
799
- }
800
-
801
- const hasPreparedComponentArtifacts = Object.values(this.params.componentArtifacts ?? {}).some(
802
- Boolean
803
- );
804
- if (this.params.preparedPlan || hasPreparedComponentArtifacts) {
805
665
  return this.runProtocolV2PreparedArtifacts(deviceFeatures, firmwareType);
806
666
  }
807
- const wantsResources = !!this.params.targetsToUpdate?.includes('resource');
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);
808
673
 
809
674
  let fwBinaryMap: ProtocolV2TargetBinary[] = [];
810
675
  let bootloaderBinary: ArrayBuffer | null = null;
811
676
  let installItems: ProtocolV2InstallItem[] | undefined;
677
+ let resourceBundles: ProtocolV2ResourceBundleBinary[] | undefined;
812
678
  try {
813
679
  this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
680
+ resourceBundles = this.prepareExplicitProtocolV2ResourceFiles();
814
681
  fwBinaryMap = this.collectExplicitTargetBinaries();
815
682
  bootloaderBinary = this.prepareBootloaderBinary();
816
- const explicitInstallItems = this.buildProtocolV2InstallItems({
817
- bootloaderBinary,
818
- fwBinaryMap,
819
- });
820
- const missingFirmwareTargets = this.getMissingProtocolV2FirmwareTargets(explicitInstallItems);
821
- const needsRemoteFirmware = this.params.targetsToUpdate?.length
822
- ? missingFirmwareTargets.length > 0
823
- : explicitInstallItems.length === 0;
824
- if (wantsResources) {
825
- throw ERRORS.TypedError(
826
- HardwareErrorCode.RuntimeError,
827
- 'Protocol V2 resource archive must be provided through a local or external PreparedPlan',
828
- {
829
- firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
830
- }
831
- );
832
- }
683
+ const needsRemoteFirmware = !this.hasExplicitProtocolV2Payload(fwBinaryMap);
833
684
  if (
834
- needsRemoteFirmware &&
685
+ (needsRemoteFirmware || needsRemoteResources || needsRemoteBootResources) &&
835
686
  (this.params.artifactReader ||
836
687
  DataManager.getSettings('firmwareManifestMode') === 'external-only')
837
688
  ) {
@@ -843,47 +694,74 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
843
694
  }
844
695
  );
845
696
  }
846
- if (needsRemoteFirmware) {
697
+ if (needsRemoteFirmware || needsRemoteResources || needsRemoteBootResources) {
847
698
  // Remote updates must use a freshly fetched config before any reboot or file write.
848
- await DataManager.forceReloadData();
699
+ await DataManager.forceReloadData({
700
+ requireResources: needsRemoteResources || needsRemoteBootResources,
701
+ });
849
702
  }
850
703
  if (needsRemoteFirmware) {
851
704
  const remoteBinaries = await this.prepareRemoteProtocolV2Binaries(
852
705
  firmwareType,
853
- deviceFeatures,
854
- explicitInstallItems
706
+ deviceFeatures
855
707
  );
856
708
  bootloaderBinary = remoteBinaries.bootloaderBinary;
857
709
  fwBinaryMap = remoteBinaries.fwBinaryMap;
858
710
  installItems = remoteBinaries.installItems;
859
711
  }
860
- this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
861
- } catch (err) {
862
- if (
863
- typeof err === 'object' &&
864
- err !== null &&
865
- 'params' in err &&
866
- (err as HardwareError).params?.firmwareUpdateCode === 'FirmwareArtifactsNotPrepared'
867
- ) {
868
- throw err;
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);
869
718
  }
719
+ } catch (err) {
870
720
  if (err instanceof HardwareError && err.errorCode === HardwareErrorCode.NetworkError) {
871
721
  throw err;
872
722
  }
873
723
  throw normalizeFirmwarePreparationError(err);
874
724
  }
875
725
 
876
- if (!bootloaderBinary && fwBinaryMap.length === 0 && !installItems?.length) {
726
+ if (
727
+ !bootloaderBinary &&
728
+ fwBinaryMap.length === 0 &&
729
+ !installItems?.length &&
730
+ !resourceBundles?.length &&
731
+ !needsRemoteResources
732
+ ) {
877
733
  throw ERRORS.TypedError(
878
734
  HardwareErrorCode.FirmwareUpdateDownloadFailed,
879
735
  'No firmware to update'
880
736
  );
881
737
  }
882
738
 
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
+
883
760
  return this.executeProtocolV2Update({
884
761
  fwBinaryMap,
885
762
  bootloaderBinary,
886
763
  ...(installItems ? { installItems } : undefined),
764
+ ...(resourceBundles?.length ? { resourceBundles } : undefined),
887
765
  });
888
766
  }
889
767
 
@@ -926,54 +804,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
926
804
  firmwareType: EFirmwareType,
927
805
  features: Features
928
806
  ): Promise<ProtocolV2InstallSource[]> {
929
- if (this.params.preparedPlan) {
930
- const requestedTargets = new Set(
931
- this.params.preparedPlan.targetsToUpdate.filter(
932
- (target): target is Exclude<FirmwareUpdateV4Target, 'resource'> => target !== 'resource'
933
- )
934
- );
935
- const installSources: ProtocolV2InstallSource[] = [];
936
- const preparedTargets = new Set<Exclude<FirmwareUpdateV4Target, 'resource'>>();
937
- for (const artifact of this.params.preparedPlan.artifacts) {
938
- if (artifact.target !== 'resource') {
939
- const target = artifact.target as Exclude<FirmwareUpdateV4Target, 'resource'>;
940
- const installTarget = PROTOCOL_V2_INSTALL_TARGET_BY_UPDATE_TARGET.get(target);
941
- if (
942
- !requestedTargets.has(target) ||
943
- artifact.role !== 'component' ||
944
- artifact.container !== 'raw' ||
945
- !installTarget ||
946
- preparedTargets.has(target)
947
- ) {
948
- throw ERRORS.TypedError(
949
- HardwareErrorCode.RuntimeError,
950
- `Protocol V2 prepared component artifact is invalid: ${artifact.artifactId}`,
951
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
952
- );
953
- }
954
- installSources.push({
955
- ...installTarget,
956
- source: await this.openProtocolV2PreparedSource(artifact.artifact),
957
- });
958
- preparedTargets.add(target);
959
- }
960
- }
961
- const missingTarget = Array.from(requestedTargets).find(
962
- target => !preparedTargets.has(target)
963
- );
964
- if (missingTarget) {
965
- throw ERRORS.TypedError(
966
- HardwareErrorCode.RuntimeError,
967
- `Protocol V2 ${missingTarget} artifact is not prepared`,
968
- {
969
- firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
970
- artifactName: missingTarget,
971
- }
972
- );
973
- }
974
- return installSources;
975
- }
976
-
977
807
  const release = DataManager.getFirmwareLatestRelease(features, firmwareType);
978
808
  if (!release) {
979
809
  throw ERRORS.TypedError(
@@ -1023,430 +853,53 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1023
853
  return installSources;
1024
854
  }
1025
855
 
1026
- private async prepareProtocolV2LocalMemoryHost({
1027
- features,
1028
- firmwareType,
1029
- }: {
1030
- features: Features;
1031
- firmwareType: EFirmwareType;
1032
- }): Promise<FirmwareUpdateV4MemoryHost> {
1033
- const installItems = this.buildProtocolV2InstallItems({
1034
- bootloaderBinary: this.prepareBootloaderBinary(),
1035
- fwBinaryMap: this.collectExplicitTargetBinaries(),
1036
- });
1037
- const requestedComponentTargets = new Set(
1038
- (this.params.targetsToUpdate ?? []).filter(
1039
- (target): target is Exclude<FirmwareUpdateV4Target, 'resource' | 'boot_resources'> =>
1040
- target !== 'resource' && target !== 'boot_resources'
1041
- )
1042
- );
1043
- const localComponentTargets = new Set(
1044
- installItems.flatMap(item => {
1045
- const target = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(item.targetId);
1046
- return target ? [target] : [];
1047
- })
1048
- );
1049
- const missingTarget = Array.from(requestedComponentTargets).find(
1050
- target => !localComponentTargets.has(target)
1051
- );
1052
- if (missingTarget) {
1053
- throw ERRORS.TypedError(
1054
- HardwareErrorCode.RuntimeError,
1055
- `Protocol V2 local update has no binary for requested target ${missingTarget}`,
1056
- {
1057
- firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
1058
- artifactName: missingTarget,
1059
- }
1060
- );
1061
- }
1062
-
1063
- const planArtifacts: Parameters<typeof buildProtocolV2LocalFirmwareUpdatePlan>[0]['artifacts'] =
1064
- [];
1065
- const memoryArtifacts: FirmwareMemoryArtifact[] = [];
1066
- for (const item of installItems) {
1067
- const target = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(item.targetId);
1068
- if (!target || item.binary.byteLength <= 0) {
1069
- throw ERRORS.TypedError(
1070
- HardwareErrorCode.RuntimeError,
1071
- `Protocol V2 local firmware artifact is invalid: ${item.fileName}`,
1072
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1073
- );
1074
- }
1075
- const artifactId = `component:${target}`;
1076
- planArtifacts.push({
1077
- artifactId,
1078
- target,
1079
- container: 'raw',
1080
- logicalName: item.fileName,
1081
- expectedSize: item.binary.byteLength,
1082
- expectedSha256: bytesToHex(sha256(new Uint8Array(item.binary))),
1083
- ...(this.params.expectedTargetVersions?.[target]
1084
- ? { targetVersion: this.params.expectedTargetVersions[target] }
1085
- : {}),
1086
- });
1087
- memoryArtifacts.push({ artifactId, binary: item.binary });
1088
- }
1089
-
1090
- const resourceArchive = await this.prepareProtocolV2LocalResourceArchive(
1091
- this.params.resourceArchiveBinary as ArrayBuffer
1092
- );
1093
- const resourceArtifactId = 'resource:archive';
1094
- planArtifacts.push({
1095
- artifactId: resourceArtifactId,
1096
- target: 'resource',
1097
- container: 'zip',
1098
- logicalName: 'protocol-v2-local-resource-archive',
1099
- expectedSize: resourceArchive.binary.byteLength,
1100
- expectedSha256: bytesToHex(sha256(new Uint8Array(resourceArchive.binary))),
1101
- });
1102
- memoryArtifacts.push({
1103
- artifactId: resourceArtifactId,
1104
- binary: resourceArchive.binary,
1105
- materializedEntries: resourceArchive.materializedEntries,
1106
- });
1107
-
1108
- const plan = buildProtocolV2LocalFirmwareUpdatePlan({
1109
- features,
1110
- firmwareType,
1111
- platform: this.params.platform,
1112
- artifacts: planArtifacts,
1113
- });
1114
- let memoryHost: FirmwareUpdateV4MemoryHost | undefined;
1115
- try {
1116
- memoryHost = prepareFirmwareUpdateV4MemoryHost({
1117
- sdk: {
1118
- prepareFirmwareUpdatePlan,
1119
- registerFirmwareUpdateHostBinding,
1120
- unregisterFirmwareUpdateHostBinding,
1121
- },
1122
- plan,
1123
- artifacts: memoryArtifacts,
1124
- });
1125
- const preparedPlan = validateFirmwareUpdatePreparedPlan(memoryHost.preparedPlan);
1126
- assertFirmwareUpdatePreparedPlanBinding({
1127
- preparedPlan,
1128
- executor: 'v4',
1129
- platform: this.params.platform,
1130
- scopeTargets: [],
1131
- bindings: [],
1132
- });
1133
- assertFirmwareUpdatePreparedPlanDeviceIdentity({
1134
- preparedPlan,
1135
- deviceIdentity: this.protocolV2ExpectedSerialNumber,
1136
- deviceModel: this.getProtocolV2PreparedPlanDeviceModel(features),
1137
- });
1138
- const hostBinding = resolveFirmwareUpdateHostBinding(
1139
- memoryHost.hostBindingGeneration,
1140
- preparedPlan.preparedPlanDigest
1141
- );
1142
- this.params.preparedPlan = preparedPlan;
1143
- this.params.targetsToUpdate = [...preparedPlan.targetsToUpdate] as FirmwareUpdateV4Target[];
1144
- this.params.artifactReader = hostBinding.artifactReader;
1145
- this.params.componentArtifacts = undefined;
1146
- return memoryHost;
1147
- } catch (error) {
1148
- memoryHost?.release();
1149
- throw error;
1150
- }
1151
- }
1152
-
1153
- private async prepareProtocolV2LocalResourceArchive(
1154
- binary: ArrayBuffer
1155
- ): Promise<ProtocolV2LocalResourceArchive> {
1156
- if (binary.byteLength <= 0 || binary.byteLength > PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES) {
1157
- throw ERRORS.TypedError(
1158
- HardwareErrorCode.RuntimeError,
1159
- 'Protocol V2 local resource ZIP archive size is invalid',
1160
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1161
- );
1162
- }
1163
- const zip = await JSZip.loadAsync(binary);
1164
- const zipEntries = Object.values(zip.files);
1165
- if (
1166
- zipEntries.some(entry => entry.unsafeOriginalName && entry.unsafeOriginalName !== entry.name)
1167
- ) {
1168
- throw ERRORS.TypedError(
1169
- HardwareErrorCode.RuntimeError,
1170
- 'Protocol V2 local resource ZIP contains an unsafe entry path',
1171
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1172
- );
1173
- }
1174
- const entries = zipEntries.filter(entry => !entry.dir);
1175
- if (entries.length === 0 || entries.length > PROTOCOL_V2_RESOURCE_FILE_MAX_COUNT + 1) {
1176
- throw ERRORS.TypedError(
1177
- HardwareErrorCode.RuntimeError,
1178
- 'Protocol V2 local resource ZIP entry set is invalid',
1179
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1180
- );
1181
- }
1182
- let declaredUncompressedSize = 0;
1183
- let declaredCompressedSize = 0;
1184
- for (const entry of entries) {
1185
- const sizes = getProtocolV2ZipEntrySizes(entry);
1186
- declaredCompressedSize += sizes.compressedSize;
1187
- declaredUncompressedSize += sizes.uncompressedSize;
1188
- const entryLimit =
1189
- entry.name === 'manifest.json'
1190
- ? PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES
1191
- : PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES;
1192
- if (
1193
- sizes.uncompressedSize > entryLimit ||
1194
- declaredCompressedSize > binary.byteLength ||
1195
- declaredUncompressedSize >
1196
- PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES + PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES
1197
- ) {
1198
- throw ERRORS.TypedError(
1199
- HardwareErrorCode.RuntimeError,
1200
- 'Protocol V2 local resource ZIP declared size exceeds the allowed limit',
1201
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1202
- );
1203
- }
1204
- }
1205
- const manifestEntry = zip.file('manifest.json');
1206
- if (!manifestEntry) {
1207
- throw ERRORS.TypedError(
1208
- HardwareErrorCode.RuntimeError,
1209
- 'Protocol V2 local resource ZIP has no manifest.json',
1210
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1211
- );
1212
- }
1213
- const manifestBinary = await manifestEntry.async('arraybuffer');
1214
- if (
1215
- manifestBinary.byteLength <= 0 ||
1216
- manifestBinary.byteLength > PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES
1217
- ) {
1218
- throw ERRORS.TypedError(
1219
- HardwareErrorCode.RuntimeError,
1220
- 'Protocol V2 local resource manifest size is invalid',
1221
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1222
- );
1223
- }
1224
-
1225
- let manifestValue: unknown;
1226
- try {
1227
- manifestValue = JSON.parse(new TextDecoder().decode(manifestBinary));
1228
- } catch (error) {
1229
- throw ERRORS.TypedError(
1230
- HardwareErrorCode.RuntimeError,
1231
- `Protocol V2 local resource manifest is invalid: ${String(error)}`,
1232
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1233
- );
1234
- }
1235
- const manifest = parseProtocolV2ResourceManifest(manifestValue);
1236
- const selectedFiles = selectProtocolV2ResourceManifestFiles({
1237
- manifest,
1238
- targetsToUpdate: this.params.targetsToUpdate ?? [],
1239
- });
1240
- const expectedEntryNames = new Set([
1241
- 'manifest.json',
1242
- ...selectedFiles.map(file => file.archive_path),
1243
- ]);
1244
- if (
1245
- entries.length !== expectedEntryNames.size ||
1246
- entries.some(entry => !expectedEntryNames.has(entry.name))
1247
- ) {
1248
- throw ERRORS.TypedError(
1249
- HardwareErrorCode.RuntimeError,
1250
- 'Protocol V2 local resource ZIP contains an unexpected entry',
1251
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1252
- );
1253
- }
1254
-
1255
- let totalSize = 0;
1256
- const materializedEntries: FirmwareMemoryArtifactEntry[] = [
1257
- { entryName: 'manifest.json', binary: manifestBinary },
1258
- ];
1259
- for (const file of selectedFiles) {
1260
- const entry = zip.file(file.archive_path);
1261
- if (!entry) {
1262
- throw ERRORS.TypedError(
1263
- HardwareErrorCode.RuntimeError,
1264
- `Protocol V2 local resource ZIP is missing ${file.archive_path}`,
1265
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1266
- );
1267
- }
1268
- const { uncompressedSize } = getProtocolV2ZipEntrySizes(entry);
1269
- totalSize += uncompressedSize;
1270
- if (uncompressedSize !== file.size || totalSize > PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES) {
1271
- throw ERRORS.TypedError(
1272
- HardwareErrorCode.RuntimeError,
1273
- `Protocol V2 local resource file declared size is invalid: ${file.archive_path}`,
1274
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1275
- );
1276
- }
1277
- const fileBinary = await entry.async('arraybuffer');
1278
- const digest = bytesToHex(sha256(new Uint8Array(fileBinary)));
1279
- if (fileBinary.byteLength !== file.size || digest !== file.sha256.toLowerCase()) {
1280
- throw ERRORS.TypedError(
1281
- HardwareErrorCode.RuntimeError,
1282
- `Protocol V2 local resource file does not match manifest: ${file.archive_path}`,
1283
- { firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
1284
- );
1285
- }
1286
- materializedEntries.push({ entryName: file.archive_path, binary: fileBinary });
1287
- }
1288
- return { binary, materializedEntries };
1289
- }
1290
-
1291
- private async prepareProtocolV2ResourceSources(): Promise<ProtocolV2ResourceBundleSource[]> {
1292
- const resourceRequested = this.params.targetsToUpdate?.includes('resource') ?? false;
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;
1293
863
  if (!resourceRequested) {
1294
864
  return [];
1295
865
  }
1296
- const archiveSources = await this.prepareProtocolV2ResourceArchiveSources();
1297
- if (archiveSources) {
1298
- return archiveSources;
1299
- }
1300
- throw ERRORS.TypedError(
1301
- HardwareErrorCode.RuntimeError,
1302
- 'Protocol V2 resource archive is not prepared',
1303
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1304
- );
1305
- }
1306
-
1307
- private async prepareProtocolV2ResourceArchiveSources(): Promise<
1308
- ProtocolV2ResourceBundleSource[] | undefined
1309
- > {
1310
- const archiveArtifacts =
1311
- this.params.preparedPlan?.artifacts.filter(
1312
- artifact =>
1313
- artifact.role === 'resourceBundle' &&
1314
- artifact.target === 'resource' &&
1315
- artifact.container === 'zip'
1316
- ) ?? [];
1317
- if (archiveArtifacts.length === 0) {
1318
- return undefined;
1319
- }
1320
- if (archiveArtifacts.length !== 1) {
1321
- throw ERRORS.TypedError(
1322
- HardwareErrorCode.RuntimeError,
1323
- 'Protocol V2 prepared plan must contain exactly one resource archive',
1324
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1325
- );
1326
- }
1327
-
1328
- const archiveArtifact = archiveArtifacts[0];
1329
- const archiveSource = await this.openProtocolV2PreparedSource(archiveArtifact.artifact);
1330
- if (archiveSource.size > PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES) {
1331
- throw ERRORS.TypedError(
1332
- HardwareErrorCode.RuntimeError,
1333
- 'Protocol V2 prepared resource archive exceeds the total size limit',
1334
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1335
- );
1336
- }
1337
-
1338
- let archiveBinary: ArrayBuffer;
1339
- try {
1340
- archiveBinary = await readFirmwareByteSourceFully(archiveSource);
1341
- } finally {
1342
- await archiveSource.close();
1343
- }
1344
- const archiveDigest = bytesToHex(sha256(new Uint8Array(archiveBinary)));
1345
- if (archiveDigest !== archiveArtifact.artifact.sha256.toLowerCase()) {
1346
- throw ERRORS.TypedError(
1347
- HardwareErrorCode.RuntimeError,
1348
- 'Protocol V2 prepared resource archive does not match its approved receipt',
1349
- { firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
1350
- );
1351
- }
1352
-
1353
- // materializedEntries 由宿主生成,只有与获批 ZIP 的规范字节完全一致后才能使用。
1354
- const verifiedArchive = await this.prepareProtocolV2LocalResourceArchive(archiveBinary);
1355
- const entries = archiveArtifact.materializedEntries ?? [];
1356
- const entriesByName = new Map(entries.map(entry => [entry.entryName, entry] as const));
1357
- if (
1358
- entries.length !== verifiedArchive.materializedEntries.length ||
1359
- verifiedArchive.materializedEntries.some(entry => {
1360
- const preparedEntry = entriesByName.get(entry.entryName);
1361
- const digest = bytesToHex(sha256(new Uint8Array(entry.binary)));
1362
- return (
1363
- !preparedEntry ||
1364
- preparedEntry.artifact.size !== entry.binary.byteLength ||
1365
- preparedEntry.artifact.sha256.toLowerCase() !== digest
1366
- );
1367
- })
1368
- ) {
1369
- throw ERRORS.TypedError(
1370
- HardwareErrorCode.RuntimeError,
1371
- 'Protocol V2 prepared resource entries do not match the approved archive',
1372
- { firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
1373
- );
1374
- }
1375
-
1376
- const verifiedEntriesByName = new Map(
1377
- verifiedArchive.materializedEntries.map(entry => [entry.entryName, entry.binary] as const)
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)
1378
870
  );
1379
- const manifestBinary = verifiedEntriesByName.get('manifest.json');
1380
- if (!manifestBinary) {
1381
- throw ERRORS.TypedError(
1382
- HardwareErrorCode.RuntimeError,
1383
- 'Protocol V2 prepared resource archive has no valid manifest.json',
1384
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1385
- );
1386
- }
1387
-
1388
- let manifestValue: unknown;
1389
- try {
1390
- manifestValue = JSON.parse(new TextDecoder().decode(manifestBinary));
1391
- } catch (error) {
1392
- throw ERRORS.TypedError(
1393
- HardwareErrorCode.RuntimeError,
1394
- `Protocol V2 prepared resource manifest is invalid: ${String(error)}`,
1395
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1396
- );
1397
- }
1398
- const manifest = parseProtocolV2ResourceManifest(manifestValue);
1399
- const selectedFiles = selectProtocolV2ResourceManifestFiles({
1400
- manifest,
1401
- targetsToUpdate: this.params.targetsToUpdate ?? [],
1402
- });
1403
- if (selectedFiles.length > PROTOCOL_V2_RESOURCE_FILE_MAX_COUNT) {
1404
- throw ERRORS.TypedError(
1405
- HardwareErrorCode.RuntimeError,
1406
- 'Protocol V2 prepared resource archive contains too many files',
1407
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1408
- );
1409
- }
1410
-
1411
- let totalSize = 0;
1412
871
  const sources: ProtocolV2ResourceBundleSource[] = [];
1413
- for (const [index, file] of selectedFiles.entries()) {
1414
- const binary = verifiedEntriesByName.get(file.archive_path);
1415
- if (!binary || binary.byteLength !== file.size) {
1416
- throw ERRORS.TypedError(
1417
- HardwareErrorCode.RuntimeError,
1418
- `Protocol V2 prepared resource file does not match manifest: ${file.archive_path}`,
1419
- { firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
1420
- );
1421
- }
1422
- totalSize += binary.byteLength;
1423
- if (totalSize > PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES) {
872
+ for (const descriptor of descriptors) {
873
+ const artifact = artifactByName.get(descriptor.name);
874
+ if (!artifact) {
1424
875
  throw ERRORS.TypedError(
1425
876
  HardwareErrorCode.RuntimeError,
1426
- 'Protocol V2 prepared resource archive exceeds the total size limit',
1427
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
877
+ `Protocol V2 resource bundle ${descriptor.name} is not prepared`,
878
+ {
879
+ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
880
+ artifactName: descriptor.name,
881
+ }
1428
882
  );
1429
883
  }
1430
- // 使用从获批 ZIP 中提取的规范字节,避免宿主在校验后替换 entry reader 内容。
1431
- const source = await this.openProtocolV2MemorySource(binary);
1432
- const header =
1433
- source.size >= PROTOCOL_V2_OKPP_HEADER_SIZE
1434
- ? parseProtocolV2OkppHeader(
1435
- new Uint8Array(await source.readAt(0, PROTOCOL_V2_OKPP_HEADER_SIZE))
1436
- )
1437
- : null;
884
+ const devicePath = validateProtocolV2FilesystemPath(
885
+ descriptor.devicePath,
886
+ 'resourceBundles[].devicePath'
887
+ );
1438
888
  sources.push({
1439
- name: file.original_name || `resource-${index}`,
1440
- source,
1441
- devicePath: file.device_path,
1442
- ...(header
1443
- ? {
1444
- version: header.version,
1445
- payloadHash: header.payloadHash,
1446
- headerHash: header.headerHash,
1447
- }
1448
- : {}),
889
+ name: descriptor.name,
890
+ source: await this.openProtocolV2PreparedSource(artifact),
891
+ devicePath,
892
+ version: descriptor.version,
893
+ payloadHash: descriptor.payloadHash,
894
+ headerHash: descriptor.headerHash,
1449
895
  });
896
+ artifactByName.delete(descriptor.name);
897
+ }
898
+ if (artifactByName.size > 0) {
899
+ throw ERRORS.TypedError(
900
+ HardwareErrorCode.RuntimeError,
901
+ `Protocol V2 release does not contain resource bundle ${artifactByName.keys().next().value}`
902
+ );
1450
903
  }
1451
904
  return sources;
1452
905
  }
@@ -1455,7 +908,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1455
908
  try {
1456
909
  this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
1457
910
  const installSources = await this.prepareProtocolV2InstallSources(firmwareType, features);
1458
- const resourceSources = await this.prepareProtocolV2ResourceSources();
911
+ const resourceSources = await this.prepareProtocolV2ResourceSources(firmwareType, features);
1459
912
  if (installSources.length === 0 && resourceSources.length === 0) {
1460
913
  throw ERRORS.TypedError(
1461
914
  HardwareErrorCode.FirmwareUpdateDownloadFailed,
@@ -1485,7 +938,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1485
938
  for (const [target, version] of Object.entries(expected)) {
1486
939
  if (
1487
940
  !Array.from(PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.values()).includes(
1488
- target as Exclude<FirmwareUpdateV4Target, 'boot_resources'>
941
+ target as FirmwareUpdateV4Target
1489
942
  ) ||
1490
943
  typeof version !== 'string' ||
1491
944
  !/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/u.test(version) ||
@@ -1571,13 +1024,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1571
1024
  if (this.params.targetsToUpdate?.length) {
1572
1025
  return [...new Set(this.params.targetsToUpdate)];
1573
1026
  }
1574
- if (this.params.preparedPlan?.targetsToUpdate.length) {
1575
- return [...new Set(this.params.preparedPlan.targetsToUpdate)] as FirmwareUpdateV4Target[];
1576
- }
1577
1027
  const targets = new Set<FirmwareUpdateV4Target>();
1578
1028
  Object.keys(this.params.componentArtifacts ?? {}).forEach(target =>
1579
1029
  targets.add(target as FirmwareUpdateV4Target)
1580
1030
  );
1031
+ if (this.params.resourceBundleArtifacts?.length || this.params.resourceFiles?.length) {
1032
+ targets.add('resource');
1033
+ }
1581
1034
  if (this.params.bootloaderBinary) targets.add('boot');
1582
1035
  if (this.params.applicationP1Binary) targets.add('app_v1');
1583
1036
  if (this.params.applicationP2Binary) targets.add('app_v2');
@@ -1605,34 +1058,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1605
1058
  return serialNumber || undefined;
1606
1059
  }
1607
1060
 
1608
- private getProtocolV2PreparedPlanDeviceModel(features?: Features) {
1609
- const currentDeviceType = this.device.getCurrentDeviceType();
1610
- const featureDeviceType = features ? getDeviceType(features) : undefined;
1611
- const deviceType =
1612
- currentDeviceType === EDeviceType.Pro2 || currentDeviceType === EDeviceType.Neo
1613
- ? currentDeviceType
1614
- : featureDeviceType;
1615
- return deviceType === EDeviceType.Pro2 || deviceType === EDeviceType.Neo
1616
- ? String(deviceType)
1617
- : undefined;
1618
- }
1619
-
1620
- private getProtocolV2ConnectionRoute() {
1621
- const descriptor = this.device.originalDescriptor;
1622
- return (
1623
- descriptor?.path?.trim() ||
1624
- this.device.getConnectId?.()?.trim() ||
1625
- descriptor?.id?.trim() ||
1626
- undefined
1627
- );
1628
- }
1629
-
1630
1061
  private assertProtocolV2DeviceInfoIdentity(deviceInfo: ProtocolV2DeviceInfo) {
1631
1062
  assertProtocolV2ReconnectIdentity(
1632
1063
  this.protocolV2ExpectedSerialNumber,
1633
1064
  this.getProtocolV2SerialNumber(deviceInfo),
1634
1065
  this.protocolV2ExpectedPath,
1635
- this.getProtocolV2ConnectionRoute()
1066
+ this.device.originalDescriptor.path
1636
1067
  );
1637
1068
  }
1638
1069
 
@@ -1652,12 +1083,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1652
1083
  private async captureProtocolV2PhysicalIdentity() {
1653
1084
  const deviceInfo = await this.requestProtocolV2PhysicalIdentity();
1654
1085
  const serialNumber = this.getProtocolV2SerialNumber(deviceInfo);
1655
- const path = this.getProtocolV2ConnectionRoute();
1086
+ const path = this.device.originalDescriptor?.path?.trim() || undefined;
1656
1087
  if (this.params?.preparedPlan) {
1657
1088
  assertFirmwareUpdatePreparedPlanDeviceIdentity({
1658
1089
  preparedPlan: this.params.preparedPlan,
1659
1090
  deviceIdentity: serialNumber,
1660
- deviceModel: this.getProtocolV2PreparedPlanDeviceModel(this.device.features),
1661
1091
  });
1662
1092
  } else if (this.params?.expectedDeviceId) {
1663
1093
  assertProtocolV2ReconnectIdentity(this.params?.expectedDeviceId, serialNumber);
@@ -1678,19 +1108,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1678
1108
  return this.params.bootloaderBinary ?? null;
1679
1109
  }
1680
1110
 
1681
- private getMissingProtocolV2FirmwareTargets(installItems: ProtocolV2InstallItem[]) {
1682
- const preparedTargets = new Set(
1683
- installItems.flatMap(item => {
1684
- const target = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(item.targetId);
1685
- return target ? [target] : [];
1686
- })
1111
+ private hasExplicitProtocolV2Payload(fwBinaryMap: ProtocolV2TargetBinary[]) {
1112
+ return (
1113
+ !!this.params.resourceFiles?.length ||
1114
+ !!this.params.bootloaderBinary ||
1115
+ fwBinaryMap.length > 0
1687
1116
  );
1688
- return (this.params.targetsToUpdate ?? [])
1689
- .filter(
1690
- (target): target is Exclude<FirmwareUpdateV4Target, 'resource' | 'boot_resources'> =>
1691
- target !== 'resource' && target !== 'boot_resources'
1692
- )
1693
- .filter(target => !preparedTargets.has(target));
1694
1117
  }
1695
1118
 
1696
1119
  private buildProtocolV2InstallItems({
@@ -1761,28 +1184,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1761
1184
  `Missing Protocol V2 firmware component url: ${key}/${component.target}`
1762
1185
  );
1763
1186
  }
1764
- const expectedFingerprint = normalizeProtocolV2Hex(component.fingerprint);
1765
- if (
1766
- !Number.isSafeInteger(component.expectedSize) ||
1767
- Number(component.expectedSize) <= 0 ||
1768
- !expectedFingerprint ||
1769
- !/^[0-9a-f]{64}$/u.test(expectedFingerprint)
1770
- ) {
1771
- throw ERRORS.TypedError(
1772
- HardwareErrorCode.RuntimeError,
1773
- `Protocol V2 firmware component integrity metadata is invalid: ${key}/${component.target}`,
1774
- { firmwareUpdateCode: 'FirmwarePlanInvalid' }
1775
- );
1776
- }
1777
1187
 
1778
1188
  const { binary } = await getSysResourceBinary(component.url);
1779
- if (binary.byteLength !== component.expectedSize) {
1780
- throw ERRORS.TypedError(
1781
- HardwareErrorCode.RuntimeError,
1782
- `Protocol V2 firmware size mismatch: ${key}/${component.target}`,
1783
- { firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
1784
- );
1785
- }
1786
1189
  if (!isProtocolV2FirmwareFingerprintValid(binary, component.fingerprint)) {
1787
1190
  throw ERRORS.TypedError(
1788
1191
  HardwareErrorCode.RuntimeError,
@@ -1805,11 +1208,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1805
1208
  };
1806
1209
  }
1807
1210
 
1808
- private async prepareRemoteProtocolV2Binaries(
1809
- firmwareType: EFirmwareType,
1810
- features: Features,
1811
- explicitInstallItems: ProtocolV2InstallItem[] = []
1812
- ) {
1211
+ private async prepareRemoteProtocolV2Binaries(firmwareType: EFirmwareType, features: Features) {
1813
1212
  const release = DataManager.getFirmwareLatestRelease(features, firmwareType);
1814
1213
 
1815
1214
  let bootloaderBinary: ArrayBuffer | null = null;
@@ -1817,15 +1216,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1817
1216
  const installItems: ProtocolV2InstallItem[] = [];
1818
1217
 
1819
1218
  if (!release) {
1820
- const missingFirmwareTargets = this.getMissingProtocolV2FirmwareTargets(explicitInstallItems);
1821
- if (missingFirmwareTargets.length > 0) {
1822
- throw ERRORS.TypedError(
1823
- HardwareErrorCode.RuntimeError,
1824
- `Protocol V2 firmware release is unavailable for requested targets: ${missingFirmwareTargets.join(
1825
- ', '
1826
- )}`
1827
- );
1828
- }
1829
1219
  return {
1830
1220
  bootloaderBinary,
1831
1221
  fwBinaryMap,
@@ -1835,10 +1225,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1835
1225
 
1836
1226
  const entries = this.getRemoteComponentEntries(release);
1837
1227
  const targetsToUpdate = new Set(this.params.targetsToUpdate ?? []);
1838
- const explicitInstallItemByTargetId = new Map(
1839
- explicitInstallItems.map(item => [item.targetId, item] as const)
1840
- );
1841
- const preparedTargets = new Set<FirmwareUpdateV4Target>();
1842
1228
 
1843
1229
  for (const [key, component] of entries) {
1844
1230
  const targetName = component.target?.toUpperCase();
@@ -1847,34 +1233,27 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1847
1233
  ? PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(target.targetId)
1848
1234
  : undefined;
1849
1235
  if (updateTarget && targetsToUpdate.has(updateTarget)) {
1850
- const explicitInstallItem = explicitInstallItemByTargetId.get(target.targetId);
1851
- const installItem =
1852
- explicitInstallItem ?? (await this.downloadRemoteProtocolV2Component(key, component));
1853
- if (installItem.kind === 'bootloader') {
1854
- bootloaderBinary = installItem.binary;
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
1245
  } else {
1856
1246
  const binaryEntry = {
1857
- fileName: installItem.fileName,
1858
- binary: installItem.binary,
1859
- targetId: installItem.targetId,
1247
+ fileName: remoteBinary.fileName,
1248
+ binary: remoteBinary.binary,
1249
+ targetId: remoteBinary.targetId,
1860
1250
  };
1861
1251
  fwBinaryMap.push(binaryEntry);
1252
+ installItems.push({ ...binaryEntry, kind: remoteBinary.kind });
1862
1253
  }
1863
- installItems.push({ ...installItem });
1864
- preparedTargets.add(updateTarget);
1865
1254
  }
1866
1255
  }
1867
1256
 
1868
- const missingTarget = Array.from(targetsToUpdate).find(
1869
- target => target !== 'resource' && !preparedTargets.has(target)
1870
- );
1871
- if (missingTarget) {
1872
- throw ERRORS.TypedError(
1873
- HardwareErrorCode.RuntimeError,
1874
- `Protocol V2 release does not contain requested target ${missingTarget}`
1875
- );
1876
- }
1877
-
1878
1257
  return {
1879
1258
  bootloaderBinary,
1880
1259
  fwBinaryMap,
@@ -1882,13 +1261,211 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1882
1261
  };
1883
1262
  }
1884
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}`
1438
+ );
1439
+
1440
+ const bundles: ProtocolV2ResourceBundleBinary[] = [];
1441
+ for (const resource of plan.resources) {
1442
+ bundles.push(await this.downloadProtocolV2Resource(resource));
1443
+ }
1444
+ return bundles;
1445
+ }
1446
+
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
+ return {
1456
+ name: `${resource.type}.okpkg`,
1457
+ binary,
1458
+ devicePath: PROTOCOL_V2_RESOURCE_DEVICE_PATHS[resource.type],
1459
+ };
1460
+ }
1461
+
1885
1462
  private getProtocolV2ResourceFilePath(path: string) {
1886
1463
  if (path.startsWith('vol')) return path;
1887
1464
  if (path.startsWith('/')) return `vol0:${path}`;
1888
1465
  return `vol0:/${path}`;
1889
1466
  }
1890
1467
 
1891
- private async readProtocolV2DeviceFileHeader(path: string, expectedSize?: number) {
1468
+ private async readProtocolV2DeviceFileHeader(path: string) {
1892
1469
  const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
1893
1470
  const filePath = this.getProtocolV2ResourceFilePath(path);
1894
1471
  const pathInfoRes = await typedCall('FilesystemPathInfoQuery', 'FilesystemPathInfo', {
@@ -1899,8 +1476,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1899
1476
  !pathInfoRes.message?.exist ||
1900
1477
  pathInfoRes.message?.directory ||
1901
1478
  fileSize === undefined ||
1902
- fileSize < PROTOCOL_V2_OKPP_HEADER_SIZE ||
1903
- (expectedSize !== undefined && fileSize !== expectedSize)
1479
+ fileSize < PROTOCOL_V2_OKPP_HEADER_SIZE
1904
1480
  ) {
1905
1481
  return null;
1906
1482
  }
@@ -1934,31 +1510,32 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1934
1510
  return parseProtocolV2OkppHeader(headerBytes);
1935
1511
  }
1936
1512
 
1937
- /** Compare the downloaded and installed okpkg headers before transferring a resource. */
1513
+ /** Compare a prepared okpkg header when its manifest supplies version or hash metadata. */
1938
1514
  private async isProtocolV2ResourceBundleUpToDate(
1939
1515
  bundle: Pick<
1940
1516
  ProtocolV2ResourceBundleSource,
1941
- 'name' | 'source' | 'devicePath' | 'version' | 'payloadHash' | 'headerHash'
1517
+ 'name' | 'devicePath' | 'version' | 'payloadHash' | 'headerHash'
1942
1518
  >
1943
1519
  ): Promise<boolean> {
1944
1520
  if (this.params?.forcedUpdateRes) return false;
1945
- if (!bundle.payloadHash || !bundle.headerHash) return false;
1521
+ if (!bundle.version && !bundle.payloadHash) return false;
1946
1522
 
1947
1523
  try {
1948
- const header = await this.readProtocolV2DeviceFileHeader(
1949
- bundle.devicePath,
1950
- bundle.source.size
1951
- );
1524
+ const header = await this.readProtocolV2DeviceFileHeader(bundle.devicePath);
1952
1525
  if (!header) return false;
1953
1526
 
1954
1527
  if (bundle.version) {
1955
1528
  const cmp = compareProtocolV2Versions(header.version, bundle.version);
1956
1529
  if (cmp === undefined || cmp !== 0) return false;
1957
1530
  }
1958
- const expectedPayloadHash = normalizeProtocolV2Hex(bundle.payloadHash);
1959
- const expectedHeaderHash = normalizeProtocolV2Hex(bundle.headerHash);
1960
- if (!expectedPayloadHash || header.payloadHash !== expectedPayloadHash) return false;
1961
- if (!expectedHeaderHash || header.headerHash !== expectedHeaderHash) return false;
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
+ }
1962
1539
  return true;
1963
1540
  } catch (error) {
1964
1541
  Log.log(`[FirmwareUpdateV4] RESC bundle ${bundle.name} header check failed: `, error);
@@ -2114,6 +1691,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2114
1691
  resourceSources: ProtocolV2ResourceBundleSource[];
2115
1692
  }): ProtocolV2ExecutionPhase[] {
2116
1693
  const phases: ProtocolV2ExecutionPhase[] = [];
1694
+ if (resourceSources.length > 0) {
1695
+ phases.push({
1696
+ kind: 'resource-sync',
1697
+ installSources: [],
1698
+ resourceSources,
1699
+ });
1700
+ }
2117
1701
  const bootloaderSources = installSources.filter(source => source.kind === 'bootloader');
2118
1702
  if (bootloaderSources.length > 0) {
2119
1703
  phases.push(
@@ -2129,13 +1713,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2129
1713
  }
2130
1714
  );
2131
1715
  }
2132
- if (resourceSources.length > 0) {
2133
- phases.push({
2134
- kind: 'resource-sync',
2135
- installSources: [],
2136
- resourceSources,
2137
- });
2138
- }
2139
1716
  const componentSources = installSources.filter(source => source.kind !== 'bootloader');
2140
1717
  if (componentSources.length > 0) {
2141
1718
  phases.push({
@@ -2202,10 +1779,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2202
1779
  fwBinaryMap,
2203
1780
  bootloaderBinary,
2204
1781
  installItems,
1782
+ resourceBundles,
2205
1783
  }: {
2206
1784
  fwBinaryMap?: ProtocolV2TargetBinary[];
2207
1785
  bootloaderBinary?: ArrayBuffer | null;
2208
1786
  installItems?: ProtocolV2InstallItem[];
1787
+ resourceBundles?: ProtocolV2ResourceBundleBinary[];
2209
1788
  }) {
2210
1789
  const memoryInstallItems =
2211
1790
  installItems ??
@@ -2222,9 +1801,16 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2222
1801
  kind: item.kind,
2223
1802
  }))
2224
1803
  );
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
+ );
2225
1811
  return await this.executeProtocolV2Phases({
2226
1812
  installSources,
2227
- resourceSources: [],
1813
+ resourceSources,
2228
1814
  });
2229
1815
  } finally {
2230
1816
  await this.closeProtocolV2PreparedSources();
@@ -2249,16 +1835,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2249
1835
  this.postTipMessage(FirmwareUpdateTipMessage.StartTransferData);
2250
1836
  let processedSize = 0;
2251
1837
  for (const resource of resourcesToSync) {
2252
- // The bootloader keeps its live resource package mounted. FatFs rejects
2253
- // replacing an open file, so early boot promotes this staging file before mounting it.
2254
- const writePath = resolveProtocolV2ResourceWritePath(resource.devicePath);
2255
1838
  processedSize = await this.protocolV2SourceUpdateProcess({
2256
1839
  source: resource.source,
2257
- filePath: writePath,
1840
+ filePath: resource.devicePath,
2258
1841
  processedSize,
2259
1842
  totalSize,
2260
1843
  });
2261
- await this.verifyProtocolV2StagedFile(writePath, resource.source.size);
1844
+ await this.verifyProtocolV2StagedFile(resource.devicePath, resource.source.size);
2262
1845
  }
2263
1846
 
2264
1847
  const stagedInstallTargets: Array<{ targetId: number; path: string }> = [];