@onekeyfe/hd-core 1.2.1 → 1.2.2-alpha.1

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.
@@ -32,10 +32,12 @@ const createMethod = ({
32
32
  typedCall,
33
33
  supportedMessages = [60802, 60805, 60808, 61500],
34
34
  useFullBundle = false,
35
+ firmwareVersion = '1.0.0',
35
36
  }: {
36
37
  typedCall: jest.Mock;
37
38
  supportedMessages?: number[];
38
39
  useFullBundle?: boolean;
40
+ firmwareVersion?: string;
39
41
  }) => {
40
42
  const method = new DeviceUploadNft({
41
43
  id: 1,
@@ -52,6 +54,7 @@ const createMethod = ({
52
54
  });
53
55
  (method as any).device = {
54
56
  commands: { typedCall },
57
+ state: { versions: { firmware: firmwareVersion } },
55
58
  ensureProtocolV2RuntimeContext: jest.fn(() =>
56
59
  Promise.resolve({
57
60
  version: 2,
@@ -60,6 +63,7 @@ const createMethod = ({
60
63
  })
61
64
  ),
62
65
  getCurrentFirmwareType: jest.fn(),
66
+ getCurrentFirmwareVersionString: jest.fn(() => firmwareVersion),
63
67
  };
64
68
  method.postMessage = jest.fn();
65
69
 
@@ -202,6 +206,70 @@ describe('DeviceUploadNft', () => {
202
206
  });
203
207
  });
204
208
 
209
+ test('uploads one host asset package on firmware 1.0.1', async () => {
210
+ const typedCall = jest.fn((request: string, _response: string, params: any) => {
211
+ if (request === 'FilesystemFileWrite') return fileWriteSuccess(params);
212
+ if (request === 'NftUpdate') return { message: { message: 'NFT updated' } };
213
+ throw new Error(`Unexpected request: ${request}`);
214
+ });
215
+ const method = createMethod({
216
+ typedCall,
217
+ supportedMessages: [60805, 61500],
218
+ firmwareVersion: '1.0.1',
219
+ });
220
+
221
+ const result = await method.run();
222
+
223
+ const requests = typedCall.mock.calls.map(call => call[0]);
224
+ expect(requests).not.toContain('FilesystemPathInfoQuery');
225
+ expect(requests).not.toContain('FilesystemDirList');
226
+ const fileWrites = typedCall.mock.calls.filter(call => call[0] === 'FilesystemFileWrite');
227
+ expect(new Set(fileWrites.map(call => call[2].file.path))).toEqual(
228
+ new Set(['vol1:/nft/nft-deadbeef-1760000000000.okpkg'])
229
+ );
230
+ expect(fileWrites[0][2].file.data.subarray(0, 4)).toEqual(
231
+ new Uint8Array([0x4f, 0x4b, 0x50, 0x50])
232
+ );
233
+ expect(typedCall).toHaveBeenLastCalledWith(
234
+ 'NftUpdate',
235
+ 'Success',
236
+ { file_name_no_ext: result.basename },
237
+ { timeoutMs: 15_000 }
238
+ );
239
+ expect(result).toMatchObject({
240
+ imagePath: 'vol1:/nft/nft-deadbeef-1760000000000.bin',
241
+ thumbnailPath: 'vol1:/nft/nft-deadbeef-1760000000000_m.bin',
242
+ metadataPath: 'vol1:/nft/nft-deadbeef-1760000000000.json',
243
+ nftUpdated: true,
244
+ });
245
+ });
246
+
247
+ test('keeps the legacy upload flow on a 1.0.1 prerelease', async () => {
248
+ const typedCall = jest.fn((request: string, _response: string, params: any) => {
249
+ if (request === 'FilesystemPathInfoQuery') {
250
+ return { message: { exist: true, directory: true } };
251
+ }
252
+ if (request === 'FilesystemDirList') {
253
+ return { message: { path: 'vol1:/nft', child_files: '' } };
254
+ }
255
+ if (request === 'FilesystemFileWrite') return fileWriteSuccess(params);
256
+ if (request === 'NftUpdate') return { message: {} };
257
+ throw new Error(`Unexpected request: ${request}`);
258
+ });
259
+ const method = createMethod({ typedCall, firmwareVersion: '1.0.1-beta.1' });
260
+
261
+ await method.run();
262
+
263
+ const requests = typedCall.mock.calls.map(call => call[0]);
264
+ expect(requests).toContain('FilesystemPathInfoQuery');
265
+ expect(requests).toContain('FilesystemDirList');
266
+ const paths = typedCall.mock.calls
267
+ .filter(call => call[0] === 'FilesystemFileWrite')
268
+ .map(call => call[2].file.path as string);
269
+ expect(paths.some(path => path.endsWith('.okpkg'))).toBe(false);
270
+ expect(paths).toContain('vol1:/nft/nft-deadbeef-1760000000000.bin');
271
+ });
272
+
205
273
  test('treats a missing NFT directory as empty before the first upload', async () => {
206
274
  const typedCall = jest.fn((request: string, _response: string, params: any) => {
207
275
  if (request === 'FilesystemPathInfoQuery') {
@@ -1,3 +1,5 @@
1
+ import { EDeviceType } from '@onekeyfe/hd-shared';
2
+
1
3
  import EVMSignTypedData from '../src/api/evm/EVMSignTypedData';
2
4
 
3
5
  import type { EthereumSignTypedDataMessage, EthereumSignTypedDataTypes } from '../src/types';
@@ -577,3 +579,43 @@ describe('EVMSignTypedData — OneKey Pro Safe Protocol V1', () => {
577
579
  });
578
580
  });
579
581
  });
582
+
583
+ describe('EVMSignTypedData — Protocol V2 data size routing', () => {
584
+ const buildSafeTxData = (
585
+ dataSize: number
586
+ ): EthereumSignTypedDataMessage<EthereumSignTypedDataTypes> =>
587
+ ({
588
+ types: {
589
+ EIP712Domain: [],
590
+ SafeTx: [{ name: 'data', type: 'bytes' }],
591
+ },
592
+ primaryType: 'SafeTx',
593
+ domain: {},
594
+ message: { data: `0x${'ab'.repeat(dataSize)}` },
595
+ } as EthereumSignTypedDataMessage<EthereumSignTypedDataTypes>);
596
+
597
+ test.each([EDeviceType.Pro2, EDeviceType.Neo])(
598
+ 'keeps SafeTx data up to 1536 bytes on the structured route for %s',
599
+ deviceType => {
600
+ const data = buildSafeTxData(1316);
601
+ const method = createMethod(data);
602
+ method.device.getCurrentDeviceType = jest.fn(() => deviceType);
603
+ method.device.getCurrentFirmwareVersionString = jest.fn(() => '1.0.0');
604
+
605
+ expect(method.hasBiggerData(data)).toBe(false);
606
+ expect(method.hasBiggerData(buildSafeTxData(1536))).toBe(false);
607
+ }
608
+ );
609
+
610
+ test.each([EDeviceType.Pro2, EDeviceType.Neo])(
611
+ 'falls back to the hash route above 1536 bytes for %s',
612
+ deviceType => {
613
+ const data = buildSafeTxData(1537);
614
+ const method = createMethod(data);
615
+ method.device.getCurrentDeviceType = jest.fn(() => deviceType);
616
+ method.device.getCurrentFirmwareVersionString = jest.fn(() => '1.0.0');
617
+
618
+ expect(method.hasBiggerData(data)).toBe(true);
619
+ }
620
+ );
621
+ });
@@ -0,0 +1,58 @@
1
+ import { sha3_512 } from '@noble/hashes/sha3';
2
+
3
+ import {
4
+ buildPro2HostAssetPackage,
5
+ supportsPro2HostAssetPackage,
6
+ } from '../src/utils/pro2HostAssetPackage';
7
+
8
+ describe('Pro2 host asset package', () => {
9
+ test('builds the unsigned RESOURCE container and LZ4-blocked archive expected by firmware', () => {
10
+ const raw = new TextEncoder().encode('123456789');
11
+ const packageData = buildPro2HostAssetPackage([{ name: 'wallpaper.bin', data: raw }]);
12
+ const headerSize = 0x5f90;
13
+ const payload = packageData.subarray(headerSize);
14
+ const packageView = new DataView(
15
+ packageData.buffer,
16
+ packageData.byteOffset,
17
+ packageData.byteLength
18
+ );
19
+
20
+ expect(packageView.getUint32(0, true)).toBe(0x50504b4f);
21
+ expect(packageView.getUint32(4, true)).toBe(1);
22
+ expect(packageView.getUint32(8, true)).toBe(0x43534552);
23
+ expect(packageView.getUint32(0x0c, true)).toBe(headerSize);
24
+ expect(packageView.getUint32(0x10, true)).toBe(1);
25
+ expect(packageView.getUint32(0x14, true)).toBe(payload.byteLength);
26
+ expect(packageData.subarray(0x200, 0x240)).toEqual(sha3_512(payload));
27
+ expect(packageData.subarray(0x240, 0x280)).toEqual(sha3_512(packageData.subarray(0, 0x240)));
28
+ expect(packageData[0x400]).toBe(0);
29
+ expect(packageView.getUint32(0x408, true)).toBe(0x71717171);
30
+
31
+ const archiveView = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
32
+ expect(archiveView.getUint32(0, true)).toBe(0x52414b4f);
33
+ expect(archiveView.getUint32(4, true)).toBe(1);
34
+ expect(archiveView.getUint16(8, true)).toBe(1);
35
+ expect(new TextDecoder().decode(payload.subarray(43, 56))).toBe('wallpaper.bin');
36
+ expect(archiveView.getUint32(42 + 0x100, true)).toBe(340);
37
+ expect(archiveView.getUint32(42 + 0x104, true)).toBe(raw.byteLength);
38
+ expect(archiveView.getUint32(42 + 0x10c, true)).toBe(0xcbf43926);
39
+ expect(payload[42 + 0x114]).toBe(1);
40
+
41
+ const compressedOffset = archiveView.getUint32(42 + 0x100, true);
42
+ expect(archiveView.getUint16(compressedOffset, true)).toBe(1);
43
+ expect(archiveView.getUint16(compressedOffset + 2, true)).toBe(12);
44
+ expect(archiveView.getUint32(compressedOffset + 4, true)).toBe(0);
45
+ expect(archiveView.getUint32(compressedOffset + 8, true)).toBe(10);
46
+ expect(payload.subarray(compressedOffset + 12)).toEqual(new Uint8Array([0x90, ...raw]));
47
+ });
48
+
49
+ test.each([
50
+ ['1.0.0', false],
51
+ ['1.0.1-beta.1', false],
52
+ ['1.0.1', true],
53
+ ['1.1.0', true],
54
+ [undefined, false],
55
+ ])('selects package uploads for firmware %s', (firmwareVersion, expected) => {
56
+ expect(supportsPro2HostAssetPackage(firmwareVersion)).toBe(expected);
57
+ });
58
+ });
@@ -254,6 +254,88 @@ describe('DeviceUploadWallpaper', () => {
254
254
  });
255
255
  });
256
256
 
257
+ test('uploads and applies the fixed wallpaper package on firmware 1.0.1', async () => {
258
+ const typedCall = jest.fn().mockImplementation((request, _response, params) => {
259
+ if (request === 'FilesystemDirMake') return { message: {} };
260
+ if (request === 'FilesystemFileWrite') {
261
+ const file = params.file as { data: Uint8Array; offset: number };
262
+ return { message: { processed_byte: file.offset + file.data.byteLength } };
263
+ }
264
+ if (request === 'DeviceSettingsSet') {
265
+ return { message: { message: 'wallpaper applied' } };
266
+ }
267
+ throw new Error(`Unexpected request: ${request}`);
268
+ });
269
+ const method = new DeviceUploadWallpaper({
270
+ id: 1,
271
+ payload: {
272
+ method: 'deviceUploadWallpaper',
273
+ jpegBase64: createJpegBase64(604, 1024),
274
+ fileName: 'ignored-on-package-firmware.bin',
275
+ },
276
+ });
277
+ const device = stubWallpaperDevice({
278
+ commands: { typedCall },
279
+ state: { versions: { firmware: '1.0.1' } },
280
+ getCurrentFirmwareVersionString: jest.fn(() => '1.0.1'),
281
+ });
282
+ (method as any).device = device;
283
+ method.postMessage = jest.fn();
284
+
285
+ method.init();
286
+ const result = await method.run();
287
+
288
+ const fileWrites = typedCall.mock.calls.filter(call => call[0] === 'FilesystemFileWrite');
289
+ expect(new Set(fileWrites.map(call => call[2].file.path))).toEqual(
290
+ new Set(['vol1:/wallpapers/wallpaper.okpkg'])
291
+ );
292
+ expect(fileWrites[0][2].file.data.subarray(0, 4)).toEqual(
293
+ new Uint8Array([0x4f, 0x4b, 0x50, 0x50])
294
+ );
295
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceSettingsSet', 'Success', {
296
+ settings: { wallpaper_path: 'vol1:/wallpapers/wallpaper.okpkg' },
297
+ });
298
+ expect(result).toMatchObject({
299
+ path: 'vol1:/wallpapers/wallpaper.okpkg',
300
+ colorFormat: 'RGB565',
301
+ message: 'wallpaper applied',
302
+ });
303
+ });
304
+
305
+ test('keeps the legacy wallpaper path on a 1.0.1 prerelease', async () => {
306
+ const typedCall = jest.fn().mockImplementation((request, _response, params) => {
307
+ if (request === 'FilesystemDirMake') return { message: {} };
308
+ if (request === 'FilesystemFileWrite') {
309
+ const file = params.file as { data: Uint8Array; offset: number };
310
+ return { message: { processed_byte: file.offset + file.data.byteLength } };
311
+ }
312
+ if (request === 'DeviceSettingsSet') return { message: {} };
313
+ throw new Error(`Unexpected request: ${request}`);
314
+ });
315
+ const method = new DeviceUploadWallpaper({
316
+ id: 1,
317
+ payload: {
318
+ method: 'deviceUploadWallpaper',
319
+ jpegBase64: createJpegBase64(604, 1024),
320
+ fileName: 'prerelease-wallpaper.bin',
321
+ },
322
+ });
323
+ const device = stubWallpaperDevice({
324
+ commands: { typedCall },
325
+ state: { versions: { firmware: '1.0.1-beta.1' } },
326
+ });
327
+ (method as any).device = device;
328
+
329
+ method.init();
330
+ const result = await method.run();
331
+
332
+ expect(result.path).toBe('vol1:/wallpapers/prerelease-wallpaper.bin');
333
+ const fileWrites = typedCall.mock.calls.filter(call => call[0] === 'FilesystemFileWrite');
334
+ expect(new Set(fileWrites.map(call => call[2].file.path))).toEqual(
335
+ new Set(['vol1:/wallpapers/prerelease-wallpaper.bin'])
336
+ );
337
+ });
338
+
257
339
  test('文件上传失败时不修改 wallpaper_path', async () => {
258
340
  const typedCall = jest.fn().mockImplementation(request => {
259
341
  if (request === 'FilesystemDirMake') return { message: {} };
@@ -1 +1 @@
1
- {"version":3,"file":"EVMSignTypedData.d.ts","sourceRoot":"","sources":["../../../src/api/evm/EVMSignTypedData.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAI3C,OAAO,EAEL,KAAK,4BAA4B,EACjC,KAAK,0BAA0B,EAChC,MAAM,aAAa,CAAC;AAQrB,OAAO,KAAK,EAIV,UAAU,EACV,eAAe,EACf,SAAS,EACV,MAAM,wBAAwB,CAAC;AAShC,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,IAAI,EAAE,4BAA4B,CAAC,0BAA0B,CAAC,CAAC;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAUF,MAAM,CAAC,OAAO,OAAO,gBAAiB,SAAQ,UAAU,CAAC,sBAAsB,CAAC;IAC9E,qBAAqB;IAIrB,IAAI;IAuCE,mBAAmB,CAAC,EACxB,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,aAAa,GACd,EAAE;QACD,SAAS,EAAE,SAAS,CAAC;QACrB,QAAQ,EAAE,4BAA4B,CAAC,0BAA0B,CAAC,CAAC;QACnE,QAAQ,EAAE,eAAe,CAAC,UAAU,CAAC,CAAC;QACtC,aAAa,EAAE,OAAO,CAAC;KACxB;;;;IAoKK,aAAa;;;;IAgCnB,aAAa,CAAC,EACZ,SAAS,EACT,QAAQ,EACR,OAAO,EACP,UAAU,EACV,WAAW,GACZ,EAAE;QACD,SAAS,EAAE,SAAS,CAAC;QACrB,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnB,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;QAC5B,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;QAC/B,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;KACjC;IAuBD,eAAe;;;;;IAQf,aAAa,CAAC,IAAI,EAAE,4BAA4B,CAAC,0BAA0B,CAAC;IAuB5E,eAAe,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO;IA8BnC,yCAAyC,CACvC,IAAI,EAAE,4BAA4B,CAAC,0BAA0B,CAAC;IAuIhE,gBAAgB;IAcV,GAAG;CAgFV"}
1
+ {"version":3,"file":"EVMSignTypedData.d.ts","sourceRoot":"","sources":["../../../src/api/evm/EVMSignTypedData.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAI3C,OAAO,EAEL,KAAK,4BAA4B,EACjC,KAAK,0BAA0B,EAChC,MAAM,aAAa,CAAC;AAQrB,OAAO,KAAK,EAIV,UAAU,EACV,eAAe,EACf,SAAS,EACV,MAAM,wBAAwB,CAAC;AAShC,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,IAAI,EAAE,4BAA4B,CAAC,0BAA0B,CAAC,CAAC;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAUF,MAAM,CAAC,OAAO,OAAO,gBAAiB,SAAQ,UAAU,CAAC,sBAAsB,CAAC;IAC9E,qBAAqB;IAIrB,IAAI;IAuCE,mBAAmB,CAAC,EACxB,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,aAAa,GACd,EAAE;QACD,SAAS,EAAE,SAAS,CAAC;QACrB,QAAQ,EAAE,4BAA4B,CAAC,0BAA0B,CAAC,CAAC;QACnE,QAAQ,EAAE,eAAe,CAAC,UAAU,CAAC,CAAC;QACtC,aAAa,EAAE,OAAO,CAAC;KACxB;;;;IAoKK,aAAa;;;;IAgCnB,aAAa,CAAC,EACZ,SAAS,EACT,QAAQ,EACR,OAAO,EACP,UAAU,EACV,WAAW,GACZ,EAAE;QACD,SAAS,EAAE,SAAS,CAAC;QACrB,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnB,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;QAC5B,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;QAC/B,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;KACjC;IAuBD,eAAe;;;;;IAQf,aAAa,CAAC,IAAI,EAAE,4BAA4B,CAAC,0BAA0B,CAAC;IAwB5E,eAAe,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO;IA8BnC,yCAAyC,CACvC,IAAI,EAAE,4BAA4B,CAAC,0BAA0B,CAAC;IAuIhE,gBAAgB;IAcV,GAAG;CAgFV"}
@@ -1 +1 @@
1
- {"version":3,"file":"DeviceUploadNft.d.ts","sourceRoot":"","sources":["../../../src/api/protocol-v2/DeviceUploadNft.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAK3C,MAAM,MAAM,qBAAqB,GAAG;IAClC,eAAe,EAAE,MAAM,CAAC;IACxB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,IAAI,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAOF,MAAM,CAAC,OAAO,OAAO,eAAgB,SAAQ,UAAU,CAAC,qBAAqB,CAAC;IAC5E,OAAO,CAAC,MAAM,CAAC,CAAgB;IAE/B,qBAAqB;IAIrB,IAAI;YAgFU,kBAAkB;YAiBlB,qBAAqB;YAwBrB,SAAS;IASjB,GAAG,IAAI,OAAO,CAAC,uBAAuB,CAAC;CA0D9C"}
1
+ {"version":3,"file":"DeviceUploadNft.d.ts","sourceRoot":"","sources":["../../../src/api/protocol-v2/DeviceUploadNft.ts"],"names":[],"mappings":"AA0BA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAK3C,MAAM,MAAM,qBAAqB,GAAG;IAClC,eAAe,EAAE,MAAM,CAAC;IACxB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,QAAQ,EAAE,MAAM,CAAC;IAEjB,SAAS,EAAE,MAAM,CAAC;IAElB,aAAa,EAAE,MAAM,CAAC;IAEtB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,IAAI,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAOF,MAAM,CAAC,OAAO,OAAO,eAAgB,SAAQ,UAAU,CAAC,qBAAqB,CAAC;IAC5E,OAAO,CAAC,MAAM,CAAC,CAAgB;IAE/B,qBAAqB;IAIrB,IAAI;YAgFU,kBAAkB;YAiBlB,qBAAqB;YAwBrB,SAAS;IASjB,GAAG,IAAI,OAAO,CAAC,uBAAuB,CAAC;CA0E9C"}
@@ -1 +1 @@
1
- {"version":3,"file":"DeviceUploadWallpaper.d.ts","sourceRoot":"","sources":["../../../src/api/protocol-v2/DeviceUploadWallpaper.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAO3C,OAAO,EAGL,KAAK,wBAAwB,EAE9B,MAAM,2BAA2B,CAAC;AAEnC,MAAM,MAAM,2BAA2B,GAAG;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,6BAA6B,GAAG;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,wBAAwB,CAAC;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAmBF,MAAM,CAAC,OAAO,OAAO,qBAAsB,SAAQ,UAAU,CAAC,2BAA2B,CAAC;IACxF,qBAAqB;IAIrB,OAAO,CAAC,OAAO,CAAC,CAA8D;IAE9E,OAAO,CAAC,cAAc,CAAS;IAE/B,OAAO,CAAC,QAAQ,CAAS;IAEzB,OAAO,CAAC,IAAI,CAAM;IAElB,IAAI;YA2BU,kBAAkB;YAgBlB,eAAe;YAaf,MAAM;IAwBd,GAAG,IAAI,OAAO,CAAC,6BAA6B,CAAC;CAyBpD"}
1
+ {"version":3,"file":"DeviceUploadWallpaper.d.ts","sourceRoot":"","sources":["../../../src/api/protocol-v2/DeviceUploadWallpaper.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAO3C,OAAO,EAGL,KAAK,wBAAwB,EAE9B,MAAM,2BAA2B,CAAC;AAMnC,MAAM,MAAM,2BAA2B,GAAG;IACxC,UAAU,EAAE,MAAM,CAAC;IAKnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,6BAA6B,GAAG;IAK1C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,wBAAwB,CAAC;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAqBF,MAAM,CAAC,OAAO,OAAO,qBAAsB,SAAQ,UAAU,CAAC,2BAA2B,CAAC;IACxF,qBAAqB;IAIrB,OAAO,CAAC,OAAO,CAAC,CAA8D;IAE9E,OAAO,CAAC,cAAc,CAAS;IAE/B,OAAO,CAAC,QAAQ,CAAS;IAEzB,OAAO,CAAC,IAAI,CAAM;IAElB,IAAI;YA2BU,kBAAkB;YAgBlB,eAAe;YAaf,MAAM;IAsBd,GAAG,IAAI,OAAO,CAAC,6BAA6B,CAAC;CAgCpD"}
package/dist/index.d.ts CHANGED
@@ -1127,10 +1127,18 @@ declare function encodePro2Wallpaper(options: {
1127
1127
 
1128
1128
  type DeviceUploadWallpaperParams = {
1129
1129
  jpegBase64: string;
1130
+ /**
1131
+ * Legacy firmware uses this name for the uploaded `.bin` file. Firmware
1132
+ * 1.0.1+ always consumes the fixed `wallpaper.okpkg` package path.
1133
+ */
1130
1134
  fileName?: string;
1131
1135
  chunkSize?: number;
1132
1136
  };
1133
1137
  type DeviceUploadWallpaperResponse = {
1138
+ /**
1139
+ * Filesystem path sent to `DeviceSettingsSet`. On firmware 1.0.1+ this is
1140
+ * the temporary package path; firmware extracts and persists wallpaper.bin.
1141
+ */
1134
1142
  path: string;
1135
1143
  size: number;
1136
1144
  colorFormat: Pro2WallpaperColorFormat;
@@ -1149,8 +1157,11 @@ type DeviceUploadNftParams = {
1149
1157
  };
1150
1158
  type DeviceUploadNftResponse = {
1151
1159
  basename: string;
1160
+ /** Final image path after firmware extracts a host-asset package. */
1152
1161
  imagePath: string;
1162
+ /** Final thumbnail path after firmware extracts a host-asset package. */
1153
1163
  thumbnailPath: string;
1164
+ /** Final metadata path after firmware extracts a host-asset package. */
1154
1165
  metadataPath: string;
1155
1166
  totalSize: number;
1156
1167
  nftUpdated: true;
package/dist/index.js CHANGED
@@ -42623,7 +42623,7 @@ function invalidParameter$2(message) {
42623
42623
  function asBytes(rgba) {
42624
42624
  return rgba instanceof Uint8Array ? rgba : new Uint8Array(rgba);
42625
42625
  }
42626
- function align(value, boundary) {
42626
+ function align$1(value, boundary) {
42627
42627
  return Math.ceil(value / boundary) * boundary;
42628
42628
  }
42629
42629
  function encodePro2Image(options) {
@@ -42651,7 +42651,7 @@ function encodePro2Image(options) {
42651
42651
  }
42652
42652
  }
42653
42653
  const colorFormat = hasTransparency ? 'RGB565A8' : 'RGB565';
42654
- const stride = align(width * 2, 4);
42654
+ const stride = align$1(width * 2, 4);
42655
42655
  const alphaStride = stride / 2;
42656
42656
  const rgbSize = stride * height;
42657
42657
  const alphaSize = hasTransparency ? alphaStride * height : 0;
@@ -54612,7 +54612,234 @@ function decodeJpegBase64ToRgba({ jpegBase64, parameterName, expectedWidth, expe
54612
54612
  return decoded;
54613
54613
  }
54614
54614
 
54615
+ const PRO2_HOST_ASSET_PACKAGE_MIN_VERSION = '1.0.1';
54616
+ const CONTAINER_HEADER_SIZE = 0x5f90;
54617
+ const CONTAINER_HEADER_HASH_INPUT_LENGTH = 0x240;
54618
+ const CONTAINER_HASH_SECTION_OFFSET = 0x200;
54619
+ const CONTAINER_SIGNATURE_ALGORITHM_OFFSET = 0x408;
54620
+ const CONTAINER_HEADER_MAGIC = 0x50504b4f;
54621
+ const CONTAINER_HEADER_VERSION = 1;
54622
+ const CONTAINER_RESOURCE_TYPE_MAGIC = 0x43534552;
54623
+ const CONTAINER_ED25519_SIGNATURE_ALGORITHM = 0x71717171;
54624
+ const HOST_ASSET_PACKAGE_MAX_SIZE = 4 * 1024 * 1024;
54625
+ const ARCHIVE_MAGIC = 0x52414b4f;
54626
+ const ARCHIVE_VERSION = 1;
54627
+ const ARCHIVE_HEADER_SIZE = 42;
54628
+ const ARCHIVE_ENTRY_SIZE = 296;
54629
+ const ARCHIVE_ENTRY_NAME_MAX_LENGTH = 255;
54630
+ const ARCHIVE_COMPRESS_LZ4_BLOCKED = 1;
54631
+ const ARCHIVE_ALIGNMENT = 4;
54632
+ const LZ4_BLOCK_SIZE_LOG2 = 12;
54633
+ function supportsPro2HostAssetPackage(firmwareVersion) {
54634
+ return Boolean(firmwareVersion &&
54635
+ semver__default["default"].valid(firmwareVersion) &&
54636
+ semver__default["default"].gte(firmwareVersion, PRO2_HOST_ASSET_PACKAGE_MIN_VERSION));
54637
+ }
54638
+ function concatBytes(parts) {
54639
+ const output = new Uint8Array(parts.reduce((length, part) => length + part.byteLength, 0));
54640
+ let offset = 0;
54641
+ for (const part of parts) {
54642
+ output.set(part, offset);
54643
+ offset += part.byteLength;
54644
+ }
54645
+ return output;
54646
+ }
54647
+ const LZ4_MIN_MATCH = 4;
54648
+ const LZ4_LAST_LITERALS = 5;
54649
+ const LZ4_MATCH_FIND_LIMIT = 12;
54650
+ const LZ4_MAX_OFFSET = 0xffff;
54651
+ const LZ4_LENGTH_MASK = 15;
54652
+ const LZ4_HASH_LOG = 16;
54653
+ const LZ4_HASH_MULTIPLIER = 2654435761;
54654
+ const LZ4_SKIP_TRIGGER = 6;
54655
+ function writeExtendedLength(output, offset, length) {
54656
+ let remaining = length;
54657
+ let nextOffset = offset;
54658
+ while (remaining >= 255) {
54659
+ output[nextOffset] = 255;
54660
+ nextOffset += 1;
54661
+ remaining -= 255;
54662
+ }
54663
+ output[nextOffset] = remaining;
54664
+ return nextOffset + 1;
54665
+ }
54666
+ function copyBytes(output, outputOffset, input, inputOffset, length) {
54667
+ output.set(input.subarray(inputOffset, inputOffset + length), outputOffset);
54668
+ return outputOffset + length;
54669
+ }
54670
+ function emitSequence(output, outputOffset, input, anchor, literalLength, matchOffset, matchLengthCode) {
54671
+ const tokenOffset = outputOffset;
54672
+ let nextOffset = outputOffset + 1;
54673
+ let token = 0;
54674
+ if (literalLength >= LZ4_LENGTH_MASK) {
54675
+ token = LZ4_LENGTH_MASK << 4;
54676
+ nextOffset = writeExtendedLength(output, nextOffset, literalLength - LZ4_LENGTH_MASK);
54677
+ }
54678
+ else {
54679
+ token = literalLength << 4;
54680
+ }
54681
+ nextOffset = copyBytes(output, nextOffset, input, anchor, literalLength);
54682
+ output[nextOffset] = matchOffset & 0xff;
54683
+ output[nextOffset + 1] = (matchOffset >>> 8) & 0xff;
54684
+ nextOffset += 2;
54685
+ if (matchLengthCode >= LZ4_LENGTH_MASK) {
54686
+ token |= LZ4_LENGTH_MASK;
54687
+ nextOffset = writeExtendedLength(output, nextOffset, matchLengthCode - LZ4_LENGTH_MASK);
54688
+ }
54689
+ else {
54690
+ token |= matchLengthCode;
54691
+ }
54692
+ output[tokenOffset] = token;
54693
+ return nextOffset;
54694
+ }
54695
+ function emitLastLiterals(output, outputOffset, input, anchor, literalLength) {
54696
+ const tokenOffset = outputOffset;
54697
+ let nextOffset = outputOffset + 1;
54698
+ if (literalLength >= LZ4_LENGTH_MASK) {
54699
+ output[tokenOffset] = LZ4_LENGTH_MASK << 4;
54700
+ nextOffset = writeExtendedLength(output, nextOffset, literalLength - LZ4_LENGTH_MASK);
54701
+ }
54702
+ else {
54703
+ output[tokenOffset] = literalLength << 4;
54704
+ }
54705
+ return copyBytes(output, nextOffset, input, anchor, literalLength);
54706
+ }
54707
+ function compressRawLz4Block(input, hashTable) {
54708
+ const output = new Uint8Array(input.byteLength + Math.floor(input.byteLength / 255) + 16);
54709
+ const inputView = new DataView(input.buffer, input.byteOffset, input.byteLength);
54710
+ const matchFindLimit = input.byteLength - LZ4_MATCH_FIND_LIMIT;
54711
+ const matchExtendLimit = input.byteLength - LZ4_LAST_LITERALS;
54712
+ let anchor = 0;
54713
+ let inputOffset = 0;
54714
+ let outputOffset = 0;
54715
+ let searchMatchCount = 1 << LZ4_SKIP_TRIGGER;
54716
+ hashTable.fill(0);
54717
+ while (inputOffset < matchFindLimit) {
54718
+ const sequence = inputView.getUint32(inputOffset, true);
54719
+ const hash = Math.imul(sequence, LZ4_HASH_MULTIPLIER) >>> (32 - LZ4_HASH_LOG);
54720
+ const candidate = hashTable[hash] - 1;
54721
+ hashTable[hash] = inputOffset + 1;
54722
+ const hasMatch = !(candidate < 0 ||
54723
+ inputOffset - candidate > LZ4_MAX_OFFSET ||
54724
+ inputView.getUint32(candidate, true) !== sequence);
54725
+ if (!hasMatch) {
54726
+ inputOffset += searchMatchCount >> LZ4_SKIP_TRIGGER;
54727
+ searchMatchCount += 1;
54728
+ }
54729
+ else {
54730
+ searchMatchCount = 1 << LZ4_SKIP_TRIGGER;
54731
+ let matchEnd = inputOffset + LZ4_MIN_MATCH;
54732
+ let reference = candidate + LZ4_MIN_MATCH;
54733
+ while (matchEnd < matchExtendLimit && input[matchEnd] === input[reference]) {
54734
+ matchEnd += 1;
54735
+ reference += 1;
54736
+ }
54737
+ outputOffset = emitSequence(output, outputOffset, input, anchor, inputOffset - anchor, inputOffset - candidate, matchEnd - inputOffset - LZ4_MIN_MATCH);
54738
+ inputOffset = matchEnd;
54739
+ anchor = inputOffset;
54740
+ }
54741
+ }
54742
+ outputOffset = emitLastLiterals(output, outputOffset, input, anchor, input.byteLength - anchor);
54743
+ return output.slice(0, outputOffset);
54744
+ }
54745
+ function encodeLz4Blocked(data) {
54746
+ const blockSize = 1 << LZ4_BLOCK_SIZE_LOG2;
54747
+ const blockCount = Math.ceil(data.byteLength / blockSize);
54748
+ const hashTable = new Uint32Array(1 << LZ4_HASH_LOG);
54749
+ const blocks = [];
54750
+ const header = new Uint8Array(8 + blockCount * 4);
54751
+ const headerView = new DataView(header.buffer);
54752
+ headerView.setUint16(0, blockCount, true);
54753
+ headerView.setUint16(2, LZ4_BLOCK_SIZE_LOG2, true);
54754
+ for (let index = 0; index < blockCount; index += 1) {
54755
+ const block = compressRawLz4Block(data.subarray(index * blockSize, Math.min((index + 1) * blockSize, data.byteLength)), hashTable);
54756
+ headerView.setUint32(8 + index * 4, block.byteLength, true);
54757
+ blocks.push(block);
54758
+ }
54759
+ return concatBytes([header, ...blocks]);
54760
+ }
54761
+ const CRC32_TABLE = (() => {
54762
+ const table = new Uint32Array(256);
54763
+ for (let index = 0; index < table.length; index += 1) {
54764
+ let value = index;
54765
+ for (let bit = 0; bit < 8; bit += 1) {
54766
+ value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
54767
+ }
54768
+ table[index] = value >>> 0;
54769
+ }
54770
+ return table;
54771
+ })();
54772
+ function crc32(data) {
54773
+ let value = 0xffffffff;
54774
+ for (const byte of data) {
54775
+ value = CRC32_TABLE[(value ^ byte) & 0xff] ^ (value >>> 8);
54776
+ }
54777
+ return (value ^ 0xffffffff) >>> 0;
54778
+ }
54779
+ function align(value) {
54780
+ return (value + ARCHIVE_ALIGNMENT - 1) & ~(ARCHIVE_ALIGNMENT - 1);
54781
+ }
54782
+ function buildArchive(entries) {
54783
+ const textEncoder = new TextEncoder();
54784
+ let dataOffset = ARCHIVE_HEADER_SIZE + entries.length * ARCHIVE_ENTRY_SIZE;
54785
+ const encodedEntries = entries.map(entry => {
54786
+ const nameBytes = textEncoder.encode(entry.name);
54787
+ if (!entry.name || nameBytes.byteLength > ARCHIVE_ENTRY_NAME_MAX_LENGTH) {
54788
+ throw new Error('Pro2 host asset package entry names must contain 1 to 255 UTF-8 bytes.');
54789
+ }
54790
+ if (!(entry.data instanceof Uint8Array) || entry.data.byteLength === 0) {
54791
+ throw new Error(`Pro2 host asset package entry [${entry.name}] must not be empty.`);
54792
+ }
54793
+ const compressed = encodeLz4Blocked(entry.data);
54794
+ const offset = align(dataOffset);
54795
+ dataOffset = offset + compressed.byteLength;
54796
+ return Object.assign(Object.assign({}, entry), { nameBytes, compressed, offset });
54797
+ });
54798
+ const archive = new Uint8Array(dataOffset);
54799
+ const view = new DataView(archive.buffer);
54800
+ view.setUint32(0, ARCHIVE_MAGIC, true);
54801
+ view.setUint32(4, ARCHIVE_VERSION, true);
54802
+ view.setUint16(8, encodedEntries.length, true);
54803
+ encodedEntries.forEach((entry, index) => {
54804
+ const recordOffset = ARCHIVE_HEADER_SIZE + index * ARCHIVE_ENTRY_SIZE;
54805
+ archive[recordOffset] = entry.nameBytes.byteLength;
54806
+ archive.set(entry.nameBytes, recordOffset + 1);
54807
+ view.setUint32(recordOffset + 0x100, entry.offset, true);
54808
+ view.setUint32(recordOffset + 0x104, entry.data.byteLength, true);
54809
+ view.setUint32(recordOffset + 0x108, entry.compressed.byteLength, true);
54810
+ view.setUint32(recordOffset + 0x10c, crc32(entry.data), true);
54811
+ view.setUint32(recordOffset + 0x110, crc32(entry.compressed), true);
54812
+ archive[recordOffset + 0x114] = ARCHIVE_COMPRESS_LZ4_BLOCKED;
54813
+ archive.set(entry.compressed, entry.offset);
54814
+ });
54815
+ return archive;
54816
+ }
54817
+ function buildPro2HostAssetPackage(entries) {
54818
+ if (entries.length === 0 || new Set(entries.map(entry => entry.name)).size !== entries.length) {
54819
+ throw new Error('Pro2 host asset package entries must have unique, non-empty names.');
54820
+ }
54821
+ const payload = buildArchive(entries);
54822
+ const header = new Uint8Array(CONTAINER_HEADER_SIZE);
54823
+ const view = new DataView(header.buffer);
54824
+ view.setUint32(0, CONTAINER_HEADER_MAGIC, true);
54825
+ view.setUint32(4, CONTAINER_HEADER_VERSION, true);
54826
+ view.setUint32(8, CONTAINER_RESOURCE_TYPE_MAGIC, true);
54827
+ view.setUint32(0x0c, CONTAINER_HEADER_SIZE, true);
54828
+ view.setUint32(0x10, 1, true);
54829
+ view.setUint32(0x14, payload.byteLength, true);
54830
+ header.set(sha3.sha3_512(payload), CONTAINER_HASH_SECTION_OFFSET);
54831
+ header.set(sha3.sha3_512(header.subarray(0, CONTAINER_HEADER_HASH_INPUT_LENGTH)), 0x240);
54832
+ view.setUint32(CONTAINER_SIGNATURE_ALGORITHM_OFFSET, CONTAINER_ED25519_SIGNATURE_ALGORITHM, true);
54833
+ const packageData = concatBytes([header, payload]);
54834
+ if (packageData.byteLength > HOST_ASSET_PACKAGE_MAX_SIZE) {
54835
+ throw new Error('Pro2 host asset package exceeds the firmware 4 MiB limit.');
54836
+ }
54837
+ return packageData;
54838
+ }
54839
+
54615
54840
  const WALLPAPER_DIRECTORY = 'vol1:/wallpapers';
54841
+ const WALLPAPER_PACKAGE_PATH = `${WALLPAPER_DIRECTORY}/wallpaper.okpkg`;
54842
+ const WALLPAPER_PACKAGE_ENTRY = 'wallpaper.bin';
54616
54843
  const SAFE_FILE_NAME = /^[A-Za-z0-9_-]+(?:\.bin)?$/;
54617
54844
  const DEVICE_SETTINGS_SET_MESSAGE_TYPE = 60412;
54618
54845
  const FILESYSTEM_FILE_WRITE_MESSAGE_TYPE$2 = 60805;
@@ -54688,18 +54915,15 @@ class DeviceUploadWallpaper extends BaseMethod {
54688
54915
  this.directoryReady = true;
54689
54916
  });
54690
54917
  }
54691
- upload() {
54918
+ upload(path, data) {
54692
54919
  return __awaiter(this, void 0, void 0, function* () {
54693
54920
  if (this.uploaded)
54694
54921
  return;
54695
- const { encoded } = this;
54696
- if (!encoded)
54697
- throw invalidParameter$1('Wallpaper data has not been initialized.');
54698
54922
  yield writeProtocolV2File({
54699
54923
  commands: this.device.commands,
54700
- path: this.path,
54701
- data: encoded.data,
54702
- totalSize: encoded.data.byteLength,
54924
+ path,
54925
+ data,
54926
+ totalSize: data.byteLength,
54703
54927
  chunkSize: this.params.chunkSize,
54704
54928
  maxChunkRetries: 3,
54705
54929
  overwrite: true,
@@ -54715,14 +54939,20 @@ class DeviceUploadWallpaper extends BaseMethod {
54715
54939
  });
54716
54940
  }
54717
54941
  run() {
54718
- var _a;
54942
+ var _a, _b, _c;
54719
54943
  return __awaiter(this, void 0, void 0, function* () {
54720
54944
  const { encoded } = this;
54721
54945
  if (!encoded)
54722
54946
  throw invalidParameter$1('Wallpaper data has not been initialized.');
54723
54947
  yield this.assertCapabilities();
54724
54948
  yield this.ensureDirectory();
54725
- yield this.upload();
54949
+ const useHostAssetPackage = supportsPro2HostAssetPackage((_b = (_a = this.device.state) === null || _a === void 0 ? void 0 : _a.versions.firmware) !== null && _b !== void 0 ? _b : undefined);
54950
+ const data = useHostAssetPackage
54951
+ ? buildPro2HostAssetPackage([{ name: WALLPAPER_PACKAGE_ENTRY, data: encoded.data }])
54952
+ : encoded.data;
54953
+ if (useHostAssetPackage)
54954
+ this.path = WALLPAPER_PACKAGE_PATH;
54955
+ yield this.upload(this.path, data);
54726
54956
  const response = yield this.device.commands.typedCall('DeviceSettingsSet', 'Success', {
54727
54957
  settings: { wallpaper_path: this.path },
54728
54958
  });
@@ -54736,9 +54966,9 @@ class DeviceUploadWallpaper extends BaseMethod {
54736
54966
  }
54737
54967
  return {
54738
54968
  path: this.path,
54739
- size: encoded.data.byteLength,
54969
+ size: data.byteLength,
54740
54970
  colorFormat: encoded.colorFormat,
54741
- message: (_a = response.message) === null || _a === void 0 ? void 0 : _a.message,
54971
+ message: (_c = response.message) === null || _c === void 0 ? void 0 : _c.message,
54742
54972
  };
54743
54973
  });
54744
54974
  }
@@ -54815,14 +55045,14 @@ class DeviceUploadNft extends BaseMethod {
54815
55045
  this.skipForceUpdateCheck = true;
54816
55046
  this.useDevicePassphraseState = false;
54817
55047
  }
54818
- assertCapabilities() {
55048
+ assertCapabilities(useHostAssetPackage) {
54819
55049
  return __awaiter(this, void 0, void 0, function* () {
54820
55050
  const protocolInfo = yield this.device.ensureProtocolV2RuntimeContext();
54821
55051
  const hasFileWrite = supportsProtocolV2Message(protocolInfo, FILESYSTEM_FILE_WRITE_MESSAGE_TYPE$1);
54822
55052
  const hasPathInfo = supportsProtocolV2Message(protocolInfo, FILESYSTEM_PATH_INFO_QUERY_MESSAGE_TYPE);
54823
55053
  const hasDirList = supportsProtocolV2Message(protocolInfo, FILESYSTEM_DIR_LIST_MESSAGE_TYPE);
54824
55054
  const hasNftUpdate = supportsProtocolV2Message(protocolInfo, NFT_UPDATE_MESSAGE_TYPE);
54825
- if (!hasFileWrite || !hasPathInfo || !hasDirList || !hasNftUpdate) {
55055
+ if (!hasFileWrite || !hasNftUpdate || (!useHostAssetPackage && (!hasPathInfo || !hasDirList))) {
54826
55056
  throw hdShared.createDeviceNotSupportMethodError(this.name, this.device.getCurrentFirmwareType());
54827
55057
  }
54828
55058
  });
@@ -54848,20 +55078,33 @@ class DeviceUploadNft extends BaseMethod {
54848
55078
  });
54849
55079
  }
54850
55080
  run() {
54851
- var _a;
55081
+ var _a, _b, _c;
54852
55082
  return __awaiter(this, void 0, void 0, function* () {
54853
55083
  const { bundle } = this;
54854
55084
  if (!bundle)
54855
55085
  throw invalidParameter$1('NFT data has not been initialized.');
54856
- yield this.assertCapabilities();
55086
+ const useHostAssetPackage = supportsPro2HostAssetPackage((_b = (_a = this.device.state) === null || _a === void 0 ? void 0 : _a.versions.firmware) !== null && _b !== void 0 ? _b : undefined);
55087
+ yield this.assertCapabilities(useHostAssetPackage);
54857
55088
  this.throwIfAborted();
54858
- yield this.assertStorageCapacity(bundle.basename);
55089
+ if (!useHostAssetPackage)
55090
+ yield this.assertStorageCapacity(bundle.basename);
54859
55091
  this.throwIfAborted();
54860
- const files = [
55092
+ const extractedFiles = [
54861
55093
  { path: `${PRO2_NFT_DIRECTORY}/${bundle.basename}.bin`, data: bundle.image },
54862
55094
  { path: `${PRO2_NFT_DIRECTORY}/${bundle.basename}_m.bin`, data: bundle.thumbnail },
54863
55095
  { path: `${PRO2_NFT_DIRECTORY}/${bundle.basename}.json`, data: bundle.metadata },
54864
55096
  ];
55097
+ const files = useHostAssetPackage
55098
+ ? [
55099
+ {
55100
+ path: `${PRO2_NFT_DIRECTORY}/${bundle.basename}.okpkg`,
55101
+ data: buildPro2HostAssetPackage(extractedFiles.map(file => ({
55102
+ name: file.path.slice(`${PRO2_NFT_DIRECTORY}/`.length),
55103
+ data: file.data,
55104
+ }))),
55105
+ },
55106
+ ]
55107
+ : extractedFiles;
54865
55108
  const totalSize = files.reduce((sum, file) => sum + file.data.byteLength, 0);
54866
55109
  let transferredBeforeFile = 0;
54867
55110
  for (const file of files) {
@@ -54890,12 +55133,12 @@ class DeviceUploadNft extends BaseMethod {
54890
55133
  const response = yield this.updateNft(bundle.basename);
54891
55134
  return {
54892
55135
  basename: bundle.basename,
54893
- imagePath: files[0].path,
54894
- thumbnailPath: files[1].path,
54895
- metadataPath: files[2].path,
55136
+ imagePath: extractedFiles[0].path,
55137
+ thumbnailPath: extractedFiles[1].path,
55138
+ metadataPath: extractedFiles[2].path,
54896
55139
  totalSize,
54897
55140
  nftUpdated: true,
54898
- message: (_a = response.message) === null || _a === void 0 ? void 0 : _a.message,
55141
+ message: (_c = response.message) === null || _c === void 0 ? void 0 : _c.message,
54899
55142
  };
54900
55143
  });
54901
55144
  }
@@ -58187,6 +58430,7 @@ class EVMSignTypedData extends BaseMethod {
58187
58430
  const currentDeviceType = this.device.getCurrentDeviceType();
58188
58431
  const supportBiggerDataVersion = '4.4.0';
58189
58432
  const supportBiggerData = DeviceModelToTypes.model_classic1s.includes(currentDeviceType) ||
58433
+ DeviceModelToTypes.model_pro2.includes(currentDeviceType) ||
58190
58434
  (DeviceModelToTypes.model_touch.includes(currentDeviceType) &&
58191
58435
  semver__default["default"].gte(currentVersion, supportBiggerDataVersion));
58192
58436
  if (supportBiggerData) {
@@ -0,0 +1,8 @@
1
+ export declare const PRO2_HOST_ASSET_PACKAGE_MIN_VERSION = "1.0.1";
2
+ export type Pro2HostAssetPackageEntry = {
3
+ name: string;
4
+ data: Uint8Array;
5
+ };
6
+ export declare function supportsPro2HostAssetPackage(firmwareVersion: string | undefined): boolean;
7
+ export declare function buildPro2HostAssetPackage(entries: Pro2HostAssetPackageEntry[]): Uint8Array;
8
+ //# sourceMappingURL=pro2HostAssetPackage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pro2HostAssetPackage.d.ts","sourceRoot":"","sources":["../../src/utils/pro2HostAssetPackage.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,mCAAmC,UAAU,CAAC;AAwB3D,MAAM,MAAM,yBAAyB,GAAG;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,CAAC;CAClB,CAAC;AAQF,wBAAgB,4BAA4B,CAAC,eAAe,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAMzF;AA2QD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,yBAAyB,EAAE,GAAG,UAAU,CA4B1F"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-core",
3
- "version": "1.2.1",
3
+ "version": "1.2.2-alpha.1",
4
4
  "description": "Core processes and APIs for communicating with OneKey hardware devices.",
5
5
  "author": "OneKey",
6
6
  "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
@@ -25,8 +25,8 @@
25
25
  "url": "https://github.com/OneKeyHQ/hardware-js-sdk/issues"
26
26
  },
27
27
  "dependencies": {
28
- "@onekeyfe/hd-shared": "1.2.1",
29
- "@onekeyfe/hd-transport": "1.2.1",
28
+ "@onekeyfe/hd-shared": "1.2.2-alpha.1",
29
+ "@onekeyfe/hd-transport": "1.2.2-alpha.1",
30
30
  "axios": "1.15.2",
31
31
  "bignumber.js": "^9.0.2",
32
32
  "buffer": "^6.0.3",
@@ -46,5 +46,5 @@
46
46
  "@types/w3c-web-usb": "^1.0.10",
47
47
  "@types/web-bluetooth": "^0.0.21"
48
48
  },
49
- "gitHead": "43d2856e593a93fbf056fdb334e0fe407a79cdc0"
49
+ "gitHead": "66f45610d36e06a46ac82cd49a4b0e1a128f19d4"
50
50
  }
@@ -360,6 +360,7 @@ export default class EVMSignTypedData extends BaseMethod<EVMSignTypedDataParams>
360
360
 
361
361
  const supportBiggerData =
362
362
  DeviceModelToTypes.model_classic1s.includes(currentDeviceType) ||
363
+ DeviceModelToTypes.model_pro2.includes(currentDeviceType) ||
363
364
  (DeviceModelToTypes.model_touch.includes(currentDeviceType) &&
364
365
  semver.gte(currentVersion, supportBiggerDataVersion));
365
366
 
@@ -20,6 +20,10 @@ import {
20
20
  getCompletePro2NftBasenames,
21
21
  } from '../../utils/pro2Nft';
22
22
  import { encodePro2Image } from '../../utils/pro2Wallpaper';
23
+ import {
24
+ buildPro2HostAssetPackage,
25
+ supportsPro2HostAssetPackage,
26
+ } from '../../utils/pro2HostAssetPackage';
23
27
  import { BaseMethod } from '../BaseMethod';
24
28
  import { decodeJpegBase64ToRgba } from '../helpers/base64Data';
25
29
  import { invalidParameter } from '../helpers/filesystemValidation';
@@ -38,8 +42,11 @@ export type DeviceUploadNftParams = {
38
42
 
39
43
  export type DeviceUploadNftResponse = {
40
44
  basename: string;
45
+ /** Final image path after firmware extracts a host-asset package. */
41
46
  imagePath: string;
47
+ /** Final thumbnail path after firmware extracts a host-asset package. */
42
48
  thumbnailPath: string;
49
+ /** Final metadata path after firmware extracts a host-asset package. */
43
50
  metadataPath: string;
44
51
  totalSize: number;
45
52
  nftUpdated: true;
@@ -138,7 +145,7 @@ export default class DeviceUploadNft extends BaseMethod<DeviceUploadNftParams> {
138
145
  this.useDevicePassphraseState = false;
139
146
  }
140
147
 
141
- private async assertCapabilities() {
148
+ private async assertCapabilities(useHostAssetPackage: boolean) {
142
149
  const protocolInfo = await this.device.ensureProtocolV2RuntimeContext();
143
150
  const hasFileWrite = supportsProtocolV2Message(
144
151
  protocolInfo,
@@ -150,7 +157,7 @@ export default class DeviceUploadNft extends BaseMethod<DeviceUploadNftParams> {
150
157
  );
151
158
  const hasDirList = supportsProtocolV2Message(protocolInfo, FILESYSTEM_DIR_LIST_MESSAGE_TYPE);
152
159
  const hasNftUpdate = supportsProtocolV2Message(protocolInfo, NFT_UPDATE_MESSAGE_TYPE);
153
- if (!hasFileWrite || !hasPathInfo || !hasDirList || !hasNftUpdate) {
160
+ if (!hasFileWrite || !hasNftUpdate || (!useHostAssetPackage && (!hasPathInfo || !hasDirList))) {
154
161
  throw createDeviceNotSupportMethodError(this.name, this.device.getCurrentFirmwareType());
155
162
  }
156
163
  }
@@ -192,16 +199,32 @@ export default class DeviceUploadNft extends BaseMethod<DeviceUploadNftParams> {
192
199
  const { bundle } = this;
193
200
  if (!bundle) throw invalidParameter('NFT data has not been initialized.');
194
201
 
195
- await this.assertCapabilities();
202
+ const useHostAssetPackage = supportsPro2HostAssetPackage(
203
+ this.device.state?.versions.firmware ?? undefined
204
+ );
205
+ await this.assertCapabilities(useHostAssetPackage);
196
206
  this.throwIfAborted();
197
- await this.assertStorageCapacity(bundle.basename);
207
+ if (!useHostAssetPackage) await this.assertStorageCapacity(bundle.basename);
198
208
  this.throwIfAborted();
199
209
 
200
- const files = [
210
+ const extractedFiles = [
201
211
  { path: `${PRO2_NFT_DIRECTORY}/${bundle.basename}.bin`, data: bundle.image },
202
212
  { path: `${PRO2_NFT_DIRECTORY}/${bundle.basename}_m.bin`, data: bundle.thumbnail },
203
213
  { path: `${PRO2_NFT_DIRECTORY}/${bundle.basename}.json`, data: bundle.metadata },
204
214
  ];
215
+ const files = useHostAssetPackage
216
+ ? [
217
+ {
218
+ path: `${PRO2_NFT_DIRECTORY}/${bundle.basename}.okpkg`,
219
+ data: buildPro2HostAssetPackage(
220
+ extractedFiles.map(file => ({
221
+ name: file.path.slice(`${PRO2_NFT_DIRECTORY}/`.length),
222
+ data: file.data,
223
+ }))
224
+ ),
225
+ },
226
+ ]
227
+ : extractedFiles;
205
228
  const totalSize = files.reduce((sum, file) => sum + file.data.byteLength, 0);
206
229
  let transferredBeforeFile = 0;
207
230
 
@@ -238,9 +261,9 @@ export default class DeviceUploadNft extends BaseMethod<DeviceUploadNftParams> {
238
261
  const response = await this.updateNft(bundle.basename);
239
262
  return {
240
263
  basename: bundle.basename,
241
- imagePath: files[0].path,
242
- thumbnailPath: files[1].path,
243
- metadataPath: files[2].path,
264
+ imagePath: extractedFiles[0].path,
265
+ thumbnailPath: extractedFiles[1].path,
266
+ metadataPath: extractedFiles[2].path,
244
267
  totalSize,
245
268
  nftUpdated: true,
246
269
  message: response.message?.message,
@@ -16,14 +16,26 @@ import {
16
16
  type Pro2WallpaperColorFormat,
17
17
  encodePro2Wallpaper,
18
18
  } from '../../utils/pro2Wallpaper';
19
+ import {
20
+ buildPro2HostAssetPackage,
21
+ supportsPro2HostAssetPackage,
22
+ } from '../../utils/pro2HostAssetPackage';
19
23
 
20
24
  export type DeviceUploadWallpaperParams = {
21
25
  jpegBase64: string;
26
+ /**
27
+ * Legacy firmware uses this name for the uploaded `.bin` file. Firmware
28
+ * 1.0.1+ always consumes the fixed `wallpaper.okpkg` package path.
29
+ */
22
30
  fileName?: string;
23
31
  chunkSize?: number;
24
32
  };
25
33
 
26
34
  export type DeviceUploadWallpaperResponse = {
35
+ /**
36
+ * Filesystem path sent to `DeviceSettingsSet`. On firmware 1.0.1+ this is
37
+ * the temporary package path; firmware extracts and persists wallpaper.bin.
38
+ */
27
39
  path: string;
28
40
  size: number;
29
41
  colorFormat: Pro2WallpaperColorFormat;
@@ -31,6 +43,8 @@ export type DeviceUploadWallpaperResponse = {
31
43
  };
32
44
 
33
45
  const WALLPAPER_DIRECTORY = 'vol1:/wallpapers';
46
+ const WALLPAPER_PACKAGE_PATH = `${WALLPAPER_DIRECTORY}/wallpaper.okpkg`;
47
+ const WALLPAPER_PACKAGE_ENTRY = 'wallpaper.bin';
34
48
  const SAFE_FILE_NAME = /^[A-Za-z0-9_-]+(?:\.bin)?$/;
35
49
  const DEVICE_SETTINGS_SET_MESSAGE_TYPE = 60412;
36
50
  const FILESYSTEM_FILE_WRITE_MESSAGE_TYPE = 60805;
@@ -116,16 +130,14 @@ export default class DeviceUploadWallpaper extends BaseMethod<DeviceUploadWallpa
116
130
  this.directoryReady = true;
117
131
  }
118
132
 
119
- private async upload() {
133
+ private async upload(path: string, data: Uint8Array) {
120
134
  if (this.uploaded) return;
121
- const { encoded } = this;
122
- if (!encoded) throw invalidParameter('Wallpaper data has not been initialized.');
123
135
 
124
136
  await writeProtocolV2File({
125
137
  commands: this.device.commands,
126
- path: this.path,
127
- data: encoded.data,
128
- totalSize: encoded.data.byteLength,
138
+ path,
139
+ data,
140
+ totalSize: data.byteLength,
129
141
  chunkSize: this.params.chunkSize,
130
142
  maxChunkRetries: 3,
131
143
  overwrite: true,
@@ -145,7 +157,14 @@ export default class DeviceUploadWallpaper extends BaseMethod<DeviceUploadWallpa
145
157
  if (!encoded) throw invalidParameter('Wallpaper data has not been initialized.');
146
158
  await this.assertCapabilities();
147
159
  await this.ensureDirectory();
148
- await this.upload();
160
+ const useHostAssetPackage = supportsPro2HostAssetPackage(
161
+ this.device.state?.versions.firmware ?? undefined
162
+ );
163
+ const data = useHostAssetPackage
164
+ ? buildPro2HostAssetPackage([{ name: WALLPAPER_PACKAGE_ENTRY, data: encoded.data }])
165
+ : encoded.data;
166
+ if (useHostAssetPackage) this.path = WALLPAPER_PACKAGE_PATH;
167
+ await this.upload(this.path, data);
149
168
  const response = await this.device.commands.typedCall('DeviceSettingsSet', 'Success', {
150
169
  settings: { wallpaper_path: this.path },
151
170
  });
@@ -160,7 +179,7 @@ export default class DeviceUploadWallpaper extends BaseMethod<DeviceUploadWallpa
160
179
  }
161
180
  return {
162
181
  path: this.path,
163
- size: encoded.data.byteLength,
182
+ size: data.byteLength,
164
183
  colorFormat: encoded.colorFormat,
165
184
  message: response.message?.message,
166
185
  };
@@ -0,0 +1,354 @@
1
+ import { sha3_512 } from '@noble/hashes/sha3';
2
+ import semver from 'semver';
3
+
4
+ /**
5
+ * Builds the unsigned host-asset package consumed by Pro2 firmware:
6
+ *
7
+ * OKPP RESOURCE container
8
+ * └── OKAR archive
9
+ * └── independently compressed raw LZ4 blocks
10
+ *
11
+ * OKPP and OKAR are OneKey formats. Their constants mirror firmware-pro2's
12
+ * payload_package headers; the LZ4 bytes inside each block follow the standard
13
+ * raw block format and deliberately do not use an LZ4 frame or size prefix.
14
+ */
15
+
16
+ /* eslint-disable no-bitwise */
17
+
18
+ export const PRO2_HOST_ASSET_PACKAGE_MIN_VERSION = '1.0.1';
19
+
20
+ // OKPP container layout. The fixed header has seven empty signature slots even
21
+ // for a host-generated unsigned package.
22
+ const CONTAINER_HEADER_SIZE = 0x5f90;
23
+ const CONTAINER_HEADER_HASH_INPUT_LENGTH = 0x240;
24
+ const CONTAINER_HASH_SECTION_OFFSET = 0x200;
25
+ const CONTAINER_SIGNATURE_ALGORITHM_OFFSET = 0x408;
26
+ const CONTAINER_HEADER_MAGIC = 0x50504b4f;
27
+ const CONTAINER_HEADER_VERSION = 1;
28
+ const CONTAINER_RESOURCE_TYPE_MAGIC = 0x43534552;
29
+ const CONTAINER_ED25519_SIGNATURE_ALGORITHM = 0x71717171;
30
+ const HOST_ASSET_PACKAGE_MAX_SIZE = 4 * 1024 * 1024;
31
+
32
+ // OKAR archive layout.
33
+ const ARCHIVE_MAGIC = 0x52414b4f;
34
+ const ARCHIVE_VERSION = 1;
35
+ const ARCHIVE_HEADER_SIZE = 42;
36
+ const ARCHIVE_ENTRY_SIZE = 296;
37
+ const ARCHIVE_ENTRY_NAME_MAX_LENGTH = 255;
38
+ const ARCHIVE_COMPRESS_LZ4_BLOCKED = 1;
39
+ const ARCHIVE_ALIGNMENT = 4;
40
+ const LZ4_BLOCK_SIZE_LOG2 = 12;
41
+
42
+ export type Pro2HostAssetPackageEntry = {
43
+ name: string;
44
+ data: Uint8Array;
45
+ };
46
+
47
+ type EncodedArchiveEntry = Pro2HostAssetPackageEntry & {
48
+ nameBytes: Uint8Array;
49
+ compressed: Uint8Array;
50
+ offset: number;
51
+ };
52
+
53
+ export function supportsPro2HostAssetPackage(firmwareVersion: string | undefined): boolean {
54
+ return Boolean(
55
+ firmwareVersion &&
56
+ semver.valid(firmwareVersion) &&
57
+ semver.gte(firmwareVersion, PRO2_HOST_ASSET_PACKAGE_MIN_VERSION)
58
+ );
59
+ }
60
+
61
+ function concatBytes(parts: Uint8Array[]): Uint8Array {
62
+ const output = new Uint8Array(parts.reduce((length, part) => length + part.byteLength, 0));
63
+ let offset = 0;
64
+ for (const part of parts) {
65
+ output.set(part, offset);
66
+ offset += part.byteLength;
67
+ }
68
+ return output;
69
+ }
70
+
71
+ // Raw LZ4 block encoder
72
+ // ---------------------
73
+ //
74
+ // This encoder is adapted from lz4-lite 1.1.2 and intentionally kept local so
75
+ // every SDK runtime uses the same dependency-free implementation. Keep this
76
+ // section isolated from the OneKey package writer below. Firmware requires raw
77
+ // blocks here; replacing it with an LZ4 frame encoder is not compatible.
78
+ //
79
+ // MIT License
80
+ // Copyright (c) 2026 Alexander Vukov
81
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
82
+ // of this software and associated documentation files (the "Software"), to deal
83
+ // in the Software without restriction, including without limitation the rights
84
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
85
+ // copies of the Software, and to permit persons to whom the Software is
86
+ // furnished to do so, subject to the following conditions:
87
+ // The above copyright notice and this permission notice shall be included in all
88
+ // copies or substantial portions of the Software.
89
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
90
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
91
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
92
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
93
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
94
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
95
+ // SOFTWARE.
96
+
97
+ const LZ4_MIN_MATCH = 4;
98
+ const LZ4_LAST_LITERALS = 5;
99
+ const LZ4_MATCH_FIND_LIMIT = 12;
100
+ const LZ4_MAX_OFFSET = 0xffff;
101
+ const LZ4_LENGTH_MASK = 15;
102
+ const LZ4_HASH_LOG = 16;
103
+ const LZ4_HASH_MULTIPLIER = 2654435761;
104
+ const LZ4_SKIP_TRIGGER = 6;
105
+
106
+ function writeExtendedLength(output: Uint8Array, offset: number, length: number): number {
107
+ let remaining = length;
108
+ let nextOffset = offset;
109
+ while (remaining >= 255) {
110
+ output[nextOffset] = 255;
111
+ nextOffset += 1;
112
+ remaining -= 255;
113
+ }
114
+ output[nextOffset] = remaining;
115
+ return nextOffset + 1;
116
+ }
117
+
118
+ function copyBytes(
119
+ output: Uint8Array,
120
+ outputOffset: number,
121
+ input: Uint8Array,
122
+ inputOffset: number,
123
+ length: number
124
+ ): number {
125
+ output.set(input.subarray(inputOffset, inputOffset + length), outputOffset);
126
+ return outputOffset + length;
127
+ }
128
+
129
+ function emitSequence(
130
+ output: Uint8Array,
131
+ outputOffset: number,
132
+ input: Uint8Array,
133
+ anchor: number,
134
+ literalLength: number,
135
+ matchOffset: number,
136
+ matchLengthCode: number
137
+ ): number {
138
+ const tokenOffset = outputOffset;
139
+ let nextOffset = outputOffset + 1;
140
+ let token = 0;
141
+
142
+ if (literalLength >= LZ4_LENGTH_MASK) {
143
+ token = LZ4_LENGTH_MASK << 4;
144
+ nextOffset = writeExtendedLength(output, nextOffset, literalLength - LZ4_LENGTH_MASK);
145
+ } else {
146
+ token = literalLength << 4;
147
+ }
148
+ nextOffset = copyBytes(output, nextOffset, input, anchor, literalLength);
149
+ output[nextOffset] = matchOffset & 0xff;
150
+ output[nextOffset + 1] = (matchOffset >>> 8) & 0xff;
151
+ nextOffset += 2;
152
+
153
+ if (matchLengthCode >= LZ4_LENGTH_MASK) {
154
+ token |= LZ4_LENGTH_MASK;
155
+ nextOffset = writeExtendedLength(output, nextOffset, matchLengthCode - LZ4_LENGTH_MASK);
156
+ } else {
157
+ token |= matchLengthCode;
158
+ }
159
+ output[tokenOffset] = token;
160
+ return nextOffset;
161
+ }
162
+
163
+ function emitLastLiterals(
164
+ output: Uint8Array,
165
+ outputOffset: number,
166
+ input: Uint8Array,
167
+ anchor: number,
168
+ literalLength: number
169
+ ): number {
170
+ const tokenOffset = outputOffset;
171
+ let nextOffset = outputOffset + 1;
172
+ if (literalLength >= LZ4_LENGTH_MASK) {
173
+ output[tokenOffset] = LZ4_LENGTH_MASK << 4;
174
+ nextOffset = writeExtendedLength(output, nextOffset, literalLength - LZ4_LENGTH_MASK);
175
+ } else {
176
+ output[tokenOffset] = literalLength << 4;
177
+ }
178
+ return copyBytes(output, nextOffset, input, anchor, literalLength);
179
+ }
180
+
181
+ function compressRawLz4Block(input: Uint8Array, hashTable: Uint32Array): Uint8Array {
182
+ const output = new Uint8Array(input.byteLength + Math.floor(input.byteLength / 255) + 16);
183
+ const inputView = new DataView(input.buffer, input.byteOffset, input.byteLength);
184
+ const matchFindLimit = input.byteLength - LZ4_MATCH_FIND_LIMIT;
185
+ const matchExtendLimit = input.byteLength - LZ4_LAST_LITERALS;
186
+ let anchor = 0;
187
+ let inputOffset = 0;
188
+ let outputOffset = 0;
189
+ let searchMatchCount = 1 << LZ4_SKIP_TRIGGER;
190
+
191
+ hashTable.fill(0);
192
+ while (inputOffset < matchFindLimit) {
193
+ const sequence = inputView.getUint32(inputOffset, true);
194
+ const hash = Math.imul(sequence, LZ4_HASH_MULTIPLIER) >>> (32 - LZ4_HASH_LOG);
195
+ const candidate = hashTable[hash] - 1;
196
+ hashTable[hash] = inputOffset + 1;
197
+
198
+ const hasMatch = !(
199
+ candidate < 0 ||
200
+ inputOffset - candidate > LZ4_MAX_OFFSET ||
201
+ inputView.getUint32(candidate, true) !== sequence
202
+ );
203
+ if (!hasMatch) {
204
+ inputOffset += searchMatchCount >> LZ4_SKIP_TRIGGER;
205
+ searchMatchCount += 1;
206
+ } else {
207
+ searchMatchCount = 1 << LZ4_SKIP_TRIGGER;
208
+ let matchEnd = inputOffset + LZ4_MIN_MATCH;
209
+ let reference = candidate + LZ4_MIN_MATCH;
210
+ while (matchEnd < matchExtendLimit && input[matchEnd] === input[reference]) {
211
+ matchEnd += 1;
212
+ reference += 1;
213
+ }
214
+ outputOffset = emitSequence(
215
+ output,
216
+ outputOffset,
217
+ input,
218
+ anchor,
219
+ inputOffset - anchor,
220
+ inputOffset - candidate,
221
+ matchEnd - inputOffset - LZ4_MIN_MATCH
222
+ );
223
+ inputOffset = matchEnd;
224
+ anchor = inputOffset;
225
+ }
226
+ }
227
+
228
+ outputOffset = emitLastLiterals(output, outputOffset, input, anchor, input.byteLength - anchor);
229
+ return output.slice(0, outputOffset);
230
+ }
231
+
232
+ // OneKey LZ4-blocked wrapper
233
+ // --------------------------
234
+ // The archive stores an 8-byte descriptor, one compressed-size value per
235
+ // block, then the concatenated raw blocks. Blocks are independent so firmware
236
+ // can validate and decompress them with bounded memory.
237
+ function encodeLz4Blocked(data: Uint8Array): Uint8Array {
238
+ const blockSize = 1 << LZ4_BLOCK_SIZE_LOG2;
239
+ const blockCount = Math.ceil(data.byteLength / blockSize);
240
+ const hashTable = new Uint32Array(1 << LZ4_HASH_LOG);
241
+ const blocks: Uint8Array[] = [];
242
+ const header = new Uint8Array(8 + blockCount * 4);
243
+ const headerView = new DataView(header.buffer);
244
+ headerView.setUint16(0, blockCount, true);
245
+ headerView.setUint16(2, LZ4_BLOCK_SIZE_LOG2, true);
246
+
247
+ for (let index = 0; index < blockCount; index += 1) {
248
+ const block = compressRawLz4Block(
249
+ data.subarray(index * blockSize, Math.min((index + 1) * blockSize, data.byteLength)),
250
+ hashTable
251
+ );
252
+ headerView.setUint32(8 + index * 4, block.byteLength, true);
253
+ blocks.push(block);
254
+ }
255
+ return concatBytes([header, ...blocks]);
256
+ }
257
+
258
+ // OKAR integrity helpers
259
+ // ----------------------
260
+ const CRC32_TABLE = (() => {
261
+ const table = new Uint32Array(256);
262
+ for (let index = 0; index < table.length; index += 1) {
263
+ let value = index;
264
+ for (let bit = 0; bit < 8; bit += 1) {
265
+ value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
266
+ }
267
+ table[index] = value >>> 0;
268
+ }
269
+ return table;
270
+ })();
271
+
272
+ function crc32(data: Uint8Array): number {
273
+ let value = 0xffffffff;
274
+ for (const byte of data) {
275
+ value = CRC32_TABLE[(value ^ byte) & 0xff] ^ (value >>> 8);
276
+ }
277
+ return (value ^ 0xffffffff) >>> 0;
278
+ }
279
+
280
+ function align(value: number): number {
281
+ return (value + ARCHIVE_ALIGNMENT - 1) & ~(ARCHIVE_ALIGNMENT - 1);
282
+ }
283
+
284
+ // OKAR archive writer
285
+ // -------------------
286
+ function buildArchive(entries: Pro2HostAssetPackageEntry[]): Uint8Array {
287
+ const textEncoder = new TextEncoder();
288
+ let dataOffset = ARCHIVE_HEADER_SIZE + entries.length * ARCHIVE_ENTRY_SIZE;
289
+ const encodedEntries: EncodedArchiveEntry[] = entries.map(entry => {
290
+ const nameBytes = textEncoder.encode(entry.name);
291
+ if (!entry.name || nameBytes.byteLength > ARCHIVE_ENTRY_NAME_MAX_LENGTH) {
292
+ throw new Error('Pro2 host asset package entry names must contain 1 to 255 UTF-8 bytes.');
293
+ }
294
+ if (!(entry.data instanceof Uint8Array) || entry.data.byteLength === 0) {
295
+ throw new Error(`Pro2 host asset package entry [${entry.name}] must not be empty.`);
296
+ }
297
+ const compressed = encodeLz4Blocked(entry.data);
298
+ const offset = align(dataOffset);
299
+ dataOffset = offset + compressed.byteLength;
300
+ return { ...entry, nameBytes, compressed, offset };
301
+ });
302
+
303
+ const archive = new Uint8Array(dataOffset);
304
+ const view = new DataView(archive.buffer);
305
+ view.setUint32(0, ARCHIVE_MAGIC, true);
306
+ view.setUint32(4, ARCHIVE_VERSION, true);
307
+ view.setUint16(8, encodedEntries.length, true);
308
+
309
+ encodedEntries.forEach((entry, index) => {
310
+ const recordOffset = ARCHIVE_HEADER_SIZE + index * ARCHIVE_ENTRY_SIZE;
311
+ archive[recordOffset] = entry.nameBytes.byteLength;
312
+ archive.set(entry.nameBytes, recordOffset + 1);
313
+ view.setUint32(recordOffset + 0x100, entry.offset, true);
314
+ view.setUint32(recordOffset + 0x104, entry.data.byteLength, true);
315
+ view.setUint32(recordOffset + 0x108, entry.compressed.byteLength, true);
316
+ view.setUint32(recordOffset + 0x10c, crc32(entry.data), true);
317
+ view.setUint32(recordOffset + 0x110, crc32(entry.compressed), true);
318
+ archive[recordOffset + 0x114] = ARCHIVE_COMPRESS_LZ4_BLOCKED;
319
+ archive.set(entry.compressed, entry.offset);
320
+ });
321
+ return archive;
322
+ }
323
+
324
+ // OKPP container writer
325
+ // ---------------------
326
+ export function buildPro2HostAssetPackage(entries: Pro2HostAssetPackageEntry[]): Uint8Array {
327
+ if (entries.length === 0 || new Set(entries.map(entry => entry.name)).size !== entries.length) {
328
+ throw new Error('Pro2 host asset package entries must have unique, non-empty names.');
329
+ }
330
+
331
+ const payload = buildArchive(entries);
332
+ const header = new Uint8Array(CONTAINER_HEADER_SIZE);
333
+ const view = new DataView(header.buffer);
334
+ view.setUint32(0, CONTAINER_HEADER_MAGIC, true);
335
+ view.setUint32(4, CONTAINER_HEADER_VERSION, true);
336
+ view.setUint32(8, CONTAINER_RESOURCE_TYPE_MAGIC, true);
337
+ view.setUint32(0x0c, CONTAINER_HEADER_SIZE, true);
338
+ view.setUint32(0x10, 1, true);
339
+ view.setUint32(0x14, payload.byteLength, true);
340
+
341
+ // Host asset packages are unsigned, but firmware still requires a valid
342
+ // payload hash, header hash, and the Ed25519 algorithm discriminator. The
343
+ // zero-initialized header intentionally leaves sig_used_count and every
344
+ // signature slot empty.
345
+ header.set(sha3_512(payload), CONTAINER_HASH_SECTION_OFFSET);
346
+ header.set(sha3_512(header.subarray(0, CONTAINER_HEADER_HASH_INPUT_LENGTH)), 0x240);
347
+ view.setUint32(CONTAINER_SIGNATURE_ALGORITHM_OFFSET, CONTAINER_ED25519_SIGNATURE_ALGORITHM, true);
348
+
349
+ const packageData = concatBytes([header, payload]);
350
+ if (packageData.byteLength > HOST_ASSET_PACKAGE_MAX_SIZE) {
351
+ throw new Error('Pro2 host asset package exceeds the firmware 4 MiB limit.');
352
+ }
353
+ return packageData;
354
+ }