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

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