@aepyornis/fastboot.ts 0.0.13 → 0.0.15

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/mise.toml CHANGED
@@ -1,2 +1,2 @@
1
1
  [tools]
2
- node = "22"
2
+ node = "24"
package/package.json CHANGED
@@ -1,24 +1,20 @@
1
1
  {
2
2
  "name": "@aepyornis/fastboot.ts",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "Fastboot using WebUSB",
5
5
  "main": "src/index.ts",
6
6
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1",
8
- "build": "esbuild src/index.ts --bundle --sourcemap --format=esm --platform=browser --target=chrome121 --outfile=dist/fastboot.js"
7
+ "type-check": "tsc --noEmit",
8
+ "build": "tsc --noEmit && esbuild src/index.ts --bundle --sourcemap --format=esm --platform=browser --target=chrome132 --outfile=dist/fastboot.js"
9
9
  },
10
10
  "keywords": [],
11
11
  "author": "Ziggy, Calyx Institute",
12
12
  "license": "MIT",
13
13
  "devDependencies": {
14
- "@eslint/js": "^9.24.0",
15
14
  "@types/w3c-web-usb": "^1.0.10",
16
- "esbuild": "^0.25.2",
17
- "eslint": "^9.24.0",
18
- "globals": "^16.0.0",
15
+ "esbuild": "^0.27.2",
19
16
  "prettier": "^3.5.3",
20
- "typescript": "^5.8.3",
21
- "typescript-eslint": "^8.29.1"
17
+ "typescript": "^5.8.3"
22
18
  },
23
19
  "dependencies": {
24
20
  "@zip.js/zip.js": "^2.7.60"
package/src/client.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { BlobWriter, Entry } from "@zip.js/zip.js"
1
+ import { BlobWriter, type FileEntry } from "@zip.js/zip.js"
2
2
  import { IMAGES } from "./images"
3
3
  import { parseBlobHeader, splitBlob, fromRaw } from "./sparse"
4
- import { FastbootDevice, FastbootDeviceError } from "./device"
4
+ import { FastbootDevice } from "./device"
5
5
 
6
6
  export class FastbootError extends Error {}
7
7
 
@@ -17,17 +17,21 @@ interface Logger {
17
17
  log(message: string): void
18
18
  }
19
19
 
20
+ interface KeyValueDict {
21
+ [key: string]: string
22
+ }
23
+
20
24
  // higher level API to interact with fastboot device
21
25
  // translates CLI commands to
22
26
  export class FastbootClient {
23
27
  fd: FastbootDevice
24
28
  logger: Logger
25
- var_cache: Object
29
+ var_cache: KeyValueDict
26
30
 
27
31
  constructor(usb_device: USBDevice, logger: Logger = window.console) {
28
32
  this.fd = new FastbootDevice(usb_device, logger)
29
33
  this.logger = logger
30
- this.var_cache = {}
34
+ this.var_cache = {} as KeyValueDict
31
35
  }
32
36
 
33
37
  async getVar(variable: string) {
@@ -72,7 +76,6 @@ export class FastbootClient {
72
76
  }
73
77
 
74
78
  async rebootFastboot() {
75
- const serialNumber = this.fd.serialNumber
76
79
  this.logger.log("rebooting into fastboot")
77
80
  await this.fd.exec("reboot-fastboot")
78
81
  await new Promise((resolve) => setTimeout(resolve, 1000))
@@ -83,7 +86,7 @@ export class FastbootClient {
83
86
  partition: string,
84
87
  blob: Blob,
85
88
  slot: "current" | "other" | "a" | "b" = "current",
86
- applyVbmeta: boolean = false,
89
+ applyVbmeta: boolean = false, // TODO: Implement flashing vbmeta
87
90
  ) {
88
91
  // add _a or _b
89
92
  // !(await this.isUserspace()) ?
@@ -164,7 +167,11 @@ export class FastbootClient {
164
167
  }
165
168
 
166
169
  // run text, typically the contents of fastboot-info.txt
167
- async fastbootInfo(entries: Entry[], text: string, wipe: boolean = false) {
170
+ async fastbootInfo(
171
+ entries: FileEntry[],
172
+ text: string,
173
+ wipe: boolean = false,
174
+ ) {
168
175
  const lines = text
169
176
  .split("\n")
170
177
  .map((x) => x.trim())
@@ -216,7 +223,12 @@ export class FastbootClient {
216
223
  this.logger.log(
217
224
  `flashing partition ${partition} with ${filename} from nested zip`,
218
225
  )
219
- await this.doFlash(partition, blob, slot, applyVbmeta)
226
+ await this.doFlash(
227
+ partition!, // TODO: Assert this better
228
+ blob,
229
+ slot as "current" | "b" | "a" | "other" | undefined, // TODO: Assert this better
230
+ applyVbmeta,
231
+ )
220
232
  break
221
233
  }
222
234
  case "reboot":
@@ -236,7 +248,7 @@ export class FastbootClient {
236
248
  }
237
249
  break
238
250
  case "erase":
239
- await this.erase(parts[0])
251
+ await this.erase(parts[0]!) // TODO: Should not assume parts to at least have 1 ele
240
252
  break
241
253
  default:
242
254
  throw new Error(`unknown command ${command}`)
@@ -244,7 +256,7 @@ export class FastbootClient {
244
256
  }
245
257
  }
246
258
 
247
- async updateSuper(entries: Entry[], wipe: boolean) {
259
+ async updateSuper(entries: FileEntry[], wipe: boolean) {
248
260
  const superEmptyImage = entries.find(
249
261
  (e) => e.filename === "super_empty.img",
250
262
  )
@@ -255,7 +267,10 @@ export class FastbootClient {
255
267
  let superName = "super"
256
268
  try {
257
269
  superName = await this.getVar("super-partition-name")
258
- } catch (err) {}
270
+ } catch (err) {
271
+ // TODO: Maybe log this error somewhere?
272
+ void err
273
+ }
259
274
 
260
275
  // fastboot-info does this
261
276
  // await this.rebootFastboot()
@@ -329,15 +344,15 @@ export class FastbootClient {
329
344
 
330
345
  // tested on bangkk only
331
346
  async getUnlockData() {
332
- await client.fd.exec(`oem get_unlock_data`)
347
+ await this.fd.exec(`oem get_unlock_data`)
333
348
 
334
349
  let data = ""
335
350
 
336
- for (let packet of client.fd.session.packets) {
337
- if (packet.command) {
351
+ for (const packet of this.fd.session.packets) {
352
+ if ("command" in packet) {
338
353
  continue
339
354
  } else if (packet.status === "INFO") {
340
- let message = packet.message.replace("(bootloader)", "").trim()
355
+ const message = packet.message?.replace("(bootloader)", "").trim()
341
356
  if (message === "Unlock data:") {
342
357
  continue
343
358
  } else {
@@ -363,13 +378,12 @@ export class FastbootClient {
363
378
  })
364
379
  }
365
380
 
366
- static async findOrRequestDevice(serialNumber: string): Promise<Void> {
367
- for (const device of (await navigator.usb.getDevices())) {
381
+ static async findOrRequestDevice(serialNumber: string): Promise<USBDevice> {
382
+ for (const device of await navigator.usb.getDevices()) {
368
383
  if (device.serialNumber === serialNumber) {
369
- return device
384
+ return device
370
385
  }
371
386
  }
372
387
  return await FastbootClient.requestUsbDevice()
373
388
  }
374
-
375
389
  }
package/src/device.ts CHANGED
@@ -9,7 +9,7 @@ type ResponsePacket = {
9
9
  }
10
10
 
11
11
  type FastbootSession = {
12
- phase: 1 | 2 | 3 | 4 | 5
12
+ // phase: 1 | 2 | 3 | 4 | 5
13
13
  status: null | "OKAY" | "FAIL"
14
14
  packets: (CommandPacket | ResponsePacket)[]
15
15
  }
@@ -18,6 +18,15 @@ interface Logger {
18
18
  log(message: string): void
19
19
  }
20
20
 
21
+ export class FastbootUsbConnectionError extends Error {
22
+ constructor(
23
+ message: string = "Could not find device in navigator.usb.getDevices()",
24
+ ) {
25
+ super(message)
26
+ this.name = "FastbootUsbConnectionError"
27
+ }
28
+ }
29
+
21
30
  export class FastbootDeviceError extends Error {
22
31
  status: string
23
32
 
@@ -32,16 +41,22 @@ export class FastbootDeviceError extends Error {
32
41
  export class FastbootDevice {
33
42
  device: USBDevice
34
43
  serialNumber: string;
35
- in: USBEndpoint
36
- out: USBEndpoint
37
- session: FastbootSession | null
44
+ in!: USBEndpoint
45
+ out!: USBEndpoint
46
+ session: FastbootSession
38
47
  sessions: FastbootSession[]
39
48
  logger: Logger
40
49
 
41
50
  constructor(device: USBDevice, logger: Logger = window.console) {
51
+ if (!device.serialNumber) {
52
+ throw new Error(
53
+ "Access to USBDevice#serialNumber is necessary but stored only in temporary memory.",
54
+ )
55
+ }
56
+
42
57
  this.device = device
43
- this.serialNumber = this.device.serialNumber
44
- this.session = null
58
+ this.serialNumber = device.serialNumber
59
+ this.session = { status: null, packets: [] }
45
60
  this.sessions = []
46
61
  this.logger = logger
47
62
  this.setup()
@@ -51,19 +66,19 @@ export class FastbootDevice {
51
66
  setup() {
52
67
  if (this.device.configurations.length > 1) {
53
68
  console.warn(
54
- `device has ${device.configurations.length} configurations. Using the first one.`,
69
+ `device has ${this.device.configurations.length} configurations. Using the first one.`,
55
70
  )
56
71
  }
57
72
 
58
73
  // this.in = this.device.configurations[0].interfaces[0].alternate.endpoints[0]
59
74
  const endpoints =
60
- this.device.configurations[0].interfaces[0].alternate.endpoints
75
+ this.device.configurations[0]?.interfaces[0]?.alternate.endpoints
61
76
 
62
- if (endpoints.length !== 2) {
77
+ if (endpoints && endpoints.length !== 2) {
63
78
  throw new Error("USB Interface must have only 2 endpoints")
64
79
  }
65
80
 
66
- for (const endpoint of endpoints) {
81
+ for (const endpoint of endpoints ?? []) {
67
82
  if (endpoint.direction === "in") {
68
83
  this.in = endpoint
69
84
  } else if (endpoint.direction === "out") {
@@ -84,49 +99,70 @@ export class FastbootDevice {
84
99
 
85
100
  // some commands (like "flashing lock") will disconnect the device
86
101
  // we have to re-assign this.device after it reconnects
87
- async reconnect() {
102
+ async reconnect(): Promise<boolean> {
88
103
  const devices = await navigator.usb.getDevices()
89
104
  for (const device of devices) {
90
105
  if (device.serialNumber === this.serialNumber) {
106
+ this.logger.log(`reconnect: Found device ${device.serialNumber}`)
91
107
  this.device = device
92
108
  this.setup()
93
109
  await this.connect()
94
110
  return true
95
111
  }
96
112
  }
97
- throw new Error("Could not find device in navigator.usb.getDevices()")
113
+ throw new FastbootUsbConnectionError()
98
114
  }
99
115
 
116
+ // The install requires reboots and it's important we pause the
117
+ // install and reset the usb device after it reconnects
100
118
  async waitForReconnect(): Promise<boolean> {
101
- const devices = await navigator.usb.getDevices()
119
+ try {
120
+ this.logger.log("waitForReconnect try reconnect()")
121
+ return await this.reconnect()
122
+ } catch (e) {
123
+ console.error(e)
124
+ this.logger.log("waitForReconnect wait 3 seconds")
125
+ await new Promise((resolve) => setTimeout(resolve, 3000))
126
+ }
102
127
 
103
- if (devices.some(device => device.serialNumber === this.serialNumber)) {
104
- this.logger.log(`waitForReconnect: Found device ${this.serialNumber}`)
105
- await this.reconnect()
106
- return Promise.resolve(true)
107
- } else {
108
- this.logger.log(`waitForReconnect: Adding navigator.usb event listener`)
109
- return new Promise((resolve, reject) => {
110
- navigator.usb.addEventListener(
111
- "connect",
112
- async (event) => {
113
- this.logger.log(
114
- `waitForReconnect: Device connected ${event.device.productName}`,
115
- )
116
- try {
117
- await this.reconnect()
118
- resolve(true)
119
- } catch (e) {
120
- reject(e)
121
- }
122
- },
123
- { once: true },
124
- )
125
- })
128
+ try {
129
+ return await this.reconnect()
130
+ } catch (e) {
131
+ if (e instanceof FastbootUsbConnectionError) {
132
+ this.logger.log("waitForReconnect wait 30 seconds")
133
+ await new Promise((resolve) => setTimeout(resolve, 30000))
134
+ } else {
135
+ throw e
136
+ }
137
+ }
138
+
139
+ // try once more then wait for navigator.usb connect event
140
+ try {
141
+ return await this.reconnect()
142
+ } catch (e) {
143
+ if (e instanceof FastbootUsbConnectionError) {
144
+ return new Promise((resolve, reject) => {
145
+ this.logger.log("adding navigator.usb connect listener")
146
+ navigator.usb.addEventListener(
147
+ "connect",
148
+ async () => {
149
+ try {
150
+ await this.reconnect()
151
+ resolve(true)
152
+ } catch (e) {
153
+ reject(e)
154
+ }
155
+ },
156
+ { once: true },
157
+ )
158
+ })
159
+ }
126
160
  }
161
+
162
+ return false // TODO: Assume that return false if all else fails
127
163
  }
128
164
 
129
- async getPacket(): ResponsePacket {
165
+ async getPacket(): Promise<ResponsePacket> {
130
166
  this.logger.log(`receiving packet from endpoint ${this.in.endpointNumber}`)
131
167
  const inPacket = await this.device.transferIn(this.in.endpointNumber, 256)
132
168
  const inPacketText = new TextDecoder().decode(inPacket.data)
@@ -163,40 +199,39 @@ export class FastbootDevice {
163
199
  } while (["INFO", "TEXT"].includes(response.status))
164
200
  }
165
201
 
166
- async sendCommand(text): ResponsePacket {
202
+ async sendCommand(text: string): Promise<ResponsePacket> {
167
203
  this.session.packets.push({ command: text } as CommandPacket)
168
204
  const outPacket = new TextEncoder().encode(text)
169
205
  this.logger.log(
170
206
  `transfering "${text}" to endpoint ${this.out.endpointNumber}`,
171
207
  )
172
- const result: USBOutTransferResult = await this.device.transferOut(
173
- this.out.endpointNumber,
174
- outPacket,
175
- )
208
+ await this.device.transferOut(this.out.endpointNumber, outPacket)
209
+
176
210
  await this.getPackets()
177
211
 
178
- if (this.lastPacket.status === "FAIL") {
212
+ if (
213
+ this.lastPacket &&
214
+ "status" in this.lastPacket &&
215
+ this.lastPacket.status === "FAIL"
216
+ ) {
179
217
  this.session.status = "FAIL"
180
218
  throw new FastbootDeviceError(
181
219
  this.lastPacket.status,
182
- this.lastPacket.message,
220
+ this.lastPacket.message ?? "",
183
221
  )
184
222
  } else {
185
- return this.lastPacket
223
+ return this.lastPacket as ResponsePacket // TODO: Fix bad assertion
186
224
  }
187
225
  }
188
226
 
189
- async exec(command): ResponsePacket {
227
+ async exec(command: string): Promise<ResponsePacket> {
190
228
  if (this.isActive) {
191
229
  throw new Error("fastboot device is busy")
192
230
  } else if (!this.device.opened) {
193
231
  await this.connect()
194
232
  }
195
233
 
196
- if (this.session) {
197
- this.sessions.push(this.session)
198
- }
199
-
234
+ this.sessions.push(this.session)
200
235
  this.session = { status: null, packets: [] }
201
236
 
202
237
  return this.sendCommand(command)
@@ -204,22 +239,31 @@ export class FastbootDevice {
204
239
 
205
240
  async getVar(variable: string): Promise<string> {
206
241
  await this.exec(`getvar:${variable}`)
207
- return this.lastPacket.message
242
+
243
+ if (this.lastPacket && "message" in this.lastPacket) {
244
+ return this.lastPacket.message ?? ""
245
+ }
246
+ return ""
208
247
  }
209
248
 
210
249
  get lastPacket() {
211
- if (!this.session || this.session.packets.length === 0) {
250
+ if (this.session.packets.length === 0) {
212
251
  return null
252
+ } else {
253
+ return this.session.packets[this.session.packets.length - 1]
213
254
  }
214
- return this.session.packets[this.session.packets.length - 1]
215
255
  }
216
256
 
217
257
  get isActive() {
218
- if (!this.session || this.session.packets.length === 0) {
258
+ if (this.session.packets.length === 0) {
219
259
  return false
260
+ } else {
261
+ return (
262
+ this.lastPacket &&
263
+ "status" in this.lastPacket &&
264
+ !["FAIL", "OKAY"].includes(this.lastPacket.status)
265
+ )
220
266
  }
221
-
222
- return !["FAIL", "OKAY"].includes(this.lastPacket.status)
223
267
  }
224
268
 
225
269
  // send buffer to phone
package/src/flasher.ts CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  BlobReader,
4
4
  BlobWriter,
5
5
  TextWriter,
6
- Entry,
6
+ type FileEntry,
7
7
  } from "@zip.js/zip.js"
8
8
  import type { FastbootClient } from "./client"
9
9
 
@@ -21,15 +21,66 @@ type CommandName =
21
21
  | "continue"
22
22
  | "reboot"
23
23
  | "reboot-bootloader"
24
+ | "sleep"
25
+ | "oem"
24
26
  | "help"
25
27
 
28
+ type SlotName = "current" | "other" | "a" | "b"
29
+
30
+ type InstructionOptions = {
31
+ wipe?: boolean
32
+ setActive?: "other" | "a" | "b"
33
+ slot?: SlotName
34
+ skipReboot?: boolean
35
+ applyVbmeta?: boolean
36
+ }
37
+
26
38
  type Instruction = {
27
39
  command: CommandName
28
40
  args: string[]
29
- options: object
41
+ options: InstructionOptions
42
+ }
43
+
44
+ const COMMAND_NAMES: ReadonlySet<string> = new Set([
45
+ "update",
46
+ "flashall",
47
+ "flash",
48
+ "flashing",
49
+ "erase",
50
+ "format",
51
+ "getvar",
52
+ "set_active",
53
+ "boot",
54
+ "devices",
55
+ "continue",
56
+ "reboot",
57
+ "reboot-bootloader",
58
+ "sleep",
59
+ "oem",
60
+ "help",
61
+ ])
62
+
63
+ function isCommandName(word: string): word is CommandName {
64
+ return COMMAND_NAMES.has(word)
65
+ }
66
+
67
+ function isFileEntry(entry: { directory: boolean }): entry is FileEntry {
68
+ return !entry.directory
69
+ }
70
+
71
+ function requireArg(
72
+ command: CommandName,
73
+ args: string[],
74
+ index: number,
75
+ ): string {
76
+ const value = args[index]
77
+ if (!value) {
78
+ throw new Error(`Missing argument ${index + 1} for ${command}`)
79
+ }
80
+ return value
30
81
  }
31
82
 
32
- function getEntry(entries: Entry[], filename: string): Entry {
83
+ function getEntry(entries: FileEntry[], filename: string): FileEntry {
33
84
  const entry = entries.find(
34
85
  (e) => e.filename.split(/[\\/]/).pop() === filename,
35
86
  )
@@ -45,11 +96,14 @@ function parseInstruction(text: string): Instruction {
45
96
  text = text.slice(8).trim()
46
97
  }
47
98
 
48
- let command: CommandName
49
- const args = []
50
- const options = {}
99
+ let command: CommandName | undefined
100
+ const args: string[] = []
101
+ const options: InstructionOptions = {}
51
102
 
52
- const words = text.split(" ").map((x) => x.trim())
103
+ const words = text
104
+ .split(/\s+/)
105
+ .map((x) => x.trim())
106
+ .filter((x) => x !== "")
53
107
 
54
108
  for (const word of words) {
55
109
  if (word[0] === "-") {
@@ -58,15 +112,21 @@ function parseInstruction(text: string): Instruction {
58
112
  } else if (word === "--set-active=other") {
59
113
  options.setActive = "other"
60
114
  } else if (word === "--set-active=a" || word === "--set-active=b") {
61
- options.setActive = word.slice(-1)
115
+ const slot = word.slice(-1)
116
+ if (slot === "a" || slot === "b") {
117
+ options.setActive = slot
118
+ }
62
119
  } else if (word === "--slot-other") {
63
120
  options.slot = "other"
64
121
  } else if (word.slice(0, 6) === "--slot") {
65
122
  const slot = word.split("=")[1]
123
+ if (!slot) {
124
+ throw new Error("--slot requires a value")
125
+ }
66
126
  if (!["current", "other", "a", "b"].includes(slot)) {
67
127
  throw new Error(`unknown slot: ${slot}`)
68
128
  }
69
- options.slot = slot
129
+ options.slot = slot as SlotName
70
130
  } else if (word === "--skip-reboot") {
71
131
  options.skipReboot = true
72
132
  } else if (word === "--apply-vbmeta") {
@@ -78,10 +138,16 @@ function parseInstruction(text: string): Instruction {
78
138
  if (command) {
79
139
  args.push(word)
80
140
  } else {
141
+ if (!isCommandName(word)) {
142
+ throw new Error(`Unknown command: ${word}`)
143
+ }
81
144
  command = word
82
145
  }
83
146
  }
84
147
  }
148
+ if (!command) {
149
+ throw new Error("Missing command")
150
+ }
85
151
  return { command, args, options }
86
152
  }
87
153
 
@@ -96,7 +162,7 @@ function parseInstructions(text: string): Instruction[] {
96
162
 
97
163
  export class FastbootFlasher {
98
164
  client: FastbootClient
99
- reader: ZipReader
165
+ reader: ZipReader<BlobReader>
100
166
 
101
167
  constructor(client: FastbootClient, blob: Blob) {
102
168
  this.client = client
@@ -106,7 +172,7 @@ export class FastbootFlasher {
106
172
  // parses and runs flash-all.sh. it ignores all shell commands
107
173
  // except fastboot or sleep
108
174
  async runFlashAll() {
109
- const entries: Entry[] = await this.reader.getEntries()
175
+ const entries = (await this.reader.getEntries()).filter(isFileEntry)
110
176
  const flashAllSh = await getEntry(entries, "flash-all.sh").getData(
111
177
  new TextWriter(),
112
178
  )
@@ -121,16 +187,16 @@ export class FastbootFlasher {
121
187
  return this.run(instructions)
122
188
  }
123
189
 
124
- async run(instructions: text) {
125
- const entries: Entry[] = await this.reader.getEntries() // io with factory.zip
190
+ async run(instructions: string) {
191
+ const entries = (await this.reader.getEntries()).filter(isFileEntry) // io with factory.zip
126
192
  const commands: Instruction[] = parseInstructions(instructions)
127
193
 
128
194
  for (const command of commands) {
129
195
  this.client.logger.log(`‣ ${JSON.stringify(command)}`)
130
196
  if (command.command === "flash") {
131
- const partition = command.args[0]
132
- const filename = command.args[1]
133
- const slot = command.options.slot || "current"
197
+ const partition = requireArg(command.command, command.args, 0)
198
+ const filename = requireArg(command.command, command.args, 1)
199
+ const slot = command.options.slot ?? "current"
134
200
  const entry = getEntry(entries, filename)
135
201
  const blob = await entry.getData(
136
202
  new BlobWriter("application/octet-stream"),
@@ -152,15 +218,19 @@ export class FastbootFlasher {
152
218
  }
153
219
  await this.client.rebootBootloader()
154
220
  } else if (command.command === "update") {
155
- const nestedZipEntry = getEntry(entries, command.args[0])
221
+ const zipName = requireArg(command.command, command.args, 0)
222
+ const nestedZipEntry = getEntry(entries, zipName)
156
223
  const zipBlob = await nestedZipEntry.getData(
157
224
  new BlobWriter("application/zip"),
158
225
  )
159
226
  const zipReader = new ZipReader(new BlobReader(zipBlob))
160
- const nestedEntries = await zipReader.getEntries()
227
+ const nestedEntries = (await zipReader.getEntries()).filter(isFileEntry)
161
228
  const fastbootInfoFile = nestedEntries.find(
162
229
  (e) => e.filename === "fastboot-info.txt",
163
230
  )
231
+ if (!fastbootInfoFile) {
232
+ throw new Error("fastboot-info.txt not found in nested zip")
233
+ }
164
234
  const fastbootInfoText = await fastbootInfoFile.getData(
165
235
  new TextWriter(),
166
236
  )
@@ -182,10 +252,12 @@ export class FastbootFlasher {
182
252
  throw new Error(`Unknown command`)
183
253
  }
184
254
  } else if (command.command === "getvar") {
185
- const clientVar = await this.client.getVar(command.args[0])
186
- this.client.logger(`getVar(${command.args[0]}) => ${clientVar}`)
255
+ const varName = requireArg(command.command, command.args, 0)
256
+ const clientVar = await this.client.getVar(varName)
257
+ this.client.logger.log(`getVar(${varName}) => ${clientVar}`)
187
258
  } else if (command.command === "erase") {
188
- await this.client.erase(command.args[0])
259
+ const partition = requireArg(command.command, command.args, 0)
260
+ await this.client.erase(partition)
189
261
  } else if (command.command === "sleep") {
190
262
  const ms = command.args[0] ? parseInt(command.args[0]) * 1000 : 5000
191
263
  await new Promise((resolve) => setTimeout(resolve, ms))
package/src/sparse.ts CHANGED
@@ -65,6 +65,12 @@ export interface SparseChunk {
65
65
  data: Blob | null // to be populated by consumer
66
66
  }
67
67
 
68
+ export interface BlobHeader {
69
+ blobSize: number
70
+ totalBytes: number
71
+ isSparse: boolean
72
+ }
73
+
68
74
  class BlobBuilder {
69
75
  private blob: Blob
70
76
  private type: string
@@ -221,6 +227,7 @@ export async function fromRaw(blob: Blob): Promise<Blob> {
221
227
  chunks.push({
222
228
  type: ChunkType.Raw,
223
229
  blocks: chunkSize / header.blockSize,
230
+ dataBytes: chunkSize,
224
231
  data: blob.slice(0, chunkSize),
225
232
  })
226
233
  blob = blob.slice(chunkSize)
@@ -370,7 +377,7 @@ export async function* splitBlob(blob: Blob, splitSize: number) {
370
377
  // Finish the final split if necessary
371
378
  if (
372
379
  splitChunks.length > 0 &&
373
- (splitChunks.length > 1 || splitChunks[0].type !== ChunkType.Skip)
380
+ (splitChunks.length > 1 || splitChunks[0]?.type !== ChunkType.Skip)
374
381
  ) {
375
382
  const splitImage = await createImage(header, splitChunks)
376
383
  console.debug(
@@ -383,11 +390,7 @@ export async function* splitBlob(blob: Blob, splitSize: number) {
383
390
  }
384
391
  }
385
392
 
386
- export async function parseBlobHeader(blob: Blob): {
387
- blobSize: number
388
- totalBytes: number
389
- isSparse: boolean
390
- } {
393
+ export async function parseBlobHeader(blob: Blob): Promise<BlobHeader> {
391
394
  const FILE_HEADER_SIZE = 28
392
395
  const blobSize = blob.size
393
396
  let totalBytes = blobSize
package/tsconfig.json CHANGED
@@ -1,14 +1,26 @@
1
1
  {
2
2
  "compilerOptions": {
3
- "target": "es6",
4
- "lib": ["es6", "dom"],
5
- "module": "es6",
6
- "sourceMap": true,
7
- "allowJs": false,
3
+ /* Base Options: */
8
4
  "esModuleInterop": true,
9
- "forceConsistentCasingInFileNames": true,
5
+ "skipLibCheck": true,
6
+ "target": "es2022",
7
+ "allowJs": true,
8
+ "resolveJsonModule": true,
9
+ "moduleDetection": "force",
10
+ "isolatedModules": true,
11
+ "verbatimModuleSyntax": true,
12
+
13
+ /* Strictness */
10
14
  "strict": true,
11
- "noEmit": true
15
+ "noUncheckedIndexedAccess": true,
16
+ "noImplicitOverride": true,
17
+
18
+ "sourceMap": true,
19
+ "declaration": true,
20
+ "module": "preserve",
21
+ "noEmit": true,
22
+
23
+ "lib": ["es2022", "dom", "dom.iterable"],
12
24
  },
13
25
  "include": ["src/**/*"],
14
26
  "exclude": []