@onekeyfe/hd-core 1.2.0-alpha.97 → 1.2.0-alpha.98
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 +404 -84
- package/__tests__/check-firmware-release-protocol-v2.test.ts +199 -0
- package/__tests__/firmware-memory-host.test.ts +112 -0
- package/__tests__/firmware-update/device-update-bootloader.test.ts +13 -2
- package/__tests__/firmware-update/firmware-host-binding.test.ts +9 -0
- package/__tests__/firmware-update/firmware-prepared-host-digest.test.ts +573 -0
- package/__tests__/firmware-update/firmware-prepared-resource-archive.test.ts +150 -0
- package/__tests__/firmware-update/firmware-update-plan.test.ts +232 -70
- package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +53 -1
- package/__tests__/firmware-update/firmware-update-prepared-resource-executors.test.ts +326 -0
- package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +2 -2
- package/__tests__/protocol-v2-resources.test.ts +148 -232
- package/__tests__/protocol-v2.test.ts +1236 -516
- package/dist/api/CheckAllFirmwareRelease.d.ts +2 -3
- package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
- package/dist/api/CheckBLEFirmwareRelease.d.ts +3 -2
- package/dist/api/CheckBLEFirmwareRelease.d.ts.map +1 -1
- package/dist/api/CheckBootloaderRelease.d.ts +2 -1
- package/dist/api/CheckBootloaderRelease.d.ts.map +1 -1
- package/dist/api/CheckFirmwareRelease.d.ts +3 -2
- package/dist/api/CheckFirmwareRelease.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV2.d.ts +1 -0
- package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +8 -8
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/UploadPortfolio.d.ts.map +1 -1
- package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareHostBinding.d.ts +1 -1
- package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareMemoryHost.d.ts +22 -0
- package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +1 -0
- package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts +11 -0
- package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts +48 -1
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +6 -1
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
- package/dist/api/firmware/getBinary.d.ts +0 -1
- package/dist/api/firmware/getBinary.d.ts.map +1 -1
- package/dist/api/firmware/protocolV2Release.d.ts +33 -0
- package/dist/api/firmware/protocolV2Release.d.ts.map +1 -0
- package/dist/data-manager/DataManager.d.ts +4 -3
- package/dist/data-manager/DataManager.d.ts.map +1 -1
- package/dist/index.d.ts +108 -114
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1633 -967
- package/dist/protocols/protocol-v2/resources.d.ts +10 -28
- package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
- package/dist/types/api/checkAllFirmwareRelease.d.ts +20 -8
- package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
- package/dist/types/api/checkBLEFirmwareRelease.d.ts +2 -13
- package/dist/types/api/checkBLEFirmwareRelease.d.ts.map +1 -1
- package/dist/types/api/checkBootloaderRelease.d.ts +2 -6
- package/dist/types/api/checkBootloaderRelease.d.ts.map +1 -1
- package/dist/types/api/checkFirmwareRelease.d.ts +2 -13
- package/dist/types/api/checkFirmwareRelease.d.ts.map +1 -1
- package/dist/types/api/checkFirmwareTypeAvailable.d.ts +2 -2
- package/dist/types/api/checkFirmwareTypeAvailable.d.ts.map +1 -1
- package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
- package/dist/types/api/export.d.ts +1 -1
- package/dist/types/api/export.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdate.d.ts +4 -12
- package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdatePlan.d.ts +2 -2
- package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -1
- package/dist/types/settings.d.ts +35 -36
- package/dist/types/settings.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/CheckAllFirmwareRelease.ts +108 -80
- package/src/api/CheckBLEFirmwareRelease.ts +37 -5
- package/src/api/CheckBootloaderRelease.ts +35 -3
- package/src/api/CheckFirmwareRelease.ts +35 -7
- package/src/api/FirmwareUpdateV2.ts +108 -73
- package/src/api/FirmwareUpdateV3.ts +101 -56
- package/src/api/FirmwareUpdateV4.ts +885 -403
- package/src/api/UploadPortfolio.ts +18 -0
- package/src/api/device/DeviceUpdateBootloader.ts +37 -11
- package/src/api/firmware/FirmwareHostBinding.ts +16 -3
- package/src/api/firmware/FirmwareMemoryHost.ts +142 -0
- package/src/api/firmware/FirmwarePreparedResourceArchive.ts +207 -0
- package/src/api/firmware/FirmwareUpdatePlan.ts +282 -122
- package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +27 -3
- package/src/api/firmware/protocolV2Release.ts +168 -0
- package/src/data-manager/DataManager.ts +35 -19
- package/src/index.ts +10 -0
- package/src/protocols/protocol-v2/resources.ts +157 -341
- package/src/types/api/checkAllFirmwareRelease.ts +27 -12
- package/src/types/api/checkBLEFirmwareRelease.ts +4 -13
- package/src/types/api/checkBootloaderRelease.ts +2 -6
- package/src/types/api/checkFirmwareRelease.ts +2 -13
- package/src/types/api/checkFirmwareTypeAvailable.ts +2 -2
- package/src/types/api/deviceUpdateBootloader.ts +1 -0
- package/src/types/api/export.ts +6 -1
- package/src/types/api/firmwareUpdate.ts +12 -17
- package/src/types/api/firmwareUpdatePlan.ts +2 -2
- package/src/types/settings.ts +35 -61
- package/src/utils/deviceFeaturesUtils.ts +2 -2
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { EFirmwareType, ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
3
|
+
import { bytesToHex } from '@noble/hashes/utils';
|
|
4
|
+
import JSZip from 'jszip';
|
|
2
5
|
import {
|
|
3
6
|
DeviceRebootType,
|
|
4
7
|
DeviceSessionPinType,
|
|
5
8
|
DeviceSettingsPage,
|
|
6
9
|
DeviceType,
|
|
7
10
|
} from '@onekeyfe/hd-transport';
|
|
8
|
-
import { sha256 } from '@noble/hashes/sha256';
|
|
9
11
|
|
|
10
12
|
import * as firmwareBinaryApi from '../src/api/firmware/getBinary';
|
|
11
13
|
import DnxGetAddress from '../src/api/dynex/DnxGetAddress';
|
|
@@ -79,7 +81,6 @@ import {
|
|
|
79
81
|
requestProtocolV2ProtocolInfo,
|
|
80
82
|
supportsProtocolV2Message,
|
|
81
83
|
} from '../src/protocols/protocol-v2/features';
|
|
82
|
-
import { PROTOCOL_V2_RESOURCE_DEVICE_PATHS } from '../src/protocols/protocol-v2/resources';
|
|
83
84
|
import {
|
|
84
85
|
getProtocolV2WalletSession,
|
|
85
86
|
refreshProtocolV2DeviceStatus,
|
|
@@ -102,6 +103,12 @@ import {
|
|
|
102
103
|
} from '../src/utils';
|
|
103
104
|
import { getDeviceFirmwareVersion } from '../src/utils/deviceVersionUtils';
|
|
104
105
|
import { openFirmwareByteSource } from '../src/api/firmware/FirmwareArtifactSource';
|
|
106
|
+
import {
|
|
107
|
+
registerFirmwareUpdateHostBinding,
|
|
108
|
+
unregisterFirmwareUpdateHostBinding,
|
|
109
|
+
} from '../src/api/firmware/FirmwareHostBinding';
|
|
110
|
+
import { prepareFirmwareUpdatePlan } from '../src/api/firmware/FirmwareUpdatePreparedPlan';
|
|
111
|
+
import { digestFirmwareUpdateContract } from '../src/api/firmware/FirmwareUpdatePlan';
|
|
105
112
|
import {
|
|
106
113
|
getPassphraseState,
|
|
107
114
|
getPassphraseStateWithRefreshDeviceInfo,
|
|
@@ -116,6 +123,26 @@ jest.mock('../src/data/config', () => ({
|
|
|
116
123
|
DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
|
|
117
124
|
}));
|
|
118
125
|
|
|
126
|
+
const createProtocolV2OkppBinary = ({
|
|
127
|
+
version = [1, 2, 3],
|
|
128
|
+
payloadHashByte = 0x11,
|
|
129
|
+
headerHashByte = 0x22,
|
|
130
|
+
}: {
|
|
131
|
+
version?: [number, number, number];
|
|
132
|
+
payloadHashByte?: number;
|
|
133
|
+
headerHashByte?: number;
|
|
134
|
+
} = {}) => {
|
|
135
|
+
const bytes = new Uint8Array(0x52a0);
|
|
136
|
+
bytes.set([0x4f, 0x4b, 0x50, 0x50], 0);
|
|
137
|
+
bytes.set([0x52, 0x45, 0x53, 0x43], 0x08);
|
|
138
|
+
const view = new DataView(bytes.buffer);
|
|
139
|
+
view.setUint32(0x0c, 0x52a0, true);
|
|
140
|
+
view.setUint32(0x10, version[0] * 0x10000 + version[1] * 0x100 + version[2], true);
|
|
141
|
+
bytes.fill(payloadHashByte, 0x200, 0x240);
|
|
142
|
+
bytes.fill(headerHashByte, 0x240, 0x280);
|
|
143
|
+
return bytes.buffer;
|
|
144
|
+
};
|
|
145
|
+
|
|
119
146
|
const createWalletSessionTypedCall = (
|
|
120
147
|
implementation = jest.fn(),
|
|
121
148
|
status: Partial<DeviceStatus> = { passphrase_enabled: true }
|
|
@@ -227,6 +254,17 @@ describe('DeviceUploadWallpaper', () => {
|
|
|
227
254
|
});
|
|
228
255
|
|
|
229
256
|
describe('UploadPortfolio', () => {
|
|
257
|
+
const portfolioProtocolInfo = {
|
|
258
|
+
version: 2,
|
|
259
|
+
supported_messages: [60805, 61400],
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const stubPortfolioDevice = <T extends Record<string, any>>(device: T) =>
|
|
263
|
+
stubDevice({
|
|
264
|
+
...device,
|
|
265
|
+
ensureProtocolV2RuntimeContext: jest.fn().mockResolvedValue(portfolioProtocolInfo),
|
|
266
|
+
});
|
|
267
|
+
|
|
230
268
|
test('writes and applies the portfolio while the device is locked without unlocking', async () => {
|
|
231
269
|
const packageBytes = new Uint8Array([1, 2, 3]);
|
|
232
270
|
const typedCall = jest
|
|
@@ -234,7 +272,7 @@ describe('UploadPortfolio', () => {
|
|
|
234
272
|
.mockResolvedValueOnce({ message: { processed_byte: 3 } })
|
|
235
273
|
.mockResolvedValueOnce({ message: { message: 'Portfolio updated' } });
|
|
236
274
|
const unlockDevice = jest.fn().mockResolvedValue(undefined);
|
|
237
|
-
const device =
|
|
275
|
+
const device = stubPortfolioDevice({
|
|
238
276
|
features: { unlocked: false },
|
|
239
277
|
commands: { typedCall },
|
|
240
278
|
isProtocolV2: () => true,
|
|
@@ -279,7 +317,7 @@ describe('UploadPortfolio', () => {
|
|
|
279
317
|
packageBytes,
|
|
280
318
|
},
|
|
281
319
|
});
|
|
282
|
-
(method as any).device =
|
|
320
|
+
(method as any).device = stubPortfolioDevice({ commands: { typedCall } });
|
|
283
321
|
method.postMessage = jest.fn();
|
|
284
322
|
|
|
285
323
|
method.init();
|
|
@@ -327,7 +365,7 @@ describe('UploadPortfolio', () => {
|
|
|
327
365
|
packageBytes: new Uint8Array([1]),
|
|
328
366
|
},
|
|
329
367
|
});
|
|
330
|
-
(method as any).device =
|
|
368
|
+
(method as any).device = stubPortfolioDevice({ commands: { typedCall } });
|
|
331
369
|
|
|
332
370
|
method.init();
|
|
333
371
|
|
|
@@ -350,7 +388,7 @@ describe('UploadPortfolio', () => {
|
|
|
350
388
|
},
|
|
351
389
|
});
|
|
352
390
|
method.abortSignal = abortController.signal;
|
|
353
|
-
(method as any).device =
|
|
391
|
+
(method as any).device = stubPortfolioDevice({ commands: { typedCall } });
|
|
354
392
|
|
|
355
393
|
method.init();
|
|
356
394
|
|
|
@@ -360,6 +398,31 @@ describe('UploadPortfolio', () => {
|
|
|
360
398
|
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
361
399
|
expect(typedCall).not.toHaveBeenCalledWith('PortfolioUpdate', 'Success', {});
|
|
362
400
|
});
|
|
401
|
+
|
|
402
|
+
test('rejects unsupported firmware before staging the package', async () => {
|
|
403
|
+
const typedCall = jest.fn();
|
|
404
|
+
const method = new UploadPortfolio({
|
|
405
|
+
id: 1,
|
|
406
|
+
payload: {
|
|
407
|
+
method: 'uploadPortfolio',
|
|
408
|
+
packageBytes: new Uint8Array([1]),
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
(method as any).device = stubDevice({
|
|
412
|
+
commands: { typedCall },
|
|
413
|
+
ensureProtocolV2RuntimeContext: jest.fn().mockResolvedValue({
|
|
414
|
+
version: 2,
|
|
415
|
+
supported_messages: [60805],
|
|
416
|
+
}),
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
method.init();
|
|
420
|
+
|
|
421
|
+
await expect(method.run()).rejects.toMatchObject({
|
|
422
|
+
errorCode: HardwareErrorCode.DeviceNotSupportMethod,
|
|
423
|
+
});
|
|
424
|
+
expect(typedCall).not.toHaveBeenCalled();
|
|
425
|
+
});
|
|
363
426
|
});
|
|
364
427
|
|
|
365
428
|
const descriptor = {
|
|
@@ -2724,9 +2787,12 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2724
2787
|
expect(checkedTypes).toEqual(['pro2', 'model_pro2']);
|
|
2725
2788
|
});
|
|
2726
2789
|
|
|
2727
|
-
test(
|
|
2790
|
+
test.each([
|
|
2791
|
+
['Pro2', DeviceType.PRO2],
|
|
2792
|
+
['Neo', DeviceType.NEO],
|
|
2793
|
+
] as const)('uses firmware-v1 as the %s remote firmware config field', (_name, deviceType) => {
|
|
2728
2794
|
const features = normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
|
|
2729
|
-
hw: { Device_type:
|
|
2795
|
+
hw: { Device_type: deviceType },
|
|
2730
2796
|
});
|
|
2731
2797
|
|
|
2732
2798
|
expect(
|
|
@@ -3967,7 +4033,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3967
4033
|
});
|
|
3968
4034
|
});
|
|
3969
4035
|
|
|
3970
|
-
test('
|
|
4036
|
+
test('requires the external host to prepare manifest resources before bootloader entry', async () => {
|
|
3971
4037
|
const method = new FirmwareUpdateV4({
|
|
3972
4038
|
id: 1,
|
|
3973
4039
|
payload: {
|
|
@@ -3987,55 +4053,21 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3987
4053
|
bootloaderMode: false,
|
|
3988
4054
|
capabilities: [],
|
|
3989
4055
|
},
|
|
4056
|
+
getCurrentDeviceType: () => 'pro2',
|
|
3990
4057
|
isBootloader: () => false,
|
|
3991
4058
|
isRomloader: () => false,
|
|
3992
4059
|
});
|
|
3993
4060
|
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
3994
|
-
(method as any).
|
|
3995
|
-
bootloaderBinary: null,
|
|
3996
|
-
fwBinaryMap: [],
|
|
3997
|
-
installItems: [],
|
|
3998
|
-
});
|
|
3999
|
-
|
|
4000
|
-
const order: string[] = [];
|
|
4001
|
-
(method as any).prepareProtocolV2BootResources = jest.fn().mockImplementation(() => {
|
|
4002
|
-
order.push('prepare-startup-resources');
|
|
4003
|
-
return Promise.resolve([]);
|
|
4004
|
-
});
|
|
4005
|
-
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockImplementation(() => {
|
|
4006
|
-
order.push('enter-bootloader');
|
|
4007
|
-
return Promise.resolve(true);
|
|
4008
|
-
});
|
|
4009
|
-
(method as any).prepareProtocolV2ResourceBundles = jest.fn().mockImplementation(() => {
|
|
4010
|
-
order.push('prepare-resources');
|
|
4011
|
-
return Promise.resolve([
|
|
4012
|
-
{
|
|
4013
|
-
name: 'images.okpkg',
|
|
4014
|
-
binary: new Uint8Array([1]).buffer,
|
|
4015
|
-
devicePath: 'vol0:/bundles/images/images.okpkg',
|
|
4016
|
-
},
|
|
4017
|
-
]);
|
|
4018
|
-
});
|
|
4019
|
-
(method as any).executeProtocolV2Update = jest.fn().mockImplementation(() => {
|
|
4020
|
-
order.push('execute-update');
|
|
4021
|
-
return Promise.resolve();
|
|
4022
|
-
});
|
|
4023
|
-
(method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
|
|
4024
|
-
(method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
|
|
4025
|
-
bootloaderVersion: '1.0.0',
|
|
4026
|
-
bleVersion: '1.0.0',
|
|
4027
|
-
firmwareVersion: '1.0.0',
|
|
4028
|
-
});
|
|
4061
|
+
(method as any).enterProtocolV2BootloaderMode = jest.fn();
|
|
4029
4062
|
method.postTipMessage = jest.fn();
|
|
4030
4063
|
|
|
4031
|
-
await method.run()
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
]);
|
|
4064
|
+
await expect(method.run()).rejects.toMatchObject({
|
|
4065
|
+
errorCode: HardwareErrorCode.RuntimeError,
|
|
4066
|
+
params: expect.objectContaining({
|
|
4067
|
+
firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
|
|
4068
|
+
}),
|
|
4069
|
+
});
|
|
4070
|
+
expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
|
|
4039
4071
|
});
|
|
4040
4072
|
|
|
4041
4073
|
test('reboots Protocol V2 normal-mode device to bootloader before transfer', async () => {
|
|
@@ -4204,9 +4236,6 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4204
4236
|
},
|
|
4205
4237
|
});
|
|
4206
4238
|
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
4207
|
-
const prepareResourceBundles = jest
|
|
4208
|
-
.spyOn(method as any, 'prepareProtocolV2ResourceBundles')
|
|
4209
|
-
.mockResolvedValue(undefined);
|
|
4210
4239
|
(method as any).protocolV2Reboot = jest.fn();
|
|
4211
4240
|
(method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
|
|
4212
4241
|
(method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
|
|
@@ -4219,7 +4248,6 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4219
4248
|
|
|
4220
4249
|
await method.run();
|
|
4221
4250
|
|
|
4222
|
-
expect(prepareResourceBundles).not.toHaveBeenCalled();
|
|
4223
4251
|
expect((method as any).protocolV2Reboot).not.toHaveBeenCalled();
|
|
4224
4252
|
expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
|
|
4225
4253
|
bootloaderBinary: null,
|
|
@@ -5478,6 +5506,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5478
5506
|
return Number(params.processedSize ?? 0) + Number(params.source.size);
|
|
5479
5507
|
});
|
|
5480
5508
|
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
|
|
5509
|
+
(method as any).ensureProtocolV2BootResourceStagingIsEmpty = jest
|
|
5510
|
+
.fn()
|
|
5511
|
+
.mockResolvedValue(undefined);
|
|
5481
5512
|
(method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
|
|
5482
5513
|
(method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({});
|
|
5483
5514
|
(method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
|
|
@@ -5551,6 +5582,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5551
5582
|
Promise.resolve(Number(params.processedSize ?? 0) + Number(params.source.size))
|
|
5552
5583
|
);
|
|
5553
5584
|
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
|
|
5585
|
+
(method as any).ensureProtocolV2BootResourceStagingIsEmpty = jest
|
|
5586
|
+
.fn()
|
|
5587
|
+
.mockResolvedValue(undefined);
|
|
5554
5588
|
(method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
|
|
5555
5589
|
(method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
|
|
5556
5590
|
(method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
|
|
@@ -5558,21 +5592,33 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5558
5592
|
(method as any).waitForProtocolV2FirmwareUpdateComplete = jest
|
|
5559
5593
|
.fn()
|
|
5560
5594
|
.mockResolvedValue(undefined);
|
|
5595
|
+
(method as any).isProtocolV2ResourceBundleUpToDate = jest.fn().mockResolvedValue(false);
|
|
5561
5596
|
|
|
5562
|
-
await (method as any).
|
|
5563
|
-
|
|
5597
|
+
await (method as any).executeProtocolV2SourceUpdate({
|
|
5598
|
+
resourceSources: [
|
|
5564
5599
|
{
|
|
5565
5600
|
name: 'images.okpkg',
|
|
5566
|
-
|
|
5601
|
+
source: {
|
|
5602
|
+
size: 2,
|
|
5603
|
+
readAt: jest.fn(),
|
|
5604
|
+
close: jest.fn(),
|
|
5605
|
+
},
|
|
5567
5606
|
devicePath: 'vol0:/bundles/images/images.okpkg',
|
|
5607
|
+
version: [1, 2, 3],
|
|
5608
|
+
payloadHash: '11'.repeat(64),
|
|
5609
|
+
headerHash: '22'.repeat(64),
|
|
5568
5610
|
},
|
|
5569
5611
|
],
|
|
5570
|
-
|
|
5571
|
-
fwBinaryMap: [
|
|
5612
|
+
installSources: [
|
|
5572
5613
|
{
|
|
5573
5614
|
fileName: 'application_p1.bin',
|
|
5574
|
-
|
|
5615
|
+
source: {
|
|
5616
|
+
size: 1,
|
|
5617
|
+
readAt: jest.fn(),
|
|
5618
|
+
close: jest.fn(),
|
|
5619
|
+
},
|
|
5575
5620
|
targetId: 4,
|
|
5621
|
+
kind: 'firmware',
|
|
5576
5622
|
},
|
|
5577
5623
|
],
|
|
5578
5624
|
});
|
|
@@ -5589,6 +5635,100 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5589
5635
|
2,
|
|
5590
5636
|
expect.objectContaining({ processedSize: 0, totalSize: 1 })
|
|
5591
5637
|
);
|
|
5638
|
+
expect((method as any).isProtocolV2ResourceBundleUpToDate).toHaveBeenCalledWith(
|
|
5639
|
+
expect.objectContaining({
|
|
5640
|
+
version: [1, 2, 3],
|
|
5641
|
+
payloadHash: '11'.repeat(64),
|
|
5642
|
+
headerHash: '22'.repeat(64),
|
|
5643
|
+
})
|
|
5644
|
+
);
|
|
5645
|
+
});
|
|
5646
|
+
|
|
5647
|
+
test('installs and verifies bootloader before syncing manifest resources', () => {
|
|
5648
|
+
const method = new FirmwareUpdateV4({
|
|
5649
|
+
id: 1,
|
|
5650
|
+
payload: { method: 'firmwareUpdateV4' },
|
|
5651
|
+
});
|
|
5652
|
+
const phases = (method as any).buildProtocolV2ExecutionPhases({
|
|
5653
|
+
installSources: [{ kind: 'bootloader' }, { kind: 'component' }],
|
|
5654
|
+
resourceSources: [{ kind: 'resource' }],
|
|
5655
|
+
});
|
|
5656
|
+
|
|
5657
|
+
expect(phases.map((phase: { kind: string }) => phase.kind)).toEqual([
|
|
5658
|
+
'bootloader-install',
|
|
5659
|
+
'bootloader-verify',
|
|
5660
|
+
'resource-sync',
|
|
5661
|
+
'component-install',
|
|
5662
|
+
'final-verify',
|
|
5663
|
+
]);
|
|
5664
|
+
});
|
|
5665
|
+
|
|
5666
|
+
test('stages the mounted boot resource before any firmware install phase', () => {
|
|
5667
|
+
const method = new FirmwareUpdateV4({
|
|
5668
|
+
id: 1,
|
|
5669
|
+
payload: { method: 'firmwareUpdateV4' },
|
|
5670
|
+
});
|
|
5671
|
+
const bootResource = {
|
|
5672
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
5673
|
+
};
|
|
5674
|
+
const phases = (method as any).buildProtocolV2ExecutionPhases({
|
|
5675
|
+
installSources: [{ kind: 'bootloader' }, { kind: 'component' }],
|
|
5676
|
+
resourceSources: [bootResource, { devicePath: 'vol0:/resource/images/images.okpkg' }],
|
|
5677
|
+
});
|
|
5678
|
+
|
|
5679
|
+
expect(phases.map((phase: { kind: string }) => phase.kind)).toEqual([
|
|
5680
|
+
'resource-sync',
|
|
5681
|
+
'bootloader-install',
|
|
5682
|
+
'bootloader-verify',
|
|
5683
|
+
'resource-sync',
|
|
5684
|
+
'component-install',
|
|
5685
|
+
'final-verify',
|
|
5686
|
+
]);
|
|
5687
|
+
expect(phases[0].resourceSources).toEqual([bootResource]);
|
|
5688
|
+
});
|
|
5689
|
+
|
|
5690
|
+
test('removes stale boot resource staging before a component-only reboot', async () => {
|
|
5691
|
+
const method = new FirmwareUpdateV4({
|
|
5692
|
+
id: 1,
|
|
5693
|
+
payload: { method: 'firmwareUpdateV4' },
|
|
5694
|
+
});
|
|
5695
|
+
const events: string[] = [];
|
|
5696
|
+
let stagingExists = true;
|
|
5697
|
+
const typedCall = jest.fn((requestType: string) => {
|
|
5698
|
+
if (requestType === 'FilesystemPathInfoQuery') {
|
|
5699
|
+
return Promise.resolve({
|
|
5700
|
+
type: 'FilesystemPathInfo',
|
|
5701
|
+
message: { exist: stagingExists, directory: false, size: stagingExists ? 3 : 0 },
|
|
5702
|
+
});
|
|
5703
|
+
}
|
|
5704
|
+
if (requestType === 'FilesystemFileDelete') {
|
|
5705
|
+
events.push('delete-stale-staging');
|
|
5706
|
+
stagingExists = false;
|
|
5707
|
+
return Promise.resolve({ type: 'Success', message: {} });
|
|
5708
|
+
}
|
|
5709
|
+
throw new Error(`Unexpected request: ${requestType}`);
|
|
5710
|
+
});
|
|
5711
|
+
(method as any).device = stubDevice({ getCommands: () => ({ typedCall }) });
|
|
5712
|
+
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
|
|
5713
|
+
(method as any).executeProtocolV2TransferPhase = jest.fn().mockImplementation(() => {
|
|
5714
|
+
events.push('transfer-component');
|
|
5715
|
+
return Promise.resolve();
|
|
5716
|
+
});
|
|
5717
|
+
(method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockImplementation(() => {
|
|
5718
|
+
events.push('reboot-normal');
|
|
5719
|
+
return Promise.resolve();
|
|
5720
|
+
});
|
|
5721
|
+
(method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
|
|
5722
|
+
|
|
5723
|
+
await (method as any).executeProtocolV2SourceUpdate({
|
|
5724
|
+
installSources: [{ kind: 'firmware' }],
|
|
5725
|
+
resourceSources: [],
|
|
5726
|
+
});
|
|
5727
|
+
|
|
5728
|
+
expect(events).toEqual(['delete-stale-staging', 'transfer-component', 'reboot-normal']);
|
|
5729
|
+
expect(typedCall).toHaveBeenNthCalledWith(2, 'FilesystemFileDelete', 'Success', {
|
|
5730
|
+
path: 'vol0:/loaders/bootloader/boot_resource.okpkg.staging',
|
|
5731
|
+
});
|
|
5592
5732
|
});
|
|
5593
5733
|
|
|
5594
5734
|
test('does not request installation when the staged file size does not match', async () => {
|
|
@@ -5603,6 +5743,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5603
5743
|
method.postProgressMessage = jest.fn();
|
|
5604
5744
|
(method as any).protocolV2SourceUpdateProcess = jest.fn().mockResolvedValue(3);
|
|
5605
5745
|
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
|
|
5746
|
+
(method as any).ensureProtocolV2BootResourceStagingIsEmpty = jest
|
|
5747
|
+
.fn()
|
|
5748
|
+
.mockResolvedValue(undefined);
|
|
5606
5749
|
(method as any).device = stubDevice({
|
|
5607
5750
|
getCommands: () => ({
|
|
5608
5751
|
typedCall: jest.fn().mockResolvedValue({
|
|
@@ -5654,6 +5797,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5654
5797
|
Promise.resolve(Number(params.processedSize ?? 0) + Number(params.source.size))
|
|
5655
5798
|
);
|
|
5656
5799
|
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
|
|
5800
|
+
(method as any).ensureProtocolV2BootResourceStagingIsEmpty = jest
|
|
5801
|
+
.fn()
|
|
5802
|
+
.mockResolvedValue(undefined);
|
|
5657
5803
|
(method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
|
|
5658
5804
|
(method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
|
|
5659
5805
|
(method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
|
|
@@ -5733,6 +5879,14 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5733
5879
|
binary: binaries.get(url) ?? new Uint8Array([0]).buffer,
|
|
5734
5880
|
})
|
|
5735
5881
|
);
|
|
5882
|
+
const componentIntegrity = (url: string) => {
|
|
5883
|
+
const binary = binaries.get(url);
|
|
5884
|
+
if (!binary) throw new Error(`Missing test firmware binary: ${url}`);
|
|
5885
|
+
return {
|
|
5886
|
+
expectedSize: binary.byteLength,
|
|
5887
|
+
fingerprint: bytesToHex(sha256(new Uint8Array(binary))),
|
|
5888
|
+
};
|
|
5889
|
+
};
|
|
5736
5890
|
const getFirmwareLatestReleaseSpy = jest
|
|
5737
5891
|
.spyOn(DataManager, 'getFirmwareLatestRelease')
|
|
5738
5892
|
.mockReturnValue({
|
|
@@ -5757,23 +5911,43 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5757
5911
|
bootloader: {
|
|
5758
5912
|
target: 'BOOTLOADER',
|
|
5759
5913
|
url: 'https://example.com/bootloader.pp.bin',
|
|
5914
|
+
...componentIntegrity('https://example.com/bootloader.pp.bin'),
|
|
5760
5915
|
},
|
|
5761
5916
|
applicationP1: {
|
|
5762
5917
|
target: 'APPLICATION_P1',
|
|
5763
5918
|
url: 'https://example.com/applicationP1.pp.bin',
|
|
5919
|
+
...componentIntegrity('https://example.com/applicationP1.pp.bin'),
|
|
5764
5920
|
},
|
|
5765
5921
|
applicationP2: {
|
|
5766
5922
|
target: 'APPLICATION_P2',
|
|
5767
5923
|
url: 'https://example.com/applicationP2.pp.bin',
|
|
5924
|
+
...componentIntegrity('https://example.com/applicationP2.pp.bin'),
|
|
5768
5925
|
},
|
|
5769
5926
|
coprocessor: {
|
|
5770
5927
|
target: 'COPROCESSOR',
|
|
5771
5928
|
url: 'https://example.com/coprocessor.pp.bin',
|
|
5929
|
+
...componentIntegrity('https://example.com/coprocessor.pp.bin'),
|
|
5930
|
+
},
|
|
5931
|
+
se01: {
|
|
5932
|
+
target: 'SE01',
|
|
5933
|
+
url: 'https://example.com/se01.pp.bin',
|
|
5934
|
+
...componentIntegrity('https://example.com/se01.pp.bin'),
|
|
5935
|
+
},
|
|
5936
|
+
se02: {
|
|
5937
|
+
target: 'SE02',
|
|
5938
|
+
url: 'https://example.com/se02.pp.bin',
|
|
5939
|
+
...componentIntegrity('https://example.com/se02.pp.bin'),
|
|
5940
|
+
},
|
|
5941
|
+
se03: {
|
|
5942
|
+
target: 'SE03',
|
|
5943
|
+
url: 'https://example.com/se03.pp.bin',
|
|
5944
|
+
...componentIntegrity('https://example.com/se03.pp.bin'),
|
|
5945
|
+
},
|
|
5946
|
+
se04: {
|
|
5947
|
+
target: 'SE04',
|
|
5948
|
+
url: 'https://example.com/se04.pp.bin',
|
|
5949
|
+
...componentIntegrity('https://example.com/se04.pp.bin'),
|
|
5772
5950
|
},
|
|
5773
|
-
se01: { target: 'SE01', url: 'https://example.com/se01.pp.bin' },
|
|
5774
|
-
se02: { target: 'SE02', url: 'https://example.com/se02.pp.bin' },
|
|
5775
|
-
se03: { target: 'SE03', url: 'https://example.com/se03.pp.bin' },
|
|
5776
|
-
se04: { target: 'SE04', url: 'https://example.com/se04.pp.bin' },
|
|
5777
5951
|
},
|
|
5778
5952
|
fingerprint: '',
|
|
5779
5953
|
changelog: {
|
|
@@ -5814,6 +5988,84 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5814
5988
|
getFirmwareLatestReleaseSpy.mockRestore();
|
|
5815
5989
|
});
|
|
5816
5990
|
|
|
5991
|
+
test('downloads only firmware targets that are missing from an explicit mixed payload', async () => {
|
|
5992
|
+
const explicitApplicationBinary = new Uint8Array([1]).buffer;
|
|
5993
|
+
const remoteCoprocessorBinary = new Uint8Array([2]).buffer;
|
|
5994
|
+
const method = new FirmwareUpdateV4({
|
|
5995
|
+
id: 1,
|
|
5996
|
+
payload: {
|
|
5997
|
+
method: 'firmwareUpdateV4',
|
|
5998
|
+
platform: 'web',
|
|
5999
|
+
targetsToUpdate: ['app_v1', 'coprocessor'],
|
|
6000
|
+
},
|
|
6001
|
+
});
|
|
6002
|
+
method.init();
|
|
6003
|
+
|
|
6004
|
+
const getSysResourceBinarySpy = jest
|
|
6005
|
+
.spyOn(firmwareBinaryApi, 'getSysResourceBinary')
|
|
6006
|
+
.mockResolvedValue({ binary: remoteCoprocessorBinary });
|
|
6007
|
+
const getFirmwareLatestReleaseSpy = jest
|
|
6008
|
+
.spyOn(DataManager, 'getFirmwareLatestRelease')
|
|
6009
|
+
.mockReturnValue({
|
|
6010
|
+
required: false,
|
|
6011
|
+
version: [1, 0, 0],
|
|
6012
|
+
url: 'https://example.com/applicationP1.pp.bin',
|
|
6013
|
+
installOrder: ['applicationP1', 'coprocessor'],
|
|
6014
|
+
components: {
|
|
6015
|
+
applicationP1: {
|
|
6016
|
+
target: 'APPLICATION_P1',
|
|
6017
|
+
url: 'https://example.com/applicationP1.pp.bin',
|
|
6018
|
+
expectedSize: explicitApplicationBinary.byteLength,
|
|
6019
|
+
fingerprint: bytesToHex(sha256(new Uint8Array(explicitApplicationBinary))),
|
|
6020
|
+
},
|
|
6021
|
+
coprocessor: {
|
|
6022
|
+
target: 'COPROCESSOR',
|
|
6023
|
+
url: 'https://example.com/coprocessor.pp.bin',
|
|
6024
|
+
expectedSize: remoteCoprocessorBinary.byteLength,
|
|
6025
|
+
fingerprint: bytesToHex(sha256(new Uint8Array(remoteCoprocessorBinary))),
|
|
6026
|
+
},
|
|
6027
|
+
},
|
|
6028
|
+
fingerprint: '',
|
|
6029
|
+
changelog: {
|
|
6030
|
+
'zh-CN': '',
|
|
6031
|
+
'en-US': '',
|
|
6032
|
+
},
|
|
6033
|
+
});
|
|
6034
|
+
|
|
6035
|
+
const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries(
|
|
6036
|
+
'universal',
|
|
6037
|
+
{ deviceType: 'pro2', firmwareVersion: '0.0.0' },
|
|
6038
|
+
[
|
|
6039
|
+
{
|
|
6040
|
+
fileName: 'application_p1.bin',
|
|
6041
|
+
binary: explicitApplicationBinary,
|
|
6042
|
+
targetId: 4,
|
|
6043
|
+
kind: 'firmware',
|
|
6044
|
+
},
|
|
6045
|
+
]
|
|
6046
|
+
);
|
|
6047
|
+
|
|
6048
|
+
expect(getSysResourceBinarySpy).toHaveBeenCalledTimes(1);
|
|
6049
|
+
expect(getSysResourceBinarySpy).toHaveBeenCalledWith('https://example.com/coprocessor.pp.bin');
|
|
6050
|
+
expect(remoteBinaries.installItems).toEqual([
|
|
6051
|
+
{
|
|
6052
|
+
fileName: 'application_p1.bin',
|
|
6053
|
+
binary: explicitApplicationBinary,
|
|
6054
|
+
targetId: 4,
|
|
6055
|
+
kind: 'firmware',
|
|
6056
|
+
},
|
|
6057
|
+
{
|
|
6058
|
+
fileName: 'coprocessor.bin',
|
|
6059
|
+
binary: remoteCoprocessorBinary,
|
|
6060
|
+
targetId: 6,
|
|
6061
|
+
kind: 'firmware',
|
|
6062
|
+
},
|
|
6063
|
+
]);
|
|
6064
|
+
|
|
6065
|
+
getSysResourceBinarySpy.mockRestore();
|
|
6066
|
+
getFirmwareLatestReleaseSpy.mockRestore();
|
|
6067
|
+
});
|
|
6068
|
+
|
|
5817
6069
|
test('ignores an unsupported remote ROMLOADER component when installing selected targets', async () => {
|
|
5818
6070
|
const method = new FirmwareUpdateV4({
|
|
5819
6071
|
id: 1,
|
|
@@ -5978,211 +6230,759 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5978
6230
|
});
|
|
5979
6231
|
});
|
|
5980
6232
|
|
|
5981
|
-
test('
|
|
6233
|
+
test('maps a prepared resource archive manifest to artifact-backed resource sources', async () => {
|
|
5982
6234
|
const method = new FirmwareUpdateV4({
|
|
5983
6235
|
id: 1,
|
|
5984
6236
|
payload: {
|
|
5985
6237
|
method: 'firmwareUpdateV4',
|
|
5986
6238
|
},
|
|
5987
6239
|
});
|
|
5988
|
-
const
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
6240
|
+
const imagesBinary = createProtocolV2OkppBinary();
|
|
6241
|
+
const bootBinary = createProtocolV2OkppBinary({ payloadHashByte: 0x33 });
|
|
6242
|
+
const imagesSha256 = bytesToHex(sha256(new Uint8Array(imagesBinary)));
|
|
6243
|
+
const bootSha256 = bytesToHex(sha256(new Uint8Array(bootBinary)));
|
|
6244
|
+
const manifest = {
|
|
6245
|
+
schema: 1,
|
|
6246
|
+
artifact_name: 'pro2-resource',
|
|
6247
|
+
release_name: 'test-release',
|
|
6248
|
+
variant: 'resource',
|
|
6249
|
+
commit: 'abc123',
|
|
6250
|
+
short_sha: 'abc123',
|
|
6251
|
+
timestamp_utc: '2026-08-09T00:00:00Z',
|
|
6252
|
+
core_version: '1.0.0',
|
|
6253
|
+
key_set: 'production',
|
|
6254
|
+
device_root: 'vol0:',
|
|
6255
|
+
restore_mode: 'bootloader_update',
|
|
6256
|
+
trees: [{ path: 'bundles', device: 'pro2' }],
|
|
6257
|
+
files: [
|
|
5992
6258
|
{
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
6259
|
+
archive_path: 'bundles/images/images.okpkg',
|
|
6260
|
+
original_name: 'images.okpkg',
|
|
6261
|
+
device_path: 'vol0:/bundles/images/images.okpkg',
|
|
6262
|
+
size: imagesBinary.byteLength,
|
|
6263
|
+
sha256: imagesSha256,
|
|
6264
|
+
signed: true,
|
|
6265
|
+
sig_algo: 'ed25519',
|
|
6266
|
+
payload_version: '1.2.3',
|
|
6267
|
+
},
|
|
6268
|
+
{
|
|
6269
|
+
archive_path: 'loaders/bootloader/boot_resource.okpkg',
|
|
6270
|
+
original_name: 'boot_resource.okpkg',
|
|
6271
|
+
device_path: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
6272
|
+
size: bootBinary.byteLength,
|
|
6273
|
+
sha256: bootSha256,
|
|
6274
|
+
signed: true,
|
|
6275
|
+
sig_algo: 'ed25519',
|
|
6276
|
+
payload_version: '1.2.3',
|
|
5999
6277
|
},
|
|
6000
6278
|
],
|
|
6001
6279
|
};
|
|
6002
|
-
|
|
6003
|
-
const
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6280
|
+
const manifestBinary = new TextEncoder().encode(JSON.stringify(manifest)).buffer;
|
|
6281
|
+
const zip = new JSZip();
|
|
6282
|
+
zip.file('manifest.json', manifestBinary);
|
|
6283
|
+
zip.file('bundles/images/images.okpkg', imagesBinary);
|
|
6284
|
+
zip.file('loaders/bootloader/boot_resource.okpkg', bootBinary);
|
|
6285
|
+
const archiveBinary = await zip.generateAsync({ type: 'arraybuffer' });
|
|
6286
|
+
const preparedEntries = [
|
|
6287
|
+
{
|
|
6288
|
+
entryName: 'manifest.json',
|
|
6289
|
+
artifact: {
|
|
6290
|
+
artifactRef: 'manifest',
|
|
6291
|
+
size: manifestBinary.byteLength,
|
|
6292
|
+
sha256: bytesToHex(sha256(new Uint8Array(manifestBinary))),
|
|
6293
|
+
},
|
|
6294
|
+
},
|
|
6295
|
+
{
|
|
6296
|
+
entryName: 'bundles/images/images.okpkg',
|
|
6297
|
+
artifact: {
|
|
6298
|
+
artifactRef: 'images',
|
|
6299
|
+
size: imagesBinary.byteLength,
|
|
6300
|
+
sha256: imagesSha256,
|
|
6301
|
+
},
|
|
6302
|
+
},
|
|
6303
|
+
{
|
|
6304
|
+
entryName: 'loaders/bootloader/boot_resource.okpkg',
|
|
6305
|
+
artifact: {
|
|
6306
|
+
artifactRef: 'boot',
|
|
6307
|
+
size: bootBinary.byteLength,
|
|
6308
|
+
sha256: bootSha256,
|
|
6309
|
+
},
|
|
6310
|
+
},
|
|
6311
|
+
];
|
|
6312
|
+
const artifacts = new Map([['archive', archiveBinary]]);
|
|
6313
|
+
(method as any).params = {
|
|
6314
|
+
targetsToUpdate: ['resource'],
|
|
6315
|
+
preparedPlan: {
|
|
6316
|
+
artifacts: [
|
|
6010
6317
|
{
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6318
|
+
artifactId: 'resource:archive',
|
|
6319
|
+
role: 'resourceBundle',
|
|
6320
|
+
target: 'resource',
|
|
6321
|
+
container: 'zip',
|
|
6322
|
+
artifact: {
|
|
6323
|
+
artifactRef: 'archive',
|
|
6324
|
+
size: archiveBinary.byteLength,
|
|
6325
|
+
sha256: bytesToHex(sha256(new Uint8Array(archiveBinary))),
|
|
6326
|
+
},
|
|
6327
|
+
materializedEntries: preparedEntries,
|
|
6014
6328
|
},
|
|
6015
6329
|
],
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
});
|
|
6330
|
+
},
|
|
6331
|
+
};
|
|
6332
|
+
(method as any).openProtocolV2PreparedSource = jest.fn(async artifact => {
|
|
6333
|
+
const binary = artifacts.get(artifact.artifactRef);
|
|
6334
|
+
if (!binary) throw new Error(`Unknown artifact: ${artifact.artifactRef}`);
|
|
6335
|
+
return openFirmwareByteSource({ binary });
|
|
6336
|
+
});
|
|
6022
6337
|
|
|
6023
|
-
await
|
|
6024
|
-
(method as any).prepareProtocolV2ResourceSources('universal', {
|
|
6025
|
-
deviceType: 'pro2',
|
|
6026
|
-
})
|
|
6027
|
-
).rejects.toThrow('resourceBundles[].devicePath');
|
|
6028
|
-
expect(openPreparedSource).not.toHaveBeenCalled();
|
|
6338
|
+
const sources = await (method as any).prepareProtocolV2ResourceArchiveSources();
|
|
6029
6339
|
|
|
6030
|
-
|
|
6340
|
+
expect(sources).toHaveLength(2);
|
|
6341
|
+
expect(sources).toEqual([
|
|
6342
|
+
expect.objectContaining({
|
|
6343
|
+
name: 'images.okpkg',
|
|
6344
|
+
devicePath: 'vol0:/bundles/images/images.okpkg',
|
|
6345
|
+
version: [1, 2, 3],
|
|
6346
|
+
}),
|
|
6347
|
+
expect.objectContaining({
|
|
6348
|
+
name: 'boot_resource.okpkg',
|
|
6349
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
6350
|
+
version: [1, 2, 3],
|
|
6351
|
+
}),
|
|
6352
|
+
]);
|
|
6353
|
+
await Promise.all(sources.map(source => source.source.close()));
|
|
6354
|
+
|
|
6355
|
+
preparedEntries[1].artifact.sha256 = 'f'.repeat(64);
|
|
6356
|
+
await expect((method as any).prepareProtocolV2ResourceArchiveSources()).rejects.toThrow(
|
|
6357
|
+
'entries do not match the approved archive'
|
|
6358
|
+
);
|
|
6031
6359
|
});
|
|
6032
6360
|
|
|
6033
|
-
test('
|
|
6034
|
-
const
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6361
|
+
test('binds a prepared resource archive to the selected host generation', () => {
|
|
6362
|
+
const planWithoutDigest = {
|
|
6363
|
+
schemaVersion: 2 as const,
|
|
6364
|
+
executor: 'v4' as const,
|
|
6365
|
+
deviceIdentity: 'PRO2-SERIAL',
|
|
6366
|
+
deviceModel: 'pro2',
|
|
6367
|
+
firmwareType: EFirmwareType.Universal,
|
|
6368
|
+
platform: 'web' as const,
|
|
6369
|
+
targetsToUpdate: ['resource' as const],
|
|
6370
|
+
artifacts: [
|
|
6371
|
+
{
|
|
6372
|
+
artifactId: 'resource:archive',
|
|
6373
|
+
role: 'resourceBundle' as const,
|
|
6374
|
+
target: 'resource' as const,
|
|
6375
|
+
url: 'https://example.com/resource.zip',
|
|
6376
|
+
container: 'zip' as const,
|
|
6377
|
+
logicalName: 'protocol-v2-resource-archive',
|
|
6378
|
+
expectedSize: 3,
|
|
6379
|
+
expectedSha256: 'a'.repeat(64),
|
|
6380
|
+
},
|
|
6381
|
+
],
|
|
6382
|
+
};
|
|
6383
|
+
const plan = {
|
|
6384
|
+
...planWithoutDigest,
|
|
6385
|
+
planDigest: digestFirmwareUpdateContract(planWithoutDigest),
|
|
6386
|
+
};
|
|
6387
|
+
const preparedPlan = prepareFirmwareUpdatePlan({
|
|
6388
|
+
plan,
|
|
6389
|
+
leaseRef: 'resource-archive-test',
|
|
6390
|
+
artifacts: [
|
|
6391
|
+
{
|
|
6392
|
+
artifactId: 'resource:archive',
|
|
6393
|
+
artifact: {
|
|
6394
|
+
artifactRef: 'resource-archive',
|
|
6395
|
+
size: 3,
|
|
6396
|
+
sha256: 'a'.repeat(64),
|
|
6397
|
+
},
|
|
6398
|
+
materializedEntries: [
|
|
6399
|
+
{
|
|
6400
|
+
entryName: 'manifest.json',
|
|
6401
|
+
artifact: {
|
|
6402
|
+
artifactRef: 'resource-manifest',
|
|
6403
|
+
size: 2,
|
|
6404
|
+
sha256: 'b'.repeat(64),
|
|
6405
|
+
},
|
|
6406
|
+
},
|
|
6407
|
+
],
|
|
6408
|
+
},
|
|
6409
|
+
],
|
|
6410
|
+
});
|
|
6411
|
+
const mismatchedHostBindingGeneration = registerFirmwareUpdateHostBinding({
|
|
6412
|
+
preparedPlanDigest: 'c'.repeat(64),
|
|
6413
|
+
artifactReader: {
|
|
6414
|
+
open: jest.fn(),
|
|
6415
|
+
read: jest.fn(),
|
|
6416
|
+
close: jest.fn(),
|
|
6039
6417
|
},
|
|
6040
6418
|
});
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
method.postTipMessage = jest.fn();
|
|
6419
|
+
try {
|
|
6420
|
+
const mismatchedMethod = new FirmwareUpdateV4({
|
|
6421
|
+
id: 1,
|
|
6422
|
+
payload: {
|
|
6423
|
+
method: 'firmwareUpdateV4',
|
|
6424
|
+
platform: 'web',
|
|
6425
|
+
targetsToUpdate: ['resource'],
|
|
6426
|
+
preparedPlan,
|
|
6427
|
+
hostBindingGeneration: mismatchedHostBindingGeneration,
|
|
6428
|
+
},
|
|
6429
|
+
});
|
|
6053
6430
|
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6431
|
+
expect(() => mismatchedMethod.init()).toThrow('does not match the prepared plan');
|
|
6432
|
+
} finally {
|
|
6433
|
+
unregisterFirmwareUpdateHostBinding(mismatchedHostBindingGeneration);
|
|
6434
|
+
}
|
|
6435
|
+
const hostBindingGeneration = registerFirmwareUpdateHostBinding({
|
|
6436
|
+
preparedPlanDigest: preparedPlan.preparedPlanDigest,
|
|
6437
|
+
artifactReader: {
|
|
6438
|
+
open: jest.fn(),
|
|
6439
|
+
read: jest.fn(),
|
|
6440
|
+
close: jest.fn(),
|
|
6441
|
+
},
|
|
6057
6442
|
});
|
|
6443
|
+
try {
|
|
6444
|
+
const method = new FirmwareUpdateV4({
|
|
6445
|
+
id: 1,
|
|
6446
|
+
payload: {
|
|
6447
|
+
method: 'firmwareUpdateV4',
|
|
6448
|
+
platform: 'web',
|
|
6449
|
+
targetsToUpdate: ['resource'],
|
|
6450
|
+
preparedPlan,
|
|
6451
|
+
hostBindingGeneration,
|
|
6452
|
+
},
|
|
6453
|
+
});
|
|
6058
6454
|
|
|
6059
|
-
|
|
6060
|
-
|
|
6455
|
+
expect(() => method.init()).not.toThrow();
|
|
6456
|
+
} finally {
|
|
6457
|
+
unregisterFirmwareUpdateHostBinding(hostBindingGeneration);
|
|
6458
|
+
}
|
|
6061
6459
|
});
|
|
6062
6460
|
|
|
6063
|
-
test('
|
|
6064
|
-
const
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6461
|
+
test('executes a prepared component plan without duplicated component inputs', async () => {
|
|
6462
|
+
const componentBinary = new Uint8Array([1, 2, 3]).buffer;
|
|
6463
|
+
const componentArtifact = {
|
|
6464
|
+
artifactRef: 'prepared-bootloader',
|
|
6465
|
+
size: componentBinary.byteLength,
|
|
6466
|
+
sha256: 'a'.repeat(64),
|
|
6467
|
+
};
|
|
6468
|
+
const planWithoutDigest = {
|
|
6469
|
+
schemaVersion: 2 as const,
|
|
6470
|
+
executor: 'v4' as const,
|
|
6471
|
+
deviceIdentity: 'PRO2-SERIAL',
|
|
6472
|
+
deviceModel: 'pro2',
|
|
6473
|
+
firmwareType: EFirmwareType.Universal,
|
|
6474
|
+
platform: 'web' as const,
|
|
6475
|
+
targetsToUpdate: ['boot' as const],
|
|
6476
|
+
artifacts: [
|
|
6477
|
+
{
|
|
6478
|
+
artifactId: 'component:boot',
|
|
6479
|
+
role: 'component' as const,
|
|
6480
|
+
target: 'boot' as const,
|
|
6481
|
+
url: 'https://example.com/bootloader.bin',
|
|
6482
|
+
container: 'raw' as const,
|
|
6483
|
+
logicalName: 'bootloader',
|
|
6484
|
+
expectedSize: componentArtifact.size,
|
|
6485
|
+
expectedSha256: componentArtifact.sha256,
|
|
6486
|
+
targetVersion: '1.2.3',
|
|
6487
|
+
},
|
|
6488
|
+
],
|
|
6489
|
+
};
|
|
6490
|
+
const preparedPlan = prepareFirmwareUpdatePlan({
|
|
6491
|
+
plan: {
|
|
6492
|
+
...planWithoutDigest,
|
|
6493
|
+
planDigest: digestFirmwareUpdateContract(planWithoutDigest),
|
|
6070
6494
|
},
|
|
6495
|
+
leaseRef: 'prepared-component-test',
|
|
6496
|
+
artifacts: [{ artifactId: 'component:boot', artifact: componentArtifact }],
|
|
6071
6497
|
});
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
expect((method as any).prepareRemoteProtocolV2Binaries).not.toHaveBeenCalled();
|
|
6093
|
-
expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
|
|
6094
|
-
bootloaderBinary: null,
|
|
6095
|
-
fwBinaryMap: [{ fileName: 'coprocessor.bin', binary: expect.anything(), targetId: 6 }],
|
|
6498
|
+
const artifactReader = {
|
|
6499
|
+
open: jest.fn(() =>
|
|
6500
|
+
Promise.resolve({
|
|
6501
|
+
readerId: 'prepared-component-reader',
|
|
6502
|
+
size: componentBinary.byteLength,
|
|
6503
|
+
})
|
|
6504
|
+
),
|
|
6505
|
+
read: jest.fn(({ offset, length }: { offset: number; length: number }) => {
|
|
6506
|
+
const data = componentBinary.slice(offset, offset + length);
|
|
6507
|
+
return Promise.resolve({
|
|
6508
|
+
data,
|
|
6509
|
+
bytesRead: data.byteLength,
|
|
6510
|
+
eof: offset + length === componentBinary.byteLength,
|
|
6511
|
+
});
|
|
6512
|
+
}),
|
|
6513
|
+
close: jest.fn(() => Promise.resolve()),
|
|
6514
|
+
};
|
|
6515
|
+
const hostBindingGeneration = registerFirmwareUpdateHostBinding({
|
|
6516
|
+
preparedPlanDigest: preparedPlan.preparedPlanDigest,
|
|
6517
|
+
artifactReader,
|
|
6096
6518
|
});
|
|
6097
|
-
|
|
6519
|
+
const releaseSpy = jest
|
|
6520
|
+
.spyOn(DataManager, 'getFirmwareLatestRelease')
|
|
6521
|
+
.mockReturnValue(undefined);
|
|
6098
6522
|
|
|
6099
|
-
|
|
6100
|
-
'treats manual resource files as authoritative payload for %s',
|
|
6101
|
-
async target => {
|
|
6102
|
-
const resourceBundle = new Uint8Array([1, 2, 3]).buffer;
|
|
6523
|
+
try {
|
|
6103
6524
|
const method = new FirmwareUpdateV4({
|
|
6104
6525
|
id: 1,
|
|
6105
6526
|
payload: {
|
|
6106
6527
|
method: 'firmwareUpdateV4',
|
|
6107
6528
|
platform: 'web',
|
|
6108
|
-
|
|
6109
|
-
|
|
6529
|
+
preparedPlan,
|
|
6530
|
+
hostBindingGeneration,
|
|
6531
|
+
},
|
|
6532
|
+
});
|
|
6533
|
+
|
|
6534
|
+
expect(() => method.init()).not.toThrow();
|
|
6535
|
+
expect((method as any).params.targetsToUpdate).toEqual(['boot']);
|
|
6536
|
+
expect((method as any).params.expectedTargetVersions).toEqual({ boot: '1.2.3' });
|
|
6537
|
+
expect((method as any).params.componentArtifacts).toBeUndefined();
|
|
6538
|
+
(method as any).postTipMessage = jest.fn();
|
|
6539
|
+
(method as any).prepareProtocolV2ResourceSources = jest.fn().mockResolvedValue([]);
|
|
6540
|
+
(method as any).executeProtocolV2SourceUpdate = jest
|
|
6541
|
+
.fn()
|
|
6542
|
+
.mockResolvedValue('prepared-result');
|
|
6543
|
+
|
|
6544
|
+
await expect(
|
|
6545
|
+
(method as any).runProtocolV2PreparedArtifacts({} as Features, EFirmwareType.Universal)
|
|
6546
|
+
).resolves.toBe('prepared-result');
|
|
6547
|
+
expect(releaseSpy).not.toHaveBeenCalled();
|
|
6548
|
+
expect(artifactReader.open).toHaveBeenCalledWith({ artifactRef: 'prepared-bootloader' });
|
|
6549
|
+
expect((method as any).executeProtocolV2SourceUpdate).toHaveBeenCalledWith({
|
|
6550
|
+
installSources: [
|
|
6551
|
+
expect.objectContaining({
|
|
6552
|
+
fileName: 'bootloader.bin',
|
|
6553
|
+
kind: 'bootloader',
|
|
6554
|
+
}),
|
|
6555
|
+
],
|
|
6556
|
+
resourceSources: [],
|
|
6557
|
+
});
|
|
6558
|
+
} finally {
|
|
6559
|
+
releaseSpy.mockRestore();
|
|
6560
|
+
unregisterFirmwareUpdateHostBinding(hostBindingGeneration);
|
|
6561
|
+
}
|
|
6562
|
+
});
|
|
6563
|
+
|
|
6564
|
+
test('executes a resource-only prepared plan without a live firmware release', async () => {
|
|
6565
|
+
const planWithoutDigest = {
|
|
6566
|
+
schemaVersion: 2 as const,
|
|
6567
|
+
executor: 'v4' as const,
|
|
6568
|
+
deviceIdentity: 'PRO2-SERIAL',
|
|
6569
|
+
deviceModel: 'pro2',
|
|
6570
|
+
firmwareType: EFirmwareType.Universal,
|
|
6571
|
+
platform: 'web' as const,
|
|
6572
|
+
targetsToUpdate: ['resource' as const],
|
|
6573
|
+
artifacts: [
|
|
6574
|
+
{
|
|
6575
|
+
artifactId: 'resource:archive',
|
|
6576
|
+
role: 'resourceBundle' as const,
|
|
6577
|
+
target: 'resource' as const,
|
|
6578
|
+
url: 'https://example.com/resource.zip',
|
|
6579
|
+
container: 'zip' as const,
|
|
6580
|
+
logicalName: 'protocol-v2-resource-archive',
|
|
6581
|
+
expectedSize: 3,
|
|
6582
|
+
expectedSha256: 'a'.repeat(64),
|
|
6583
|
+
},
|
|
6584
|
+
],
|
|
6585
|
+
};
|
|
6586
|
+
const preparedPlan = prepareFirmwareUpdatePlan({
|
|
6587
|
+
plan: {
|
|
6588
|
+
...planWithoutDigest,
|
|
6589
|
+
planDigest: digestFirmwareUpdateContract(planWithoutDigest),
|
|
6590
|
+
},
|
|
6591
|
+
leaseRef: 'prepared-resource-only-test',
|
|
6592
|
+
artifacts: [
|
|
6593
|
+
{
|
|
6594
|
+
artifactId: 'resource:archive',
|
|
6595
|
+
artifact: {
|
|
6596
|
+
artifactRef: 'prepared-resource-archive',
|
|
6597
|
+
size: 3,
|
|
6598
|
+
sha256: 'a'.repeat(64),
|
|
6599
|
+
},
|
|
6600
|
+
materializedEntries: [
|
|
6110
6601
|
{
|
|
6111
|
-
|
|
6112
|
-
|
|
6602
|
+
entryName: 'manifest.json',
|
|
6603
|
+
artifact: {
|
|
6604
|
+
artifactRef: 'prepared-resource-manifest',
|
|
6605
|
+
size: 2,
|
|
6606
|
+
sha256: 'b'.repeat(64),
|
|
6607
|
+
},
|
|
6113
6608
|
},
|
|
6114
6609
|
],
|
|
6115
6610
|
},
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6611
|
+
],
|
|
6612
|
+
});
|
|
6613
|
+
const hostBindingGeneration = registerFirmwareUpdateHostBinding({
|
|
6614
|
+
preparedPlanDigest: preparedPlan.preparedPlanDigest,
|
|
6615
|
+
artifactReader: {
|
|
6616
|
+
open: jest.fn(),
|
|
6617
|
+
read: jest.fn(),
|
|
6618
|
+
close: jest.fn(),
|
|
6619
|
+
},
|
|
6620
|
+
});
|
|
6621
|
+
const releaseSpy = jest
|
|
6622
|
+
.spyOn(DataManager, 'getFirmwareLatestRelease')
|
|
6623
|
+
.mockReturnValue(undefined);
|
|
6120
6624
|
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6625
|
+
try {
|
|
6626
|
+
const method = new FirmwareUpdateV4({
|
|
6627
|
+
id: 1,
|
|
6628
|
+
payload: {
|
|
6629
|
+
method: 'firmwareUpdateV4',
|
|
6630
|
+
platform: 'web',
|
|
6631
|
+
preparedPlan,
|
|
6632
|
+
hostBindingGeneration,
|
|
6633
|
+
},
|
|
6124
6634
|
});
|
|
6125
|
-
|
|
6126
|
-
(
|
|
6127
|
-
(method as any).
|
|
6128
|
-
|
|
6129
|
-
(method as any).
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6635
|
+
|
|
6636
|
+
expect(() => method.init()).not.toThrow();
|
|
6637
|
+
(method as any).postTipMessage = jest.fn();
|
|
6638
|
+
const resourceSources = [{ kind: 'resource' }];
|
|
6639
|
+
(method as any).prepareProtocolV2ResourceSources = jest
|
|
6640
|
+
.fn()
|
|
6641
|
+
.mockResolvedValue(resourceSources);
|
|
6642
|
+
(method as any).executeProtocolV2SourceUpdate = jest
|
|
6643
|
+
.fn()
|
|
6644
|
+
.mockResolvedValue('resource-result');
|
|
6645
|
+
|
|
6646
|
+
await expect(
|
|
6647
|
+
(method as any).runProtocolV2PreparedArtifacts({} as Features, EFirmwareType.Universal)
|
|
6648
|
+
).resolves.toBe('resource-result');
|
|
6649
|
+
expect(releaseSpy).not.toHaveBeenCalled();
|
|
6650
|
+
expect((method as any).executeProtocolV2SourceUpdate).toHaveBeenCalledWith({
|
|
6651
|
+
installSources: [],
|
|
6652
|
+
resourceSources,
|
|
6133
6653
|
});
|
|
6134
|
-
|
|
6654
|
+
} finally {
|
|
6655
|
+
releaseSpy.mockRestore();
|
|
6656
|
+
unregisterFirmwareUpdateHostBinding(hostBindingGeneration);
|
|
6657
|
+
}
|
|
6658
|
+
});
|
|
6135
6659
|
|
|
6136
|
-
|
|
6660
|
+
test('converts a local resource ZIP into a local PreparedPlan without matching a remote release', async () => {
|
|
6661
|
+
const imagesBinary = new Uint8Array([1, 2, 3]).buffer;
|
|
6662
|
+
const bootResourceBinary = new Uint8Array([4, 5, 6]).buffer;
|
|
6663
|
+
const manifest = {
|
|
6664
|
+
schema: 1,
|
|
6665
|
+
artifact_name: 'pro2-resource',
|
|
6666
|
+
release_name: 'local-development',
|
|
6667
|
+
variant: 'resource',
|
|
6668
|
+
commit: 'local',
|
|
6669
|
+
short_sha: 'local',
|
|
6670
|
+
timestamp_utc: '2026-08-09T00:00:00Z',
|
|
6671
|
+
core_version: '1.0.0',
|
|
6672
|
+
key_set: 'development',
|
|
6673
|
+
device_root: 'vol0:',
|
|
6674
|
+
restore_mode: 'bootloader_update',
|
|
6675
|
+
trees: [{ path: 'bundles', device: 'pro2' }],
|
|
6676
|
+
files: [
|
|
6677
|
+
{
|
|
6678
|
+
archive_path: 'bundles/images/images.okpkg',
|
|
6679
|
+
original_name: 'images.okpkg',
|
|
6680
|
+
device_path: 'vol0:/bundles/images/images.okpkg',
|
|
6681
|
+
size: imagesBinary.byteLength,
|
|
6682
|
+
sha256: bytesToHex(sha256(new Uint8Array(imagesBinary))),
|
|
6683
|
+
signed: true,
|
|
6684
|
+
sig_algo: 'ed25519',
|
|
6685
|
+
payload_version: '1.0.0',
|
|
6686
|
+
},
|
|
6687
|
+
{
|
|
6688
|
+
archive_path: 'loaders/bootloader/boot_resource.okpkg',
|
|
6689
|
+
original_name: 'boot_resource.okpkg',
|
|
6690
|
+
device_path: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
6691
|
+
size: bootResourceBinary.byteLength,
|
|
6692
|
+
sha256: bytesToHex(sha256(new Uint8Array(bootResourceBinary))),
|
|
6693
|
+
signed: true,
|
|
6694
|
+
sig_algo: 'ed25519',
|
|
6695
|
+
payload_version: '1.0.0',
|
|
6696
|
+
},
|
|
6697
|
+
],
|
|
6698
|
+
};
|
|
6699
|
+
const zip = new JSZip();
|
|
6700
|
+
zip.file('manifest.json', JSON.stringify(manifest));
|
|
6701
|
+
zip.file('bundles/images/images.okpkg', imagesBinary);
|
|
6702
|
+
zip.file('loaders/bootloader/boot_resource.okpkg', bootResourceBinary);
|
|
6703
|
+
const resourceArchiveBinary = await zip.generateAsync({ type: 'arraybuffer' });
|
|
6704
|
+
const applicationP1Binary = new Uint8Array([7, 8, 9]).buffer;
|
|
6705
|
+
const method = new FirmwareUpdateV4({
|
|
6706
|
+
id: 1,
|
|
6707
|
+
payload: {
|
|
6708
|
+
method: 'firmwareUpdateV4',
|
|
6709
|
+
platform: 'web',
|
|
6710
|
+
targetsToUpdate: ['app_v1'],
|
|
6711
|
+
applicationP1Binary,
|
|
6712
|
+
resourceArchiveBinary,
|
|
6713
|
+
},
|
|
6714
|
+
});
|
|
6715
|
+
method.init();
|
|
6716
|
+
expect((method as any).params.targetsToUpdate).toEqual(['app_v1', 'resource']);
|
|
6717
|
+
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
6718
|
+
(method as any).postTipMessage = jest.fn();
|
|
6719
|
+
(method as any).device = stubDevice({
|
|
6720
|
+
originalDescriptor: { protocolType: 'V2' },
|
|
6721
|
+
features: {
|
|
6722
|
+
deviceType: 'pro2',
|
|
6723
|
+
serialNo: 'pro2-device-id',
|
|
6724
|
+
firmwareVersion: '1.0.0',
|
|
6725
|
+
capabilities: [],
|
|
6726
|
+
},
|
|
6727
|
+
getCurrentDeviceType: () => 'pro2',
|
|
6728
|
+
});
|
|
6729
|
+
(method as any).protocolV2ExpectedSerialNumber = 'pro2-device-id';
|
|
6730
|
+
(method as any).executeProtocolV2SourceUpdate = jest
|
|
6731
|
+
.fn()
|
|
6732
|
+
.mockResolvedValue('local-resource-result');
|
|
6733
|
+
const releaseSpy = jest.spyOn(DataManager, 'getFirmwareLatestRelease');
|
|
6734
|
+
const reloadSpy = jest.spyOn(DataManager, 'forceReloadData');
|
|
6137
6735
|
|
|
6138
|
-
|
|
6139
|
-
expect(
|
|
6140
|
-
expect(
|
|
6141
|
-
expect(
|
|
6736
|
+
try {
|
|
6737
|
+
await expect(method.run()).resolves.toBe('local-resource-result');
|
|
6738
|
+
expect(releaseSpy).not.toHaveBeenCalled();
|
|
6739
|
+
expect(reloadSpy).not.toHaveBeenCalled();
|
|
6740
|
+
expect((method as any).params.preparedPlan).toMatchObject({
|
|
6741
|
+
networkPolicy: 'forbid',
|
|
6742
|
+
executor: 'v4',
|
|
6743
|
+
targetsToUpdate: ['app_v1', 'resource'],
|
|
6744
|
+
});
|
|
6745
|
+
expect((method as any).executeProtocolV2SourceUpdate).toHaveBeenCalledWith(
|
|
6142
6746
|
expect.objectContaining({
|
|
6143
|
-
|
|
6144
|
-
{
|
|
6747
|
+
installSources: [
|
|
6748
|
+
expect.objectContaining({
|
|
6749
|
+
fileName: 'application_p1.bin',
|
|
6750
|
+
targetId: 4,
|
|
6751
|
+
}),
|
|
6752
|
+
],
|
|
6753
|
+
resourceSources: [
|
|
6754
|
+
expect.objectContaining({
|
|
6145
6755
|
name: 'images.okpkg',
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6756
|
+
devicePath: 'vol0:/bundles/images/images.okpkg',
|
|
6757
|
+
}),
|
|
6758
|
+
expect.objectContaining({
|
|
6759
|
+
name: 'boot_resource.okpkg',
|
|
6760
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
6761
|
+
}),
|
|
6149
6762
|
],
|
|
6150
6763
|
})
|
|
6151
6764
|
);
|
|
6765
|
+
} finally {
|
|
6766
|
+
releaseSpy.mockRestore();
|
|
6767
|
+
reloadSpy.mockRestore();
|
|
6152
6768
|
}
|
|
6153
|
-
);
|
|
6769
|
+
});
|
|
6154
6770
|
|
|
6155
|
-
test('rejects
|
|
6771
|
+
test('rejects a local resource ZIP whose file hash does not match its manifest', async () => {
|
|
6772
|
+
const resourceBinary = new Uint8Array([1, 2, 3]).buffer;
|
|
6773
|
+
const bootResourceBinary = new Uint8Array([4, 5, 6]).buffer;
|
|
6774
|
+
const zip = new JSZip();
|
|
6775
|
+
zip.file(
|
|
6776
|
+
'manifest.json',
|
|
6777
|
+
JSON.stringify({
|
|
6778
|
+
schema: 1,
|
|
6779
|
+
artifact_name: 'pro2-resource',
|
|
6780
|
+
release_name: 'local-development',
|
|
6781
|
+
variant: 'resource',
|
|
6782
|
+
commit: 'local',
|
|
6783
|
+
short_sha: 'local',
|
|
6784
|
+
timestamp_utc: '2026-08-09T00:00:00Z',
|
|
6785
|
+
core_version: '1.0.0',
|
|
6786
|
+
key_set: 'development',
|
|
6787
|
+
device_root: 'vol0:',
|
|
6788
|
+
restore_mode: 'bootloader_update',
|
|
6789
|
+
trees: [{ path: 'bundles', device: 'pro2' }],
|
|
6790
|
+
files: [
|
|
6791
|
+
{
|
|
6792
|
+
archive_path: 'bundles/images/images.okpkg',
|
|
6793
|
+
original_name: 'images.okpkg',
|
|
6794
|
+
device_path: 'vol0:/bundles/images/images.okpkg',
|
|
6795
|
+
size: resourceBinary.byteLength,
|
|
6796
|
+
sha256: '0'.repeat(64),
|
|
6797
|
+
signed: true,
|
|
6798
|
+
sig_algo: 'ed25519',
|
|
6799
|
+
payload_version: '1.0.0',
|
|
6800
|
+
},
|
|
6801
|
+
{
|
|
6802
|
+
archive_path: 'loaders/bootloader/boot_resource.okpkg',
|
|
6803
|
+
original_name: 'boot_resource.okpkg',
|
|
6804
|
+
device_path: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
6805
|
+
size: bootResourceBinary.byteLength,
|
|
6806
|
+
sha256: bytesToHex(sha256(new Uint8Array(bootResourceBinary))),
|
|
6807
|
+
signed: true,
|
|
6808
|
+
sig_algo: 'ed25519',
|
|
6809
|
+
payload_version: '1.0.0',
|
|
6810
|
+
},
|
|
6811
|
+
],
|
|
6812
|
+
})
|
|
6813
|
+
);
|
|
6814
|
+
zip.file('bundles/images/images.okpkg', resourceBinary);
|
|
6815
|
+
zip.file('loaders/bootloader/boot_resource.okpkg', bootResourceBinary);
|
|
6156
6816
|
const method = new FirmwareUpdateV4({
|
|
6157
6817
|
id: 1,
|
|
6158
6818
|
payload: {
|
|
6159
6819
|
method: 'firmwareUpdateV4',
|
|
6160
6820
|
platform: 'web',
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6821
|
+
targetsToUpdate: ['resource'],
|
|
6822
|
+
},
|
|
6823
|
+
});
|
|
6824
|
+
method.init();
|
|
6825
|
+
|
|
6826
|
+
await expect(
|
|
6827
|
+
(method as any).prepareProtocolV2LocalResourceArchive(
|
|
6828
|
+
await zip.generateAsync({ type: 'arraybuffer' })
|
|
6829
|
+
)
|
|
6830
|
+
).rejects.toThrow(
|
|
6831
|
+
'Protocol V2 local resource file does not match manifest: bundles/images/images.okpkg'
|
|
6832
|
+
);
|
|
6833
|
+
});
|
|
6834
|
+
|
|
6835
|
+
test('rejects an oversized ZIP entry before allocating its decompressed bytes', async () => {
|
|
6836
|
+
const extractEntry = jest.fn();
|
|
6837
|
+
const loadSpy = jest.spyOn(JSZip, 'loadAsync').mockResolvedValue({
|
|
6838
|
+
files: {
|
|
6839
|
+
'manifest.json': {
|
|
6840
|
+
name: 'manifest.json',
|
|
6841
|
+
dir: false,
|
|
6842
|
+
_data: {
|
|
6843
|
+
compressedSize: 1,
|
|
6844
|
+
uncompressedSize: 2 * 1024 * 1024,
|
|
6165
6845
|
},
|
|
6166
|
-
|
|
6846
|
+
async: extractEntry,
|
|
6847
|
+
},
|
|
6848
|
+
},
|
|
6849
|
+
} as unknown as JSZip);
|
|
6850
|
+
const method = new FirmwareUpdateV4({
|
|
6851
|
+
id: 1,
|
|
6852
|
+
payload: {
|
|
6853
|
+
method: 'firmwareUpdateV4',
|
|
6854
|
+
platform: 'web',
|
|
6855
|
+
targetsToUpdate: ['resource'],
|
|
6167
6856
|
},
|
|
6168
6857
|
});
|
|
6169
6858
|
method.init();
|
|
6859
|
+
|
|
6860
|
+
try {
|
|
6861
|
+
await expect(
|
|
6862
|
+
(method as any).prepareProtocolV2LocalResourceArchive(new Uint8Array([1]).buffer)
|
|
6863
|
+
).rejects.toThrow('declared size exceeds the allowed limit');
|
|
6864
|
+
expect(extractEntry).not.toHaveBeenCalled();
|
|
6865
|
+
} finally {
|
|
6866
|
+
loadSpy.mockRestore();
|
|
6867
|
+
}
|
|
6868
|
+
});
|
|
6869
|
+
|
|
6870
|
+
test('normalizes the deprecated boot_resources target to resource', () => {
|
|
6871
|
+
const method = new FirmwareUpdateV4({
|
|
6872
|
+
id: 1,
|
|
6873
|
+
payload: {
|
|
6874
|
+
method: 'firmwareUpdateV4',
|
|
6875
|
+
platform: 'web',
|
|
6876
|
+
targetsToUpdate: ['boot_resources'],
|
|
6877
|
+
},
|
|
6878
|
+
});
|
|
6879
|
+
|
|
6880
|
+
method.init();
|
|
6881
|
+
expect((method as any).params.targetsToUpdate).toEqual(['resource']);
|
|
6882
|
+
});
|
|
6883
|
+
|
|
6884
|
+
test('keeps the legacy componentArtifacts and artifactReader path without a prepared Plan', async () => {
|
|
6885
|
+
const artifactReader = {
|
|
6886
|
+
open: jest.fn(),
|
|
6887
|
+
read: jest.fn(),
|
|
6888
|
+
close: jest.fn(),
|
|
6889
|
+
};
|
|
6890
|
+
const method = new FirmwareUpdateV4({
|
|
6891
|
+
id: 1,
|
|
6892
|
+
payload: {
|
|
6893
|
+
method: 'firmwareUpdateV4',
|
|
6894
|
+
platform: 'web',
|
|
6895
|
+
targetsToUpdate: ['boot'],
|
|
6896
|
+
artifactReader,
|
|
6897
|
+
componentArtifacts: {
|
|
6898
|
+
boot: {
|
|
6899
|
+
artifactRef: 'legacy-bootloader',
|
|
6900
|
+
size: 3,
|
|
6901
|
+
sha256: 'a'.repeat(64),
|
|
6902
|
+
},
|
|
6903
|
+
},
|
|
6904
|
+
},
|
|
6905
|
+
});
|
|
6906
|
+
|
|
6907
|
+
expect(() => method.init()).not.toThrow();
|
|
6170
6908
|
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
6909
|
+
(method as any).device = stubDevice({
|
|
6910
|
+
originalDescriptor: { protocolType: 'V2' },
|
|
6911
|
+
features: { deviceType: 'pro2', firmwareVersion: '1.0.0', capabilities: [] },
|
|
6912
|
+
});
|
|
6913
|
+
(method as any).runProtocolV2PreparedArtifacts = jest.fn().mockResolvedValue('legacy-result');
|
|
6171
6914
|
|
|
6915
|
+
await expect(method.run()).resolves.toBe('legacy-result');
|
|
6916
|
+
expect((method as any).runProtocolV2PreparedArtifacts).toHaveBeenCalledTimes(1);
|
|
6917
|
+
});
|
|
6918
|
+
|
|
6919
|
+
test('stops a remote update before reboot when the latest config cannot be refreshed', async () => {
|
|
6920
|
+
const method = new FirmwareUpdateV4({
|
|
6921
|
+
id: 1,
|
|
6922
|
+
payload: {
|
|
6923
|
+
method: 'firmwareUpdateV4',
|
|
6924
|
+
platform: 'web',
|
|
6925
|
+
},
|
|
6926
|
+
});
|
|
6927
|
+
method.init();
|
|
6928
|
+
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
6172
6929
|
(method as any).device = stubDevice({
|
|
6173
6930
|
originalDescriptor: { protocolType: 'V2' },
|
|
6174
6931
|
features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] },
|
|
6175
6932
|
});
|
|
6933
|
+
forceReloadDataSpy.mockRejectedValue(
|
|
6934
|
+
ERRORS.TypedError(HardwareErrorCode.NetworkError, 'latest config unavailable')
|
|
6935
|
+
);
|
|
6176
6936
|
(method as any).prepareRemoteProtocolV2Binaries = jest.fn();
|
|
6177
6937
|
(method as any).enterProtocolV2BootloaderMode = jest.fn();
|
|
6178
6938
|
method.postTipMessage = jest.fn();
|
|
6179
6939
|
|
|
6180
|
-
await expect(method.run()).rejects.
|
|
6940
|
+
await expect(method.run()).rejects.toMatchObject({
|
|
6941
|
+
errorCode: HardwareErrorCode.NetworkError,
|
|
6942
|
+
message: 'latest config unavailable',
|
|
6943
|
+
});
|
|
6944
|
+
|
|
6181
6945
|
expect((method as any).prepareRemoteProtocolV2Binaries).not.toHaveBeenCalled();
|
|
6182
6946
|
expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
|
|
6183
6947
|
});
|
|
6184
6948
|
|
|
6185
|
-
test('
|
|
6949
|
+
test('keeps explicit Protocol V2 binaries isolated from remote component auto-fill', async () => {
|
|
6950
|
+
const method = new FirmwareUpdateV4({
|
|
6951
|
+
id: 1,
|
|
6952
|
+
payload: {
|
|
6953
|
+
method: 'firmwareUpdateV4',
|
|
6954
|
+
platform: 'web',
|
|
6955
|
+
coprocessorBinary: new Uint8Array([1]).buffer,
|
|
6956
|
+
},
|
|
6957
|
+
});
|
|
6958
|
+
method.init();
|
|
6959
|
+
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
6960
|
+
|
|
6961
|
+
(method as any).device = stubDevice({
|
|
6962
|
+
originalDescriptor: { protocolType: 'V2' },
|
|
6963
|
+
features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] },
|
|
6964
|
+
});
|
|
6965
|
+
(method as any).prepareRemoteProtocolV2Binaries = jest.fn();
|
|
6966
|
+
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true);
|
|
6967
|
+
(method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
|
|
6968
|
+
(method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
|
|
6969
|
+
(method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
|
|
6970
|
+
bootloaderVersion: '1.0.0',
|
|
6971
|
+
bleVersion: '0.0.0',
|
|
6972
|
+
firmwareVersion: '1.0.0',
|
|
6973
|
+
});
|
|
6974
|
+
method.postTipMessage = jest.fn();
|
|
6975
|
+
|
|
6976
|
+
await method.run();
|
|
6977
|
+
|
|
6978
|
+
expect((method as any).prepareRemoteProtocolV2Binaries).not.toHaveBeenCalled();
|
|
6979
|
+
expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
|
|
6980
|
+
bootloaderBinary: null,
|
|
6981
|
+
fwBinaryMap: [{ fileName: 'coprocessor.bin', binary: expect.anything(), targetId: 6 }],
|
|
6982
|
+
});
|
|
6983
|
+
});
|
|
6984
|
+
|
|
6985
|
+
test('syncs prepared resource sources without sending a firmware install request', async () => {
|
|
6186
6986
|
const method = new FirmwareUpdateV4({
|
|
6187
6987
|
id: 1,
|
|
6188
6988
|
payload: {
|
|
@@ -6194,28 +6994,118 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6194
6994
|
method.postProgressMessage = jest.fn();
|
|
6195
6995
|
(method as any).protocolV2SourceUpdateProcess = jest.fn().mockResolvedValue(3);
|
|
6196
6996
|
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
|
|
6997
|
+
(method as any).ensureProtocolV2BootResourceStagingIsEmpty = jest
|
|
6998
|
+
.fn()
|
|
6999
|
+
.mockResolvedValue(undefined);
|
|
6197
7000
|
(method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
|
|
6198
7001
|
(method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
|
|
6199
7002
|
(method as any).protocolV2StartFirmwareUpdate = jest.fn();
|
|
6200
7003
|
(method as any).waitForProtocolV2FirmwareUpdateComplete = jest.fn();
|
|
6201
7004
|
|
|
6202
|
-
await (method as any).
|
|
6203
|
-
|
|
7005
|
+
await (method as any).executeProtocolV2SourceUpdate({
|
|
7006
|
+
installSources: [],
|
|
7007
|
+
resourceSources: [
|
|
6204
7008
|
{
|
|
6205
7009
|
name: 'images.okpkg',
|
|
6206
|
-
|
|
7010
|
+
source: {
|
|
7011
|
+
size: 3,
|
|
7012
|
+
readAt: jest.fn(),
|
|
7013
|
+
close: jest.fn(),
|
|
7014
|
+
},
|
|
6207
7015
|
devicePath: 'vol0:/resource/images/images.okpkg',
|
|
6208
7016
|
},
|
|
6209
7017
|
],
|
|
6210
|
-
bootloaderBinary: null,
|
|
6211
|
-
fwBinaryMap: [],
|
|
6212
7018
|
});
|
|
6213
7019
|
|
|
6214
7020
|
expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenCalledTimes(1);
|
|
7021
|
+
expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenCalledWith(
|
|
7022
|
+
expect.objectContaining({ filePath: 'vol0:/resource/images/images.okpkg' })
|
|
7023
|
+
);
|
|
7024
|
+
expect((method as any).verifyProtocolV2StagedFile).toHaveBeenCalledWith(
|
|
7025
|
+
'vol0:/resource/images/images.okpkg',
|
|
7026
|
+
3
|
|
7027
|
+
);
|
|
6215
7028
|
expect((method as any).protocolV2StartFirmwareUpdate).not.toHaveBeenCalled();
|
|
6216
7029
|
expect((method as any).waitForProtocolV2FirmwareUpdateComplete).not.toHaveBeenCalled();
|
|
6217
7030
|
});
|
|
6218
7031
|
|
|
7032
|
+
test('writes the mounted boot resource package through its staging path', async () => {
|
|
7033
|
+
const method = new FirmwareUpdateV4({
|
|
7034
|
+
id: 1,
|
|
7035
|
+
payload: {
|
|
7036
|
+
method: 'firmwareUpdateV4',
|
|
7037
|
+
},
|
|
7038
|
+
});
|
|
7039
|
+
|
|
7040
|
+
method.postTipMessage = jest.fn();
|
|
7041
|
+
method.postProgressMessage = jest.fn();
|
|
7042
|
+
(method as any).protocolV2SourceUpdateProcess = jest.fn().mockResolvedValue(3);
|
|
7043
|
+
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
|
|
7044
|
+
(method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
|
|
7045
|
+
(method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
|
|
7046
|
+
|
|
7047
|
+
await (method as any).executeProtocolV2SourceUpdate({
|
|
7048
|
+
installSources: [],
|
|
7049
|
+
resourceSources: [
|
|
7050
|
+
{
|
|
7051
|
+
name: 'boot_resource.okpkg',
|
|
7052
|
+
source: {
|
|
7053
|
+
size: 3,
|
|
7054
|
+
readAt: jest.fn(),
|
|
7055
|
+
close: jest.fn(),
|
|
7056
|
+
},
|
|
7057
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
7058
|
+
},
|
|
7059
|
+
],
|
|
7060
|
+
});
|
|
7061
|
+
|
|
7062
|
+
const stagingPath = 'vol0:/loaders/bootloader/boot_resource.okpkg.staging';
|
|
7063
|
+
expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenCalledWith(
|
|
7064
|
+
expect.objectContaining({ filePath: stagingPath })
|
|
7065
|
+
);
|
|
7066
|
+
expect((method as any).verifyProtocolV2StagedFile).toHaveBeenCalledWith(stagingPath, 3);
|
|
7067
|
+
});
|
|
7068
|
+
|
|
7069
|
+
test('rewrites the mounted boot resource staging file even when the final file is current', async () => {
|
|
7070
|
+
const method = new FirmwareUpdateV4({
|
|
7071
|
+
id: 1,
|
|
7072
|
+
payload: {
|
|
7073
|
+
method: 'firmwareUpdateV4',
|
|
7074
|
+
},
|
|
7075
|
+
});
|
|
7076
|
+
|
|
7077
|
+
method.postTipMessage = jest.fn();
|
|
7078
|
+
method.postProgressMessage = jest.fn();
|
|
7079
|
+
(method as any).isProtocolV2ResourceBundleUpToDate = jest.fn().mockResolvedValue(true);
|
|
7080
|
+
(method as any).protocolV2SourceUpdateProcess = jest.fn().mockResolvedValue(3);
|
|
7081
|
+
(method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
|
|
7082
|
+
|
|
7083
|
+
const resource = {
|
|
7084
|
+
name: 'boot_resource.okpkg',
|
|
7085
|
+
source: {
|
|
7086
|
+
size: 3,
|
|
7087
|
+
readAt: jest.fn(),
|
|
7088
|
+
close: jest.fn(),
|
|
7089
|
+
},
|
|
7090
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
7091
|
+
version: [1, 0, 0],
|
|
7092
|
+
payloadHash: '11'.repeat(64),
|
|
7093
|
+
headerHash: '22'.repeat(64),
|
|
7094
|
+
};
|
|
7095
|
+
|
|
7096
|
+
await (method as any).executeProtocolV2TransferPhase({
|
|
7097
|
+
installSources: [],
|
|
7098
|
+
resourceSources: [resource],
|
|
7099
|
+
});
|
|
7100
|
+
|
|
7101
|
+
const stagingPath = 'vol0:/loaders/bootloader/boot_resource.okpkg.staging';
|
|
7102
|
+
expect((method as any).isProtocolV2ResourceBundleUpToDate).not.toHaveBeenCalled();
|
|
7103
|
+
expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenCalledWith(
|
|
7104
|
+
expect.objectContaining({ source: resource.source, filePath: stagingPath, totalSize: 3 })
|
|
7105
|
+
);
|
|
7106
|
+
expect((method as any).verifyProtocolV2StagedFile).toHaveBeenCalledWith(stagingPath, 3);
|
|
7107
|
+
});
|
|
7108
|
+
|
|
6219
7109
|
test('uses absolute processed_byte offsets and disables append for firmware file writes', async () => {
|
|
6220
7110
|
const method = new FirmwareUpdateV4({
|
|
6221
7111
|
id: 1,
|
|
@@ -6919,64 +7809,6 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
6919
7809
|
});
|
|
6920
7810
|
|
|
6921
7811
|
describe('Protocol V2 firmware reconnect identity', () => {
|
|
6922
|
-
const createResourceFilesystemTypedCall = (
|
|
6923
|
-
stable: Array<{ type: string; size: number; headerHash: string }>,
|
|
6924
|
-
missingTypes: string[] = []
|
|
6925
|
-
) => {
|
|
6926
|
-
const missing = new Set(missingTypes);
|
|
6927
|
-
const resourceByPath = new Map(
|
|
6928
|
-
stable.map(resource => [
|
|
6929
|
-
PROTOCOL_V2_RESOURCE_DEVICE_PATHS[
|
|
6930
|
-
resource.type as keyof typeof PROTOCOL_V2_RESOURCE_DEVICE_PATHS
|
|
6931
|
-
],
|
|
6932
|
-
resource,
|
|
6933
|
-
])
|
|
6934
|
-
);
|
|
6935
|
-
return jest.fn((requestType: string, _responseType: string, payload: Record<string, any>) => {
|
|
6936
|
-
if (requestType === 'ResourceInventoryGet') {
|
|
6937
|
-
throw new Error('ResourceInventoryGet is unavailable on released firmware');
|
|
6938
|
-
}
|
|
6939
|
-
if (requestType === 'FilesystemPathInfoQuery') {
|
|
6940
|
-
const resource = resourceByPath.get(payload.path);
|
|
6941
|
-
const exists = Boolean(resource && !missing.has(resource.type));
|
|
6942
|
-
return {
|
|
6943
|
-
message: {
|
|
6944
|
-
exist: exists,
|
|
6945
|
-
directory: false,
|
|
6946
|
-
size: exists ? resource?.size : 0,
|
|
6947
|
-
},
|
|
6948
|
-
};
|
|
6949
|
-
}
|
|
6950
|
-
if (requestType === 'FilesystemFileRead') {
|
|
6951
|
-
const resource = resourceByPath.get(payload.file.path);
|
|
6952
|
-
if (!resource || missing.has(resource.type)) throw new Error('missing resource');
|
|
6953
|
-
const header = new Uint8Array(0x52a0);
|
|
6954
|
-
const view = new DataView(header.buffer);
|
|
6955
|
-
'OKPP'.split('').forEach((char, index) => {
|
|
6956
|
-
header[index] = char.charCodeAt(0);
|
|
6957
|
-
});
|
|
6958
|
-
'RESC'.split('').forEach((char, index) => {
|
|
6959
|
-
header[0x08 + index] = char.charCodeAt(0);
|
|
6960
|
-
});
|
|
6961
|
-
view.setUint32(0x0c, header.byteLength, true);
|
|
6962
|
-
for (let index = 0; index < resource.headerHash.length / 2; index++) {
|
|
6963
|
-
header[0x240 + index] = Number.parseInt(
|
|
6964
|
-
resource.headerHash.slice(index * 2, index * 2 + 2),
|
|
6965
|
-
16
|
|
6966
|
-
);
|
|
6967
|
-
}
|
|
6968
|
-
const offset = Number(payload.file.offset);
|
|
6969
|
-
const chunkLength = Number(payload.chunk_len);
|
|
6970
|
-
return {
|
|
6971
|
-
message: {
|
|
6972
|
-
data: header.slice(offset, offset + chunkLength),
|
|
6973
|
-
},
|
|
6974
|
-
};
|
|
6975
|
-
}
|
|
6976
|
-
throw new Error(`Unexpected request: ${requestType}`);
|
|
6977
|
-
});
|
|
6978
|
-
};
|
|
6979
|
-
|
|
6980
7812
|
test('keeps the host plan identity as the V4 reconnect anchor', () => {
|
|
6981
7813
|
const method = new FirmwareUpdateV4({
|
|
6982
7814
|
id: 1,
|
|
@@ -7043,7 +7875,7 @@ describe('Protocol V2 firmware reconnect identity', () => {
|
|
|
7043
7875
|
);
|
|
7044
7876
|
});
|
|
7045
7877
|
|
|
7046
|
-
test('
|
|
7878
|
+
test('uses the connection route when DeviceInfo has no physical serial', async () => {
|
|
7047
7879
|
const method = new FirmwareUpdateV4({
|
|
7048
7880
|
id: 1,
|
|
7049
7881
|
payload: { method: 'firmwareUpdateV4' },
|
|
@@ -7053,12 +7885,98 @@ describe('Protocol V2 firmware reconnect identity', () => {
|
|
|
7053
7885
|
message: { protocol_version: 1, hw: {} },
|
|
7054
7886
|
});
|
|
7055
7887
|
(method as any).device = stubDevice({
|
|
7888
|
+
originalDescriptor: { path: '' },
|
|
7889
|
+
getConnectId: () => '000000000000',
|
|
7056
7890
|
getCommands: () => ({ typedCall }),
|
|
7057
7891
|
});
|
|
7058
7892
|
|
|
7059
|
-
await expect((method as any).captureProtocolV2PhysicalIdentity()).
|
|
7060
|
-
|
|
7893
|
+
await expect((method as any).captureProtocolV2PhysicalIdentity()).resolves.toBeUndefined();
|
|
7894
|
+
expect((method as any).protocolV2ExpectedSerialNumber).toBeUndefined();
|
|
7895
|
+
expect((method as any).protocolV2ExpectedPath).toBe('000000000000');
|
|
7896
|
+
});
|
|
7897
|
+
|
|
7898
|
+
test('executes a serial-less Pro2 prepared plan when the live model matches', async () => {
|
|
7899
|
+
const planWithoutDigest = {
|
|
7900
|
+
schemaVersion: 2 as const,
|
|
7901
|
+
executor: 'v4' as const,
|
|
7902
|
+
deviceIdentity: 'unavailable',
|
|
7903
|
+
deviceModel: 'pro2',
|
|
7904
|
+
firmwareType: EFirmwareType.Universal,
|
|
7905
|
+
platform: 'web' as const,
|
|
7906
|
+
targetsToUpdate: ['boot' as const],
|
|
7907
|
+
artifacts: [
|
|
7908
|
+
{
|
|
7909
|
+
artifactId: 'component:boot',
|
|
7910
|
+
role: 'component' as const,
|
|
7911
|
+
target: 'boot' as const,
|
|
7912
|
+
url: 'https://example.com/bootloader.bin',
|
|
7913
|
+
container: 'raw' as const,
|
|
7914
|
+
logicalName: 'bootloader',
|
|
7915
|
+
expectedSize: 1,
|
|
7916
|
+
expectedSha256: 'a'.repeat(64),
|
|
7917
|
+
},
|
|
7918
|
+
],
|
|
7919
|
+
};
|
|
7920
|
+
const preparedPlan = prepareFirmwareUpdatePlan({
|
|
7921
|
+
plan: {
|
|
7922
|
+
...planWithoutDigest,
|
|
7923
|
+
planDigest: digestFirmwareUpdateContract(planWithoutDigest),
|
|
7924
|
+
},
|
|
7925
|
+
leaseRef: 'serial-less-v4-executor-test',
|
|
7926
|
+
artifacts: [
|
|
7927
|
+
{
|
|
7928
|
+
artifactId: 'component:boot',
|
|
7929
|
+
artifact: {
|
|
7930
|
+
artifactRef: 'serial-less-bootloader',
|
|
7931
|
+
size: 1,
|
|
7932
|
+
sha256: 'a'.repeat(64),
|
|
7933
|
+
},
|
|
7934
|
+
},
|
|
7935
|
+
],
|
|
7936
|
+
});
|
|
7937
|
+
const hostBindingGeneration = registerFirmwareUpdateHostBinding({
|
|
7938
|
+
preparedPlanDigest: preparedPlan.preparedPlanDigest,
|
|
7939
|
+
artifactReader: {
|
|
7940
|
+
open: jest.fn(),
|
|
7941
|
+
read: jest.fn(),
|
|
7942
|
+
close: jest.fn(),
|
|
7943
|
+
},
|
|
7061
7944
|
});
|
|
7945
|
+
try {
|
|
7946
|
+
const method = new FirmwareUpdateV4({
|
|
7947
|
+
id: 1,
|
|
7948
|
+
payload: {
|
|
7949
|
+
method: 'firmwareUpdateV4',
|
|
7950
|
+
platform: 'web',
|
|
7951
|
+
preparedPlan,
|
|
7952
|
+
hostBindingGeneration,
|
|
7953
|
+
},
|
|
7954
|
+
});
|
|
7955
|
+
method.init();
|
|
7956
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
7957
|
+
type: 'DeviceInfo',
|
|
7958
|
+
message: { protocol_version: 1, hw: {} },
|
|
7959
|
+
});
|
|
7960
|
+
(method as any).device = stubDevice({
|
|
7961
|
+
originalDescriptor: { protocolType: 'V2', path: 'serial-less-pro2' },
|
|
7962
|
+
features: {
|
|
7963
|
+
deviceType: 'pro2',
|
|
7964
|
+
firmwareVersion: '1.0.0',
|
|
7965
|
+
capabilities: [],
|
|
7966
|
+
},
|
|
7967
|
+
getCommands: () => ({ typedCall }),
|
|
7968
|
+
});
|
|
7969
|
+
(method as any).runProtocolV2PreparedArtifacts = jest
|
|
7970
|
+
.fn()
|
|
7971
|
+
.mockResolvedValue('serial-less-result');
|
|
7972
|
+
|
|
7973
|
+
await expect(method.run()).resolves.toBe('serial-less-result');
|
|
7974
|
+
expect((method as any).runProtocolV2PreparedArtifacts).toHaveBeenCalled();
|
|
7975
|
+
expect((method as any).protocolV2ExpectedSerialNumber).toBeUndefined();
|
|
7976
|
+
expect((method as any).protocolV2ExpectedPath).toBe('serial-less-pro2');
|
|
7977
|
+
} finally {
|
|
7978
|
+
unregisterFirmwareUpdateHostBinding(hostBindingGeneration);
|
|
7979
|
+
}
|
|
7062
7980
|
});
|
|
7063
7981
|
|
|
7064
7982
|
test('uses the BLE read limit when reading an existing firmware bundle header', async () => {
|
|
@@ -7091,292 +8009,94 @@ describe('Protocol V2 firmware reconnect identity', () => {
|
|
|
7091
8009
|
expect(Math.max(...readChunkLengths)).toBe(900);
|
|
7092
8010
|
});
|
|
7093
8011
|
|
|
7094
|
-
test('
|
|
7095
|
-
const method = new FirmwareUpdateV4({
|
|
7096
|
-
id: 1,
|
|
7097
|
-
payload: {
|
|
7098
|
-
method: 'firmwareUpdateV4',
|
|
7099
|
-
platform: 'web',
|
|
7100
|
-
targetsToUpdate: ['resource'],
|
|
7101
|
-
},
|
|
7102
|
-
});
|
|
7103
|
-
method.init();
|
|
7104
|
-
const stable = ['images', 'animation', 'wallpaper', 'translations', 'roobert', 'noto'].map(
|
|
7105
|
-
(type, index) => ({
|
|
7106
|
-
type,
|
|
7107
|
-
url: `https://example.com/${type}.okpkg`,
|
|
7108
|
-
size: 0x52a0 + index + 1,
|
|
7109
|
-
fileHash: 'a'.repeat(64),
|
|
7110
|
-
headerHash: index.toString(16).padStart(128, '0'),
|
|
7111
|
-
})
|
|
7112
|
-
);
|
|
7113
|
-
const typedCall = createResourceFilesystemTypedCall(stable);
|
|
7114
|
-
(method as any).device = stubDevice({
|
|
7115
|
-
getCommands: () => ({ typedCall }),
|
|
7116
|
-
getCurrentDeviceType: () => 'pro2',
|
|
7117
|
-
});
|
|
7118
|
-
const resourcesSpy = jest
|
|
7119
|
-
.spyOn(DataManager, 'getProtocolV2Resources')
|
|
7120
|
-
.mockReturnValue(stable as any);
|
|
7121
|
-
(method as any).downloadProtocolV2Resource = jest.fn();
|
|
7122
|
-
|
|
7123
|
-
await expect((method as any).prepareProtocolV2ResourceBundles()).resolves.toEqual([]);
|
|
7124
|
-
expect(typedCall.mock.calls.some(call => call[0] === 'ResourceInventoryGet')).toBe(false);
|
|
7125
|
-
expect(typedCall.mock.calls.some(call => call[0] === 'FilesystemFileRead')).toBe(true);
|
|
7126
|
-
expect((method as any).downloadProtocolV2Resource).not.toHaveBeenCalled();
|
|
7127
|
-
resourcesSpy.mockRestore();
|
|
7128
|
-
});
|
|
7129
|
-
|
|
7130
|
-
test('downloads only changed resources from loader inventory', async () => {
|
|
7131
|
-
const method = new FirmwareUpdateV4({
|
|
7132
|
-
id: 1,
|
|
7133
|
-
payload: { method: 'firmwareUpdateV4', platform: 'web', targetsToUpdate: ['resource'] },
|
|
7134
|
-
});
|
|
7135
|
-
method.init();
|
|
7136
|
-
const stable = ['images', 'animation', 'wallpaper', 'translations', 'roobert', 'noto'].map(
|
|
7137
|
-
(type, index) => ({
|
|
7138
|
-
type,
|
|
7139
|
-
url: `https://example.com/${type}.okpkg`,
|
|
7140
|
-
size: 0x52a0 + index + 1,
|
|
7141
|
-
fileHash: 'a'.repeat(64),
|
|
7142
|
-
headerHash: index.toString(16).padStart(128, '0'),
|
|
7143
|
-
})
|
|
7144
|
-
);
|
|
7145
|
-
const typedCall = createResourceFilesystemTypedCall(stable, ['images']);
|
|
7146
|
-
(method as any).device = stubDevice({
|
|
7147
|
-
getCommands: () => ({ typedCall }),
|
|
7148
|
-
getCurrentDeviceType: () => 'pro2',
|
|
7149
|
-
});
|
|
7150
|
-
const resourcesSpy = jest
|
|
7151
|
-
.spyOn(DataManager, 'getProtocolV2Resources')
|
|
7152
|
-
.mockReturnValue(stable as any);
|
|
7153
|
-
(method as any).downloadProtocolV2Resource = jest.fn(resource =>
|
|
7154
|
-
Promise.resolve({
|
|
7155
|
-
name: `${resource.type}.okpkg`,
|
|
7156
|
-
binary: new ArrayBuffer(resource.size),
|
|
7157
|
-
devicePath: `vol0:/${resource.type}.okpkg`,
|
|
7158
|
-
})
|
|
7159
|
-
);
|
|
7160
|
-
|
|
7161
|
-
const bundles = await (method as any).prepareProtocolV2ResourceBundles();
|
|
7162
|
-
|
|
7163
|
-
expect(bundles).toHaveLength(1);
|
|
7164
|
-
expect((method as any).downloadProtocolV2Resource).toHaveBeenCalledWith(stable[0]);
|
|
7165
|
-
resourcesSpy.mockRestore();
|
|
7166
|
-
});
|
|
7167
|
-
|
|
7168
|
-
test('uses filesystem inventory for incremental Bootloader recovery', async () => {
|
|
7169
|
-
const method = new FirmwareUpdateV4({
|
|
7170
|
-
id: 1,
|
|
7171
|
-
payload: { method: 'firmwareUpdateV4', platform: 'web', targetsToUpdate: ['resource'] },
|
|
7172
|
-
});
|
|
7173
|
-
method.init();
|
|
7174
|
-
const stable = ['images', 'animation', 'wallpaper', 'translations', 'roobert', 'noto'].map(
|
|
7175
|
-
(type, index) => ({
|
|
7176
|
-
type,
|
|
7177
|
-
url: `https://example.com/${type}.okpkg`,
|
|
7178
|
-
size: 0x52a0 + index + 1,
|
|
7179
|
-
fileHash: 'a'.repeat(64),
|
|
7180
|
-
headerHash: index.toString(16).padStart(128, '0'),
|
|
7181
|
-
})
|
|
7182
|
-
);
|
|
7183
|
-
const typedCall = createResourceFilesystemTypedCall(stable, ['images']);
|
|
7184
|
-
(method as any).device = stubDevice({
|
|
7185
|
-
getCommands: () => ({ typedCall }),
|
|
7186
|
-
getCurrentDeviceType: () => 'pro2',
|
|
7187
|
-
});
|
|
7188
|
-
const resourcesSpy = jest
|
|
7189
|
-
.spyOn(DataManager, 'getProtocolV2Resources')
|
|
7190
|
-
.mockReturnValue(stable as any);
|
|
7191
|
-
(method as any).downloadProtocolV2Resource = jest.fn(resource =>
|
|
7192
|
-
Promise.resolve({
|
|
7193
|
-
name: `${resource.type}.okpkg`,
|
|
7194
|
-
binary: new ArrayBuffer(resource.size),
|
|
7195
|
-
devicePath: `vol0:/${resource.type}.okpkg`,
|
|
7196
|
-
})
|
|
7197
|
-
);
|
|
7198
|
-
|
|
7199
|
-
const bundles = await (method as any).prepareProtocolV2ResourceBundles();
|
|
7200
|
-
|
|
7201
|
-
expect(bundles).toHaveLength(1);
|
|
7202
|
-
expect(typedCall.mock.calls.some(call => call[0] === 'ResourceInventoryGet')).toBe(false);
|
|
7203
|
-
expect(typedCall.mock.calls.some(call => call[0] === 'FilesystemPathInfoQuery')).toBe(true);
|
|
7204
|
-
expect((method as any).downloadProtocolV2Resource).toHaveBeenCalledWith(stable[0]);
|
|
7205
|
-
resourcesSpy.mockRestore();
|
|
7206
|
-
});
|
|
7207
|
-
|
|
7208
|
-
test('does not resolve boot resources when no resource target is selected', async () => {
|
|
7209
|
-
const method = new FirmwareUpdateV4({
|
|
7210
|
-
id: 1,
|
|
7211
|
-
payload: { method: 'firmwareUpdateV4', platform: 'web' },
|
|
7212
|
-
});
|
|
7213
|
-
method.init();
|
|
7214
|
-
const configSpy = jest.spyOn(DataManager, 'getProtocolV2BootResources');
|
|
7215
|
-
const downloadSpy = jest.spyOn(firmwareBinaryApi, 'getSysResourceBinary');
|
|
7216
|
-
|
|
7217
|
-
await expect((method as any).prepareProtocolV2BootResources()).resolves.toBeUndefined();
|
|
7218
|
-
|
|
7219
|
-
expect(configSpy).not.toHaveBeenCalled();
|
|
7220
|
-
expect(downloadSpy).not.toHaveBeenCalled();
|
|
7221
|
-
});
|
|
7222
|
-
|
|
7223
|
-
test('continues a normal resource update when boot resources are not configured', async () => {
|
|
7224
|
-
const method = new FirmwareUpdateV4({
|
|
7225
|
-
id: 1,
|
|
7226
|
-
payload: { method: 'firmwareUpdateV4', platform: 'web', targetsToUpdate: ['resource'] },
|
|
7227
|
-
});
|
|
7228
|
-
method.init();
|
|
7229
|
-
(method as any).device = stubDevice({ getCurrentDeviceType: () => 'pro2' });
|
|
7230
|
-
jest.spyOn(DataManager, 'getProtocolV2BootResources').mockReturnValue(undefined);
|
|
7231
|
-
const downloadSpy = jest.spyOn(firmwareBinaryApi, 'getSysResourceBinary');
|
|
7232
|
-
|
|
7233
|
-
await expect((method as any).prepareProtocolV2BootResources()).resolves.toBeUndefined();
|
|
7234
|
-
expect(downloadSpy).not.toHaveBeenCalled();
|
|
7235
|
-
});
|
|
7236
|
-
|
|
7237
|
-
test('requires boot configuration for an explicit boot_resources target', async () => {
|
|
8012
|
+
test('skips resource transfer when installed okpkg hashes match the downloaded package', async () => {
|
|
7238
8013
|
const method = new FirmwareUpdateV4({
|
|
7239
8014
|
id: 1,
|
|
7240
8015
|
payload: {
|
|
7241
8016
|
method: 'firmwareUpdateV4',
|
|
7242
8017
|
platform: 'web',
|
|
7243
|
-
targetsToUpdate: ['boot_resources'],
|
|
7244
8018
|
},
|
|
7245
8019
|
});
|
|
7246
8020
|
method.init();
|
|
7247
|
-
|
|
7248
|
-
jest.
|
|
7249
|
-
|
|
7250
|
-
await expect((method as any).prepareProtocolV2BootResources()).rejects.toThrow(
|
|
7251
|
-
'Missing Protocol V2 boot resources configuration'
|
|
7252
|
-
);
|
|
7253
|
-
});
|
|
7254
|
-
|
|
7255
|
-
test('skips downloading a boot resource when the installed SHA-256 matches', async () => {
|
|
7256
|
-
const bytes = new Uint8Array([1, 2, 3, 4]);
|
|
7257
|
-
const fileHash = Array.from(sha256(bytes), byte => byte.toString(16).padStart(2, '0')).join('');
|
|
7258
|
-
const resource = {
|
|
7259
|
-
required: false as const,
|
|
7260
|
-
target: 'RES' as const,
|
|
7261
|
-
files: [
|
|
7262
|
-
{
|
|
7263
|
-
name: 'bootloader_crest.bin',
|
|
7264
|
-
url: 'https://example.com/bootloader_crest.bin',
|
|
7265
|
-
devicePath: 'vol0:/assets/loaders/boot.staging/graphics/bootloader_crest.bin',
|
|
7266
|
-
size: bytes.byteLength,
|
|
7267
|
-
fileHash,
|
|
7268
|
-
},
|
|
7269
|
-
],
|
|
7270
|
-
};
|
|
7271
|
-
const typedCall = jest.fn((name: string, _response: string, payload: any) => {
|
|
7272
|
-
if (name === 'FilesystemPathInfoQuery') {
|
|
8021
|
+
const installedBinary = new Uint8Array(createProtocolV2OkppBinary());
|
|
8022
|
+
const typedCall = jest.fn((requestType: string, _responseType: string, request: any) => {
|
|
8023
|
+
if (requestType === 'FilesystemPathInfoQuery') {
|
|
7273
8024
|
return Promise.resolve({
|
|
7274
|
-
message: { exist: true, directory: false, size:
|
|
8025
|
+
message: { exist: true, directory: false, size: installedBinary.byteLength },
|
|
7275
8026
|
});
|
|
7276
8027
|
}
|
|
7277
|
-
if (
|
|
7278
|
-
const offset = Number(payload.file.offset);
|
|
8028
|
+
if (requestType === 'FilesystemFileRead') {
|
|
7279
8029
|
return Promise.resolve({
|
|
7280
|
-
message: {
|
|
8030
|
+
message: {
|
|
8031
|
+
data: installedBinary.slice(
|
|
8032
|
+
Number(request.file.offset),
|
|
8033
|
+
Number(request.file.offset) + Number(request.chunk_len)
|
|
8034
|
+
),
|
|
8035
|
+
},
|
|
7281
8036
|
});
|
|
7282
8037
|
}
|
|
7283
|
-
|
|
7284
|
-
});
|
|
7285
|
-
const method = new FirmwareUpdateV4({
|
|
7286
|
-
id: 1,
|
|
7287
|
-
payload: { method: 'firmwareUpdateV4', platform: 'web', targetsToUpdate: ['resource'] },
|
|
8038
|
+
throw new Error(`Unexpected request: ${requestType}`);
|
|
7288
8039
|
});
|
|
7289
|
-
method.init();
|
|
7290
8040
|
(method as any).device = stubDevice({
|
|
7291
|
-
getCurrentDeviceType: () => 'pro2',
|
|
7292
8041
|
getCommands: () => ({ typedCall }),
|
|
7293
8042
|
});
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
await expect((method as any).prepareProtocolV2BootResources()).resolves.toEqual([]);
|
|
7298
|
-
expect(typedCall.mock.calls.map(call => call[0])).toEqual([
|
|
7299
|
-
'FilesystemPathInfoQuery',
|
|
7300
|
-
'FilesystemFileRead',
|
|
7301
|
-
]);
|
|
7302
|
-
expect(downloadSpy).not.toHaveBeenCalled();
|
|
7303
|
-
});
|
|
7304
|
-
|
|
7305
|
-
test('rejects duplicate device paths across explicit and remote resource sources', () => {
|
|
7306
|
-
const method = new FirmwareUpdateV4({
|
|
7307
|
-
id: 1,
|
|
7308
|
-
payload: { method: 'firmwareUpdateV4' },
|
|
7309
|
-
});
|
|
7310
|
-
const devicePath = 'vol0:/assets/shared.bin';
|
|
7311
|
-
|
|
7312
|
-
expect(() =>
|
|
7313
|
-
(method as any).mergeProtocolV2ResourceBundles(
|
|
7314
|
-
[{ name: 'local.bin', binary: new ArrayBuffer(1), devicePath }],
|
|
7315
|
-
[{ name: 'remote.bin', binary: new ArrayBuffer(1), devicePath }]
|
|
7316
|
-
)
|
|
7317
|
-
).toThrow(`Duplicate Protocol V2 resource devicePath: ${devicePath}`);
|
|
7318
|
-
});
|
|
8043
|
+
method.postTipMessage = jest.fn();
|
|
8044
|
+
method.postProgressMessage = jest.fn();
|
|
8045
|
+
(method as any).protocolV2SourceUpdateProcess = jest.fn();
|
|
7319
8046
|
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
const fileHash = Array.from(sha256(bytes), byte => byte.toString(16).padStart(2, '0')).join('');
|
|
7324
|
-
const resource = {
|
|
7325
|
-
required: false as const,
|
|
7326
|
-
target: 'RES' as const,
|
|
7327
|
-
files: [
|
|
8047
|
+
await (method as any).executeProtocolV2TransferPhase({
|
|
8048
|
+
installSources: [],
|
|
8049
|
+
resourceSources: [
|
|
7328
8050
|
{
|
|
7329
|
-
name: '
|
|
7330
|
-
|
|
7331
|
-
devicePath: 'vol0:/
|
|
7332
|
-
|
|
7333
|
-
|
|
8051
|
+
name: 'images.okpkg',
|
|
8052
|
+
source: { size: installedBinary.byteLength },
|
|
8053
|
+
devicePath: 'vol0:/bundles/images/images.okpkg',
|
|
8054
|
+
version: [1, 2, 3],
|
|
8055
|
+
payloadHash: '11'.repeat(64),
|
|
8056
|
+
headerHash: '22'.repeat(64),
|
|
7334
8057
|
},
|
|
7335
8058
|
],
|
|
7336
|
-
};
|
|
7337
|
-
const method = new FirmwareUpdateV4({
|
|
7338
|
-
id: 1,
|
|
7339
|
-
payload: {
|
|
7340
|
-
method: 'firmwareUpdateV4',
|
|
7341
|
-
platform: 'web',
|
|
7342
|
-
targetsToUpdate: ['resource'],
|
|
7343
|
-
},
|
|
7344
8059
|
});
|
|
7345
|
-
method.init();
|
|
7346
|
-
(method as any).device = stubDevice({ getCurrentDeviceType: () => 'pro2' });
|
|
7347
|
-
jest.spyOn(DataManager, 'getProtocolV2BootResources').mockReturnValue(resource);
|
|
7348
|
-
jest.spyOn(firmwareBinaryApi, 'getSysResourceBinary').mockResolvedValue({ binary });
|
|
7349
8060
|
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
devicePath: 'vol0:/assets/loaders/boot.staging/graphics/bootloader_crest.bin',
|
|
7355
|
-
},
|
|
7356
|
-
]);
|
|
8061
|
+
expect(typedCall).toHaveBeenCalledWith('FilesystemPathInfoQuery', 'FilesystemPathInfo', {
|
|
8062
|
+
path: 'vol0:/bundles/images/images.okpkg',
|
|
8063
|
+
});
|
|
8064
|
+
expect((method as any).protocolV2SourceUpdateProcess).not.toHaveBeenCalled();
|
|
7357
8065
|
});
|
|
7358
8066
|
|
|
7359
|
-
test('
|
|
8067
|
+
test('updates a resource when the installed file size differs despite matching headers', async () => {
|
|
7360
8068
|
const method = new FirmwareUpdateV4({
|
|
7361
8069
|
id: 1,
|
|
7362
8070
|
payload: {
|
|
7363
8071
|
method: 'firmwareUpdateV4',
|
|
7364
8072
|
platform: 'web',
|
|
7365
|
-
resourceFiles: [
|
|
7366
|
-
{
|
|
7367
|
-
binary: new Uint8Array([1, 2, 3]).buffer,
|
|
7368
|
-
devicePath: 'vol0:/assets/loaders/boot.staging/graphics/bootloader_crest.bin',
|
|
7369
|
-
size: 3,
|
|
7370
|
-
fileHash: '00'.repeat(32),
|
|
7371
|
-
},
|
|
7372
|
-
],
|
|
7373
8073
|
},
|
|
7374
8074
|
});
|
|
7375
8075
|
method.init();
|
|
8076
|
+
const installedBinary = new Uint8Array(createProtocolV2OkppBinary());
|
|
8077
|
+
const typedCall = jest.fn((requestType: string) => {
|
|
8078
|
+
if (requestType === 'FilesystemPathInfoQuery') {
|
|
8079
|
+
return Promise.resolve({
|
|
8080
|
+
message: { exist: true, directory: false, size: installedBinary.byteLength + 1 },
|
|
8081
|
+
});
|
|
8082
|
+
}
|
|
8083
|
+
throw new Error(`Unexpected request: ${requestType}`);
|
|
8084
|
+
});
|
|
8085
|
+
(method as any).device = stubDevice({
|
|
8086
|
+
getCommands: () => ({ typedCall }),
|
|
8087
|
+
});
|
|
7376
8088
|
|
|
7377
|
-
expect(
|
|
7378
|
-
|
|
7379
|
-
|
|
8089
|
+
await expect(
|
|
8090
|
+
(method as any).isProtocolV2ResourceBundleUpToDate({
|
|
8091
|
+
name: 'images.okpkg',
|
|
8092
|
+
source: { size: installedBinary.byteLength },
|
|
8093
|
+
devicePath: 'vol0:/bundles/images/images.okpkg',
|
|
8094
|
+
version: [1, 2, 3],
|
|
8095
|
+
payloadHash: '11'.repeat(64),
|
|
8096
|
+
headerHash: '22'.repeat(64),
|
|
8097
|
+
})
|
|
8098
|
+
).resolves.toBe(false);
|
|
8099
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
7380
8100
|
});
|
|
7381
8101
|
});
|
|
7382
8102
|
|