@onekeyfe/hd-core 1.2.0-alpha.47 → 1.2.0-alpha.49
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 +110 -15
- package/__tests__/device-lifecycle-events.test.ts +105 -3
- package/__tests__/method-protocol-support.test.ts +19 -0
- package/__tests__/protocol-binding.test.ts +89 -0
- package/__tests__/protocol-v2-resources.test.ts +87 -42
- package/__tests__/protocol-v2.test.ts +347 -40
- package/__tests__/search-devices.test.ts +12 -3
- package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
- package/dist/api/DetectDeviceConnectProtocol.d.ts +7 -0
- package/dist/api/DetectDeviceConnectProtocol.d.ts.map +1 -0
- package/dist/api/FirmwareUpdate.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +1 -0
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/SearchDevices.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
- package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/device/Device.d.ts +5 -1
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DevicePool.d.ts.map +1 -1
- package/dist/index.d.ts +20 -3
- package/dist/index.js +361 -203
- package/dist/inject.d.ts +6 -1
- package/dist/inject.d.ts.map +1 -1
- package/dist/lowLevelInject.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/resources.d.ts +4 -4
- package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
- package/dist/topLevelInject.d.ts.map +1 -1
- package/dist/types/api/detectDeviceConnectProtocol.d.ts +4 -0
- package/dist/types/api/detectDeviceConnectProtocol.d.ts.map +1 -0
- package/dist/types/api/index.d.ts +4 -0
- package/dist/types/api/index.d.ts.map +1 -1
- package/dist/types/params.d.ts +1 -0
- package/dist/types/params.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 +10 -9
- package/src/api/DetectDeviceConnectProtocol.ts +18 -0
- package/src/api/FirmwareUpdate.ts +6 -2
- package/src/api/FirmwareUpdateV2.ts +5 -2
- package/src/api/FirmwareUpdateV3.ts +6 -2
- package/src/api/FirmwareUpdateV4.ts +73 -43
- package/src/api/SearchDevices.ts +3 -1
- package/src/api/firmware/FirmwareUpdateBaseMethod.ts +15 -4
- package/src/api/firmware/uploadFirmware.ts +17 -4
- package/src/api/index.ts +1 -0
- package/src/core/index.ts +11 -2
- package/src/data/messages/messages-protocol-v2.json +0 -43
- package/src/device/Device.ts +53 -15
- package/src/device/DevicePool.ts +7 -2
- package/src/inject.ts +57 -2
- package/src/lowLevelInject.ts +6 -3
- package/src/protocols/protocol-v2/resources.ts +150 -56
- package/src/topLevelInject.ts +6 -3
- package/src/types/api/detectDeviceConnectProtocol.ts +7 -0
- package/src/types/api/index.ts +11 -0
- package/src/types/params.ts +7 -1
|
@@ -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,
|
|
@@ -223,7 +291,7 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
|
|
|
223
291
|
expect(result.targetsToUpdate).toEqual(['app_v1', 'app_v2']);
|
|
224
292
|
});
|
|
225
293
|
|
|
226
|
-
test('
|
|
294
|
+
test('does not read vol0 resource inventory while Protocol V2 is in Application mode', async () => {
|
|
227
295
|
const method = new CheckAllFirmwareRelease({
|
|
228
296
|
id: 1,
|
|
229
297
|
payload: {
|
|
@@ -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,34 +316,60 @@ 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',
|
|
267
326
|
deviceType: 'pro2',
|
|
268
327
|
status: 'required',
|
|
269
|
-
resourceStatus: '
|
|
270
|
-
targetsToUpdate: ['boot', 'app_v1'
|
|
328
|
+
resourceStatus: 'unknown',
|
|
329
|
+
targetsToUpdate: ['boot', 'app_v1'],
|
|
271
330
|
firmware: {
|
|
272
331
|
status: 'required',
|
|
273
332
|
},
|
|
274
333
|
});
|
|
334
|
+
expect(typedCall).not.toHaveBeenCalled();
|
|
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;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
1
2
|
import { TRANSPORT_EVENT } from '@onekeyfe/hd-transport';
|
|
2
3
|
|
|
3
4
|
import { initConnector, initCore } from '../src/core';
|
|
@@ -95,7 +96,7 @@ describe('public device lifecycle events', () => {
|
|
|
95
96
|
expect(messages.filter(message => message.type === DEVICE.DISCONNECT)).toHaveLength(1);
|
|
96
97
|
});
|
|
97
98
|
|
|
98
|
-
test('uses a cached protocol as a
|
|
99
|
+
test('uses a verified cached protocol as a strict expected protocol', async () => {
|
|
99
100
|
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
100
101
|
const device = Device.fromDescriptor({
|
|
101
102
|
id: 'ble-id',
|
|
@@ -103,16 +104,117 @@ describe('public device lifecycle events', () => {
|
|
|
103
104
|
commType: 'ble',
|
|
104
105
|
protocolType: 'V1',
|
|
105
106
|
} as never);
|
|
106
|
-
const acquire = jest.fn().mockResolvedValue({ uuid: 'ble-id', protocolType: '
|
|
107
|
+
const acquire = jest.fn().mockResolvedValue({ uuid: 'ble-id', protocolType: 'V1' });
|
|
107
108
|
device.deviceConnector = { acquire } as never;
|
|
108
109
|
|
|
109
110
|
await device.acquire();
|
|
110
111
|
|
|
111
|
-
expect(acquire).toHaveBeenCalledWith('ble-id', undefined, true,
|
|
112
|
+
expect(acquire).toHaveBeenCalledWith('ble-id', undefined, true, 'V1', undefined);
|
|
113
|
+
expect(device.getProtocol()).toBe('V1');
|
|
114
|
+
expect(device.originalDescriptor.protocolType).toBe('V1');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('bypasses a cached protocol only for explicit active detection', async () => {
|
|
118
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
119
|
+
const device = Device.fromDescriptor({
|
|
120
|
+
id: 'ble-id',
|
|
121
|
+
path: 'ble-id',
|
|
122
|
+
commType: 'ble',
|
|
123
|
+
protocolType: 'V1',
|
|
124
|
+
} as never);
|
|
125
|
+
const acquire = jest.fn().mockResolvedValue({ uuid: 'ble-id', protocolType: 'V2' });
|
|
126
|
+
device.deviceConnector = { acquire } as never;
|
|
127
|
+
|
|
128
|
+
await device.acquire(undefined, { forceProtocolDetection: true });
|
|
129
|
+
|
|
130
|
+
expect(acquire).toHaveBeenCalledWith('ble-id', undefined, true, undefined, undefined);
|
|
112
131
|
expect(device.getProtocol()).toBe('V2');
|
|
113
132
|
expect(device.originalDescriptor.protocolType).toBe('V2');
|
|
114
133
|
});
|
|
115
134
|
|
|
135
|
+
test('does not reuse a stale protocol when active detection returns no protocol', async () => {
|
|
136
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
137
|
+
const device = Device.fromDescriptor({
|
|
138
|
+
id: 'ble-id',
|
|
139
|
+
path: 'ble-id',
|
|
140
|
+
commType: 'ble',
|
|
141
|
+
protocolType: 'V1',
|
|
142
|
+
} as never);
|
|
143
|
+
const acquire = jest.fn().mockResolvedValue({ uuid: 'ble-id' });
|
|
144
|
+
const release = jest.fn().mockResolvedValue(undefined);
|
|
145
|
+
device.deviceConnector = { acquire, release } as never;
|
|
146
|
+
await expect(
|
|
147
|
+
device.acquire(undefined, {
|
|
148
|
+
forceProtocolDetection: true,
|
|
149
|
+
throwOnRunPromiseError: true,
|
|
150
|
+
})
|
|
151
|
+
).rejects.toMatchObject({ errorCode: HardwareErrorCode.RuntimeError });
|
|
152
|
+
expect(device.originalDescriptor.protocolType).toBe('V1');
|
|
153
|
+
expect(device.hasDeviceAcquire()).toBe(false);
|
|
154
|
+
expect(release).toHaveBeenCalledWith('ble-id', false);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('actively reacquires an already connected device without initializing it', async () => {
|
|
158
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('webusb' as never);
|
|
159
|
+
const device = Device.fromDescriptor({
|
|
160
|
+
id: 'usb-device',
|
|
161
|
+
path: 'usb-device',
|
|
162
|
+
session: 'old-session',
|
|
163
|
+
protocolType: 'V1',
|
|
164
|
+
} as never);
|
|
165
|
+
device.mainId = 'old-session';
|
|
166
|
+
device.commands = { disposed: false } as never;
|
|
167
|
+
const release = jest.spyOn(device, 'release').mockImplementation(() => {
|
|
168
|
+
device.mainId = null;
|
|
169
|
+
device.originalDescriptor.session = null;
|
|
170
|
+
device.commands.disposed = true;
|
|
171
|
+
return Promise.resolve();
|
|
172
|
+
});
|
|
173
|
+
const acquire = jest.spyOn(device, 'acquire').mockImplementation(() => {
|
|
174
|
+
device.mainId = 'new-session';
|
|
175
|
+
device.originalDescriptor.session = 'new-session';
|
|
176
|
+
device.originalDescriptor.protocolType = 'V2';
|
|
177
|
+
device.commands.disposed = false;
|
|
178
|
+
return Promise.resolve();
|
|
179
|
+
});
|
|
180
|
+
const initialize = jest.spyOn(device, 'initialize').mockResolvedValue(undefined);
|
|
181
|
+
|
|
182
|
+
await device.run(() => Promise.resolve(undefined), {
|
|
183
|
+
forceProtocolDetection: true,
|
|
184
|
+
skipInitialize: true,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(release).toHaveBeenCalled();
|
|
188
|
+
expect(acquire).toHaveBeenCalledWith(undefined, {
|
|
189
|
+
forceProtocolDetection: true,
|
|
190
|
+
});
|
|
191
|
+
expect(initialize).not.toHaveBeenCalled();
|
|
192
|
+
expect(device.getProtocol()).toBe('V2');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('keeps the freshly probed React Native BLE session until the call completes', async () => {
|
|
196
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
197
|
+
const device = Device.fromDescriptor({
|
|
198
|
+
id: 'ble-device',
|
|
199
|
+
path: 'ble-device',
|
|
200
|
+
commType: 'ble',
|
|
201
|
+
protocolType: 'V1',
|
|
202
|
+
} as never);
|
|
203
|
+
device.mainId = 'ble-device';
|
|
204
|
+
device.commands = { disposed: false } as never;
|
|
205
|
+
(device as unknown as { deviceAcquired: boolean }).deviceAcquired = true;
|
|
206
|
+
const release = jest.spyOn(device, 'release').mockResolvedValue(undefined);
|
|
207
|
+
const run = jest.fn().mockResolvedValue(undefined);
|
|
208
|
+
|
|
209
|
+
await device.run(run, {
|
|
210
|
+
forceProtocolDetection: true,
|
|
211
|
+
skipInitialize: true,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
expect(run).toHaveBeenCalledTimes(1);
|
|
215
|
+
expect(release).toHaveBeenCalledTimes(1);
|
|
216
|
+
});
|
|
217
|
+
|
|
116
218
|
test('keeps an explicit connectProtocol as a strict expected protocol', async () => {
|
|
117
219
|
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
118
220
|
const device = Device.fromDescriptor({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EFirmwareType, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
2
|
|
|
3
3
|
import { BaseMethod } from '../src/api/BaseMethod';
|
|
4
|
+
import { findMethod } from '../src/api/utils';
|
|
4
5
|
import BTCGetAddress from '../src/api/btc/BTCGetAddress';
|
|
5
6
|
import DeviceReboot from '../src/api/protocol-v2/DeviceReboot';
|
|
6
7
|
import SolGetAddress from '../src/api/solana/SolGetAddress';
|
|
@@ -20,6 +21,24 @@ class DefaultMethod extends BaseMethod {
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
describe('method protocol support', () => {
|
|
24
|
+
test('creates a protocol detection method that returns only the current protocol', async () => {
|
|
25
|
+
const method = findMethod({
|
|
26
|
+
id: 6,
|
|
27
|
+
payload: {
|
|
28
|
+
method: 'detectDeviceConnectProtocol',
|
|
29
|
+
connectId: 'ble-device',
|
|
30
|
+
},
|
|
31
|
+
} as never);
|
|
32
|
+
|
|
33
|
+
method.init();
|
|
34
|
+
(method as unknown as { device: unknown }).device = {
|
|
35
|
+
getProtocol: () => 'V2',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
expect(method.getSupportedProtocols()).toEqual(['V1', 'V2']);
|
|
39
|
+
await expect(method.run()).resolves.toBe('V2');
|
|
40
|
+
});
|
|
41
|
+
|
|
23
42
|
test('defaults existing methods to Protocol V1 only', () => {
|
|
24
43
|
const method = new DefaultMethod({
|
|
25
44
|
id: 1,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { createCoreApi, createProtocolAwareCall } from '../src/inject';
|
|
2
|
+
|
|
3
|
+
import type { CoreApi } from '../src/types/api';
|
|
4
|
+
|
|
5
|
+
describe('SDK protocol binding', () => {
|
|
6
|
+
test('exposes a minimal API that returns the actively detected protocol', async () => {
|
|
7
|
+
const response = { success: true as const, payload: 'V2' as const };
|
|
8
|
+
const rawCall = jest.fn().mockResolvedValue(response);
|
|
9
|
+
const api = createCoreApi(rawCall as CoreApi['call']);
|
|
10
|
+
|
|
11
|
+
await expect(api.detectDeviceConnectProtocol('USB_DEVICE_A')).resolves.toBe(response);
|
|
12
|
+
expect(rawCall).toHaveBeenCalledWith({
|
|
13
|
+
connectId: 'USB_DEVICE_A',
|
|
14
|
+
forceProtocolDetection: true,
|
|
15
|
+
method: 'detectDeviceConnectProtocol',
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('injects a strict protocol into every later call and isolates devices', async () => {
|
|
20
|
+
const rawCall = jest.fn(params => Promise.resolve({ success: true, payload: params }));
|
|
21
|
+
const protocolAwareCall = createProtocolAwareCall(rawCall as CoreApi['call']);
|
|
22
|
+
const api = createCoreApi(protocolAwareCall.call);
|
|
23
|
+
|
|
24
|
+
protocolAwareCall.setDeviceConnectProtocol('USB_DEVICE_A', 'V2');
|
|
25
|
+
protocolAwareCall.setDeviceConnectProtocol('ble-device-b', 'V1');
|
|
26
|
+
|
|
27
|
+
await api.deviceWipe('usb_device_a');
|
|
28
|
+
await api.deviceSupportFeatures('BLE-DEVICE-B');
|
|
29
|
+
await api.getFeatures('USB_DEVICE_A', { connectProtocol: 'V1' });
|
|
30
|
+
await api.getDeviceState('USB_DEVICE_A', { forceProtocolDetection: true });
|
|
31
|
+
await protocolAwareCall.call({
|
|
32
|
+
connectId: 'USB_DEVICE_A',
|
|
33
|
+
method: 'deviceUpdateReboot',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
expect(rawCall).toHaveBeenNthCalledWith(1, {
|
|
37
|
+
connectId: 'usb_device_a',
|
|
38
|
+
method: 'deviceWipe',
|
|
39
|
+
connectProtocol: 'V2',
|
|
40
|
+
});
|
|
41
|
+
expect(rawCall).toHaveBeenNthCalledWith(2, {
|
|
42
|
+
connectId: 'BLE-DEVICE-B',
|
|
43
|
+
method: 'deviceSupportFeatures',
|
|
44
|
+
connectProtocol: 'V1',
|
|
45
|
+
});
|
|
46
|
+
expect(rawCall).toHaveBeenNthCalledWith(3, {
|
|
47
|
+
connectId: 'USB_DEVICE_A',
|
|
48
|
+
method: 'getFeatures',
|
|
49
|
+
connectProtocol: 'V1',
|
|
50
|
+
});
|
|
51
|
+
expect(rawCall).toHaveBeenNthCalledWith(4, {
|
|
52
|
+
connectId: 'USB_DEVICE_A',
|
|
53
|
+
method: 'getDeviceState',
|
|
54
|
+
forceProtocolDetection: true,
|
|
55
|
+
});
|
|
56
|
+
expect(rawCall).toHaveBeenNthCalledWith(5, {
|
|
57
|
+
connectId: 'USB_DEVICE_A',
|
|
58
|
+
method: 'deviceUpdateReboot',
|
|
59
|
+
connectProtocol: 'V2',
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('keeps bindings local to one SDK instance and supports clearing them', async () => {
|
|
64
|
+
const firstRawCall = jest.fn(() => Promise.resolve({ success: true, payload: {} }));
|
|
65
|
+
const secondRawCall = jest.fn(() => Promise.resolve({ success: true, payload: {} }));
|
|
66
|
+
const first = createProtocolAwareCall(firstRawCall as CoreApi['call']);
|
|
67
|
+
const second = createProtocolAwareCall(secondRawCall as CoreApi['call']);
|
|
68
|
+
|
|
69
|
+
first.setDeviceConnectProtocol('same-device', 'V2');
|
|
70
|
+
await first.call({ connectId: 'same-device', method: 'deviceWipe' });
|
|
71
|
+
await second.call({ connectId: 'same-device', method: 'deviceWipe' });
|
|
72
|
+
first.setDeviceConnectProtocol('same-device', undefined);
|
|
73
|
+
await first.call({ connectId: 'same-device', method: 'deviceWipe' });
|
|
74
|
+
|
|
75
|
+
expect(firstRawCall).toHaveBeenNthCalledWith(1, {
|
|
76
|
+
connectId: 'same-device',
|
|
77
|
+
method: 'deviceWipe',
|
|
78
|
+
connectProtocol: 'V2',
|
|
79
|
+
});
|
|
80
|
+
expect(secondRawCall).toHaveBeenCalledWith({
|
|
81
|
+
connectId: 'same-device',
|
|
82
|
+
method: 'deviceWipe',
|
|
83
|
+
});
|
|
84
|
+
expect(firstRawCall).toHaveBeenNthCalledWith(2, {
|
|
85
|
+
connectId: 'same-device',
|
|
86
|
+
method: 'deviceWipe',
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -7,9 +7,8 @@ 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
14
|
import type {
|
|
@@ -28,6 +27,24 @@ jest.mock('../src/data/config', () => ({
|
|
|
28
27
|
const bytesToHex = (bytes: Uint8Array) =>
|
|
29
28
|
Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
30
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
|
+
|
|
31
48
|
const resources: IProtocolV2Resource[] = PROTOCOL_V2_RESOURCE_TYPES.map((type, index) => ({
|
|
32
49
|
type,
|
|
33
50
|
url: `https://example.com/${type}.okpkg`,
|
|
@@ -124,50 +141,62 @@ describe('Pro2 resource configuration', () => {
|
|
|
124
141
|
).toEqual({ status: 'valid', resources: [] });
|
|
125
142
|
});
|
|
126
143
|
|
|
127
|
-
test('
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
size:
|
|
131
|
-
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,
|
|
132
148
|
}));
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
+
}
|
|
154
184
|
);
|
|
155
|
-
});
|
|
156
185
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
{ type: 'IMAGES', size: 1, header_hash: 'b'.repeat(128) },
|
|
163
|
-
],
|
|
164
|
-
})
|
|
165
|
-
).toThrow('duplicate resource type');
|
|
166
|
-
expect(() =>
|
|
167
|
-
parseProtocolV2ResourceInventory({
|
|
168
|
-
items: [{ type: 'IMAGES', size: 1, header_hash: 'bad' }],
|
|
186
|
+
await expect(
|
|
187
|
+
readProtocolV2ResourceInventory({
|
|
188
|
+
commands: { typedCall },
|
|
189
|
+
resources: installedResources,
|
|
190
|
+
chunkSize: 4000,
|
|
169
191
|
})
|
|
170
|
-
).
|
|
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);
|
|
171
200
|
});
|
|
172
201
|
|
|
173
202
|
test('selects only the changed or missing resource in application mode', () => {
|
|
@@ -199,6 +228,22 @@ describe('Pro2 resource configuration', () => {
|
|
|
199
228
|
).toHaveLength(6);
|
|
200
229
|
});
|
|
201
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
|
+
|
|
202
247
|
test('verifies both full file size and SHA-256 before transfer', () => {
|
|
203
248
|
const bytes = new Uint8Array([1, 2, 3]);
|
|
204
249
|
const binary = bytes.buffer;
|