@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.
Files changed (39) hide show
  1. package/__tests__/device-lifecycle-events.test.ts +177 -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 +6 -0
  17. package/dist/device/Device.d.ts.map +1 -1
  18. package/dist/index.d.ts +17 -20
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +212 -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 +31 -106
  35. package/src/device/Device.ts +71 -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,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
- // 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 (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
- // 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
- }
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, abortSignal, nextRetry);
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, abortSignal);
1121
+ await connectDeviceForBle(method, device);
1197
1122
  }
1198
1123
  resolve(device);
1199
1124
  return;
@@ -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 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;