@onekeyfe/hd-core 1.2.0-alpha.147 → 1.2.0-alpha.149
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 +177 -2
- package/__tests__/device-utils.test.ts +8 -0
- package/__tests__/deviceUploadNft.test.ts +3 -0
- package/__tests__/protocol-v2-resources.test.ts +35 -89
- package/__tests__/protocol-v2-unlock-policy.test.ts +72 -0
- package/__tests__/protocol-v2.test.ts +51 -188
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/device/DeviceRebootToBoardloader.d.ts.map +1 -1
- package/dist/api/device/DeviceRebootToBootloader.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceReboot.d.ts +1 -1
- package/dist/api/protocol-v2/DeviceReboot.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/device/Device.d.ts +6 -0
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/index.d.ts +17 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +212 -314
- package/dist/protocols/protocol-v2/resources.d.ts +11 -7
- package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
- package/dist/types/settings.d.ts +0 -13
- package/dist/types/settings.d.ts.map +1 -1
- package/dist/utils/deviceInfoUtils.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/FirmwareUpdateV4.ts +61 -238
- package/src/api/PromptWebDeviceAccess.ts +2 -2
- package/src/api/device/DeviceRebootToBoardloader.ts +3 -1
- package/src/api/device/DeviceRebootToBootloader.ts +3 -1
- package/src/api/protocol-v2/DeviceReboot.ts +5 -0
- package/src/api/protocol-v2/DeviceUploadNft.ts +5 -1
- package/src/api/protocol-v2/DeviceUploadWallpaper.ts +5 -1
- package/src/core/index.ts +31 -106
- package/src/device/Device.ts +71 -8
- package/src/index.ts +0 -4
- package/src/protocols/protocol-v2/resources.ts +92 -102
- package/src/types/settings.ts +0 -15
- package/src/utils/deviceInfoUtils.ts +7 -2
package/src/core/index.ts
CHANGED
|
@@ -415,10 +415,6 @@ const onCallDevice = async (
|
|
|
415
415
|
if (method.payload?.onlyConnectBleDevice) {
|
|
416
416
|
preWarmCallbackTask?.resolve();
|
|
417
417
|
Log.debug('Call API - only connect ble device: ', device?.mainId);
|
|
418
|
-
// This early return bypasses the normal-path releaseTask at the end of the
|
|
419
|
-
// call; without it the task leaks and haunts every later queue snapshot
|
|
420
|
-
// and cancel sweep (field log: a completed task lingered for 6 minutes).
|
|
421
|
-
requestQueue.releaseTask(method.responseID);
|
|
422
418
|
return createResponseMessage(method.responseID, true, null);
|
|
423
419
|
}
|
|
424
420
|
|
|
@@ -918,16 +914,32 @@ function canSkipInitialize(method: BaseMethod, device: Device): boolean {
|
|
|
918
914
|
return true;
|
|
919
915
|
}
|
|
920
916
|
|
|
921
|
-
function isRetryableBleProtocolV2ProbeError(method: BaseMethod, error: unknown) {
|
|
922
|
-
const
|
|
917
|
+
export function isRetryableBleProtocolV2ProbeError(method: BaseMethod, error: unknown) {
|
|
918
|
+
const typedError = error as { errorCode?: unknown; message?: unknown };
|
|
919
|
+
const message =
|
|
920
|
+
typeof typedError?.message === 'string' ? typedError.message : String(error ?? '');
|
|
923
921
|
return (
|
|
924
922
|
method.payload.connectProtocol === 'V2' &&
|
|
923
|
+
typedError?.errorCode === HardwareErrorCode.RuntimeError &&
|
|
925
924
|
message.includes('Device protocol mismatch') &&
|
|
926
925
|
message.includes('expected V2') &&
|
|
927
926
|
message.includes('did not respond to expected protocol')
|
|
928
927
|
);
|
|
929
928
|
}
|
|
930
929
|
|
|
930
|
+
export function isRetryableBleConnectionError(method: BaseMethod, error: unknown) {
|
|
931
|
+
if (method.device?.wasInterruptedByUser()) {
|
|
932
|
+
return false;
|
|
933
|
+
}
|
|
934
|
+
const typedError = error as { errorCode?: unknown };
|
|
935
|
+
return (
|
|
936
|
+
typedError?.errorCode === HardwareErrorCode.BleTimeoutError ||
|
|
937
|
+
typedError?.errorCode === HardwareErrorCode.BleConnectedError ||
|
|
938
|
+
isRetryableBleProtocolV2ProbeError(method, error) ||
|
|
939
|
+
isMissingDetectedProtocolV2Error(method, error)
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
|
|
931
943
|
export function isMissingDetectedProtocolV2Error(method: BaseMethod, error: unknown) {
|
|
932
944
|
const typedError = error as { errorCode?: unknown; message?: unknown };
|
|
933
945
|
return (
|
|
@@ -942,61 +954,14 @@ export function isMissingDetectedProtocolV2Error(method: BaseMethod, error: unkn
|
|
|
942
954
|
* If the Bluetooth connection times out, retry up to 6 times
|
|
943
955
|
* @param retryCount - Current retry count (default 0)
|
|
944
956
|
*/
|
|
945
|
-
|
|
946
|
-
// transport that never settles (field case: Electron main lost an IPC reply,
|
|
947
|
-
// "reply was never sent" after 5 minutes) hangs the call forever and cancel()
|
|
948
|
-
// only takes effect at poll checkpoints. Race acquire against a deadline and
|
|
949
|
-
// the caller's abort signal so the hang is bounded and cancel is immediate.
|
|
950
|
-
const BLE_ACQUIRE_DEADLINE_MS = 60 * 1000;
|
|
951
|
-
|
|
952
|
-
function raceBleAcquire<T>(acquirePromise: Promise<T>, abortSignal?: AbortSignal): Promise<T> {
|
|
953
|
-
return new Promise<T>((resolve, reject) => {
|
|
954
|
-
let settled = false;
|
|
955
|
-
const settle = (fn: () => void) => {
|
|
956
|
-
if (settled) return;
|
|
957
|
-
settled = true;
|
|
958
|
-
clearTimeout(deadline);
|
|
959
|
-
abortSignal?.removeEventListener('abort', onAbort);
|
|
960
|
-
fn();
|
|
961
|
-
};
|
|
962
|
-
const onAbort = () =>
|
|
963
|
-
settle(() => reject(ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled)));
|
|
964
|
-
const deadline = setTimeout(
|
|
965
|
-
() =>
|
|
966
|
-
settle(() =>
|
|
967
|
-
reject(
|
|
968
|
-
ERRORS.TypedError(
|
|
969
|
-
HardwareErrorCode.BleTimeoutError,
|
|
970
|
-
`BLE acquire exceeded ${BLE_ACQUIRE_DEADLINE_MS}ms deadline`
|
|
971
|
-
)
|
|
972
|
-
)
|
|
973
|
-
),
|
|
974
|
-
BLE_ACQUIRE_DEADLINE_MS
|
|
975
|
-
);
|
|
976
|
-
// Attach before any early return so a late settlement of acquirePromise
|
|
977
|
-
// is always consumed — an abort or deadline must never leave the acquire
|
|
978
|
-
// rejection unhandled.
|
|
979
|
-
acquirePromise.then(
|
|
980
|
-
value => settle(() => resolve(value)),
|
|
981
|
-
error => settle(() => reject(error))
|
|
982
|
-
);
|
|
983
|
-
if (abortSignal) {
|
|
984
|
-
if (abortSignal.aborted) {
|
|
985
|
-
onAbort();
|
|
986
|
-
return;
|
|
987
|
-
}
|
|
988
|
-
abortSignal.addEventListener('abort', onAbort);
|
|
989
|
-
}
|
|
990
|
-
});
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
async function connectDeviceForBle(
|
|
994
|
-
method: BaseMethod,
|
|
995
|
-
device: Device,
|
|
996
|
-
abortSignal?: AbortSignal,
|
|
997
|
-
retryCount = 0
|
|
998
|
-
) {
|
|
957
|
+
async function connectDeviceForBle(method: BaseMethod, device: Device, retryCount = 0) {
|
|
999
958
|
try {
|
|
959
|
+
if (retryCount === 0) {
|
|
960
|
+
device.beginConnectionAttempt();
|
|
961
|
+
}
|
|
962
|
+
if (device.wasInterruptedByUser()) {
|
|
963
|
+
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
|
|
964
|
+
}
|
|
1000
965
|
if (method.payload.forceProtocolDetection && device.hasDeviceAcquire()) {
|
|
1001
966
|
await device.release();
|
|
1002
967
|
}
|
|
@@ -1006,43 +971,9 @@ async function connectDeviceForBle(
|
|
|
1006
971
|
!device.commands ||
|
|
1007
972
|
device.commands.disposed;
|
|
1008
973
|
if (shouldAcquire) {
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
// legitimately block on a user-driven system bonding prompt for longer
|
|
1013
|
-
// than any sane deadline. Other envs keep the plain acquire unchanged.
|
|
1014
|
-
const useAcquireGuards = DataManager.getSettings('env') === 'desktop-web-ble';
|
|
1015
|
-
// A cancel landing during the retry backoff must not start a new acquire.
|
|
1016
|
-
if (useAcquireGuards && abortSignal?.aborted) {
|
|
1017
|
-
throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
|
|
1018
|
-
}
|
|
1019
|
-
if (!useAcquireGuards) {
|
|
1020
|
-
await device.acquire(method.payload.connectProtocol, {
|
|
1021
|
-
forceProtocolDetection: method.payload.forceProtocolDetection,
|
|
1022
|
-
});
|
|
1023
|
-
} else {
|
|
1024
|
-
try {
|
|
1025
|
-
await raceBleAcquire(
|
|
1026
|
-
device.acquire(method.payload.connectProtocol, {
|
|
1027
|
-
forceProtocolDetection: method.payload.forceProtocolDetection,
|
|
1028
|
-
}),
|
|
1029
|
-
abortSignal
|
|
1030
|
-
);
|
|
1031
|
-
} catch (err) {
|
|
1032
|
-
// A deadline hit means the transport is wedged mid-acquire; drop the
|
|
1033
|
-
// link before the retry so it cold-connects instead of stacking a
|
|
1034
|
-
// second connect onto the half-open one.
|
|
1035
|
-
if (
|
|
1036
|
-
err.errorCode === HardwareErrorCode.BleTimeoutError &&
|
|
1037
|
-
device.mainId &&
|
|
1038
|
-
device.deviceConnector
|
|
1039
|
-
) {
|
|
1040
|
-
await device.deviceConnector.disconnect(device.mainId).catch(() => undefined);
|
|
1041
|
-
device.markTransportDisconnected();
|
|
1042
|
-
}
|
|
1043
|
-
throw err;
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
974
|
+
await device.acquire(method.payload.connectProtocol, {
|
|
975
|
+
forceProtocolDetection: method.payload.forceProtocolDetection,
|
|
976
|
+
});
|
|
1046
977
|
}
|
|
1047
978
|
if (method.payload?.onlyConnectBleDevice) {
|
|
1048
979
|
if (shouldAcquire) {
|
|
@@ -1078,17 +1009,11 @@ async function connectDeviceForBle(
|
|
|
1078
1009
|
// next attempt skip acquire and initialize onto the link we just cut.
|
|
1079
1010
|
device.markTransportDisconnected();
|
|
1080
1011
|
}
|
|
1081
|
-
if (
|
|
1082
|
-
(err.errorCode === HardwareErrorCode.BleTimeoutError ||
|
|
1083
|
-
err.errorCode === HardwareErrorCode.BleConnectedError ||
|
|
1084
|
-
isRetryableBleProtocolV2ProbeError(method, err) ||
|
|
1085
|
-
requiresColdReconnect) &&
|
|
1086
|
-
retryCount < 6
|
|
1087
|
-
) {
|
|
1012
|
+
if (isRetryableBleConnectionError(method, err) && retryCount < 6) {
|
|
1088
1013
|
const nextRetry = retryCount + 1;
|
|
1089
1014
|
Log.debug(`Bluetooth connection will retry, retry count: ${nextRetry}`);
|
|
1090
1015
|
await wait(3000);
|
|
1091
|
-
await connectDeviceForBle(method, device,
|
|
1016
|
+
await connectDeviceForBle(method, device, nextRetry);
|
|
1092
1017
|
} else {
|
|
1093
1018
|
throw err;
|
|
1094
1019
|
}
|
|
@@ -1193,7 +1118,7 @@ const ensureConnected = async (
|
|
|
1193
1118
|
if (abort()) {
|
|
1194
1119
|
return;
|
|
1195
1120
|
}
|
|
1196
|
-
await connectDeviceForBle(method, device
|
|
1121
|
+
await connectDeviceForBle(method, device);
|
|
1197
1122
|
}
|
|
1198
1123
|
resolve(device);
|
|
1199
1124
|
return;
|
package/src/device/Device.ts
CHANGED
|
@@ -235,6 +235,15 @@ export class Device extends EventEmitter {
|
|
|
235
235
|
*/
|
|
236
236
|
private deviceAcquired = false;
|
|
237
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Connection-attempt generation. interruptionFromUser() pins the current
|
|
240
|
+
* attempt so a late acquire cannot finish, while the next public connect
|
|
241
|
+
* increments this and is allowed to proceed.
|
|
242
|
+
*/
|
|
243
|
+
private connectionAttempt = 0;
|
|
244
|
+
|
|
245
|
+
private interruptedAttempt: number | null = null;
|
|
246
|
+
|
|
238
247
|
/** Canonical device-state cache; legacy Features is a compatibility projection. */
|
|
239
248
|
private stateStore = new DeviceStateStore();
|
|
240
249
|
|
|
@@ -429,6 +438,8 @@ export class Device extends EventEmitter {
|
|
|
429
438
|
expectedProtocol?: HardwareConnectProtocol,
|
|
430
439
|
options?: { throwOnRunPromiseError?: boolean; forceProtocolDetection?: boolean }
|
|
431
440
|
) {
|
|
441
|
+
const attempt = this.connectionAttempt;
|
|
442
|
+
this.throwIfInterruptedByUser();
|
|
432
443
|
const env = DataManager.getSettings('env');
|
|
433
444
|
const mainIdKey = DataManager.isBleConnect(env) ? 'id' : 'session';
|
|
434
445
|
const previousProtocol = this.originalDescriptor.protocolType;
|
|
@@ -483,6 +494,15 @@ export class Device extends EventEmitter {
|
|
|
483
494
|
if (detectedProtocol) {
|
|
484
495
|
this.originalDescriptor.protocolType = detectedProtocol;
|
|
485
496
|
}
|
|
497
|
+
if (this.interruptedAttempt === attempt || this.connectionAttempt !== attempt) {
|
|
498
|
+
const session = this.mainId;
|
|
499
|
+
if (session && this.deviceConnector?.disconnect) {
|
|
500
|
+
await this.deviceConnector.disconnect(session).catch(disconnectError => {
|
|
501
|
+
Log.debug('Ignored disconnect after user cancel during acquire', disconnectError);
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
|
|
505
|
+
}
|
|
486
506
|
this.deviceAcquired = true;
|
|
487
507
|
this.updateDescriptor({ [mainIdKey]: this.mainId } as unknown as DeviceDescriptor);
|
|
488
508
|
|
|
@@ -903,6 +923,7 @@ export class Device extends EventEmitter {
|
|
|
903
923
|
}
|
|
904
924
|
|
|
905
925
|
async initialize(options?: InitOptions) {
|
|
926
|
+
this.throwIfInterruptedByUser();
|
|
906
927
|
// Protocol V2 does not support legacy Initialize; use its dedicated flow.
|
|
907
928
|
if (this.isProtocolV2()) {
|
|
908
929
|
this.passphraseState = options?.passphraseState;
|
|
@@ -1445,6 +1466,7 @@ export class Device extends EventEmitter {
|
|
|
1445
1466
|
Log.debug('[Device] run error:', 'Device is running, but will cancel previous operate');
|
|
1446
1467
|
}
|
|
1447
1468
|
|
|
1469
|
+
this.beginConnectionAttempt();
|
|
1448
1470
|
options = parseRunOptions(options);
|
|
1449
1471
|
|
|
1450
1472
|
const runPromise = createDeferred<void>();
|
|
@@ -1574,21 +1596,24 @@ export class Device extends EventEmitter {
|
|
|
1574
1596
|
|
|
1575
1597
|
async interruptionFromUser() {
|
|
1576
1598
|
const error = ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
|
|
1599
|
+
this.interruptedAttempt = this.connectionAttempt;
|
|
1577
1600
|
const cleanupPromise = this.runCleanupPromise;
|
|
1578
1601
|
const { cancelableAction } = this;
|
|
1579
|
-
const env = DataManager.getSettings('env');
|
|
1580
1602
|
if (cancelableAction) {
|
|
1581
1603
|
await cancelableAction(error);
|
|
1582
|
-
} else if (
|
|
1583
|
-
this.isProtocolV2() &&
|
|
1584
|
-
(DataManager.isBleConnect(env) ||
|
|
1585
|
-
DataManager.isBrowserWebUsb(env) ||
|
|
1586
|
-
DataManager.isDesktopWebUsb(env)) &&
|
|
1587
|
-
this.hasDeviceAcquire()
|
|
1588
|
-
) {
|
|
1604
|
+
} else if (this.shouldSendFallbackProtocolCancel()) {
|
|
1589
1605
|
await this.commands?.cancelDevice?.().catch(cancelError => {
|
|
1590
1606
|
Log.debug('Protocol V2 fallback cancel error', cancelError);
|
|
1591
1607
|
});
|
|
1608
|
+
} else if (!this.hasDeviceAcquire()) {
|
|
1609
|
+
// Pairing / connect-native / probe: drop the physical link only.
|
|
1610
|
+
// Never acquire or send protocol Cancel just to abort setup.
|
|
1611
|
+
if (this.mainId && this.deviceConnector?.disconnect) {
|
|
1612
|
+
await this.deviceConnector.disconnect(this.mainId).catch(disconnectError => {
|
|
1613
|
+
Log.debug('Ignored disconnect during user cancel without acquire', disconnectError);
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
this.markTransportDisconnected();
|
|
1592
1617
|
}
|
|
1593
1618
|
await this.commands?.cancel();
|
|
1594
1619
|
|
|
@@ -1652,6 +1677,44 @@ export class Device extends EventEmitter {
|
|
|
1652
1677
|
return typeof this.originalDescriptor.session === 'string';
|
|
1653
1678
|
}
|
|
1654
1679
|
|
|
1680
|
+
beginConnectionAttempt() {
|
|
1681
|
+
this.connectionAttempt += 1;
|
|
1682
|
+
return this.connectionAttempt;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
wasInterruptedByUser() {
|
|
1686
|
+
return (
|
|
1687
|
+
typeof this.interruptedAttempt === 'number' &&
|
|
1688
|
+
this.interruptedAttempt === this.connectionAttempt
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
private throwIfInterruptedByUser() {
|
|
1693
|
+
if (this.wasInterruptedByUser()) {
|
|
1694
|
+
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
/**
|
|
1699
|
+
* Protocol Cancel is only for an acquired session that is already in a
|
|
1700
|
+
* user-facing prompt. Connect, probe and initialize must not send Cancel
|
|
1701
|
+
* and must not re-acquire just to deliver one.
|
|
1702
|
+
*/
|
|
1703
|
+
private shouldSendFallbackProtocolCancel() {
|
|
1704
|
+
if (!this.hasDeviceAcquire() || !this.isProtocolV2()) {
|
|
1705
|
+
return false;
|
|
1706
|
+
}
|
|
1707
|
+
if (!this.hasOpenProtocolV2UiInteraction()) {
|
|
1708
|
+
return false;
|
|
1709
|
+
}
|
|
1710
|
+
const env = DataManager.getSettings('env');
|
|
1711
|
+
return (
|
|
1712
|
+
DataManager.isBleConnect(env) ||
|
|
1713
|
+
DataManager.isBrowserWebUsb(env) ||
|
|
1714
|
+
DataManager.isDesktopWebUsb(env)
|
|
1715
|
+
);
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1655
1718
|
hasDeviceAcquire() {
|
|
1656
1719
|
const env = DataManager.getSettings('env');
|
|
1657
1720
|
if (DataManager.isBleConnect(env)) {
|
package/src/index.ts
CHANGED
|
@@ -21,10 +21,6 @@ export { executeCallback, cleanupCallback };
|
|
|
21
21
|
export { preloadSessionCache } from './device/Device';
|
|
22
22
|
export { projectFeatures as projectDeviceStateFeatures } from './device/DeviceStateProjector';
|
|
23
23
|
export { getMethodSupportedProtocols } from './api/utils';
|
|
24
|
-
export {
|
|
25
|
-
parseProtocolV2ResourceManifest,
|
|
26
|
-
selectProtocolV2ResourceManifestFiles,
|
|
27
|
-
} from './protocols/protocol-v2/resources';
|
|
28
24
|
export { prepareFirmwareUpdateV4MemoryHost } from './api/firmware/FirmwareMemoryHost';
|
|
29
25
|
export type {
|
|
30
26
|
FirmwareMemoryArtifact,
|
|
@@ -1,29 +1,20 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
IProtocolV2Resources,
|
|
5
|
-
} from '../../types';
|
|
6
|
-
import type { FirmwareUpdateV4Target } from '../../types/api/firmwareUpdate';
|
|
1
|
+
import { bytesToHex } from '@noble/hashes/utils';
|
|
2
|
+
|
|
3
|
+
import type { IProtocolV2Resources, IVersionArray } from '../../types';
|
|
7
4
|
|
|
8
5
|
export const PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH =
|
|
9
6
|
'vol0:/loaders/bootloader/boot_resource.okpkg';
|
|
10
7
|
export const PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_STAGING_PATH = `${PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH}.staging`;
|
|
11
8
|
export const PROTOCOL_V2_ROM_PARAMS_PACKAGE_PATH = 'vol0:/loaders/rom/params.okpkg';
|
|
9
|
+
export const PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_SIZE = 0x5f90;
|
|
12
10
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (normalized.length !== expectedLength || !/^[0-9a-f]+$/.test(normalized)) {
|
|
21
|
-
throw new Error(
|
|
22
|
-
`Invalid Pro2 resource ${field}: expected ${expectedLength} hexadecimal characters`
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
return normalized;
|
|
26
|
-
}
|
|
11
|
+
const PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_VERSION = 1;
|
|
12
|
+
const PROTOCOL_V2_RESOURCE_PACKAGE_FLEXIBLE_OFFSET = 0x6c;
|
|
13
|
+
const PROTOCOL_V2_RESOURCE_PACKAGE_FLEXIBLE_SIZE = 64;
|
|
14
|
+
const PROTOCOL_V2_RESOURCE_PACKAGE_PAYLOAD_HASH_OFFSET = 0x200;
|
|
15
|
+
const PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_HASH_OFFSET = 0x240;
|
|
16
|
+
const PROTOCOL_V2_RESOURCE_PACKAGE_HASH_SIZE = 64;
|
|
17
|
+
const PROTOCOL_V2_RESOURCE_PACKAGE_TYPE = 'RESC';
|
|
27
18
|
|
|
28
19
|
/** Validate a complete Pro2 stable resource set from remote configuration. */
|
|
29
20
|
export function parseProtocolV2Resources(value: unknown): IProtocolV2Resources | undefined {
|
|
@@ -58,114 +49,113 @@ export function parseProtocolV2Resources(value: unknown): IProtocolV2Resources |
|
|
|
58
49
|
};
|
|
59
50
|
}
|
|
60
51
|
|
|
61
|
-
const
|
|
62
|
-
'vol0:/bundles/',
|
|
63
|
-
'vol0:/loaders/rom/',
|
|
64
|
-
] as const;
|
|
52
|
+
const PROTOCOL_V2_RESOURCE_DEVICE_ROOTS = ['vol0:/bundles/', 'vol0:/loaders/rom/'] as const;
|
|
65
53
|
|
|
66
|
-
function
|
|
54
|
+
function isAllowedResourceDevicePath(path: string): boolean {
|
|
67
55
|
if (
|
|
68
|
-
!path.endsWith('.okpkg') ||
|
|
69
56
|
path.includes('\\') ||
|
|
70
57
|
path.includes('//') ||
|
|
58
|
+
[...path].some(char => {
|
|
59
|
+
const code = char.charCodeAt(0);
|
|
60
|
+
return code <= 0x1f || code === 0x7f;
|
|
61
|
+
}) ||
|
|
71
62
|
path.split('/').some(part => part === '.' || part === '..')
|
|
72
63
|
) {
|
|
73
64
|
return false;
|
|
74
65
|
}
|
|
75
|
-
if (path ===
|
|
66
|
+
if (path === PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_STAGING_PATH) {
|
|
76
67
|
return true;
|
|
77
68
|
}
|
|
78
|
-
return
|
|
69
|
+
return (
|
|
70
|
+
path.endsWith('.okpkg') && PROTOCOL_V2_RESOURCE_DEVICE_ROOTS.some(root => path.startsWith(root))
|
|
71
|
+
);
|
|
79
72
|
}
|
|
80
73
|
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return value;
|
|
74
|
+
function readAscii(bytes: Uint8Array, offset: number, length: number): string {
|
|
75
|
+
return Array.from(bytes.slice(offset, offset + length))
|
|
76
|
+
.map(byte => String.fromCharCode(byte))
|
|
77
|
+
.join('');
|
|
86
78
|
}
|
|
87
79
|
|
|
88
|
-
function
|
|
89
|
-
const
|
|
80
|
+
function readResourceDevicePath(bytes: Uint8Array): string {
|
|
81
|
+
const metadata = bytes.slice(
|
|
82
|
+
PROTOCOL_V2_RESOURCE_PACKAGE_FLEXIBLE_OFFSET,
|
|
83
|
+
PROTOCOL_V2_RESOURCE_PACKAGE_FLEXIBLE_OFFSET + PROTOCOL_V2_RESOURCE_PACKAGE_FLEXIBLE_SIZE
|
|
84
|
+
);
|
|
85
|
+
const terminator = metadata.indexOf(0);
|
|
86
|
+
const pathBytes = terminator === -1 ? metadata : metadata.slice(0, terminator);
|
|
87
|
+
const padding = terminator === -1 ? new Uint8Array(0) : metadata.slice(terminator);
|
|
90
88
|
if (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
path.split('/').some(part => !part || part === '.' || part === '..')
|
|
89
|
+
pathBytes.byteLength === 0 ||
|
|
90
|
+
Array.from(pathBytes).some(byte => byte < 0x20 || byte > 0x7e) ||
|
|
91
|
+
Array.from(padding).some(byte => byte !== 0)
|
|
95
92
|
) {
|
|
96
|
-
throw new Error(
|
|
93
|
+
throw new Error('Invalid Pro2 RESOURCE package device path metadata');
|
|
94
|
+
}
|
|
95
|
+
const path = readAscii(pathBytes, 0, pathBytes.byteLength);
|
|
96
|
+
if (!isAllowedResourceDevicePath(path)) {
|
|
97
|
+
throw new Error(`Invalid Pro2 RESOURCE package device path: ${path}`);
|
|
97
98
|
}
|
|
98
99
|
return path;
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const file = value as Partial<IProtocolV2ResourceManifestFile>;
|
|
109
|
-
const archivePath = assertManifestRelativePath(file.archive_path, `files[${index}].archive_path`);
|
|
110
|
-
const originalName =
|
|
111
|
-
file.original_name === undefined
|
|
112
|
-
? archivePath.split('/').pop() ?? archivePath
|
|
113
|
-
: assertManifestRelativePath(file.original_name, `files[${index}].original_name`);
|
|
114
|
-
if (originalName.includes('/')) {
|
|
115
|
-
throw new Error(`Invalid Pro2 resource manifest files[${index}].original_name`);
|
|
116
|
-
}
|
|
117
|
-
const devicePath = assertManifestString(file.device_path, `files[${index}].device_path`);
|
|
118
|
-
if (!isAllowedManifestDevicePath(devicePath)) {
|
|
119
|
-
throw new Error(`Invalid Pro2 resource manifest files[${index}].device_path`);
|
|
120
|
-
}
|
|
121
|
-
if (!Number.isSafeInteger(file.size) || Number(file.size) <= 0) {
|
|
122
|
-
throw new Error(`Invalid Pro2 resource manifest files[${index}].size`);
|
|
123
|
-
}
|
|
124
|
-
const digest = normalizeHex(file.sha256, SHA256_HEX_LENGTH, `files[${index}].sha256`);
|
|
125
|
-
if (!archivePath.endsWith('.okpkg') || !originalName.endsWith('.okpkg')) {
|
|
126
|
-
throw new Error(`Invalid Pro2 resource manifest files[${index}] package extension`);
|
|
127
|
-
}
|
|
128
|
-
return {
|
|
129
|
-
archive_path: archivePath,
|
|
130
|
-
original_name: originalName,
|
|
131
|
-
device_path: devicePath,
|
|
132
|
-
size: Number(file.size),
|
|
133
|
-
sha256: digest,
|
|
134
|
-
...(file.signed === undefined ? {} : { signed: file.signed }),
|
|
135
|
-
...(file.sig_algo === undefined ? {} : { sig_algo: file.sig_algo }),
|
|
136
|
-
...(file.payload_version === undefined ? {} : { payload_version: file.payload_version }),
|
|
137
|
-
};
|
|
138
|
-
}
|
|
102
|
+
export type ProtocolV2ResourcePackageHeader = {
|
|
103
|
+
version: IVersionArray;
|
|
104
|
+
payloadLength: number;
|
|
105
|
+
devicePath: string;
|
|
106
|
+
payloadHash: string;
|
|
107
|
+
headerHash: string;
|
|
108
|
+
};
|
|
139
109
|
|
|
140
|
-
export function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
const
|
|
110
|
+
export function parseProtocolV2ResourcePackageHeader(
|
|
111
|
+
bytes: Uint8Array,
|
|
112
|
+
packageSize: number
|
|
113
|
+
): ProtocolV2ResourcePackageHeader {
|
|
114
|
+
if (bytes.byteLength < PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_SIZE) {
|
|
115
|
+
throw new Error('Pro2 RESOURCE package is shorter than its header');
|
|
116
|
+
}
|
|
117
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
118
|
+
const headerVersion = view.getUint32(0x04, true);
|
|
119
|
+
const headerLength = view.getUint32(0x0c, true);
|
|
120
|
+
const payloadLength = view.getUint32(0x14, true);
|
|
151
121
|
if (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
122
|
+
readAscii(bytes, 0, 4) !== 'OKPP' ||
|
|
123
|
+
readAscii(bytes, 0x08, 4) !== PROTOCOL_V2_RESOURCE_PACKAGE_TYPE ||
|
|
124
|
+
headerVersion !== PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_VERSION ||
|
|
125
|
+
headerLength !== PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_SIZE ||
|
|
126
|
+
payloadLength <= 0 ||
|
|
127
|
+
headerLength + payloadLength !== packageSize
|
|
155
128
|
) {
|
|
156
|
-
throw new Error('Invalid Pro2
|
|
129
|
+
throw new Error('Invalid Pro2 RESOURCE package header');
|
|
157
130
|
}
|
|
131
|
+
|
|
132
|
+
const packedVersion = view.getUint32(0x10, true);
|
|
158
133
|
return {
|
|
159
|
-
|
|
134
|
+
version: [
|
|
135
|
+
Math.floor(packedVersion / 0x10000) % 0x100,
|
|
136
|
+
Math.floor(packedVersion / 0x100) % 0x100,
|
|
137
|
+
packedVersion % 0x100,
|
|
138
|
+
],
|
|
139
|
+
payloadLength,
|
|
140
|
+
devicePath: readResourceDevicePath(bytes),
|
|
141
|
+
payloadHash: bytesToHex(
|
|
142
|
+
bytes.slice(
|
|
143
|
+
PROTOCOL_V2_RESOURCE_PACKAGE_PAYLOAD_HASH_OFFSET,
|
|
144
|
+
PROTOCOL_V2_RESOURCE_PACKAGE_PAYLOAD_HASH_OFFSET + PROTOCOL_V2_RESOURCE_PACKAGE_HASH_SIZE
|
|
145
|
+
)
|
|
146
|
+
),
|
|
147
|
+
headerHash: bytesToHex(
|
|
148
|
+
bytes.slice(
|
|
149
|
+
PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_HASH_OFFSET,
|
|
150
|
+
PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_HASH_OFFSET + PROTOCOL_V2_RESOURCE_PACKAGE_HASH_SIZE
|
|
151
|
+
)
|
|
152
|
+
),
|
|
160
153
|
};
|
|
161
154
|
}
|
|
162
155
|
|
|
163
|
-
export function
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
targetsToUpdate: readonly FirmwareUpdateV4Target[];
|
|
169
|
-
}): IProtocolV2ResourceManifestFile[] {
|
|
170
|
-
return targetsToUpdate.includes('resource') ? [...manifest.files] : [];
|
|
156
|
+
export function parseProtocolV2ResourcePackage(
|
|
157
|
+
binary: ArrayBuffer | Uint8Array
|
|
158
|
+
): ProtocolV2ResourcePackageHeader {
|
|
159
|
+
const bytes = binary instanceof Uint8Array ? binary : new Uint8Array(binary);
|
|
160
|
+
return parseProtocolV2ResourcePackageHeader(bytes, bytes.byteLength);
|
|
171
161
|
}
|
package/src/types/settings.ts
CHANGED
|
@@ -74,21 +74,6 @@ export type IProtocolV2Resources = {
|
|
|
74
74
|
source: IProtocolV2ResourceSource;
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
-
export type IProtocolV2ResourceManifestFile = {
|
|
78
|
-
archive_path: string;
|
|
79
|
-
original_name?: string;
|
|
80
|
-
device_path: string;
|
|
81
|
-
size: number;
|
|
82
|
-
sha256: string;
|
|
83
|
-
signed?: boolean;
|
|
84
|
-
sig_algo?: string;
|
|
85
|
-
payload_version?: string | null;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
export type IProtocolV2ResourceManifest = {
|
|
89
|
-
files: IProtocolV2ResourceManifestFile[];
|
|
90
|
-
};
|
|
91
|
-
|
|
92
77
|
/** STM32 firmware config */
|
|
93
78
|
export type IFirmwareReleaseInfo = {
|
|
94
79
|
required: boolean;
|
|
@@ -30,8 +30,13 @@ export const getDeviceTypeByBleName = (name?: string): IDeviceType => {
|
|
|
30
30
|
if (/^T/i.test(name)) return EDeviceType.Touch;
|
|
31
31
|
if (/^Touch/i.test(name)) return EDeviceType.Touch;
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
if (/\
|
|
33
|
+
const compactName = name.replace(/[\s-]/g, '');
|
|
34
|
+
if (/\bPro\s*2\b/i.test(name) || /^Pro2/i.test(name) || /^(?:OneKey)?Pro2/i.test(compactName)) {
|
|
35
|
+
return EDeviceType.Pro2;
|
|
36
|
+
}
|
|
37
|
+
if (/\bNeo\b/i.test(name) || /^Neo/i.test(name) || /^(?:OneKey)?Neo/i.test(compactName)) {
|
|
38
|
+
return EDeviceType.Neo;
|
|
39
|
+
}
|
|
35
40
|
if (/\bPro\b/i.test(name) || /^Pro/i.test(name)) return EDeviceType.Pro;
|
|
36
41
|
|
|
37
42
|
return EDeviceType.Unknown;
|