@onekeyfe/hd-core 1.2.1 → 1.2.2-alpha.0
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__/deviceUploadNft.test.ts +41 -0
- package/__tests__/evmSignTypedData.test.ts +42 -0
- package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +21 -322
- package/__tests__/pro2HostAssetPackage.test.ts +58 -0
- package/__tests__/protocol-v2.test.ts +46 -1
- package/dist/api/FirmwareUpdateV4.d.ts +0 -1
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/evm/EVMSignTypedData.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
- package/dist/index.js +273 -45
- package/dist/utils/pro2HostAssetPackage.d.ts +8 -0
- package/dist/utils/pro2HostAssetPackage.d.ts.map +1 -0
- package/package.json +4 -4
- package/src/api/FirmwareUpdateV4.ts +13 -31
- package/src/api/evm/EVMSignTypedData.ts +1 -0
- package/src/api/protocol-v2/DeviceUploadNft.ts +28 -8
- package/src/api/protocol-v2/DeviceUploadWallpaper.ts +19 -8
- package/src/utils/pro2HostAssetPackage.ts +315 -0
|
@@ -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';
|
|
@@ -138,7 +142,7 @@ export default class DeviceUploadNft extends BaseMethod<DeviceUploadNftParams> {
|
|
|
138
142
|
this.useDevicePassphraseState = false;
|
|
139
143
|
}
|
|
140
144
|
|
|
141
|
-
private async assertCapabilities() {
|
|
145
|
+
private async assertCapabilities(useHostAssetPackage: boolean) {
|
|
142
146
|
const protocolInfo = await this.device.ensureProtocolV2RuntimeContext();
|
|
143
147
|
const hasFileWrite = supportsProtocolV2Message(
|
|
144
148
|
protocolInfo,
|
|
@@ -150,7 +154,7 @@ export default class DeviceUploadNft extends BaseMethod<DeviceUploadNftParams> {
|
|
|
150
154
|
);
|
|
151
155
|
const hasDirList = supportsProtocolV2Message(protocolInfo, FILESYSTEM_DIR_LIST_MESSAGE_TYPE);
|
|
152
156
|
const hasNftUpdate = supportsProtocolV2Message(protocolInfo, NFT_UPDATE_MESSAGE_TYPE);
|
|
153
|
-
if (!hasFileWrite || !
|
|
157
|
+
if (!hasFileWrite || !hasNftUpdate || (!useHostAssetPackage && (!hasPathInfo || !hasDirList))) {
|
|
154
158
|
throw createDeviceNotSupportMethodError(this.name, this.device.getCurrentFirmwareType());
|
|
155
159
|
}
|
|
156
160
|
}
|
|
@@ -192,16 +196,32 @@ export default class DeviceUploadNft extends BaseMethod<DeviceUploadNftParams> {
|
|
|
192
196
|
const { bundle } = this;
|
|
193
197
|
if (!bundle) throw invalidParameter('NFT data has not been initialized.');
|
|
194
198
|
|
|
195
|
-
|
|
199
|
+
const useHostAssetPackage = supportsPro2HostAssetPackage(
|
|
200
|
+
this.device.getCurrentFirmwareVersionString()
|
|
201
|
+
);
|
|
202
|
+
await this.assertCapabilities(useHostAssetPackage);
|
|
196
203
|
this.throwIfAborted();
|
|
197
|
-
await this.assertStorageCapacity(bundle.basename);
|
|
204
|
+
if (!useHostAssetPackage) await this.assertStorageCapacity(bundle.basename);
|
|
198
205
|
this.throwIfAborted();
|
|
199
206
|
|
|
200
|
-
const
|
|
207
|
+
const extractedFiles = [
|
|
201
208
|
{ path: `${PRO2_NFT_DIRECTORY}/${bundle.basename}.bin`, data: bundle.image },
|
|
202
209
|
{ path: `${PRO2_NFT_DIRECTORY}/${bundle.basename}_m.bin`, data: bundle.thumbnail },
|
|
203
210
|
{ path: `${PRO2_NFT_DIRECTORY}/${bundle.basename}.json`, data: bundle.metadata },
|
|
204
211
|
];
|
|
212
|
+
const files = useHostAssetPackage
|
|
213
|
+
? [
|
|
214
|
+
{
|
|
215
|
+
path: `${PRO2_NFT_DIRECTORY}/${bundle.basename}.okpkg`,
|
|
216
|
+
data: buildPro2HostAssetPackage(
|
|
217
|
+
extractedFiles.map(file => ({
|
|
218
|
+
name: file.path.slice(`${PRO2_NFT_DIRECTORY}/`.length),
|
|
219
|
+
data: file.data,
|
|
220
|
+
}))
|
|
221
|
+
),
|
|
222
|
+
},
|
|
223
|
+
]
|
|
224
|
+
: extractedFiles;
|
|
205
225
|
const totalSize = files.reduce((sum, file) => sum + file.data.byteLength, 0);
|
|
206
226
|
let transferredBeforeFile = 0;
|
|
207
227
|
|
|
@@ -238,9 +258,9 @@ export default class DeviceUploadNft extends BaseMethod<DeviceUploadNftParams> {
|
|
|
238
258
|
const response = await this.updateNft(bundle.basename);
|
|
239
259
|
return {
|
|
240
260
|
basename: bundle.basename,
|
|
241
|
-
imagePath:
|
|
242
|
-
thumbnailPath:
|
|
243
|
-
metadataPath:
|
|
261
|
+
imagePath: extractedFiles[0].path,
|
|
262
|
+
thumbnailPath: extractedFiles[1].path,
|
|
263
|
+
metadataPath: extractedFiles[2].path,
|
|
244
264
|
totalSize,
|
|
245
265
|
nftUpdated: true,
|
|
246
266
|
message: response.message?.message,
|
|
@@ -16,6 +16,10 @@ 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;
|
|
@@ -31,6 +35,8 @@ export type DeviceUploadWallpaperResponse = {
|
|
|
31
35
|
};
|
|
32
36
|
|
|
33
37
|
const WALLPAPER_DIRECTORY = 'vol1:/wallpapers';
|
|
38
|
+
const WALLPAPER_PACKAGE_PATH = `${WALLPAPER_DIRECTORY}/wallpaper.okpkg`;
|
|
39
|
+
const WALLPAPER_PACKAGE_ENTRY = 'wallpaper.bin';
|
|
34
40
|
const SAFE_FILE_NAME = /^[A-Za-z0-9_-]+(?:\.bin)?$/;
|
|
35
41
|
const DEVICE_SETTINGS_SET_MESSAGE_TYPE = 60412;
|
|
36
42
|
const FILESYSTEM_FILE_WRITE_MESSAGE_TYPE = 60805;
|
|
@@ -116,16 +122,14 @@ export default class DeviceUploadWallpaper extends BaseMethod<DeviceUploadWallpa
|
|
|
116
122
|
this.directoryReady = true;
|
|
117
123
|
}
|
|
118
124
|
|
|
119
|
-
private async upload() {
|
|
125
|
+
private async upload(path: string, data: Uint8Array) {
|
|
120
126
|
if (this.uploaded) return;
|
|
121
|
-
const { encoded } = this;
|
|
122
|
-
if (!encoded) throw invalidParameter('Wallpaper data has not been initialized.');
|
|
123
127
|
|
|
124
128
|
await writeProtocolV2File({
|
|
125
129
|
commands: this.device.commands,
|
|
126
|
-
path
|
|
127
|
-
data
|
|
128
|
-
totalSize:
|
|
130
|
+
path,
|
|
131
|
+
data,
|
|
132
|
+
totalSize: data.byteLength,
|
|
129
133
|
chunkSize: this.params.chunkSize,
|
|
130
134
|
maxChunkRetries: 3,
|
|
131
135
|
overwrite: true,
|
|
@@ -145,7 +149,14 @@ export default class DeviceUploadWallpaper extends BaseMethod<DeviceUploadWallpa
|
|
|
145
149
|
if (!encoded) throw invalidParameter('Wallpaper data has not been initialized.');
|
|
146
150
|
await this.assertCapabilities();
|
|
147
151
|
await this.ensureDirectory();
|
|
148
|
-
|
|
152
|
+
const useHostAssetPackage = supportsPro2HostAssetPackage(
|
|
153
|
+
this.device.getCurrentFirmwareVersionString()
|
|
154
|
+
);
|
|
155
|
+
const data = useHostAssetPackage
|
|
156
|
+
? buildPro2HostAssetPackage([{ name: WALLPAPER_PACKAGE_ENTRY, data: encoded.data }])
|
|
157
|
+
: encoded.data;
|
|
158
|
+
if (useHostAssetPackage) this.path = WALLPAPER_PACKAGE_PATH;
|
|
159
|
+
await this.upload(this.path, data);
|
|
149
160
|
const response = await this.device.commands.typedCall('DeviceSettingsSet', 'Success', {
|
|
150
161
|
settings: { wallpaper_path: this.path },
|
|
151
162
|
});
|
|
@@ -160,7 +171,7 @@ export default class DeviceUploadWallpaper extends BaseMethod<DeviceUploadWallpa
|
|
|
160
171
|
}
|
|
161
172
|
return {
|
|
162
173
|
path: this.path,
|
|
163
|
-
size:
|
|
174
|
+
size: data.byteLength,
|
|
164
175
|
colorFormat: encoded.colorFormat,
|
|
165
176
|
message: response.message?.message,
|
|
166
177
|
};
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { sha3_512 } from '@noble/hashes/sha3';
|
|
2
|
+
import semver from 'semver';
|
|
3
|
+
|
|
4
|
+
// The raw LZ4 block encoder below is adapted from lz4-lite 1.1.2.
|
|
5
|
+
//
|
|
6
|
+
// MIT License
|
|
7
|
+
// Copyright (c) 2026 Alexander Vukov
|
|
8
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
// of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
// in the Software without restriction, including without limitation the rights
|
|
11
|
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
// copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
// furnished to do so, subject to the following conditions:
|
|
14
|
+
// The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
// copies or substantial portions of the Software.
|
|
16
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
// SOFTWARE.
|
|
23
|
+
|
|
24
|
+
/* eslint-disable no-bitwise */
|
|
25
|
+
|
|
26
|
+
export const PRO2_HOST_ASSET_PACKAGE_MIN_VERSION = '1.0.1';
|
|
27
|
+
|
|
28
|
+
const CONTAINER_HEADER_SIZE = 0x5f90;
|
|
29
|
+
const CONTAINER_HEADER_HASH_INPUT_LENGTH = 0x240;
|
|
30
|
+
const CONTAINER_HASH_SECTION_OFFSET = 0x200;
|
|
31
|
+
const CONTAINER_SIGNATURE_ALGORITHM_OFFSET = 0x408;
|
|
32
|
+
const CONTAINER_HEADER_MAGIC = 0x50504b4f;
|
|
33
|
+
const CONTAINER_HEADER_VERSION = 1;
|
|
34
|
+
const CONTAINER_RESOURCE_TYPE_MAGIC = 0x43534552;
|
|
35
|
+
const CONTAINER_ED25519_SIGNATURE_ALGORITHM = 0x71717171;
|
|
36
|
+
const ARCHIVE_MAGIC = 0x52414b4f;
|
|
37
|
+
const ARCHIVE_VERSION = 1;
|
|
38
|
+
const ARCHIVE_HEADER_SIZE = 42;
|
|
39
|
+
const ARCHIVE_ENTRY_SIZE = 296;
|
|
40
|
+
const ARCHIVE_ENTRY_NAME_MAX_LENGTH = 255;
|
|
41
|
+
const ARCHIVE_COMPRESS_LZ4_BLOCKED = 1;
|
|
42
|
+
const ARCHIVE_ALIGNMENT = 4;
|
|
43
|
+
const LZ4_BLOCK_SIZE_LOG2 = 12;
|
|
44
|
+
const LZ4_MIN_MATCH = 4;
|
|
45
|
+
const LZ4_LAST_LITERALS = 5;
|
|
46
|
+
const LZ4_MATCH_FIND_LIMIT = 12;
|
|
47
|
+
const LZ4_MAX_OFFSET = 0xffff;
|
|
48
|
+
const LZ4_LENGTH_MASK = 15;
|
|
49
|
+
const LZ4_HASH_LOG = 16;
|
|
50
|
+
const LZ4_HASH_MULTIPLIER = 2654435761;
|
|
51
|
+
const LZ4_SKIP_TRIGGER = 6;
|
|
52
|
+
const HOST_ASSET_PACKAGE_MAX_SIZE = 4 * 1024 * 1024;
|
|
53
|
+
|
|
54
|
+
export type Pro2HostAssetPackageEntry = {
|
|
55
|
+
name: string;
|
|
56
|
+
data: Uint8Array;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type EncodedArchiveEntry = Pro2HostAssetPackageEntry & {
|
|
60
|
+
nameBytes: Uint8Array;
|
|
61
|
+
compressed: Uint8Array;
|
|
62
|
+
offset: number;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export function supportsPro2HostAssetPackage(firmwareVersion: string | undefined): boolean {
|
|
66
|
+
return Boolean(
|
|
67
|
+
firmwareVersion &&
|
|
68
|
+
semver.valid(firmwareVersion) &&
|
|
69
|
+
semver.gte(firmwareVersion, PRO2_HOST_ASSET_PACKAGE_MIN_VERSION)
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function align(value: number): number {
|
|
74
|
+
return (value + ARCHIVE_ALIGNMENT - 1) & ~(ARCHIVE_ALIGNMENT - 1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function concatBytes(parts: Uint8Array[]): Uint8Array {
|
|
78
|
+
const output = new Uint8Array(parts.reduce((length, part) => length + part.byteLength, 0));
|
|
79
|
+
let offset = 0;
|
|
80
|
+
for (const part of parts) {
|
|
81
|
+
output.set(part, offset);
|
|
82
|
+
offset += part.byteLength;
|
|
83
|
+
}
|
|
84
|
+
return output;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function writeExtendedLength(output: Uint8Array, offset: number, length: number): number {
|
|
88
|
+
let remaining = length;
|
|
89
|
+
let nextOffset = offset;
|
|
90
|
+
while (remaining >= 255) {
|
|
91
|
+
output[nextOffset] = 255;
|
|
92
|
+
nextOffset += 1;
|
|
93
|
+
remaining -= 255;
|
|
94
|
+
}
|
|
95
|
+
output[nextOffset] = remaining;
|
|
96
|
+
return nextOffset + 1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function copyBytes(
|
|
100
|
+
output: Uint8Array,
|
|
101
|
+
outputOffset: number,
|
|
102
|
+
input: Uint8Array,
|
|
103
|
+
inputOffset: number,
|
|
104
|
+
length: number
|
|
105
|
+
): number {
|
|
106
|
+
output.set(input.subarray(inputOffset, inputOffset + length), outputOffset);
|
|
107
|
+
return outputOffset + length;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function emitSequence(
|
|
111
|
+
output: Uint8Array,
|
|
112
|
+
outputOffset: number,
|
|
113
|
+
input: Uint8Array,
|
|
114
|
+
anchor: number,
|
|
115
|
+
literalLength: number,
|
|
116
|
+
matchOffset: number,
|
|
117
|
+
matchLengthCode: number
|
|
118
|
+
): number {
|
|
119
|
+
const tokenOffset = outputOffset;
|
|
120
|
+
let nextOffset = outputOffset + 1;
|
|
121
|
+
let token = 0;
|
|
122
|
+
|
|
123
|
+
if (literalLength >= LZ4_LENGTH_MASK) {
|
|
124
|
+
token = LZ4_LENGTH_MASK << 4;
|
|
125
|
+
nextOffset = writeExtendedLength(output, nextOffset, literalLength - LZ4_LENGTH_MASK);
|
|
126
|
+
} else {
|
|
127
|
+
token = literalLength << 4;
|
|
128
|
+
}
|
|
129
|
+
nextOffset = copyBytes(output, nextOffset, input, anchor, literalLength);
|
|
130
|
+
output[nextOffset] = matchOffset & 0xff;
|
|
131
|
+
output[nextOffset + 1] = (matchOffset >>> 8) & 0xff;
|
|
132
|
+
nextOffset += 2;
|
|
133
|
+
|
|
134
|
+
if (matchLengthCode >= LZ4_LENGTH_MASK) {
|
|
135
|
+
token |= LZ4_LENGTH_MASK;
|
|
136
|
+
nextOffset = writeExtendedLength(output, nextOffset, matchLengthCode - LZ4_LENGTH_MASK);
|
|
137
|
+
} else {
|
|
138
|
+
token |= matchLengthCode;
|
|
139
|
+
}
|
|
140
|
+
output[tokenOffset] = token;
|
|
141
|
+
return nextOffset;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function emitLastLiterals(
|
|
145
|
+
output: Uint8Array,
|
|
146
|
+
outputOffset: number,
|
|
147
|
+
input: Uint8Array,
|
|
148
|
+
anchor: number,
|
|
149
|
+
literalLength: number
|
|
150
|
+
): number {
|
|
151
|
+
const tokenOffset = outputOffset;
|
|
152
|
+
let nextOffset = outputOffset + 1;
|
|
153
|
+
if (literalLength >= LZ4_LENGTH_MASK) {
|
|
154
|
+
output[tokenOffset] = LZ4_LENGTH_MASK << 4;
|
|
155
|
+
nextOffset = writeExtendedLength(output, nextOffset, literalLength - LZ4_LENGTH_MASK);
|
|
156
|
+
} else {
|
|
157
|
+
output[tokenOffset] = literalLength << 4;
|
|
158
|
+
}
|
|
159
|
+
return copyBytes(output, nextOffset, input, anchor, literalLength);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function compressRawLz4Block(input: Uint8Array, hashTable: Uint32Array): Uint8Array {
|
|
163
|
+
const output = new Uint8Array(input.byteLength + Math.floor(input.byteLength / 255) + 16);
|
|
164
|
+
const inputView = new DataView(input.buffer, input.byteOffset, input.byteLength);
|
|
165
|
+
const matchFindLimit = input.byteLength - LZ4_MATCH_FIND_LIMIT;
|
|
166
|
+
const matchExtendLimit = input.byteLength - LZ4_LAST_LITERALS;
|
|
167
|
+
let anchor = 0;
|
|
168
|
+
let inputOffset = 0;
|
|
169
|
+
let outputOffset = 0;
|
|
170
|
+
let searchMatchCount = 1 << LZ4_SKIP_TRIGGER;
|
|
171
|
+
|
|
172
|
+
hashTable.fill(0);
|
|
173
|
+
while (inputOffset < matchFindLimit) {
|
|
174
|
+
const sequence = inputView.getUint32(inputOffset, true);
|
|
175
|
+
const hash = Math.imul(sequence, LZ4_HASH_MULTIPLIER) >>> (32 - LZ4_HASH_LOG);
|
|
176
|
+
const candidate = hashTable[hash] - 1;
|
|
177
|
+
hashTable[hash] = inputOffset + 1;
|
|
178
|
+
|
|
179
|
+
const hasMatch = !(
|
|
180
|
+
candidate < 0 ||
|
|
181
|
+
inputOffset - candidate > LZ4_MAX_OFFSET ||
|
|
182
|
+
inputView.getUint32(candidate, true) !== sequence
|
|
183
|
+
);
|
|
184
|
+
if (!hasMatch) {
|
|
185
|
+
inputOffset += searchMatchCount >> LZ4_SKIP_TRIGGER;
|
|
186
|
+
searchMatchCount += 1;
|
|
187
|
+
} else {
|
|
188
|
+
searchMatchCount = 1 << LZ4_SKIP_TRIGGER;
|
|
189
|
+
let matchEnd = inputOffset + LZ4_MIN_MATCH;
|
|
190
|
+
let reference = candidate + LZ4_MIN_MATCH;
|
|
191
|
+
while (matchEnd < matchExtendLimit && input[matchEnd] === input[reference]) {
|
|
192
|
+
matchEnd += 1;
|
|
193
|
+
reference += 1;
|
|
194
|
+
}
|
|
195
|
+
outputOffset = emitSequence(
|
|
196
|
+
output,
|
|
197
|
+
outputOffset,
|
|
198
|
+
input,
|
|
199
|
+
anchor,
|
|
200
|
+
inputOffset - anchor,
|
|
201
|
+
inputOffset - candidate,
|
|
202
|
+
matchEnd - inputOffset - LZ4_MIN_MATCH
|
|
203
|
+
);
|
|
204
|
+
inputOffset = matchEnd;
|
|
205
|
+
anchor = inputOffset;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
outputOffset = emitLastLiterals(output, outputOffset, input, anchor, input.byteLength - anchor);
|
|
210
|
+
return output.slice(0, outputOffset);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function encodeLz4Blocked(data: Uint8Array): Uint8Array {
|
|
214
|
+
const blockSize = 1 << LZ4_BLOCK_SIZE_LOG2;
|
|
215
|
+
const blockCount = Math.ceil(data.byteLength / blockSize);
|
|
216
|
+
const hashTable = new Uint32Array(1 << LZ4_HASH_LOG);
|
|
217
|
+
const blocks: Uint8Array[] = [];
|
|
218
|
+
const header = new Uint8Array(8 + blockCount * 4);
|
|
219
|
+
const headerView = new DataView(header.buffer);
|
|
220
|
+
headerView.setUint16(0, blockCount, true);
|
|
221
|
+
headerView.setUint16(2, LZ4_BLOCK_SIZE_LOG2, true);
|
|
222
|
+
|
|
223
|
+
for (let index = 0; index < blockCount; index += 1) {
|
|
224
|
+
const block = compressRawLz4Block(
|
|
225
|
+
data.subarray(index * blockSize, Math.min((index + 1) * blockSize, data.byteLength)),
|
|
226
|
+
hashTable
|
|
227
|
+
);
|
|
228
|
+
headerView.setUint32(8 + index * 4, block.byteLength, true);
|
|
229
|
+
blocks.push(block);
|
|
230
|
+
}
|
|
231
|
+
return concatBytes([header, ...blocks]);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const CRC32_TABLE = (() => {
|
|
235
|
+
const table = new Uint32Array(256);
|
|
236
|
+
for (let index = 0; index < table.length; index += 1) {
|
|
237
|
+
let value = index;
|
|
238
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
239
|
+
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
|
|
240
|
+
}
|
|
241
|
+
table[index] = value >>> 0;
|
|
242
|
+
}
|
|
243
|
+
return table;
|
|
244
|
+
})();
|
|
245
|
+
|
|
246
|
+
function crc32(data: Uint8Array): number {
|
|
247
|
+
let value = 0xffffffff;
|
|
248
|
+
for (const byte of data) {
|
|
249
|
+
value = CRC32_TABLE[(value ^ byte) & 0xff] ^ (value >>> 8);
|
|
250
|
+
}
|
|
251
|
+
return (value ^ 0xffffffff) >>> 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function buildArchive(entries: Pro2HostAssetPackageEntry[]): Uint8Array {
|
|
255
|
+
const textEncoder = new TextEncoder();
|
|
256
|
+
let dataOffset = ARCHIVE_HEADER_SIZE + entries.length * ARCHIVE_ENTRY_SIZE;
|
|
257
|
+
const encodedEntries: EncodedArchiveEntry[] = entries.map(entry => {
|
|
258
|
+
const nameBytes = textEncoder.encode(entry.name);
|
|
259
|
+
if (!entry.name || nameBytes.byteLength > ARCHIVE_ENTRY_NAME_MAX_LENGTH) {
|
|
260
|
+
throw new Error('Pro2 host asset package entry names must contain 1 to 255 UTF-8 bytes.');
|
|
261
|
+
}
|
|
262
|
+
if (!(entry.data instanceof Uint8Array) || entry.data.byteLength === 0) {
|
|
263
|
+
throw new Error(`Pro2 host asset package entry [${entry.name}] must not be empty.`);
|
|
264
|
+
}
|
|
265
|
+
const compressed = encodeLz4Blocked(entry.data);
|
|
266
|
+
const offset = align(dataOffset);
|
|
267
|
+
dataOffset = offset + compressed.byteLength;
|
|
268
|
+
return { ...entry, nameBytes, compressed, offset };
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const archive = new Uint8Array(dataOffset);
|
|
272
|
+
const view = new DataView(archive.buffer);
|
|
273
|
+
view.setUint32(0, ARCHIVE_MAGIC, true);
|
|
274
|
+
view.setUint32(4, ARCHIVE_VERSION, true);
|
|
275
|
+
view.setUint16(8, encodedEntries.length, true);
|
|
276
|
+
|
|
277
|
+
encodedEntries.forEach((entry, index) => {
|
|
278
|
+
const recordOffset = ARCHIVE_HEADER_SIZE + index * ARCHIVE_ENTRY_SIZE;
|
|
279
|
+
archive[recordOffset] = entry.nameBytes.byteLength;
|
|
280
|
+
archive.set(entry.nameBytes, recordOffset + 1);
|
|
281
|
+
view.setUint32(recordOffset + 0x100, entry.offset, true);
|
|
282
|
+
view.setUint32(recordOffset + 0x104, entry.data.byteLength, true);
|
|
283
|
+
view.setUint32(recordOffset + 0x108, entry.compressed.byteLength, true);
|
|
284
|
+
view.setUint32(recordOffset + 0x10c, crc32(entry.data), true);
|
|
285
|
+
view.setUint32(recordOffset + 0x110, crc32(entry.compressed), true);
|
|
286
|
+
archive[recordOffset + 0x114] = ARCHIVE_COMPRESS_LZ4_BLOCKED;
|
|
287
|
+
archive.set(entry.compressed, entry.offset);
|
|
288
|
+
});
|
|
289
|
+
return archive;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function buildPro2HostAssetPackage(entries: Pro2HostAssetPackageEntry[]): Uint8Array {
|
|
293
|
+
if (entries.length === 0 || new Set(entries.map(entry => entry.name)).size !== entries.length) {
|
|
294
|
+
throw new Error('Pro2 host asset package entries must have unique, non-empty names.');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const payload = buildArchive(entries);
|
|
298
|
+
const header = new Uint8Array(CONTAINER_HEADER_SIZE);
|
|
299
|
+
const view = new DataView(header.buffer);
|
|
300
|
+
view.setUint32(0, CONTAINER_HEADER_MAGIC, true);
|
|
301
|
+
view.setUint32(4, CONTAINER_HEADER_VERSION, true);
|
|
302
|
+
view.setUint32(8, CONTAINER_RESOURCE_TYPE_MAGIC, true);
|
|
303
|
+
view.setUint32(0x0c, CONTAINER_HEADER_SIZE, true);
|
|
304
|
+
view.setUint32(0x10, 1, true);
|
|
305
|
+
view.setUint32(0x14, payload.byteLength, true);
|
|
306
|
+
header.set(sha3_512(payload), CONTAINER_HASH_SECTION_OFFSET);
|
|
307
|
+
header.set(sha3_512(header.subarray(0, CONTAINER_HEADER_HASH_INPUT_LENGTH)), 0x240);
|
|
308
|
+
view.setUint32(CONTAINER_SIGNATURE_ALGORITHM_OFFSET, CONTAINER_ED25519_SIGNATURE_ALGORITHM, true);
|
|
309
|
+
|
|
310
|
+
const packageData = concatBytes([header, payload]);
|
|
311
|
+
if (packageData.byteLength > HOST_ASSET_PACKAGE_MAX_SIZE) {
|
|
312
|
+
throw new Error('Pro2 host asset package exceeds the firmware 4 MiB limit.');
|
|
313
|
+
}
|
|
314
|
+
return packageData;
|
|
315
|
+
}
|