@onekeyfe/hd-core 1.2.0-alpha.78 → 1.2.0-alpha.79
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 +12 -71
- package/__tests__/protocol-v2-resources.test.ts +102 -222
- package/__tests__/protocol-v2.test.ts +110 -380
- package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +0 -6
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/UploadPortfolio.d.ts.map +1 -1
- package/dist/data-manager/DataManager.d.ts +1 -2
- package/dist/data-manager/DataManager.d.ts.map +1 -1
- package/dist/index.d.ts +74 -32
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +255 -499
- package/dist/inject.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/resources.d.ts +31 -28
- package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
- package/dist/types/api/checkAllFirmwareRelease.d.ts +1 -0
- package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
- package/dist/types/api/export.d.ts +1 -0
- package/dist/types/api/export.d.ts.map +1 -1
- package/dist/types/api/index.d.ts +2 -0
- package/dist/types/api/index.d.ts.map +1 -1
- package/dist/types/api/protocolV2ResourceManifest.d.ts +17 -0
- package/dist/types/api/protocolV2ResourceManifest.d.ts.map +1 -0
- package/dist/types/settings.d.ts +30 -21
- package/dist/types/settings.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/CheckAllFirmwareRelease.ts +5 -34
- package/src/api/FirmwareUpdateV4.ts +48 -220
- package/src/api/UploadPortfolio.ts +18 -0
- package/src/data-manager/DataManager.ts +2 -6
- package/src/index.ts +6 -0
- package/src/inject.ts +2 -0
- package/src/protocols/protocol-v2/resources.ts +193 -324
- package/src/types/api/checkAllFirmwareRelease.ts +1 -0
- package/src/types/api/export.ts +4 -0
- package/src/types/api/index.ts +2 -0
- package/src/types/api/protocolV2ResourceManifest.ts +19 -0
- package/src/types/settings.ts +30 -36
|
@@ -5,7 +5,6 @@ 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';
|
|
9
8
|
|
|
10
9
|
import type { CoreApi } from '../src/types/api';
|
|
11
10
|
import type { DeviceStateVersions, IFirmwareReleaseInfo } from '../src/types';
|
|
@@ -73,67 +72,8 @@ const release: IFirmwareReleaseInfo = {
|
|
|
73
72
|
],
|
|
74
73
|
};
|
|
75
74
|
|
|
76
|
-
const
|
|
77
|
-
|
|
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
|
-
});
|
|
75
|
+
const resourceSource = {
|
|
76
|
+
manifestUrl: 'https://example.com/pro2-resource/manifest.json',
|
|
137
77
|
};
|
|
138
78
|
|
|
139
79
|
describe('checkAllFirmwareRelease Protocol V2 support', () => {
|
|
@@ -370,7 +310,7 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
|
|
|
370
310
|
status: { mode: 'normal' },
|
|
371
311
|
versions: currentVersions,
|
|
372
312
|
});
|
|
373
|
-
const typedCall =
|
|
313
|
+
const typedCall = jest.fn();
|
|
374
314
|
method.device = {
|
|
375
315
|
isProtocolV2: () => true,
|
|
376
316
|
features: {
|
|
@@ -381,13 +321,14 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
|
|
|
381
321
|
getCommands: () => ({ typedCall }),
|
|
382
322
|
} as unknown as CheckAllFirmwareRelease['device'];
|
|
383
323
|
jest.spyOn(DataManager, 'getFirmwareLatestRelease').mockReturnValue(release);
|
|
384
|
-
jest.spyOn(DataManager, '
|
|
324
|
+
jest.spyOn(DataManager, 'getProtocolV2ResourceSource').mockReturnValue(resourceSource);
|
|
385
325
|
|
|
386
326
|
await expect(method.run()).resolves.toMatchObject({
|
|
387
327
|
protocol: 'V2',
|
|
388
328
|
deviceType: 'pro2',
|
|
389
329
|
status: 'required',
|
|
390
330
|
resourceStatus: 'unknown',
|
|
331
|
+
resourceManifestUrl: resourceSource.manifestUrl,
|
|
391
332
|
targetsToUpdate: ['boot', 'app_v1'],
|
|
392
333
|
firmware: {
|
|
393
334
|
status: 'required',
|
|
@@ -437,7 +378,7 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
|
|
|
437
378
|
getCommands: () => ({ typedCall: jest.fn() }),
|
|
438
379
|
} as unknown as CheckAllFirmwareRelease['device'];
|
|
439
380
|
jest.spyOn(DataManager, 'getFirmwareLatestRelease').mockReturnValue(packageSet);
|
|
440
|
-
jest.spyOn(DataManager, '
|
|
381
|
+
jest.spyOn(DataManager, 'getProtocolV2ResourceSource').mockReturnValue(undefined);
|
|
441
382
|
|
|
442
383
|
await expect(method.run()).resolves.toMatchObject({
|
|
443
384
|
status: 'outdated',
|
|
@@ -449,7 +390,7 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
|
|
|
449
390
|
});
|
|
450
391
|
|
|
451
392
|
test.each(['bootloader', 'romloader'] as const)(
|
|
452
|
-
'
|
|
393
|
+
'does not read vol0 resource inventory in %s mode',
|
|
453
394
|
async mode => {
|
|
454
395
|
const method = new CheckAllFirmwareRelease({
|
|
455
396
|
id: 1,
|
|
@@ -459,7 +400,7 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
|
|
|
459
400
|
},
|
|
460
401
|
});
|
|
461
402
|
method.init();
|
|
462
|
-
const typedCall =
|
|
403
|
+
const typedCall = jest.fn();
|
|
463
404
|
method.device = {
|
|
464
405
|
isProtocolV2: () => true,
|
|
465
406
|
features: { deviceType: 'pro2', firmwareVersion: '1.0.0' },
|
|
@@ -471,15 +412,15 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
|
|
|
471
412
|
getCommands: () => ({ typedCall }),
|
|
472
413
|
} as unknown as CheckAllFirmwareRelease['device'];
|
|
473
414
|
jest.spyOn(DataManager, 'getFirmwareLatestRelease').mockReturnValue(release);
|
|
474
|
-
jest.spyOn(DataManager, '
|
|
415
|
+
jest.spyOn(DataManager, 'getProtocolV2ResourceSource').mockReturnValue(resourceSource);
|
|
475
416
|
|
|
476
417
|
await expect(method.run()).resolves.toMatchObject({
|
|
477
418
|
protocol: 'V2',
|
|
478
|
-
resourceStatus: '
|
|
419
|
+
resourceStatus: 'unknown',
|
|
420
|
+
resourceManifestUrl: resourceSource.manifestUrl,
|
|
479
421
|
targetsToUpdate: ['boot', 'app_v1'],
|
|
480
422
|
});
|
|
481
|
-
expect(typedCall.
|
|
482
|
-
expect(typedCall.mock.calls.some(call => call[0] === 'FilesystemFileRead')).toBe(true);
|
|
423
|
+
expect(typedCall).not.toHaveBeenCalled();
|
|
483
424
|
}
|
|
484
425
|
);
|
|
485
426
|
|
|
@@ -4,20 +4,13 @@ import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
|
4
4
|
|
|
5
5
|
import { DataManager } from '../src/data-manager';
|
|
6
6
|
import {
|
|
7
|
-
PROTOCOL_V2_RESOURCE_DEVICE_PATHS,
|
|
8
|
-
PROTOCOL_V2_RESOURCE_TYPES,
|
|
9
|
-
buildProtocolV2ResourceUpdatePlan,
|
|
10
7
|
isProtocolV2ResourceFileValid,
|
|
8
|
+
parseProtocolV2ResourceManifest,
|
|
11
9
|
parseProtocolV2Resources,
|
|
12
|
-
|
|
10
|
+
prepareProtocolV2ResourceFiles,
|
|
13
11
|
} from '../src/protocols/protocol-v2/resources';
|
|
14
12
|
|
|
15
|
-
import type {
|
|
16
|
-
ConnectSettings,
|
|
17
|
-
IProtocolV2BootResources,
|
|
18
|
-
IProtocolV2Resource,
|
|
19
|
-
RemoteConfigResponse,
|
|
20
|
-
} from '../src/types';
|
|
13
|
+
import type { ConnectSettings, RemoteConfigResponse } from '../src/types';
|
|
21
14
|
|
|
22
15
|
jest.mock('axios');
|
|
23
16
|
jest.mock('../src/data/config', () => ({
|
|
@@ -28,45 +21,69 @@ jest.mock('../src/data/config', () => ({
|
|
|
28
21
|
const bytesToHex = (bytes: Uint8Array) =>
|
|
29
22
|
Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
30
23
|
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
const createResourceHeader = (headerHash: string) => {
|
|
34
|
-
const header = new Uint8Array(PROTOCOL_V2_OKPP_HEADER_SIZE);
|
|
35
|
-
const view = new DataView(header.buffer);
|
|
36
|
-
'OKPP'.split('').forEach((char, index) => {
|
|
37
|
-
header[index] = char.charCodeAt(0);
|
|
38
|
-
});
|
|
39
|
-
'RESC'.split('').forEach((char, index) => {
|
|
40
|
-
header[0x08 + index] = char.charCodeAt(0);
|
|
41
|
-
});
|
|
42
|
-
view.setUint32(0x0c, header.byteLength, true);
|
|
43
|
-
for (let index = 0; index < headerHash.length / 2; index++) {
|
|
44
|
-
header[0x240 + index] = Number.parseInt(headerHash.slice(index * 2, index * 2 + 2), 16);
|
|
45
|
-
}
|
|
46
|
-
return header;
|
|
24
|
+
const resourceSource = {
|
|
25
|
+
manifestUrl: 'https://example.com/resource/manifest.json',
|
|
47
26
|
};
|
|
48
27
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
28
|
+
const manifestFiles = [
|
|
29
|
+
['bundles/firmware_logo-build.okpkg', 'firmware_logo.okpkg', 'vol0:/bundles/firmware_logo.okpkg'],
|
|
30
|
+
['bundles/font/noto-build.okpkg', 'noto.okpkg', 'vol0:/bundles/font/noto.okpkg'],
|
|
31
|
+
['bundles/font/roobert-build.okpkg', 'roobert.okpkg', 'vol0:/bundles/font/roobert.okpkg'],
|
|
32
|
+
[
|
|
33
|
+
'bundles/images/animation-build.okpkg',
|
|
34
|
+
'animation.okpkg',
|
|
35
|
+
'vol0:/bundles/images/animation.okpkg',
|
|
36
|
+
],
|
|
37
|
+
['bundles/images/images-build.okpkg', 'images.okpkg', 'vol0:/bundles/images/images.okpkg'],
|
|
38
|
+
[
|
|
39
|
+
'bundles/images/wallpaper-build.okpkg',
|
|
40
|
+
'wallpaper.okpkg',
|
|
41
|
+
'vol0:/bundles/images/wallpaper.okpkg',
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
'bundles/translations/translations-build.okpkg',
|
|
45
|
+
'translations.okpkg',
|
|
46
|
+
'vol0:/bundles/translations/translations.okpkg',
|
|
47
|
+
],
|
|
48
|
+
[
|
|
49
|
+
'loaders/bootloader/boot_resource-build.okpkg',
|
|
50
|
+
'boot_resource.okpkg',
|
|
51
|
+
'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
52
|
+
],
|
|
53
|
+
['loaders/rom/params-build.okpkg', 'params.okpkg', 'vol0:/loaders/rom/params.okpkg'],
|
|
54
|
+
].map(([archive_path, original_name, device_path], index) => {
|
|
55
|
+
const binary = new Uint8Array([index + 1]).buffer;
|
|
56
|
+
return {
|
|
57
|
+
archive_path,
|
|
58
|
+
original_name,
|
|
59
|
+
device_path,
|
|
60
|
+
size: 1,
|
|
61
|
+
sha256: bytesToHex(sha256(new Uint8Array(binary))),
|
|
62
|
+
signed: true,
|
|
63
|
+
sig_algo: device_path.startsWith('vol0:/bundles/') ? 'ed25519' : 'mldsa65',
|
|
64
|
+
payload_version: '1.0.0',
|
|
65
|
+
binary,
|
|
66
|
+
};
|
|
67
|
+
});
|
|
56
68
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
const resourceManifest = {
|
|
70
|
+
schema: 1,
|
|
71
|
+
artifact_name: 'pro2-resource-build',
|
|
72
|
+
release_name: 'resource-build',
|
|
73
|
+
variant: 'resource',
|
|
74
|
+
commit: 'a'.repeat(40),
|
|
75
|
+
short_sha: 'aaaaaaa',
|
|
76
|
+
timestamp_utc: '20260807_091424',
|
|
77
|
+
core_version: '1.0.0',
|
|
78
|
+
key_set: 'dev',
|
|
79
|
+
device_root: 'vol0:',
|
|
80
|
+
restore_mode: 'bootloader_update',
|
|
81
|
+
trees: [
|
|
82
|
+
{ path: 'bundles', device: 'vol0:/bundles' },
|
|
83
|
+
{ path: 'loaders/bootloader', device: 'vol0:/loaders/bootloader' },
|
|
84
|
+
{ path: 'loaders/rom', device: 'vol0:/loaders/rom' },
|
|
69
85
|
],
|
|
86
|
+
files: manifestFiles.map(({ binary: _binary, ...file }) => file),
|
|
70
87
|
};
|
|
71
88
|
|
|
72
89
|
const createSettings = (configFetcher: ConnectSettings['configFetcher']): ConnectSettings =>
|
|
@@ -85,7 +102,7 @@ const createRemoteConfig = (): RemoteConfigResponse =>
|
|
|
85
102
|
mini: { firmware: [], ble: [] },
|
|
86
103
|
touch: { firmware: [], ble: [] },
|
|
87
104
|
pro: { firmware: [], ble: [] },
|
|
88
|
-
pro2: { firmware: [], ble: [], resources: {
|
|
105
|
+
pro2: { firmware: [], ble: [], resources: { source: resourceSource } },
|
|
89
106
|
bridge: {},
|
|
90
107
|
} as unknown as RemoteConfigResponse);
|
|
91
108
|
|
|
@@ -95,186 +112,51 @@ describe('Pro2 resource configuration', () => {
|
|
|
95
112
|
DataManager.lastCheckTimestamp = 0;
|
|
96
113
|
});
|
|
97
114
|
|
|
98
|
-
test('accepts
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
boot: bootResources,
|
|
115
|
+
test('accepts the manifest source and the CI schema 1 file set', () => {
|
|
116
|
+
expect(parseProtocolV2Resources({ source: resourceSource })).toEqual({
|
|
117
|
+
source: resourceSource,
|
|
102
118
|
});
|
|
103
|
-
|
|
104
|
-
expect(parsed?.stable.map(item => item.type)).toEqual(PROTOCOL_V2_RESOURCE_TYPES);
|
|
105
|
-
expect(parsed?.boot).toEqual(bootResources);
|
|
106
|
-
expect(PROTOCOL_V2_RESOURCE_DEVICE_PATHS.translations).toBe(
|
|
107
|
-
'vol0:/bundles/translations/translations.okpkg'
|
|
108
|
-
);
|
|
109
|
-
expect(PROTOCOL_V2_RESOURCE_DEVICE_PATHS.firmware_logo).toBe(
|
|
110
|
-
'vol0:/bundles/firmware_logo.okpkg'
|
|
111
|
-
);
|
|
119
|
+
expect(parseProtocolV2ResourceManifest(resourceManifest).files).toHaveLength(9);
|
|
112
120
|
});
|
|
113
121
|
|
|
114
|
-
test('rejects
|
|
115
|
-
expect(() => parseProtocolV2Resources({
|
|
116
|
-
'7 unique resource types'
|
|
117
|
-
);
|
|
122
|
+
test('rejects missing source and malformed manifest paths', () => {
|
|
123
|
+
expect(() => parseProtocolV2Resources({})).toThrow('source is required');
|
|
118
124
|
expect(() =>
|
|
119
|
-
parseProtocolV2Resources({
|
|
120
|
-
).toThrow('
|
|
125
|
+
parseProtocolV2Resources({ source: { manifestUrl: 'http://example.com/manifest.json' } })
|
|
126
|
+
).toThrow('must use HTTPS');
|
|
121
127
|
expect(() =>
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
128
|
+
parseProtocolV2ResourceManifest({
|
|
129
|
+
...resourceManifest,
|
|
130
|
+
files: resourceManifest.files.map((file, index) =>
|
|
131
|
+
index === 0 ? { ...file, archive_path: '../outside.okpkg' } : file
|
|
125
132
|
),
|
|
126
133
|
})
|
|
127
|
-
).toThrow('
|
|
134
|
+
).toThrow('archive_path');
|
|
128
135
|
});
|
|
129
136
|
|
|
130
|
-
test('
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
)
|
|
137
|
+
test('verifies selected manifest files and preserves manifest device paths', () => {
|
|
138
|
+
const prepared = prepareProtocolV2ResourceFiles({
|
|
139
|
+
manifest: resourceManifest,
|
|
140
|
+
files: manifestFiles.map(file => ({
|
|
141
|
+
archivePath: file.archive_path,
|
|
142
|
+
binary: file.binary,
|
|
143
|
+
})),
|
|
144
|
+
targetsToUpdate: ['boot_resources'],
|
|
145
|
+
});
|
|
146
|
+
expect(prepared.map(file => file.devicePath)).toEqual([
|
|
147
|
+
'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
148
|
+
'vol0:/loaders/rom/params.okpkg',
|
|
149
|
+
]);
|
|
143
150
|
expect(() =>
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
},
|
|
151
|
+
prepareProtocolV2ResourceFiles({
|
|
152
|
+
manifest: resourceManifest,
|
|
153
|
+
files: manifestFiles.map((file, index) => ({
|
|
154
|
+
archivePath: file.archive_path,
|
|
155
|
+
binary: index === 0 ? new Uint8Array([0]).buffer : file.binary,
|
|
156
|
+
})),
|
|
157
|
+
targetsToUpdate: ['resource'],
|
|
150
158
|
})
|
|
151
|
-
).toThrow('
|
|
152
|
-
|
|
153
|
-
for (const devicePath of [
|
|
154
|
-
'vol0:/assets/loaders\\bootloader_crest.bin',
|
|
155
|
-
'vol0:/assets/loaders\\\\bootloader_crest.bin',
|
|
156
|
-
]) {
|
|
157
|
-
expect(() =>
|
|
158
|
-
parseProtocolV2Resources({
|
|
159
|
-
stable: resources,
|
|
160
|
-
boot: {
|
|
161
|
-
...bootResources,
|
|
162
|
-
files: [{ ...bootResources.files[0], devicePath }],
|
|
163
|
-
},
|
|
164
|
-
})
|
|
165
|
-
).toThrow('devicePath');
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
test('downloads nothing when all resource identities match', () => {
|
|
170
|
-
const inventory = resources.map(({ type, size, headerHash }) => ({ type, size, headerHash }));
|
|
171
|
-
|
|
172
|
-
expect(
|
|
173
|
-
buildProtocolV2ResourceUpdatePlan({ resources, inventory, mode: 'application' })
|
|
174
|
-
).toEqual({ status: 'valid', resources: [] });
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test('builds inventory from existing filesystem calls without ResourceInventoryGet', async () => {
|
|
178
|
-
const installedResources = resources.map((resource, index) => ({
|
|
179
|
-
...resource,
|
|
180
|
-
size: PROTOCOL_V2_OKPP_HEADER_SIZE + index + 1,
|
|
181
|
-
}));
|
|
182
|
-
const resourceByPath = new Map(
|
|
183
|
-
installedResources.map(resource => [
|
|
184
|
-
PROTOCOL_V2_RESOURCE_DEVICE_PATHS[resource.type],
|
|
185
|
-
resource,
|
|
186
|
-
])
|
|
187
|
-
);
|
|
188
|
-
const typedCall = jest.fn(
|
|
189
|
-
(requestType: string, _responseType: string, payload: Record<string, any>) => {
|
|
190
|
-
if (requestType === 'ResourceInventoryGet') {
|
|
191
|
-
throw new Error('ResourceInventoryGet is unavailable on released firmware');
|
|
192
|
-
}
|
|
193
|
-
if (requestType === 'FilesystemPathInfoQuery') {
|
|
194
|
-
const resource = resourceByPath.get(payload.path);
|
|
195
|
-
return {
|
|
196
|
-
message: {
|
|
197
|
-
exist: Boolean(resource),
|
|
198
|
-
directory: false,
|
|
199
|
-
size: resource?.size ?? 0,
|
|
200
|
-
},
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
if (requestType === 'FilesystemFileRead') {
|
|
204
|
-
const resource = resourceByPath.get(payload.file.path);
|
|
205
|
-
if (!resource) throw new Error('missing resource');
|
|
206
|
-
const header = createResourceHeader(resource.headerHash);
|
|
207
|
-
const offset = Number(payload.file.offset);
|
|
208
|
-
const chunkLength = Number(payload.chunk_len);
|
|
209
|
-
return {
|
|
210
|
-
message: {
|
|
211
|
-
data: header.slice(offset, offset + chunkLength),
|
|
212
|
-
},
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
throw new Error(`Unexpected request: ${requestType}`);
|
|
216
|
-
}
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
await expect(
|
|
220
|
-
readProtocolV2ResourceInventory({
|
|
221
|
-
commands: { typedCall },
|
|
222
|
-
resources: installedResources,
|
|
223
|
-
chunkSize: 4000,
|
|
224
|
-
})
|
|
225
|
-
).resolves.toEqual(
|
|
226
|
-
installedResources.map(({ type, size, headerHash }) => ({ type, size, headerHash }))
|
|
227
|
-
);
|
|
228
|
-
expect(typedCall.mock.calls.some(call => call[0] === 'ResourceInventoryGet')).toBe(false);
|
|
229
|
-
expect(typedCall.mock.calls.filter(call => call[0] === 'FilesystemPathInfoQuery')).toHaveLength(
|
|
230
|
-
7
|
|
231
|
-
);
|
|
232
|
-
expect(typedCall.mock.calls.some(call => call[0] === 'FilesystemFileRead')).toBe(true);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
test('selects only the changed or missing resource in application mode', () => {
|
|
236
|
-
const inventory = resources
|
|
237
|
-
.filter(resource => resource.type !== 'noto')
|
|
238
|
-
.map(({ type, size, headerHash }) => ({
|
|
239
|
-
type,
|
|
240
|
-
size: type === 'images' ? size + 1 : size,
|
|
241
|
-
headerHash,
|
|
242
|
-
}));
|
|
243
|
-
|
|
244
|
-
const result = buildProtocolV2ResourceUpdatePlan({
|
|
245
|
-
resources,
|
|
246
|
-
inventory,
|
|
247
|
-
mode: 'application',
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
expect(result.status).toBe('outdated');
|
|
251
|
-
expect(result.resources.map(resource => resource.type)).toEqual(['images', 'noto']);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
test('reports unknown without an application inventory and selects all in recovery mode', () => {
|
|
255
|
-
expect(buildProtocolV2ResourceUpdatePlan({ resources, mode: 'application' })).toEqual({
|
|
256
|
-
status: 'unknown',
|
|
257
|
-
resources: [],
|
|
258
|
-
});
|
|
259
|
-
expect(
|
|
260
|
-
buildProtocolV2ResourceUpdatePlan({ resources, mode: 'bootloader-recovery' }).resources
|
|
261
|
-
).toHaveLength(7);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
test('uses a filesystem inventory for incremental recovery mode updates', () => {
|
|
265
|
-
const inventory = resources.slice(1).map(({ type, size, headerHash }) => ({
|
|
266
|
-
type,
|
|
267
|
-
size,
|
|
268
|
-
headerHash,
|
|
269
|
-
}));
|
|
270
|
-
|
|
271
|
-
expect(
|
|
272
|
-
buildProtocolV2ResourceUpdatePlan({
|
|
273
|
-
resources,
|
|
274
|
-
inventory,
|
|
275
|
-
mode: 'bootloader-recovery',
|
|
276
|
-
}).resources.map(resource => resource.type)
|
|
277
|
-
).toEqual(['images']);
|
|
159
|
+
).toThrow('verification failed');
|
|
278
160
|
});
|
|
279
161
|
|
|
280
162
|
test('verifies both full file size and SHA-256 before transfer', () => {
|
|
@@ -289,7 +171,7 @@ describe('Pro2 resource configuration', () => {
|
|
|
289
171
|
);
|
|
290
172
|
});
|
|
291
173
|
|
|
292
|
-
test('applies a validated pre-release config and exposes
|
|
174
|
+
test('applies a validated pre-release config and exposes its manifest source', async () => {
|
|
293
175
|
const configFetcher = jest.fn().mockResolvedValue(createRemoteConfig());
|
|
294
176
|
|
|
295
177
|
await expect(DataManager.load(createSettings(configFetcher))).resolves.toBe(true);
|
|
@@ -297,8 +179,7 @@ describe('Pro2 resource configuration', () => {
|
|
|
297
179
|
expect(configFetcher).toHaveBeenCalledWith(
|
|
298
180
|
expect.stringMatching(/^https:\/\/data\.onekey\.so\/pre-config\.json\?noCache=/)
|
|
299
181
|
);
|
|
300
|
-
expect(DataManager.
|
|
301
|
-
expect(DataManager.getProtocolV2BootResources()).toEqual(bootResources);
|
|
182
|
+
expect(DataManager.getProtocolV2ResourceSource()).toEqual(resourceSource);
|
|
302
183
|
expect(DataManager.lastCheckTimestamp).toBeGreaterThan(0);
|
|
303
184
|
});
|
|
304
185
|
|
|
@@ -314,18 +195,17 @@ describe('Pro2 resource configuration', () => {
|
|
|
314
195
|
|
|
315
196
|
test('keeps base SDK initialization available when remote Pro2 resources are invalid', async () => {
|
|
316
197
|
const remoteConfig = createRemoteConfig();
|
|
317
|
-
(remoteConfig.pro2 as { resources?: unknown }).resources = {
|
|
198
|
+
(remoteConfig.pro2 as { resources?: unknown }).resources = { source: {} };
|
|
318
199
|
const configFetcher = jest.fn().mockResolvedValue(remoteConfig);
|
|
319
200
|
|
|
320
201
|
await expect(DataManager.load(createSettings(configFetcher))).resolves.toBe(true);
|
|
321
202
|
|
|
322
|
-
expect(DataManager.
|
|
323
|
-
expect(DataManager.getProtocolV2BootResources()).toBeUndefined();
|
|
203
|
+
expect(DataManager.getProtocolV2ResourceSource()).toBeUndefined();
|
|
324
204
|
});
|
|
325
205
|
|
|
326
206
|
test('only blocks resource mutation when the refreshed Pro2 resources are invalid', async () => {
|
|
327
207
|
const remoteConfig = createRemoteConfig();
|
|
328
|
-
(remoteConfig.pro2 as { resources?: unknown }).resources = {
|
|
208
|
+
(remoteConfig.pro2 as { resources?: unknown }).resources = { source: {} };
|
|
329
209
|
const settings = createSettings(jest.fn().mockResolvedValue(remoteConfig));
|
|
330
210
|
DataManager.settings = settings;
|
|
331
211
|
|