@aepyornis/fastboot.ts 0.0.1 → 0.0.2
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 +1 -1
- package/src/client.ts +4 -35
- package/src/device.ts +4 -1
- package/src/flasher.ts +2 -2
- package/src/sparse.ts +66 -66
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BlobWriter, Entry } from "@zip.js/zip.js"
|
|
2
2
|
import { IMAGES } from "./images"
|
|
3
|
-
import {
|
|
3
|
+
import { parseBlobHeader, splitBlob, fromRaw } from "./sparse"
|
|
4
4
|
import { FastbootDevice } from "./device"
|
|
5
5
|
|
|
6
6
|
export class FastbootError extends Error {}
|
|
@@ -32,7 +32,6 @@ export class FastbootClient {
|
|
|
32
32
|
|
|
33
33
|
async lock() {
|
|
34
34
|
await this.flashing("lock")
|
|
35
|
-
await this.fd.waitForReconnect()
|
|
36
35
|
if (await this.unlocked()) {
|
|
37
36
|
throw new FastbootError("failed to lock device")
|
|
38
37
|
}
|
|
@@ -40,7 +39,6 @@ export class FastbootClient {
|
|
|
40
39
|
|
|
41
40
|
async unlock() {
|
|
42
41
|
await this.flashing("unlock")
|
|
43
|
-
await this.fd.waitForReconnect()
|
|
44
42
|
if (await this.locked()) {
|
|
45
43
|
throw new FastbootError("failed to unlock device")
|
|
46
44
|
}
|
|
@@ -147,14 +145,9 @@ export class FastbootClient {
|
|
|
147
145
|
await this.fd.waitForReconnect()
|
|
148
146
|
}
|
|
149
147
|
|
|
150
|
-
// run
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
entries: Entry[],
|
|
154
|
-
directions: string,
|
|
155
|
-
wipe: boolean = false,
|
|
156
|
-
) {
|
|
157
|
-
const lines = directions
|
|
148
|
+
// run text, typically the contents of fastboot-info.txt
|
|
149
|
+
async fastbootInfo(entries: Entry[], text: string, wipe: boolean = false) {
|
|
150
|
+
const lines = text
|
|
158
151
|
.split("\n")
|
|
159
152
|
.map((x) => x.trim())
|
|
160
153
|
.filter((l) => !(l == "" || l[0] == "#" || l.slice(0, 7) == "version"))
|
|
@@ -318,27 +311,3 @@ export class FastbootClient {
|
|
|
318
311
|
})
|
|
319
312
|
}
|
|
320
313
|
}
|
|
321
|
-
|
|
322
|
-
async function parseBlobHeader(blob: Blob): {
|
|
323
|
-
blobSize: number
|
|
324
|
-
totalBytes: number
|
|
325
|
-
isSparse: boolean
|
|
326
|
-
} {
|
|
327
|
-
const FILE_HEADER_SIZE = 28
|
|
328
|
-
const blobSize = blob.size
|
|
329
|
-
let totalBytes = blobSize
|
|
330
|
-
let isSparse = false
|
|
331
|
-
|
|
332
|
-
try {
|
|
333
|
-
const fileHeader = await blob.slice(0, FILE_HEADER_SIZE).arrayBuffer()
|
|
334
|
-
const sparseHeader = parseFileHeader(fileHeader)
|
|
335
|
-
if (sparseHeader !== null) {
|
|
336
|
-
totalBytes = sparseHeader.blocks * sparseHeader.blockSize
|
|
337
|
-
isSparse = true
|
|
338
|
-
}
|
|
339
|
-
} catch (error) {
|
|
340
|
-
console.debug(error)
|
|
341
|
-
// ImageError = invalid, so keep blob.size
|
|
342
|
-
}
|
|
343
|
-
return { blobSize, totalBytes, isSparse }
|
|
344
|
-
}
|
package/src/device.ts
CHANGED
|
@@ -101,7 +101,10 @@ export class FastbootDevice {
|
|
|
101
101
|
return new Promise((resolve, reject) => {
|
|
102
102
|
navigator.usb.addEventListener(
|
|
103
103
|
"connect",
|
|
104
|
-
async () => {
|
|
104
|
+
async (event) => {
|
|
105
|
+
this.logger.log(
|
|
106
|
+
`waitForReconnect: device connected ${event.device.productName}`,
|
|
107
|
+
)
|
|
105
108
|
try {
|
|
106
109
|
await this.reconnect()
|
|
107
110
|
resolve(true)
|
package/src/flasher.ts
CHANGED
|
@@ -57,8 +57,8 @@ function parseInstruction(text: string): Instruction {
|
|
|
57
57
|
options.wipe = true
|
|
58
58
|
} else if (word === "--set-active=other") {
|
|
59
59
|
options.setActive = "other"
|
|
60
|
-
} else if (word ===
|
|
61
|
-
|
|
60
|
+
} else if (word === "--slot-other") {
|
|
61
|
+
options.slot = "other"
|
|
62
62
|
} else if (word.slice(0, 6) === "--slot") {
|
|
63
63
|
const slot = word.split("=")[1]
|
|
64
64
|
if (!["current", "other", "a", "b"].includes(slot)) {
|
package/src/sparse.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The MIT License (MIT)
|
|
2
2
|
|
|
3
3
|
// Copyright (c) 2021 Danny Lin <danny@kdrag0n.dev>
|
|
4
|
+
// Copyright (c) 2025 ziggy <ziggy@calyxinstitute.org>
|
|
4
5
|
|
|
5
6
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
7
|
// of this software and associated documentation files (the "Software"), to deal
|
|
@@ -20,26 +21,6 @@
|
|
|
20
21
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
22
|
// SOFTWARE.
|
|
22
23
|
|
|
23
|
-
function readBlobAsBuffer(blob: Blob): Promise<ArrayBuffer> {
|
|
24
|
-
return new Promise((resolve, reject) => {
|
|
25
|
-
let reader = new FileReader()
|
|
26
|
-
reader.onload = () => {
|
|
27
|
-
resolve(reader.result! as ArrayBuffer)
|
|
28
|
-
}
|
|
29
|
-
reader.onerror = () => {
|
|
30
|
-
reject(reader.error)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
reader.readAsArrayBuffer(blob)
|
|
34
|
-
})
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const common = {
|
|
38
|
-
readBlobAsBuffer: readBlobAsBuffer,
|
|
39
|
-
logVerbose: (...data) => console.log(...data),
|
|
40
|
-
logDebug: (...data) => console.log(...data),
|
|
41
|
-
}
|
|
42
|
-
|
|
43
24
|
const FILE_MAGIC = 0xed26ff3a
|
|
44
25
|
|
|
45
26
|
const MAJOR_VERSION = 1
|
|
@@ -109,29 +90,29 @@ class BlobBuilder {
|
|
|
109
90
|
* @returns {SparseHeader} Object containing the header information.
|
|
110
91
|
*/
|
|
111
92
|
export function parseFileHeader(buffer: ArrayBuffer): SparseHeader | null {
|
|
112
|
-
|
|
93
|
+
const view = new DataView(buffer)
|
|
113
94
|
|
|
114
|
-
|
|
95
|
+
const magic = view.getUint32(0, true)
|
|
115
96
|
if (magic !== FILE_MAGIC) {
|
|
116
97
|
return null
|
|
117
98
|
}
|
|
118
99
|
|
|
119
100
|
// v1.0+
|
|
120
|
-
|
|
121
|
-
|
|
101
|
+
const major = view.getUint16(4, true)
|
|
102
|
+
const minor = view.getUint16(6, true)
|
|
122
103
|
if (major !== MAJOR_VERSION || minor < MINOR_VERSION) {
|
|
123
104
|
throw new ImageError(`Unsupported sparse image version ${major}.${minor}`)
|
|
124
105
|
}
|
|
125
106
|
|
|
126
|
-
|
|
127
|
-
|
|
107
|
+
const fileHdrSize = view.getUint16(8, true)
|
|
108
|
+
const chunkHdrSize = view.getUint16(10, true)
|
|
128
109
|
if (fileHdrSize !== FILE_HEADER_SIZE || chunkHdrSize !== CHUNK_HEADER_SIZE) {
|
|
129
110
|
throw new ImageError(
|
|
130
111
|
`Invalid file header size ${fileHdrSize}, chunk header size ${chunkHdrSize}`,
|
|
131
112
|
)
|
|
132
113
|
}
|
|
133
114
|
|
|
134
|
-
|
|
115
|
+
const blockSize = view.getUint32(12, true)
|
|
135
116
|
if (blockSize % 4 !== 0) {
|
|
136
117
|
throw new ImageError(`Block size ${blockSize} is not a multiple of 4`)
|
|
137
118
|
}
|
|
@@ -144,8 +125,8 @@ export function parseFileHeader(buffer: ArrayBuffer): SparseHeader | null {
|
|
|
144
125
|
}
|
|
145
126
|
}
|
|
146
127
|
|
|
147
|
-
function parseChunkHeader(buffer: ArrayBuffer) {
|
|
148
|
-
|
|
128
|
+
function parseChunkHeader(buffer: ArrayBuffer): SparseChunk {
|
|
129
|
+
const view = new DataView(buffer)
|
|
149
130
|
|
|
150
131
|
// This isn't the same as what createImage takes.
|
|
151
132
|
// Further processing needs to be done on the chunks.
|
|
@@ -155,7 +136,7 @@ function parseChunkHeader(buffer: ArrayBuffer) {
|
|
|
155
136
|
blocks: view.getUint32(4, true),
|
|
156
137
|
dataBytes: view.getUint32(8, true) - CHUNK_HEADER_SIZE,
|
|
157
138
|
data: null, // to be populated by consumer
|
|
158
|
-
}
|
|
139
|
+
}
|
|
159
140
|
}
|
|
160
141
|
|
|
161
142
|
function calcChunksBlockSize(chunks: Array<SparseChunk>) {
|
|
@@ -170,7 +151,7 @@ function calcChunksDataSize(chunks: Array<SparseChunk>) {
|
|
|
170
151
|
|
|
171
152
|
function calcChunksSize(chunks: Array<SparseChunk>) {
|
|
172
153
|
// 28-byte file header, 12-byte chunk headers
|
|
173
|
-
|
|
154
|
+
const overhead = FILE_HEADER_SIZE + CHUNK_HEADER_SIZE * chunks.length
|
|
174
155
|
return overhead + calcChunksDataSize(chunks)
|
|
175
156
|
}
|
|
176
157
|
|
|
@@ -178,7 +159,7 @@ async function createImage(
|
|
|
178
159
|
header: SparseHeader,
|
|
179
160
|
chunks: Array<SparseChunk>,
|
|
180
161
|
): Promise<Blob> {
|
|
181
|
-
|
|
162
|
+
const blobBuilder = new BlobBuilder()
|
|
182
163
|
|
|
183
164
|
let buffer = new ArrayBuffer(FILE_HEADER_SIZE)
|
|
184
165
|
let dataView = new DataView(buffer)
|
|
@@ -202,7 +183,7 @@ async function createImage(
|
|
|
202
183
|
dataView.setUint32(24, 0, true)
|
|
203
184
|
|
|
204
185
|
blobBuilder.append(new Blob([buffer]))
|
|
205
|
-
for (
|
|
186
|
+
for (const chunk of chunks) {
|
|
206
187
|
buffer = new ArrayBuffer(CHUNK_HEADER_SIZE + chunk.data!.size)
|
|
207
188
|
dataView = new DataView(buffer)
|
|
208
189
|
arrayView = new Uint8Array(buffer)
|
|
@@ -212,9 +193,7 @@ async function createImage(
|
|
|
212
193
|
dataView.setUint32(4, chunk.blocks, true)
|
|
213
194
|
dataView.setUint32(8, CHUNK_HEADER_SIZE + chunk.data!.size, true)
|
|
214
195
|
|
|
215
|
-
|
|
216
|
-
await common.readBlobAsBuffer(chunk.data!),
|
|
217
|
-
)
|
|
196
|
+
const chunkArrayView = new Uint8Array(await chunk.data!.arrayBuffer())
|
|
218
197
|
arrayView.set(chunkArrayView, CHUNK_HEADER_SIZE)
|
|
219
198
|
blobBuilder.append(new Blob([buffer]))
|
|
220
199
|
}
|
|
@@ -229,21 +208,21 @@ async function createImage(
|
|
|
229
208
|
* @returns {Promise<Blob>} Promise that resolves the blob containing the new sparse image.
|
|
230
209
|
*/
|
|
231
210
|
export async function fromRaw(blob: Blob): Promise<Blob> {
|
|
232
|
-
|
|
211
|
+
const header = {
|
|
233
212
|
blockSize: 4096,
|
|
234
213
|
blocks: blob.size / 4096,
|
|
235
214
|
chunks: 1,
|
|
236
215
|
crc32: 0,
|
|
237
216
|
}
|
|
238
217
|
|
|
239
|
-
|
|
218
|
+
const chunks: SparseChunk[] = []
|
|
240
219
|
while (blob.size > 0) {
|
|
241
|
-
|
|
220
|
+
const chunkSize = Math.min(blob.size, RAW_CHUNK_SIZE)
|
|
242
221
|
chunks.push({
|
|
243
222
|
type: ChunkType.Raw,
|
|
244
223
|
blocks: chunkSize / header.blockSize,
|
|
245
224
|
data: blob.slice(0, chunkSize),
|
|
246
|
-
}
|
|
225
|
+
})
|
|
247
226
|
blob = blob.slice(chunkSize)
|
|
248
227
|
}
|
|
249
228
|
|
|
@@ -260,7 +239,7 @@ export async function fromRaw(blob: Blob): Promise<Blob> {
|
|
|
260
239
|
* @yields {Object} Data of the next split image and its output size in bytes.
|
|
261
240
|
*/
|
|
262
241
|
export async function* splitBlob(blob: Blob, splitSize: number) {
|
|
263
|
-
|
|
242
|
+
console.debug(
|
|
264
243
|
`Splitting ${blob.size}-byte sparse image into ${splitSize}-byte chunks`,
|
|
265
244
|
)
|
|
266
245
|
|
|
@@ -270,18 +249,17 @@ export async function* splitBlob(blob: Blob, splitSize: number) {
|
|
|
270
249
|
|
|
271
250
|
// Short-circuit if splitting isn't required
|
|
272
251
|
if (blob.size <= splitSize) {
|
|
273
|
-
|
|
252
|
+
console.debug("Blob fits in 1 payload, not splitting")
|
|
274
253
|
yield {
|
|
275
|
-
data: await
|
|
254
|
+
data: await blob.arrayBuffer(),
|
|
276
255
|
bytes: blob.size,
|
|
277
256
|
} as SparseSplit
|
|
278
257
|
return
|
|
279
258
|
}
|
|
280
259
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
)
|
|
284
|
-
let header = parseFileHeader(headerData)
|
|
260
|
+
const headerData = await blob.slice(0, FILE_HEADER_SIZE).arrayBuffer()
|
|
261
|
+
|
|
262
|
+
const header = parseFileHeader(headerData)
|
|
285
263
|
if (header === null) {
|
|
286
264
|
throw new ImageError("Blob is not a sparse image")
|
|
287
265
|
}
|
|
@@ -293,21 +271,19 @@ export async function* splitBlob(blob: Blob, splitSize: number) {
|
|
|
293
271
|
let splitChunks: Array<SparseChunk> = []
|
|
294
272
|
let splitDataBytes = 0
|
|
295
273
|
for (let i = 0; i < header.chunks; i++) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
)
|
|
299
|
-
let originalChunk = parseChunkHeader(chunkHeaderData)
|
|
274
|
+
const chunkHeaderData = await blob.slice(0, CHUNK_HEADER_SIZE).arrayBuffer()
|
|
275
|
+
const originalChunk = parseChunkHeader(chunkHeaderData)
|
|
300
276
|
originalChunk.data = blob.slice(
|
|
301
277
|
CHUNK_HEADER_SIZE,
|
|
302
278
|
CHUNK_HEADER_SIZE + originalChunk.dataBytes,
|
|
303
279
|
)
|
|
304
280
|
blob = blob.slice(CHUNK_HEADER_SIZE + originalChunk.dataBytes)
|
|
305
281
|
|
|
306
|
-
|
|
282
|
+
const chunksToProcess: SparseChunk[] = []
|
|
307
283
|
|
|
308
284
|
// take into account cases where the chunk data is bigger than the maximum allowed download size
|
|
309
285
|
if (originalChunk.dataBytes > safeSendValue) {
|
|
310
|
-
|
|
286
|
+
console.debug(
|
|
311
287
|
`Data of chunk ${i} is bigger than the maximum allowed download size: ${originalChunk.dataBytes} > ${safeSendValue}`,
|
|
312
288
|
)
|
|
313
289
|
|
|
@@ -329,20 +305,20 @@ export async function* splitBlob(blob: Blob, splitSize: number) {
|
|
|
329
305
|
originalDataBytes -= toSend
|
|
330
306
|
}
|
|
331
307
|
|
|
332
|
-
|
|
308
|
+
console.debug("chunksToProcess", chunksToProcess)
|
|
333
309
|
} else {
|
|
334
310
|
chunksToProcess.push(originalChunk)
|
|
335
311
|
}
|
|
336
312
|
|
|
337
313
|
for (const chunk of chunksToProcess) {
|
|
338
|
-
|
|
339
|
-
|
|
314
|
+
const bytesRemaining = splitSize - calcChunksSize(splitChunks)
|
|
315
|
+
console.debug(
|
|
340
316
|
` Chunk ${i}: type ${chunk.type}, ${chunk.dataBytes} bytes / ${chunk.blocks} blocks, ${bytesRemaining} bytes remaining`,
|
|
341
317
|
)
|
|
342
318
|
|
|
343
319
|
if (bytesRemaining >= chunk.dataBytes) {
|
|
344
320
|
// Read the chunk and add it
|
|
345
|
-
|
|
321
|
+
console.debug(" Space is available, adding chunk")
|
|
346
322
|
splitChunks.push(chunk)
|
|
347
323
|
// Track amount of data written on the output device, in bytes
|
|
348
324
|
splitDataBytes += chunk.blocks * header.blockSize
|
|
@@ -350,30 +326,30 @@ export async function* splitBlob(blob: Blob, splitSize: number) {
|
|
|
350
326
|
// Out of space, finish this split
|
|
351
327
|
// Blocks need to be calculated from chunk headers instead of going by size
|
|
352
328
|
// because FILL and SKIP chunks cover more blocks than the data they contain.
|
|
353
|
-
|
|
329
|
+
const splitBlocks = calcChunksBlockSize(splitChunks)
|
|
354
330
|
splitChunks.push({
|
|
355
331
|
type: ChunkType.Skip,
|
|
356
332
|
blocks: header.blocks - splitBlocks,
|
|
357
333
|
data: new Blob([]),
|
|
358
334
|
dataBytes: 0,
|
|
359
335
|
})
|
|
360
|
-
|
|
336
|
+
console.debug(
|
|
361
337
|
`Partition is ${header.blocks} blocks, used ${splitBlocks}, padded with ${
|
|
362
338
|
header.blocks - splitBlocks
|
|
363
339
|
}, finishing split with ${calcChunksBlockSize(splitChunks)} blocks`,
|
|
364
340
|
)
|
|
365
|
-
|
|
366
|
-
|
|
341
|
+
const splitImage = await createImage(header, splitChunks)
|
|
342
|
+
console.debug(
|
|
367
343
|
`Finished ${splitImage.size}-byte split with ${splitChunks.length} chunks`,
|
|
368
344
|
)
|
|
369
345
|
yield {
|
|
370
|
-
data: await
|
|
346
|
+
data: await splitImage.arrayBuffer(),
|
|
371
347
|
bytes: splitDataBytes,
|
|
372
348
|
} as SparseSplit
|
|
373
349
|
|
|
374
350
|
// Start a new split. Every split is considered a full image by the
|
|
375
351
|
// bootloader, so we need to skip the *total* written blocks.
|
|
376
|
-
|
|
352
|
+
console.debug(
|
|
377
353
|
`Starting new split: skipping first ${splitBlocks} blocks and adding chunk`,
|
|
378
354
|
)
|
|
379
355
|
splitChunks = [
|
|
@@ -396,13 +372,37 @@ export async function* splitBlob(blob: Blob, splitSize: number) {
|
|
|
396
372
|
splitChunks.length > 0 &&
|
|
397
373
|
(splitChunks.length > 1 || splitChunks[0].type !== ChunkType.Skip)
|
|
398
374
|
) {
|
|
399
|
-
|
|
400
|
-
|
|
375
|
+
const splitImage = await createImage(header, splitChunks)
|
|
376
|
+
console.debug(
|
|
401
377
|
`Finishing final ${splitImage.size}-byte split with ${splitChunks.length} chunks`,
|
|
402
378
|
)
|
|
403
379
|
yield {
|
|
404
|
-
data: await
|
|
380
|
+
data: await splitImage.arrayBuffer(),
|
|
405
381
|
bytes: splitDataBytes,
|
|
406
382
|
} as SparseSplit
|
|
407
383
|
}
|
|
408
384
|
}
|
|
385
|
+
|
|
386
|
+
export async function parseBlobHeader(blob: Blob): {
|
|
387
|
+
blobSize: number
|
|
388
|
+
totalBytes: number
|
|
389
|
+
isSparse: boolean
|
|
390
|
+
} {
|
|
391
|
+
const FILE_HEADER_SIZE = 28
|
|
392
|
+
const blobSize = blob.size
|
|
393
|
+
let totalBytes = blobSize
|
|
394
|
+
let isSparse = false
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
const fileHeader = await blob.slice(0, FILE_HEADER_SIZE).arrayBuffer()
|
|
398
|
+
const sparseHeader = parseFileHeader(fileHeader)
|
|
399
|
+
if (sparseHeader !== null) {
|
|
400
|
+
totalBytes = sparseHeader.blocks * sparseHeader.blockSize
|
|
401
|
+
isSparse = true
|
|
402
|
+
}
|
|
403
|
+
} catch (error) {
|
|
404
|
+
console.debug(error)
|
|
405
|
+
// ImageError = invalid, so keep blob.size
|
|
406
|
+
}
|
|
407
|
+
return { blobSize, totalBytes, isSparse }
|
|
408
|
+
}
|