@onekeyfe/hd-core 1.2.0-alpha.97 → 1.2.0-alpha.99
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 +411 -86
- package/__tests__/check-firmware-release-protocol-v2.test.ts +199 -0
- package/__tests__/firmware-memory-host.test.ts +126 -0
- package/__tests__/firmware-update/device-update-bootloader.test.ts +14 -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-bootloader-poll.test.ts +65 -2
- package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +80 -0
- package/__tests__/firmware-update/firmware-update-plan.test.ts +293 -71
- 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-firmware-targets.test.ts +16 -0
- package/__tests__/protocol-v2-resources.test.ts +148 -232
- package/__tests__/protocol-v2.test.ts +1326 -510
- 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 +11 -9
- 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/FirmwareUpdateBaseMethod.d.ts.map +1 -1
- 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 +7 -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 +1825 -1006
- 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 +113 -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 +231 -138
- package/src/api/FirmwareUpdateV3.ts +101 -56
- package/src/api/FirmwareUpdateV4.ts +936 -408
- package/src/api/UploadPortfolio.ts +18 -0
- package/src/api/device/DeviceUpdateBootloader.ts +37 -11
- package/src/api/firmware/FirmwareHostBinding.ts +26 -3
- package/src/api/firmware/FirmwareMemoryHost.ts +143 -0
- package/src/api/firmware/FirmwarePreparedResourceArchive.ts +207 -0
- package/src/api/firmware/FirmwareUpdateBaseMethod.ts +122 -73
- package/src/api/firmware/FirmwareUpdatePlan.ts +295 -122
- package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +49 -12
- 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,72 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3967
4033
|
});
|
|
3968
4034
|
});
|
|
3969
4035
|
|
|
3970
|
-
test('
|
|
4036
|
+
test('filters unrequested binaries from a direct local Protocol V2 update', async () => {
|
|
4037
|
+
const applicationP1Binary = new Uint8Array([1, 2, 3]).buffer;
|
|
4038
|
+
const method = new FirmwareUpdateV4({
|
|
4039
|
+
id: 1,
|
|
4040
|
+
payload: {
|
|
4041
|
+
method: 'firmwareUpdateV4',
|
|
4042
|
+
platform: 'web',
|
|
4043
|
+
targetsToUpdate: ['app_v1'],
|
|
4044
|
+
applicationP1Binary,
|
|
4045
|
+
bootloaderBinary: new Uint8Array([4, 5, 6]).buffer,
|
|
4046
|
+
},
|
|
4047
|
+
});
|
|
4048
|
+
method.init();
|
|
4049
|
+
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
4050
|
+
(method as any).device = stubDevice({
|
|
4051
|
+
originalDescriptor: { protocolType: 'V2' },
|
|
4052
|
+
features: {
|
|
4053
|
+
deviceType: 'pro2',
|
|
4054
|
+
firmwareVersion: '1.0.0',
|
|
4055
|
+
capabilities: [],
|
|
4056
|
+
},
|
|
4057
|
+
getCurrentDeviceType: () => 'pro2',
|
|
4058
|
+
});
|
|
4059
|
+
(method as any).executeProtocolV2Update = jest.fn().mockResolvedValue('local-result');
|
|
4060
|
+
method.postTipMessage = jest.fn();
|
|
4061
|
+
|
|
4062
|
+
await expect(method.run()).resolves.toBe('local-result');
|
|
4063
|
+
expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
|
|
4064
|
+
bootloaderBinary: null,
|
|
4065
|
+
fwBinaryMap: [
|
|
4066
|
+
{
|
|
4067
|
+
fileName: 'application_p1.bin',
|
|
4068
|
+
binary: applicationP1Binary,
|
|
4069
|
+
targetId: 4,
|
|
4070
|
+
},
|
|
4071
|
+
],
|
|
4072
|
+
});
|
|
4073
|
+
});
|
|
4074
|
+
|
|
4075
|
+
test('keeps all local binaries when no target list was supplied', () => {
|
|
4076
|
+
const method = new FirmwareUpdateV4({
|
|
4077
|
+
id: 1,
|
|
4078
|
+
payload: {
|
|
4079
|
+
method: 'firmwareUpdateV4',
|
|
4080
|
+
platform: 'web',
|
|
4081
|
+
applicationP1Binary: new Uint8Array([1]).buffer,
|
|
4082
|
+
bootloaderBinary: new Uint8Array([2]).buffer,
|
|
4083
|
+
resourceArchiveBinary: new Uint8Array([3]).buffer,
|
|
4084
|
+
},
|
|
4085
|
+
});
|
|
4086
|
+
method.init();
|
|
4087
|
+
|
|
4088
|
+
const availableInstallItems = (method as any).buildProtocolV2InstallItems({
|
|
4089
|
+
bootloaderBinary: (method as any).prepareBootloaderBinary(),
|
|
4090
|
+
fwBinaryMap: (method as any).collectExplicitTargetBinaries(),
|
|
4091
|
+
});
|
|
4092
|
+
|
|
4093
|
+
expect((method as any).params.targetsToUpdate).toEqual(['resource']);
|
|
4094
|
+
expect(
|
|
4095
|
+
(method as any)
|
|
4096
|
+
.filterProtocolV2LocalInstallItems(availableInstallItems)
|
|
4097
|
+
.map((item: { targetId: number }) => item.targetId)
|
|
4098
|
+
).toEqual([3, 4]);
|
|
4099
|
+
});
|
|
4100
|
+
|
|
4101
|
+
test('requires the external host to prepare manifest resources before bootloader entry', async () => {
|
|
3971
4102
|
const method = new FirmwareUpdateV4({
|
|
3972
4103
|
id: 1,
|
|
3973
4104
|
payload: {
|
|
@@ -3987,55 +4118,21 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3987
4118
|
bootloaderMode: false,
|
|
3988
4119
|
capabilities: [],
|
|
3989
4120
|
},
|
|
4121
|
+
getCurrentDeviceType: () => 'pro2',
|
|
3990
4122
|
isBootloader: () => false,
|
|
3991
4123
|
isRomloader: () => false,
|
|
3992
4124
|
});
|
|
3993
4125
|
(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
|
-
});
|
|
4126
|
+
(method as any).enterProtocolV2BootloaderMode = jest.fn();
|
|
4029
4127
|
method.postTipMessage = jest.fn();
|
|
4030
4128
|
|
|
4031
|
-
await method.run()
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
]);
|
|
4129
|
+
await expect(method.run()).rejects.toMatchObject({
|
|
4130
|
+
errorCode: HardwareErrorCode.RuntimeError,
|
|
4131
|
+
params: expect.objectContaining({
|
|
4132
|
+
firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
|
|
4133
|
+
}),
|
|
4134
|
+
});
|
|
4135
|
+
expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
|
|
4039
4136
|
});
|
|
4040
4137
|
|
|
4041
4138
|
test('reboots Protocol V2 normal-mode device to bootloader before transfer', async () => {
|
|
@@ -4204,9 +4301,6 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4204
4301
|
},
|
|
4205
4302
|
});
|
|
4206
4303
|
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
4207
|
-
const prepareResourceBundles = jest
|
|
4208
|
-
.spyOn(method as any, 'prepareProtocolV2ResourceBundles')
|
|
4209
|
-
.mockResolvedValue(undefined);
|
|
4210
4304
|
(method as any).protocolV2Reboot = jest.fn();
|
|
4211
4305
|
(method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
|
|
4212
4306
|
(method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
|
|
@@ -4219,7 +4313,6 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4219
4313
|
|
|
4220
4314
|
await method.run();
|
|
4221
4315
|
|
|
4222
|
-
expect(prepareResourceBundles).not.toHaveBeenCalled();
|
|
4223
4316
|
expect((method as any).protocolV2Reboot).not.toHaveBeenCalled();
|
|
4224
4317
|
expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
|
|
4225
4318
|
bootloaderBinary: null,
|
|
@@ -5478,6 +5571,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5478
5571
|
return Number(params.processedSize ?? 0) + Number(params.source.size);
|
|
5479
5572
|
});
|
|
5480
5573
|
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
|
|
5574
|
+
(method as any).ensureProtocolV2BootResourceStagingIsEmpty = jest
|
|
5575
|
+
.fn()
|
|
5576
|
+
.mockResolvedValue(undefined);
|
|
5481
5577
|
(method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
|
|
5482
5578
|
(method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({});
|
|
5483
5579
|
(method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
|
|
@@ -5551,6 +5647,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5551
5647
|
Promise.resolve(Number(params.processedSize ?? 0) + Number(params.source.size))
|
|
5552
5648
|
);
|
|
5553
5649
|
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
|
|
5650
|
+
(method as any).ensureProtocolV2BootResourceStagingIsEmpty = jest
|
|
5651
|
+
.fn()
|
|
5652
|
+
.mockResolvedValue(undefined);
|
|
5554
5653
|
(method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
|
|
5555
5654
|
(method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
|
|
5556
5655
|
(method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
|
|
@@ -5558,21 +5657,33 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5558
5657
|
(method as any).waitForProtocolV2FirmwareUpdateComplete = jest
|
|
5559
5658
|
.fn()
|
|
5560
5659
|
.mockResolvedValue(undefined);
|
|
5660
|
+
(method as any).isProtocolV2ResourceBundleUpToDate = jest.fn().mockResolvedValue(false);
|
|
5561
5661
|
|
|
5562
|
-
await (method as any).
|
|
5563
|
-
|
|
5662
|
+
await (method as any).executeProtocolV2SourceUpdate({
|
|
5663
|
+
resourceSources: [
|
|
5564
5664
|
{
|
|
5565
5665
|
name: 'images.okpkg',
|
|
5566
|
-
|
|
5666
|
+
source: {
|
|
5667
|
+
size: 2,
|
|
5668
|
+
readAt: jest.fn(),
|
|
5669
|
+
close: jest.fn(),
|
|
5670
|
+
},
|
|
5567
5671
|
devicePath: 'vol0:/bundles/images/images.okpkg',
|
|
5672
|
+
version: [1, 2, 3],
|
|
5673
|
+
payloadHash: '11'.repeat(64),
|
|
5674
|
+
headerHash: '22'.repeat(64),
|
|
5568
5675
|
},
|
|
5569
5676
|
],
|
|
5570
|
-
|
|
5571
|
-
fwBinaryMap: [
|
|
5677
|
+
installSources: [
|
|
5572
5678
|
{
|
|
5573
5679
|
fileName: 'application_p1.bin',
|
|
5574
|
-
|
|
5680
|
+
source: {
|
|
5681
|
+
size: 1,
|
|
5682
|
+
readAt: jest.fn(),
|
|
5683
|
+
close: jest.fn(),
|
|
5684
|
+
},
|
|
5575
5685
|
targetId: 4,
|
|
5686
|
+
kind: 'firmware',
|
|
5576
5687
|
},
|
|
5577
5688
|
],
|
|
5578
5689
|
});
|
|
@@ -5589,6 +5700,100 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5589
5700
|
2,
|
|
5590
5701
|
expect.objectContaining({ processedSize: 0, totalSize: 1 })
|
|
5591
5702
|
);
|
|
5703
|
+
expect((method as any).isProtocolV2ResourceBundleUpToDate).toHaveBeenCalledWith(
|
|
5704
|
+
expect.objectContaining({
|
|
5705
|
+
version: [1, 2, 3],
|
|
5706
|
+
payloadHash: '11'.repeat(64),
|
|
5707
|
+
headerHash: '22'.repeat(64),
|
|
5708
|
+
})
|
|
5709
|
+
);
|
|
5710
|
+
});
|
|
5711
|
+
|
|
5712
|
+
test('installs and verifies bootloader before syncing manifest resources', () => {
|
|
5713
|
+
const method = new FirmwareUpdateV4({
|
|
5714
|
+
id: 1,
|
|
5715
|
+
payload: { method: 'firmwareUpdateV4' },
|
|
5716
|
+
});
|
|
5717
|
+
const phases = (method as any).buildProtocolV2ExecutionPhases({
|
|
5718
|
+
installSources: [{ kind: 'bootloader' }, { kind: 'component' }],
|
|
5719
|
+
resourceSources: [{ kind: 'resource' }],
|
|
5720
|
+
});
|
|
5721
|
+
|
|
5722
|
+
expect(phases.map((phase: { kind: string }) => phase.kind)).toEqual([
|
|
5723
|
+
'bootloader-install',
|
|
5724
|
+
'bootloader-verify',
|
|
5725
|
+
'resource-sync',
|
|
5726
|
+
'component-install',
|
|
5727
|
+
'final-verify',
|
|
5728
|
+
]);
|
|
5729
|
+
});
|
|
5730
|
+
|
|
5731
|
+
test('stages the mounted boot resource before any firmware install phase', () => {
|
|
5732
|
+
const method = new FirmwareUpdateV4({
|
|
5733
|
+
id: 1,
|
|
5734
|
+
payload: { method: 'firmwareUpdateV4' },
|
|
5735
|
+
});
|
|
5736
|
+
const bootResource = {
|
|
5737
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
5738
|
+
};
|
|
5739
|
+
const phases = (method as any).buildProtocolV2ExecutionPhases({
|
|
5740
|
+
installSources: [{ kind: 'bootloader' }, { kind: 'component' }],
|
|
5741
|
+
resourceSources: [bootResource, { devicePath: 'vol0:/resource/images/images.okpkg' }],
|
|
5742
|
+
});
|
|
5743
|
+
|
|
5744
|
+
expect(phases.map((phase: { kind: string }) => phase.kind)).toEqual([
|
|
5745
|
+
'resource-sync',
|
|
5746
|
+
'bootloader-install',
|
|
5747
|
+
'bootloader-verify',
|
|
5748
|
+
'resource-sync',
|
|
5749
|
+
'component-install',
|
|
5750
|
+
'final-verify',
|
|
5751
|
+
]);
|
|
5752
|
+
expect(phases[0].resourceSources).toEqual([bootResource]);
|
|
5753
|
+
});
|
|
5754
|
+
|
|
5755
|
+
test('removes stale boot resource staging before a component-only reboot', async () => {
|
|
5756
|
+
const method = new FirmwareUpdateV4({
|
|
5757
|
+
id: 1,
|
|
5758
|
+
payload: { method: 'firmwareUpdateV4' },
|
|
5759
|
+
});
|
|
5760
|
+
const events: string[] = [];
|
|
5761
|
+
let stagingExists = true;
|
|
5762
|
+
const typedCall = jest.fn((requestType: string) => {
|
|
5763
|
+
if (requestType === 'FilesystemPathInfoQuery') {
|
|
5764
|
+
return Promise.resolve({
|
|
5765
|
+
type: 'FilesystemPathInfo',
|
|
5766
|
+
message: { exist: stagingExists, directory: false, size: stagingExists ? 3 : 0 },
|
|
5767
|
+
});
|
|
5768
|
+
}
|
|
5769
|
+
if (requestType === 'FilesystemFileDelete') {
|
|
5770
|
+
events.push('delete-stale-staging');
|
|
5771
|
+
stagingExists = false;
|
|
5772
|
+
return Promise.resolve({ type: 'Success', message: {} });
|
|
5773
|
+
}
|
|
5774
|
+
throw new Error(`Unexpected request: ${requestType}`);
|
|
5775
|
+
});
|
|
5776
|
+
(method as any).device = stubDevice({ getCommands: () => ({ typedCall }) });
|
|
5777
|
+
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
|
|
5778
|
+
(method as any).executeProtocolV2TransferPhase = jest.fn().mockImplementation(() => {
|
|
5779
|
+
events.push('transfer-component');
|
|
5780
|
+
return Promise.resolve();
|
|
5781
|
+
});
|
|
5782
|
+
(method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockImplementation(() => {
|
|
5783
|
+
events.push('reboot-normal');
|
|
5784
|
+
return Promise.resolve();
|
|
5785
|
+
});
|
|
5786
|
+
(method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
|
|
5787
|
+
|
|
5788
|
+
await (method as any).executeProtocolV2SourceUpdate({
|
|
5789
|
+
installSources: [{ kind: 'firmware' }],
|
|
5790
|
+
resourceSources: [],
|
|
5791
|
+
});
|
|
5792
|
+
|
|
5793
|
+
expect(events).toEqual(['delete-stale-staging', 'transfer-component', 'reboot-normal']);
|
|
5794
|
+
expect(typedCall).toHaveBeenNthCalledWith(2, 'FilesystemFileDelete', 'Success', {
|
|
5795
|
+
path: 'vol0:/loaders/bootloader/boot_resource.okpkg.staging',
|
|
5796
|
+
});
|
|
5592
5797
|
});
|
|
5593
5798
|
|
|
5594
5799
|
test('does not request installation when the staged file size does not match', async () => {
|
|
@@ -5603,6 +5808,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5603
5808
|
method.postProgressMessage = jest.fn();
|
|
5604
5809
|
(method as any).protocolV2SourceUpdateProcess = jest.fn().mockResolvedValue(3);
|
|
5605
5810
|
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
|
|
5811
|
+
(method as any).ensureProtocolV2BootResourceStagingIsEmpty = jest
|
|
5812
|
+
.fn()
|
|
5813
|
+
.mockResolvedValue(undefined);
|
|
5606
5814
|
(method as any).device = stubDevice({
|
|
5607
5815
|
getCommands: () => ({
|
|
5608
5816
|
typedCall: jest.fn().mockResolvedValue({
|
|
@@ -5654,6 +5862,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5654
5862
|
Promise.resolve(Number(params.processedSize ?? 0) + Number(params.source.size))
|
|
5655
5863
|
);
|
|
5656
5864
|
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
|
|
5865
|
+
(method as any).ensureProtocolV2BootResourceStagingIsEmpty = jest
|
|
5866
|
+
.fn()
|
|
5867
|
+
.mockResolvedValue(undefined);
|
|
5657
5868
|
(method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
|
|
5658
5869
|
(method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
|
|
5659
5870
|
(method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
|
|
@@ -5733,6 +5944,14 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5733
5944
|
binary: binaries.get(url) ?? new Uint8Array([0]).buffer,
|
|
5734
5945
|
})
|
|
5735
5946
|
);
|
|
5947
|
+
const componentIntegrity = (url: string) => {
|
|
5948
|
+
const binary = binaries.get(url);
|
|
5949
|
+
if (!binary) throw new Error(`Missing test firmware binary: ${url}`);
|
|
5950
|
+
return {
|
|
5951
|
+
expectedSize: binary.byteLength,
|
|
5952
|
+
fingerprint: bytesToHex(sha256(new Uint8Array(binary))),
|
|
5953
|
+
};
|
|
5954
|
+
};
|
|
5736
5955
|
const getFirmwareLatestReleaseSpy = jest
|
|
5737
5956
|
.spyOn(DataManager, 'getFirmwareLatestRelease')
|
|
5738
5957
|
.mockReturnValue({
|
|
@@ -5757,23 +5976,43 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5757
5976
|
bootloader: {
|
|
5758
5977
|
target: 'BOOTLOADER',
|
|
5759
5978
|
url: 'https://example.com/bootloader.pp.bin',
|
|
5979
|
+
...componentIntegrity('https://example.com/bootloader.pp.bin'),
|
|
5760
5980
|
},
|
|
5761
5981
|
applicationP1: {
|
|
5762
5982
|
target: 'APPLICATION_P1',
|
|
5763
5983
|
url: 'https://example.com/applicationP1.pp.bin',
|
|
5984
|
+
...componentIntegrity('https://example.com/applicationP1.pp.bin'),
|
|
5764
5985
|
},
|
|
5765
5986
|
applicationP2: {
|
|
5766
5987
|
target: 'APPLICATION_P2',
|
|
5767
5988
|
url: 'https://example.com/applicationP2.pp.bin',
|
|
5989
|
+
...componentIntegrity('https://example.com/applicationP2.pp.bin'),
|
|
5768
5990
|
},
|
|
5769
5991
|
coprocessor: {
|
|
5770
5992
|
target: 'COPROCESSOR',
|
|
5771
5993
|
url: 'https://example.com/coprocessor.pp.bin',
|
|
5994
|
+
...componentIntegrity('https://example.com/coprocessor.pp.bin'),
|
|
5995
|
+
},
|
|
5996
|
+
se01: {
|
|
5997
|
+
target: 'SE01',
|
|
5998
|
+
url: 'https://example.com/se01.pp.bin',
|
|
5999
|
+
...componentIntegrity('https://example.com/se01.pp.bin'),
|
|
6000
|
+
},
|
|
6001
|
+
se02: {
|
|
6002
|
+
target: 'SE02',
|
|
6003
|
+
url: 'https://example.com/se02.pp.bin',
|
|
6004
|
+
...componentIntegrity('https://example.com/se02.pp.bin'),
|
|
6005
|
+
},
|
|
6006
|
+
se03: {
|
|
6007
|
+
target: 'SE03',
|
|
6008
|
+
url: 'https://example.com/se03.pp.bin',
|
|
6009
|
+
...componentIntegrity('https://example.com/se03.pp.bin'),
|
|
6010
|
+
},
|
|
6011
|
+
se04: {
|
|
6012
|
+
target: 'SE04',
|
|
6013
|
+
url: 'https://example.com/se04.pp.bin',
|
|
6014
|
+
...componentIntegrity('https://example.com/se04.pp.bin'),
|
|
5772
6015
|
},
|
|
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
6016
|
},
|
|
5778
6017
|
fingerprint: '',
|
|
5779
6018
|
changelog: {
|
|
@@ -5814,6 +6053,84 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5814
6053
|
getFirmwareLatestReleaseSpy.mockRestore();
|
|
5815
6054
|
});
|
|
5816
6055
|
|
|
6056
|
+
test('downloads only firmware targets that are missing from an explicit mixed payload', async () => {
|
|
6057
|
+
const explicitApplicationBinary = new Uint8Array([1]).buffer;
|
|
6058
|
+
const remoteCoprocessorBinary = new Uint8Array([2]).buffer;
|
|
6059
|
+
const method = new FirmwareUpdateV4({
|
|
6060
|
+
id: 1,
|
|
6061
|
+
payload: {
|
|
6062
|
+
method: 'firmwareUpdateV4',
|
|
6063
|
+
platform: 'web',
|
|
6064
|
+
targetsToUpdate: ['app_v1', 'coprocessor'],
|
|
6065
|
+
},
|
|
6066
|
+
});
|
|
6067
|
+
method.init();
|
|
6068
|
+
|
|
6069
|
+
const getSysResourceBinarySpy = jest
|
|
6070
|
+
.spyOn(firmwareBinaryApi, 'getSysResourceBinary')
|
|
6071
|
+
.mockResolvedValue({ binary: remoteCoprocessorBinary });
|
|
6072
|
+
const getFirmwareLatestReleaseSpy = jest
|
|
6073
|
+
.spyOn(DataManager, 'getFirmwareLatestRelease')
|
|
6074
|
+
.mockReturnValue({
|
|
6075
|
+
required: false,
|
|
6076
|
+
version: [1, 0, 0],
|
|
6077
|
+
url: 'https://example.com/applicationP1.pp.bin',
|
|
6078
|
+
installOrder: ['applicationP1', 'coprocessor'],
|
|
6079
|
+
components: {
|
|
6080
|
+
applicationP1: {
|
|
6081
|
+
target: 'APPLICATION_P1',
|
|
6082
|
+
url: 'https://example.com/applicationP1.pp.bin',
|
|
6083
|
+
expectedSize: explicitApplicationBinary.byteLength,
|
|
6084
|
+
fingerprint: bytesToHex(sha256(new Uint8Array(explicitApplicationBinary))),
|
|
6085
|
+
},
|
|
6086
|
+
coprocessor: {
|
|
6087
|
+
target: 'COPROCESSOR',
|
|
6088
|
+
url: 'https://example.com/coprocessor.pp.bin',
|
|
6089
|
+
expectedSize: remoteCoprocessorBinary.byteLength,
|
|
6090
|
+
fingerprint: bytesToHex(sha256(new Uint8Array(remoteCoprocessorBinary))),
|
|
6091
|
+
},
|
|
6092
|
+
},
|
|
6093
|
+
fingerprint: '',
|
|
6094
|
+
changelog: {
|
|
6095
|
+
'zh-CN': '',
|
|
6096
|
+
'en-US': '',
|
|
6097
|
+
},
|
|
6098
|
+
});
|
|
6099
|
+
|
|
6100
|
+
const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries(
|
|
6101
|
+
'universal',
|
|
6102
|
+
{ deviceType: 'pro2', firmwareVersion: '0.0.0' },
|
|
6103
|
+
[
|
|
6104
|
+
{
|
|
6105
|
+
fileName: 'application_p1.bin',
|
|
6106
|
+
binary: explicitApplicationBinary,
|
|
6107
|
+
targetId: 4,
|
|
6108
|
+
kind: 'firmware',
|
|
6109
|
+
},
|
|
6110
|
+
]
|
|
6111
|
+
);
|
|
6112
|
+
|
|
6113
|
+
expect(getSysResourceBinarySpy).toHaveBeenCalledTimes(1);
|
|
6114
|
+
expect(getSysResourceBinarySpy).toHaveBeenCalledWith('https://example.com/coprocessor.pp.bin');
|
|
6115
|
+
expect(remoteBinaries.installItems).toEqual([
|
|
6116
|
+
{
|
|
6117
|
+
fileName: 'application_p1.bin',
|
|
6118
|
+
binary: explicitApplicationBinary,
|
|
6119
|
+
targetId: 4,
|
|
6120
|
+
kind: 'firmware',
|
|
6121
|
+
},
|
|
6122
|
+
{
|
|
6123
|
+
fileName: 'coprocessor.bin',
|
|
6124
|
+
binary: remoteCoprocessorBinary,
|
|
6125
|
+
targetId: 6,
|
|
6126
|
+
kind: 'firmware',
|
|
6127
|
+
},
|
|
6128
|
+
]);
|
|
6129
|
+
|
|
6130
|
+
getSysResourceBinarySpy.mockRestore();
|
|
6131
|
+
getFirmwareLatestReleaseSpy.mockRestore();
|
|
6132
|
+
});
|
|
6133
|
+
|
|
5817
6134
|
test('ignores an unsupported remote ROMLOADER component when installing selected targets', async () => {
|
|
5818
6135
|
const method = new FirmwareUpdateV4({
|
|
5819
6136
|
id: 1,
|
|
@@ -5978,56 +6295,721 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5978
6295
|
});
|
|
5979
6296
|
});
|
|
5980
6297
|
|
|
5981
|
-
test('
|
|
6298
|
+
test('maps a prepared resource archive manifest to artifact-backed resource sources', async () => {
|
|
5982
6299
|
const method = new FirmwareUpdateV4({
|
|
5983
6300
|
id: 1,
|
|
5984
6301
|
payload: {
|
|
5985
6302
|
method: 'firmwareUpdateV4',
|
|
5986
6303
|
},
|
|
5987
6304
|
});
|
|
5988
|
-
const
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
6305
|
+
const imagesBinary = createProtocolV2OkppBinary();
|
|
6306
|
+
const bootBinary = createProtocolV2OkppBinary({ payloadHashByte: 0x33 });
|
|
6307
|
+
const imagesSha256 = bytesToHex(sha256(new Uint8Array(imagesBinary)));
|
|
6308
|
+
const bootSha256 = bytesToHex(sha256(new Uint8Array(bootBinary)));
|
|
6309
|
+
const manifest = {
|
|
6310
|
+
schema: 1,
|
|
6311
|
+
artifact_name: 'pro2-resource',
|
|
6312
|
+
release_name: 'test-release',
|
|
6313
|
+
variant: 'resource',
|
|
6314
|
+
commit: 'abc123',
|
|
6315
|
+
short_sha: 'abc123',
|
|
6316
|
+
timestamp_utc: '2026-08-09T00:00:00Z',
|
|
6317
|
+
core_version: '1.0.0',
|
|
6318
|
+
key_set: 'production',
|
|
6319
|
+
device_root: 'vol0:',
|
|
6320
|
+
restore_mode: 'bootloader_update',
|
|
6321
|
+
trees: [{ path: 'bundles', device: 'pro2' }],
|
|
6322
|
+
files: [
|
|
5992
6323
|
{
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
6324
|
+
archive_path: 'bundles/images/images.okpkg',
|
|
6325
|
+
original_name: 'images.okpkg',
|
|
6326
|
+
device_path: 'vol0:/bundles/images/images.okpkg',
|
|
6327
|
+
size: imagesBinary.byteLength,
|
|
6328
|
+
sha256: imagesSha256,
|
|
6329
|
+
signed: true,
|
|
6330
|
+
sig_algo: 'ed25519',
|
|
6331
|
+
payload_version: '1.2.3',
|
|
6332
|
+
},
|
|
6333
|
+
{
|
|
6334
|
+
archive_path: 'loaders/bootloader/boot_resource.okpkg',
|
|
6335
|
+
original_name: 'boot_resource.okpkg',
|
|
6336
|
+
device_path: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
6337
|
+
size: bootBinary.byteLength,
|
|
6338
|
+
sha256: bootSha256,
|
|
6339
|
+
signed: true,
|
|
6340
|
+
sig_algo: 'ed25519',
|
|
6341
|
+
payload_version: '1.2.3',
|
|
5999
6342
|
},
|
|
6000
6343
|
],
|
|
6001
6344
|
};
|
|
6002
|
-
|
|
6003
|
-
const
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
],
|
|
6016
|
-
fingerprint: '',
|
|
6017
|
-
changelog: {
|
|
6018
|
-
'zh-CN': '',
|
|
6019
|
-
'en-US': '',
|
|
6345
|
+
const manifestBinary = new TextEncoder().encode(JSON.stringify(manifest)).buffer;
|
|
6346
|
+
const zip = new JSZip();
|
|
6347
|
+
zip.file('manifest.json', manifestBinary);
|
|
6348
|
+
zip.file('bundles/images/images.okpkg', imagesBinary);
|
|
6349
|
+
zip.file('loaders/bootloader/boot_resource.okpkg', bootBinary);
|
|
6350
|
+
const archiveBinary = await zip.generateAsync({ type: 'arraybuffer' });
|
|
6351
|
+
const preparedEntries = [
|
|
6352
|
+
{
|
|
6353
|
+
entryName: 'manifest.json',
|
|
6354
|
+
artifact: {
|
|
6355
|
+
artifactRef: 'manifest',
|
|
6356
|
+
size: manifestBinary.byteLength,
|
|
6357
|
+
sha256: bytesToHex(sha256(new Uint8Array(manifestBinary))),
|
|
6020
6358
|
},
|
|
6021
|
-
}
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6359
|
+
},
|
|
6360
|
+
{
|
|
6361
|
+
entryName: 'bundles/images/images.okpkg',
|
|
6362
|
+
artifact: {
|
|
6363
|
+
artifactRef: 'images',
|
|
6364
|
+
size: imagesBinary.byteLength,
|
|
6365
|
+
sha256: imagesSha256,
|
|
6366
|
+
},
|
|
6367
|
+
},
|
|
6368
|
+
{
|
|
6369
|
+
entryName: 'loaders/bootloader/boot_resource.okpkg',
|
|
6370
|
+
artifact: {
|
|
6371
|
+
artifactRef: 'boot',
|
|
6372
|
+
size: bootBinary.byteLength,
|
|
6373
|
+
sha256: bootSha256,
|
|
6374
|
+
},
|
|
6375
|
+
},
|
|
6376
|
+
];
|
|
6377
|
+
const artifacts = new Map([['archive', archiveBinary]]);
|
|
6378
|
+
(method as any).params = {
|
|
6379
|
+
targetsToUpdate: ['resource'],
|
|
6380
|
+
preparedPlan: {
|
|
6381
|
+
artifacts: [
|
|
6382
|
+
{
|
|
6383
|
+
artifactId: 'resource:archive',
|
|
6384
|
+
role: 'resourceBundle',
|
|
6385
|
+
target: 'resource',
|
|
6386
|
+
container: 'zip',
|
|
6387
|
+
artifact: {
|
|
6388
|
+
artifactRef: 'archive',
|
|
6389
|
+
size: archiveBinary.byteLength,
|
|
6390
|
+
sha256: bytesToHex(sha256(new Uint8Array(archiveBinary))),
|
|
6391
|
+
},
|
|
6392
|
+
materializedEntries: preparedEntries,
|
|
6393
|
+
},
|
|
6394
|
+
],
|
|
6395
|
+
},
|
|
6396
|
+
};
|
|
6397
|
+
(method as any).openProtocolV2PreparedSource = jest.fn(async artifact => {
|
|
6398
|
+
const binary = artifacts.get(artifact.artifactRef);
|
|
6399
|
+
if (!binary) throw new Error(`Unknown artifact: ${artifact.artifactRef}`);
|
|
6400
|
+
return openFirmwareByteSource({ binary });
|
|
6401
|
+
});
|
|
6402
|
+
|
|
6403
|
+
const sources = await (method as any).prepareProtocolV2ResourceArchiveSources();
|
|
6404
|
+
|
|
6405
|
+
expect(sources).toHaveLength(2);
|
|
6406
|
+
expect(sources).toEqual([
|
|
6407
|
+
expect.objectContaining({
|
|
6408
|
+
name: 'images.okpkg',
|
|
6409
|
+
devicePath: 'vol0:/bundles/images/images.okpkg',
|
|
6410
|
+
version: [1, 2, 3],
|
|
6411
|
+
}),
|
|
6412
|
+
expect.objectContaining({
|
|
6413
|
+
name: 'boot_resource.okpkg',
|
|
6414
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
6415
|
+
version: [1, 2, 3],
|
|
6416
|
+
}),
|
|
6417
|
+
]);
|
|
6418
|
+
await Promise.all(sources.map(source => source.source.close()));
|
|
6419
|
+
|
|
6420
|
+
preparedEntries[1].artifact.sha256 = 'f'.repeat(64);
|
|
6421
|
+
await expect((method as any).prepareProtocolV2ResourceArchiveSources()).rejects.toThrow(
|
|
6422
|
+
'entries do not match the approved archive'
|
|
6423
|
+
);
|
|
6424
|
+
});
|
|
6425
|
+
|
|
6426
|
+
test('binds a prepared resource archive to the selected host generation', () => {
|
|
6427
|
+
const planWithoutDigest = {
|
|
6428
|
+
schemaVersion: 2 as const,
|
|
6429
|
+
executor: 'v4' as const,
|
|
6430
|
+
deviceIdentity: 'PRO2-SERIAL',
|
|
6431
|
+
deviceModel: 'pro2',
|
|
6432
|
+
firmwareType: EFirmwareType.Universal,
|
|
6433
|
+
platform: 'web' as const,
|
|
6434
|
+
targetsToUpdate: ['resource' as const],
|
|
6435
|
+
artifacts: [
|
|
6436
|
+
{
|
|
6437
|
+
artifactId: 'resource:archive',
|
|
6438
|
+
role: 'resourceBundle' as const,
|
|
6439
|
+
target: 'resource' as const,
|
|
6440
|
+
url: 'https://example.com/resource.zip',
|
|
6441
|
+
container: 'zip' as const,
|
|
6442
|
+
logicalName: 'protocol-v2-resource-archive',
|
|
6443
|
+
expectedSize: 3,
|
|
6444
|
+
expectedSha256: 'a'.repeat(64),
|
|
6445
|
+
},
|
|
6446
|
+
],
|
|
6447
|
+
};
|
|
6448
|
+
const plan = {
|
|
6449
|
+
...planWithoutDigest,
|
|
6450
|
+
planDigest: digestFirmwareUpdateContract(planWithoutDigest),
|
|
6451
|
+
};
|
|
6452
|
+
const preparedPlan = prepareFirmwareUpdatePlan({
|
|
6453
|
+
plan,
|
|
6454
|
+
leaseRef: 'resource-archive-test',
|
|
6455
|
+
artifacts: [
|
|
6456
|
+
{
|
|
6457
|
+
artifactId: 'resource:archive',
|
|
6458
|
+
artifact: {
|
|
6459
|
+
artifactRef: 'resource-archive',
|
|
6460
|
+
size: 3,
|
|
6461
|
+
sha256: 'a'.repeat(64),
|
|
6462
|
+
},
|
|
6463
|
+
materializedEntries: [
|
|
6464
|
+
{
|
|
6465
|
+
entryName: 'manifest.json',
|
|
6466
|
+
artifact: {
|
|
6467
|
+
artifactRef: 'resource-manifest',
|
|
6468
|
+
size: 2,
|
|
6469
|
+
sha256: 'b'.repeat(64),
|
|
6470
|
+
},
|
|
6471
|
+
},
|
|
6472
|
+
],
|
|
6473
|
+
},
|
|
6474
|
+
],
|
|
6475
|
+
});
|
|
6476
|
+
const mismatchedHostBindingGeneration = registerFirmwareUpdateHostBinding({
|
|
6477
|
+
preparedPlanDigest: 'c'.repeat(64),
|
|
6478
|
+
artifactReader: {
|
|
6479
|
+
open: jest.fn(),
|
|
6480
|
+
read: jest.fn(),
|
|
6481
|
+
close: jest.fn(),
|
|
6482
|
+
},
|
|
6483
|
+
});
|
|
6484
|
+
try {
|
|
6485
|
+
const mismatchedMethod = new FirmwareUpdateV4({
|
|
6486
|
+
id: 1,
|
|
6487
|
+
payload: {
|
|
6488
|
+
method: 'firmwareUpdateV4',
|
|
6489
|
+
platform: 'web',
|
|
6490
|
+
targetsToUpdate: ['resource'],
|
|
6491
|
+
preparedPlan,
|
|
6492
|
+
hostBindingGeneration: mismatchedHostBindingGeneration,
|
|
6493
|
+
},
|
|
6494
|
+
});
|
|
6495
|
+
|
|
6496
|
+
expect(() => mismatchedMethod.init()).toThrow('does not match the prepared plan');
|
|
6497
|
+
} finally {
|
|
6498
|
+
unregisterFirmwareUpdateHostBinding(mismatchedHostBindingGeneration);
|
|
6499
|
+
}
|
|
6500
|
+
const hostBindingGeneration = registerFirmwareUpdateHostBinding({
|
|
6501
|
+
preparedPlanDigest: preparedPlan.preparedPlanDigest,
|
|
6502
|
+
artifactReader: {
|
|
6503
|
+
open: jest.fn(),
|
|
6504
|
+
read: jest.fn(),
|
|
6505
|
+
close: jest.fn(),
|
|
6506
|
+
},
|
|
6507
|
+
});
|
|
6508
|
+
try {
|
|
6509
|
+
const method = new FirmwareUpdateV4({
|
|
6510
|
+
id: 1,
|
|
6511
|
+
payload: {
|
|
6512
|
+
method: 'firmwareUpdateV4',
|
|
6513
|
+
platform: 'web',
|
|
6514
|
+
targetsToUpdate: ['resource'],
|
|
6515
|
+
preparedPlan,
|
|
6516
|
+
hostBindingGeneration,
|
|
6517
|
+
},
|
|
6518
|
+
});
|
|
6519
|
+
|
|
6520
|
+
expect(() => method.init()).not.toThrow();
|
|
6521
|
+
} finally {
|
|
6522
|
+
unregisterFirmwareUpdateHostBinding(hostBindingGeneration);
|
|
6523
|
+
}
|
|
6524
|
+
});
|
|
6525
|
+
|
|
6526
|
+
test('executes a prepared component plan without duplicated component inputs', async () => {
|
|
6527
|
+
const componentBinary = new Uint8Array([1, 2, 3]).buffer;
|
|
6528
|
+
const componentArtifact = {
|
|
6529
|
+
artifactRef: 'prepared-bootloader',
|
|
6530
|
+
size: componentBinary.byteLength,
|
|
6531
|
+
sha256: 'a'.repeat(64),
|
|
6532
|
+
};
|
|
6533
|
+
const planWithoutDigest = {
|
|
6534
|
+
schemaVersion: 2 as const,
|
|
6535
|
+
executor: 'v4' as const,
|
|
6536
|
+
deviceIdentity: 'PRO2-SERIAL',
|
|
6537
|
+
deviceModel: 'pro2',
|
|
6538
|
+
firmwareType: EFirmwareType.Universal,
|
|
6539
|
+
platform: 'web' as const,
|
|
6540
|
+
targetsToUpdate: ['boot' as const],
|
|
6541
|
+
artifacts: [
|
|
6542
|
+
{
|
|
6543
|
+
artifactId: 'component:boot',
|
|
6544
|
+
role: 'component' as const,
|
|
6545
|
+
target: 'boot' as const,
|
|
6546
|
+
url: 'https://example.com/bootloader.bin',
|
|
6547
|
+
container: 'raw' as const,
|
|
6548
|
+
logicalName: 'bootloader',
|
|
6549
|
+
expectedSize: componentArtifact.size,
|
|
6550
|
+
expectedSha256: componentArtifact.sha256,
|
|
6551
|
+
targetVersion: '1.2.3',
|
|
6552
|
+
},
|
|
6553
|
+
],
|
|
6554
|
+
};
|
|
6555
|
+
const preparedPlan = prepareFirmwareUpdatePlan({
|
|
6556
|
+
plan: {
|
|
6557
|
+
...planWithoutDigest,
|
|
6558
|
+
planDigest: digestFirmwareUpdateContract(planWithoutDigest),
|
|
6559
|
+
},
|
|
6560
|
+
leaseRef: 'prepared-component-test',
|
|
6561
|
+
artifacts: [{ artifactId: 'component:boot', artifact: componentArtifact }],
|
|
6562
|
+
});
|
|
6563
|
+
const artifactReader = {
|
|
6564
|
+
open: jest.fn(() =>
|
|
6565
|
+
Promise.resolve({
|
|
6566
|
+
readerId: 'prepared-component-reader',
|
|
6567
|
+
size: componentBinary.byteLength,
|
|
6568
|
+
})
|
|
6569
|
+
),
|
|
6570
|
+
read: jest.fn(({ offset, length }: { offset: number; length: number }) => {
|
|
6571
|
+
const data = componentBinary.slice(offset, offset + length);
|
|
6572
|
+
return Promise.resolve({
|
|
6573
|
+
data,
|
|
6574
|
+
bytesRead: data.byteLength,
|
|
6575
|
+
eof: offset + length === componentBinary.byteLength,
|
|
6576
|
+
});
|
|
6577
|
+
}),
|
|
6578
|
+
close: jest.fn(() => Promise.resolve()),
|
|
6579
|
+
};
|
|
6580
|
+
const hostBindingGeneration = registerFirmwareUpdateHostBinding({
|
|
6581
|
+
preparedPlanDigest: preparedPlan.preparedPlanDigest,
|
|
6582
|
+
artifactReader,
|
|
6583
|
+
});
|
|
6584
|
+
const releaseSpy = jest
|
|
6585
|
+
.spyOn(DataManager, 'getFirmwareLatestRelease')
|
|
6586
|
+
.mockReturnValue(undefined);
|
|
6587
|
+
|
|
6588
|
+
try {
|
|
6589
|
+
const method = new FirmwareUpdateV4({
|
|
6590
|
+
id: 1,
|
|
6591
|
+
payload: {
|
|
6592
|
+
method: 'firmwareUpdateV4',
|
|
6593
|
+
platform: 'web',
|
|
6594
|
+
preparedPlan,
|
|
6595
|
+
hostBindingGeneration,
|
|
6596
|
+
},
|
|
6597
|
+
});
|
|
6598
|
+
|
|
6599
|
+
expect(() => method.init()).not.toThrow();
|
|
6600
|
+
expect((method as any).params.targetsToUpdate).toEqual(['boot']);
|
|
6601
|
+
expect((method as any).params.expectedTargetVersions).toEqual({ boot: '1.2.3' });
|
|
6602
|
+
expect((method as any).params.componentArtifacts).toBeUndefined();
|
|
6603
|
+
(method as any).postTipMessage = jest.fn();
|
|
6604
|
+
(method as any).prepareProtocolV2ResourceSources = jest.fn().mockResolvedValue([]);
|
|
6605
|
+
(method as any).executeProtocolV2SourceUpdate = jest
|
|
6606
|
+
.fn()
|
|
6607
|
+
.mockResolvedValue('prepared-result');
|
|
6608
|
+
|
|
6609
|
+
await expect(
|
|
6610
|
+
(method as any).runProtocolV2PreparedArtifacts({} as Features, EFirmwareType.Universal)
|
|
6611
|
+
).resolves.toBe('prepared-result');
|
|
6612
|
+
expect(releaseSpy).not.toHaveBeenCalled();
|
|
6613
|
+
expect(artifactReader.open).toHaveBeenCalledWith({ artifactRef: 'prepared-bootloader' });
|
|
6614
|
+
expect((method as any).executeProtocolV2SourceUpdate).toHaveBeenCalledWith({
|
|
6615
|
+
installSources: [
|
|
6616
|
+
expect.objectContaining({
|
|
6617
|
+
fileName: 'bootloader.bin',
|
|
6618
|
+
kind: 'bootloader',
|
|
6619
|
+
}),
|
|
6620
|
+
],
|
|
6621
|
+
resourceSources: [],
|
|
6622
|
+
});
|
|
6623
|
+
} finally {
|
|
6624
|
+
releaseSpy.mockRestore();
|
|
6625
|
+
unregisterFirmwareUpdateHostBinding(hostBindingGeneration);
|
|
6626
|
+
}
|
|
6627
|
+
});
|
|
6628
|
+
|
|
6629
|
+
test('executes a resource-only prepared plan without a live firmware release', async () => {
|
|
6630
|
+
const planWithoutDigest = {
|
|
6631
|
+
schemaVersion: 2 as const,
|
|
6632
|
+
executor: 'v4' as const,
|
|
6633
|
+
deviceIdentity: 'PRO2-SERIAL',
|
|
6634
|
+
deviceModel: 'pro2',
|
|
6635
|
+
firmwareType: EFirmwareType.Universal,
|
|
6636
|
+
platform: 'web' as const,
|
|
6637
|
+
targetsToUpdate: ['resource' as const],
|
|
6638
|
+
artifacts: [
|
|
6639
|
+
{
|
|
6640
|
+
artifactId: 'resource:archive',
|
|
6641
|
+
role: 'resourceBundle' as const,
|
|
6642
|
+
target: 'resource' as const,
|
|
6643
|
+
url: 'https://example.com/resource.zip',
|
|
6644
|
+
container: 'zip' as const,
|
|
6645
|
+
logicalName: 'protocol-v2-resource-archive',
|
|
6646
|
+
expectedSize: 3,
|
|
6647
|
+
expectedSha256: 'a'.repeat(64),
|
|
6648
|
+
},
|
|
6649
|
+
],
|
|
6650
|
+
};
|
|
6651
|
+
const preparedPlan = prepareFirmwareUpdatePlan({
|
|
6652
|
+
plan: {
|
|
6653
|
+
...planWithoutDigest,
|
|
6654
|
+
planDigest: digestFirmwareUpdateContract(planWithoutDigest),
|
|
6655
|
+
},
|
|
6656
|
+
leaseRef: 'prepared-resource-only-test',
|
|
6657
|
+
artifacts: [
|
|
6658
|
+
{
|
|
6659
|
+
artifactId: 'resource:archive',
|
|
6660
|
+
artifact: {
|
|
6661
|
+
artifactRef: 'prepared-resource-archive',
|
|
6662
|
+
size: 3,
|
|
6663
|
+
sha256: 'a'.repeat(64),
|
|
6664
|
+
},
|
|
6665
|
+
materializedEntries: [
|
|
6666
|
+
{
|
|
6667
|
+
entryName: 'manifest.json',
|
|
6668
|
+
artifact: {
|
|
6669
|
+
artifactRef: 'prepared-resource-manifest',
|
|
6670
|
+
size: 2,
|
|
6671
|
+
sha256: 'b'.repeat(64),
|
|
6672
|
+
},
|
|
6673
|
+
},
|
|
6674
|
+
],
|
|
6675
|
+
},
|
|
6676
|
+
],
|
|
6677
|
+
});
|
|
6678
|
+
const hostBindingGeneration = registerFirmwareUpdateHostBinding({
|
|
6679
|
+
preparedPlanDigest: preparedPlan.preparedPlanDigest,
|
|
6680
|
+
artifactReader: {
|
|
6681
|
+
open: jest.fn(),
|
|
6682
|
+
read: jest.fn(),
|
|
6683
|
+
close: jest.fn(),
|
|
6684
|
+
},
|
|
6685
|
+
});
|
|
6686
|
+
const releaseSpy = jest
|
|
6687
|
+
.spyOn(DataManager, 'getFirmwareLatestRelease')
|
|
6688
|
+
.mockReturnValue(undefined);
|
|
6689
|
+
|
|
6690
|
+
try {
|
|
6691
|
+
const method = new FirmwareUpdateV4({
|
|
6692
|
+
id: 1,
|
|
6693
|
+
payload: {
|
|
6694
|
+
method: 'firmwareUpdateV4',
|
|
6695
|
+
platform: 'web',
|
|
6696
|
+
preparedPlan,
|
|
6697
|
+
hostBindingGeneration,
|
|
6698
|
+
},
|
|
6699
|
+
});
|
|
6700
|
+
|
|
6701
|
+
expect(() => method.init()).not.toThrow();
|
|
6702
|
+
(method as any).postTipMessage = jest.fn();
|
|
6703
|
+
const resourceSources = [{ kind: 'resource' }];
|
|
6704
|
+
(method as any).prepareProtocolV2ResourceSources = jest
|
|
6705
|
+
.fn()
|
|
6706
|
+
.mockResolvedValue(resourceSources);
|
|
6707
|
+
(method as any).executeProtocolV2SourceUpdate = jest
|
|
6708
|
+
.fn()
|
|
6709
|
+
.mockResolvedValue('resource-result');
|
|
6710
|
+
|
|
6711
|
+
await expect(
|
|
6712
|
+
(method as any).runProtocolV2PreparedArtifacts({} as Features, EFirmwareType.Universal)
|
|
6713
|
+
).resolves.toBe('resource-result');
|
|
6714
|
+
expect(releaseSpy).not.toHaveBeenCalled();
|
|
6715
|
+
expect((method as any).executeProtocolV2SourceUpdate).toHaveBeenCalledWith({
|
|
6716
|
+
installSources: [],
|
|
6717
|
+
resourceSources,
|
|
6718
|
+
});
|
|
6719
|
+
} finally {
|
|
6720
|
+
releaseSpy.mockRestore();
|
|
6721
|
+
unregisterFirmwareUpdateHostBinding(hostBindingGeneration);
|
|
6722
|
+
}
|
|
6723
|
+
});
|
|
6724
|
+
|
|
6725
|
+
test('converts a local resource ZIP into a local PreparedPlan without matching a remote release', async () => {
|
|
6726
|
+
const imagesBinary = new Uint8Array([1, 2, 3]).buffer;
|
|
6727
|
+
const bootResourceBinary = new Uint8Array([4, 5, 6]).buffer;
|
|
6728
|
+
const manifest = {
|
|
6729
|
+
schema: 1,
|
|
6730
|
+
artifact_name: 'pro2-resource',
|
|
6731
|
+
release_name: 'local-development',
|
|
6732
|
+
variant: 'resource',
|
|
6733
|
+
commit: 'local',
|
|
6734
|
+
short_sha: 'local',
|
|
6735
|
+
timestamp_utc: '2026-08-09T00:00:00Z',
|
|
6736
|
+
core_version: '1.0.0',
|
|
6737
|
+
key_set: 'development',
|
|
6738
|
+
device_root: 'vol0:',
|
|
6739
|
+
restore_mode: 'bootloader_update',
|
|
6740
|
+
trees: [{ path: 'bundles', device: 'pro2' }],
|
|
6741
|
+
files: [
|
|
6742
|
+
{
|
|
6743
|
+
archive_path: 'bundles/images/images.okpkg',
|
|
6744
|
+
original_name: 'images.okpkg',
|
|
6745
|
+
device_path: 'vol0:/bundles/images/images.okpkg',
|
|
6746
|
+
size: imagesBinary.byteLength,
|
|
6747
|
+
sha256: bytesToHex(sha256(new Uint8Array(imagesBinary))),
|
|
6748
|
+
signed: true,
|
|
6749
|
+
sig_algo: 'ed25519',
|
|
6750
|
+
payload_version: '1.0.0',
|
|
6751
|
+
},
|
|
6752
|
+
{
|
|
6753
|
+
archive_path: 'loaders/bootloader/boot_resource.okpkg',
|
|
6754
|
+
original_name: 'boot_resource.okpkg',
|
|
6755
|
+
device_path: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
6756
|
+
size: bootResourceBinary.byteLength,
|
|
6757
|
+
sha256: bytesToHex(sha256(new Uint8Array(bootResourceBinary))),
|
|
6758
|
+
signed: true,
|
|
6759
|
+
sig_algo: 'ed25519',
|
|
6760
|
+
payload_version: '1.0.0',
|
|
6761
|
+
},
|
|
6762
|
+
],
|
|
6763
|
+
};
|
|
6764
|
+
const zip = new JSZip();
|
|
6765
|
+
zip.file('manifest.json', JSON.stringify(manifest));
|
|
6766
|
+
zip.file('bundles/images/images.okpkg', imagesBinary);
|
|
6767
|
+
zip.file('loaders/bootloader/boot_resource.okpkg', bootResourceBinary);
|
|
6768
|
+
const resourceArchiveBinary = await zip.generateAsync({ type: 'arraybuffer' });
|
|
6769
|
+
const applicationP1Binary = new Uint8Array([7, 8, 9]).buffer;
|
|
6770
|
+
const bootloaderBinary = new Uint8Array([10, 11, 12]).buffer;
|
|
6771
|
+
const method = new FirmwareUpdateV4({
|
|
6772
|
+
id: 1,
|
|
6773
|
+
payload: {
|
|
6774
|
+
method: 'firmwareUpdateV4',
|
|
6775
|
+
platform: 'web',
|
|
6776
|
+
targetsToUpdate: ['app_v1'],
|
|
6777
|
+
applicationP1Binary,
|
|
6778
|
+
bootloaderBinary,
|
|
6779
|
+
resourceArchiveBinary,
|
|
6780
|
+
},
|
|
6781
|
+
});
|
|
6782
|
+
method.init();
|
|
6783
|
+
expect((method as any).params.targetsToUpdate).toEqual(['app_v1', 'resource']);
|
|
6784
|
+
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
6785
|
+
(method as any).postTipMessage = jest.fn();
|
|
6786
|
+
(method as any).device = stubDevice({
|
|
6787
|
+
originalDescriptor: { protocolType: 'V2' },
|
|
6788
|
+
features: {
|
|
6025
6789
|
deviceType: 'pro2',
|
|
6790
|
+
serialNo: 'pro2-device-id',
|
|
6791
|
+
firmwareVersion: '1.0.0',
|
|
6792
|
+
capabilities: [],
|
|
6793
|
+
},
|
|
6794
|
+
getCurrentDeviceType: () => 'pro2',
|
|
6795
|
+
});
|
|
6796
|
+
(method as any).protocolV2ExpectedSerialNumber = 'pro2-device-id';
|
|
6797
|
+
(method as any).executeProtocolV2SourceUpdate = jest
|
|
6798
|
+
.fn()
|
|
6799
|
+
.mockResolvedValue('local-resource-result');
|
|
6800
|
+
const releaseSpy = jest.spyOn(DataManager, 'getFirmwareLatestRelease');
|
|
6801
|
+
const reloadSpy = jest.spyOn(DataManager, 'forceReloadData');
|
|
6802
|
+
|
|
6803
|
+
try {
|
|
6804
|
+
await expect(method.run()).resolves.toBe('local-resource-result');
|
|
6805
|
+
expect(releaseSpy).not.toHaveBeenCalled();
|
|
6806
|
+
expect(reloadSpy).not.toHaveBeenCalled();
|
|
6807
|
+
expect((method as any).params.preparedPlan).toMatchObject({
|
|
6808
|
+
networkPolicy: 'forbid',
|
|
6809
|
+
executor: 'v4',
|
|
6810
|
+
targetsToUpdate: ['app_v1', 'resource'],
|
|
6811
|
+
});
|
|
6812
|
+
expect(
|
|
6813
|
+
(method as any).params.preparedPlan.artifacts.some(
|
|
6814
|
+
(artifact: { target: string }) => artifact.target === 'boot'
|
|
6815
|
+
)
|
|
6816
|
+
).toBe(false);
|
|
6817
|
+
expect((method as any).executeProtocolV2SourceUpdate).toHaveBeenCalledWith(
|
|
6818
|
+
expect.objectContaining({
|
|
6819
|
+
installSources: [
|
|
6820
|
+
expect.objectContaining({
|
|
6821
|
+
fileName: 'application_p1.bin',
|
|
6822
|
+
targetId: 4,
|
|
6823
|
+
}),
|
|
6824
|
+
],
|
|
6825
|
+
resourceSources: [
|
|
6826
|
+
expect.objectContaining({
|
|
6827
|
+
name: 'images.okpkg',
|
|
6828
|
+
devicePath: 'vol0:/bundles/images/images.okpkg',
|
|
6829
|
+
}),
|
|
6830
|
+
expect.objectContaining({
|
|
6831
|
+
name: 'boot_resource.okpkg',
|
|
6832
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
6833
|
+
}),
|
|
6834
|
+
],
|
|
6835
|
+
})
|
|
6836
|
+
);
|
|
6837
|
+
} finally {
|
|
6838
|
+
releaseSpy.mockRestore();
|
|
6839
|
+
reloadSpy.mockRestore();
|
|
6840
|
+
}
|
|
6841
|
+
});
|
|
6842
|
+
|
|
6843
|
+
test('rejects a local resource ZIP whose file hash does not match its manifest', async () => {
|
|
6844
|
+
const resourceBinary = new Uint8Array([1, 2, 3]).buffer;
|
|
6845
|
+
const bootResourceBinary = new Uint8Array([4, 5, 6]).buffer;
|
|
6846
|
+
const zip = new JSZip();
|
|
6847
|
+
zip.file(
|
|
6848
|
+
'manifest.json',
|
|
6849
|
+
JSON.stringify({
|
|
6850
|
+
schema: 1,
|
|
6851
|
+
artifact_name: 'pro2-resource',
|
|
6852
|
+
release_name: 'local-development',
|
|
6853
|
+
variant: 'resource',
|
|
6854
|
+
commit: 'local',
|
|
6855
|
+
short_sha: 'local',
|
|
6856
|
+
timestamp_utc: '2026-08-09T00:00:00Z',
|
|
6857
|
+
core_version: '1.0.0',
|
|
6858
|
+
key_set: 'development',
|
|
6859
|
+
device_root: 'vol0:',
|
|
6860
|
+
restore_mode: 'bootloader_update',
|
|
6861
|
+
trees: [{ path: 'bundles', device: 'pro2' }],
|
|
6862
|
+
files: [
|
|
6863
|
+
{
|
|
6864
|
+
archive_path: 'bundles/images/images.okpkg',
|
|
6865
|
+
original_name: 'images.okpkg',
|
|
6866
|
+
device_path: 'vol0:/bundles/images/images.okpkg',
|
|
6867
|
+
size: resourceBinary.byteLength,
|
|
6868
|
+
sha256: '0'.repeat(64),
|
|
6869
|
+
signed: true,
|
|
6870
|
+
sig_algo: 'ed25519',
|
|
6871
|
+
payload_version: '1.0.0',
|
|
6872
|
+
},
|
|
6873
|
+
{
|
|
6874
|
+
archive_path: 'loaders/bootloader/boot_resource.okpkg',
|
|
6875
|
+
original_name: 'boot_resource.okpkg',
|
|
6876
|
+
device_path: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
6877
|
+
size: bootResourceBinary.byteLength,
|
|
6878
|
+
sha256: bytesToHex(sha256(new Uint8Array(bootResourceBinary))),
|
|
6879
|
+
signed: true,
|
|
6880
|
+
sig_algo: 'ed25519',
|
|
6881
|
+
payload_version: '1.0.0',
|
|
6882
|
+
},
|
|
6883
|
+
],
|
|
6026
6884
|
})
|
|
6027
|
-
)
|
|
6028
|
-
|
|
6885
|
+
);
|
|
6886
|
+
zip.file('bundles/images/images.okpkg', resourceBinary);
|
|
6887
|
+
zip.file('loaders/bootloader/boot_resource.okpkg', bootResourceBinary);
|
|
6888
|
+
const method = new FirmwareUpdateV4({
|
|
6889
|
+
id: 1,
|
|
6890
|
+
payload: {
|
|
6891
|
+
method: 'firmwareUpdateV4',
|
|
6892
|
+
platform: 'web',
|
|
6893
|
+
targetsToUpdate: ['resource'],
|
|
6894
|
+
},
|
|
6895
|
+
});
|
|
6896
|
+
method.init();
|
|
6029
6897
|
|
|
6030
|
-
|
|
6898
|
+
await expect(
|
|
6899
|
+
(method as any).prepareProtocolV2LocalResourceArchive(
|
|
6900
|
+
await zip.generateAsync({ type: 'arraybuffer' })
|
|
6901
|
+
)
|
|
6902
|
+
).rejects.toThrow(
|
|
6903
|
+
'Protocol V2 local resource file does not match manifest: bundles/images/images.okpkg'
|
|
6904
|
+
);
|
|
6905
|
+
});
|
|
6906
|
+
|
|
6907
|
+
test('maps malformed resource ZIPs to a typed firmware preparation error', async () => {
|
|
6908
|
+
const loadSpy = jest.spyOn(JSZip, 'loadAsync').mockRejectedValue(new Error('corrupt ZIP'));
|
|
6909
|
+
const method = new FirmwareUpdateV4({
|
|
6910
|
+
id: 1,
|
|
6911
|
+
payload: {
|
|
6912
|
+
method: 'firmwareUpdateV4',
|
|
6913
|
+
platform: 'web',
|
|
6914
|
+
targetsToUpdate: ['resource'],
|
|
6915
|
+
},
|
|
6916
|
+
});
|
|
6917
|
+
method.init();
|
|
6918
|
+
|
|
6919
|
+
try {
|
|
6920
|
+
await expect(
|
|
6921
|
+
(method as any).prepareProtocolV2LocalResourceArchive(new Uint8Array([1]).buffer)
|
|
6922
|
+
).rejects.toMatchObject({
|
|
6923
|
+
errorCode: HardwareErrorCode.RuntimeError,
|
|
6924
|
+
params: { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' },
|
|
6925
|
+
});
|
|
6926
|
+
} finally {
|
|
6927
|
+
loadSpy.mockRestore();
|
|
6928
|
+
}
|
|
6929
|
+
});
|
|
6930
|
+
|
|
6931
|
+
test('rejects an oversized ZIP entry before allocating its decompressed bytes', async () => {
|
|
6932
|
+
const extractEntry = jest.fn();
|
|
6933
|
+
const loadSpy = jest.spyOn(JSZip, 'loadAsync').mockResolvedValue({
|
|
6934
|
+
files: {
|
|
6935
|
+
'manifest.json': {
|
|
6936
|
+
name: 'manifest.json',
|
|
6937
|
+
dir: false,
|
|
6938
|
+
_data: {
|
|
6939
|
+
compressedSize: 1,
|
|
6940
|
+
uncompressedSize: 2 * 1024 * 1024,
|
|
6941
|
+
},
|
|
6942
|
+
async: extractEntry,
|
|
6943
|
+
},
|
|
6944
|
+
},
|
|
6945
|
+
} as unknown as JSZip);
|
|
6946
|
+
const method = new FirmwareUpdateV4({
|
|
6947
|
+
id: 1,
|
|
6948
|
+
payload: {
|
|
6949
|
+
method: 'firmwareUpdateV4',
|
|
6950
|
+
platform: 'web',
|
|
6951
|
+
targetsToUpdate: ['resource'],
|
|
6952
|
+
},
|
|
6953
|
+
});
|
|
6954
|
+
method.init();
|
|
6955
|
+
|
|
6956
|
+
try {
|
|
6957
|
+
await expect(
|
|
6958
|
+
(method as any).prepareProtocolV2LocalResourceArchive(new Uint8Array([1]).buffer)
|
|
6959
|
+
).rejects.toThrow('declared size exceeds the allowed limit');
|
|
6960
|
+
expect(extractEntry).not.toHaveBeenCalled();
|
|
6961
|
+
} finally {
|
|
6962
|
+
loadSpy.mockRestore();
|
|
6963
|
+
}
|
|
6964
|
+
});
|
|
6965
|
+
|
|
6966
|
+
test('normalizes the deprecated boot_resources target to resource', () => {
|
|
6967
|
+
const method = new FirmwareUpdateV4({
|
|
6968
|
+
id: 1,
|
|
6969
|
+
payload: {
|
|
6970
|
+
method: 'firmwareUpdateV4',
|
|
6971
|
+
platform: 'web',
|
|
6972
|
+
targetsToUpdate: ['boot_resources'],
|
|
6973
|
+
},
|
|
6974
|
+
});
|
|
6975
|
+
|
|
6976
|
+
method.init();
|
|
6977
|
+
expect((method as any).params.targetsToUpdate).toEqual(['resource']);
|
|
6978
|
+
});
|
|
6979
|
+
|
|
6980
|
+
test('keeps the legacy componentArtifacts and artifactReader path without a prepared Plan', async () => {
|
|
6981
|
+
const artifactReader = {
|
|
6982
|
+
open: jest.fn(),
|
|
6983
|
+
read: jest.fn(),
|
|
6984
|
+
close: jest.fn(),
|
|
6985
|
+
};
|
|
6986
|
+
const method = new FirmwareUpdateV4({
|
|
6987
|
+
id: 1,
|
|
6988
|
+
payload: {
|
|
6989
|
+
method: 'firmwareUpdateV4',
|
|
6990
|
+
platform: 'web',
|
|
6991
|
+
targetsToUpdate: ['boot'],
|
|
6992
|
+
artifactReader,
|
|
6993
|
+
componentArtifacts: {
|
|
6994
|
+
boot: {
|
|
6995
|
+
artifactRef: 'legacy-bootloader',
|
|
6996
|
+
size: 3,
|
|
6997
|
+
sha256: 'a'.repeat(64),
|
|
6998
|
+
},
|
|
6999
|
+
},
|
|
7000
|
+
},
|
|
7001
|
+
});
|
|
7002
|
+
|
|
7003
|
+
expect(() => method.init()).not.toThrow();
|
|
7004
|
+
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
7005
|
+
(method as any).device = stubDevice({
|
|
7006
|
+
originalDescriptor: { protocolType: 'V2' },
|
|
7007
|
+
features: { deviceType: 'pro2', firmwareVersion: '1.0.0', capabilities: [] },
|
|
7008
|
+
});
|
|
7009
|
+
(method as any).runProtocolV2PreparedArtifacts = jest.fn().mockResolvedValue('legacy-result');
|
|
7010
|
+
|
|
7011
|
+
await expect(method.run()).resolves.toBe('legacy-result');
|
|
7012
|
+
expect((method as any).runProtocolV2PreparedArtifacts).toHaveBeenCalledTimes(1);
|
|
6031
7013
|
});
|
|
6032
7014
|
|
|
6033
7015
|
test('stops a remote update before reboot when the latest config cannot be refreshed', async () => {
|
|
@@ -6090,99 +7072,13 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6090
7072
|
await method.run();
|
|
6091
7073
|
|
|
6092
7074
|
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 }],
|
|
6096
|
-
});
|
|
6097
|
-
});
|
|
6098
|
-
|
|
6099
|
-
test.each(['resource', 'boot_resources'] as const)(
|
|
6100
|
-
'treats manual resource files as authoritative payload for %s',
|
|
6101
|
-
async target => {
|
|
6102
|
-
const resourceBundle = new Uint8Array([1, 2, 3]).buffer;
|
|
6103
|
-
const method = new FirmwareUpdateV4({
|
|
6104
|
-
id: 1,
|
|
6105
|
-
payload: {
|
|
6106
|
-
method: 'firmwareUpdateV4',
|
|
6107
|
-
platform: 'web',
|
|
6108
|
-
targetsToUpdate: [target],
|
|
6109
|
-
resourceFiles: [
|
|
6110
|
-
{
|
|
6111
|
-
binary: resourceBundle,
|
|
6112
|
-
devicePath: ' VOL0:/resource/images/images.okpkg ',
|
|
6113
|
-
},
|
|
6114
|
-
],
|
|
6115
|
-
},
|
|
6116
|
-
});
|
|
6117
|
-
method.init();
|
|
6118
|
-
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
6119
|
-
const bootResourcesSpy = jest.spyOn(DataManager, 'getProtocolV2BootResources');
|
|
6120
|
-
|
|
6121
|
-
(method as any).device = stubDevice({
|
|
6122
|
-
originalDescriptor: { protocolType: 'V2' },
|
|
6123
|
-
features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] },
|
|
6124
|
-
});
|
|
6125
|
-
(method as any).prepareRemoteProtocolV2Binaries = jest.fn();
|
|
6126
|
-
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true);
|
|
6127
|
-
(method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
|
|
6128
|
-
(method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
|
|
6129
|
-
(method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
|
|
6130
|
-
bootloaderVersion: '1.0.0',
|
|
6131
|
-
bleVersion: '0.0.0',
|
|
6132
|
-
firmwareVersion: '1.0.0',
|
|
6133
|
-
});
|
|
6134
|
-
method.postTipMessage = jest.fn();
|
|
6135
|
-
|
|
6136
|
-
await method.run();
|
|
6137
|
-
|
|
6138
|
-
expect(forceReloadDataSpy).not.toHaveBeenCalled();
|
|
6139
|
-
expect(bootResourcesSpy).not.toHaveBeenCalled();
|
|
6140
|
-
expect((method as any).prepareRemoteProtocolV2Binaries).not.toHaveBeenCalled();
|
|
6141
|
-
expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith(
|
|
6142
|
-
expect.objectContaining({
|
|
6143
|
-
resourceBundles: [
|
|
6144
|
-
{
|
|
6145
|
-
name: 'images.okpkg',
|
|
6146
|
-
binary: resourceBundle,
|
|
6147
|
-
devicePath: 'vol0:/resource/images/images.okpkg',
|
|
6148
|
-
},
|
|
6149
|
-
],
|
|
6150
|
-
})
|
|
6151
|
-
);
|
|
6152
|
-
}
|
|
6153
|
-
);
|
|
6154
|
-
|
|
6155
|
-
test('rejects manual RESC bundle paths before bootloader entry', async () => {
|
|
6156
|
-
const method = new FirmwareUpdateV4({
|
|
6157
|
-
id: 1,
|
|
6158
|
-
payload: {
|
|
6159
|
-
method: 'firmwareUpdateV4',
|
|
6160
|
-
platform: 'web',
|
|
6161
|
-
resourceFiles: [
|
|
6162
|
-
{
|
|
6163
|
-
binary: new Uint8Array([1, 2, 3]).buffer,
|
|
6164
|
-
devicePath: 'vol2:/resource/images/images.okpkg',
|
|
6165
|
-
},
|
|
6166
|
-
],
|
|
6167
|
-
},
|
|
6168
|
-
});
|
|
6169
|
-
method.init();
|
|
6170
|
-
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
6171
|
-
|
|
6172
|
-
(method as any).device = stubDevice({
|
|
6173
|
-
originalDescriptor: { protocolType: 'V2' },
|
|
6174
|
-
features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] },
|
|
6175
|
-
});
|
|
6176
|
-
(method as any).prepareRemoteProtocolV2Binaries = jest.fn();
|
|
6177
|
-
(method as any).enterProtocolV2BootloaderMode = jest.fn();
|
|
6178
|
-
method.postTipMessage = jest.fn();
|
|
6179
|
-
|
|
6180
|
-
await expect(method.run()).rejects.toThrow('resourceFiles[0].devicePath');
|
|
6181
|
-
expect((method as any).prepareRemoteProtocolV2Binaries).not.toHaveBeenCalled();
|
|
6182
|
-
expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
|
|
7075
|
+
expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
|
|
7076
|
+
bootloaderBinary: null,
|
|
7077
|
+
fwBinaryMap: [{ fileName: 'coprocessor.bin', binary: expect.anything(), targetId: 6 }],
|
|
7078
|
+
});
|
|
6183
7079
|
});
|
|
6184
7080
|
|
|
6185
|
-
test('syncs resource
|
|
7081
|
+
test('syncs prepared resource sources without sending a firmware install request', async () => {
|
|
6186
7082
|
const method = new FirmwareUpdateV4({
|
|
6187
7083
|
id: 1,
|
|
6188
7084
|
payload: {
|
|
@@ -6194,28 +7090,118 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6194
7090
|
method.postProgressMessage = jest.fn();
|
|
6195
7091
|
(method as any).protocolV2SourceUpdateProcess = jest.fn().mockResolvedValue(3);
|
|
6196
7092
|
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
|
|
7093
|
+
(method as any).ensureProtocolV2BootResourceStagingIsEmpty = jest
|
|
7094
|
+
.fn()
|
|
7095
|
+
.mockResolvedValue(undefined);
|
|
6197
7096
|
(method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
|
|
6198
7097
|
(method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
|
|
6199
7098
|
(method as any).protocolV2StartFirmwareUpdate = jest.fn();
|
|
6200
7099
|
(method as any).waitForProtocolV2FirmwareUpdateComplete = jest.fn();
|
|
6201
7100
|
|
|
6202
|
-
await (method as any).
|
|
6203
|
-
|
|
7101
|
+
await (method as any).executeProtocolV2SourceUpdate({
|
|
7102
|
+
installSources: [],
|
|
7103
|
+
resourceSources: [
|
|
6204
7104
|
{
|
|
6205
7105
|
name: 'images.okpkg',
|
|
6206
|
-
|
|
7106
|
+
source: {
|
|
7107
|
+
size: 3,
|
|
7108
|
+
readAt: jest.fn(),
|
|
7109
|
+
close: jest.fn(),
|
|
7110
|
+
},
|
|
6207
7111
|
devicePath: 'vol0:/resource/images/images.okpkg',
|
|
6208
7112
|
},
|
|
6209
7113
|
],
|
|
6210
|
-
bootloaderBinary: null,
|
|
6211
|
-
fwBinaryMap: [],
|
|
6212
7114
|
});
|
|
6213
7115
|
|
|
6214
7116
|
expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenCalledTimes(1);
|
|
7117
|
+
expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenCalledWith(
|
|
7118
|
+
expect.objectContaining({ filePath: 'vol0:/resource/images/images.okpkg' })
|
|
7119
|
+
);
|
|
7120
|
+
expect((method as any).verifyProtocolV2StagedFile).toHaveBeenCalledWith(
|
|
7121
|
+
'vol0:/resource/images/images.okpkg',
|
|
7122
|
+
3
|
|
7123
|
+
);
|
|
6215
7124
|
expect((method as any).protocolV2StartFirmwareUpdate).not.toHaveBeenCalled();
|
|
6216
7125
|
expect((method as any).waitForProtocolV2FirmwareUpdateComplete).not.toHaveBeenCalled();
|
|
6217
7126
|
});
|
|
6218
7127
|
|
|
7128
|
+
test('writes the mounted boot resource package through its staging path', async () => {
|
|
7129
|
+
const method = new FirmwareUpdateV4({
|
|
7130
|
+
id: 1,
|
|
7131
|
+
payload: {
|
|
7132
|
+
method: 'firmwareUpdateV4',
|
|
7133
|
+
},
|
|
7134
|
+
});
|
|
7135
|
+
|
|
7136
|
+
method.postTipMessage = jest.fn();
|
|
7137
|
+
method.postProgressMessage = jest.fn();
|
|
7138
|
+
(method as any).protocolV2SourceUpdateProcess = jest.fn().mockResolvedValue(3);
|
|
7139
|
+
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
|
|
7140
|
+
(method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
|
|
7141
|
+
(method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
|
|
7142
|
+
|
|
7143
|
+
await (method as any).executeProtocolV2SourceUpdate({
|
|
7144
|
+
installSources: [],
|
|
7145
|
+
resourceSources: [
|
|
7146
|
+
{
|
|
7147
|
+
name: 'boot_resource.okpkg',
|
|
7148
|
+
source: {
|
|
7149
|
+
size: 3,
|
|
7150
|
+
readAt: jest.fn(),
|
|
7151
|
+
close: jest.fn(),
|
|
7152
|
+
},
|
|
7153
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
7154
|
+
},
|
|
7155
|
+
],
|
|
7156
|
+
});
|
|
7157
|
+
|
|
7158
|
+
const stagingPath = 'vol0:/loaders/bootloader/boot_resource.okpkg.staging';
|
|
7159
|
+
expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenCalledWith(
|
|
7160
|
+
expect.objectContaining({ filePath: stagingPath })
|
|
7161
|
+
);
|
|
7162
|
+
expect((method as any).verifyProtocolV2StagedFile).toHaveBeenCalledWith(stagingPath, 3);
|
|
7163
|
+
});
|
|
7164
|
+
|
|
7165
|
+
test('rewrites the mounted boot resource staging file even when the final file is current', async () => {
|
|
7166
|
+
const method = new FirmwareUpdateV4({
|
|
7167
|
+
id: 1,
|
|
7168
|
+
payload: {
|
|
7169
|
+
method: 'firmwareUpdateV4',
|
|
7170
|
+
},
|
|
7171
|
+
});
|
|
7172
|
+
|
|
7173
|
+
method.postTipMessage = jest.fn();
|
|
7174
|
+
method.postProgressMessage = jest.fn();
|
|
7175
|
+
(method as any).isProtocolV2ResourceBundleUpToDate = jest.fn().mockResolvedValue(true);
|
|
7176
|
+
(method as any).protocolV2SourceUpdateProcess = jest.fn().mockResolvedValue(3);
|
|
7177
|
+
(method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
|
|
7178
|
+
|
|
7179
|
+
const resource = {
|
|
7180
|
+
name: 'boot_resource.okpkg',
|
|
7181
|
+
source: {
|
|
7182
|
+
size: 3,
|
|
7183
|
+
readAt: jest.fn(),
|
|
7184
|
+
close: jest.fn(),
|
|
7185
|
+
},
|
|
7186
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
7187
|
+
version: [1, 0, 0],
|
|
7188
|
+
payloadHash: '11'.repeat(64),
|
|
7189
|
+
headerHash: '22'.repeat(64),
|
|
7190
|
+
};
|
|
7191
|
+
|
|
7192
|
+
await (method as any).executeProtocolV2TransferPhase({
|
|
7193
|
+
installSources: [],
|
|
7194
|
+
resourceSources: [resource],
|
|
7195
|
+
});
|
|
7196
|
+
|
|
7197
|
+
const stagingPath = 'vol0:/loaders/bootloader/boot_resource.okpkg.staging';
|
|
7198
|
+
expect((method as any).isProtocolV2ResourceBundleUpToDate).not.toHaveBeenCalled();
|
|
7199
|
+
expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenCalledWith(
|
|
7200
|
+
expect.objectContaining({ source: resource.source, filePath: stagingPath, totalSize: 3 })
|
|
7201
|
+
);
|
|
7202
|
+
expect((method as any).verifyProtocolV2StagedFile).toHaveBeenCalledWith(stagingPath, 3);
|
|
7203
|
+
});
|
|
7204
|
+
|
|
6219
7205
|
test('uses absolute processed_byte offsets and disables append for firmware file writes', async () => {
|
|
6220
7206
|
const method = new FirmwareUpdateV4({
|
|
6221
7207
|
id: 1,
|
|
@@ -6919,64 +7905,6 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
6919
7905
|
});
|
|
6920
7906
|
|
|
6921
7907
|
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
7908
|
test('keeps the host plan identity as the V4 reconnect anchor', () => {
|
|
6981
7909
|
const method = new FirmwareUpdateV4({
|
|
6982
7910
|
id: 1,
|
|
@@ -7043,7 +7971,7 @@ describe('Protocol V2 firmware reconnect identity', () => {
|
|
|
7043
7971
|
);
|
|
7044
7972
|
});
|
|
7045
7973
|
|
|
7046
|
-
test('
|
|
7974
|
+
test('uses the connection route when DeviceInfo has no physical serial', async () => {
|
|
7047
7975
|
const method = new FirmwareUpdateV4({
|
|
7048
7976
|
id: 1,
|
|
7049
7977
|
payload: { method: 'firmwareUpdateV4' },
|
|
@@ -7053,12 +7981,98 @@ describe('Protocol V2 firmware reconnect identity', () => {
|
|
|
7053
7981
|
message: { protocol_version: 1, hw: {} },
|
|
7054
7982
|
});
|
|
7055
7983
|
(method as any).device = stubDevice({
|
|
7984
|
+
originalDescriptor: { path: '' },
|
|
7985
|
+
getConnectId: () => '000000000000',
|
|
7056
7986
|
getCommands: () => ({ typedCall }),
|
|
7057
7987
|
});
|
|
7058
7988
|
|
|
7059
|
-
await expect((method as any).captureProtocolV2PhysicalIdentity()).
|
|
7060
|
-
|
|
7989
|
+
await expect((method as any).captureProtocolV2PhysicalIdentity()).resolves.toBeUndefined();
|
|
7990
|
+
expect((method as any).protocolV2ExpectedSerialNumber).toBeUndefined();
|
|
7991
|
+
expect((method as any).protocolV2ExpectedPath).toBe('000000000000');
|
|
7992
|
+
});
|
|
7993
|
+
|
|
7994
|
+
test('executes a serial-less Pro2 prepared plan when the live model matches', async () => {
|
|
7995
|
+
const planWithoutDigest = {
|
|
7996
|
+
schemaVersion: 2 as const,
|
|
7997
|
+
executor: 'v4' as const,
|
|
7998
|
+
deviceIdentity: 'unavailable',
|
|
7999
|
+
deviceModel: 'pro2',
|
|
8000
|
+
firmwareType: EFirmwareType.Universal,
|
|
8001
|
+
platform: 'web' as const,
|
|
8002
|
+
targetsToUpdate: ['boot' as const],
|
|
8003
|
+
artifacts: [
|
|
8004
|
+
{
|
|
8005
|
+
artifactId: 'component:boot',
|
|
8006
|
+
role: 'component' as const,
|
|
8007
|
+
target: 'boot' as const,
|
|
8008
|
+
url: 'https://example.com/bootloader.bin',
|
|
8009
|
+
container: 'raw' as const,
|
|
8010
|
+
logicalName: 'bootloader',
|
|
8011
|
+
expectedSize: 1,
|
|
8012
|
+
expectedSha256: 'a'.repeat(64),
|
|
8013
|
+
},
|
|
8014
|
+
],
|
|
8015
|
+
};
|
|
8016
|
+
const preparedPlan = prepareFirmwareUpdatePlan({
|
|
8017
|
+
plan: {
|
|
8018
|
+
...planWithoutDigest,
|
|
8019
|
+
planDigest: digestFirmwareUpdateContract(planWithoutDigest),
|
|
8020
|
+
},
|
|
8021
|
+
leaseRef: 'serial-less-v4-executor-test',
|
|
8022
|
+
artifacts: [
|
|
8023
|
+
{
|
|
8024
|
+
artifactId: 'component:boot',
|
|
8025
|
+
artifact: {
|
|
8026
|
+
artifactRef: 'serial-less-bootloader',
|
|
8027
|
+
size: 1,
|
|
8028
|
+
sha256: 'a'.repeat(64),
|
|
8029
|
+
},
|
|
8030
|
+
},
|
|
8031
|
+
],
|
|
8032
|
+
});
|
|
8033
|
+
const hostBindingGeneration = registerFirmwareUpdateHostBinding({
|
|
8034
|
+
preparedPlanDigest: preparedPlan.preparedPlanDigest,
|
|
8035
|
+
artifactReader: {
|
|
8036
|
+
open: jest.fn(),
|
|
8037
|
+
read: jest.fn(),
|
|
8038
|
+
close: jest.fn(),
|
|
8039
|
+
},
|
|
7061
8040
|
});
|
|
8041
|
+
try {
|
|
8042
|
+
const method = new FirmwareUpdateV4({
|
|
8043
|
+
id: 1,
|
|
8044
|
+
payload: {
|
|
8045
|
+
method: 'firmwareUpdateV4',
|
|
8046
|
+
platform: 'web',
|
|
8047
|
+
preparedPlan,
|
|
8048
|
+
hostBindingGeneration,
|
|
8049
|
+
},
|
|
8050
|
+
});
|
|
8051
|
+
method.init();
|
|
8052
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
8053
|
+
type: 'DeviceInfo',
|
|
8054
|
+
message: { protocol_version: 1, hw: {} },
|
|
8055
|
+
});
|
|
8056
|
+
(method as any).device = stubDevice({
|
|
8057
|
+
originalDescriptor: { protocolType: 'V2', path: 'serial-less-pro2' },
|
|
8058
|
+
features: {
|
|
8059
|
+
deviceType: 'pro2',
|
|
8060
|
+
firmwareVersion: '1.0.0',
|
|
8061
|
+
capabilities: [],
|
|
8062
|
+
},
|
|
8063
|
+
getCommands: () => ({ typedCall }),
|
|
8064
|
+
});
|
|
8065
|
+
(method as any).runProtocolV2PreparedArtifacts = jest
|
|
8066
|
+
.fn()
|
|
8067
|
+
.mockResolvedValue('serial-less-result');
|
|
8068
|
+
|
|
8069
|
+
await expect(method.run()).resolves.toBe('serial-less-result');
|
|
8070
|
+
expect((method as any).runProtocolV2PreparedArtifacts).toHaveBeenCalled();
|
|
8071
|
+
expect((method as any).protocolV2ExpectedSerialNumber).toBeUndefined();
|
|
8072
|
+
expect((method as any).protocolV2ExpectedPath).toBe('serial-less-pro2');
|
|
8073
|
+
} finally {
|
|
8074
|
+
unregisterFirmwareUpdateHostBinding(hostBindingGeneration);
|
|
8075
|
+
}
|
|
7062
8076
|
});
|
|
7063
8077
|
|
|
7064
8078
|
test('uses the BLE read limit when reading an existing firmware bundle header', async () => {
|
|
@@ -7091,292 +8105,94 @@ describe('Protocol V2 firmware reconnect identity', () => {
|
|
|
7091
8105
|
expect(Math.max(...readChunkLengths)).toBe(900);
|
|
7092
8106
|
});
|
|
7093
8107
|
|
|
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 () => {
|
|
8108
|
+
test('skips resource transfer when installed okpkg hashes match the downloaded package', async () => {
|
|
7238
8109
|
const method = new FirmwareUpdateV4({
|
|
7239
8110
|
id: 1,
|
|
7240
8111
|
payload: {
|
|
7241
8112
|
method: 'firmwareUpdateV4',
|
|
7242
8113
|
platform: 'web',
|
|
7243
|
-
targetsToUpdate: ['boot_resources'],
|
|
7244
8114
|
},
|
|
7245
8115
|
});
|
|
7246
8116
|
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') {
|
|
8117
|
+
const installedBinary = new Uint8Array(createProtocolV2OkppBinary());
|
|
8118
|
+
const typedCall = jest.fn((requestType: string, _responseType: string, request: any) => {
|
|
8119
|
+
if (requestType === 'FilesystemPathInfoQuery') {
|
|
7273
8120
|
return Promise.resolve({
|
|
7274
|
-
message: { exist: true, directory: false, size:
|
|
8121
|
+
message: { exist: true, directory: false, size: installedBinary.byteLength },
|
|
7275
8122
|
});
|
|
7276
8123
|
}
|
|
7277
|
-
if (
|
|
7278
|
-
const offset = Number(payload.file.offset);
|
|
8124
|
+
if (requestType === 'FilesystemFileRead') {
|
|
7279
8125
|
return Promise.resolve({
|
|
7280
|
-
message: {
|
|
8126
|
+
message: {
|
|
8127
|
+
data: installedBinary.slice(
|
|
8128
|
+
Number(request.file.offset),
|
|
8129
|
+
Number(request.file.offset) + Number(request.chunk_len)
|
|
8130
|
+
),
|
|
8131
|
+
},
|
|
7281
8132
|
});
|
|
7282
8133
|
}
|
|
7283
|
-
|
|
7284
|
-
});
|
|
7285
|
-
const method = new FirmwareUpdateV4({
|
|
7286
|
-
id: 1,
|
|
7287
|
-
payload: { method: 'firmwareUpdateV4', platform: 'web', targetsToUpdate: ['resource'] },
|
|
8134
|
+
throw new Error(`Unexpected request: ${requestType}`);
|
|
7288
8135
|
});
|
|
7289
|
-
method.init();
|
|
7290
8136
|
(method as any).device = stubDevice({
|
|
7291
|
-
getCurrentDeviceType: () => 'pro2',
|
|
7292
8137
|
getCommands: () => ({ typedCall }),
|
|
7293
8138
|
});
|
|
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
|
-
});
|
|
8139
|
+
method.postTipMessage = jest.fn();
|
|
8140
|
+
method.postProgressMessage = jest.fn();
|
|
8141
|
+
(method as any).protocolV2SourceUpdateProcess = jest.fn();
|
|
7319
8142
|
|
|
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: [
|
|
8143
|
+
await (method as any).executeProtocolV2TransferPhase({
|
|
8144
|
+
installSources: [],
|
|
8145
|
+
resourceSources: [
|
|
7328
8146
|
{
|
|
7329
|
-
name: '
|
|
7330
|
-
|
|
7331
|
-
devicePath: 'vol0:/
|
|
7332
|
-
|
|
7333
|
-
|
|
8147
|
+
name: 'images.okpkg',
|
|
8148
|
+
source: { size: installedBinary.byteLength },
|
|
8149
|
+
devicePath: 'vol0:/bundles/images/images.okpkg',
|
|
8150
|
+
version: [1, 2, 3],
|
|
8151
|
+
payloadHash: '11'.repeat(64),
|
|
8152
|
+
headerHash: '22'.repeat(64),
|
|
7334
8153
|
},
|
|
7335
8154
|
],
|
|
7336
|
-
};
|
|
7337
|
-
const method = new FirmwareUpdateV4({
|
|
7338
|
-
id: 1,
|
|
7339
|
-
payload: {
|
|
7340
|
-
method: 'firmwareUpdateV4',
|
|
7341
|
-
platform: 'web',
|
|
7342
|
-
targetsToUpdate: ['resource'],
|
|
7343
|
-
},
|
|
7344
8155
|
});
|
|
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
8156
|
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
devicePath: 'vol0:/assets/loaders/boot.staging/graphics/bootloader_crest.bin',
|
|
7355
|
-
},
|
|
7356
|
-
]);
|
|
8157
|
+
expect(typedCall).toHaveBeenCalledWith('FilesystemPathInfoQuery', 'FilesystemPathInfo', {
|
|
8158
|
+
path: 'vol0:/bundles/images/images.okpkg',
|
|
8159
|
+
});
|
|
8160
|
+
expect((method as any).protocolV2SourceUpdateProcess).not.toHaveBeenCalled();
|
|
7357
8161
|
});
|
|
7358
8162
|
|
|
7359
|
-
test('
|
|
8163
|
+
test('updates a resource when the installed file size differs despite matching headers', async () => {
|
|
7360
8164
|
const method = new FirmwareUpdateV4({
|
|
7361
8165
|
id: 1,
|
|
7362
8166
|
payload: {
|
|
7363
8167
|
method: 'firmwareUpdateV4',
|
|
7364
8168
|
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
8169
|
},
|
|
7374
8170
|
});
|
|
7375
8171
|
method.init();
|
|
8172
|
+
const installedBinary = new Uint8Array(createProtocolV2OkppBinary());
|
|
8173
|
+
const typedCall = jest.fn((requestType: string) => {
|
|
8174
|
+
if (requestType === 'FilesystemPathInfoQuery') {
|
|
8175
|
+
return Promise.resolve({
|
|
8176
|
+
message: { exist: true, directory: false, size: installedBinary.byteLength + 1 },
|
|
8177
|
+
});
|
|
8178
|
+
}
|
|
8179
|
+
throw new Error(`Unexpected request: ${requestType}`);
|
|
8180
|
+
});
|
|
8181
|
+
(method as any).device = stubDevice({
|
|
8182
|
+
getCommands: () => ({ typedCall }),
|
|
8183
|
+
});
|
|
7376
8184
|
|
|
7377
|
-
expect(
|
|
7378
|
-
|
|
7379
|
-
|
|
8185
|
+
await expect(
|
|
8186
|
+
(method as any).isProtocolV2ResourceBundleUpToDate({
|
|
8187
|
+
name: 'images.okpkg',
|
|
8188
|
+
source: { size: installedBinary.byteLength },
|
|
8189
|
+
devicePath: 'vol0:/bundles/images/images.okpkg',
|
|
8190
|
+
version: [1, 2, 3],
|
|
8191
|
+
payloadHash: '11'.repeat(64),
|
|
8192
|
+
headerHash: '22'.repeat(64),
|
|
8193
|
+
})
|
|
8194
|
+
).resolves.toBe(false);
|
|
8195
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
7380
8196
|
});
|
|
7381
8197
|
});
|
|
7382
8198
|
|