@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.
- package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +2 -2
- package/__tests__/device-lifecycle-events.test.ts +67 -0
- package/__tests__/open-wallet-session.test.ts +613 -80
- package/__tests__/pro2HostAssetPackage.test.ts +108 -1
- package/__tests__/protocol-v2.test.ts +194 -49
- package/__tests__/protocolV2FileWrite.test.ts +40 -0
- package/dist/api/FirmwareUpdateV4.d.ts +1 -0
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/OpenWalletSession.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
- package/dist/api/helpers/protocolV2FileWrite.d.ts +1 -0
- package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
- package/dist/core/RequestQueue.d.ts +2 -0
- package/dist/core/RequestQueue.d.ts.map +1 -1
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1411 -1117
- package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
- package/dist/utils/patch.d.ts +1 -1
- package/dist/utils/patch.d.ts.map +1 -1
- package/dist/utils/pro2HostAssetPackage.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/FirmwareUpdateV4.ts +42 -10
- package/src/api/OpenWalletSession.ts +0 -3
- package/src/api/allnetwork/AllNetworkGetAddressBase.ts +3 -1
- package/src/api/helpers/protocolV2FileWrite.ts +11 -5
- package/src/api/protocol-v2/DeviceUploadWallpaper.ts +8 -2
- package/src/core/RequestQueue.ts +16 -1
- package/src/core/index.ts +48 -21
- package/src/data/messages/messages-protocol-v2.json +1471 -1367
- package/src/protocols/protocol-v2/walletSession.ts +157 -38
- 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',
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
//
|
|
316
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
200
|
+
let candidate = hashTable[hash] - 1;
|
|
201
|
+
matchChain[inputOffset] = candidate;
|
|
196
202
|
hashTable[hash] = inputOffset + 1;
|
|
197
203
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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 -
|
|
221
|
-
|
|
238
|
+
inputOffset - bestCandidate,
|
|
239
|
+
bestMatchEnd - inputOffset - LZ4_MIN_MATCH
|
|
222
240
|
);
|
|
223
|
-
inputOffset =
|
|
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
|
|
238
|
-
|
|
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,
|
|
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 = (() => {
|