@onekeyfe/hd-core 1.2.0-alpha.46 → 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 +125 -45
- package/__tests__/protocol-v2.test.ts +168 -34
- package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +2 -0
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/data-manager/DataManager.d.ts +1 -0
- package/dist/data-manager/DataManager.d.ts.map +1 -1
- package/dist/index.d.ts +21 -3
- package/dist/index.js +228 -111
- package/dist/protocols/protocol-v2/resources.d.ts +4 -4
- package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdate.d.ts +2 -1
- package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
- package/dist/types/settings.d.ts +10 -0
- package/dist/types/settings.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 +77 -6
- package/src/data/messages/messages-protocol-v2.json +0 -43
- package/src/data-manager/DataManager.ts +4 -0
- package/src/protocols/protocol-v2/resources.ts +183 -57
- package/src/types/api/firmwareUpdate.ts +3 -0
- package/src/types/settings.ts +16 -0
|
@@ -5,6 +5,7 @@ import CheckAllFirmwareRelease, {
|
|
|
5
5
|
} from '../src/api/CheckAllFirmwareRelease';
|
|
6
6
|
import { DataManager } from '../src/data-manager';
|
|
7
7
|
import { createCoreApi } from '../src/inject';
|
|
8
|
+
import { PROTOCOL_V2_RESOURCE_DEVICE_PATHS } from '../src/protocols/protocol-v2/resources';
|
|
8
9
|
|
|
9
10
|
import type { CoreApi } from '../src/types/api';
|
|
10
11
|
import type { DeviceStateVersions, IFirmwareReleaseInfo } from '../src/types';
|
|
@@ -72,7 +73,74 @@ const release: IFirmwareReleaseInfo = {
|
|
|
72
73
|
],
|
|
73
74
|
};
|
|
74
75
|
|
|
76
|
+
const stableResources = ['images', 'animation', 'wallpaper', 'translations', 'roobert', 'noto'].map(
|
|
77
|
+
(type, index) => ({
|
|
78
|
+
type,
|
|
79
|
+
url: `https://example.com/${type}.okpkg`,
|
|
80
|
+
size: 0x52a0 + index + 1,
|
|
81
|
+
fileHash: 'a'.repeat(64),
|
|
82
|
+
headerHash: index.toString(16).padStart(128, '0'),
|
|
83
|
+
})
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const createFilesystemTypedCall = (missingAll = false) => {
|
|
87
|
+
const resourceByPath = new Map(
|
|
88
|
+
stableResources.map(resource => [
|
|
89
|
+
PROTOCOL_V2_RESOURCE_DEVICE_PATHS[
|
|
90
|
+
resource.type as keyof typeof PROTOCOL_V2_RESOURCE_DEVICE_PATHS
|
|
91
|
+
],
|
|
92
|
+
resource,
|
|
93
|
+
])
|
|
94
|
+
);
|
|
95
|
+
return jest.fn((requestType: string, _responseType: string, payload: Record<string, any>) => {
|
|
96
|
+
if (requestType === 'ResourceInventoryGet') {
|
|
97
|
+
throw new Error('ResourceInventoryGet is unavailable on released firmware');
|
|
98
|
+
}
|
|
99
|
+
if (requestType === 'FilesystemPathInfoQuery') {
|
|
100
|
+
const resource = resourceByPath.get(payload.path);
|
|
101
|
+
return {
|
|
102
|
+
message: {
|
|
103
|
+
exist: Boolean(resource) && !missingAll,
|
|
104
|
+
directory: false,
|
|
105
|
+
size: missingAll ? 0 : resource?.size,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (requestType === 'FilesystemFileRead') {
|
|
110
|
+
const resource = resourceByPath.get(payload.file.path);
|
|
111
|
+
if (!resource || missingAll) throw new Error('missing resource');
|
|
112
|
+
const header = new Uint8Array(0x52a0);
|
|
113
|
+
const view = new DataView(header.buffer);
|
|
114
|
+
'OKPP'.split('').forEach((char, index) => {
|
|
115
|
+
header[index] = char.charCodeAt(0);
|
|
116
|
+
});
|
|
117
|
+
'RESC'.split('').forEach((char, index) => {
|
|
118
|
+
header[0x08 + index] = char.charCodeAt(0);
|
|
119
|
+
});
|
|
120
|
+
view.setUint32(0x0c, header.byteLength, true);
|
|
121
|
+
for (let index = 0; index < resource.headerHash.length / 2; index++) {
|
|
122
|
+
header[0x240 + index] = Number.parseInt(
|
|
123
|
+
resource.headerHash.slice(index * 2, index * 2 + 2),
|
|
124
|
+
16
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
const offset = Number(payload.file.offset);
|
|
128
|
+
const chunkLength = Number(payload.chunk_len);
|
|
129
|
+
return {
|
|
130
|
+
message: {
|
|
131
|
+
data: header.slice(offset, offset + chunkLength),
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
throw new Error(`Unexpected request: ${requestType}`);
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
|
|
75
139
|
describe('checkAllFirmwareRelease Protocol V2 support', () => {
|
|
140
|
+
afterEach(() => {
|
|
141
|
+
jest.restoreAllMocks();
|
|
142
|
+
});
|
|
143
|
+
|
|
76
144
|
test('builds ordered firmwareUpdateV4 targets from recommended component versions', () => {
|
|
77
145
|
const result = buildProtocolV2FirmwareRelease({
|
|
78
146
|
currentVersions,
|
|
@@ -240,6 +308,7 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
|
|
|
240
308
|
status: { mode: 'normal' },
|
|
241
309
|
versions: currentVersions,
|
|
242
310
|
});
|
|
311
|
+
const typedCall = createFilesystemTypedCall(true);
|
|
243
312
|
method.device = {
|
|
244
313
|
isProtocolV2: () => true,
|
|
245
314
|
features: {
|
|
@@ -247,20 +316,10 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
|
|
|
247
316
|
firmwareVersion: '1.0.0',
|
|
248
317
|
},
|
|
249
318
|
getDeviceState,
|
|
250
|
-
getCommands: () => ({ typedCall
|
|
319
|
+
getCommands: () => ({ typedCall }),
|
|
251
320
|
} as unknown as CheckAllFirmwareRelease['device'];
|
|
252
321
|
jest.spyOn(DataManager, 'getFirmwareLatestRelease').mockReturnValue(release);
|
|
253
|
-
jest.spyOn(DataManager, 'getProtocolV2Resources').mockReturnValue(
|
|
254
|
-
['images', 'animation', 'wallpaper', 'translations', 'roobert', 'noto'].map(
|
|
255
|
-
(type, index) => ({
|
|
256
|
-
type,
|
|
257
|
-
url: `https://example.com/${type}.okpkg`,
|
|
258
|
-
size: index + 1,
|
|
259
|
-
fileHash: 'a'.repeat(64),
|
|
260
|
-
headerHash: index.toString(16).padStart(128, '0'),
|
|
261
|
-
})
|
|
262
|
-
) as any
|
|
263
|
-
);
|
|
322
|
+
jest.spyOn(DataManager, 'getProtocolV2Resources').mockReturnValue(stableResources as any);
|
|
264
323
|
|
|
265
324
|
await expect(method.run()).resolves.toMatchObject({
|
|
266
325
|
protocol: 'V2',
|
|
@@ -272,9 +331,45 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
|
|
|
272
331
|
status: 'required',
|
|
273
332
|
},
|
|
274
333
|
});
|
|
334
|
+
expect(typedCall.mock.calls.some(call => call[0] === 'ResourceInventoryGet')).toBe(false);
|
|
275
335
|
expect(method.getSupportedProtocols()).toEqual(['V1', 'V2']);
|
|
276
336
|
});
|
|
277
337
|
|
|
338
|
+
test.each(['bootloader', 'romloader'] as const)(
|
|
339
|
+
'uses filesystem inventory in %s mode instead of forcing all resources',
|
|
340
|
+
async mode => {
|
|
341
|
+
const method = new CheckAllFirmwareRelease({
|
|
342
|
+
id: 1,
|
|
343
|
+
payload: {
|
|
344
|
+
method: 'checkAllFirmwareRelease',
|
|
345
|
+
firmwareType: EFirmwareType.Universal,
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
method.init();
|
|
349
|
+
const typedCall = createFilesystemTypedCall();
|
|
350
|
+
method.device = {
|
|
351
|
+
isProtocolV2: () => true,
|
|
352
|
+
features: { deviceType: 'pro2', firmwareVersion: '1.0.0' },
|
|
353
|
+
getDeviceState: jest.fn().mockResolvedValue({
|
|
354
|
+
identity: { deviceType: 'pro2', firmwareType: EFirmwareType.Universal },
|
|
355
|
+
status: { mode },
|
|
356
|
+
versions: currentVersions,
|
|
357
|
+
}),
|
|
358
|
+
getCommands: () => ({ typedCall }),
|
|
359
|
+
} as unknown as CheckAllFirmwareRelease['device'];
|
|
360
|
+
jest.spyOn(DataManager, 'getFirmwareLatestRelease').mockReturnValue(release);
|
|
361
|
+
jest.spyOn(DataManager, 'getProtocolV2Resources').mockReturnValue(stableResources as any);
|
|
362
|
+
|
|
363
|
+
await expect(method.run()).resolves.toMatchObject({
|
|
364
|
+
protocol: 'V2',
|
|
365
|
+
resourceStatus: 'valid',
|
|
366
|
+
targetsToUpdate: ['boot', 'app_v1'],
|
|
367
|
+
});
|
|
368
|
+
expect(typedCall.mock.calls.some(call => call[0] === 'ResourceInventoryGet')).toBe(false);
|
|
369
|
+
expect(typedCall.mock.calls.some(call => call[0] === 'FilesystemFileRead')).toBe(true);
|
|
370
|
+
}
|
|
371
|
+
);
|
|
372
|
+
|
|
278
373
|
test('continues forwarding the existing public method', async () => {
|
|
279
374
|
const call = jest.fn().mockResolvedValue({ success: true, payload: {} });
|
|
280
375
|
const api = createCoreApi(call as CoreApi['call']) as CoreApi;
|
|
@@ -7,12 +7,16 @@ import {
|
|
|
7
7
|
PROTOCOL_V2_RESOURCE_TYPES,
|
|
8
8
|
buildProtocolV2ResourceUpdatePlan,
|
|
9
9
|
isProtocolV2ResourceFileValid,
|
|
10
|
-
parseProtocolV2ResourceInventory,
|
|
11
10
|
parseProtocolV2Resources,
|
|
12
|
-
|
|
11
|
+
readProtocolV2ResourceInventory,
|
|
13
12
|
} from '../src/protocols/protocol-v2/resources';
|
|
14
13
|
|
|
15
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
ConnectSettings,
|
|
16
|
+
IProtocolV2BootResources,
|
|
17
|
+
IProtocolV2Resource,
|
|
18
|
+
RemoteConfigResponse,
|
|
19
|
+
} from '../src/types';
|
|
16
20
|
|
|
17
21
|
jest.mock('axios');
|
|
18
22
|
jest.mock('../src/data/config', () => ({
|
|
@@ -23,6 +27,24 @@ jest.mock('../src/data/config', () => ({
|
|
|
23
27
|
const bytesToHex = (bytes: Uint8Array) =>
|
|
24
28
|
Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
25
29
|
|
|
30
|
+
const PROTOCOL_V2_OKPP_HEADER_SIZE = 0x52a0;
|
|
31
|
+
|
|
32
|
+
const createResourceHeader = (headerHash: string) => {
|
|
33
|
+
const header = new Uint8Array(PROTOCOL_V2_OKPP_HEADER_SIZE);
|
|
34
|
+
const view = new DataView(header.buffer);
|
|
35
|
+
'OKPP'.split('').forEach((char, index) => {
|
|
36
|
+
header[index] = char.charCodeAt(0);
|
|
37
|
+
});
|
|
38
|
+
'RESC'.split('').forEach((char, index) => {
|
|
39
|
+
header[0x08 + index] = char.charCodeAt(0);
|
|
40
|
+
});
|
|
41
|
+
view.setUint32(0x0c, header.byteLength, true);
|
|
42
|
+
for (let index = 0; index < headerHash.length / 2; index++) {
|
|
43
|
+
header[0x240 + index] = Number.parseInt(headerHash.slice(index * 2, index * 2 + 2), 16);
|
|
44
|
+
}
|
|
45
|
+
return header;
|
|
46
|
+
};
|
|
47
|
+
|
|
26
48
|
const resources: IProtocolV2Resource[] = PROTOCOL_V2_RESOURCE_TYPES.map((type, index) => ({
|
|
27
49
|
type,
|
|
28
50
|
url: `https://example.com/${type}.okpkg`,
|
|
@@ -31,6 +53,16 @@ const resources: IProtocolV2Resource[] = PROTOCOL_V2_RESOURCE_TYPES.map((type, i
|
|
|
31
53
|
headerHash: index.toString(16).padStart(128, '0'),
|
|
32
54
|
}));
|
|
33
55
|
|
|
56
|
+
const bootResources: IProtocolV2BootResources = {
|
|
57
|
+
required: false,
|
|
58
|
+
target: 'CRATE',
|
|
59
|
+
url: 'https://example.com/boot-resources.crate.okpkg',
|
|
60
|
+
size: 1234,
|
|
61
|
+
fileHash: 'ab'.repeat(32),
|
|
62
|
+
payloadHash: 'cd'.repeat(64),
|
|
63
|
+
headerHash: 'ef'.repeat(64),
|
|
64
|
+
};
|
|
65
|
+
|
|
34
66
|
const createSettings = (configFetcher: ConnectSettings['configFetcher']): ConnectSettings =>
|
|
35
67
|
({
|
|
36
68
|
env: 'node',
|
|
@@ -47,7 +79,7 @@ const createRemoteConfig = (): RemoteConfigResponse =>
|
|
|
47
79
|
mini: { firmware: [], ble: [] },
|
|
48
80
|
touch: { firmware: [], ble: [] },
|
|
49
81
|
pro: { firmware: [], ble: [] },
|
|
50
|
-
pro2: { firmware: [], ble: [], resources: { stable: resources } },
|
|
82
|
+
pro2: { firmware: [], ble: [], resources: { stable: resources, boot: bootResources } },
|
|
51
83
|
bridge: {},
|
|
52
84
|
} as unknown as RemoteConfigResponse);
|
|
53
85
|
|
|
@@ -58,9 +90,13 @@ describe('Pro2 resource configuration', () => {
|
|
|
58
90
|
});
|
|
59
91
|
|
|
60
92
|
test('accepts exactly six resources and normalizes their deterministic order', () => {
|
|
61
|
-
const parsed = parseProtocolV2Resources({
|
|
93
|
+
const parsed = parseProtocolV2Resources({
|
|
94
|
+
stable: [...resources].reverse(),
|
|
95
|
+
boot: bootResources,
|
|
96
|
+
});
|
|
62
97
|
|
|
63
98
|
expect(parsed?.stable.map(item => item.type)).toEqual(PROTOCOL_V2_RESOURCE_TYPES);
|
|
99
|
+
expect(parsed?.boot).toEqual(bootResources);
|
|
64
100
|
expect(PROTOCOL_V2_RESOURCE_DEVICE_PATHS.translations).toBe(
|
|
65
101
|
'vol0:/bundles/translations/translations.okpkg'
|
|
66
102
|
);
|
|
@@ -82,6 +118,21 @@ describe('Pro2 resource configuration', () => {
|
|
|
82
118
|
).toThrow('headerHash');
|
|
83
119
|
});
|
|
84
120
|
|
|
121
|
+
test('requires boot resources to remain optional and use a CRATE package', () => {
|
|
122
|
+
expect(() =>
|
|
123
|
+
parseProtocolV2Resources({
|
|
124
|
+
stable: resources,
|
|
125
|
+
boot: { ...bootResources, required: true },
|
|
126
|
+
})
|
|
127
|
+
).toThrow('required flag');
|
|
128
|
+
expect(() =>
|
|
129
|
+
parseProtocolV2Resources({
|
|
130
|
+
stable: resources,
|
|
131
|
+
boot: { ...bootResources, target: 'RESC' },
|
|
132
|
+
})
|
|
133
|
+
).toThrow('expected CRATE');
|
|
134
|
+
});
|
|
135
|
+
|
|
85
136
|
test('downloads nothing when all resource identities match', () => {
|
|
86
137
|
const inventory = resources.map(({ type, size, headerHash }) => ({ type, size, headerHash }));
|
|
87
138
|
|
|
@@ -90,50 +141,62 @@ describe('Pro2 resource configuration', () => {
|
|
|
90
141
|
).toEqual({ status: 'valid', resources: [] });
|
|
91
142
|
});
|
|
92
143
|
|
|
93
|
-
test('
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
size:
|
|
97
|
-
header_hash: resource.headerHash,
|
|
144
|
+
test('builds inventory from existing filesystem calls without ResourceInventoryGet', async () => {
|
|
145
|
+
const installedResources = resources.map((resource, index) => ({
|
|
146
|
+
...resource,
|
|
147
|
+
size: PROTOCOL_V2_OKPP_HEADER_SIZE + index + 1,
|
|
98
148
|
}));
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
149
|
+
const resourceByPath = new Map(
|
|
150
|
+
installedResources.map(resource => [
|
|
151
|
+
PROTOCOL_V2_RESOURCE_DEVICE_PATHS[resource.type],
|
|
152
|
+
resource,
|
|
153
|
+
])
|
|
154
|
+
);
|
|
155
|
+
const typedCall = jest.fn(
|
|
156
|
+
(requestType: string, _responseType: string, payload: Record<string, any>) => {
|
|
157
|
+
if (requestType === 'ResourceInventoryGet') {
|
|
158
|
+
throw new Error('ResourceInventoryGet is unavailable on released firmware');
|
|
159
|
+
}
|
|
160
|
+
if (requestType === 'FilesystemPathInfoQuery') {
|
|
161
|
+
const resource = resourceByPath.get(payload.path);
|
|
162
|
+
return {
|
|
163
|
+
message: {
|
|
164
|
+
exist: Boolean(resource),
|
|
165
|
+
directory: false,
|
|
166
|
+
size: resource?.size ?? 0,
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (requestType === 'FilesystemFileRead') {
|
|
171
|
+
const resource = resourceByPath.get(payload.file.path);
|
|
172
|
+
if (!resource) throw new Error('missing resource');
|
|
173
|
+
const header = createResourceHeader(resource.headerHash);
|
|
174
|
+
const offset = Number(payload.file.offset);
|
|
175
|
+
const chunkLength = Number(payload.chunk_len);
|
|
176
|
+
return {
|
|
177
|
+
message: {
|
|
178
|
+
data: header.slice(offset, offset + chunkLength),
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
throw new Error(`Unexpected request: ${requestType}`);
|
|
183
|
+
}
|
|
120
184
|
);
|
|
121
|
-
});
|
|
122
185
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
{ type: 'IMAGES', size: 1, header_hash: 'b'.repeat(128) },
|
|
129
|
-
],
|
|
130
|
-
})
|
|
131
|
-
).toThrow('duplicate resource type');
|
|
132
|
-
expect(() =>
|
|
133
|
-
parseProtocolV2ResourceInventory({
|
|
134
|
-
items: [{ type: 'IMAGES', size: 1, header_hash: 'bad' }],
|
|
186
|
+
await expect(
|
|
187
|
+
readProtocolV2ResourceInventory({
|
|
188
|
+
commands: { typedCall },
|
|
189
|
+
resources: installedResources,
|
|
190
|
+
chunkSize: 4000,
|
|
135
191
|
})
|
|
136
|
-
).
|
|
192
|
+
).resolves.toEqual(
|
|
193
|
+
installedResources.map(({ type, size, headerHash }) => ({ type, size, headerHash }))
|
|
194
|
+
);
|
|
195
|
+
expect(typedCall.mock.calls.some(call => call[0] === 'ResourceInventoryGet')).toBe(false);
|
|
196
|
+
expect(typedCall.mock.calls.filter(call => call[0] === 'FilesystemPathInfoQuery')).toHaveLength(
|
|
197
|
+
6
|
|
198
|
+
);
|
|
199
|
+
expect(typedCall.mock.calls.some(call => call[0] === 'FilesystemFileRead')).toBe(true);
|
|
137
200
|
});
|
|
138
201
|
|
|
139
202
|
test('selects only the changed or missing resource in application mode', () => {
|
|
@@ -165,6 +228,22 @@ describe('Pro2 resource configuration', () => {
|
|
|
165
228
|
).toHaveLength(6);
|
|
166
229
|
});
|
|
167
230
|
|
|
231
|
+
test('uses a filesystem inventory for incremental recovery mode updates', () => {
|
|
232
|
+
const inventory = resources.slice(1).map(({ type, size, headerHash }) => ({
|
|
233
|
+
type,
|
|
234
|
+
size,
|
|
235
|
+
headerHash,
|
|
236
|
+
}));
|
|
237
|
+
|
|
238
|
+
expect(
|
|
239
|
+
buildProtocolV2ResourceUpdatePlan({
|
|
240
|
+
resources,
|
|
241
|
+
inventory,
|
|
242
|
+
mode: 'bootloader-recovery',
|
|
243
|
+
}).resources.map(resource => resource.type)
|
|
244
|
+
).toEqual(['images']);
|
|
245
|
+
});
|
|
246
|
+
|
|
168
247
|
test('verifies both full file size and SHA-256 before transfer', () => {
|
|
169
248
|
const bytes = new Uint8Array([1, 2, 3]);
|
|
170
249
|
const binary = bytes.buffer;
|
|
@@ -186,6 +265,7 @@ describe('Pro2 resource configuration', () => {
|
|
|
186
265
|
expect.stringMatching(/^https:\/\/data\.onekey\.so\/pre-config\.json\?noCache=/)
|
|
187
266
|
);
|
|
188
267
|
expect(DataManager.getProtocolV2Resources()).toEqual(resources);
|
|
268
|
+
expect(DataManager.getProtocolV2BootResources()).toEqual(bootResources);
|
|
189
269
|
});
|
|
190
270
|
|
|
191
271
|
test('does not advance the cache timestamp when refresh fails', async () => {
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
DeviceSettingsPage,
|
|
6
6
|
DeviceType,
|
|
7
7
|
} from '@onekeyfe/hd-transport';
|
|
8
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
8
9
|
|
|
9
10
|
import * as firmwareBinaryApi from '../src/api/firmware/getBinary';
|
|
10
11
|
import DnxGetAddress from '../src/api/dynex/DnxGetAddress';
|
|
@@ -75,6 +76,7 @@ import {
|
|
|
75
76
|
requestProtocolV2ProtocolInfo,
|
|
76
77
|
supportsProtocolV2Message,
|
|
77
78
|
} from '../src/protocols/protocol-v2/features';
|
|
79
|
+
import { PROTOCOL_V2_RESOURCE_DEVICE_PATHS } from '../src/protocols/protocol-v2/resources';
|
|
78
80
|
import {
|
|
79
81
|
getProtocolV2WalletSession,
|
|
80
82
|
refreshProtocolV2DeviceStatus,
|
|
@@ -5788,6 +5790,64 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
5788
5790
|
});
|
|
5789
5791
|
|
|
5790
5792
|
describe('Protocol V2 firmware reconnect identity', () => {
|
|
5793
|
+
const createResourceFilesystemTypedCall = (
|
|
5794
|
+
stable: Array<{ type: string; size: number; headerHash: string }>,
|
|
5795
|
+
missingTypes: string[] = []
|
|
5796
|
+
) => {
|
|
5797
|
+
const missing = new Set(missingTypes);
|
|
5798
|
+
const resourceByPath = new Map(
|
|
5799
|
+
stable.map(resource => [
|
|
5800
|
+
PROTOCOL_V2_RESOURCE_DEVICE_PATHS[
|
|
5801
|
+
resource.type as keyof typeof PROTOCOL_V2_RESOURCE_DEVICE_PATHS
|
|
5802
|
+
],
|
|
5803
|
+
resource,
|
|
5804
|
+
])
|
|
5805
|
+
);
|
|
5806
|
+
return jest.fn((requestType: string, _responseType: string, payload: Record<string, any>) => {
|
|
5807
|
+
if (requestType === 'ResourceInventoryGet') {
|
|
5808
|
+
throw new Error('ResourceInventoryGet is unavailable on released firmware');
|
|
5809
|
+
}
|
|
5810
|
+
if (requestType === 'FilesystemPathInfoQuery') {
|
|
5811
|
+
const resource = resourceByPath.get(payload.path);
|
|
5812
|
+
const exists = Boolean(resource && !missing.has(resource.type));
|
|
5813
|
+
return {
|
|
5814
|
+
message: {
|
|
5815
|
+
exist: exists,
|
|
5816
|
+
directory: false,
|
|
5817
|
+
size: exists ? resource?.size : 0,
|
|
5818
|
+
},
|
|
5819
|
+
};
|
|
5820
|
+
}
|
|
5821
|
+
if (requestType === 'FilesystemFileRead') {
|
|
5822
|
+
const resource = resourceByPath.get(payload.file.path);
|
|
5823
|
+
if (!resource || missing.has(resource.type)) throw new Error('missing resource');
|
|
5824
|
+
const header = new Uint8Array(0x52a0);
|
|
5825
|
+
const view = new DataView(header.buffer);
|
|
5826
|
+
'OKPP'.split('').forEach((char, index) => {
|
|
5827
|
+
header[index] = char.charCodeAt(0);
|
|
5828
|
+
});
|
|
5829
|
+
'RESC'.split('').forEach((char, index) => {
|
|
5830
|
+
header[0x08 + index] = char.charCodeAt(0);
|
|
5831
|
+
});
|
|
5832
|
+
view.setUint32(0x0c, header.byteLength, true);
|
|
5833
|
+
for (let index = 0; index < resource.headerHash.length / 2; index++) {
|
|
5834
|
+
header[0x240 + index] = Number.parseInt(
|
|
5835
|
+
resource.headerHash.slice(index * 2, index * 2 + 2),
|
|
5836
|
+
16
|
|
5837
|
+
);
|
|
5838
|
+
}
|
|
5839
|
+
const offset = Number(payload.file.offset);
|
|
5840
|
+
const chunkLength = Number(payload.chunk_len);
|
|
5841
|
+
return {
|
|
5842
|
+
message: {
|
|
5843
|
+
data: header.slice(offset, offset + chunkLength),
|
|
5844
|
+
},
|
|
5845
|
+
};
|
|
5846
|
+
}
|
|
5847
|
+
throw new Error(`Unexpected request: ${requestType}`);
|
|
5848
|
+
});
|
|
5849
|
+
};
|
|
5850
|
+
|
|
5791
5851
|
test('rejects a different physical serial before firmware transfer resumes', () => {
|
|
5792
5852
|
expect(() => assertProtocolV2ReconnectIdentity('expected-serial', 'other-serial')).toThrow(
|
|
5793
5853
|
'identity mismatch'
|
|
@@ -5850,7 +5910,7 @@ describe('Protocol V2 firmware reconnect identity', () => {
|
|
|
5850
5910
|
expect((method as any).protocolV2ExpectedSerialNumber).toBeUndefined();
|
|
5851
5911
|
});
|
|
5852
5912
|
|
|
5853
|
-
test('uses
|
|
5913
|
+
test('uses filesystem reads instead of ResourceInventoryGet for resource comparison', async () => {
|
|
5854
5914
|
const method = new FirmwareUpdateV4({
|
|
5855
5915
|
id: 1,
|
|
5856
5916
|
payload: {
|
|
@@ -5864,20 +5924,12 @@ describe('Protocol V2 firmware reconnect identity', () => {
|
|
|
5864
5924
|
(type, index) => ({
|
|
5865
5925
|
type,
|
|
5866
5926
|
url: `https://example.com/${type}.okpkg`,
|
|
5867
|
-
size: index + 1,
|
|
5927
|
+
size: 0x52a0 + index + 1,
|
|
5868
5928
|
fileHash: 'a'.repeat(64),
|
|
5869
5929
|
headerHash: index.toString(16).padStart(128, '0'),
|
|
5870
5930
|
})
|
|
5871
5931
|
);
|
|
5872
|
-
const typedCall =
|
|
5873
|
-
message: {
|
|
5874
|
-
items: stable.map(item => ({
|
|
5875
|
-
type: item.type.toUpperCase(),
|
|
5876
|
-
size: item.size,
|
|
5877
|
-
header_hash: item.headerHash,
|
|
5878
|
-
})),
|
|
5879
|
-
},
|
|
5880
|
-
});
|
|
5932
|
+
const typedCall = createResourceFilesystemTypedCall(stable);
|
|
5881
5933
|
(method as any).device = stubDevice({
|
|
5882
5934
|
getCommands: () => ({ typedCall }),
|
|
5883
5935
|
});
|
|
@@ -5887,13 +5939,8 @@ describe('Protocol V2 firmware reconnect identity', () => {
|
|
|
5887
5939
|
(method as any).downloadProtocolV2Resource = jest.fn();
|
|
5888
5940
|
|
|
5889
5941
|
await expect((method as any).prepareProtocolV2ResourceBundles(false)).resolves.toEqual([]);
|
|
5890
|
-
expect(typedCall).
|
|
5891
|
-
|
|
5892
|
-
'ResourceInventory',
|
|
5893
|
-
{},
|
|
5894
|
-
{ timeoutMs: 5000 }
|
|
5895
|
-
);
|
|
5896
|
-
expect(typedCall.mock.calls.some(call => call[0] === 'FilesystemFileRead')).toBe(false);
|
|
5942
|
+
expect(typedCall.mock.calls.some(call => call[0] === 'ResourceInventoryGet')).toBe(false);
|
|
5943
|
+
expect(typedCall.mock.calls.some(call => call[0] === 'FilesystemFileRead')).toBe(true);
|
|
5897
5944
|
expect((method as any).downloadProtocolV2Resource).not.toHaveBeenCalled();
|
|
5898
5945
|
resourcesSpy.mockRestore();
|
|
5899
5946
|
});
|
|
@@ -5908,20 +5955,12 @@ describe('Protocol V2 firmware reconnect identity', () => {
|
|
|
5908
5955
|
(type, index) => ({
|
|
5909
5956
|
type,
|
|
5910
5957
|
url: `https://example.com/${type}.okpkg`,
|
|
5911
|
-
size: index + 1,
|
|
5958
|
+
size: 0x52a0 + index + 1,
|
|
5912
5959
|
fileHash: 'a'.repeat(64),
|
|
5913
5960
|
headerHash: index.toString(16).padStart(128, '0'),
|
|
5914
5961
|
})
|
|
5915
5962
|
);
|
|
5916
|
-
const typedCall =
|
|
5917
|
-
message: {
|
|
5918
|
-
items: stable.slice(1).map(item => ({
|
|
5919
|
-
type: item.type.toUpperCase(),
|
|
5920
|
-
size: item.size,
|
|
5921
|
-
header_hash: item.headerHash,
|
|
5922
|
-
})),
|
|
5923
|
-
},
|
|
5924
|
-
});
|
|
5963
|
+
const typedCall = createResourceFilesystemTypedCall(stable, ['images']);
|
|
5925
5964
|
(method as any).device = stubDevice({ getCommands: () => ({ typedCall }) });
|
|
5926
5965
|
const resourcesSpy = jest
|
|
5927
5966
|
.spyOn(DataManager, 'getProtocolV2Resources')
|
|
@@ -5941,7 +5980,7 @@ describe('Protocol V2 firmware reconnect identity', () => {
|
|
|
5941
5980
|
resourcesSpy.mockRestore();
|
|
5942
5981
|
});
|
|
5943
5982
|
|
|
5944
|
-
test('
|
|
5983
|
+
test('uses filesystem inventory for incremental Bootloader recovery', async () => {
|
|
5945
5984
|
const method = new FirmwareUpdateV4({
|
|
5946
5985
|
id: 1,
|
|
5947
5986
|
payload: { method: 'firmwareUpdateV4', platform: 'web', targetsToUpdate: ['resource'] },
|
|
@@ -5951,12 +5990,12 @@ describe('Protocol V2 firmware reconnect identity', () => {
|
|
|
5951
5990
|
(type, index) => ({
|
|
5952
5991
|
type,
|
|
5953
5992
|
url: `https://example.com/${type}.okpkg`,
|
|
5954
|
-
size: index + 1,
|
|
5993
|
+
size: 0x52a0 + index + 1,
|
|
5955
5994
|
fileHash: 'a'.repeat(64),
|
|
5956
5995
|
headerHash: index.toString(16).padStart(128, '0'),
|
|
5957
5996
|
})
|
|
5958
5997
|
);
|
|
5959
|
-
const typedCall =
|
|
5998
|
+
const typedCall = createResourceFilesystemTypedCall(stable, ['images']);
|
|
5960
5999
|
(method as any).device = stubDevice({ getCommands: () => ({ typedCall }) });
|
|
5961
6000
|
const resourcesSpy = jest
|
|
5962
6001
|
.spyOn(DataManager, 'getProtocolV2Resources')
|
|
@@ -5971,11 +6010,106 @@ describe('Protocol V2 firmware reconnect identity', () => {
|
|
|
5971
6010
|
|
|
5972
6011
|
const bundles = await (method as any).prepareProtocolV2ResourceBundles(true);
|
|
5973
6012
|
|
|
5974
|
-
expect(bundles).toHaveLength(
|
|
5975
|
-
expect(typedCall
|
|
5976
|
-
expect((
|
|
6013
|
+
expect(bundles).toHaveLength(1);
|
|
6014
|
+
expect(typedCall.mock.calls.some(call => call[0] === 'ResourceInventoryGet')).toBe(false);
|
|
6015
|
+
expect(typedCall.mock.calls.some(call => call[0] === 'FilesystemPathInfoQuery')).toBe(true);
|
|
6016
|
+
expect((method as any).downloadProtocolV2Resource).toHaveBeenCalledWith(stable[0]);
|
|
5977
6017
|
resourcesSpy.mockRestore();
|
|
5978
6018
|
});
|
|
6019
|
+
|
|
6020
|
+
const buildBootResourcesHeader = ({
|
|
6021
|
+
type = 'CRAT',
|
|
6022
|
+
payloadHash = 'ab'.repeat(64),
|
|
6023
|
+
headerHash = 'cd'.repeat(64),
|
|
6024
|
+
}: {
|
|
6025
|
+
type?: string;
|
|
6026
|
+
payloadHash?: string;
|
|
6027
|
+
headerHash?: string;
|
|
6028
|
+
} = {}) => {
|
|
6029
|
+
const header = new Uint8Array(0x52a0);
|
|
6030
|
+
const view = new DataView(header.buffer);
|
|
6031
|
+
'OKPP'.split('').forEach((char, index) => {
|
|
6032
|
+
header[index] = char.charCodeAt(0);
|
|
6033
|
+
});
|
|
6034
|
+
type.split('').forEach((char, index) => {
|
|
6035
|
+
header[0x08 + index] = char.charCodeAt(0);
|
|
6036
|
+
});
|
|
6037
|
+
view.setUint32(0x0c, header.byteLength, true);
|
|
6038
|
+
const writeHex = (offset: number, hex: string) => {
|
|
6039
|
+
for (let index = 0; index < hex.length / 2; index++) {
|
|
6040
|
+
header[offset + index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
|
|
6041
|
+
}
|
|
6042
|
+
};
|
|
6043
|
+
writeHex(0x200, payloadHash);
|
|
6044
|
+
writeHex(0x240, headerHash);
|
|
6045
|
+
return header;
|
|
6046
|
+
};
|
|
6047
|
+
|
|
6048
|
+
test('does not resolve boot resources unless the optional target is selected', async () => {
|
|
6049
|
+
const method = new FirmwareUpdateV4({
|
|
6050
|
+
id: 1,
|
|
6051
|
+
payload: { method: 'firmwareUpdateV4', platform: 'web' },
|
|
6052
|
+
});
|
|
6053
|
+
method.init();
|
|
6054
|
+
const configSpy = jest.spyOn(DataManager, 'getProtocolV2BootResources');
|
|
6055
|
+
const downloadSpy = jest.spyOn(firmwareBinaryApi, 'getSysResourceBinary');
|
|
6056
|
+
|
|
6057
|
+
await expect((method as any).prepareProtocolV2BootResources()).resolves.toBeUndefined();
|
|
6058
|
+
|
|
6059
|
+
expect(configSpy).not.toHaveBeenCalled();
|
|
6060
|
+
expect(downloadSpy).not.toHaveBeenCalled();
|
|
6061
|
+
});
|
|
6062
|
+
|
|
6063
|
+
test('downloads and maps the selected boot resources CRATE target', async () => {
|
|
6064
|
+
const payloadHash = 'ab'.repeat(64);
|
|
6065
|
+
const headerHash = 'cd'.repeat(64);
|
|
6066
|
+
const bytes = buildBootResourcesHeader({ payloadHash, headerHash });
|
|
6067
|
+
const binary = bytes.buffer as ArrayBuffer;
|
|
6068
|
+
const fileHash = Array.from(sha256(bytes), byte => byte.toString(16).padStart(2, '0')).join('');
|
|
6069
|
+
const resource = {
|
|
6070
|
+
required: false as const,
|
|
6071
|
+
target: 'CRATE' as const,
|
|
6072
|
+
url: 'https://example.com/boot-resources.crate.okpkg',
|
|
6073
|
+
size: bytes.byteLength,
|
|
6074
|
+
fileHash,
|
|
6075
|
+
payloadHash,
|
|
6076
|
+
headerHash,
|
|
6077
|
+
};
|
|
6078
|
+
const method = new FirmwareUpdateV4({
|
|
6079
|
+
id: 1,
|
|
6080
|
+
payload: {
|
|
6081
|
+
method: 'firmwareUpdateV4',
|
|
6082
|
+
platform: 'web',
|
|
6083
|
+
targetsToUpdate: ['boot_resources'],
|
|
6084
|
+
},
|
|
6085
|
+
});
|
|
6086
|
+
method.init();
|
|
6087
|
+
jest.spyOn(DataManager, 'getProtocolV2BootResources').mockReturnValue(resource);
|
|
6088
|
+
jest.spyOn(firmwareBinaryApi, 'getSysResourceBinary').mockResolvedValue({ binary });
|
|
6089
|
+
|
|
6090
|
+
await expect((method as any).prepareProtocolV2BootResources()).resolves.toEqual({
|
|
6091
|
+
fileName: 'boot_resources.crate.okpkg',
|
|
6092
|
+
binary,
|
|
6093
|
+
targetId: 1,
|
|
6094
|
+
kind: 'boot_resources',
|
|
6095
|
+
});
|
|
6096
|
+
});
|
|
6097
|
+
|
|
6098
|
+
test('rejects a non-CRATE manual boot resources package', async () => {
|
|
6099
|
+
const method = new FirmwareUpdateV4({
|
|
6100
|
+
id: 1,
|
|
6101
|
+
payload: {
|
|
6102
|
+
method: 'firmwareUpdateV4',
|
|
6103
|
+
platform: 'web',
|
|
6104
|
+
bootResourcesBinary: buildBootResourcesHeader({ type: 'RESC' }).buffer,
|
|
6105
|
+
},
|
|
6106
|
+
});
|
|
6107
|
+
method.init();
|
|
6108
|
+
|
|
6109
|
+
await expect((method as any).prepareProtocolV2BootResources()).rejects.toThrow(
|
|
6110
|
+
'Invalid Pro2 boot resources CRATE header'
|
|
6111
|
+
);
|
|
6112
|
+
});
|
|
5979
6113
|
});
|
|
5980
6114
|
|
|
5981
6115
|
describe('Protocol V2 explicit USB device selection', () => {
|