@onekeyfe/hd-core 1.2.2-alpha.5 → 1.2.2-alpha.7

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.
package/src/core/index.ts CHANGED
@@ -2,7 +2,11 @@ import semver from 'semver';
2
2
  import EventEmitter from 'events';
3
3
  import {
4
4
  DeviceSessionPinType,
5
+ type LowlevelTransportSharedPlugin,
6
+ type OneKeyDeviceInfo,
7
+ type ProtocolType,
5
8
  TRANSPORT_EVENT,
9
+ type TransportDeviceDisconnectEvent,
6
10
  isProtocolV2LinkDisabledError,
7
11
  } from '@onekeyfe/hd-transport';
8
12
  import {
@@ -74,11 +78,6 @@ import type { CoreMessage, IFrameCallMessage, UiPromise, UiPromiseResponse } fro
74
78
  import type { DeviceEvents, InitOptions, RunOptions } from '../device/Device';
75
79
  import type { SdkTracingContext } from '../utils/tracing';
76
80
  import type { Deferred } from '@onekeyfe/hd-shared';
77
- import type {
78
- LowlevelTransportSharedPlugin,
79
- OneKeyDeviceInfo,
80
- TransportDeviceDisconnectEvent,
81
- } from '@onekeyfe/hd-transport';
82
81
  import type { BaseMethod } from '../api/BaseMethod';
83
82
 
84
83
  const Log = getLogger(LoggerNames.Core);
@@ -1044,6 +1043,7 @@ async function connectDeviceForBle(
1044
1043
  !device.commands ||
1045
1044
  device.commands.disposed;
1046
1045
  if (shouldAcquire) {
1046
+ const connectProtocol = resolveBleConnectProtocol(method);
1047
1047
  // The deadline/abort guards are scoped to the desktop electron
1048
1048
  // transport: its IPC acquire is the only path with a proven
1049
1049
  // never-settling failure mode, while react-native/lowlevel acquire may
@@ -1055,13 +1055,13 @@ async function connectDeviceForBle(
1055
1055
  throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
1056
1056
  }
1057
1057
  if (!useAcquireGuards) {
1058
- await device.acquire(method.payload.connectProtocol, {
1058
+ await device.acquire(connectProtocol, {
1059
1059
  forceProtocolDetection: method.payload.forceProtocolDetection,
1060
1060
  });
1061
1061
  } else {
1062
1062
  try {
1063
1063
  await raceBleAcquire(
1064
- device.acquire(method.payload.connectProtocol, {
1064
+ device.acquire(connectProtocol, {
1065
1065
  forceProtocolDetection: method.payload.forceProtocolDetection,
1066
1066
  }),
1067
1067
  abortSignal
@@ -1127,6 +1127,14 @@ async function connectDeviceForBle(
1127
1127
  }
1128
1128
  }
1129
1129
 
1130
+ export function resolveBleConnectProtocol(method: BaseMethod): ProtocolType | undefined {
1131
+ if (method.payload.connectProtocol === 'V1' || method.payload.connectProtocol === 'V2') {
1132
+ return method.payload.connectProtocol;
1133
+ }
1134
+ const supportedProtocols = method.getSupportedProtocols();
1135
+ return supportedProtocols.length === 1 && supportedProtocols[0] === 'V2' ? 'V2' : undefined;
1136
+ }
1137
+
1130
1138
  type IPollFn<T> = (time?: number) => T;
1131
1139
  // eslint-disable-next-line @typescript-eslint/require-await
1132
1140
  const ensureConnected = async (
@@ -1333,7 +1341,10 @@ export const cancel = (context: CoreContext, connectId?: string) => {
1333
1341
  );
1334
1342
  // Abort before rejecting: rejectRequest releases the task and would make
1335
1343
  // its AbortController unreachable to an in-flight method loop.
1336
- requestQueue.abortRequestsByConnectId(connectId);
1344
+ // This branch rejects every queued request below. Abort the same set first so
1345
+ // methods whose physical connectId is selected internally (for example
1346
+ // Desktop WebUSB firmwareUpdateV4) cannot keep retrying after rejection.
1347
+ requestQueue.abortAllRequests();
1337
1348
  const canceledDevices: Device[] = [];
1338
1349
  const interruptDevice = (device: Device | undefined, deviceConnectId: string) => {
1339
1350
  if (!device || canceledDevices.includes(device)) {
@@ -12852,36 +12852,57 @@
12852
12852
  },
12853
12853
  "reserved": [[2, 2], "placeholder"]
12854
12854
  },
12855
- "ViewData": {
12855
+ "ViewContentPreview": {
12856
12856
  "fields": {
12857
+ "content_key": {
12858
+ "rule": "required",
12859
+ "type": "uint32",
12860
+ "id": 1
12861
+ },
12857
12862
  "preview": {
12858
12863
  "rule": "required",
12859
12864
  "type": "bytes",
12860
- "id": 1
12865
+ "id": 2
12861
12866
  },
12862
12867
  "total_bytes": {
12863
- "rule": "required",
12864
12868
  "type": "uint32",
12865
- "id": 2
12869
+ "id": 3
12866
12870
  }
12867
12871
  }
12868
12872
  },
12869
- "ViewDataPage": {
12873
+ "ViewContentEntry": {
12870
12874
  "fields": {
12871
- "chunk": {
12875
+ "entry_key": {
12872
12876
  "rule": "required",
12873
- "type": "bytes",
12877
+ "type": "uint32",
12874
12878
  "id": 1
12875
12879
  },
12880
+ "value": {
12881
+ "rule": "required",
12882
+ "type": "bytes",
12883
+ "id": 2
12884
+ }
12885
+ }
12886
+ },
12887
+ "ViewContentPage": {
12888
+ "fields": {
12876
12889
  "page_index": {
12877
12890
  "rule": "required",
12878
12891
  "type": "uint32",
12879
- "id": 2
12892
+ "id": 1
12880
12893
  },
12881
12894
  "page_count": {
12882
12895
  "rule": "required",
12883
12896
  "type": "uint32",
12897
+ "id": 2
12898
+ },
12899
+ "chunk": {
12900
+ "type": "bytes",
12884
12901
  "id": 3
12902
+ },
12903
+ "entry": {
12904
+ "type": "ViewContentEntry",
12905
+ "id": 4
12885
12906
  }
12886
12907
  }
12887
12908
  },
@@ -12940,8 +12961,8 @@
12940
12961
  "type": "string",
12941
12962
  "id": 9
12942
12963
  },
12943
- "data": {
12944
- "type": "ViewData",
12964
+ "content": {
12965
+ "type": "ViewContentPreview",
12945
12966
  "id": 10
12946
12967
  },
12947
12968
  "custom_field": {
@@ -12965,6 +12986,10 @@
12965
12986
  "text_arg": {
12966
12987
  "type": "string",
12967
12988
  "id": 3
12989
+ },
12990
+ "cancellable": {
12991
+ "type": "bool",
12992
+ "id": 4
12968
12993
  }
12969
12994
  }
12970
12995
  },
@@ -13003,6 +13028,10 @@
13003
13028
  "chain_id": {
13004
13029
  "type": "uint32",
13005
13030
  "id": 8
13031
+ },
13032
+ "content": {
13033
+ "type": "ViewContentPreview",
13034
+ "id": 9
13006
13035
  }
13007
13036
  }
13008
13037
  },
@@ -76,9 +76,7 @@ const deviceSessionHasCardano = (message: { seed_domains?: DeviceSessionSeedDoma
76
76
  Array.isArray(message.seed_domains) &&
77
77
  message.seed_domains.includes(DeviceSessionSeedDomain.SeedDomain_Cardano);
78
78
 
79
- // AskPassphrase.seed_domains selects which seeds to generate. DeviceSessionGet
80
- // only resumes or reads the current session; firmware rejects unknown Get
81
- // fields, including leftover seed_domains.
79
+ // origin/dev Get is read/resume only. Seed generation lives on AskPassphrase.
82
80
  const buildDeviceSessionGetRequest = ({
83
81
  sessionId,
84
82
  expectedPassphraseState,
@@ -171,7 +169,22 @@ const selectDeviceSession = async (
171
169
  interaction: attachPinInteraction,
172
170
  });
173
171
  onStatusRefreshed?.();
174
- return getDeviceSession(device, buildDeviceSessionGetRequest());
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 });
175
188
  }
176
189
 
177
190
  if (hasHostPassphrase) {
@@ -221,6 +234,10 @@ export async function getProtocolV2WalletSession(
221
234
  const forceWalletSelection =
222
235
  options?.forceWalletSelection === true || options?.initSession === true;
223
236
  const readCurrentAttachPinSession = options?.readCurrentAttachPinSession === true;
237
+ const sessionIsAttachPinWallet = (session?: { viaAttachPin?: boolean }) =>
238
+ readCurrentAttachPinSession ||
239
+ session?.viaAttachPin === true ||
240
+ device.features?.unlockedAttachPin === true;
224
241
 
225
242
  if (forceWalletSelection) {
226
243
  if (options.onlyMainPin) {
@@ -271,6 +288,38 @@ export async function getProtocolV2WalletSession(
271
288
  }
272
289
  };
273
290
 
291
+ const sessionGetRequest = ({
292
+ sessionId,
293
+ expectedPassphraseState: passphraseState,
294
+ }: {
295
+ sessionId?: string;
296
+ expectedPassphraseState?: string;
297
+ } = {}) =>
298
+ buildDeviceSessionGetRequest({
299
+ sessionId,
300
+ expectedPassphraseState: passphraseState,
301
+ });
302
+
303
+ const askEmptyPassphraseAndGet = async ({
304
+ deriveCardano,
305
+ keepAttachPin,
306
+ }: {
307
+ deriveCardano?: boolean;
308
+ keepAttachPin?: boolean;
309
+ } = {}) => {
310
+ await askDevicePassphrase(
311
+ device,
312
+ { passphrase: '', on_device: false },
313
+ deriveCardano,
314
+ markWalletStatusRefreshed
315
+ );
316
+ if (keepAttachPin && device.features) {
317
+ device.features.unlockedAttachPin = true;
318
+ }
319
+ const session = await getDeviceSession(device, buildDeviceSessionGetRequest());
320
+ return keepAttachPin ? Object.assign(session, { viaAttachPin: true as const }) : session;
321
+ };
322
+
274
323
  const rejectMismatchedAttachPinWallet = async () => {
275
324
  const features = await refreshProtocolV2DeviceStatus(device);
276
325
  markWalletStatusRefreshed();
@@ -292,6 +341,19 @@ export async function getProtocolV2WalletSession(
292
341
  throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckUnlockTypeError);
293
342
  };
294
343
 
344
+ // The passphrase picker would prompt. Empty host AskPassphrase does not;
345
+ // that path is Attach PIN / standard Cardano. Switching to a different
346
+ // passphrase wallet still locks first.
347
+ const lockAttachPinBeforePassphraseSelection = async () => {
348
+ if (readCurrentAttachPinSession || options?.onlyMainPin) {
349
+ return;
350
+ }
351
+ if (device.features?.unlockedAttachPin !== true) {
352
+ return;
353
+ }
354
+ await rejectMismatchedAttachPinWallet();
355
+ };
356
+
295
357
  if (options?.onlyMainPin && options.rejectAttachPinForMainWallet) {
296
358
  await rejectMismatchedAttachPinWallet();
297
359
  }
@@ -342,7 +404,7 @@ export async function getProtocolV2WalletSession(
342
404
  device.clearInternalState();
343
405
  throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckUnlockTypeError);
344
406
  }
345
- response = await getDeviceSession(device, buildDeviceSessionGetRequest());
407
+ response = await getDeviceSession(device, sessionGetRequest());
346
408
  } else if (options?.onlyMainPin) {
347
409
  expectedPassphraseState = cachedStandardSession?.passphraseState;
348
410
  if (cachedStandardSession) {
@@ -352,7 +414,7 @@ export async function getProtocolV2WalletSession(
352
414
  }
353
415
  response = await getDeviceSession(
354
416
  device,
355
- buildDeviceSessionGetRequest({
417
+ sessionGetRequest({
356
418
  sessionId: cachedStandardSession.sessionId,
357
419
  expectedPassphraseState,
358
420
  })
@@ -369,13 +431,13 @@ export async function getProtocolV2WalletSession(
369
431
 
370
432
  if (!response) {
371
433
  await selectStandardWallet();
372
- response = await getDeviceSession(device, buildDeviceSessionGetRequest());
434
+ response = await getDeviceSession(device, sessionGetRequest());
373
435
  }
374
436
  } else if (cachedSessionId && expectedPassphraseState) {
375
437
  try {
376
438
  response = await getDeviceSession(
377
439
  device,
378
- buildDeviceSessionGetRequest({
440
+ sessionGetRequest({
379
441
  sessionId: cachedSessionId,
380
442
  expectedPassphraseState,
381
443
  })
@@ -392,7 +454,7 @@ export async function getProtocolV2WalletSession(
392
454
  try {
393
455
  response = await getDeviceSession(
394
456
  device,
395
- buildDeviceSessionGetRequest({
457
+ sessionGetRequest({
396
458
  expectedPassphraseState,
397
459
  })
398
460
  );
@@ -408,6 +470,7 @@ export async function getProtocolV2WalletSession(
408
470
  device.clearInternalState();
409
471
  throw ERRORS.TypedError(HardwareErrorCode.WalletSessionInvalid);
410
472
  }
473
+ await lockAttachPinBeforePassphraseSelection();
411
474
  response = await selectDeviceSession(
412
475
  device,
413
476
  expectedPassphraseState,
@@ -437,9 +500,10 @@ export async function getProtocolV2WalletSession(
437
500
  if (options?.onlyMainPin) {
438
501
  device.clearStandardInternalState?.();
439
502
  await selectStandardWallet();
440
- response = await getDeviceSession(device, buildDeviceSessionGetRequest());
503
+ response = await getDeviceSession(device, sessionGetRequest());
441
504
  } else {
442
505
  device.clearInternalState();
506
+ await lockAttachPinBeforePassphraseSelection();
443
507
  response = await selectDeviceSession(
444
508
  device,
445
509
  expectedPassphraseState,
@@ -465,18 +529,25 @@ export async function getProtocolV2WalletSession(
465
529
  }
466
530
  }
467
531
 
468
- // DeviceSession.seed_domains reports what the SE already generated. Get
469
- // cannot add Cardano, so a Cardano call on a Standard-only session must Ask again.
532
+ // origin/dev generates Cardano on AskPassphrase, not Get. Empty host
533
+ // passphrase is the Attach PIN / standard-wallet secret. Hidden wallets
534
+ // still need a real passphrase Ask. Passphrase-off Get auto-requests Cardano.
470
535
  if (options?.deriveCardano === true && !deviceSessionHasCardano(message)) {
471
536
  if (options?.resumeOnly) {
472
537
  device.clearInternalState();
473
538
  throw ERRORS.TypedError(HardwareErrorCode.WalletSessionInvalid);
474
539
  }
475
540
  resumed = false;
476
- if (options?.onlyMainPin) {
477
- await selectStandardWallet();
541
+ const previousAddress = message.btc_test_address;
542
+ if (sessionIsAttachPinWallet(response)) {
543
+ response = await askEmptyPassphraseAndGet({ deriveCardano: true, keepAttachPin: true });
544
+ } else if (device.features?.passphraseProtection === false) {
478
545
  response = await getDeviceSession(device, buildDeviceSessionGetRequest());
546
+ } else if (options?.onlyMainPin) {
547
+ await selectMainPin();
548
+ response = await askEmptyPassphraseAndGet({ deriveCardano: true });
479
549
  } else {
550
+ await lockAttachPinBeforePassphraseSelection();
480
551
  response = await selectDeviceSession(
481
552
  device,
482
553
  expectedPassphraseState,
@@ -495,6 +566,15 @@ export async function getProtocolV2WalletSession(
495
566
  }
496
567
  throw error;
497
568
  }
569
+ if (previousAddress && previousAddress !== message.btc_test_address) {
570
+ await rejectMismatchedAttachPinWallet();
571
+ clearCurrentWalletSession();
572
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckPassphraseStateError);
573
+ }
574
+ if (!deviceSessionHasCardano(message)) {
575
+ clearCurrentWalletSession();
576
+ throw ERRORS.TypedError(HardwareErrorCode.WalletSessionInvalid);
577
+ }
498
578
  }
499
579
 
500
580
  const internalStateArgs = [
@@ -511,7 +591,7 @@ export async function getProtocolV2WalletSession(
511
591
  }
512
592
 
513
593
  let unlockedAttachPin: boolean | undefined;
514
- if (readCurrentAttachPinSession) {
594
+ if (readCurrentAttachPinSession || sessionIsAttachPinWallet(response)) {
515
595
  unlockedAttachPin = true;
516
596
  } else if (mainPinAuthenticated) {
517
597
  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 = (() => {