@onekeyfe/hd-core 1.2.0-alpha.88 → 1.2.0-alpha.89

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.
@@ -43,12 +43,19 @@ import {
43
43
  readFirmwareByteSourceFully,
44
44
  writeFirmwareByteSource,
45
45
  } from './firmware/FirmwareArtifactSource';
46
- import { resolveFirmwareUpdateHostBinding } from './firmware/FirmwareHostBinding';
46
+ import {
47
+ registerFirmwareUpdateHostBinding,
48
+ resolveFirmwareUpdateHostBinding,
49
+ unregisterFirmwareUpdateHostBinding,
50
+ } from './firmware/FirmwareHostBinding';
47
51
  import {
48
52
  assertFirmwareUpdatePreparedPlanBinding,
49
53
  assertFirmwareUpdatePreparedPlanDeviceIdentity,
54
+ prepareFirmwareUpdatePlan,
50
55
  validateFirmwareUpdatePreparedPlan,
51
56
  } from './firmware/FirmwareUpdatePreparedPlan';
57
+ import { prepareFirmwareUpdateV4MemoryHost } from './firmware/FirmwareMemoryHost';
58
+ import { buildProtocolV2LocalFirmwareUpdatePlan } from './firmware/FirmwareUpdatePlan';
52
59
 
53
60
  import type {
54
61
  FirmwareArtifactReference,
@@ -65,6 +72,11 @@ import type {
65
72
  IVersionArray,
66
73
  } from '../types';
67
74
  import type { FirmwareByteSource } from './firmware/FirmwareArtifactSource';
75
+ import type {
76
+ FirmwareMemoryArtifact,
77
+ FirmwareMemoryArtifactEntry,
78
+ FirmwareUpdateV4MemoryHost,
79
+ } from './firmware/FirmwareMemoryHost';
68
80
 
69
81
  const Log = getLogger(LoggerNames.Method);
70
82
 
@@ -102,6 +114,28 @@ const PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES = 256 * 1024 * 1024;
102
114
 
103
115
  const PROTOCOL_V2_NEO_UNSUPPORTED_TARGETS = new Set<FirmwareUpdateV4Target>(['se03', 'se04']);
104
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
+
105
139
  export function assertProtocolV2FirmwareTargetsSupported(
106
140
  deviceType: EDeviceType | string | undefined,
107
141
  params: FirmwareUpdateV4Params
@@ -183,6 +217,18 @@ type ProtocolV2ResourceBundleBinary = Omit<ProtocolV2ResourceBundleSource, 'sour
183
217
  binary: ArrayBuffer;
184
218
  };
185
219
 
220
+ type ProtocolV2LocalResourceArchive = {
221
+ binary: ArrayBuffer;
222
+ materializedEntries: FirmwareMemoryArtifactEntry[];
223
+ };
224
+
225
+ type JSZipSizedEntry = JSZip.JSZipObject & {
226
+ _data?: {
227
+ compressedSize?: unknown;
228
+ uncompressedSize?: unknown;
229
+ };
230
+ };
231
+
186
232
  type ProtocolV2ExecutionPhaseKind =
187
233
  | 'resource-sync'
188
234
  | 'bootloader-install'
@@ -264,7 +310,10 @@ const resolveProtocolV2ResourceWritePath = (devicePath: string) => {
264
310
  : devicePath;
265
311
  };
266
312
 
267
- const PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID = new Map<number, FirmwareUpdateV4Target>([
313
+ const PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID = new Map<
314
+ number,
315
+ Exclude<FirmwareUpdateV4Target, 'boot_resources'>
316
+ >([
268
317
  [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_BOOTLOADER, 'boot'],
269
318
  [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P1, 'app_v1'],
270
319
  [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P2, 'app_v2'],
@@ -560,6 +609,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
560
609
  { name: 'se03Binary', type: 'buffer' },
561
610
  { name: 'se04Binary', type: 'buffer' },
562
611
  { name: 'resourceArchiveBinary', type: 'buffer' },
612
+ { name: 'resourceFiles', type: 'array', allowEmpty: true },
613
+ { name: 'resourceBundleArtifacts', type: 'array', allowEmpty: true },
563
614
  { name: 'firmwareType', type: 'string' },
564
615
  { name: 'targetsToUpdate', type: 'array', allowEmpty: true },
565
616
  { name: 'platform', type: 'string' },
@@ -574,6 +625,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
574
625
  'Protocol V2 expected device identity is invalid'
575
626
  );
576
627
  }
628
+ if (payload.resourceFiles?.length || payload.resourceBundleArtifacts?.length) {
629
+ throw ERRORS.TypedError(
630
+ HardwareErrorCode.CallMethodInvalidParameter,
631
+ 'Protocol V2 resourceFiles and resourceBundleArtifacts are deprecated; provide a complete signed resource ZIP through resourceArchiveBinary or preparedPlan'
632
+ );
633
+ }
577
634
  const preparedPlan = payload.preparedPlan
578
635
  ? validateFirmwareUpdatePreparedPlan(payload.preparedPlan)
579
636
  : undefined;
@@ -667,7 +724,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
667
724
  firmwareType: preparedPlan?.firmwareType ?? payload.firmwareType,
668
725
  targetsToUpdate: preparedPlan
669
726
  ? ([...preparedPlan.targetsToUpdate] as FirmwareUpdateV4Target[])
670
- : payload.targetsToUpdate,
727
+ : payload.targetsToUpdate?.map((target: FirmwareUpdateV4Target) =>
728
+ target === 'boot_resources' ? 'resource' : target
729
+ ),
671
730
  expectedTargetVersions: preparedPlan
672
731
  ? preparedExpectedTargetVersions
673
732
  : payload.expectedTargetVersions,
@@ -720,6 +779,22 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
720
779
  const firmwareType = this.params.firmwareType ?? deviceFirmwareType;
721
780
  this.validateExpectedTargetVersions();
722
781
 
782
+ if (
783
+ !this.params.preparedPlan &&
784
+ this.params.resourceArchiveBinary &&
785
+ this.params.targetsToUpdate?.includes('resource')
786
+ ) {
787
+ const localMemoryHost = await this.prepareProtocolV2LocalMemoryHost({
788
+ features: deviceFeatures,
789
+ firmwareType,
790
+ });
791
+ try {
792
+ return await this.runProtocolV2PreparedArtifacts(deviceFeatures, firmwareType);
793
+ } finally {
794
+ localMemoryHost.release();
795
+ }
796
+ }
797
+
723
798
  const hasPreparedComponentArtifacts = Object.values(this.params.componentArtifacts ?? {}).some(
724
799
  Boolean
725
800
  );
@@ -731,7 +806,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
731
806
  let fwBinaryMap: ProtocolV2TargetBinary[] = [];
732
807
  let bootloaderBinary: ArrayBuffer | null = null;
733
808
  let installItems: ProtocolV2InstallItem[] | undefined;
734
- let resourceBundles: ProtocolV2ResourceBundleBinary[] = [];
735
809
  try {
736
810
  this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
737
811
  fwBinaryMap = this.collectExplicitTargetBinaries();
@@ -745,17 +819,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
745
819
  ? missingFirmwareTargets.length > 0
746
820
  : explicitInstallItems.length === 0;
747
821
  if (wantsResources) {
748
- if (!this.params.resourceArchiveBinary) {
749
- throw ERRORS.TypedError(
750
- HardwareErrorCode.RuntimeError,
751
- 'Protocol V2 resource archive must be provided for a local resource update',
752
- {
753
- firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
754
- }
755
- );
756
- }
757
- resourceBundles = await this.prepareProtocolV2LocalResourceArchive(
758
- this.params.resourceArchiveBinary
822
+ throw ERRORS.TypedError(
823
+ HardwareErrorCode.RuntimeError,
824
+ 'Protocol V2 resource archive must be provided through a local or external PreparedPlan',
825
+ {
826
+ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
827
+ }
759
828
  );
760
829
  }
761
830
  if (
@@ -801,12 +870,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
801
870
  throw normalizeFirmwarePreparationError(err);
802
871
  }
803
872
 
804
- if (
805
- !bootloaderBinary &&
806
- fwBinaryMap.length === 0 &&
807
- !installItems?.length &&
808
- resourceBundles.length === 0
809
- ) {
873
+ if (!bootloaderBinary && fwBinaryMap.length === 0 && !installItems?.length) {
810
874
  throw ERRORS.TypedError(
811
875
  HardwareErrorCode.FirmwareUpdateDownloadFailed,
812
876
  'No firmware to update'
@@ -817,7 +881,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
817
881
  fwBinaryMap,
818
882
  bootloaderBinary,
819
883
  ...(installItems ? { installItems } : undefined),
820
- ...(resourceBundles.length > 0 ? { resourceBundles } : undefined),
821
884
  });
822
885
  }
823
886
 
@@ -957,11 +1020,154 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
957
1020
  return installSources;
958
1021
  }
959
1022
 
1023
+ private async prepareProtocolV2LocalMemoryHost({
1024
+ features,
1025
+ firmwareType,
1026
+ }: {
1027
+ features: Features;
1028
+ firmwareType: EFirmwareType;
1029
+ }): Promise<FirmwareUpdateV4MemoryHost> {
1030
+ const installItems = this.buildProtocolV2InstallItems({
1031
+ bootloaderBinary: this.prepareBootloaderBinary(),
1032
+ fwBinaryMap: this.collectExplicitTargetBinaries(),
1033
+ });
1034
+ const requestedComponentTargets = new Set(
1035
+ (this.params.targetsToUpdate ?? []).filter(
1036
+ (target): target is Exclude<FirmwareUpdateV4Target, 'resource' | 'boot_resources'> =>
1037
+ target !== 'resource' && target !== 'boot_resources'
1038
+ )
1039
+ );
1040
+ const localComponentTargets = new Set(
1041
+ installItems.flatMap(item => {
1042
+ const target = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(item.targetId);
1043
+ return target ? [target] : [];
1044
+ })
1045
+ );
1046
+ const missingTarget = Array.from(requestedComponentTargets).find(
1047
+ target => !localComponentTargets.has(target)
1048
+ );
1049
+ if (missingTarget) {
1050
+ throw ERRORS.TypedError(
1051
+ HardwareErrorCode.RuntimeError,
1052
+ `Protocol V2 local update has no binary for requested target ${missingTarget}`,
1053
+ {
1054
+ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
1055
+ artifactName: missingTarget,
1056
+ }
1057
+ );
1058
+ }
1059
+
1060
+ const planArtifacts: Parameters<typeof buildProtocolV2LocalFirmwareUpdatePlan>[0]['artifacts'] =
1061
+ [];
1062
+ const memoryArtifacts: FirmwareMemoryArtifact[] = [];
1063
+ for (const item of installItems) {
1064
+ const target = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(item.targetId);
1065
+ if (!target || item.binary.byteLength <= 0) {
1066
+ throw ERRORS.TypedError(
1067
+ HardwareErrorCode.RuntimeError,
1068
+ `Protocol V2 local firmware artifact is invalid: ${item.fileName}`,
1069
+ { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1070
+ );
1071
+ }
1072
+ const artifactId = `component:${target}`;
1073
+ planArtifacts.push({
1074
+ artifactId,
1075
+ target,
1076
+ container: 'raw',
1077
+ logicalName: item.fileName,
1078
+ expectedSize: item.binary.byteLength,
1079
+ expectedSha256: bytesToHex(sha256(new Uint8Array(item.binary))),
1080
+ ...(this.params.expectedTargetVersions?.[target]
1081
+ ? { targetVersion: this.params.expectedTargetVersions[target] }
1082
+ : {}),
1083
+ });
1084
+ memoryArtifacts.push({ artifactId, binary: item.binary });
1085
+ }
1086
+
1087
+ const resourceArchive = await this.prepareProtocolV2LocalResourceArchive(
1088
+ this.params.resourceArchiveBinary as ArrayBuffer
1089
+ );
1090
+ const resourceArtifactId = 'resource:archive';
1091
+ planArtifacts.push({
1092
+ artifactId: resourceArtifactId,
1093
+ target: 'resource',
1094
+ container: 'zip',
1095
+ logicalName: 'protocol-v2-local-resource-archive',
1096
+ expectedSize: resourceArchive.binary.byteLength,
1097
+ expectedSha256: bytesToHex(sha256(new Uint8Array(resourceArchive.binary))),
1098
+ });
1099
+ memoryArtifacts.push({
1100
+ artifactId: resourceArtifactId,
1101
+ binary: resourceArchive.binary,
1102
+ materializedEntries: resourceArchive.materializedEntries,
1103
+ });
1104
+
1105
+ const plan = buildProtocolV2LocalFirmwareUpdatePlan({
1106
+ features,
1107
+ firmwareType,
1108
+ platform: this.params.platform,
1109
+ artifacts: planArtifacts,
1110
+ });
1111
+ let memoryHost: FirmwareUpdateV4MemoryHost | undefined;
1112
+ try {
1113
+ memoryHost = prepareFirmwareUpdateV4MemoryHost({
1114
+ sdk: {
1115
+ prepareFirmwareUpdatePlan,
1116
+ registerFirmwareUpdateHostBinding,
1117
+ unregisterFirmwareUpdateHostBinding,
1118
+ },
1119
+ plan,
1120
+ artifacts: memoryArtifacts,
1121
+ });
1122
+ const preparedPlan = validateFirmwareUpdatePreparedPlan(memoryHost.preparedPlan);
1123
+ assertFirmwareUpdatePreparedPlanBinding({
1124
+ preparedPlan,
1125
+ executor: 'v4',
1126
+ platform: this.params.platform,
1127
+ scopeTargets: [],
1128
+ bindings: [],
1129
+ });
1130
+ assertFirmwareUpdatePreparedPlanDeviceIdentity({
1131
+ preparedPlan,
1132
+ deviceIdentity: this.protocolV2ExpectedSerialNumber,
1133
+ });
1134
+ const hostBinding = resolveFirmwareUpdateHostBinding(
1135
+ memoryHost.hostBindingGeneration,
1136
+ preparedPlan.preparedPlanDigest
1137
+ );
1138
+ this.params.preparedPlan = preparedPlan;
1139
+ this.params.targetsToUpdate = [...preparedPlan.targetsToUpdate] as FirmwareUpdateV4Target[];
1140
+ this.params.artifactReader = hostBinding.artifactReader;
1141
+ this.params.componentArtifacts = undefined;
1142
+ return memoryHost;
1143
+ } catch (error) {
1144
+ memoryHost?.release();
1145
+ throw error;
1146
+ }
1147
+ }
1148
+
960
1149
  private async prepareProtocolV2LocalResourceArchive(
961
1150
  binary: ArrayBuffer
962
- ): Promise<ProtocolV2ResourceBundleBinary[]> {
1151
+ ): Promise<ProtocolV2LocalResourceArchive> {
1152
+ if (binary.byteLength <= 0 || binary.byteLength > PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES) {
1153
+ throw ERRORS.TypedError(
1154
+ HardwareErrorCode.RuntimeError,
1155
+ 'Protocol V2 local resource ZIP archive size is invalid',
1156
+ { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1157
+ );
1158
+ }
963
1159
  const zip = await JSZip.loadAsync(binary);
964
- const entries = Object.values(zip.files).filter(entry => !entry.dir);
1160
+ const zipEntries = Object.values(zip.files);
1161
+ if (
1162
+ zipEntries.some(entry => entry.unsafeOriginalName && entry.unsafeOriginalName !== entry.name)
1163
+ ) {
1164
+ throw ERRORS.TypedError(
1165
+ HardwareErrorCode.RuntimeError,
1166
+ 'Protocol V2 local resource ZIP contains an unsafe entry path',
1167
+ { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1168
+ );
1169
+ }
1170
+ const entries = zipEntries.filter(entry => !entry.dir);
965
1171
  if (entries.length === 0 || entries.length > PROTOCOL_V2_RESOURCE_FILE_MAX_COUNT + 1) {
966
1172
  throw ERRORS.TypedError(
967
1173
  HardwareErrorCode.RuntimeError,
@@ -969,6 +1175,29 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
969
1175
  { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
970
1176
  );
971
1177
  }
1178
+ let declaredUncompressedSize = 0;
1179
+ let declaredCompressedSize = 0;
1180
+ for (const entry of entries) {
1181
+ const sizes = getProtocolV2ZipEntrySizes(entry);
1182
+ declaredCompressedSize += sizes.compressedSize;
1183
+ declaredUncompressedSize += sizes.uncompressedSize;
1184
+ const entryLimit =
1185
+ entry.name === 'manifest.json'
1186
+ ? PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES
1187
+ : PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES;
1188
+ if (
1189
+ sizes.uncompressedSize > entryLimit ||
1190
+ declaredCompressedSize > binary.byteLength ||
1191
+ declaredUncompressedSize >
1192
+ PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES + PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES
1193
+ ) {
1194
+ throw ERRORS.TypedError(
1195
+ HardwareErrorCode.RuntimeError,
1196
+ 'Protocol V2 local resource ZIP declared size exceeds the allowed limit',
1197
+ { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1198
+ );
1199
+ }
1200
+ }
972
1201
  const manifestEntry = zip.file('manifest.json');
973
1202
  if (!manifestEntry) {
974
1203
  throw ERRORS.TypedError(
@@ -1020,8 +1249,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1020
1249
  }
1021
1250
 
1022
1251
  let totalSize = 0;
1023
- const resourceBundles: ProtocolV2ResourceBundleBinary[] = [];
1024
- for (const [index, file] of selectedFiles.entries()) {
1252
+ const materializedEntries: FirmwareMemoryArtifactEntry[] = [
1253
+ { entryName: 'manifest.json', binary: manifestBinary },
1254
+ ];
1255
+ for (const file of selectedFiles) {
1025
1256
  const entry = zip.file(file.archive_path);
1026
1257
  if (!entry) {
1027
1258
  throw ERRORS.TypedError(
@@ -1030,6 +1261,15 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1030
1261
  { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1031
1262
  );
1032
1263
  }
1264
+ const { uncompressedSize } = getProtocolV2ZipEntrySizes(entry);
1265
+ totalSize += uncompressedSize;
1266
+ if (uncompressedSize !== file.size || totalSize > PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES) {
1267
+ throw ERRORS.TypedError(
1268
+ HardwareErrorCode.RuntimeError,
1269
+ `Protocol V2 local resource file declared size is invalid: ${file.archive_path}`,
1270
+ { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1271
+ );
1272
+ }
1033
1273
  const fileBinary = await entry.async('arraybuffer');
1034
1274
  const digest = bytesToHex(sha256(new Uint8Array(fileBinary)));
1035
1275
  if (fileBinary.byteLength !== file.size || digest !== file.sha256.toLowerCase()) {
@@ -1039,32 +1279,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1039
1279
  { firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
1040
1280
  );
1041
1281
  }
1042
- totalSize += fileBinary.byteLength;
1043
- if (totalSize > PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES) {
1044
- throw ERRORS.TypedError(
1045
- HardwareErrorCode.RuntimeError,
1046
- 'Protocol V2 local resource ZIP exceeds the total size limit',
1047
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1048
- );
1049
- }
1050
- const header =
1051
- fileBinary.byteLength >= PROTOCOL_V2_OKPP_HEADER_SIZE
1052
- ? parseProtocolV2OkppHeader(new Uint8Array(fileBinary))
1053
- : null;
1054
- resourceBundles.push({
1055
- name: file.original_name || `resource-${index}`,
1056
- binary: fileBinary,
1057
- devicePath: file.device_path,
1058
- ...(header
1059
- ? {
1060
- version: header.version,
1061
- payloadHash: header.payloadHash,
1062
- headerHash: header.headerHash,
1063
- }
1064
- : {}),
1065
- });
1282
+ materializedEntries.push({ entryName: file.archive_path, binary: fileBinary });
1066
1283
  }
1067
- return resourceBundles;
1284
+ return { binary, materializedEntries };
1068
1285
  }
1069
1286
 
1070
1287
  private async prepareProtocolV2ResourceSources(): Promise<ProtocolV2ResourceBundleSource[]> {
@@ -1237,7 +1454,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1237
1454
  for (const [target, version] of Object.entries(expected)) {
1238
1455
  if (
1239
1456
  !Array.from(PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.values()).includes(
1240
- target as FirmwareUpdateV4Target
1457
+ target as Exclude<FirmwareUpdateV4Target, 'boot_resources'>
1241
1458
  ) ||
1242
1459
  typeof version !== 'string' ||
1243
1460
  !/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/u.test(version) ||
@@ -1414,9 +1631,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1414
1631
  return target ? [target] : [];
1415
1632
  })
1416
1633
  );
1417
- return (this.params.targetsToUpdate ?? []).filter(
1418
- target => target !== 'resource' && !preparedTargets.has(target)
1419
- );
1634
+ return (this.params.targetsToUpdate ?? [])
1635
+ .filter(
1636
+ (target): target is Exclude<FirmwareUpdateV4Target, 'resource' | 'boot_resources'> =>
1637
+ target !== 'resource' && target !== 'boot_resources'
1638
+ )
1639
+ .filter(target => !preparedTargets.has(target));
1420
1640
  }
1421
1641
 
1422
1642
  private buildProtocolV2InstallItems({
@@ -1487,8 +1707,28 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1487
1707
  `Missing Protocol V2 firmware component url: ${key}/${component.target}`
1488
1708
  );
1489
1709
  }
1710
+ const expectedFingerprint = normalizeProtocolV2Hex(component.fingerprint);
1711
+ if (
1712
+ !Number.isSafeInteger(component.expectedSize) ||
1713
+ Number(component.expectedSize) <= 0 ||
1714
+ !expectedFingerprint ||
1715
+ !/^[0-9a-f]{64}$/u.test(expectedFingerprint)
1716
+ ) {
1717
+ throw ERRORS.TypedError(
1718
+ HardwareErrorCode.RuntimeError,
1719
+ `Protocol V2 firmware component integrity metadata is invalid: ${key}/${component.target}`,
1720
+ { firmwareUpdateCode: 'FirmwarePlanInvalid' }
1721
+ );
1722
+ }
1490
1723
 
1491
1724
  const { binary } = await getSysResourceBinary(component.url);
1725
+ if (binary.byteLength !== component.expectedSize) {
1726
+ throw ERRORS.TypedError(
1727
+ HardwareErrorCode.RuntimeError,
1728
+ `Protocol V2 firmware size mismatch: ${key}/${component.target}`,
1729
+ { firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
1730
+ );
1731
+ }
1492
1732
  if (!isProtocolV2FirmwareFingerprintValid(binary, component.fingerprint)) {
1493
1733
  throw ERRORS.TypedError(
1494
1734
  HardwareErrorCode.RuntimeError,
@@ -61,14 +61,19 @@ export function prepareFirmwareUpdateV4MemoryHost({
61
61
  const hostId = `${Date.now()}:${memoryHostSequence}`;
62
62
  const binaries = new Map<string, Uint8Array>();
63
63
  const inputs = artifacts.map((input, artifactIndex) => {
64
- const artifact = createReference(input.binary, `${hostId}:artifact:${artifactIndex}`);
65
- binaries.set(artifact.artifactRef, new Uint8Array(input.binary));
64
+ const artifactBinary = new Uint8Array(input.binary).slice();
65
+ const artifact = createReference(
66
+ artifactBinary.buffer as ArrayBuffer,
67
+ `${hostId}:artifact:${artifactIndex}`
68
+ );
69
+ binaries.set(artifact.artifactRef, artifactBinary);
66
70
  const materializedEntries = input.materializedEntries?.map((entry, entryIndex) => {
71
+ const entryBinary = new Uint8Array(entry.binary).slice();
67
72
  const entryArtifact = createReference(
68
- entry.binary,
73
+ entryBinary.buffer as ArrayBuffer,
69
74
  `${hostId}:entry:${artifactIndex}:${entryIndex}`
70
75
  );
71
- binaries.set(entryArtifact.artifactRef, new Uint8Array(entry.binary));
76
+ binaries.set(entryArtifact.artifactRef, entryBinary);
72
77
  return {
73
78
  entryName: entry.entryName,
74
79
  artifact: entryArtifact,
@@ -504,6 +504,13 @@ const buildProtocolV2Artifacts = (
504
504
  );
505
505
  }
506
506
  componentTargetSet.add(target);
507
+ const integrity = asIntegrity({
508
+ size: component.expectedSize,
509
+ sha256: component.fingerprint,
510
+ });
511
+ if (integrity.expectedSize === undefined || integrity.expectedSha256 === undefined) {
512
+ planError(`Protocol V2 component ${key} integrity metadata is invalid`);
513
+ }
507
514
  artifacts.push({
508
515
  artifactId: `component:${target}`,
509
516
  role: 'component',
@@ -511,10 +518,7 @@ const buildProtocolV2Artifacts = (
511
518
  url: assertArtifactUrl(component.url, `Protocol V2 component ${key}`),
512
519
  container: 'raw',
513
520
  logicalName: key,
514
- ...asIntegrity({
515
- size: component.expectedSize,
516
- sha256: component.fingerprint,
517
- }),
521
+ ...integrity,
518
522
  ...(asVersion(component.version) ? { targetVersion: asVersion(component.version) } : {}),
519
523
  });
520
524
  targets.push(target);
@@ -632,6 +636,47 @@ const finalizeFirmwareUpdatePlan = ({
632
636
  });
633
637
  };
634
638
 
639
+ export type ProtocolV2LocalFirmwareUpdatePlanArtifact = {
640
+ artifactId: string;
641
+ target: Exclude<FirmwareUpdateV4Target, 'boot_resources'>;
642
+ container: 'raw' | 'zip';
643
+ logicalName: string;
644
+ expectedSize: number;
645
+ expectedSha256: string;
646
+ targetVersion?: string;
647
+ };
648
+
649
+ export const buildProtocolV2LocalFirmwareUpdatePlan = ({
650
+ features,
651
+ firmwareType,
652
+ platform,
653
+ artifacts,
654
+ }: {
655
+ features: Features;
656
+ firmwareType: EFirmwareType;
657
+ platform: FirmwareUpdatePlatform;
658
+ artifacts: ProtocolV2LocalFirmwareUpdatePlanArtifact[];
659
+ }): FirmwareUpdatePlan => {
660
+ if (artifacts.length === 0) {
661
+ return planError('Protocol V2 local firmware plan has no artifacts');
662
+ }
663
+ const plan = finalizeFirmwareUpdatePlan({
664
+ features,
665
+ firmwareType,
666
+ platform,
667
+ artifacts: artifacts.map(artifact => ({
668
+ ...artifact,
669
+ role: artifact.target === 'resource' ? 'resourceBundle' : 'component',
670
+ url: `https://local-firmware.invalid/${encodeURIComponent(artifact.artifactId)}`,
671
+ })),
672
+ targetsToUpdate: artifacts.map(artifact => artifact.target),
673
+ });
674
+ if (plan.executor !== 'v4') {
675
+ return planError('Protocol V2 local firmware plan requires executor v4');
676
+ }
677
+ return plan;
678
+ };
679
+
635
680
  export const buildProtocolV2FirmwareUpdatePlan = ({
636
681
  features,
637
682
  firmwareType,
@@ -120,6 +120,8 @@ export interface FirmwareUpdateV3Params {
120
120
  */
121
121
  export type FirmwareUpdateV4Target =
122
122
  | 'boot'
123
+ /** @deprecated Use resource with resourceArchiveBinary. */
124
+ | 'boot_resources'
123
125
  | 'app_v1'
124
126
  | 'app_v2'
125
127
  | 'coprocessor'
@@ -154,13 +156,28 @@ export interface FirmwareUpdateV4Params {
154
156
  se02Binary?: ArrayBuffer;
155
157
  se03Binary?: ArrayBuffer;
156
158
  se04Binary?: ArrayBuffer;
157
- /** Complete Protocol V2 resource ZIP for local development; never bound to a remote Plan. */
159
+ /** Complete Protocol V2 resource ZIP for local development; Core converts it to a local PreparedPlan. */
158
160
  resourceArchiveBinary?: ArrayBuffer;
161
+ /** @deprecated Package the complete signed resource set as a ZIP and use resourceArchiveBinary. */
162
+ resourceFiles?: Array<{
163
+ binary: ArrayBuffer;
164
+ devicePath: string;
165
+ size?: number;
166
+ fileHash?: string;
167
+ }>;
159
168
  forcedUpdateRes?: boolean;
160
169
  artifactReader?: FirmwareArtifactReader;
161
170
  componentArtifacts?: Partial<
162
- Record<Exclude<FirmwareUpdateV4Target, 'resource'>, FirmwareArtifactReference>
171
+ Record<
172
+ Exclude<FirmwareUpdateV4Target, 'resource' | 'boot_resources'>,
173
+ FirmwareArtifactReference
174
+ >
163
175
  >;
176
+ /** @deprecated Use a ZIP artifact in preparedPlan instead. */
177
+ resourceBundleArtifacts?: Array<{
178
+ name: string;
179
+ artifact: FirmwareArtifactReference;
180
+ }>;
164
181
  }
165
182
 
166
183
  export declare function registerFirmwareUpdateHostBinding(