@onekeyfe/hd-core 1.2.0-alpha.75 → 1.2.0-alpha.77
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__/connectSettings.test.ts +45 -5
- package/__tests__/device-initialize-timeout.test.ts +56 -0
- package/__tests__/firmware-update/check-all-firmware-release.test.ts +175 -0
- package/__tests__/firmware-update/device-update-bootloader.test.ts +87 -0
- package/__tests__/firmware-update/firmware-artifact-source.test.ts +119 -0
- package/__tests__/firmware-update/firmware-host-binding.test.ts +70 -0
- package/__tests__/firmware-update/firmware-preparation-error.test.ts +27 -0
- package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +200 -0
- package/__tests__/firmware-update/firmware-update-capabilities.test.ts +13 -0
- package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +394 -0
- package/__tests__/firmware-update/firmware-update-plan.test.ts +593 -0
- package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +231 -0
- package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +86 -31
- package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +121 -0
- package/__tests__/firmware-update/firmware-upload-source.test.ts +70 -0
- package/__tests__/protocol-v2.test.ts +405 -100
- package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
- package/dist/api/FirmwareUpdate.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV2.d.ts +10 -0
- package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV3.d.ts +7 -3
- package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +25 -5
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/device/DeviceUpdateBootloader.d.ts +5 -1
- package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareArtifactSource.d.ts +25 -0
- package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareHostBinding.d.ts +6 -0
- package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -0
- package/dist/api/firmware/FirmwarePreparationError.d.ts +3 -0
- package/dist/api/firmware/FirmwarePreparationError.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +8 -0
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +3 -0
- package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts +25 -0
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +32 -0
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -0
- package/dist/api/firmware/getBinary.d.ts +7 -0
- package/dist/api/firmware/getBinary.d.ts.map +1 -1
- package/dist/api/firmware/updateBootloader.d.ts +2 -0
- package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
- package/dist/api/firmware/uploadFirmware.d.ts +9 -1
- package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
- package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
- package/dist/data-manager/DataManager.d.ts +1 -0
- package/dist/data-manager/DataManager.d.ts.map +1 -1
- package/dist/data-manager/connectSettings.d.ts.map +1 -1
- package/dist/device/Device.d.ts +1 -0
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/index.d.ts +179 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3088 -1063
- package/dist/inject.d.ts.map +1 -1
- package/dist/lowLevelInject.d.ts.map +1 -1
- package/dist/topLevelInject.d.ts.map +1 -1
- package/dist/types/api/checkAllFirmwareRelease.d.ts +4 -0
- package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
- package/dist/types/api/deviceUpdateBootloader.d.ts +6 -0
- package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
- package/dist/types/api/export.d.ts +4 -1
- package/dist/types/api/export.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdate.d.ts +68 -0
- package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdateCapabilities.d.ts +9 -0
- package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +1 -0
- package/dist/types/api/firmwareUpdatePlan.d.ts +28 -0
- package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -0
- package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +42 -0
- package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +1 -0
- package/dist/types/api/index.d.ts +9 -1
- package/dist/types/api/index.d.ts.map +1 -1
- package/dist/types/settings.d.ts +13 -0
- package/dist/types/settings.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesCompat.d.ts +1 -0
- package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/CheckAllFirmwareRelease.ts +57 -6
- package/src/api/FirmwareUpdate.ts +13 -1
- package/src/api/FirmwareUpdateV2.ts +308 -121
- package/src/api/FirmwareUpdateV3.ts +265 -58
- package/src/api/FirmwareUpdateV4.ts +881 -349
- package/src/api/device/DeviceUpdateBootloader.ts +125 -6
- package/src/api/firmware/FirmwareArtifactSource.ts +278 -0
- package/src/api/firmware/FirmwareHostBinding.ts +100 -0
- package/src/api/firmware/FirmwarePreparationError.ts +12 -0
- package/src/api/firmware/FirmwareUpdateBaseMethod.ts +72 -1
- package/src/api/firmware/FirmwareUpdateCapabilities.ts +9 -0
- package/src/api/firmware/FirmwareUpdatePlan.ts +772 -0
- package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +449 -0
- package/src/api/firmware/updateBootloader.ts +19 -4
- package/src/api/firmware/uploadFirmware.ts +140 -22
- package/src/data-manager/DataManager.ts +62 -54
- package/src/data-manager/connectSettings.ts +11 -0
- package/src/device/Device.ts +6 -1
- package/src/index.ts +5 -0
- package/src/inject.ts +22 -2
- package/src/lowLevelInject.ts +5 -1
- package/src/topLevelInject.ts +5 -1
- package/src/types/api/checkAllFirmwareRelease.ts +4 -0
- package/src/types/api/deviceUpdateBootloader.ts +6 -0
- package/src/types/api/export.ts +18 -0
- package/src/types/api/firmwareUpdate.ts +75 -0
- package/src/types/api/firmwareUpdateCapabilities.ts +9 -0
- package/src/types/api/firmwareUpdatePlan.ts +38 -0
- package/src/types/api/firmwareUpdatePreparedPlan.ts +53 -0
- package/src/types/api/index.ts +14 -0
- package/src/types/settings.ts +13 -0
- package/src/utils/deviceFeaturesCompat.ts +6 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { EDeviceType, EFirmwareType } from '@onekeyfe/hd-shared';
|
|
2
|
+
|
|
3
|
+
import { buildFirmwareUpdatePlan } from '../../src/api/firmware/FirmwareUpdatePlan';
|
|
4
|
+
import {
|
|
5
|
+
assertFirmwareUpdatePreparedPlanBinding,
|
|
6
|
+
assertFirmwareUpdatePreparedPlanDeviceIdentity,
|
|
7
|
+
prepareFirmwareUpdatePlan,
|
|
8
|
+
validateFirmwareUpdatePreparedPlan,
|
|
9
|
+
} from '../../src/api/firmware/FirmwareUpdatePreparedPlan';
|
|
10
|
+
|
|
11
|
+
import type { Features } from '../../src/types';
|
|
12
|
+
|
|
13
|
+
const buildPlan = () =>
|
|
14
|
+
buildFirmwareUpdatePlan({
|
|
15
|
+
features: {
|
|
16
|
+
deviceType: EDeviceType.Classic1s,
|
|
17
|
+
serialNo: 'classic1s-device-id',
|
|
18
|
+
firmwareVersion: '1.0.0',
|
|
19
|
+
bootloaderVersion: '1.0.0',
|
|
20
|
+
} as Features,
|
|
21
|
+
firmwareType: EFirmwareType.Universal,
|
|
22
|
+
platform: 'native',
|
|
23
|
+
firmware: {
|
|
24
|
+
status: 'outdated',
|
|
25
|
+
release: {
|
|
26
|
+
url: 'https://firmware.onekey.so/classic/firmware.bin',
|
|
27
|
+
expectedSize: 4,
|
|
28
|
+
fingerprint: 'a'.repeat(64),
|
|
29
|
+
version: [3, 0, 0],
|
|
30
|
+
resource: 'https://firmware.onekey.so/classic/resource.zip',
|
|
31
|
+
resourceExpectedSize: 8,
|
|
32
|
+
resourceFingerprint: 'b'.repeat(64),
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
ble: {
|
|
36
|
+
status: 'valid',
|
|
37
|
+
release: undefined,
|
|
38
|
+
},
|
|
39
|
+
bootloader: {
|
|
40
|
+
status: 'valid',
|
|
41
|
+
release: undefined,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const artifact = (sha256: string, size: number) => ({
|
|
46
|
+
artifactRef: `fw:${sha256}`,
|
|
47
|
+
size,
|
|
48
|
+
sha256,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('FirmwareUpdatePreparedPlan', () => {
|
|
52
|
+
test('binds all receipts into a URL-free, network-forbidden plan', () => {
|
|
53
|
+
const plan = buildPlan();
|
|
54
|
+
const prepared = prepareFirmwareUpdatePlan({
|
|
55
|
+
plan,
|
|
56
|
+
leaseRef: 'fwlease:00000000-0000-4000-8000-000000000001',
|
|
57
|
+
artifacts: [
|
|
58
|
+
{
|
|
59
|
+
artifactId: 'firmware',
|
|
60
|
+
artifact: artifact('a'.repeat(64), 4),
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
artifactId: 'resource',
|
|
64
|
+
artifact: artifact('b'.repeat(64), 8),
|
|
65
|
+
materializedEntries: [
|
|
66
|
+
{
|
|
67
|
+
entryName: 'resource/index.bin',
|
|
68
|
+
artifact: artifact('c'.repeat(64), 2),
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(prepared.networkPolicy).toBe('forbid');
|
|
76
|
+
expect(prepared.planDigest).toBe(plan.planDigest);
|
|
77
|
+
expect(prepared.preparedPlanDigest).toMatch(/^[a-f0-9]{64}$/u);
|
|
78
|
+
expect(JSON.stringify(prepared)).not.toContain('https://');
|
|
79
|
+
expect(validateFirmwareUpdatePreparedPlan(prepared)).toEqual(prepared);
|
|
80
|
+
expect(
|
|
81
|
+
assertFirmwareUpdatePreparedPlanDeviceIdentity({
|
|
82
|
+
preparedPlan: prepared,
|
|
83
|
+
deviceIdentity: plan.deviceIdentity,
|
|
84
|
+
})
|
|
85
|
+
).toEqual(prepared);
|
|
86
|
+
expect(() =>
|
|
87
|
+
assertFirmwareUpdatePreparedPlanDeviceIdentity({
|
|
88
|
+
preparedPlan: prepared,
|
|
89
|
+
deviceIdentity: 'different-device-id',
|
|
90
|
+
})
|
|
91
|
+
).toThrow();
|
|
92
|
+
expect(
|
|
93
|
+
assertFirmwareUpdatePreparedPlanBinding({
|
|
94
|
+
preparedPlan: prepared,
|
|
95
|
+
executor: 'v2',
|
|
96
|
+
platform: 'native',
|
|
97
|
+
scopeTargets: ['firmware', 'resource'],
|
|
98
|
+
bindings: [
|
|
99
|
+
{
|
|
100
|
+
target: 'firmware',
|
|
101
|
+
artifact: artifact('a'.repeat(64), 4),
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
target: 'resource',
|
|
105
|
+
entryName: 'resource/index.bin',
|
|
106
|
+
artifact: artifact('c'.repeat(64), 2),
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
})
|
|
110
|
+
).toEqual(prepared);
|
|
111
|
+
expect(() =>
|
|
112
|
+
assertFirmwareUpdatePreparedPlanBinding({
|
|
113
|
+
preparedPlan: prepared,
|
|
114
|
+
executor: 'v2',
|
|
115
|
+
platform: 'native',
|
|
116
|
+
scopeTargets: ['firmware', 'resource'],
|
|
117
|
+
bindings: [
|
|
118
|
+
{
|
|
119
|
+
target: 'firmware',
|
|
120
|
+
artifact: artifact('a'.repeat(64), 4),
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
target: 'resource',
|
|
124
|
+
artifact: artifact('b'.repeat(64), 8),
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
})
|
|
128
|
+
).toThrow('artifact binding is invalid');
|
|
129
|
+
expect(() =>
|
|
130
|
+
assertFirmwareUpdatePreparedPlanBinding({
|
|
131
|
+
preparedPlan: prepared,
|
|
132
|
+
executor: 'v2',
|
|
133
|
+
platform: 'native',
|
|
134
|
+
scopeTargets: ['firmware'],
|
|
135
|
+
bindings: [
|
|
136
|
+
{
|
|
137
|
+
target: 'firmware',
|
|
138
|
+
artifact: artifact('b'.repeat(64), 8),
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
})
|
|
142
|
+
).toThrow('artifact binding is invalid');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('rejects receipt drift and a tampered update plan digest', () => {
|
|
146
|
+
const plan = buildPlan();
|
|
147
|
+
expect(() =>
|
|
148
|
+
prepareFirmwareUpdatePlan({
|
|
149
|
+
plan,
|
|
150
|
+
leaseRef: 'fwlease:00000000-0000-4000-8000-000000000001',
|
|
151
|
+
artifacts: [
|
|
152
|
+
{
|
|
153
|
+
artifactId: 'firmware',
|
|
154
|
+
artifact: artifact('d'.repeat(64), 4),
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
artifactId: 'resource',
|
|
158
|
+
artifact: artifact('b'.repeat(64), 8),
|
|
159
|
+
materializedEntries: [
|
|
160
|
+
{
|
|
161
|
+
entryName: 'resource/index.bin',
|
|
162
|
+
artifact: artifact('c'.repeat(64), 2),
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
})
|
|
168
|
+
).toThrow('receipt does not match');
|
|
169
|
+
|
|
170
|
+
expect(() =>
|
|
171
|
+
prepareFirmwareUpdatePlan({
|
|
172
|
+
plan: { ...plan, planDigest: 'f'.repeat(64) },
|
|
173
|
+
leaseRef: 'fwlease:00000000-0000-4000-8000-000000000001',
|
|
174
|
+
artifacts: [],
|
|
175
|
+
})
|
|
176
|
+
).toThrow('plan digest is invalid');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('projects portable archive paths to unique device resource names', () => {
|
|
180
|
+
const plan = buildPlan();
|
|
181
|
+
const buildArtifacts = (entryNames: string[]) => [
|
|
182
|
+
{
|
|
183
|
+
artifactId: 'firmware',
|
|
184
|
+
artifact: artifact('a'.repeat(64), 4),
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
artifactId: 'resource',
|
|
188
|
+
artifact: artifact('b'.repeat(64), 8),
|
|
189
|
+
materializedEntries: entryNames.map((entryName, index) => ({
|
|
190
|
+
entryName,
|
|
191
|
+
artifact: artifact(String(index + 1).repeat(64), 2),
|
|
192
|
+
})),
|
|
193
|
+
},
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
expect(() =>
|
|
197
|
+
prepareFirmwareUpdatePlan({
|
|
198
|
+
plan,
|
|
199
|
+
leaseRef: 'fwlease:00000000-0000-4000-8000-000000000001',
|
|
200
|
+
artifacts: buildArtifacts(['icons/index.bin', 'fonts/index.bin']),
|
|
201
|
+
})
|
|
202
|
+
).toThrow('duplicate entry names');
|
|
203
|
+
expect(() =>
|
|
204
|
+
prepareFirmwareUpdatePlan({
|
|
205
|
+
plan,
|
|
206
|
+
leaseRef: 'fwlease:00000000-0000-4000-8000-000000000001',
|
|
207
|
+
artifacts: buildArtifacts(['../index.bin']),
|
|
208
|
+
})
|
|
209
|
+
).toThrow('entry name is invalid');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('rejects unknown fields even when a caller presents a plan-shaped object', () => {
|
|
213
|
+
const plan = buildPlan();
|
|
214
|
+
expect(() =>
|
|
215
|
+
prepareFirmwareUpdatePlan({
|
|
216
|
+
plan: {
|
|
217
|
+
...plan,
|
|
218
|
+
artifacts: [
|
|
219
|
+
{
|
|
220
|
+
...plan.artifacts[0],
|
|
221
|
+
unexpectedUrl: 'https://attacker.invalid/payload',
|
|
222
|
+
},
|
|
223
|
+
plan.artifacts[1],
|
|
224
|
+
],
|
|
225
|
+
} as typeof plan,
|
|
226
|
+
leaseRef: 'fwlease:00000000-0000-4000-8000-000000000001',
|
|
227
|
+
artifacts: [],
|
|
228
|
+
})
|
|
229
|
+
).toThrow('invalid field set');
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { EDeviceType, EFirmwareType, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
1
|
+
import { EDeviceType, EFirmwareType, ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
2
|
|
|
3
3
|
import FirmwareUpdateV2 from '../../src/api/FirmwareUpdateV2';
|
|
4
4
|
import { getBinary } from '../../src/api/firmware/getBinary';
|
|
5
|
-
import {
|
|
5
|
+
import { uploadFirmwareFromSource } from '../../src/api/firmware/uploadFirmware';
|
|
6
6
|
import * as utils from '../../src/utils';
|
|
7
7
|
|
|
8
8
|
jest.mock('../../src/data/config', () => ({
|
|
@@ -18,7 +18,8 @@ jest.mock('../../src/api/firmware/getBinary', () => ({
|
|
|
18
18
|
|
|
19
19
|
jest.mock('../../src/api/firmware/uploadFirmware', () => ({
|
|
20
20
|
updateResources: jest.fn(),
|
|
21
|
-
|
|
21
|
+
updateResourcesFromSources: jest.fn(),
|
|
22
|
+
uploadFirmwareFromSource: jest.fn(),
|
|
22
23
|
}));
|
|
23
24
|
|
|
24
25
|
jest.mock('../../src/device/DevicePool', () => ({
|
|
@@ -29,7 +30,9 @@ jest.mock('../../src/device/DevicePool', () => ({
|
|
|
29
30
|
}));
|
|
30
31
|
|
|
31
32
|
const mockGetBinary = getBinary as jest.MockedFunction<typeof getBinary>;
|
|
32
|
-
const
|
|
33
|
+
const mockUploadFirmwareFromSource = uploadFirmwareFromSource as jest.MockedFunction<
|
|
34
|
+
typeof uploadFirmwareFromSource
|
|
35
|
+
>;
|
|
33
36
|
|
|
34
37
|
type DeviceType = 'CLASSIC1S' | 'PURE';
|
|
35
38
|
|
|
@@ -148,6 +151,33 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
|
|
|
148
151
|
jest.restoreAllMocks();
|
|
149
152
|
});
|
|
150
153
|
|
|
154
|
+
it('rejects mixed binary and prepared artifact inputs during initialization', () => {
|
|
155
|
+
const method = new FirmwareUpdateV2({
|
|
156
|
+
id: 1,
|
|
157
|
+
payload: {
|
|
158
|
+
method: 'firmwareUpdateV2',
|
|
159
|
+
connectId: 'connect-id',
|
|
160
|
+
platform: 'desktop',
|
|
161
|
+
updateType: 'firmware',
|
|
162
|
+
binary: new ArrayBuffer(4),
|
|
163
|
+
artifact: {
|
|
164
|
+
artifactRef: `fw:${'a'.repeat(64)}`,
|
|
165
|
+
size: 4,
|
|
166
|
+
sha256: 'a'.repeat(64),
|
|
167
|
+
},
|
|
168
|
+
preparedPlan: {},
|
|
169
|
+
hostBindingGeneration: 1,
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
expect(() => method.init()).toThrow(
|
|
174
|
+
expect.objectContaining({
|
|
175
|
+
errorCode: HardwareErrorCode.CallMethodInvalidParameter,
|
|
176
|
+
message: 'Firmware update binary and artifact are mutually exclusive',
|
|
177
|
+
})
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
151
181
|
it.each([
|
|
152
182
|
{
|
|
153
183
|
context: 'Classic 1S normal mode',
|
|
@@ -175,7 +205,26 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
|
|
|
175
205
|
expect(mockGetBinary).toHaveBeenCalledTimes(1);
|
|
176
206
|
expectNoFirmwareMutationCalls(typedCall);
|
|
177
207
|
expect(acquire).not.toHaveBeenCalled();
|
|
178
|
-
expect(
|
|
208
|
+
expect(mockUploadFirmwareFromSource).not.toHaveBeenCalled();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('preserves structured preparation errors before any firmware mutation', async () => {
|
|
212
|
+
const preparationError = ERRORS.TypedError(
|
|
213
|
+
HardwareErrorCode.RuntimeError,
|
|
214
|
+
'Firmware must be prepared by the external firmware host',
|
|
215
|
+
{
|
|
216
|
+
firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
|
|
217
|
+
artifactName: 'firmware',
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
mockGetBinary.mockRejectedValue(preparationError);
|
|
221
|
+
const { method, typedCall, acquire } = createMethod();
|
|
222
|
+
|
|
223
|
+
await expect(method.run()).rejects.toBe(preparationError);
|
|
224
|
+
|
|
225
|
+
expectNoFirmwareMutationCalls(typedCall);
|
|
226
|
+
expect(acquire).not.toHaveBeenCalled();
|
|
227
|
+
expect(mockUploadFirmwareFromSource).not.toHaveBeenCalled();
|
|
179
228
|
});
|
|
180
229
|
|
|
181
230
|
it.each([
|
|
@@ -216,9 +265,11 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
|
|
|
216
265
|
binary: firmwareBinary,
|
|
217
266
|
} as Awaited<ReturnType<typeof getBinary>>);
|
|
218
267
|
});
|
|
219
|
-
|
|
268
|
+
mockUploadFirmwareFromSource.mockImplementation(() => {
|
|
220
269
|
callOrder.push('upload');
|
|
221
|
-
return Promise.resolve(
|
|
270
|
+
return Promise.resolve(
|
|
271
|
+
expectedResponse as Awaited<ReturnType<typeof uploadFirmwareFromSource>>
|
|
272
|
+
);
|
|
222
273
|
});
|
|
223
274
|
const { method, typedCall } = createMethod({
|
|
224
275
|
updateType,
|
|
@@ -248,15 +299,15 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
|
|
|
248
299
|
},
|
|
249
300
|
})
|
|
250
301
|
);
|
|
251
|
-
expect(
|
|
302
|
+
expect(mockUploadFirmwareFromSource).toHaveBeenCalledWith(
|
|
252
303
|
updateType,
|
|
253
304
|
expect.any(Function),
|
|
254
305
|
expect.any(Function),
|
|
255
306
|
method.device,
|
|
256
|
-
{
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
307
|
+
expect.objectContaining({
|
|
308
|
+
size: firmwareBinary.byteLength,
|
|
309
|
+
}),
|
|
310
|
+
true,
|
|
260
311
|
isUpdateBootloader
|
|
261
312
|
);
|
|
262
313
|
}
|
|
@@ -271,11 +322,11 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
|
|
|
271
322
|
binary: firmwareBinary,
|
|
272
323
|
} as Awaited<ReturnType<typeof getBinary>>);
|
|
273
324
|
});
|
|
274
|
-
|
|
325
|
+
mockUploadFirmwareFromSource.mockImplementation(() => {
|
|
275
326
|
callOrder.push('upload');
|
|
276
327
|
return Promise.resolve({
|
|
277
328
|
success: true,
|
|
278
|
-
} as Awaited<ReturnType<typeof
|
|
329
|
+
} as Awaited<ReturnType<typeof uploadFirmwareFromSource>>);
|
|
279
330
|
});
|
|
280
331
|
const { method, typedCall, acquire } = createMethod({
|
|
281
332
|
bootloaderMode: true,
|
|
@@ -300,9 +351,9 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
|
|
|
300
351
|
resolveDownload = resolve;
|
|
301
352
|
})
|
|
302
353
|
);
|
|
303
|
-
|
|
354
|
+
mockUploadFirmwareFromSource.mockResolvedValue({
|
|
304
355
|
success: true,
|
|
305
|
-
} as Awaited<ReturnType<typeof
|
|
356
|
+
} as Awaited<ReturnType<typeof uploadFirmwareFromSource>>);
|
|
306
357
|
const { method, typedCall } = createMethod();
|
|
307
358
|
|
|
308
359
|
const runPromise = method.run();
|
|
@@ -333,7 +384,7 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
|
|
|
333
384
|
|
|
334
385
|
expectNoFirmwareMutationCalls(typedCall);
|
|
335
386
|
expect(acquire).not.toHaveBeenCalled();
|
|
336
|
-
expect(
|
|
387
|
+
expect(mockUploadFirmwareFromSource).not.toHaveBeenCalled();
|
|
337
388
|
});
|
|
338
389
|
|
|
339
390
|
it.each([
|
|
@@ -342,23 +393,23 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
|
|
|
342
393
|
])(
|
|
343
394
|
'keeps the non-empty %s binary overload and never downloads it again',
|
|
344
395
|
async (_kind, binary) => {
|
|
345
|
-
|
|
396
|
+
mockUploadFirmwareFromSource.mockResolvedValue({
|
|
346
397
|
success: true,
|
|
347
|
-
} as Awaited<ReturnType<typeof
|
|
398
|
+
} as Awaited<ReturnType<typeof uploadFirmwareFromSource>>);
|
|
348
399
|
const { method } = createMethod({ binary });
|
|
349
400
|
|
|
350
401
|
await method.run();
|
|
351
402
|
|
|
352
403
|
expect(mockGetBinary).not.toHaveBeenCalled();
|
|
353
|
-
expect(
|
|
404
|
+
expect(mockUploadFirmwareFromSource).toHaveBeenCalledWith(
|
|
354
405
|
'firmware',
|
|
355
406
|
expect.any(Function),
|
|
356
407
|
expect.any(Function),
|
|
357
408
|
method.device,
|
|
358
|
-
{
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
409
|
+
expect.objectContaining({
|
|
410
|
+
size: binary.byteLength,
|
|
411
|
+
}),
|
|
412
|
+
true,
|
|
362
413
|
undefined
|
|
363
414
|
);
|
|
364
415
|
}
|
|
@@ -369,17 +420,21 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
|
|
|
369
420
|
['DataView', new DataView(new Uint8Array([9, 3, 4, 9]).buffer, 1, 2), [3, 4]],
|
|
370
421
|
['custom Buffer', createCustomBuffer(4), [1, 2, 3, 4]],
|
|
371
422
|
])('normalizes a non-empty %s before rebooting', async (_kind, binary, expectedBytes) => {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
423
|
+
let uploadedBytes: number[] = [];
|
|
424
|
+
mockUploadFirmwareFromSource.mockImplementation(
|
|
425
|
+
async (_updateType, _typedCall, _postMessage, _device, source) => {
|
|
426
|
+
uploadedBytes = Array.from(new Uint8Array(await source.readAt(0, source.size)));
|
|
427
|
+
return {
|
|
428
|
+
success: true,
|
|
429
|
+
} as Awaited<ReturnType<typeof uploadFirmwareFromSource>>;
|
|
430
|
+
}
|
|
431
|
+
);
|
|
375
432
|
const { method } = createMethod({ binary });
|
|
376
433
|
|
|
377
434
|
await method.run();
|
|
378
435
|
|
|
379
436
|
expect(mockGetBinary).not.toHaveBeenCalled();
|
|
380
|
-
|
|
381
|
-
expect(normalizedBinary).toBeInstanceOf(ArrayBuffer);
|
|
382
|
-
expect(Array.from(new Uint8Array(normalizedBinary))).toEqual(expectedBytes);
|
|
437
|
+
expect(uploadedBytes).toEqual(expectedBytes);
|
|
383
438
|
});
|
|
384
439
|
|
|
385
440
|
it.each([
|
|
@@ -397,6 +452,6 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
|
|
|
397
452
|
expect(mockGetBinary).not.toHaveBeenCalled();
|
|
398
453
|
expectNoFirmwareMutationCalls(typedCall);
|
|
399
454
|
expect(acquire).not.toHaveBeenCalled();
|
|
400
|
-
expect(
|
|
455
|
+
expect(mockUploadFirmwareFromSource).not.toHaveBeenCalled();
|
|
401
456
|
});
|
|
402
457
|
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import * as hdShared from '@onekeyfe/hd-shared';
|
|
2
|
+
|
|
3
|
+
import FirmwareUpdateV3 from '../../src/api/FirmwareUpdateV3';
|
|
4
|
+
import { DevicePool } from '../../src/device/DevicePool';
|
|
5
|
+
|
|
6
|
+
import type { Device } from '../../src/device/Device';
|
|
7
|
+
|
|
8
|
+
jest.mock('../../src/data/config', () => ({
|
|
9
|
+
getSDKVersion: jest.fn(() => '1.0.0'),
|
|
10
|
+
DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
describe('FirmwareUpdateV3 reconnect', () => {
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
jest.restoreAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('keeps transient USB acquire failures inside the reconnect loop', async () => {
|
|
19
|
+
const acquire = jest.fn().mockResolvedValue(undefined);
|
|
20
|
+
const commands = {
|
|
21
|
+
disposed: true,
|
|
22
|
+
mainId: '',
|
|
23
|
+
};
|
|
24
|
+
const cachedDevice = {} as Device;
|
|
25
|
+
const updateFromCache = jest.fn();
|
|
26
|
+
jest.spyOn(DevicePool, 'getDevices').mockResolvedValue({
|
|
27
|
+
devices: {},
|
|
28
|
+
deviceList: [cachedDevice],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const method = new FirmwareUpdateV3({
|
|
32
|
+
id: 1,
|
|
33
|
+
payload: {
|
|
34
|
+
method: 'firmwareUpdateV3',
|
|
35
|
+
connectId: 'usb-path',
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
method.isBleReconnect = jest.fn(() => false);
|
|
39
|
+
method.device = {
|
|
40
|
+
originalDescriptor: {
|
|
41
|
+
id: 'usb-path',
|
|
42
|
+
path: 'usb-path',
|
|
43
|
+
protocolType: 'V1',
|
|
44
|
+
},
|
|
45
|
+
deviceConnector: {
|
|
46
|
+
enumerate: jest.fn().mockResolvedValue({
|
|
47
|
+
descriptors: [{ path: 'usb-path' }],
|
|
48
|
+
}),
|
|
49
|
+
},
|
|
50
|
+
updateFromCache,
|
|
51
|
+
acquire,
|
|
52
|
+
commands,
|
|
53
|
+
getCommands: () => commands,
|
|
54
|
+
mainId: 'usb-session',
|
|
55
|
+
} as unknown as Device;
|
|
56
|
+
|
|
57
|
+
await method.waitForDeviceReconnect(1000);
|
|
58
|
+
|
|
59
|
+
expect(updateFromCache).toHaveBeenCalledWith(cachedDevice);
|
|
60
|
+
expect(acquire).toHaveBeenCalledWith(undefined, {
|
|
61
|
+
throwOnRunPromiseError: true,
|
|
62
|
+
});
|
|
63
|
+
expect(commands.disposed).toBe(false);
|
|
64
|
+
expect(commands.mainId).toBe('usb-session');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('uses the BLE reconnect budget for a prepared BLE artifact', async () => {
|
|
68
|
+
jest.useFakeTimers({ doNotFake: ['performance'] });
|
|
69
|
+
jest.spyOn(hdShared, 'wait').mockResolvedValue(undefined);
|
|
70
|
+
const method = new FirmwareUpdateV3({
|
|
71
|
+
id: 1,
|
|
72
|
+
payload: {
|
|
73
|
+
method: 'firmwareUpdateV3',
|
|
74
|
+
connectId: 'ble-device',
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
const reconnectError = new Error('stop after reconnect budget assertion');
|
|
78
|
+
const waitForDeviceReconnect = jest
|
|
79
|
+
.spyOn(method, 'waitForDeviceReconnect')
|
|
80
|
+
.mockRejectedValue(reconnectError);
|
|
81
|
+
method.isBleReconnect = jest.fn(() => true);
|
|
82
|
+
method.params = {
|
|
83
|
+
platform: 'native',
|
|
84
|
+
artifacts: {
|
|
85
|
+
ble: {
|
|
86
|
+
artifactRef: `fw:${'a'.repeat(64)}`,
|
|
87
|
+
size: 4,
|
|
88
|
+
sha256: 'a'.repeat(64),
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
} as any;
|
|
92
|
+
const commands = {
|
|
93
|
+
typedCall: jest.fn().mockRejectedValue(new Error('device rebooting')),
|
|
94
|
+
};
|
|
95
|
+
method.device = {
|
|
96
|
+
features: {},
|
|
97
|
+
getCommands: () => commands,
|
|
98
|
+
} as unknown as Device;
|
|
99
|
+
(method as any).createUpdatesFolderIfNotExists = jest.fn().mockResolvedValue(undefined);
|
|
100
|
+
(method as any).startEmmcFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
|
|
101
|
+
method.postTipMessage = jest.fn();
|
|
102
|
+
method.postProcessingMessage = jest.fn();
|
|
103
|
+
method.postProgressMessage = jest.fn();
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
await expect(
|
|
107
|
+
(method as any).executeUpdate({
|
|
108
|
+
resourceBinary: null,
|
|
109
|
+
resourceEntries: [],
|
|
110
|
+
fwSources: [],
|
|
111
|
+
bootloaderSource: null,
|
|
112
|
+
})
|
|
113
|
+
).rejects.toBe(reconnectError);
|
|
114
|
+
|
|
115
|
+
expect(waitForDeviceReconnect).toHaveBeenCalledWith(3 * 60 * 1000);
|
|
116
|
+
} finally {
|
|
117
|
+
jest.clearAllTimers();
|
|
118
|
+
jest.useRealTimers();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { EDeviceType } from '@onekeyfe/hd-shared';
|
|
2
|
+
|
|
3
|
+
import { uploadFirmwareFromSource } from '../../src/api/firmware/uploadFirmware';
|
|
4
|
+
|
|
5
|
+
import type { Device } from '../../src/device/Device';
|
|
6
|
+
import type { FirmwareByteSource } from '../../src/api/firmware/FirmwareArtifactSource';
|
|
7
|
+
|
|
8
|
+
jest.mock('../../src/data/config', () => ({
|
|
9
|
+
getSDKVersion: jest.fn(() => '1.0.0'),
|
|
10
|
+
DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
describe('uploadFirmwareFromSource', () => {
|
|
14
|
+
it('serves legacy Touch firmware requests without materializing the complete artifact', async () => {
|
|
15
|
+
const size = 700 * 1024;
|
|
16
|
+
const requestedRanges: Array<{ offset: number; length: number }> = [];
|
|
17
|
+
const source: FirmwareByteSource = {
|
|
18
|
+
size,
|
|
19
|
+
readAt: jest.fn((offset: number, length: number) => {
|
|
20
|
+
requestedRanges.push({ offset, length });
|
|
21
|
+
return Promise.resolve(new ArrayBuffer(length));
|
|
22
|
+
}),
|
|
23
|
+
close: jest.fn(() => Promise.resolve()),
|
|
24
|
+
};
|
|
25
|
+
let nextOffset = 0;
|
|
26
|
+
const requestedLength = 128 * 1024;
|
|
27
|
+
const typedCall = jest.fn((type: string) => {
|
|
28
|
+
if (type === 'FirmwareErase') {
|
|
29
|
+
return Promise.resolve({
|
|
30
|
+
type: 'FirmwareRequest',
|
|
31
|
+
message: {
|
|
32
|
+
offset: nextOffset,
|
|
33
|
+
length: Math.min(requestedLength, size - nextOffset),
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (type === 'FirmwareUpload') {
|
|
38
|
+
nextOffset += Math.min(requestedLength, size - nextOffset);
|
|
39
|
+
if (nextOffset < size) {
|
|
40
|
+
return Promise.resolve({
|
|
41
|
+
type: 'FirmwareRequest',
|
|
42
|
+
message: {
|
|
43
|
+
offset: nextOffset,
|
|
44
|
+
length: Math.min(requestedLength, size - nextOffset),
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return Promise.resolve({
|
|
49
|
+
type: 'Success',
|
|
50
|
+
message: { message: 'installed' },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return Promise.reject(new Error(`Unexpected command: ${type}`));
|
|
54
|
+
});
|
|
55
|
+
const device = {
|
|
56
|
+
features: undefined,
|
|
57
|
+
emit: jest.fn(),
|
|
58
|
+
getCurrentDeviceType: () => EDeviceType.Touch,
|
|
59
|
+
toMessageObject: () => ({}),
|
|
60
|
+
} as unknown as Device;
|
|
61
|
+
|
|
62
|
+
await expect(
|
|
63
|
+
uploadFirmwareFromSource('firmware', typedCall as never, jest.fn(), device, source)
|
|
64
|
+
).resolves.toEqual({ message: 'installed' });
|
|
65
|
+
|
|
66
|
+
expect(requestedRanges).toHaveLength(Math.ceil(size / requestedLength));
|
|
67
|
+
expect(requestedRanges.every(({ length }) => length <= 256 * 1024)).toBe(true);
|
|
68
|
+
expect(requestedRanges.some(({ length }) => length === size)).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
});
|