@onekeyfe/hd-core 1.2.2-alpha.1 → 1.2.2-alpha.10

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 (34) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +2 -2
  2. package/__tests__/device-lifecycle-events.test.ts +67 -0
  3. package/__tests__/open-wallet-session.test.ts +613 -80
  4. package/__tests__/pro2HostAssetPackage.test.ts +108 -1
  5. package/__tests__/protocol-v2.test.ts +194 -49
  6. package/__tests__/protocolV2FileWrite.test.ts +40 -0
  7. package/dist/api/FirmwareUpdateV4.d.ts +1 -0
  8. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  9. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  10. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  11. package/dist/api/helpers/protocolV2FileWrite.d.ts +1 -0
  12. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  13. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  14. package/dist/core/RequestQueue.d.ts +2 -0
  15. package/dist/core/RequestQueue.d.ts.map +1 -1
  16. package/dist/core/index.d.ts +2 -1
  17. package/dist/core/index.d.ts.map +1 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.js +1411 -1117
  20. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  21. package/dist/utils/patch.d.ts +1 -1
  22. package/dist/utils/patch.d.ts.map +1 -1
  23. package/dist/utils/pro2HostAssetPackage.d.ts.map +1 -1
  24. package/package.json +4 -4
  25. package/src/api/FirmwareUpdateV4.ts +42 -10
  26. package/src/api/OpenWalletSession.ts +0 -3
  27. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +3 -1
  28. package/src/api/helpers/protocolV2FileWrite.ts +11 -5
  29. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +8 -2
  30. package/src/core/RequestQueue.ts +16 -1
  31. package/src/core/index.ts +48 -21
  32. package/src/data/messages/messages-protocol-v2.json +1471 -1367
  33. package/src/protocols/protocol-v2/walletSession.ts +157 -38
  34. package/src/utils/pro2HostAssetPackage.ts +73 -27
@@ -63,32 +63,41 @@ const negotiateEventlessWalletSession = async (device: Device) => {
63
63
  const getDeviceSession = async (device: Device, request: DeviceSessionGet) =>
64
64
  device.commands.typedCall('DeviceSessionGet', 'DeviceSession', request);
65
65
 
66
+ const STANDARD_SEED_DOMAINS = [DeviceSessionSeedDomain.SeedDomain_Standard];
67
+ const CARDANO_SEED_DOMAINS = [
68
+ DeviceSessionSeedDomain.SeedDomain_Standard,
69
+ DeviceSessionSeedDomain.SeedDomain_Cardano,
70
+ ];
71
+
72
+ const buildDeviceSessionSeedDomains = (deriveCardano?: boolean): DeviceSessionSeedDomain[] =>
73
+ deriveCardano === true ? CARDANO_SEED_DOMAINS : STANDARD_SEED_DOMAINS;
74
+
75
+ const deviceSessionHasCardano = (message: { seed_domains?: DeviceSessionSeedDomain[] }) =>
76
+ Array.isArray(message.seed_domains) &&
77
+ message.seed_domains.includes(DeviceSessionSeedDomain.SeedDomain_Cardano);
78
+
79
+ // origin/dev Get is read/resume only. Seed generation lives on AskPassphrase.
66
80
  const buildDeviceSessionGetRequest = ({
67
81
  sessionId,
68
82
  expectedPassphraseState,
69
- deriveCardano,
70
83
  }: {
71
84
  sessionId?: string;
72
85
  expectedPassphraseState?: string;
73
- deriveCardano?: boolean;
74
86
  } = {}): DeviceSessionGet => ({
75
87
  ...(sessionId ? { session_id: sessionId } : {}),
76
88
  ...(expectedPassphraseState ? { btc_test_address: expectedPassphraseState } : {}),
77
- seed_domains:
78
- deriveCardano === undefined
79
- ? []
80
- : [
81
- DeviceSessionSeedDomain.SeedDomain_Standard,
82
- ...(deriveCardano ? [DeviceSessionSeedDomain.SeedDomain_Cardano] : []),
83
- ],
84
89
  });
85
90
 
86
91
  const askDevicePassphrase = async (
87
92
  device: Device,
88
- requestPayload: DeviceSessionAskPassphrase,
93
+ requestPayload: Omit<DeviceSessionAskPassphrase, 'seed_domains'>,
94
+ deriveCardano?: boolean,
89
95
  onStatusRefreshed?: () => void
90
96
  ) => {
91
- await device.commands.typedCall('DeviceSessionAskPassphrase', 'Success', requestPayload);
97
+ await device.commands.typedCall('DeviceSessionAskPassphrase', 'Success', {
98
+ ...requestPayload,
99
+ seed_domains: buildDeviceSessionSeedDomains(deriveCardano),
100
+ });
92
101
  await refreshProtocolV2DeviceStatus(device);
93
102
  onStatusRefreshed?.();
94
103
  };
@@ -160,7 +169,22 @@ const selectDeviceSession = async (
160
169
  interaction: attachPinInteraction,
161
170
  });
162
171
  onStatusRefreshed?.();
163
- return getDeviceSession(device, buildDeviceSessionGetRequest({ deriveCardano }));
172
+ if (deriveCardano === true) {
173
+ await askDevicePassphrase(
174
+ device,
175
+ { passphrase: '', on_device: false },
176
+ true,
177
+ onStatusRefreshed
178
+ );
179
+ // Firmware AskPassphrase Success clears unlocked_by_attach_to_pin.
180
+ // Identity is still the Attach PIN wallet; keep the SDK flag so a later
181
+ // passphrase picker still locks instead of prompting.
182
+ if (device.features) {
183
+ device.features.unlockedAttachPin = true;
184
+ }
185
+ }
186
+ const attachPinSession = await getDeviceSession(device, buildDeviceSessionGetRequest());
187
+ return Object.assign(attachPinSession, { viaAttachPin: true as const });
164
188
  }
165
189
 
166
190
  if (hasHostPassphrase) {
@@ -170,9 +194,10 @@ const selectDeviceSession = async (
170
194
  passphrase: hostPassphrase,
171
195
  on_device: false,
172
196
  },
197
+ deriveCardano,
173
198
  onStatusRefreshed
174
199
  );
175
- return getDeviceSession(device, buildDeviceSessionGetRequest({ deriveCardano }));
200
+ return getDeviceSession(device, buildDeviceSessionGetRequest());
176
201
  }
177
202
 
178
203
  const passphraseOnDeviceInteraction = device.createProtocolV2UiPhaseMetadata?.(
@@ -183,8 +208,8 @@ const selectDeviceSession = async (
183
208
  ...metadata,
184
209
  ...(passphraseOnDeviceInteraction ? { interaction: passphraseOnDeviceInteraction } : {}),
185
210
  });
186
- await askDevicePassphrase(device, { on_device: true }, onStatusRefreshed);
187
- return getDeviceSession(device, buildDeviceSessionGetRequest({ deriveCardano }));
211
+ await askDevicePassphrase(device, { on_device: true }, deriveCardano, onStatusRefreshed);
212
+ return getDeviceSession(device, buildDeviceSessionGetRequest());
188
213
  };
189
214
 
190
215
  export async function getProtocolV2WalletSession(
@@ -209,6 +234,10 @@ export async function getProtocolV2WalletSession(
209
234
  const forceWalletSelection =
210
235
  options?.forceWalletSelection === true || options?.initSession === true;
211
236
  const readCurrentAttachPinSession = options?.readCurrentAttachPinSession === true;
237
+ const sessionIsAttachPinWallet = (session?: { viaAttachPin?: boolean }) =>
238
+ readCurrentAttachPinSession ||
239
+ session?.viaAttachPin === true ||
240
+ device.features?.unlockedAttachPin === true;
212
241
 
213
242
  if (forceWalletSelection) {
214
243
  if (options.onlyMainPin) {
@@ -244,6 +273,10 @@ export async function getProtocolV2WalletSession(
244
273
  const markWalletStatusRefreshed = () => {
245
274
  walletStatusRefreshed = true;
246
275
  };
276
+ if (options?.onlyMainPin && options.mainPinSelected !== true) {
277
+ await refreshProtocolV2DeviceStatus(device);
278
+ markWalletStatusRefreshed();
279
+ }
247
280
  let mainPinAuthenticated =
248
281
  options?.mainPinSelected === true ||
249
282
  (options?.onlyMainPin === true &&
@@ -259,6 +292,38 @@ export async function getProtocolV2WalletSession(
259
292
  }
260
293
  };
261
294
 
295
+ const sessionGetRequest = ({
296
+ sessionId,
297
+ expectedPassphraseState: passphraseState,
298
+ }: {
299
+ sessionId?: string;
300
+ expectedPassphraseState?: string;
301
+ } = {}) =>
302
+ buildDeviceSessionGetRequest({
303
+ sessionId,
304
+ expectedPassphraseState: passphraseState,
305
+ });
306
+
307
+ const askEmptyPassphraseAndGet = async ({
308
+ deriveCardano,
309
+ keepAttachPin,
310
+ }: {
311
+ deriveCardano?: boolean;
312
+ keepAttachPin?: boolean;
313
+ } = {}) => {
314
+ await askDevicePassphrase(
315
+ device,
316
+ { passphrase: '', on_device: false },
317
+ deriveCardano,
318
+ markWalletStatusRefreshed
319
+ );
320
+ if (keepAttachPin && device.features) {
321
+ device.features.unlockedAttachPin = true;
322
+ }
323
+ const session = await getDeviceSession(device, buildDeviceSessionGetRequest());
324
+ return keepAttachPin ? Object.assign(session, { viaAttachPin: true as const }) : session;
325
+ };
326
+
262
327
  const rejectMismatchedAttachPinWallet = async () => {
263
328
  const features = await refreshProtocolV2DeviceStatus(device);
264
329
  markWalletStatusRefreshed();
@@ -280,6 +345,19 @@ export async function getProtocolV2WalletSession(
280
345
  throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckUnlockTypeError);
281
346
  };
282
347
 
348
+ // The passphrase picker would prompt. Empty host AskPassphrase does not;
349
+ // that path is Attach PIN / standard Cardano. Switching to a different
350
+ // passphrase wallet still locks first.
351
+ const lockAttachPinBeforePassphraseSelection = async () => {
352
+ if (readCurrentAttachPinSession || options?.onlyMainPin) {
353
+ return;
354
+ }
355
+ if (device.features?.unlockedAttachPin !== true) {
356
+ return;
357
+ }
358
+ await rejectMismatchedAttachPinWallet();
359
+ };
360
+
283
361
  if (options?.onlyMainPin && options.rejectAttachPinForMainWallet) {
284
362
  await rejectMismatchedAttachPinWallet();
285
363
  }
@@ -297,7 +375,7 @@ export async function getProtocolV2WalletSession(
297
375
  }
298
376
  };
299
377
 
300
- const selectStandardWallet = async () => {
378
+ const selectStandardWallet = async (forceMainPin = false) => {
301
379
  if (device.features?.passphraseProtection === true) {
302
380
  // Main PIN authenticates the device; an empty host passphrase selects the standard derivation.
303
381
  await selectMainPin();
@@ -307,13 +385,16 @@ export async function getProtocolV2WalletSession(
307
385
  passphrase: '',
308
386
  on_device: false,
309
387
  },
388
+ options?.deriveCardano,
310
389
  markWalletStatusRefreshed
311
390
  );
312
391
  standardWalletSelected = true;
313
392
  } else if (!standardWalletSelected) {
314
393
  // Without passphrase protection there is no empty-passphrase selector.
315
- // Main PIN selection is the only authoritative switch back to the standard wallet.
316
- await selectMainPin(true);
394
+ // An unlocked non-Attach-PIN device is already in the only available wallet context.
395
+ // Force Main PIN only when recovering from a mismatched cached standard session.
396
+ await selectMainPin(forceMainPin);
397
+ standardWalletSelected = true;
317
398
  }
318
399
  };
319
400
 
@@ -329,10 +410,7 @@ export async function getProtocolV2WalletSession(
329
410
  device.clearInternalState();
330
411
  throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckUnlockTypeError);
331
412
  }
332
- response = await getDeviceSession(
333
- device,
334
- buildDeviceSessionGetRequest({ deriveCardano: options?.deriveCardano })
335
- );
413
+ response = await getDeviceSession(device, sessionGetRequest());
336
414
  } else if (options?.onlyMainPin) {
337
415
  expectedPassphraseState = cachedStandardSession?.passphraseState;
338
416
  if (cachedStandardSession) {
@@ -342,10 +420,9 @@ export async function getProtocolV2WalletSession(
342
420
  }
343
421
  response = await getDeviceSession(
344
422
  device,
345
- buildDeviceSessionGetRequest({
423
+ sessionGetRequest({
346
424
  sessionId: cachedStandardSession.sessionId,
347
425
  expectedPassphraseState,
348
- deriveCardano: options?.deriveCardano,
349
426
  })
350
427
  );
351
428
  resumed = true;
@@ -360,19 +437,15 @@ export async function getProtocolV2WalletSession(
360
437
 
361
438
  if (!response) {
362
439
  await selectStandardWallet();
363
- response = await getDeviceSession(
364
- device,
365
- buildDeviceSessionGetRequest({ deriveCardano: options?.deriveCardano })
366
- );
440
+ response = await getDeviceSession(device, sessionGetRequest());
367
441
  }
368
442
  } else if (cachedSessionId && expectedPassphraseState) {
369
443
  try {
370
444
  response = await getDeviceSession(
371
445
  device,
372
- buildDeviceSessionGetRequest({
446
+ sessionGetRequest({
373
447
  sessionId: cachedSessionId,
374
448
  expectedPassphraseState,
375
- deriveCardano: options?.deriveCardano,
376
449
  })
377
450
  );
378
451
  resumed = true;
@@ -387,9 +460,8 @@ export async function getProtocolV2WalletSession(
387
460
  try {
388
461
  response = await getDeviceSession(
389
462
  device,
390
- buildDeviceSessionGetRequest({
463
+ sessionGetRequest({
391
464
  expectedPassphraseState,
392
- deriveCardano: options?.deriveCardano,
393
465
  })
394
466
  );
395
467
  } catch (error) {
@@ -404,6 +476,7 @@ export async function getProtocolV2WalletSession(
404
476
  device.clearInternalState();
405
477
  throw ERRORS.TypedError(HardwareErrorCode.WalletSessionInvalid);
406
478
  }
479
+ await lockAttachPinBeforePassphraseSelection();
407
480
  response = await selectDeviceSession(
408
481
  device,
409
482
  expectedPassphraseState,
@@ -432,13 +505,11 @@ export async function getProtocolV2WalletSession(
432
505
  }
433
506
  if (options?.onlyMainPin) {
434
507
  device.clearStandardInternalState?.();
435
- await selectStandardWallet();
436
- response = await getDeviceSession(
437
- device,
438
- buildDeviceSessionGetRequest({ deriveCardano: options?.deriveCardano })
439
- );
508
+ await selectStandardWallet(true);
509
+ response = await getDeviceSession(device, sessionGetRequest());
440
510
  } else {
441
511
  device.clearInternalState();
512
+ await lockAttachPinBeforePassphraseSelection();
442
513
  response = await selectDeviceSession(
443
514
  device,
444
515
  expectedPassphraseState,
@@ -464,6 +535,54 @@ export async function getProtocolV2WalletSession(
464
535
  }
465
536
  }
466
537
 
538
+ // origin/dev generates Cardano on AskPassphrase, not Get. Empty host
539
+ // passphrase is the Attach PIN / standard-wallet secret. Hidden wallets
540
+ // still need a real passphrase Ask. Passphrase-off Get auto-requests Cardano.
541
+ if (options?.deriveCardano === true && !deviceSessionHasCardano(message)) {
542
+ if (options?.resumeOnly) {
543
+ device.clearInternalState();
544
+ throw ERRORS.TypedError(HardwareErrorCode.WalletSessionInvalid);
545
+ }
546
+ resumed = false;
547
+ const previousAddress = message.btc_test_address;
548
+ if (sessionIsAttachPinWallet(response)) {
549
+ response = await askEmptyPassphraseAndGet({ deriveCardano: true, keepAttachPin: true });
550
+ } else if (device.features?.passphraseProtection === false) {
551
+ response = await getDeviceSession(device, buildDeviceSessionGetRequest());
552
+ } else if (options?.onlyMainPin) {
553
+ await selectMainPin();
554
+ response = await askEmptyPassphraseAndGet({ deriveCardano: true });
555
+ } else {
556
+ await lockAttachPinBeforePassphraseSelection();
557
+ response = await selectDeviceSession(
558
+ device,
559
+ expectedPassphraseState,
560
+ true,
561
+ markWalletStatusRefreshed
562
+ );
563
+ }
564
+ message = response.message;
565
+ try {
566
+ assertCompleteDeviceSession(message);
567
+ } catch (error) {
568
+ if (options?.onlyMainPin) {
569
+ device.clearStandardInternalState?.();
570
+ } else {
571
+ device.clearInternalState();
572
+ }
573
+ throw error;
574
+ }
575
+ if (previousAddress && previousAddress !== message.btc_test_address) {
576
+ await rejectMismatchedAttachPinWallet();
577
+ clearCurrentWalletSession();
578
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckPassphraseStateError);
579
+ }
580
+ if (!deviceSessionHasCardano(message)) {
581
+ clearCurrentWalletSession();
582
+ throw ERRORS.TypedError(HardwareErrorCode.WalletSessionInvalid);
583
+ }
584
+ }
585
+
467
586
  const internalStateArgs = [
468
587
  true,
469
588
  message.btc_test_address,
@@ -478,7 +597,7 @@ export async function getProtocolV2WalletSession(
478
597
  }
479
598
 
480
599
  let unlockedAttachPin: boolean | undefined;
481
- if (readCurrentAttachPinSession) {
600
+ if (readCurrentAttachPinSession || sessionIsAttachPinWallet(response)) {
482
601
  unlockedAttachPin = true;
483
602
  } else if (mainPinAuthenticated) {
484
603
  unlockedAttachPin = false;
@@ -37,7 +37,9 @@ const ARCHIVE_ENTRY_SIZE = 296;
37
37
  const ARCHIVE_ENTRY_NAME_MAX_LENGTH = 255;
38
38
  const ARCHIVE_COMPRESS_LZ4_BLOCKED = 1;
39
39
  const ARCHIVE_ALIGNMENT = 4;
40
- const LZ4_BLOCK_SIZE_LOG2 = 12;
40
+ const LZ4_PREFERRED_BLOCK_SIZE_LOG2 = 14;
41
+ const LZ4_FALLBACK_BLOCK_SIZE_LOG2 = 13;
42
+ const LZ4_COMPRESSED_BLOCK_SIZE_MAX = 1 << 14;
41
43
 
42
44
  export type Pro2HostAssetPackageEntry = {
43
45
  name: string;
@@ -101,7 +103,7 @@ const LZ4_MAX_OFFSET = 0xffff;
101
103
  const LZ4_LENGTH_MASK = 15;
102
104
  const LZ4_HASH_LOG = 16;
103
105
  const LZ4_HASH_MULTIPLIER = 2654435761;
104
- const LZ4_SKIP_TRIGGER = 6;
106
+ const LZ4_MAX_SEARCH_DEPTH = 64;
105
107
 
106
108
  function writeExtendedLength(output: Uint8Array, offset: number, length: number): number {
107
109
  let remaining = length;
@@ -178,7 +180,11 @@ function emitLastLiterals(
178
180
  return copyBytes(output, nextOffset, input, anchor, literalLength);
179
181
  }
180
182
 
181
- function compressRawLz4Block(input: Uint8Array, hashTable: Uint32Array): Uint8Array {
183
+ function compressRawLz4Block(
184
+ input: Uint8Array,
185
+ hashTable: Uint32Array,
186
+ matchChain: Int32Array
187
+ ): Uint8Array {
182
188
  const output = new Uint8Array(input.byteLength + Math.floor(input.byteLength / 255) + 16);
183
189
  const inputView = new DataView(input.buffer, input.byteOffset, input.byteLength);
184
190
  const matchFindLimit = input.byteLength - LZ4_MATCH_FIND_LIMIT;
@@ -186,42 +192,65 @@ function compressRawLz4Block(input: Uint8Array, hashTable: Uint32Array): Uint8Ar
186
192
  let anchor = 0;
187
193
  let inputOffset = 0;
188
194
  let outputOffset = 0;
189
- let searchMatchCount = 1 << LZ4_SKIP_TRIGGER;
190
195
 
191
196
  hashTable.fill(0);
192
197
  while (inputOffset < matchFindLimit) {
193
198
  const sequence = inputView.getUint32(inputOffset, true);
194
199
  const hash = Math.imul(sequence, LZ4_HASH_MULTIPLIER) >>> (32 - LZ4_HASH_LOG);
195
- const candidate = hashTable[hash] - 1;
200
+ let candidate = hashTable[hash] - 1;
201
+ matchChain[inputOffset] = candidate;
196
202
  hashTable[hash] = inputOffset + 1;
197
203
 
198
- const hasMatch = !(
199
- candidate < 0 ||
200
- inputOffset - candidate > LZ4_MAX_OFFSET ||
201
- inputView.getUint32(candidate, true) !== sequence
202
- );
203
- if (!hasMatch) {
204
- inputOffset += searchMatchCount >> LZ4_SKIP_TRIGGER;
205
- searchMatchCount += 1;
206
- } else {
207
- searchMatchCount = 1 << LZ4_SKIP_TRIGGER;
208
- let matchEnd = inputOffset + LZ4_MIN_MATCH;
209
- let reference = candidate + LZ4_MIN_MATCH;
210
- while (matchEnd < matchExtendLimit && input[matchEnd] === input[reference]) {
211
- matchEnd += 1;
212
- reference += 1;
204
+ let bestCandidate = -1;
205
+ let bestMatchEnd = inputOffset;
206
+ let searchDepth = 0;
207
+ while (
208
+ candidate >= 0 &&
209
+ inputOffset - candidate <= LZ4_MAX_OFFSET &&
210
+ searchDepth < LZ4_MAX_SEARCH_DEPTH
211
+ ) {
212
+ if (inputView.getUint32(candidate, true) === sequence) {
213
+ let matchEnd = inputOffset + LZ4_MIN_MATCH;
214
+ let reference = candidate + LZ4_MIN_MATCH;
215
+ while (matchEnd < matchExtendLimit && input[matchEnd] === input[reference]) {
216
+ matchEnd += 1;
217
+ reference += 1;
218
+ }
219
+ if (matchEnd > bestMatchEnd) {
220
+ bestCandidate = candidate;
221
+ bestMatchEnd = matchEnd;
222
+ }
213
223
  }
224
+ candidate = matchChain[candidate];
225
+ searchDepth += 1;
226
+ }
227
+
228
+ if (bestCandidate < 0) {
229
+ inputOffset += 1;
230
+ } else {
231
+ const matchStart = inputOffset;
214
232
  outputOffset = emitSequence(
215
233
  output,
216
234
  outputOffset,
217
235
  input,
218
236
  anchor,
219
237
  inputOffset - anchor,
220
- inputOffset - candidate,
221
- matchEnd - inputOffset - LZ4_MIN_MATCH
238
+ inputOffset - bestCandidate,
239
+ bestMatchEnd - inputOffset - LZ4_MIN_MATCH
222
240
  );
223
- inputOffset = matchEnd;
241
+ inputOffset = bestMatchEnd;
224
242
  anchor = inputOffset;
243
+
244
+ for (
245
+ let skippedOffset = matchStart + 1;
246
+ skippedOffset < inputOffset && skippedOffset < matchFindLimit;
247
+ skippedOffset += 1
248
+ ) {
249
+ const skippedSequence = inputView.getUint32(skippedOffset, true);
250
+ const skippedHash = Math.imul(skippedSequence, LZ4_HASH_MULTIPLIER) >>> (32 - LZ4_HASH_LOG);
251
+ matchChain[skippedOffset] = hashTable[skippedHash] - 1;
252
+ hashTable[skippedHash] = skippedOffset + 1;
253
+ }
225
254
  }
226
255
  }
227
256
 
@@ -234,27 +263,44 @@ function compressRawLz4Block(input: Uint8Array, hashTable: Uint32Array): Uint8Ar
234
263
  // The archive stores an 8-byte descriptor, one compressed-size value per
235
264
  // block, then the concatenated raw blocks. Blocks are independent so firmware
236
265
  // can validate and decompress them with bounded memory.
237
- function encodeLz4Blocked(data: Uint8Array): Uint8Array {
238
- const blockSize = 1 << LZ4_BLOCK_SIZE_LOG2;
266
+ function encodeLz4BlockedWithBlockSize(
267
+ data: Uint8Array,
268
+ blockSizeLog2: number
269
+ ): Uint8Array | undefined {
270
+ const blockSize = 1 << blockSizeLog2;
239
271
  const blockCount = Math.ceil(data.byteLength / blockSize);
240
272
  const hashTable = new Uint32Array(1 << LZ4_HASH_LOG);
273
+ const matchChain = new Int32Array(blockSize);
241
274
  const blocks: Uint8Array[] = [];
242
275
  const header = new Uint8Array(8 + blockCount * 4);
243
276
  const headerView = new DataView(header.buffer);
244
277
  headerView.setUint16(0, blockCount, true);
245
- headerView.setUint16(2, LZ4_BLOCK_SIZE_LOG2, true);
278
+ headerView.setUint16(2, blockSizeLog2, true);
246
279
 
247
280
  for (let index = 0; index < blockCount; index += 1) {
248
281
  const block = compressRawLz4Block(
249
282
  data.subarray(index * blockSize, Math.min((index + 1) * blockSize, data.byteLength)),
250
- hashTable
283
+ hashTable,
284
+ matchChain
251
285
  );
286
+ if (block.byteLength > LZ4_COMPRESSED_BLOCK_SIZE_MAX) return undefined;
252
287
  headerView.setUint32(8 + index * 4, block.byteLength, true);
253
288
  blocks.push(block);
254
289
  }
255
290
  return concatBytes([header, ...blocks]);
256
291
  }
257
292
 
293
+ function encodeLz4Blocked(data: Uint8Array): Uint8Array {
294
+ const preferred = encodeLz4BlockedWithBlockSize(data, LZ4_PREFERRED_BLOCK_SIZE_LOG2);
295
+ if (preferred) return preferred;
296
+
297
+ const fallback = encodeLz4BlockedWithBlockSize(data, LZ4_FALLBACK_BLOCK_SIZE_LOG2);
298
+ if (!fallback) {
299
+ throw new Error('Pro2 host asset package LZ4 block exceeds the firmware buffer limit.');
300
+ }
301
+ return fallback;
302
+ }
303
+
258
304
  // OKAR integrity helpers
259
305
  // ----------------------
260
306
  const CRC32_TABLE = (() => {