@onekeyfe/hd-core 1.2.0-alpha.147 → 1.2.0-alpha.148

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/__tests__/device-lifecycle-events.test.ts +128 -2
  2. package/__tests__/device-utils.test.ts +8 -0
  3. package/__tests__/deviceUploadNft.test.ts +3 -0
  4. package/__tests__/protocol-v2-resources.test.ts +35 -89
  5. package/__tests__/protocol-v2-unlock-policy.test.ts +72 -0
  6. package/__tests__/protocol-v2.test.ts +51 -188
  7. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  8. package/dist/api/device/DeviceRebootToBoardloader.d.ts.map +1 -1
  9. package/dist/api/device/DeviceRebootToBootloader.d.ts.map +1 -1
  10. package/dist/api/protocol-v2/DeviceReboot.d.ts +1 -1
  11. package/dist/api/protocol-v2/DeviceReboot.d.ts.map +1 -1
  12. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  13. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  14. package/dist/core/index.d.ts +2 -0
  15. package/dist/core/index.d.ts.map +1 -1
  16. package/dist/device/Device.d.ts +4 -0
  17. package/dist/device/Device.d.ts.map +1 -1
  18. package/dist/index.d.ts +14 -20
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +202 -314
  21. package/dist/protocols/protocol-v2/resources.d.ts +11 -7
  22. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  23. package/dist/types/settings.d.ts +0 -13
  24. package/dist/types/settings.d.ts.map +1 -1
  25. package/dist/utils/deviceInfoUtils.d.ts.map +1 -1
  26. package/package.json +4 -4
  27. package/src/api/FirmwareUpdateV4.ts +61 -238
  28. package/src/api/PromptWebDeviceAccess.ts +2 -2
  29. package/src/api/device/DeviceRebootToBoardloader.ts +3 -1
  30. package/src/api/device/DeviceRebootToBootloader.ts +3 -1
  31. package/src/api/protocol-v2/DeviceReboot.ts +5 -0
  32. package/src/api/protocol-v2/DeviceUploadNft.ts +5 -1
  33. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +5 -1
  34. package/src/core/index.ts +28 -106
  35. package/src/device/Device.ts +59 -8
  36. package/src/index.ts +0 -4
  37. package/src/protocols/protocol-v2/resources.ts +92 -102
  38. package/src/types/settings.ts +0 -15
  39. 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 message = error instanceof Error ? error.message : String(error ?? '');
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,11 @@ 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
- // device.acquire awaits a transport reply with no deadline of its own; a
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 (device.wasInterruptedByUser()) {
960
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
961
+ }
1000
962
  if (method.payload.forceProtocolDetection && device.hasDeviceAcquire()) {
1001
963
  await device.release();
1002
964
  }
@@ -1006,43 +968,9 @@ async function connectDeviceForBle(
1006
968
  !device.commands ||
1007
969
  device.commands.disposed;
1008
970
  if (shouldAcquire) {
1009
- // The deadline/abort guards are scoped to the desktop electron
1010
- // transport: its IPC acquire is the only path with a proven
1011
- // never-settling failure mode, while react-native/lowlevel acquire may
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
- }
971
+ await device.acquire(method.payload.connectProtocol, {
972
+ forceProtocolDetection: method.payload.forceProtocolDetection,
973
+ });
1046
974
  }
1047
975
  if (method.payload?.onlyConnectBleDevice) {
1048
976
  if (shouldAcquire) {
@@ -1078,17 +1006,11 @@ async function connectDeviceForBle(
1078
1006
  // next attempt skip acquire and initialize onto the link we just cut.
1079
1007
  device.markTransportDisconnected();
1080
1008
  }
1081
- if (
1082
- (err.errorCode === HardwareErrorCode.BleTimeoutError ||
1083
- err.errorCode === HardwareErrorCode.BleConnectedError ||
1084
- isRetryableBleProtocolV2ProbeError(method, err) ||
1085
- requiresColdReconnect) &&
1086
- retryCount < 6
1087
- ) {
1009
+ if (isRetryableBleConnectionError(method, err) && retryCount < 6) {
1088
1010
  const nextRetry = retryCount + 1;
1089
1011
  Log.debug(`Bluetooth connection will retry, retry count: ${nextRetry}`);
1090
1012
  await wait(3000);
1091
- await connectDeviceForBle(method, device, abortSignal, nextRetry);
1013
+ await connectDeviceForBle(method, device, nextRetry);
1092
1014
  } else {
1093
1015
  throw err;
1094
1016
  }
@@ -1193,7 +1115,7 @@ const ensureConnected = async (
1193
1115
  if (abort()) {
1194
1116
  return;
1195
1117
  }
1196
- await connectDeviceForBle(method, device, abortSignal);
1118
+ await connectDeviceForBle(method, device);
1197
1119
  }
1198
1120
  resolve(device);
1199
1121
  return;
@@ -235,6 +235,12 @@ export class Device extends EventEmitter {
235
235
  */
236
236
  private deviceAcquired = false;
237
237
 
238
+ /**
239
+ * Set by interruptionFromUser() so an in-flight acquire/initialize cannot
240
+ * finish the link and send Cancel after the caller already aborted.
241
+ */
242
+ private interruptedByUser = false;
243
+
238
244
  /** Canonical device-state cache; legacy Features is a compatibility projection. */
239
245
  private stateStore = new DeviceStateStore();
240
246
 
@@ -429,6 +435,7 @@ export class Device extends EventEmitter {
429
435
  expectedProtocol?: HardwareConnectProtocol,
430
436
  options?: { throwOnRunPromiseError?: boolean; forceProtocolDetection?: boolean }
431
437
  ) {
438
+ this.throwIfInterruptedByUser();
432
439
  const env = DataManager.getSettings('env');
433
440
  const mainIdKey = DataManager.isBleConnect(env) ? 'id' : 'session';
434
441
  const previousProtocol = this.originalDescriptor.protocolType;
@@ -483,6 +490,15 @@ export class Device extends EventEmitter {
483
490
  if (detectedProtocol) {
484
491
  this.originalDescriptor.protocolType = detectedProtocol;
485
492
  }
493
+ if (this.interruptedByUser) {
494
+ const session = this.mainId;
495
+ if (session && this.deviceConnector?.disconnect) {
496
+ await this.deviceConnector.disconnect(session).catch(disconnectError => {
497
+ Log.debug('Ignored disconnect after user cancel during acquire', disconnectError);
498
+ });
499
+ }
500
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
501
+ }
486
502
  this.deviceAcquired = true;
487
503
  this.updateDescriptor({ [mainIdKey]: this.mainId } as unknown as DeviceDescriptor);
488
504
 
@@ -903,6 +919,7 @@ export class Device extends EventEmitter {
903
919
  }
904
920
 
905
921
  async initialize(options?: InitOptions) {
922
+ this.throwIfInterruptedByUser();
906
923
  // Protocol V2 does not support legacy Initialize; use its dedicated flow.
907
924
  if (this.isProtocolV2()) {
908
925
  this.passphraseState = options?.passphraseState;
@@ -1445,6 +1462,7 @@ export class Device extends EventEmitter {
1445
1462
  Log.debug('[Device] run error:', 'Device is running, but will cancel previous operate');
1446
1463
  }
1447
1464
 
1465
+ this.interruptedByUser = false;
1448
1466
  options = parseRunOptions(options);
1449
1467
 
1450
1468
  const runPromise = createDeferred<void>();
@@ -1574,21 +1592,24 @@ export class Device extends EventEmitter {
1574
1592
 
1575
1593
  async interruptionFromUser() {
1576
1594
  const error = ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
1595
+ this.interruptedByUser = true;
1577
1596
  const cleanupPromise = this.runCleanupPromise;
1578
1597
  const { cancelableAction } = this;
1579
- const env = DataManager.getSettings('env');
1580
1598
  if (cancelableAction) {
1581
1599
  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
- ) {
1600
+ } else if (this.shouldSendFallbackProtocolCancel()) {
1589
1601
  await this.commands?.cancelDevice?.().catch(cancelError => {
1590
1602
  Log.debug('Protocol V2 fallback cancel error', cancelError);
1591
1603
  });
1604
+ } else if (!this.hasDeviceAcquire()) {
1605
+ // Pairing / connect-native / probe: drop the physical link only.
1606
+ // Never acquire or send protocol Cancel just to abort setup.
1607
+ if (this.mainId && this.deviceConnector?.disconnect) {
1608
+ await this.deviceConnector.disconnect(this.mainId).catch(disconnectError => {
1609
+ Log.debug('Ignored disconnect during user cancel without acquire', disconnectError);
1610
+ });
1611
+ }
1612
+ this.markTransportDisconnected();
1592
1613
  }
1593
1614
  await this.commands?.cancel();
1594
1615
 
@@ -1652,6 +1673,36 @@ export class Device extends EventEmitter {
1652
1673
  return typeof this.originalDescriptor.session === 'string';
1653
1674
  }
1654
1675
 
1676
+ wasInterruptedByUser() {
1677
+ return this.interruptedByUser;
1678
+ }
1679
+
1680
+ private throwIfInterruptedByUser() {
1681
+ if (this.interruptedByUser) {
1682
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
1683
+ }
1684
+ }
1685
+
1686
+ /**
1687
+ * Protocol Cancel is only for an acquired session that is already in a
1688
+ * user-facing prompt. Connect, probe and initialize must not send Cancel
1689
+ * and must not re-acquire just to deliver one.
1690
+ */
1691
+ private shouldSendFallbackProtocolCancel() {
1692
+ if (!this.hasDeviceAcquire() || !this.isProtocolV2()) {
1693
+ return false;
1694
+ }
1695
+ if (!this.hasOpenProtocolV2UiInteraction()) {
1696
+ return false;
1697
+ }
1698
+ const env = DataManager.getSettings('env');
1699
+ return (
1700
+ DataManager.isBleConnect(env) ||
1701
+ DataManager.isBrowserWebUsb(env) ||
1702
+ DataManager.isDesktopWebUsb(env)
1703
+ );
1704
+ }
1705
+
1655
1706
  hasDeviceAcquire() {
1656
1707
  const env = DataManager.getSettings('env');
1657
1708
  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 type {
2
- IProtocolV2ResourceManifest,
3
- IProtocolV2ResourceManifestFile,
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 SHA256_HEX_LENGTH = 64;
14
-
15
- function normalizeHex(value: unknown, expectedLength: number, field: string): string {
16
- if (typeof value !== 'string') {
17
- throw new Error(`Invalid Pro2 resource ${field}: expected a hexadecimal string`);
18
- }
19
- const normalized = value.replace(/^0x/i, '').toLowerCase();
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 PROTOCOL_V2_RESOURCE_MANIFEST_DEVICE_ROOTS = [
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 isAllowedManifestDevicePath(path: string): boolean {
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 === PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH) {
66
+ if (path === PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_STAGING_PATH) {
76
67
  return true;
77
68
  }
78
- return PROTOCOL_V2_RESOURCE_MANIFEST_DEVICE_ROOTS.some(root => path.startsWith(root));
69
+ return (
70
+ path.endsWith('.okpkg') && PROTOCOL_V2_RESOURCE_DEVICE_ROOTS.some(root => path.startsWith(root))
71
+ );
79
72
  }
80
73
 
81
- function assertManifestString(value: unknown, field: string): string {
82
- if (typeof value !== 'string' || value.length === 0) {
83
- throw new Error(`Invalid Pro2 resource manifest ${field}`);
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 assertManifestRelativePath(value: unknown, field: string): string {
89
- const path = assertManifestString(value, field);
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
- path.startsWith('/') ||
92
- path.includes('\\') ||
93
- path.includes(':') ||
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(`Invalid Pro2 resource manifest ${field}`);
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
- function parseProtocolV2ResourceManifestFile(
102
- value: unknown,
103
- index: number
104
- ): IProtocolV2ResourceManifestFile {
105
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
106
- throw new Error(`Invalid Pro2 resource manifest files[${index}]`);
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 parseProtocolV2ResourceManifest(value: unknown): IProtocolV2ResourceManifest {
141
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
142
- throw new Error('Invalid Pro2 resource manifest');
143
- }
144
- const manifest = value as Partial<IProtocolV2ResourceManifest>;
145
- if (!Array.isArray(manifest.files)) {
146
- throw new Error('Invalid Pro2 resource manifest files');
147
- }
148
- const files = manifest.files.map(parseProtocolV2ResourceManifestFile);
149
- const devicePaths = new Set(files.map(file => file.device_path));
150
- const archivePaths = new Set(files.map(file => file.archive_path));
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
- files.length === 0 ||
153
- devicePaths.size !== files.length ||
154
- archivePaths.size !== files.length
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 resource manifest file set');
129
+ throw new Error('Invalid Pro2 RESOURCE package header');
157
130
  }
131
+
132
+ const packedVersion = view.getUint32(0x10, true);
158
133
  return {
159
- files,
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 selectProtocolV2ResourceManifestFiles({
164
- manifest,
165
- targetsToUpdate,
166
- }: {
167
- manifest: IProtocolV2ResourceManifest;
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
  }
@@ -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
- if (/\bPro\s*2\b/i.test(name) || /^Pro2/i.test(name)) return EDeviceType.Pro2;
34
- if (/\bNeo\b/i.test(name) || /^Neo/i.test(name)) return EDeviceType.Neo;
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;