@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aepyornis/fastboot.ts",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "fastboot again",
5
5
  "main": "src/index.ts",
6
6
  "scripts": {
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 { parseFileHeader, splitBlob, fromRaw } from "./sparse"
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 directions, typically the contents of fastboot-info.txt
151
- // retriving the flashing data from files in entries
152
- async fastbootInfo(
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 === '--slot-other') {
61
- options.slot = 'other'
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
- let view = new DataView(buffer)
93
+ const view = new DataView(buffer)
113
94
 
114
- let magic = view.getUint32(0, true)
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
- let major = view.getUint16(4, true)
121
- let minor = view.getUint16(6, true)
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
- let fileHdrSize = view.getUint16(8, true)
127
- let chunkHdrSize = view.getUint16(10, true)
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
- let blockSize = view.getUint32(12, true)
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
- let view = new DataView(buffer)
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
- } as SparseChunk
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
- let overhead = FILE_HEADER_SIZE + CHUNK_HEADER_SIZE * chunks.length
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
- let blobBuilder = new BlobBuilder()
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 (let chunk of chunks) {
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
- let chunkArrayView = new Uint8Array(
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
- let header = {
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
- let chunks = []
218
+ const chunks: SparseChunk[] = []
240
219
  while (blob.size > 0) {
241
- let chunkSize = Math.min(blob.size, RAW_CHUNK_SIZE)
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
- } as SparseChunk)
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
- common.logDebug(
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
- common.logDebug("Blob fits in 1 payload, not splitting")
252
+ console.debug("Blob fits in 1 payload, not splitting")
274
253
  yield {
275
- data: await common.readBlobAsBuffer(blob),
254
+ data: await blob.arrayBuffer(),
276
255
  bytes: blob.size,
277
256
  } as SparseSplit
278
257
  return
279
258
  }
280
259
 
281
- let headerData = await common.readBlobAsBuffer(
282
- blob.slice(0, FILE_HEADER_SIZE),
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
- let chunkHeaderData = await common.readBlobAsBuffer(
297
- blob.slice(0, CHUNK_HEADER_SIZE),
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
- let chunksToProcess: SparseChunk[] = []
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
- common.logDebug(
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
- common.logDebug("chunksToProcess", chunksToProcess)
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
- let bytesRemaining = splitSize - calcChunksSize(splitChunks)
339
- common.logVerbose(
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
- common.logVerbose(" Space is available, adding chunk")
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
- let splitBlocks = calcChunksBlockSize(splitChunks)
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
- common.logVerbose(
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
- let splitImage = await createImage(header, splitChunks)
366
- common.logDebug(
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 common.readBlobAsBuffer(splitImage),
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
- common.logVerbose(
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
- let splitImage = await createImage(header, splitChunks)
400
- common.logDebug(
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 common.readBlobAsBuffer(splitImage),
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
+ }