@altronix/webtobin 0.4.0 → 0.6.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/package.json +6 -5
- package/webtobin.mjs +214 -9
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@altronix/webtobin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "",
|
|
6
6
|
"main": "./webtobin.mjs",
|
|
7
7
|
"types": "./webtobin.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules npx jest"
|
|
10
|
+
},
|
|
8
11
|
"files": [
|
|
9
12
|
"webtobin.d.ts",
|
|
10
13
|
"webtobin.mjs",
|
|
@@ -13,11 +16,9 @@
|
|
|
13
16
|
"keywords": [],
|
|
14
17
|
"author": "",
|
|
15
18
|
"license": "ISC",
|
|
19
|
+
"packageManager": "pnpm@10.5.2",
|
|
16
20
|
"devDependencies": {
|
|
17
21
|
"cross-env": "^10.0.0",
|
|
18
22
|
"jest": "^30.1.3"
|
|
19
|
-
},
|
|
20
|
-
"scripts": {
|
|
21
|
-
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules npx jest"
|
|
22
23
|
}
|
|
23
|
-
}
|
|
24
|
+
}
|
package/webtobin.mjs
CHANGED
|
@@ -1,5 +1,214 @@
|
|
|
1
1
|
//! webtobin.mjs
|
|
2
2
|
|
|
3
|
+
const IMAGE_MAGIC = 0x96f3b83d;
|
|
4
|
+
const TLV_INFO_MAGIC = 0x6907;
|
|
5
|
+
const TLV_PROT_INFO_MAGIC = 0x6908;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} McubootVersion
|
|
9
|
+
* @property {number} major
|
|
10
|
+
* @property {number} minor
|
|
11
|
+
* @property {number} revision
|
|
12
|
+
* @property {number} build
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} McubootHeader
|
|
17
|
+
* @property {number} magic
|
|
18
|
+
* @property {number} loadAddr
|
|
19
|
+
* @property {number} hdrSize
|
|
20
|
+
* @property {number} protectTlvSize
|
|
21
|
+
* @property {number} imgSize
|
|
22
|
+
* @property {number} flags
|
|
23
|
+
* @property {McubootVersion} version
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {Object} McubootTlv
|
|
28
|
+
* @property {number} type
|
|
29
|
+
* @property {Uint8Array} value
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {Object} McubootImage
|
|
34
|
+
* @property {McubootHeader} header
|
|
35
|
+
* @property {Uint8Array} image
|
|
36
|
+
* @property {McubootTlv[]} protectedTlvs
|
|
37
|
+
* @property {McubootTlv[]} tlvs
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse MCUboot signed binary into array of images
|
|
42
|
+
* @param {Buffer|Uint8Array} bin
|
|
43
|
+
* @returns {McubootImage[]}
|
|
44
|
+
*/
|
|
45
|
+
export function parseMcubootImages(bin) {
|
|
46
|
+
const images = [];
|
|
47
|
+
const buf = new Uint8Array(bin).buffer;
|
|
48
|
+
const view = new DataView(buf);
|
|
49
|
+
const le = true;
|
|
50
|
+
|
|
51
|
+
let offset = 0;
|
|
52
|
+
while (offset < buf.byteLength) {
|
|
53
|
+
const magic = view.getUint32(offset, le);
|
|
54
|
+
if (magic !== IMAGE_MAGIC) break;
|
|
55
|
+
|
|
56
|
+
const header = parseHeader(view, offset);
|
|
57
|
+
const imgStart = offset + header.hdrSize;
|
|
58
|
+
const imgEnd = imgStart + header.imgSize;
|
|
59
|
+
const image = new Uint8Array(buf.slice(imgStart, imgEnd));
|
|
60
|
+
|
|
61
|
+
const protectedTlvs = [];
|
|
62
|
+
const tlvs = [];
|
|
63
|
+
let tlvOffset = imgEnd;
|
|
64
|
+
|
|
65
|
+
// Parse protected TLVs (magic 0x6908)
|
|
66
|
+
if (header.protectTlvSize > 0) {
|
|
67
|
+
const protMagic = view.getUint16(tlvOffset, le);
|
|
68
|
+
if (protMagic === TLV_PROT_INFO_MAGIC) {
|
|
69
|
+
const protSize = view.getUint16(tlvOffset + 2, le);
|
|
70
|
+
parseTlvs(view, tlvOffset + 4, protSize - 4, protectedTlvs);
|
|
71
|
+
tlvOffset += protSize;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Parse standard TLVs (magic 0x6907)
|
|
76
|
+
const tlvMagic = view.getUint16(tlvOffset, le);
|
|
77
|
+
if (tlvMagic === TLV_INFO_MAGIC) {
|
|
78
|
+
const tlvSize = view.getUint16(tlvOffset + 2, le);
|
|
79
|
+
parseTlvs(view, tlvOffset + 4, tlvSize - 4, tlvs);
|
|
80
|
+
tlvOffset += tlvSize;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
images.push({ header, image, protectedTlvs, tlvs });
|
|
84
|
+
offset = tlvOffset;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return images;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {DataView} view
|
|
92
|
+
* @param {number} offset
|
|
93
|
+
* @returns {McubootHeader}
|
|
94
|
+
*/
|
|
95
|
+
function parseHeader(view, offset) {
|
|
96
|
+
const le = true;
|
|
97
|
+
return {
|
|
98
|
+
magic: view.getUint32(offset + 0x00, le),
|
|
99
|
+
loadAddr: view.getUint32(offset + 0x04, le),
|
|
100
|
+
hdrSize: view.getUint16(offset + 0x08, le),
|
|
101
|
+
protectTlvSize: view.getUint16(offset + 0x0a, le),
|
|
102
|
+
imgSize: view.getUint32(offset + 0x0c, le),
|
|
103
|
+
flags: view.getUint32(offset + 0x10, le),
|
|
104
|
+
version: {
|
|
105
|
+
major: view.getUint8(offset + 0x14),
|
|
106
|
+
minor: view.getUint8(offset + 0x15),
|
|
107
|
+
revision: view.getUint16(offset + 0x16, le),
|
|
108
|
+
build: view.getUint32(offset + 0x18, le),
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {DataView} view
|
|
115
|
+
* @param {number} offset
|
|
116
|
+
* @param {number} length
|
|
117
|
+
* @param {McubootTlv[]} out
|
|
118
|
+
*/
|
|
119
|
+
function parseTlvs(view, offset, length, out) {
|
|
120
|
+
const le = true;
|
|
121
|
+
const end = offset + length;
|
|
122
|
+
while (offset < end) {
|
|
123
|
+
const type = view.getUint16(offset, le);
|
|
124
|
+
const len = view.getUint16(offset + 2, le);
|
|
125
|
+
const data = new Uint8Array(view.buffer.slice(offset + 4, offset + 4 + len));
|
|
126
|
+
out.push({ type, value: data });
|
|
127
|
+
offset += 4 + len;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Application-specific TLV types
|
|
132
|
+
const TLV_PARTITION_INDEX = 0x8001;
|
|
133
|
+
const TLV_PARTITION_NAME = 0x8002;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @typedef {Object} AltronixImageInfo
|
|
137
|
+
* @property {number} partitionIndex
|
|
138
|
+
* @property {string} partitionName
|
|
139
|
+
* @property {McubootHeader} header
|
|
140
|
+
* @property {Uint8Array} image
|
|
141
|
+
* @property {McubootVersion} version
|
|
142
|
+
*/
|
|
143
|
+
|
|
144
|
+
/** Application-specific wrapper for MCUboot images */
|
|
145
|
+
export class AltronixImage {
|
|
146
|
+
#mcuboot;
|
|
147
|
+
|
|
148
|
+
/** @type {number} */
|
|
149
|
+
partitionIndex;
|
|
150
|
+
|
|
151
|
+
/** @type {string} */
|
|
152
|
+
partitionName;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {McubootImage} mcubootImage
|
|
156
|
+
*/
|
|
157
|
+
constructor(mcubootImage) {
|
|
158
|
+
this.#mcuboot = mcubootImage;
|
|
159
|
+
|
|
160
|
+
const indexTlv = this.#mcuboot.protectedTlvs.find((t) => t.type === TLV_PARTITION_INDEX);
|
|
161
|
+
if (!indexTlv) {
|
|
162
|
+
throw new Error("AltronixImage: missing required TLV_PARTITION_INDEX (0x8001)");
|
|
163
|
+
}
|
|
164
|
+
this.partitionIndex = new DataView(
|
|
165
|
+
indexTlv.value.buffer,
|
|
166
|
+
indexTlv.value.byteOffset,
|
|
167
|
+
indexTlv.value.byteLength
|
|
168
|
+
).getUint32(0, true);
|
|
169
|
+
|
|
170
|
+
const nameTlv = this.#mcuboot.protectedTlvs.find((t) => t.type === TLV_PARTITION_NAME);
|
|
171
|
+
if (!nameTlv) {
|
|
172
|
+
throw new Error("AltronixImage: missing required TLV_PARTITION_NAME (0x8002)");
|
|
173
|
+
}
|
|
174
|
+
this.partitionName = new TextDecoder().decode(nameTlv.value);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Create AltronixImage from McubootImage
|
|
179
|
+
* @param {McubootImage} img
|
|
180
|
+
* @returns {AltronixImage}
|
|
181
|
+
*/
|
|
182
|
+
static fromMcubootImage(img) {
|
|
183
|
+
return new AltronixImage(img);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Read a TLV value by type from protected TLVs
|
|
188
|
+
* @param {number} type
|
|
189
|
+
* @returns {Uint8Array|null}
|
|
190
|
+
*/
|
|
191
|
+
readTlv(type) {
|
|
192
|
+
const tlv = this.#mcuboot.protectedTlvs.find((t) => t.type === type);
|
|
193
|
+
return tlv ? tlv.value : null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** @returns {McubootHeader} */
|
|
197
|
+
get header() {
|
|
198
|
+
return this.#mcuboot.header;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** @returns {Uint8Array} */
|
|
202
|
+
get image() {
|
|
203
|
+
return this.#mcuboot.image;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** @returns {McubootVersion} */
|
|
207
|
+
get version() {
|
|
208
|
+
return this.#mcuboot.header.version;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
3
212
|
/** Class holding Webtobin state */
|
|
4
213
|
export default class Webtobin {
|
|
5
214
|
constructor() {
|
|
@@ -62,7 +271,7 @@ export default class Webtobin {
|
|
|
62
271
|
*/
|
|
63
272
|
print() {
|
|
64
273
|
this.files.sort((a, b) => a.path.localeCompare(b.path));
|
|
65
|
-
let arr;
|
|
274
|
+
let arr = new Uint8Array([]);
|
|
66
275
|
for (const item of this.files) {
|
|
67
276
|
const encoder = new TextEncoder();
|
|
68
277
|
const path = encoder.encode(item.path);
|
|
@@ -74,14 +283,10 @@ export default class Webtobin {
|
|
|
74
283
|
path.forEach((v, idx) => view.setUint8(idx + 4, v));
|
|
75
284
|
view.setUint8(path.length + 4, 0);
|
|
76
285
|
item.data.forEach((v, idx) => view.setUint8(idx + path.length + 5, v));
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
next.set(arr, 0);
|
|
82
|
-
next.set(new Uint8Array(view.buffer), arr.length);
|
|
83
|
-
arr = next;
|
|
84
|
-
}
|
|
286
|
+
const next = new Uint8Array(arr.length + view.byteLength);
|
|
287
|
+
next.set(arr, 0);
|
|
288
|
+
next.set(new Uint8Array(view.buffer), arr.length);
|
|
289
|
+
arr = next;
|
|
85
290
|
}
|
|
86
291
|
let ret = new Uint8Array(arr.length + 4);
|
|
87
292
|
ret.set(arr, 0);
|