@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/__tests__/device-lifecycle-events.test.ts +14 -0
- package/__tests__/open-wallet-session.test.ts +318 -20
- package/__tests__/pro2HostAssetPackage.test.ts +108 -1
- package/__tests__/protocol-v2.test.ts +130 -20
- package/__tests__/protocolV2FileWrite.test.ts +40 -0
- package/dist/api/FirmwareUpdateV4.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/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 +217 -68
- 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 +33 -6
- package/src/api/helpers/protocolV2FileWrite.ts +11 -5
- package/src/api/protocol-v2/DeviceUploadWallpaper.ts +8 -2
- package/src/core/index.ts +19 -8
- package/src/data/messages/messages-protocol-v2.json +39 -10
- package/src/protocols/protocol-v2/walletSession.ts +95 -15
- package/src/utils/pro2HostAssetPackage.ts +73 -27
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
"
|
|
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":
|
|
12865
|
+
"id": 2
|
|
12861
12866
|
},
|
|
12862
12867
|
"total_bytes": {
|
|
12863
|
-
"rule": "required",
|
|
12864
12868
|
"type": "uint32",
|
|
12865
|
-
"id":
|
|
12869
|
+
"id": 3
|
|
12866
12870
|
}
|
|
12867
12871
|
}
|
|
12868
12872
|
},
|
|
12869
|
-
"
|
|
12873
|
+
"ViewContentEntry": {
|
|
12870
12874
|
"fields": {
|
|
12871
|
-
"
|
|
12875
|
+
"entry_key": {
|
|
12872
12876
|
"rule": "required",
|
|
12873
|
-
"type": "
|
|
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":
|
|
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
|
-
"
|
|
12944
|
-
"type": "
|
|
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
|
-
//
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
//
|
|
469
|
-
//
|
|
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
|
-
|
|
477
|
-
|
|
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
|
|
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 = (() => {
|