@onekeyfe/hd-core 1.2.0-alpha.47 → 1.2.0-alpha.48
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__/check-all-firmware-release-protocol-v2.test.ts +107 -12
- package/__tests__/protocol-v2-resources.test.ts +87 -42
- package/__tests__/protocol-v2.test.ts +73 -34
- package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +127 -102
- package/dist/protocols/protocol-v2/resources.d.ts +4 -4
- package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
- package/dist/utils/patch.d.ts +1 -1
- package/dist/utils/patch.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/CheckAllFirmwareRelease.ts +9 -10
- package/src/api/FirmwareUpdateV4.ts +5 -3
- package/src/data/messages/messages-protocol-v2.json +0 -43
- package/src/protocols/protocol-v2/resources.ts +150 -56
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { sha256 } from '@noble/hashes/sha256';
|
|
2
|
+
import { PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE } from '@onekeyfe/hd-transport';
|
|
2
3
|
|
|
3
4
|
import type {
|
|
4
5
|
IProtocolV2BootResources,
|
|
@@ -7,7 +8,6 @@ import type {
|
|
|
7
8
|
IProtocolV2Resources,
|
|
8
9
|
} from '../../types';
|
|
9
10
|
import type { DeviceCommands } from '../../device/DeviceCommands';
|
|
10
|
-
import type { ResourceInventory } from '@onekeyfe/hd-transport';
|
|
11
11
|
|
|
12
12
|
export const PROTOCOL_V2_RESOURCE_TYPES = [
|
|
13
13
|
'images',
|
|
@@ -31,23 +31,16 @@ export const PROTOCOL_V2_RESOURCE_DEVICE_PATHS: Readonly<Record<IProtocolV2Resou
|
|
|
31
31
|
const RESOURCE_TYPE_SET = new Set<string>(PROTOCOL_V2_RESOURCE_TYPES);
|
|
32
32
|
const SHA256_HEX_LENGTH = 64;
|
|
33
33
|
const SHA3_512_HEX_LENGTH = 128;
|
|
34
|
+
const PROTOCOL_V2_OKPP_HEADER_SIZE = 0x52a0;
|
|
35
|
+
const PROTOCOL_V2_OKPP_TYPE_OFFSET = 0x08;
|
|
36
|
+
const PROTOCOL_V2_OKPP_HEADER_LENGTH_OFFSET = 0x0c;
|
|
37
|
+
const PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET = 0x240;
|
|
38
|
+
const PROTOCOL_V2_OKPP_HASH_SIZE = 64;
|
|
39
|
+
const PROTOCOL_V2_RESOURCE_IDENTITY_READ_SIZE =
|
|
40
|
+
PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET + PROTOCOL_V2_OKPP_HASH_SIZE;
|
|
41
|
+
const PROTOCOL_V2_MIN_FILE_READ_CHUNK_SIZE = 64;
|
|
34
42
|
export const PROTOCOL_V2_RESOURCE_INVENTORY_TIMEOUT_MS = 5 * 1000;
|
|
35
43
|
|
|
36
|
-
const RESOURCE_TYPE_BY_DEVICE_VALUE: Readonly<Record<string, IProtocolV2ResourceType>> = {
|
|
37
|
-
'0': 'images',
|
|
38
|
-
IMAGES: 'images',
|
|
39
|
-
'1': 'animation',
|
|
40
|
-
ANIMATION: 'animation',
|
|
41
|
-
'2': 'wallpaper',
|
|
42
|
-
WALLPAPER: 'wallpaper',
|
|
43
|
-
'3': 'translations',
|
|
44
|
-
TRANSLATIONS: 'translations',
|
|
45
|
-
'4': 'roobert',
|
|
46
|
-
ROOBERT: 'roobert',
|
|
47
|
-
'5': 'noto',
|
|
48
|
-
NOTO: 'noto',
|
|
49
|
-
};
|
|
50
|
-
|
|
51
44
|
export type ProtocolV2ResourceInventoryItem = {
|
|
52
45
|
type: IProtocolV2ResourceType;
|
|
53
46
|
size: number;
|
|
@@ -61,57 +54,153 @@ export type ProtocolV2ResourceUpdatePlan = {
|
|
|
61
54
|
resources: IProtocolV2Resource[];
|
|
62
55
|
};
|
|
63
56
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
value
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (!Array.isArray(items)) {
|
|
70
|
-
throw new Error('Invalid Pro2 resource inventory: items must be an array');
|
|
57
|
+
function toFiniteNumber(value: unknown): number | undefined {
|
|
58
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
59
|
+
if (typeof value === 'string') {
|
|
60
|
+
const numeric = Number(value);
|
|
61
|
+
return Number.isFinite(numeric) ? numeric : undefined;
|
|
71
62
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (
|
|
75
|
-
|
|
63
|
+
if (value && typeof value === 'object') {
|
|
64
|
+
const longLike = value as { toNumber?: () => number };
|
|
65
|
+
if (typeof longLike.toNumber === 'function') {
|
|
66
|
+
const numeric = longLike.toNumber();
|
|
67
|
+
return Number.isFinite(numeric) ? numeric : undefined;
|
|
76
68
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function toUint8Array(value: unknown): Uint8Array {
|
|
74
|
+
if (value instanceof Uint8Array) return value;
|
|
75
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
76
|
+
if (ArrayBuffer.isView(value)) {
|
|
77
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
78
|
+
}
|
|
79
|
+
if (typeof value === 'string') {
|
|
80
|
+
const hex = value.replace(/^0x/i, '');
|
|
81
|
+
if (!hex || hex.length % 2 !== 0 || /[^0-9a-f]/i.test(hex)) {
|
|
82
|
+
return new Uint8Array(0);
|
|
81
83
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
85
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
86
|
+
bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
|
|
84
87
|
}
|
|
85
|
-
return
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
return bytes;
|
|
89
|
+
}
|
|
90
|
+
return new Uint8Array(0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readAscii(bytes: Uint8Array, offset: number, length: number): string {
|
|
94
|
+
return Array.from(bytes.slice(offset, offset + length), byte => String.fromCharCode(byte)).join(
|
|
95
|
+
''
|
|
96
|
+
);
|
|
97
|
+
}
|
|
91
98
|
|
|
92
|
-
|
|
93
|
-
|
|
99
|
+
function parseProtocolV2ResourceHeaderHash(bytes: Uint8Array): string | undefined {
|
|
100
|
+
if (bytes.byteLength < PROTOCOL_V2_RESOURCE_IDENTITY_READ_SIZE) return undefined;
|
|
101
|
+
if (readAscii(bytes, 0, 4) !== 'OKPP') return undefined;
|
|
102
|
+
if (readAscii(bytes, PROTOCOL_V2_OKPP_TYPE_OFFSET, 4) !== 'RESC') return undefined;
|
|
103
|
+
|
|
104
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
105
|
+
if (
|
|
106
|
+
view.getUint32(PROTOCOL_V2_OKPP_HEADER_LENGTH_OFFSET, true) !== PROTOCOL_V2_OKPP_HEADER_SIZE
|
|
107
|
+
) {
|
|
108
|
+
return undefined;
|
|
94
109
|
}
|
|
95
|
-
return
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
110
|
+
return bytesToHex(
|
|
111
|
+
bytes.slice(
|
|
112
|
+
PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET,
|
|
113
|
+
PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET + PROTOCOL_V2_OKPP_HASH_SIZE
|
|
114
|
+
)
|
|
115
|
+
);
|
|
99
116
|
}
|
|
100
117
|
|
|
101
|
-
|
|
118
|
+
async function readProtocolV2ResourceIdentity({
|
|
102
119
|
commands,
|
|
120
|
+
resource,
|
|
121
|
+
chunkSize,
|
|
103
122
|
timeoutMs = PROTOCOL_V2_RESOURCE_INVENTORY_TIMEOUT_MS,
|
|
104
123
|
}: {
|
|
105
|
-
commands: DeviceCommands
|
|
124
|
+
commands: Pick<DeviceCommands, 'typedCall'>;
|
|
125
|
+
resource: IProtocolV2Resource;
|
|
126
|
+
chunkSize: number;
|
|
106
127
|
timeoutMs?: number;
|
|
107
|
-
}): Promise<ProtocolV2ResourceInventoryItem
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
'
|
|
111
|
-
|
|
128
|
+
}): Promise<ProtocolV2ResourceInventoryItem | undefined> {
|
|
129
|
+
const path = PROTOCOL_V2_RESOURCE_DEVICE_PATHS[resource.type];
|
|
130
|
+
const pathInfo = await commands.typedCall(
|
|
131
|
+
'FilesystemPathInfoQuery',
|
|
132
|
+
'FilesystemPathInfo',
|
|
133
|
+
{ path },
|
|
112
134
|
{ timeoutMs }
|
|
113
135
|
);
|
|
114
|
-
|
|
136
|
+
const size = toFiniteNumber(pathInfo.message?.size);
|
|
137
|
+
if (
|
|
138
|
+
!pathInfo.message?.exist ||
|
|
139
|
+
pathInfo.message?.directory ||
|
|
140
|
+
!Number.isSafeInteger(size) ||
|
|
141
|
+
size !== resource.size ||
|
|
142
|
+
size < PROTOCOL_V2_OKPP_HEADER_SIZE
|
|
143
|
+
) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const header = new Uint8Array(PROTOCOL_V2_RESOURCE_IDENTITY_READ_SIZE);
|
|
148
|
+
let offset = 0;
|
|
149
|
+
while (offset < header.byteLength) {
|
|
150
|
+
const readLength = Math.min(chunkSize, header.byteLength - offset);
|
|
151
|
+
const response = await commands.typedCall(
|
|
152
|
+
'FilesystemFileRead',
|
|
153
|
+
'FilesystemFile',
|
|
154
|
+
{
|
|
155
|
+
file: { path, offset, total_size: 0 },
|
|
156
|
+
chunk_len: readLength,
|
|
157
|
+
},
|
|
158
|
+
{ timeoutMs }
|
|
159
|
+
);
|
|
160
|
+
const data = toUint8Array(response.message?.data);
|
|
161
|
+
if (data.byteLength === 0) return undefined;
|
|
162
|
+
const copied = Math.min(data.byteLength, header.byteLength - offset);
|
|
163
|
+
header.set(data.subarray(0, copied), offset);
|
|
164
|
+
offset += copied;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const headerHash = parseProtocolV2ResourceHeaderHash(header);
|
|
168
|
+
return headerHash ? { type: resource.type, size, headerHash } : undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 使用已发布 Pro2 固件支持的文件系统消息构建资源清单。
|
|
173
|
+
* 缺失、无法读取或格式错误的文件不会进入清单,因此会被选中重写。
|
|
174
|
+
*/
|
|
175
|
+
export async function readProtocolV2ResourceInventory({
|
|
176
|
+
commands,
|
|
177
|
+
resources,
|
|
178
|
+
chunkSize = PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE,
|
|
179
|
+
timeoutMs = PROTOCOL_V2_RESOURCE_INVENTORY_TIMEOUT_MS,
|
|
180
|
+
}: {
|
|
181
|
+
commands: Pick<DeviceCommands, 'typedCall'>;
|
|
182
|
+
resources: readonly IProtocolV2Resource[];
|
|
183
|
+
chunkSize?: number;
|
|
184
|
+
timeoutMs?: number;
|
|
185
|
+
}): Promise<ProtocolV2ResourceInventoryItem[]> {
|
|
186
|
+
const normalizedChunkSize = Number.isFinite(chunkSize)
|
|
187
|
+
? Math.max(Math.floor(chunkSize), PROTOCOL_V2_MIN_FILE_READ_CHUNK_SIZE)
|
|
188
|
+
: PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE;
|
|
189
|
+
const inventory: ProtocolV2ResourceInventoryItem[] = [];
|
|
190
|
+
for (const resource of resources) {
|
|
191
|
+
try {
|
|
192
|
+
const item = await readProtocolV2ResourceIdentity({
|
|
193
|
+
commands,
|
|
194
|
+
resource,
|
|
195
|
+
chunkSize: normalizedChunkSize,
|
|
196
|
+
timeoutMs,
|
|
197
|
+
});
|
|
198
|
+
if (item) inventory.push(item);
|
|
199
|
+
} catch {
|
|
200
|
+
// 单个资源无法读取时按缺失处理,不阻断其余资源的增量检查。
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return inventory;
|
|
115
204
|
}
|
|
116
205
|
|
|
117
206
|
function normalizeHex(value: unknown, expectedLength: number, field: string): string {
|
|
@@ -213,7 +302,7 @@ export function parseProtocolV2Resources(value: unknown): IProtocolV2Resources |
|
|
|
213
302
|
};
|
|
214
303
|
}
|
|
215
304
|
|
|
216
|
-
/**
|
|
305
|
+
/** 比较文件系统资源清单;恢复模式无法取得清单时回退到全量更新。 */
|
|
217
306
|
export function buildProtocolV2ResourceUpdatePlan({
|
|
218
307
|
resources,
|
|
219
308
|
inventory,
|
|
@@ -225,14 +314,19 @@ export function buildProtocolV2ResourceUpdatePlan({
|
|
|
225
314
|
mode: ProtocolV2ResourceUpdateMode;
|
|
226
315
|
forced?: boolean;
|
|
227
316
|
}): ProtocolV2ResourceUpdatePlan {
|
|
228
|
-
if (
|
|
317
|
+
if (forced) {
|
|
229
318
|
return {
|
|
230
319
|
status: resources.length > 0 ? 'outdated' : 'valid',
|
|
231
320
|
resources: [...resources],
|
|
232
321
|
};
|
|
233
322
|
}
|
|
234
323
|
if (!inventory) {
|
|
235
|
-
return
|
|
324
|
+
return mode === 'bootloader-recovery'
|
|
325
|
+
? {
|
|
326
|
+
status: resources.length > 0 ? 'outdated' : 'valid',
|
|
327
|
+
resources: [...resources],
|
|
328
|
+
}
|
|
329
|
+
: { status: 'unknown', resources: [] };
|
|
236
330
|
}
|
|
237
331
|
|
|
238
332
|
const inventoryByType = new Map(inventory.map(item => [item.type, item]));
|