@calyxos/fastboot.ts 0.0.18-rc.1
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/README.md +34 -0
- package/dist/fastboot.d.ts +101 -0
- package/dist/fastboot.js +1019 -0
- package/dist/fastboot.js.map +1 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# fastboot.ts
|
|
2
|
+
|
|
3
|
+
Android Fastboot implementation for WebUSB
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install
|
|
7
|
+
npm run build
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
src/device.ts handles interfacing with WebUSB and implements fastboot protocol
|
|
11
|
+
src/client.ts implements higher level API, similar to fastboot cli tool
|
|
12
|
+
src/flasher.ts flashes zip image from a list of instructions
|
|
13
|
+
src/sparse.ts sparse image utilities Copyright (c) 2021 Danny Lin <danny@kdrag0n.dev>
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { FastbootDevice, FastbootClient, FastbootFlasher } from "fastboot.ts"
|
|
17
|
+
|
|
18
|
+
const client = await FastbootClient.create()
|
|
19
|
+
|
|
20
|
+
// run commands
|
|
21
|
+
await client.unlock()
|
|
22
|
+
await client.getVar("product")
|
|
23
|
+
|
|
24
|
+
// flash CalyxOS
|
|
25
|
+
import OpfsBlobStore from "@aepyornis/opfs_blob_store"
|
|
26
|
+
const opfs = await OpfsBlobStore.create()
|
|
27
|
+
const hash = "db9ab330a1b5d5ebf131f378dca8b5f6400337f438a97aef2a09a1ba88f3935c"
|
|
28
|
+
const url = "https://release.calyxinstitute.org/bangkk-factory-25608210.zip"
|
|
29
|
+
await opfs.fetch(hash, url)
|
|
30
|
+
const file = await opfs.get(hash)
|
|
31
|
+
const client = await FastbootClient.create()
|
|
32
|
+
const deviceFlasher = new FastbootFlasher(client, file)
|
|
33
|
+
await deviceFlasher.runFlashAll()
|
|
34
|
+
```
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { BlobReader, FileEntry, ZipReader } from "@zip.js/zip.js";
|
|
2
|
+
|
|
3
|
+
//#region src/device.d.ts
|
|
4
|
+
type CommandPacket = {
|
|
5
|
+
command: string;
|
|
6
|
+
};
|
|
7
|
+
type ResponsePacket = {
|
|
8
|
+
status: "OKAY" | "FAIL" | "DATA" | "INFO" | "TEXT";
|
|
9
|
+
message?: string;
|
|
10
|
+
dataLength?: number;
|
|
11
|
+
};
|
|
12
|
+
type FastbootSession = {
|
|
13
|
+
status: null | "OKAY" | "FAIL";
|
|
14
|
+
packets: (CommandPacket | ResponsePacket)[];
|
|
15
|
+
};
|
|
16
|
+
interface Logger$1 {
|
|
17
|
+
log(message: string): void;
|
|
18
|
+
}
|
|
19
|
+
declare class FastbootDevice {
|
|
20
|
+
device: USBDevice;
|
|
21
|
+
serialNumber: string;
|
|
22
|
+
in: USBEndpoint;
|
|
23
|
+
out: USBEndpoint;
|
|
24
|
+
session: FastbootSession;
|
|
25
|
+
sessions: FastbootSession[];
|
|
26
|
+
logger: Logger$1;
|
|
27
|
+
constructor(device: USBDevice, logger?: Logger$1);
|
|
28
|
+
setup(): void;
|
|
29
|
+
connect(): Promise<void>;
|
|
30
|
+
reconnect(): Promise<boolean>;
|
|
31
|
+
private retryReconnect;
|
|
32
|
+
waitForReconnect(): Promise<boolean>;
|
|
33
|
+
waitForReconnectFastboot(userAction: () => Promise<unknown>): Promise<boolean>;
|
|
34
|
+
getPacket(): Promise<ResponsePacket>;
|
|
35
|
+
getPackets(): Promise<void>;
|
|
36
|
+
sendCommand(text: string): Promise<ResponsePacket>;
|
|
37
|
+
exec(command: string): Promise<ResponsePacket>;
|
|
38
|
+
getVar(variable: string): Promise<string>;
|
|
39
|
+
get lastPacket(): ResponsePacket | CommandPacket | null | undefined;
|
|
40
|
+
get isActive(): boolean | null | undefined;
|
|
41
|
+
transferData(buffer: ArrayBuffer): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/client.d.ts
|
|
45
|
+
interface Logger {
|
|
46
|
+
log(message: string): void;
|
|
47
|
+
}
|
|
48
|
+
interface KeyValueDict {
|
|
49
|
+
[key: string]: string;
|
|
50
|
+
}
|
|
51
|
+
declare class FastbootClient {
|
|
52
|
+
fd: FastbootDevice;
|
|
53
|
+
logger: Logger;
|
|
54
|
+
var_cache: KeyValueDict;
|
|
55
|
+
reconnectUserAction: () => Promise<unknown>;
|
|
56
|
+
constructor(usb_device: USBDevice, logger?: Logger);
|
|
57
|
+
getVar(variable: string): Promise<string>;
|
|
58
|
+
getVarCache(variable: string): Promise<string>;
|
|
59
|
+
lock(): Promise<void>;
|
|
60
|
+
unlock(): Promise<void>;
|
|
61
|
+
reboot(): Promise<void>;
|
|
62
|
+
rebootBootloader(): Promise<void>;
|
|
63
|
+
rebootFastboot(): Promise<void>;
|
|
64
|
+
doFlash(partition: string, blob: Blob, slot?: "current" | "other" | "a" | "b", applyVbmeta?: boolean): Promise<void>;
|
|
65
|
+
resizePartition(name: string, totalBytes: number): Promise<void>;
|
|
66
|
+
flashing(command: "unlock" | "lock"): Promise<true | undefined>;
|
|
67
|
+
fastbootInfo(entries: FileEntry[], text: string, wipe?: boolean): Promise<void>;
|
|
68
|
+
updateSuper(entries: FileEntry[], wipe: boolean): Promise<void>;
|
|
69
|
+
erase(partition: string): Promise<{
|
|
70
|
+
status: "OKAY" | "FAIL" | "DATA" | "INFO" | "TEXT";
|
|
71
|
+
message?: string;
|
|
72
|
+
dataLength?: number;
|
|
73
|
+
}>;
|
|
74
|
+
setActiveOtherSlot(): Promise<{
|
|
75
|
+
status: "OKAY" | "FAIL" | "DATA" | "INFO" | "TEXT";
|
|
76
|
+
message?: string;
|
|
77
|
+
dataLength?: number;
|
|
78
|
+
}>;
|
|
79
|
+
maxDownloadSize(): Promise<number>;
|
|
80
|
+
unlocked(): Promise<boolean>;
|
|
81
|
+
locked(): Promise<boolean>;
|
|
82
|
+
currentSlot(): Promise<string>;
|
|
83
|
+
otherSlot(): Promise<"b" | "a">;
|
|
84
|
+
isUserspace(): Promise<boolean>;
|
|
85
|
+
getUnlockData(): Promise<string>;
|
|
86
|
+
static create(): Promise<FastbootClient>;
|
|
87
|
+
static requestUsbDevice(): Promise<USBDevice>;
|
|
88
|
+
static findOrRequestDevice(serialNumber: string): Promise<USBDevice>;
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/flasher.d.ts
|
|
92
|
+
declare class FastbootFlasher {
|
|
93
|
+
client: FastbootClient;
|
|
94
|
+
reader: ZipReader<BlobReader>;
|
|
95
|
+
constructor(client: FastbootClient, blob: Blob);
|
|
96
|
+
runFlashAll(): Promise<void>;
|
|
97
|
+
run(instructions: string): Promise<void>;
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
100
|
+
export { FastbootClient, FastbootDevice, FastbootFlasher };
|
|
101
|
+
//# sourceMappingURL=fastboot.d.ts.map
|
package/dist/fastboot.js
ADDED
|
@@ -0,0 +1,1019 @@
|
|
|
1
|
+
import { BlobReader, BlobWriter, TextWriter, ZipReader } from "@zip.js/zip.js";
|
|
2
|
+
//#region src/device.ts
|
|
3
|
+
var FastbootUsbConnectionError = class extends Error {
|
|
4
|
+
constructor(message = "Could not find device in navigator.usb.getDevices()") {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "FastbootUsbConnectionError";
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
var FastbootDeviceError = class extends Error {
|
|
10
|
+
status;
|
|
11
|
+
constructor(status, message) {
|
|
12
|
+
super(`Bootloader replied with ${status}: ${message}`);
|
|
13
|
+
this.status = status;
|
|
14
|
+
this.name = "FastbootDeviceError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var FastbootDevice = class {
|
|
18
|
+
device;
|
|
19
|
+
serialNumber;
|
|
20
|
+
in;
|
|
21
|
+
out;
|
|
22
|
+
session;
|
|
23
|
+
sessions;
|
|
24
|
+
logger;
|
|
25
|
+
constructor(device, logger = window.console) {
|
|
26
|
+
if (!device.serialNumber) throw new Error("Access to USBDevice#serialNumber is necessary but stored only in temporary memory.");
|
|
27
|
+
this.device = device;
|
|
28
|
+
this.serialNumber = device.serialNumber;
|
|
29
|
+
this.session = {
|
|
30
|
+
status: null,
|
|
31
|
+
packets: []
|
|
32
|
+
};
|
|
33
|
+
this.sessions = [];
|
|
34
|
+
this.logger = logger;
|
|
35
|
+
this.setup();
|
|
36
|
+
}
|
|
37
|
+
setup() {
|
|
38
|
+
if (this.device.configurations.length > 1) console.warn(`device has ${this.device.configurations.length} configurations. Using the first one.`);
|
|
39
|
+
const endpoints = this.device.configurations[0]?.interfaces[0]?.alternate.endpoints;
|
|
40
|
+
if (endpoints && endpoints.length !== 2) throw new Error("USB Interface must have only 2 endpoints");
|
|
41
|
+
for (const endpoint of endpoints ?? []) if (endpoint.direction === "in") this.in = endpoint;
|
|
42
|
+
else if (endpoint.direction === "out") this.out = endpoint;
|
|
43
|
+
else throw new Error(`Endpoint error: ${endpoint}`);
|
|
44
|
+
}
|
|
45
|
+
async connect() {
|
|
46
|
+
if (!this.device.opened) await this.device.open();
|
|
47
|
+
await this.device.selectConfiguration(1);
|
|
48
|
+
await this.device.claimInterface(0);
|
|
49
|
+
}
|
|
50
|
+
async reconnect() {
|
|
51
|
+
const devices = await navigator.usb.getDevices();
|
|
52
|
+
for (const device of devices) if (device.serialNumber === this.serialNumber) {
|
|
53
|
+
this.logger.log(`reconnect: Found device ${device.serialNumber}`);
|
|
54
|
+
this.device = device;
|
|
55
|
+
this.setup();
|
|
56
|
+
await this.connect();
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
throw new FastbootUsbConnectionError();
|
|
60
|
+
}
|
|
61
|
+
async retryReconnect(label) {
|
|
62
|
+
try {
|
|
63
|
+
this.logger.log(`${label} try reconnect()`);
|
|
64
|
+
return await this.reconnect();
|
|
65
|
+
} catch (e) {
|
|
66
|
+
console.error(e);
|
|
67
|
+
this.logger.log(`${label} wait 3 seconds`);
|
|
68
|
+
await new Promise((resolve) => setTimeout(resolve, 3e3));
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
return await this.reconnect();
|
|
72
|
+
} catch (e) {
|
|
73
|
+
if (e instanceof FastbootUsbConnectionError) {
|
|
74
|
+
console.error(e);
|
|
75
|
+
this.logger.log(`${label} wait 30 seconds`);
|
|
76
|
+
await new Promise((resolve) => setTimeout(resolve, 3e4));
|
|
77
|
+
} else throw e;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
async waitForReconnect() {
|
|
82
|
+
if (await this.retryReconnect("waitForReconnect")) return true;
|
|
83
|
+
try {
|
|
84
|
+
return await this.reconnect();
|
|
85
|
+
} catch (e) {
|
|
86
|
+
if (e instanceof FastbootUsbConnectionError) return new Promise((resolve, reject) => {
|
|
87
|
+
this.logger.log("adding navigator.usb connect listener");
|
|
88
|
+
navigator.usb.addEventListener("connect", async () => {
|
|
89
|
+
try {
|
|
90
|
+
await this.reconnect();
|
|
91
|
+
resolve(true);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
reject(e);
|
|
94
|
+
}
|
|
95
|
+
}, { once: true });
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
async waitForReconnectFastboot(userAction) {
|
|
101
|
+
if (await this.retryReconnect("waitForReconnectFastboot")) return true;
|
|
102
|
+
try {
|
|
103
|
+
return await this.reconnect();
|
|
104
|
+
} catch (e) {
|
|
105
|
+
console.error(e);
|
|
106
|
+
try {
|
|
107
|
+
await userAction();
|
|
108
|
+
this.logger.log("waitForReconnectFastboot after user action");
|
|
109
|
+
return await this.reconnect();
|
|
110
|
+
} catch (e) {
|
|
111
|
+
console.error(e);
|
|
112
|
+
this.logger.log("waitForReconnectFastboot reconnect() failed after user action");
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async getPacket() {
|
|
118
|
+
this.logger.log(`receiving packet from endpoint ${this.in.endpointNumber}`);
|
|
119
|
+
const inPacket = await this.device.transferIn(this.in.endpointNumber, 256);
|
|
120
|
+
const inPacketText = new TextDecoder().decode(inPacket.data);
|
|
121
|
+
const status = inPacketText.substring(0, 4);
|
|
122
|
+
const message = inPacketText.slice(4).trim();
|
|
123
|
+
switch (status) {
|
|
124
|
+
case "INFO": return {
|
|
125
|
+
status,
|
|
126
|
+
message: `(bootloader) ${message}`
|
|
127
|
+
};
|
|
128
|
+
case "TEXT": return {
|
|
129
|
+
status,
|
|
130
|
+
message
|
|
131
|
+
};
|
|
132
|
+
case "FAIL": return {
|
|
133
|
+
status,
|
|
134
|
+
message
|
|
135
|
+
};
|
|
136
|
+
case "OKAY": return {
|
|
137
|
+
status,
|
|
138
|
+
message
|
|
139
|
+
};
|
|
140
|
+
case "DATA": {
|
|
141
|
+
const dataLength = parseInt(inPacketText.slice(4, 12), 16);
|
|
142
|
+
return {
|
|
143
|
+
status,
|
|
144
|
+
dataLength,
|
|
145
|
+
message: `ready to transfer ${dataLength} bytes`
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
default: throw new Error(`invalid packet: ${inPacketText}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async getPackets() {
|
|
152
|
+
let response;
|
|
153
|
+
do {
|
|
154
|
+
response = await this.getPacket();
|
|
155
|
+
this.session.packets.push(response);
|
|
156
|
+
this.logger.log(`[${response.status}] ${response.message}`);
|
|
157
|
+
} while (["INFO", "TEXT"].includes(response.status));
|
|
158
|
+
}
|
|
159
|
+
async sendCommand(text) {
|
|
160
|
+
this.session.packets.push({ command: text });
|
|
161
|
+
const outPacket = new TextEncoder().encode(text);
|
|
162
|
+
this.logger.log(`transfering "${text}" to endpoint ${this.out.endpointNumber}`);
|
|
163
|
+
await this.device.transferOut(this.out.endpointNumber, outPacket);
|
|
164
|
+
await this.getPackets();
|
|
165
|
+
if (this.lastPacket && "status" in this.lastPacket && this.lastPacket.status === "FAIL") {
|
|
166
|
+
this.session.status = "FAIL";
|
|
167
|
+
throw new FastbootDeviceError(this.lastPacket.status, this.lastPacket.message ?? "");
|
|
168
|
+
} else return this.lastPacket;
|
|
169
|
+
}
|
|
170
|
+
async exec(command) {
|
|
171
|
+
if (this.isActive) throw new Error("fastboot device is busy");
|
|
172
|
+
else if (!this.device.opened) await this.connect();
|
|
173
|
+
this.sessions.push(this.session);
|
|
174
|
+
this.session = {
|
|
175
|
+
status: null,
|
|
176
|
+
packets: []
|
|
177
|
+
};
|
|
178
|
+
return this.sendCommand(command);
|
|
179
|
+
}
|
|
180
|
+
async getVar(variable) {
|
|
181
|
+
await this.exec(`getvar:${variable}`);
|
|
182
|
+
if (this.lastPacket && "message" in this.lastPacket) return this.lastPacket.message ?? "";
|
|
183
|
+
return "";
|
|
184
|
+
}
|
|
185
|
+
get lastPacket() {
|
|
186
|
+
if (this.session.packets.length === 0) return null;
|
|
187
|
+
else return this.session.packets[this.session.packets.length - 1];
|
|
188
|
+
}
|
|
189
|
+
get isActive() {
|
|
190
|
+
if (this.session.packets.length === 0) return false;
|
|
191
|
+
else return this.lastPacket && "status" in this.lastPacket && !["FAIL", "OKAY"].includes(this.lastPacket.status);
|
|
192
|
+
}
|
|
193
|
+
async transferData(buffer) {
|
|
194
|
+
const xferHex = buffer.byteLength.toString(16).padStart(8, "0");
|
|
195
|
+
if (xferHex.length !== 8) throw new FastbootDeviceError("FAIL", `Transfer size overflow: ${xferHex} is more than 8 digits`);
|
|
196
|
+
this.logger.log(`Sending command download:${xferHex}.`);
|
|
197
|
+
const response = await this.sendCommand(`download:${xferHex}`);
|
|
198
|
+
if (response.status !== "DATA") throw new FastbootDeviceError("FAIL", `response to download:${xferHex} is ${response.status}. Expected DATA.`);
|
|
199
|
+
else if (response.dataLength !== buffer.byteLength) throw new FastbootDeviceError("FAIL", `Bootloader wants ${response.dataLength} bytes, requested to send ${buffer.byteLength} bytes`);
|
|
200
|
+
const BULK_TRANSFER_SIZE = 16384;
|
|
201
|
+
this.logger.log(`Sending payload: ${buffer.byteLength} bytes`);
|
|
202
|
+
let i = 0;
|
|
203
|
+
let remainingBytes = buffer.byteLength;
|
|
204
|
+
while (remainingBytes > 0) {
|
|
205
|
+
const chunk = buffer.slice(i * BULK_TRANSFER_SIZE, (i + 1) * BULK_TRANSFER_SIZE);
|
|
206
|
+
if (i % 1e3 === 0) this.logger.log(`Sending ${chunk.byteLength} bytes to endpoint, ${remainingBytes} remaining, i=${i}`);
|
|
207
|
+
await this.device.transferOut(this.out.endpointNumber, chunk);
|
|
208
|
+
remainingBytes -= chunk.byteLength;
|
|
209
|
+
i += 1;
|
|
210
|
+
}
|
|
211
|
+
this.logger.log("Payload sent, waiting for response...");
|
|
212
|
+
await this.getPackets();
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
//#endregion
|
|
216
|
+
//#region src/images.ts
|
|
217
|
+
const IMAGES = [
|
|
218
|
+
{
|
|
219
|
+
nickname: "boot",
|
|
220
|
+
img_name: "boot.img",
|
|
221
|
+
sig_name: "boot.sig",
|
|
222
|
+
part_name: "boot",
|
|
223
|
+
optional: false,
|
|
224
|
+
type: "BootCritical"
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
nickname: "bootloader",
|
|
228
|
+
img_name: "bootloader.img",
|
|
229
|
+
sig_name: "",
|
|
230
|
+
part_name: "bootloader",
|
|
231
|
+
optional: true,
|
|
232
|
+
type: "Extra"
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
nickname: "init_boot",
|
|
236
|
+
img_name: "init_boot.img",
|
|
237
|
+
sig_name: "init_boot.sig",
|
|
238
|
+
part_name: "init_boot",
|
|
239
|
+
optional: true,
|
|
240
|
+
type: "BootCritical"
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
nickname: "",
|
|
244
|
+
img_name: "boot_other.img",
|
|
245
|
+
sig_name: "boot.sig",
|
|
246
|
+
part_name: "boot",
|
|
247
|
+
optional: true,
|
|
248
|
+
type: "Normal"
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
nickname: "cache",
|
|
252
|
+
img_name: "cache.img",
|
|
253
|
+
sig_name: "cache.sig",
|
|
254
|
+
part_name: "cache",
|
|
255
|
+
optional: true,
|
|
256
|
+
type: "Extra"
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
nickname: "dtbo",
|
|
260
|
+
img_name: "dtbo.img",
|
|
261
|
+
sig_name: "dtbo.sig",
|
|
262
|
+
part_name: "dtbo",
|
|
263
|
+
optional: true,
|
|
264
|
+
type: "BootCritical"
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
nickname: "dts",
|
|
268
|
+
img_name: "dt.img",
|
|
269
|
+
sig_name: "dt.sig",
|
|
270
|
+
part_name: "dts",
|
|
271
|
+
optional: true,
|
|
272
|
+
type: "BootCritical"
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
nickname: "odm",
|
|
276
|
+
img_name: "odm.img",
|
|
277
|
+
sig_name: "odm.sig",
|
|
278
|
+
part_name: "odm",
|
|
279
|
+
optional: true,
|
|
280
|
+
type: "Normal"
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
nickname: "odm_dlkm",
|
|
284
|
+
img_name: "odm_dlkm.img",
|
|
285
|
+
sig_name: "odm_dlkm.sig",
|
|
286
|
+
part_name: "odm_dlkm",
|
|
287
|
+
optional: true,
|
|
288
|
+
type: "Normal"
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
nickname: "product",
|
|
292
|
+
img_name: "product.img",
|
|
293
|
+
sig_name: "product.sig",
|
|
294
|
+
part_name: "product",
|
|
295
|
+
optional: true,
|
|
296
|
+
type: "Normal"
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
nickname: "pvmfw",
|
|
300
|
+
img_name: "pvmfw.img",
|
|
301
|
+
sig_name: "pvmfw.sig",
|
|
302
|
+
part_name: "pvmfw",
|
|
303
|
+
optional: true,
|
|
304
|
+
type: "BootCritical"
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
nickname: "radio",
|
|
308
|
+
img_name: "radio.img",
|
|
309
|
+
sig_name: "",
|
|
310
|
+
part_name: "radio",
|
|
311
|
+
optional: true,
|
|
312
|
+
type: "Extra"
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
nickname: "recovery",
|
|
316
|
+
img_name: "recovery.img",
|
|
317
|
+
sig_name: "recovery.sig",
|
|
318
|
+
part_name: "recovery",
|
|
319
|
+
optional: true,
|
|
320
|
+
type: "BootCritical"
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
nickname: "super",
|
|
324
|
+
img_name: "super.img",
|
|
325
|
+
sig_name: "super.sig",
|
|
326
|
+
part_name: "super",
|
|
327
|
+
optional: true,
|
|
328
|
+
type: "Extra"
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
nickname: "system",
|
|
332
|
+
img_name: "system.img",
|
|
333
|
+
sig_name: "system.sig",
|
|
334
|
+
part_name: "system",
|
|
335
|
+
optional: false,
|
|
336
|
+
type: "Normal"
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
nickname: "system_dlkm",
|
|
340
|
+
img_name: "system_dlkm.img",
|
|
341
|
+
sig_name: "system_dlkm.sig",
|
|
342
|
+
part_name: "system_dlkm",
|
|
343
|
+
optional: true,
|
|
344
|
+
type: "Normal"
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
nickname: "system_ext",
|
|
348
|
+
img_name: "system_ext.img",
|
|
349
|
+
sig_name: "system_ext.sig",
|
|
350
|
+
part_name: "system_ext",
|
|
351
|
+
optional: true,
|
|
352
|
+
type: "Normal"
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
nickname: "",
|
|
356
|
+
img_name: "system_other.img",
|
|
357
|
+
sig_name: "system.sig",
|
|
358
|
+
part_name: "system",
|
|
359
|
+
optional: true,
|
|
360
|
+
type: "Normal"
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
nickname: "userdata",
|
|
364
|
+
img_name: "userdata.img",
|
|
365
|
+
sig_name: "userdata.sig",
|
|
366
|
+
part_name: "userdata",
|
|
367
|
+
optional: true,
|
|
368
|
+
type: "Extra"
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
nickname: "vbmeta",
|
|
372
|
+
img_name: "vbmeta.img",
|
|
373
|
+
sig_name: "vbmeta.sig",
|
|
374
|
+
part_name: "vbmeta",
|
|
375
|
+
optional: true,
|
|
376
|
+
type: "BootCritical"
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
nickname: "vbmeta_system",
|
|
380
|
+
img_name: "vbmeta_system.img",
|
|
381
|
+
sig_name: "vbmeta_system.sig",
|
|
382
|
+
part_name: "vbmeta_system",
|
|
383
|
+
optional: true,
|
|
384
|
+
type: "BootCritical"
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
nickname: "vbmeta_vendor",
|
|
388
|
+
img_name: "vbmeta_vendor.img",
|
|
389
|
+
sig_name: "vbmeta_vendor.sig",
|
|
390
|
+
part_name: "vbmeta_vendor",
|
|
391
|
+
optional: true,
|
|
392
|
+
type: "BootCritical"
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
nickname: "vendor",
|
|
396
|
+
img_name: "vendor.img",
|
|
397
|
+
sig_name: "vendor.sig",
|
|
398
|
+
part_name: "vendor",
|
|
399
|
+
optional: true,
|
|
400
|
+
type: "Normal"
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
nickname: "vendor_boot",
|
|
404
|
+
img_name: "vendor_boot.img",
|
|
405
|
+
sig_name: "vendor_boot.sig",
|
|
406
|
+
part_name: "vendor_boot",
|
|
407
|
+
optional: true,
|
|
408
|
+
type: "BootCritical"
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
nickname: "vendor_dlkm",
|
|
412
|
+
img_name: "vendor_dlkm.img",
|
|
413
|
+
sig_name: "vendor_dlkm.sig",
|
|
414
|
+
part_name: "vendor_dlkm",
|
|
415
|
+
optional: true,
|
|
416
|
+
type: "Normal"
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
nickname: "vendor_kernel_boot",
|
|
420
|
+
img_name: "vendor_kernel_boot.img",
|
|
421
|
+
sig_name: "vendor_kernel_boot.sig",
|
|
422
|
+
part_name: "vendor_kernel_boot",
|
|
423
|
+
optional: true,
|
|
424
|
+
type: "BootCritical"
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
nickname: "",
|
|
428
|
+
img_name: "vendor_other.img",
|
|
429
|
+
sig_name: "vendor.sig",
|
|
430
|
+
part_name: "vendor",
|
|
431
|
+
optional: true,
|
|
432
|
+
type: "Normal"
|
|
433
|
+
}
|
|
434
|
+
];
|
|
435
|
+
//#endregion
|
|
436
|
+
//#region src/sparse.ts
|
|
437
|
+
const FILE_MAGIC = 3978755898;
|
|
438
|
+
const MAJOR_VERSION = 1;
|
|
439
|
+
const MINOR_VERSION = 0;
|
|
440
|
+
const CHUNK_HEADER_SIZE = 12;
|
|
441
|
+
const RAW_CHUNK_SIZE = 64 * 1024 * 1024;
|
|
442
|
+
var ImageError = class extends Error {
|
|
443
|
+
constructor(message) {
|
|
444
|
+
super(message);
|
|
445
|
+
this.name = "ImageError";
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
var BlobBuilder = class {
|
|
449
|
+
blob;
|
|
450
|
+
type;
|
|
451
|
+
constructor(type = "") {
|
|
452
|
+
this.type = type;
|
|
453
|
+
this.blob = new Blob([], { type: this.type });
|
|
454
|
+
}
|
|
455
|
+
append(blob) {
|
|
456
|
+
this.blob = new Blob([this.blob, blob], { type: this.type });
|
|
457
|
+
}
|
|
458
|
+
getBlob() {
|
|
459
|
+
return this.blob;
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
/**
|
|
463
|
+
* Returns a parsed version of the sparse image file header from the given buffer.
|
|
464
|
+
*
|
|
465
|
+
* @param {ArrayBuffer} buffer - Raw file header data.
|
|
466
|
+
* @returns {SparseHeader} Object containing the header information.
|
|
467
|
+
*/
|
|
468
|
+
function parseFileHeader(buffer) {
|
|
469
|
+
const view = new DataView(buffer);
|
|
470
|
+
if (view.getUint32(0, true) !== FILE_MAGIC) return null;
|
|
471
|
+
const major = view.getUint16(4, true);
|
|
472
|
+
const minor = view.getUint16(6, true);
|
|
473
|
+
if (major !== MAJOR_VERSION || minor < MINOR_VERSION) throw new ImageError(`Unsupported sparse image version ${major}.${minor}`);
|
|
474
|
+
const fileHdrSize = view.getUint16(8, true);
|
|
475
|
+
const chunkHdrSize = view.getUint16(10, true);
|
|
476
|
+
if (fileHdrSize !== 28 || chunkHdrSize !== CHUNK_HEADER_SIZE) throw new ImageError(`Invalid file header size ${fileHdrSize}, chunk header size ${chunkHdrSize}`);
|
|
477
|
+
const blockSize = view.getUint32(12, true);
|
|
478
|
+
if (blockSize % 4 !== 0) throw new ImageError(`Block size ${blockSize} is not a multiple of 4`);
|
|
479
|
+
return {
|
|
480
|
+
blockSize,
|
|
481
|
+
blocks: view.getUint32(16, true),
|
|
482
|
+
chunks: view.getUint32(20, true),
|
|
483
|
+
crc32: view.getUint32(24, true)
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
function parseChunkHeader(buffer) {
|
|
487
|
+
const view = new DataView(buffer);
|
|
488
|
+
return {
|
|
489
|
+
type: view.getUint16(0, true),
|
|
490
|
+
blocks: view.getUint32(4, true),
|
|
491
|
+
dataBytes: view.getUint32(8, true) - CHUNK_HEADER_SIZE,
|
|
492
|
+
data: null
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
function calcChunksBlockSize(chunks) {
|
|
496
|
+
return chunks.map((chunk) => chunk.blocks).reduce((total, c) => total + c, 0);
|
|
497
|
+
}
|
|
498
|
+
function calcChunksDataSize(chunks) {
|
|
499
|
+
return chunks.map((chunk) => chunk.data.size).reduce((total, c) => total + c, 0);
|
|
500
|
+
}
|
|
501
|
+
function calcChunksSize(chunks) {
|
|
502
|
+
return 28 + CHUNK_HEADER_SIZE * chunks.length + calcChunksDataSize(chunks);
|
|
503
|
+
}
|
|
504
|
+
async function createImage(header, chunks) {
|
|
505
|
+
const blobBuilder = new BlobBuilder();
|
|
506
|
+
let buffer = /* @__PURE__ */ new ArrayBuffer(28);
|
|
507
|
+
let dataView = new DataView(buffer);
|
|
508
|
+
let arrayView = new Uint8Array(buffer);
|
|
509
|
+
dataView.setUint32(0, FILE_MAGIC, true);
|
|
510
|
+
dataView.setUint16(4, MAJOR_VERSION, true);
|
|
511
|
+
dataView.setUint16(6, MINOR_VERSION, true);
|
|
512
|
+
dataView.setUint16(8, 28, true);
|
|
513
|
+
dataView.setUint16(10, CHUNK_HEADER_SIZE, true);
|
|
514
|
+
dataView.setUint32(12, header.blockSize, true);
|
|
515
|
+
dataView.setUint32(16, header.blocks, true);
|
|
516
|
+
dataView.setUint32(20, chunks.length, true);
|
|
517
|
+
dataView.setUint32(24, 0, true);
|
|
518
|
+
blobBuilder.append(new Blob([buffer]));
|
|
519
|
+
for (const chunk of chunks) {
|
|
520
|
+
buffer = new ArrayBuffer(CHUNK_HEADER_SIZE + chunk.data.size);
|
|
521
|
+
dataView = new DataView(buffer);
|
|
522
|
+
arrayView = new Uint8Array(buffer);
|
|
523
|
+
dataView.setUint16(0, chunk.type, true);
|
|
524
|
+
dataView.setUint16(2, 0, true);
|
|
525
|
+
dataView.setUint32(4, chunk.blocks, true);
|
|
526
|
+
dataView.setUint32(8, CHUNK_HEADER_SIZE + chunk.data.size, true);
|
|
527
|
+
const chunkArrayView = new Uint8Array(await chunk.data.arrayBuffer());
|
|
528
|
+
arrayView.set(chunkArrayView, CHUNK_HEADER_SIZE);
|
|
529
|
+
blobBuilder.append(new Blob([buffer]));
|
|
530
|
+
}
|
|
531
|
+
return blobBuilder.getBlob();
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Creates a sparse image from buffer containing raw image data.
|
|
535
|
+
*
|
|
536
|
+
* @param {Blob} blob - Blob containing the raw image data.
|
|
537
|
+
* @returns {Promise<Blob>} Promise that resolves the blob containing the new sparse image.
|
|
538
|
+
*/
|
|
539
|
+
async function fromRaw(blob) {
|
|
540
|
+
const header = {
|
|
541
|
+
blockSize: 4096,
|
|
542
|
+
blocks: blob.size / 4096,
|
|
543
|
+
chunks: 1,
|
|
544
|
+
crc32: 0
|
|
545
|
+
};
|
|
546
|
+
const chunks = [];
|
|
547
|
+
while (blob.size > 0) {
|
|
548
|
+
const chunkSize = Math.min(blob.size, RAW_CHUNK_SIZE);
|
|
549
|
+
chunks.push({
|
|
550
|
+
type: 51905,
|
|
551
|
+
blocks: chunkSize / header.blockSize,
|
|
552
|
+
dataBytes: chunkSize,
|
|
553
|
+
data: blob.slice(0, chunkSize)
|
|
554
|
+
});
|
|
555
|
+
blob = blob.slice(chunkSize);
|
|
556
|
+
}
|
|
557
|
+
return createImage(header, chunks);
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Split a sparse image into smaller sparse images within the given size.
|
|
561
|
+
* This takes a Blob instead of an ArrayBuffer because it may process images
|
|
562
|
+
* larger than RAM.
|
|
563
|
+
*
|
|
564
|
+
* @param {Blob} blob - Blob containing the sparse image to split.
|
|
565
|
+
* @param {number} splitSize - Maximum size per split.
|
|
566
|
+
* @yields {Object} Data of the next split image and its output size in bytes.
|
|
567
|
+
*/
|
|
568
|
+
async function* splitBlob(blob, splitSize) {
|
|
569
|
+
console.debug(`Splitting ${blob.size}-byte sparse image into ${splitSize}-byte chunks`);
|
|
570
|
+
const safeSendValue = Math.floor(splitSize * (7 / 8));
|
|
571
|
+
if (blob.size <= splitSize) {
|
|
572
|
+
console.debug("Blob fits in 1 payload, not splitting");
|
|
573
|
+
yield {
|
|
574
|
+
data: await blob.arrayBuffer(),
|
|
575
|
+
bytes: blob.size
|
|
576
|
+
};
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const header = parseFileHeader(await blob.slice(0, 28).arrayBuffer());
|
|
580
|
+
if (header === null) throw new ImageError("Blob is not a sparse image");
|
|
581
|
+
header.crc32 = 0;
|
|
582
|
+
blob = blob.slice(28);
|
|
583
|
+
let splitChunks = [];
|
|
584
|
+
let splitDataBytes = 0;
|
|
585
|
+
for (let i = 0; i < header.chunks; i++) {
|
|
586
|
+
const originalChunk = parseChunkHeader(await blob.slice(0, CHUNK_HEADER_SIZE).arrayBuffer());
|
|
587
|
+
originalChunk.data = blob.slice(CHUNK_HEADER_SIZE, CHUNK_HEADER_SIZE + originalChunk.dataBytes);
|
|
588
|
+
blob = blob.slice(CHUNK_HEADER_SIZE + originalChunk.dataBytes);
|
|
589
|
+
const chunksToProcess = [];
|
|
590
|
+
if (originalChunk.dataBytes > safeSendValue) {
|
|
591
|
+
console.debug(`Data of chunk ${i} is bigger than the maximum allowed download size: ${originalChunk.dataBytes} > ${safeSendValue}`);
|
|
592
|
+
let originalDataBytes = originalChunk.dataBytes;
|
|
593
|
+
let originalData = originalChunk.data;
|
|
594
|
+
while (originalDataBytes > 0) {
|
|
595
|
+
const toSend = Math.min(safeSendValue, originalDataBytes);
|
|
596
|
+
chunksToProcess.push({
|
|
597
|
+
type: originalChunk.type,
|
|
598
|
+
dataBytes: toSend,
|
|
599
|
+
data: originalData.slice(0, toSend),
|
|
600
|
+
blocks: toSend / header?.blockSize
|
|
601
|
+
});
|
|
602
|
+
originalData = originalData.slice(toSend);
|
|
603
|
+
originalDataBytes -= toSend;
|
|
604
|
+
}
|
|
605
|
+
console.debug("chunksToProcess", chunksToProcess);
|
|
606
|
+
} else chunksToProcess.push(originalChunk);
|
|
607
|
+
for (const chunk of chunksToProcess) {
|
|
608
|
+
const bytesRemaining = splitSize - calcChunksSize(splitChunks);
|
|
609
|
+
console.debug(` Chunk ${i}: type ${chunk.type}, ${chunk.dataBytes} bytes / ${chunk.blocks} blocks, ${bytesRemaining} bytes remaining`);
|
|
610
|
+
if (bytesRemaining >= chunk.dataBytes) {
|
|
611
|
+
console.debug(" Space is available, adding chunk");
|
|
612
|
+
splitChunks.push(chunk);
|
|
613
|
+
splitDataBytes += chunk.blocks * header.blockSize;
|
|
614
|
+
} else {
|
|
615
|
+
const splitBlocks = calcChunksBlockSize(splitChunks);
|
|
616
|
+
splitChunks.push({
|
|
617
|
+
type: 51907,
|
|
618
|
+
blocks: header.blocks - splitBlocks,
|
|
619
|
+
data: new Blob([]),
|
|
620
|
+
dataBytes: 0
|
|
621
|
+
});
|
|
622
|
+
console.debug(`Partition is ${header.blocks} blocks, used ${splitBlocks}, padded with ${header.blocks - splitBlocks}, finishing split with ${calcChunksBlockSize(splitChunks)} blocks`);
|
|
623
|
+
const splitImage = await createImage(header, splitChunks);
|
|
624
|
+
console.debug(`Finished ${splitImage.size}-byte split with ${splitChunks.length} chunks`);
|
|
625
|
+
yield {
|
|
626
|
+
data: await splitImage.arrayBuffer(),
|
|
627
|
+
bytes: splitDataBytes
|
|
628
|
+
};
|
|
629
|
+
console.debug(`Starting new split: skipping first ${splitBlocks} blocks and adding chunk`);
|
|
630
|
+
splitChunks = [{
|
|
631
|
+
type: 51907,
|
|
632
|
+
blocks: splitBlocks,
|
|
633
|
+
data: new Blob([]),
|
|
634
|
+
dataBytes: 0
|
|
635
|
+
}, chunk];
|
|
636
|
+
splitDataBytes = chunk.dataBytes;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (splitChunks.length > 0 && (splitChunks.length > 1 || splitChunks[0]?.type !== 51907)) {
|
|
641
|
+
const splitImage = await createImage(header, splitChunks);
|
|
642
|
+
console.debug(`Finishing final ${splitImage.size}-byte split with ${splitChunks.length} chunks`);
|
|
643
|
+
yield {
|
|
644
|
+
data: await splitImage.arrayBuffer(),
|
|
645
|
+
bytes: splitDataBytes
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
async function parseBlobHeader(blob) {
|
|
650
|
+
const FILE_HEADER_SIZE = 28;
|
|
651
|
+
const blobSize = blob.size;
|
|
652
|
+
let totalBytes = blobSize;
|
|
653
|
+
let isSparse = false;
|
|
654
|
+
try {
|
|
655
|
+
const sparseHeader = parseFileHeader(await blob.slice(0, FILE_HEADER_SIZE).arrayBuffer());
|
|
656
|
+
if (sparseHeader !== null) {
|
|
657
|
+
totalBytes = sparseHeader.blocks * sparseHeader.blockSize;
|
|
658
|
+
isSparse = true;
|
|
659
|
+
}
|
|
660
|
+
} catch (error) {
|
|
661
|
+
console.debug(error);
|
|
662
|
+
}
|
|
663
|
+
return {
|
|
664
|
+
blobSize,
|
|
665
|
+
totalBytes,
|
|
666
|
+
isSparse
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
//#endregion
|
|
670
|
+
//#region src/client.ts
|
|
671
|
+
var FastbootError = class extends Error {};
|
|
672
|
+
const FastbootUSBDeviceFilter = {
|
|
673
|
+
classCode: 255,
|
|
674
|
+
subclassCode: 66,
|
|
675
|
+
protocolCode: 3
|
|
676
|
+
};
|
|
677
|
+
const MotorolaProducts = [
|
|
678
|
+
"fogo",
|
|
679
|
+
"fogos",
|
|
680
|
+
"bangkk",
|
|
681
|
+
"rhode",
|
|
682
|
+
"hawao",
|
|
683
|
+
"devon"
|
|
684
|
+
];
|
|
685
|
+
var FastbootClient = class FastbootClient {
|
|
686
|
+
fd;
|
|
687
|
+
logger;
|
|
688
|
+
var_cache;
|
|
689
|
+
reconnectUserAction;
|
|
690
|
+
constructor(usb_device, logger = window.console) {
|
|
691
|
+
this.fd = new FastbootDevice(usb_device, logger);
|
|
692
|
+
this.logger = logger;
|
|
693
|
+
this.var_cache = {};
|
|
694
|
+
this.reconnectUserAction = () => FastbootClient.requestUsbDevice();
|
|
695
|
+
}
|
|
696
|
+
async getVar(variable) {
|
|
697
|
+
return this.fd.getVar(variable);
|
|
698
|
+
}
|
|
699
|
+
async getVarCache(variable) {
|
|
700
|
+
if (this.var_cache[variable]) return this.var_cache[variable];
|
|
701
|
+
else {
|
|
702
|
+
this.var_cache[variable] = await this.getVar(variable);
|
|
703
|
+
return this.var_cache[variable];
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
async lock() {
|
|
707
|
+
await this.flashing("lock");
|
|
708
|
+
if (await this.unlocked()) throw new FastbootError("failed to lock device");
|
|
709
|
+
}
|
|
710
|
+
async unlock() {
|
|
711
|
+
await this.flashing("unlock");
|
|
712
|
+
if (await this.locked()) throw new FastbootError("failed to unlock device");
|
|
713
|
+
}
|
|
714
|
+
async reboot() {
|
|
715
|
+
this.logger.log("rebooting");
|
|
716
|
+
await this.fd.exec("reboot");
|
|
717
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
718
|
+
await this.fd.waitForReconnect();
|
|
719
|
+
}
|
|
720
|
+
async rebootBootloader() {
|
|
721
|
+
this.logger.log("rebooting into bootloader");
|
|
722
|
+
await this.fd.exec("reboot-bootloader");
|
|
723
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
724
|
+
await this.fd.waitForReconnect();
|
|
725
|
+
}
|
|
726
|
+
async rebootFastboot() {
|
|
727
|
+
this.logger.log("rebooting into fastboot");
|
|
728
|
+
await this.fd.exec("reboot-fastboot");
|
|
729
|
+
await new Promise((resolve) => setTimeout(resolve, 5e3));
|
|
730
|
+
await this.fd.waitForReconnectFastboot(this.reconnectUserAction);
|
|
731
|
+
}
|
|
732
|
+
async doFlash(partition, blob, slot = "current", applyVbmeta = false) {
|
|
733
|
+
if (partition !== "avb_custom_key" && await this.getVar(`has-slot:${partition}`) === "yes") if (slot === "current") partition += "_" + await this.currentSlot();
|
|
734
|
+
else if (slot === "other") partition += "_" + await this.otherSlot();
|
|
735
|
+
else if (slot === "a" || slot === "b") partition += "_" + slot;
|
|
736
|
+
else throw new FastbootError(`Unknown Slot: ${slot}`);
|
|
737
|
+
const { blobSize, totalBytes, isSparse } = await parseBlobHeader(blob);
|
|
738
|
+
if (await this.isUserspace() && await this.getVar(`is-logical:${partition}`) === "yes") await this.resizePartition(partition, totalBytes);
|
|
739
|
+
const max = await this.maxDownloadSize();
|
|
740
|
+
if (blobSize > max && !isSparse) {
|
|
741
|
+
this.logger.log(`${partition} image is raw, converting to sparse`);
|
|
742
|
+
blob = await fromRaw(blob);
|
|
743
|
+
}
|
|
744
|
+
this.logger.log(`Flashing ${totalBytes} bytes to ${partition} w/ max ${max} bytes per split`);
|
|
745
|
+
let splits = 0;
|
|
746
|
+
let sentBytes = 0;
|
|
747
|
+
for await (const split of splitBlob(blob, max)) {
|
|
748
|
+
await this.fd.transferData(split.data);
|
|
749
|
+
this.logger.log(`run command flash:${partition}`);
|
|
750
|
+
await this.fd.sendCommand(`flash:${partition}`);
|
|
751
|
+
sentBytes += split.bytes;
|
|
752
|
+
splits += 1;
|
|
753
|
+
this.logger.log(`${partition} #${splits}) sent ${split.bytes} bytes. ${sentBytes}/${blobSize}`);
|
|
754
|
+
}
|
|
755
|
+
this.logger.log(`Flashed ${partition} with ${splits} split(s). Bytes sent: ${sentBytes}`);
|
|
756
|
+
}
|
|
757
|
+
async resizePartition(name, totalBytes) {
|
|
758
|
+
await this.fd.sendCommand(`resize-logical-partition:${name}:0`);
|
|
759
|
+
await this.fd.sendCommand(`resize-logical-partition:${name}:${totalBytes}`);
|
|
760
|
+
}
|
|
761
|
+
async flashing(command) {
|
|
762
|
+
if (command === "unlock" && await this.unlocked() || command === "lock" && await this.locked()) return true;
|
|
763
|
+
this.logger.log(`ACTION NEEDED: flashing ${command}`);
|
|
764
|
+
await this.fd.exec(`flashing ${command}`);
|
|
765
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
766
|
+
await this.fd.waitForReconnect();
|
|
767
|
+
}
|
|
768
|
+
async fastbootInfo(entries, text, wipe = false) {
|
|
769
|
+
const lines = text.split("\n").map((x) => x.trim()).filter((l) => !(l == "" || l[0] == "#" || l.slice(0, 7) == "version"));
|
|
770
|
+
for (const line of lines) {
|
|
771
|
+
this.logger.log(`fastboot-info: ${line}`);
|
|
772
|
+
const parts = line.split(" ").map((x) => x.trim());
|
|
773
|
+
const command = parts.shift();
|
|
774
|
+
switch (command) {
|
|
775
|
+
case "flash": {
|
|
776
|
+
let slot = "current";
|
|
777
|
+
let applyVbmeta = false;
|
|
778
|
+
let partition = null;
|
|
779
|
+
let filename = null;
|
|
780
|
+
for (const arg of parts) if (arg === "--slot-other") slot = "other";
|
|
781
|
+
else if (arg === "--apply-vbmeta") applyVbmeta = true;
|
|
782
|
+
else if (partition == null) partition = arg;
|
|
783
|
+
else filename = arg;
|
|
784
|
+
if (filename === null) {
|
|
785
|
+
filename = IMAGES.find((img) => img["nickname"] === partition)?.img_name;
|
|
786
|
+
if (!filename) throw new Error(`Unknown partition: ${partition}`);
|
|
787
|
+
}
|
|
788
|
+
const entry = entries.find((e) => e.filename === filename);
|
|
789
|
+
if (!entry) throw new Error(`partition ${partition} with filename ${filename} not found in zipfile.`);
|
|
790
|
+
this.logger.log(`Extracting ${filename}`);
|
|
791
|
+
const blob = await entry.getData(new BlobWriter("application/octet-stream"));
|
|
792
|
+
this.logger.log(`flashing partition ${partition} with ${filename} from nested zip`);
|
|
793
|
+
await this.doFlash(partition, blob, slot, applyVbmeta);
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
case "reboot":
|
|
797
|
+
if (parts[0] === "fastboot") await this.rebootFastboot();
|
|
798
|
+
else await this.rebootBootloader();
|
|
799
|
+
break;
|
|
800
|
+
case "update-super":
|
|
801
|
+
await this.updateSuper(entries, wipe);
|
|
802
|
+
break;
|
|
803
|
+
case "if-wipe":
|
|
804
|
+
if (wipe && parts[0] === "erase" && parts[1]) await this.erase(parts[1]);
|
|
805
|
+
break;
|
|
806
|
+
case "erase":
|
|
807
|
+
await this.erase(parts[0]);
|
|
808
|
+
break;
|
|
809
|
+
default: throw new Error(`unknown command ${command}`);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
async updateSuper(entries, wipe) {
|
|
814
|
+
const superEmptyImage = entries.find((e) => e.filename === "super_empty.img");
|
|
815
|
+
if (!superEmptyImage) throw new FastbootError(`super_empty.img not found`);
|
|
816
|
+
let superName = "super";
|
|
817
|
+
try {
|
|
818
|
+
superName = await this.getVar("super-partition-name");
|
|
819
|
+
} catch (err) {
|
|
820
|
+
console.debug(err);
|
|
821
|
+
}
|
|
822
|
+
const buffer = await (await superEmptyImage.getData(new BlobWriter("application/octet-stream"))).arrayBuffer();
|
|
823
|
+
await this.fd.transferData(buffer);
|
|
824
|
+
await this.fd.sendCommand(`update-super:${superName}${wipe ? ":wipe" : ""}`);
|
|
825
|
+
}
|
|
826
|
+
async erase(partition) {
|
|
827
|
+
return this.fd.exec(`erase:${partition}`);
|
|
828
|
+
}
|
|
829
|
+
async setActiveOtherSlot() {
|
|
830
|
+
const otherSlot = await this.otherSlot();
|
|
831
|
+
return await this.fd.exec(`set_active:${otherSlot}`);
|
|
832
|
+
}
|
|
833
|
+
async maxDownloadSize() {
|
|
834
|
+
try {
|
|
835
|
+
const deviceMax = parseInt(await this.getVar("max-download-size"), 16);
|
|
836
|
+
return Math.min(deviceMax, 1024 * 1024 * 1024 * 1);
|
|
837
|
+
} catch (err) {
|
|
838
|
+
console.debug(err);
|
|
839
|
+
}
|
|
840
|
+
return 512 * 1024 * 1024;
|
|
841
|
+
}
|
|
842
|
+
async unlocked() {
|
|
843
|
+
if (MotorolaProducts.includes(await this.getVarCache("product"))) return await this.getVar("securestate") === "flashing_unlocked";
|
|
844
|
+
else return await this.getVar("unlocked") === "yes";
|
|
845
|
+
}
|
|
846
|
+
async locked() {
|
|
847
|
+
if (MotorolaProducts.includes(await this.getVarCache("product"))) return await this.getVar("securestate") === "flashing_locked";
|
|
848
|
+
else return await this.getVar("unlocked") === "no";
|
|
849
|
+
}
|
|
850
|
+
async currentSlot() {
|
|
851
|
+
return this.getVar("current-slot");
|
|
852
|
+
}
|
|
853
|
+
async otherSlot() {
|
|
854
|
+
const currentSlot = await this.getVar("current-slot");
|
|
855
|
+
if (currentSlot === "a") return "b";
|
|
856
|
+
else if (currentSlot === "b") return "a";
|
|
857
|
+
else throw new Error(`Unable to determine other slot, current slot: ${currentSlot}`);
|
|
858
|
+
}
|
|
859
|
+
async isUserspace() {
|
|
860
|
+
return await this.getVar("is-userspace") === "yes";
|
|
861
|
+
}
|
|
862
|
+
async getUnlockData() {
|
|
863
|
+
await this.fd.exec(`oem get_unlock_data`);
|
|
864
|
+
let data = "";
|
|
865
|
+
for (const packet of this.fd.session.packets) if ("command" in packet) continue;
|
|
866
|
+
else if (packet.status === "INFO") {
|
|
867
|
+
const message = packet.message?.replace("(bootloader)", "").trim();
|
|
868
|
+
if (message === "Unlock data:") continue;
|
|
869
|
+
else data += message;
|
|
870
|
+
} else if (packet.status === "OKAY") break;
|
|
871
|
+
else throw new Error(`packet status == ${packet.status}`);
|
|
872
|
+
return data;
|
|
873
|
+
}
|
|
874
|
+
static async create() {
|
|
875
|
+
return new FastbootClient(await this.requestUsbDevice(), window.console);
|
|
876
|
+
}
|
|
877
|
+
static async requestUsbDevice() {
|
|
878
|
+
return window.navigator.usb.requestDevice({ filters: [FastbootUSBDeviceFilter] });
|
|
879
|
+
}
|
|
880
|
+
static async findOrRequestDevice(serialNumber) {
|
|
881
|
+
for (const device of await navigator.usb.getDevices()) if (device.serialNumber === serialNumber) return device;
|
|
882
|
+
return await FastbootClient.requestUsbDevice();
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
//#endregion
|
|
886
|
+
//#region src/flasher.ts
|
|
887
|
+
const COMMAND_NAMES = new Set([
|
|
888
|
+
"update",
|
|
889
|
+
"flashall",
|
|
890
|
+
"flash",
|
|
891
|
+
"flashing",
|
|
892
|
+
"erase",
|
|
893
|
+
"format",
|
|
894
|
+
"getvar",
|
|
895
|
+
"set_active",
|
|
896
|
+
"boot",
|
|
897
|
+
"devices",
|
|
898
|
+
"continue",
|
|
899
|
+
"reboot",
|
|
900
|
+
"reboot-bootloader",
|
|
901
|
+
"sleep",
|
|
902
|
+
"oem",
|
|
903
|
+
"help"
|
|
904
|
+
]);
|
|
905
|
+
function isCommandName(word) {
|
|
906
|
+
return COMMAND_NAMES.has(word);
|
|
907
|
+
}
|
|
908
|
+
function isFileEntry(entry) {
|
|
909
|
+
return !entry.directory;
|
|
910
|
+
}
|
|
911
|
+
function requireArg(command, args, index) {
|
|
912
|
+
const value = args[index];
|
|
913
|
+
if (!value) throw new Error(`Missing argument ${index + 1} for ${command}`);
|
|
914
|
+
return value;
|
|
915
|
+
}
|
|
916
|
+
function getEntry(entries, filename) {
|
|
917
|
+
const entry = entries.find((e) => e.filename.split(/[\\/]/).pop() === filename);
|
|
918
|
+
if (entry) return entry;
|
|
919
|
+
else throw new Error(`${filename} not found in zip`);
|
|
920
|
+
}
|
|
921
|
+
function parseInstruction(text) {
|
|
922
|
+
if (text.slice(0, 8) === "fastboot") text = text.slice(8).trim();
|
|
923
|
+
let command;
|
|
924
|
+
const args = [];
|
|
925
|
+
const options = {};
|
|
926
|
+
const words = text.split(/\s+/).map((x) => x.trim()).filter((x) => x !== "");
|
|
927
|
+
for (const word of words) if (word[0] === "-") if (word === "-w" || word === "--wipe") options.wipe = true;
|
|
928
|
+
else if (word === "--set-active=other") options.setActive = "other";
|
|
929
|
+
else if (word === "--set-active=a" || word === "--set-active=b") {
|
|
930
|
+
const slot = word.slice(-1);
|
|
931
|
+
if (slot === "a" || slot === "b") options.setActive = slot;
|
|
932
|
+
} else if (word === "--slot-other") options.slot = "other";
|
|
933
|
+
else if (word.slice(0, 6) === "--slot") {
|
|
934
|
+
const slot = word.split("=")[1];
|
|
935
|
+
if (!slot) throw new Error("--slot requires a value");
|
|
936
|
+
if (![
|
|
937
|
+
"current",
|
|
938
|
+
"other",
|
|
939
|
+
"a",
|
|
940
|
+
"b"
|
|
941
|
+
].includes(slot)) throw new Error(`unknown slot: ${slot}`);
|
|
942
|
+
options.slot = slot;
|
|
943
|
+
} else if (word === "--skip-reboot") options.skipReboot = true;
|
|
944
|
+
else if (word === "--apply-vbmeta") options.applyVbmeta = true;
|
|
945
|
+
else console.warn(`Unknown option: ${word}`);
|
|
946
|
+
else if (command) args.push(word);
|
|
947
|
+
else {
|
|
948
|
+
if (!isCommandName(word)) throw new Error(`Unknown command: ${word}`);
|
|
949
|
+
command = word;
|
|
950
|
+
}
|
|
951
|
+
if (!command) throw new Error("Missing command");
|
|
952
|
+
return {
|
|
953
|
+
command,
|
|
954
|
+
args,
|
|
955
|
+
options
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
function parseInstructions(text) {
|
|
959
|
+
return text.split("\n").map((x) => x.trim()).filter((x) => x !== "").filter((x) => x[0] !== "#").map(parseInstruction);
|
|
960
|
+
}
|
|
961
|
+
var FastbootFlasher = class {
|
|
962
|
+
client;
|
|
963
|
+
reader;
|
|
964
|
+
constructor(client, blob) {
|
|
965
|
+
this.client = client;
|
|
966
|
+
this.reader = new ZipReader(new BlobReader(blob));
|
|
967
|
+
}
|
|
968
|
+
async runFlashAll() {
|
|
969
|
+
const flashAllSh = await getEntry((await this.reader.getEntries()).filter(isFileEntry), "flash-all.sh").getData(new TextWriter());
|
|
970
|
+
this.client.logger.log("flash-all.sh\n" + flashAllSh);
|
|
971
|
+
const instructions = flashAllSh.split("\n").map((x) => x.trim()).filter((x) => x.slice(0, 9) === "fastboot " || x.slice(0, 5) === "sleep").join("\n");
|
|
972
|
+
return this.run(instructions);
|
|
973
|
+
}
|
|
974
|
+
async run(instructions) {
|
|
975
|
+
const entries = (await this.reader.getEntries()).filter(isFileEntry);
|
|
976
|
+
const commands = parseInstructions(instructions);
|
|
977
|
+
for (const command of commands) {
|
|
978
|
+
this.client.logger.log(`‣ ${JSON.stringify(command)}`);
|
|
979
|
+
if (command.command === "flash") {
|
|
980
|
+
const partition = requireArg(command.command, command.args, 0);
|
|
981
|
+
const filename = requireArg(command.command, command.args, 1);
|
|
982
|
+
const slot = command.options.slot ?? "current";
|
|
983
|
+
const blob = await getEntry(entries, filename).getData(new BlobWriter("application/octet-stream"));
|
|
984
|
+
await this.client.doFlash(partition, blob, slot, Boolean(command.options.applyVbmeta));
|
|
985
|
+
} else if (command.command === "reboot-bootloader") {
|
|
986
|
+
if (command.options.setActive === "other") await this.client.setActiveOtherSlot();
|
|
987
|
+
else if (command.options.setActive === "a") await this.client.fd.exec("set_active:a");
|
|
988
|
+
else if (command.options.setActive === "b") await this.client.fd.exec("set_active:b");
|
|
989
|
+
await this.client.rebootBootloader();
|
|
990
|
+
} else if (command.command === "update") {
|
|
991
|
+
const nestedEntries = (await new ZipReader(new BlobReader(await getEntry(entries, requireArg(command.command, command.args, 0)).getData(new BlobWriter("application/zip")))).getEntries()).filter(isFileEntry);
|
|
992
|
+
const fastbootInfoFile = nestedEntries.find((e) => e.filename === "fastboot-info.txt");
|
|
993
|
+
if (!fastbootInfoFile) throw new Error("fastboot-info.txt not found in nested zip");
|
|
994
|
+
const fastbootInfoText = await fastbootInfoFile.getData(new TextWriter());
|
|
995
|
+
this.client.logger.log(`fastboot-info.txt: ${fastbootInfoText}`);
|
|
996
|
+
await this.client.fastbootInfo(nestedEntries, fastbootInfoText, Boolean(command.options.wipe));
|
|
997
|
+
} else if (command.command === "flashing") if (command.args[0] === "lock") await this.client.lock();
|
|
998
|
+
else if (command.args[0] === "unlock") await this.client.unlock();
|
|
999
|
+
else throw new Error(`Unknown command`);
|
|
1000
|
+
else if (command.command === "getvar") {
|
|
1001
|
+
const varName = requireArg(command.command, command.args, 0);
|
|
1002
|
+
const clientVar = await this.client.getVar(varName);
|
|
1003
|
+
this.client.logger.log(`getVar(${varName}) => ${clientVar}`);
|
|
1004
|
+
} else if (command.command === "erase") {
|
|
1005
|
+
const partition = requireArg(command.command, command.args, 0);
|
|
1006
|
+
await this.client.erase(partition);
|
|
1007
|
+
} else if (command.command === "sleep") {
|
|
1008
|
+
const ms = command.args[0] ? parseInt(command.args[0]) * 1e3 : 5e3;
|
|
1009
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
1010
|
+
} else if (command.command === "oem") if (command.args[0] === "fb_mode_set" || command.args[0] === "fb_mode_clear") await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1011
|
+
else throw new Error(`Fastboot oem command ${command.args[0]} not implemented`);
|
|
1012
|
+
else throw new Error(`Fastboot command ${command.command} not implemented`);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
//#endregion
|
|
1017
|
+
export { FastbootClient, FastbootDevice, FastbootFlasher };
|
|
1018
|
+
|
|
1019
|
+
//# sourceMappingURL=fastboot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fastboot.js","names":[],"sources":["../src/device.ts","../src/images.ts","../src/sparse.ts","../src/client.ts","../src/flasher.ts"],"sourcesContent":["type CommandPacket = {\n command: string\n}\n\ntype ResponsePacket = {\n status: \"OKAY\" | \"FAIL\" | \"DATA\" | \"INFO\" | \"TEXT\"\n message?: string\n dataLength?: number\n}\n\ntype FastbootSession = {\n // phase: 1 | 2 | 3 | 4 | 5\n status: null | \"OKAY\" | \"FAIL\"\n packets: (CommandPacket | ResponsePacket)[]\n}\n\ninterface Logger {\n log(message: string): void\n}\n\nexport class FastbootUsbConnectionError extends Error {\n constructor(message: string = \"Could not find device in navigator.usb.getDevices()\") {\n super(message)\n this.name = \"FastbootUsbConnectionError\"\n }\n}\n\nexport class FastbootDeviceError extends Error {\n status: string\n\n constructor(status: string, message: string) {\n super(`Bootloader replied with ${status}: ${message}`)\n this.status = status\n this.name = \"FastbootDeviceError\"\n }\n}\n\n// implements the fastboot protocol over WebUSB\nexport class FastbootDevice {\n device: USBDevice\n serialNumber: string\n in!: USBEndpoint\n out!: USBEndpoint\n session: FastbootSession\n sessions: FastbootSession[]\n logger: Logger\n\n constructor(device: USBDevice, logger: Logger = window.console) {\n if (!device.serialNumber) {\n throw new Error(\n \"Access to USBDevice#serialNumber is necessary but stored only in temporary memory.\",\n )\n }\n\n this.device = device\n this.serialNumber = device.serialNumber\n this.session = { status: null, packets: [] }\n this.sessions = []\n this.logger = logger\n this.setup()\n }\n\n // validate device and assign endpoints attributes\n setup() {\n if (this.device.configurations.length > 1) {\n console.warn(\n `device has ${this.device.configurations.length} configurations. Using the first one.`,\n )\n }\n\n // this.in = this.device.configurations[0].interfaces[0].alternate.endpoints[0]\n const endpoints = this.device.configurations[0]?.interfaces[0]?.alternate.endpoints\n\n if (endpoints && endpoints.length !== 2) {\n throw new Error(\"USB Interface must have only 2 endpoints\")\n }\n\n for (const endpoint of endpoints ?? []) {\n if (endpoint.direction === \"in\") {\n this.in = endpoint\n } else if (endpoint.direction === \"out\") {\n this.out = endpoint\n } else {\n throw new Error(`Endpoint error: ${endpoint}`)\n }\n }\n }\n\n async connect() {\n if (!this.device.opened) {\n await this.device.open()\n }\n await this.device.selectConfiguration(1)\n await this.device.claimInterface(0)\n }\n\n // After a reboot, the USBDevice object can go stale. We have to wait\n // for the device to appear in getDevices() and re-open it.\n async reconnect(): Promise<boolean> {\n const devices = await navigator.usb.getDevices()\n for (const device of devices) {\n if (device.serialNumber === this.serialNumber) {\n this.logger.log(`reconnect: Found device ${device.serialNumber}`)\n this.device = device\n this.setup()\n await this.connect()\n return true\n }\n }\n throw new FastbootUsbConnectionError()\n }\n\n // Try to reconnect with escalating backoff: an immediate attempt,\n // then retries after 3s and 30s waits. Returns true on success, or\n // false once both waits are exhausted without the device reappearing.\n // Some commands (like \"flashing lock\") will disconnect the device\n // we have to wait for it to be reconnected.\n\n private async retryReconnect(label: string): Promise<boolean> {\n try {\n this.logger.log(`${label} try reconnect()`)\n return await this.reconnect()\n } catch (e) {\n console.error(e)\n this.logger.log(`${label} wait 3 seconds`)\n await new Promise((resolve) => setTimeout(resolve, 3000))\n }\n\n try {\n return await this.reconnect()\n } catch (e) {\n if (e instanceof FastbootUsbConnectionError) {\n console.error(e)\n this.logger.log(`${label} wait 30 seconds`)\n await new Promise((resolve) => setTimeout(resolve, 30000))\n } else {\n throw e\n }\n }\n\n return false\n }\n\n // Some commands (like \"flashing lock\") will disconnect the device\n // we have to wait for it to be reconnected.\n async waitForReconnect(): Promise<boolean> {\n if (await this.retryReconnect(\"waitForReconnect\")) {\n return true\n }\n\n // try once more then wait for navigator.usb connect event\n try {\n return await this.reconnect()\n } catch (e) {\n if (e instanceof FastbootUsbConnectionError) {\n return new Promise((resolve, reject) => {\n this.logger.log(\"adding navigator.usb connect listener\")\n navigator.usb.addEventListener(\n \"connect\",\n async () => {\n try {\n await this.reconnect()\n resolve(true)\n } catch (e) {\n reject(e)\n }\n },\n { once: true },\n )\n })\n }\n }\n\n return false\n }\n\n // During the install, the device reboots into fastbootd. Fastbootd\n // mode can be considered a separate device from the perspective of\n // WebUSB and we may have to use navigator.usb.requestDevice which\n // requires user action and permission.\n async waitForReconnectFastboot(userAction: () => Promise<unknown>): Promise<boolean> {\n if (await this.retryReconnect(\"waitForReconnectFastboot\")) {\n return true\n }\n\n try {\n return await this.reconnect()\n } catch (e) {\n console.error(e)\n try {\n await userAction()\n this.logger.log(\"waitForReconnectFastboot after user action\")\n return await this.reconnect()\n } catch (e) {\n console.error(e)\n this.logger.log(\"waitForReconnectFastboot reconnect() failed after user action\")\n return false\n }\n }\n }\n\n async getPacket(): Promise<ResponsePacket> {\n this.logger.log(`receiving packet from endpoint ${this.in.endpointNumber}`)\n const inPacket = await this.device.transferIn(this.in.endpointNumber, 256)\n const inPacketText = new TextDecoder().decode(inPacket.data)\n const status = inPacketText.substring(0, 4)\n const message = inPacketText.slice(4).trim()\n switch (status) {\n case \"INFO\":\n return { status, message: `(bootloader) ${message}` }\n case \"TEXT\":\n return { status, message }\n case \"FAIL\":\n return { status, message }\n case \"OKAY\":\n return { status, message }\n case \"DATA\": {\n const dataLength = parseInt(inPacketText.slice(4, 12), 16)\n return {\n status,\n dataLength,\n message: `ready to transfer ${dataLength} bytes`,\n }\n }\n default:\n throw new Error(`invalid packet: ${inPacketText}`)\n }\n }\n\n async getPackets() {\n let response\n do {\n response = await this.getPacket()\n this.session.packets.push(response)\n this.logger.log(`[${response.status}] ${response.message}`)\n } while ([\"INFO\", \"TEXT\"].includes(response.status))\n }\n\n async sendCommand(text: string): Promise<ResponsePacket> {\n this.session.packets.push({ command: text } as CommandPacket)\n const outPacket = new TextEncoder().encode(text)\n this.logger.log(`transfering \"${text}\" to endpoint ${this.out.endpointNumber}`)\n await this.device.transferOut(this.out.endpointNumber, outPacket)\n\n await this.getPackets()\n\n if (this.lastPacket && \"status\" in this.lastPacket && this.lastPacket.status === \"FAIL\") {\n this.session.status = \"FAIL\"\n throw new FastbootDeviceError(this.lastPacket.status, this.lastPacket.message ?? \"\")\n } else {\n return this.lastPacket as ResponsePacket // TODO: Fix bad assertion\n }\n }\n\n async exec(command: string): Promise<ResponsePacket> {\n if (this.isActive) {\n throw new Error(\"fastboot device is busy\")\n } else if (!this.device.opened) {\n await this.connect()\n }\n\n this.sessions.push(this.session)\n this.session = { status: null, packets: [] }\n\n return this.sendCommand(command)\n }\n\n async getVar(variable: string): Promise<string> {\n await this.exec(`getvar:${variable}`)\n\n if (this.lastPacket && \"message\" in this.lastPacket) {\n return this.lastPacket.message ?? \"\"\n }\n return \"\"\n }\n\n get lastPacket() {\n if (this.session.packets.length === 0) {\n return null\n } else {\n return this.session.packets[this.session.packets.length - 1]\n }\n }\n\n get isActive() {\n if (this.session.packets.length === 0) {\n return false\n } else {\n return (\n this.lastPacket &&\n \"status\" in this.lastPacket &&\n ![\"FAIL\", \"OKAY\"].includes(this.lastPacket.status)\n )\n }\n }\n\n // send buffer to phone\n // download:00001234 -> \"DATA\" -> transferOut -> \"OKAY\"\n async transferData(buffer: ArrayBuffer) {\n // Bootloader requires an 8-digit hex number\n const xferHex = buffer.byteLength.toString(16).padStart(8, \"0\")\n if (xferHex.length !== 8) {\n throw new FastbootDeviceError(\n \"FAIL\",\n `Transfer size overflow: ${xferHex} is more than 8 digits`,\n )\n }\n\n this.logger.log(`Sending command download:${xferHex}.`)\n const response = await this.sendCommand(`download:${xferHex}`)\n\n if (response.status !== \"DATA\") {\n throw new FastbootDeviceError(\n \"FAIL\",\n `response to download:${xferHex} is ${response.status}. Expected DATA.`,\n )\n } else if (response.dataLength !== buffer.byteLength) {\n throw new FastbootDeviceError(\n \"FAIL\",\n `Bootloader wants ${response.dataLength} bytes, requested to send ${buffer.byteLength} bytes`,\n )\n }\n\n const BULK_TRANSFER_SIZE = 16384\n this.logger.log(`Sending payload: ${buffer.byteLength} bytes`)\n let i = 0\n let remainingBytes = buffer.byteLength\n while (remainingBytes > 0) {\n const chunk = buffer.slice(i * BULK_TRANSFER_SIZE, (i + 1) * BULK_TRANSFER_SIZE)\n if (i % 1000 === 0) {\n this.logger.log(\n `Sending ${chunk.byteLength} bytes to endpoint, ${remainingBytes} remaining, i=${i}`,\n )\n }\n await this.device.transferOut(this.out.endpointNumber, chunk)\n remainingBytes -= chunk.byteLength\n i += 1\n }\n this.logger.log(\"Payload sent, waiting for response...\")\n await this.getPackets()\n }\n}\n","export const IMAGES = [\n {\n nickname: \"boot\",\n img_name: \"boot.img\",\n sig_name: \"boot.sig\",\n part_name: \"boot\",\n optional: false,\n type: \"BootCritical\",\n },\n {\n nickname: \"bootloader\",\n img_name: \"bootloader.img\",\n sig_name: \"\",\n part_name: \"bootloader\",\n optional: true,\n type: \"Extra\",\n },\n {\n nickname: \"init_boot\",\n img_name: \"init_boot.img\",\n sig_name: \"init_boot.sig\",\n part_name: \"init_boot\",\n optional: true,\n type: \"BootCritical\",\n },\n {\n nickname: \"\",\n img_name: \"boot_other.img\",\n sig_name: \"boot.sig\",\n part_name: \"boot\",\n optional: true,\n type: \"Normal\",\n },\n {\n nickname: \"cache\",\n img_name: \"cache.img\",\n sig_name: \"cache.sig\",\n part_name: \"cache\",\n optional: true,\n type: \"Extra\",\n },\n {\n nickname: \"dtbo\",\n img_name: \"dtbo.img\",\n sig_name: \"dtbo.sig\",\n part_name: \"dtbo\",\n optional: true,\n type: \"BootCritical\",\n },\n {\n nickname: \"dts\",\n img_name: \"dt.img\",\n sig_name: \"dt.sig\",\n part_name: \"dts\",\n optional: true,\n type: \"BootCritical\",\n },\n {\n nickname: \"odm\",\n img_name: \"odm.img\",\n sig_name: \"odm.sig\",\n part_name: \"odm\",\n optional: true,\n type: \"Normal\",\n },\n {\n nickname: \"odm_dlkm\",\n img_name: \"odm_dlkm.img\",\n sig_name: \"odm_dlkm.sig\",\n part_name: \"odm_dlkm\",\n optional: true,\n type: \"Normal\",\n },\n {\n nickname: \"product\",\n img_name: \"product.img\",\n sig_name: \"product.sig\",\n part_name: \"product\",\n optional: true,\n type: \"Normal\",\n },\n {\n nickname: \"pvmfw\",\n img_name: \"pvmfw.img\",\n sig_name: \"pvmfw.sig\",\n part_name: \"pvmfw\",\n optional: true,\n type: \"BootCritical\",\n },\n {\n nickname: \"radio\",\n img_name: \"radio.img\",\n sig_name: \"\",\n part_name: \"radio\",\n optional: true,\n type: \"Extra\",\n },\n {\n nickname: \"recovery\",\n img_name: \"recovery.img\",\n sig_name: \"recovery.sig\",\n part_name: \"recovery\",\n optional: true,\n type: \"BootCritical\",\n },\n {\n nickname: \"super\",\n img_name: \"super.img\",\n sig_name: \"super.sig\",\n part_name: \"super\",\n optional: true,\n type: \"Extra\",\n },\n {\n nickname: \"system\",\n img_name: \"system.img\",\n sig_name: \"system.sig\",\n part_name: \"system\",\n optional: false,\n type: \"Normal\",\n },\n {\n nickname: \"system_dlkm\",\n img_name: \"system_dlkm.img\",\n sig_name: \"system_dlkm.sig\",\n part_name: \"system_dlkm\",\n optional: true,\n type: \"Normal\",\n },\n {\n nickname: \"system_ext\",\n img_name: \"system_ext.img\",\n sig_name: \"system_ext.sig\",\n part_name: \"system_ext\",\n optional: true,\n type: \"Normal\",\n },\n {\n nickname: \"\",\n img_name: \"system_other.img\",\n sig_name: \"system.sig\",\n part_name: \"system\",\n optional: true,\n type: \"Normal\",\n },\n {\n nickname: \"userdata\",\n img_name: \"userdata.img\",\n sig_name: \"userdata.sig\",\n part_name: \"userdata\",\n optional: true,\n type: \"Extra\",\n },\n {\n nickname: \"vbmeta\",\n img_name: \"vbmeta.img\",\n sig_name: \"vbmeta.sig\",\n part_name: \"vbmeta\",\n optional: true,\n type: \"BootCritical\",\n },\n {\n nickname: \"vbmeta_system\",\n img_name: \"vbmeta_system.img\",\n sig_name: \"vbmeta_system.sig\",\n part_name: \"vbmeta_system\",\n optional: true,\n type: \"BootCritical\",\n },\n {\n nickname: \"vbmeta_vendor\",\n img_name: \"vbmeta_vendor.img\",\n sig_name: \"vbmeta_vendor.sig\",\n part_name: \"vbmeta_vendor\",\n optional: true,\n type: \"BootCritical\",\n },\n {\n nickname: \"vendor\",\n img_name: \"vendor.img\",\n sig_name: \"vendor.sig\",\n part_name: \"vendor\",\n optional: true,\n type: \"Normal\",\n },\n {\n nickname: \"vendor_boot\",\n img_name: \"vendor_boot.img\",\n sig_name: \"vendor_boot.sig\",\n part_name: \"vendor_boot\",\n optional: true,\n type: \"BootCritical\",\n },\n {\n nickname: \"vendor_dlkm\",\n img_name: \"vendor_dlkm.img\",\n sig_name: \"vendor_dlkm.sig\",\n part_name: \"vendor_dlkm\",\n optional: true,\n type: \"Normal\",\n },\n {\n nickname: \"vendor_kernel_boot\",\n img_name: \"vendor_kernel_boot.img\",\n sig_name: \"vendor_kernel_boot.sig\",\n part_name: \"vendor_kernel_boot\",\n optional: true,\n type: \"BootCritical\",\n },\n {\n nickname: \"\",\n img_name: \"vendor_other.img\",\n sig_name: \"vendor.sig\",\n part_name: \"vendor\",\n optional: true,\n type: \"Normal\",\n },\n]\n","// The MIT License (MIT)\n\n// Copyright (c) 2021 Danny Lin <danny@kdrag0n.dev>\n// Copyright (c) 2025 ziggy <ziggy@calyxinstitute.org>\n\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\nconst FILE_MAGIC = 0xed26ff3a\n\nconst MAJOR_VERSION = 1\nconst MINOR_VERSION = 0\nexport const FILE_HEADER_SIZE = 28\nconst CHUNK_HEADER_SIZE = 12\n\n// AOSP libsparse uses 64 MiB chunks\nconst RAW_CHUNK_SIZE = 64 * 1024 * 1024\n\nexport class ImageError extends Error {\n constructor(message: string) {\n super(message)\n this.name = \"ImageError\"\n }\n}\n\nexport interface SparseSplit {\n data: ArrayBuffer\n bytes: number\n}\n\nexport enum ChunkType {\n Raw = 0xcac1,\n Fill = 0xcac2,\n Skip = 0xcac3,\n Crc32 = 0xcac4,\n}\n\nexport interface SparseHeader {\n blockSize: number\n blocks: number\n chunks: number\n crc32: number\n}\n\nexport interface SparseChunk {\n type: ChunkType\n /* 2: reserved, 16 bits */\n blocks: number\n dataBytes: number\n data: Blob | null // to be populated by consumer\n}\n\nexport interface BlobHeader {\n blobSize: number\n totalBytes: number\n isSparse: boolean\n}\n\nclass BlobBuilder {\n private blob: Blob\n private type: string\n\n constructor(type: string = \"\") {\n this.type = type\n this.blob = new Blob([], { type: this.type })\n }\n\n append(blob: Blob) {\n this.blob = new Blob([this.blob, blob], { type: this.type })\n }\n\n getBlob(): Blob {\n return this.blob\n }\n}\n\n/**\n * Returns a parsed version of the sparse image file header from the given buffer.\n *\n * @param {ArrayBuffer} buffer - Raw file header data.\n * @returns {SparseHeader} Object containing the header information.\n */\nexport function parseFileHeader(buffer: ArrayBuffer): SparseHeader | null {\n const view = new DataView(buffer)\n\n const magic = view.getUint32(0, true)\n if (magic !== FILE_MAGIC) {\n return null\n }\n\n // v1.0+\n const major = view.getUint16(4, true)\n const minor = view.getUint16(6, true)\n if (major !== MAJOR_VERSION || minor < MINOR_VERSION) {\n throw new ImageError(`Unsupported sparse image version ${major}.${minor}`)\n }\n\n const fileHdrSize = view.getUint16(8, true)\n const chunkHdrSize = view.getUint16(10, true)\n if (fileHdrSize !== FILE_HEADER_SIZE || chunkHdrSize !== CHUNK_HEADER_SIZE) {\n throw new ImageError(\n `Invalid file header size ${fileHdrSize}, chunk header size ${chunkHdrSize}`,\n )\n }\n\n const blockSize = view.getUint32(12, true)\n if (blockSize % 4 !== 0) {\n throw new ImageError(`Block size ${blockSize} is not a multiple of 4`)\n }\n\n return {\n blockSize: blockSize,\n blocks: view.getUint32(16, true),\n chunks: view.getUint32(20, true),\n crc32: view.getUint32(24, true),\n }\n}\n\nfunction parseChunkHeader(buffer: ArrayBuffer): SparseChunk {\n const view = new DataView(buffer)\n\n // This isn't the same as what createImage takes.\n // Further processing needs to be done on the chunks.\n return {\n type: view.getUint16(0, true),\n /* 2: reserved, 16 bits */\n blocks: view.getUint32(4, true),\n dataBytes: view.getUint32(8, true) - CHUNK_HEADER_SIZE,\n data: null, // to be populated by consumer\n }\n}\n\nfunction calcChunksBlockSize(chunks: Array<SparseChunk>) {\n return chunks.map((chunk) => chunk.blocks).reduce((total, c) => total + c, 0)\n}\n\nfunction calcChunksDataSize(chunks: Array<SparseChunk>) {\n return chunks.map((chunk) => chunk.data!.size).reduce((total, c) => total + c, 0)\n}\n\nfunction calcChunksSize(chunks: Array<SparseChunk>) {\n // 28-byte file header, 12-byte chunk headers\n const overhead = FILE_HEADER_SIZE + CHUNK_HEADER_SIZE * chunks.length\n return overhead + calcChunksDataSize(chunks)\n}\n\nasync function createImage(header: SparseHeader, chunks: Array<SparseChunk>): Promise<Blob> {\n const blobBuilder = new BlobBuilder()\n\n let buffer = new ArrayBuffer(FILE_HEADER_SIZE)\n let dataView = new DataView(buffer)\n let arrayView = new Uint8Array(buffer)\n\n dataView.setUint32(0, FILE_MAGIC, true)\n // v1.0\n dataView.setUint16(4, MAJOR_VERSION, true)\n dataView.setUint16(6, MINOR_VERSION, true)\n dataView.setUint16(8, FILE_HEADER_SIZE, true)\n dataView.setUint16(10, CHUNK_HEADER_SIZE, true)\n\n // Match input parameters\n dataView.setUint32(12, header.blockSize, true)\n dataView.setUint32(16, header.blocks, true)\n dataView.setUint32(20, chunks.length, true)\n\n // We don't care about the CRC. AOSP docs specify that this should be a CRC32,\n // but AOSP libsparse always sets 0 and puts the CRC in a final undocumented\n // 0xCAC4 chunk instead.\n dataView.setUint32(24, 0, true)\n\n blobBuilder.append(new Blob([buffer]))\n for (const chunk of chunks) {\n buffer = new ArrayBuffer(CHUNK_HEADER_SIZE + chunk.data!.size)\n dataView = new DataView(buffer)\n arrayView = new Uint8Array(buffer)\n\n dataView.setUint16(0, chunk.type, true)\n dataView.setUint16(2, 0, true) // reserved\n dataView.setUint32(4, chunk.blocks, true)\n dataView.setUint32(8, CHUNK_HEADER_SIZE + chunk.data!.size, true)\n\n const chunkArrayView = new Uint8Array(await chunk.data!.arrayBuffer())\n arrayView.set(chunkArrayView, CHUNK_HEADER_SIZE)\n blobBuilder.append(new Blob([buffer]))\n }\n\n return blobBuilder.getBlob()\n}\n\n/**\n * Creates a sparse image from buffer containing raw image data.\n *\n * @param {Blob} blob - Blob containing the raw image data.\n * @returns {Promise<Blob>} Promise that resolves the blob containing the new sparse image.\n */\nexport async function fromRaw(blob: Blob): Promise<Blob> {\n const header = {\n blockSize: 4096,\n blocks: blob.size / 4096,\n chunks: 1,\n crc32: 0,\n }\n\n const chunks: SparseChunk[] = []\n while (blob.size > 0) {\n const chunkSize = Math.min(blob.size, RAW_CHUNK_SIZE)\n chunks.push({\n type: ChunkType.Raw,\n blocks: chunkSize / header.blockSize,\n dataBytes: chunkSize,\n data: blob.slice(0, chunkSize),\n })\n blob = blob.slice(chunkSize)\n }\n\n return createImage(header, chunks)\n}\n\n/**\n * Split a sparse image into smaller sparse images within the given size.\n * This takes a Blob instead of an ArrayBuffer because it may process images\n * larger than RAM.\n *\n * @param {Blob} blob - Blob containing the sparse image to split.\n * @param {number} splitSize - Maximum size per split.\n * @yields {Object} Data of the next split image and its output size in bytes.\n */\nexport async function* splitBlob(blob: Blob, splitSize: number) {\n console.debug(`Splitting ${blob.size}-byte sparse image into ${splitSize}-byte chunks`)\n\n // 7/8 is a safe value for the split size, to account for extra overhead\n // AOSP source code does the same\n const safeSendValue = Math.floor(splitSize * (7 / 8))\n\n // Short-circuit if splitting isn't required\n if (blob.size <= splitSize) {\n console.debug(\"Blob fits in 1 payload, not splitting\")\n yield {\n data: await blob.arrayBuffer(),\n bytes: blob.size,\n } as SparseSplit\n return\n }\n\n const headerData = await blob.slice(0, FILE_HEADER_SIZE).arrayBuffer()\n\n const header = parseFileHeader(headerData)\n if (header === null) {\n throw new ImageError(\"Blob is not a sparse image\")\n }\n\n // Remove CRC32 (if present), otherwise splitting will invalidate it\n header.crc32 = 0\n blob = blob.slice(FILE_HEADER_SIZE)\n\n let splitChunks: Array<SparseChunk> = []\n let splitDataBytes = 0\n for (let i = 0; i < header.chunks; i++) {\n const chunkHeaderData = await blob.slice(0, CHUNK_HEADER_SIZE).arrayBuffer()\n const originalChunk = parseChunkHeader(chunkHeaderData)\n originalChunk.data = blob.slice(CHUNK_HEADER_SIZE, CHUNK_HEADER_SIZE + originalChunk.dataBytes)\n blob = blob.slice(CHUNK_HEADER_SIZE + originalChunk.dataBytes)\n\n const chunksToProcess: SparseChunk[] = []\n\n // take into account cases where the chunk data is bigger than the maximum allowed download size\n if (originalChunk.dataBytes > safeSendValue) {\n console.debug(\n `Data of chunk ${i} is bigger than the maximum allowed download size: ${originalChunk.dataBytes} > ${safeSendValue}`,\n )\n\n // we should now split this chunk into multiple chunks that fit\n let originalDataBytes = originalChunk.dataBytes\n let originalData = originalChunk.data\n\n while (originalDataBytes > 0) {\n const toSend = Math.min(safeSendValue, originalDataBytes)\n\n chunksToProcess.push({\n type: originalChunk.type,\n dataBytes: toSend,\n data: originalData.slice(0, toSend),\n blocks: toSend / header?.blockSize,\n })\n\n originalData = originalData.slice(toSend)\n originalDataBytes -= toSend\n }\n\n console.debug(\"chunksToProcess\", chunksToProcess)\n } else {\n chunksToProcess.push(originalChunk)\n }\n\n for (const chunk of chunksToProcess) {\n const bytesRemaining = splitSize - calcChunksSize(splitChunks)\n console.debug(\n ` Chunk ${i}: type ${chunk.type}, ${chunk.dataBytes} bytes / ${chunk.blocks} blocks, ${bytesRemaining} bytes remaining`,\n )\n\n if (bytesRemaining >= chunk.dataBytes) {\n // Read the chunk and add it\n console.debug(\" Space is available, adding chunk\")\n splitChunks.push(chunk)\n // Track amount of data written on the output device, in bytes\n splitDataBytes += chunk.blocks * header.blockSize\n } else {\n // Out of space, finish this split\n // Blocks need to be calculated from chunk headers instead of going by size\n // because FILL and SKIP chunks cover more blocks than the data they contain.\n const splitBlocks = calcChunksBlockSize(splitChunks)\n splitChunks.push({\n type: ChunkType.Skip,\n blocks: header.blocks - splitBlocks,\n data: new Blob([]),\n dataBytes: 0,\n })\n console.debug(\n `Partition is ${header.blocks} blocks, used ${splitBlocks}, padded with ${\n header.blocks - splitBlocks\n }, finishing split with ${calcChunksBlockSize(splitChunks)} blocks`,\n )\n const splitImage = await createImage(header, splitChunks)\n console.debug(`Finished ${splitImage.size}-byte split with ${splitChunks.length} chunks`)\n yield {\n data: await splitImage.arrayBuffer(),\n bytes: splitDataBytes,\n } as SparseSplit\n\n // Start a new split. Every split is considered a full image by the\n // bootloader, so we need to skip the *total* written blocks.\n console.debug(`Starting new split: skipping first ${splitBlocks} blocks and adding chunk`)\n splitChunks = [\n {\n type: ChunkType.Skip,\n blocks: splitBlocks,\n data: new Blob([]),\n dataBytes: 0,\n },\n chunk,\n ]\n\n splitDataBytes = chunk.dataBytes\n }\n }\n }\n\n // Finish the final split if necessary\n if (\n splitChunks.length > 0 &&\n (splitChunks.length > 1 || splitChunks[0]?.type !== ChunkType.Skip)\n ) {\n const splitImage = await createImage(header, splitChunks)\n console.debug(`Finishing final ${splitImage.size}-byte split with ${splitChunks.length} chunks`)\n yield {\n data: await splitImage.arrayBuffer(),\n bytes: splitDataBytes,\n } as SparseSplit\n }\n}\n\nexport async function parseBlobHeader(blob: Blob): Promise<BlobHeader> {\n const FILE_HEADER_SIZE = 28\n const blobSize = blob.size\n let totalBytes = blobSize\n let isSparse = false\n\n try {\n const fileHeader = await blob.slice(0, FILE_HEADER_SIZE).arrayBuffer()\n const sparseHeader = parseFileHeader(fileHeader)\n if (sparseHeader !== null) {\n totalBytes = sparseHeader.blocks * sparseHeader.blockSize\n isSparse = true\n }\n } catch (error) {\n console.debug(error)\n // ImageError = invalid, so keep blob.size\n }\n return { blobSize, totalBytes, isSparse }\n}\n","import { BlobWriter, type FileEntry } from \"@zip.js/zip.js\"\nimport { IMAGES } from \"./images.js\"\nimport { parseBlobHeader, splitBlob, fromRaw } from \"./sparse.js\"\nimport { FastbootDevice } from \"./device.js\"\n\nexport class FastbootError extends Error {}\n\nconst FastbootUSBDeviceFilter = {\n classCode: 0xff,\n subclassCode: 0x42,\n protocolCode: 0x03,\n}\n\nconst MotorolaProducts = [\"fogo\", \"fogos\", \"bangkk\", \"rhode\", \"hawao\", \"devon\"]\n\ninterface Logger {\n log(message: string): void\n}\n\ninterface KeyValueDict {\n [key: string]: string\n}\n\n// higher level API to interact with fastboot device\n// translates CLI commands to\nexport class FastbootClient {\n fd: FastbootDevice\n logger: Logger\n var_cache: KeyValueDict\n reconnectUserAction: () => Promise<unknown>\n\n constructor(usb_device: USBDevice, logger: Logger = window.console) {\n this.fd = new FastbootDevice(usb_device, logger)\n this.logger = logger\n this.var_cache = {} as KeyValueDict\n this.reconnectUserAction = () => FastbootClient.requestUsbDevice()\n }\n\n async getVar(variable: string) {\n return this.fd.getVar(variable)\n }\n\n async getVarCache(variable: string) {\n if (this.var_cache[variable]) {\n return this.var_cache[variable]\n } else {\n this.var_cache[variable] = await this.getVar(variable)\n return this.var_cache[variable]\n }\n }\n\n async lock() {\n await this.flashing(\"lock\")\n if (await this.unlocked()) {\n throw new FastbootError(\"failed to lock device\")\n }\n }\n\n async unlock() {\n await this.flashing(\"unlock\")\n if (await this.locked()) {\n throw new FastbootError(\"failed to unlock device\")\n }\n }\n\n async reboot() {\n this.logger.log(\"rebooting\")\n await this.fd.exec(\"reboot\")\n await new Promise((resolve) => setTimeout(resolve, 1000))\n await this.fd.waitForReconnect()\n }\n\n async rebootBootloader() {\n this.logger.log(\"rebooting into bootloader\")\n await this.fd.exec(\"reboot-bootloader\")\n await new Promise((resolve) => setTimeout(resolve, 1000))\n await this.fd.waitForReconnect()\n }\n\n async rebootFastboot() {\n this.logger.log(\"rebooting into fastboot\")\n await this.fd.exec(\"reboot-fastboot\")\n await new Promise((resolve) => setTimeout(resolve, 5000))\n await this.fd.waitForReconnectFastboot(this.reconnectUserAction)\n }\n\n async doFlash(\n partition: string,\n blob: Blob,\n slot: \"current\" | \"other\" | \"a\" | \"b\" = \"current\",\n applyVbmeta: boolean = false, // TODO: Implement flashing vbmeta\n ) {\n // add _a or _b\n // !(await this.isUserspace()) ?\n if (partition !== \"avb_custom_key\" && (await this.getVar(`has-slot:${partition}`)) === \"yes\") {\n if (slot === \"current\") {\n partition += \"_\" + (await this.currentSlot())\n } else if (slot === \"other\") {\n partition += \"_\" + (await this.otherSlot())\n } else if (slot === \"a\" || slot === \"b\") {\n partition += \"_\" + slot\n } else {\n throw new FastbootError(`Unknown Slot: ${slot}`)\n }\n }\n\n const { blobSize, totalBytes, isSparse } = await parseBlobHeader(blob)\n\n // should_flash_in_userspace() ?\n // Logical partitions need to be resized before flashing because\n // they're sized perfectly to the payload.\n if ((await this.isUserspace()) && (await this.getVar(`is-logical:${partition}`)) === \"yes\") {\n await this.resizePartition(partition, totalBytes)\n }\n\n const max = await this.maxDownloadSize()\n\n if (blobSize > max && !isSparse) {\n this.logger.log(`${partition} image is raw, converting to sparse`)\n blob = await fromRaw(blob)\n }\n\n this.logger.log(`Flashing ${totalBytes} bytes to ${partition} w/ max ${max} bytes per split`)\n\n let splits = 0\n let sentBytes = 0\n for await (const split of splitBlob(blob, max)) {\n await this.fd.transferData(split.data)\n this.logger.log(`run command flash:${partition}`)\n await this.fd.sendCommand(`flash:${partition}`)\n sentBytes += split.bytes\n splits += 1\n this.logger.log(\n `${partition} #${splits}) sent ${split.bytes} bytes. ${sentBytes}/${blobSize}`,\n )\n }\n this.logger.log(`Flashed ${partition} with ${splits} split(s). Bytes sent: ${sentBytes}`)\n }\n\n // fb->ResizePartition\n async resizePartition(name: string, totalBytes: number) {\n // As per AOSP fastboot, we reset the partition to 0 bytes first\n // to optimize extent allocation before setting the actual size.\n await this.fd.sendCommand(`resize-logical-partition:${name}:0`)\n await this.fd.sendCommand(`resize-logical-partition:${name}:${totalBytes}`)\n }\n\n async flashing(command: \"unlock\" | \"lock\") {\n if (\n (command === \"unlock\" && (await this.unlocked())) ||\n (command === \"lock\" && (await this.locked()))\n ) {\n return true\n }\n this.logger.log(`ACTION NEEDED: flashing ${command}`)\n await this.fd.exec(`flashing ${command}`)\n await new Promise((resolve) => setTimeout(resolve, 1000))\n await this.fd.waitForReconnect()\n }\n\n // run text, typically the contents of fastboot-info.txt\n async fastbootInfo(entries: FileEntry[], text: string, wipe: boolean = false) {\n const lines = text\n .split(\"\\n\")\n .map((x) => x.trim())\n .filter((l) => !(l == \"\" || l[0] == \"#\" || l.slice(0, 7) == \"version\"))\n\n for (const line of lines) {\n this.logger.log(`fastboot-info: ${line}`)\n const parts = line.split(\" \").map((x) => x.trim())\n const command = parts.shift()\n\n switch (command) {\n case \"flash\": {\n let slot = \"current\"\n let applyVbmeta = false\n let partition = null\n let filename = null\n\n for (const arg of parts) {\n if (arg === \"--slot-other\") {\n slot = \"other\"\n } else if (arg === \"--apply-vbmeta\") {\n applyVbmeta = true\n } else if (partition == null) {\n partition = arg\n } else {\n filename = arg\n }\n }\n\n if (filename === null) {\n filename = IMAGES.find((img) => img[\"nickname\"] === partition)?.img_name\n if (!filename) {\n throw new Error(`Unknown partition: ${partition}`)\n }\n }\n\n const entry = entries.find((e) => e.filename === filename)\n if (!entry) {\n throw new Error(\n `partition ${partition} with filename ${filename} not found in zipfile.`,\n )\n }\n this.logger.log(`Extracting ${filename}`)\n const blob = await entry.getData(new BlobWriter(\"application/octet-stream\"))\n this.logger.log(`flashing partition ${partition} with ${filename} from nested zip`)\n await this.doFlash(\n partition!, // TODO: Assert this better\n blob,\n slot as \"current\" | \"b\" | \"a\" | \"other\" | undefined, // TODO: Assert this better\n applyVbmeta,\n )\n break\n }\n case \"reboot\":\n if (parts[0] === \"fastboot\") {\n await this.rebootFastboot()\n } else {\n await this.rebootBootloader()\n }\n break\n case \"update-super\": {\n await this.updateSuper(entries, wipe)\n break\n }\n case \"if-wipe\":\n if (wipe && parts[0] === \"erase\" && parts[1]) {\n await this.erase(parts[1])\n }\n break\n case \"erase\":\n await this.erase(parts[0]!) // TODO: Should not assume parts to at least have 1 ele\n break\n default:\n throw new Error(`unknown command ${command}`)\n }\n }\n }\n\n async updateSuper(entries: FileEntry[], wipe: boolean) {\n const superEmptyImage = entries.find((e) => e.filename === \"super_empty.img\")\n if (!superEmptyImage) {\n throw new FastbootError(`super_empty.img not found`)\n }\n\n let superName = \"super\"\n try {\n superName = await this.getVar(\"super-partition-name\")\n } catch (err) {\n console.debug(err)\n }\n\n // fastboot-info does this\n // await this.rebootFastboot()\n\n const blob = await superEmptyImage.getData(new BlobWriter(\"application/octet-stream\"))\n const buffer = await blob.arrayBuffer()\n\n await this.fd.transferData(buffer)\n await this.fd.sendCommand(`update-super:${superName}${wipe ? \":wipe\" : \"\"}`)\n }\n\n async erase(partition: string) {\n return this.fd.exec(`erase:${partition}`)\n }\n\n async setActiveOtherSlot() {\n const otherSlot = await this.otherSlot()\n return await this.fd.exec(`set_active:${otherSlot}`)\n }\n\n async maxDownloadSize() {\n try {\n const deviceMax = parseInt(await this.getVar(\"max-download-size\"), 16)\n const upperLimit = 1024 * 1024 * 1024 * 1 // 1 GiB\n return Math.min(deviceMax, upperLimit)\n } catch (err) {\n console.debug(err)\n }\n // FAIL or empty variable means no max, set a reasonable limit to conserve memory\n return 512 * 1024 * 1024 // 512 MiB\n }\n\n async unlocked() {\n if (MotorolaProducts.includes(await this.getVarCache(\"product\"))) {\n return (await this.getVar(\"securestate\")) === \"flashing_unlocked\"\n } else {\n return (await this.getVar(\"unlocked\")) === \"yes\"\n }\n }\n\n async locked() {\n if (MotorolaProducts.includes(await this.getVarCache(\"product\"))) {\n return (await this.getVar(\"securestate\")) === \"flashing_locked\"\n } else {\n return (await this.getVar(\"unlocked\")) === \"no\"\n }\n }\n\n async currentSlot() {\n return this.getVar(\"current-slot\")\n }\n\n async otherSlot() {\n const currentSlot = await this.getVar(\"current-slot\")\n if (currentSlot === \"a\") {\n return \"b\"\n } else if (currentSlot === \"b\") {\n return \"a\"\n } else {\n throw new Error(`Unable to determine other slot, current slot: ${currentSlot}`)\n }\n }\n\n async isUserspace() {\n return (await this.getVar(\"is-userspace\")) === \"yes\"\n }\n\n // tested on bangkk only\n async getUnlockData() {\n await this.fd.exec(`oem get_unlock_data`)\n\n let data = \"\"\n\n for (const packet of this.fd.session.packets) {\n if (\"command\" in packet) {\n continue\n } else if (packet.status === \"INFO\") {\n const message = packet.message?.replace(\"(bootloader)\", \"\").trim()\n if (message === \"Unlock data:\") {\n continue\n } else {\n data += message\n }\n } else if (packet.status === \"OKAY\") {\n break\n } else {\n throw new Error(`packet status == ${packet.status}`)\n }\n }\n\n return data\n }\n\n static async create() {\n return new FastbootClient(await this.requestUsbDevice(), window.console)\n }\n\n static async requestUsbDevice(): Promise<USBDevice> {\n return window.navigator.usb.requestDevice({\n filters: [FastbootUSBDeviceFilter],\n })\n }\n\n static async findOrRequestDevice(serialNumber: string): Promise<USBDevice> {\n for (const device of await navigator.usb.getDevices()) {\n if (device.serialNumber === serialNumber) {\n return device\n }\n }\n return await FastbootClient.requestUsbDevice()\n }\n}\n","import { ZipReader, BlobReader, BlobWriter, TextWriter, type FileEntry } from \"@zip.js/zip.js\"\nimport type { FastbootClient } from \"./client.js\"\n\ntype CommandName =\n | \"update\"\n | \"flashall\"\n | \"flash\"\n | \"flashing\"\n | \"erase\"\n | \"format\"\n | \"getvar\"\n | \"set_active\"\n | \"boot\"\n | \"devices\"\n | \"continue\"\n | \"reboot\"\n | \"reboot-bootloader\"\n | \"sleep\"\n | \"oem\"\n | \"help\"\n\ntype SlotName = \"current\" | \"other\" | \"a\" | \"b\"\n\ntype InstructionOptions = {\n wipe?: boolean\n setActive?: \"other\" | \"a\" | \"b\"\n slot?: SlotName\n skipReboot?: boolean\n applyVbmeta?: boolean\n}\n\ntype Instruction = {\n command: CommandName\n args: string[]\n options: InstructionOptions\n}\n\nconst COMMAND_NAMES: ReadonlySet<string> = new Set([\n \"update\",\n \"flashall\",\n \"flash\",\n \"flashing\",\n \"erase\",\n \"format\",\n \"getvar\",\n \"set_active\",\n \"boot\",\n \"devices\",\n \"continue\",\n \"reboot\",\n \"reboot-bootloader\",\n \"sleep\",\n \"oem\",\n \"help\",\n])\n\nfunction isCommandName(word: string): word is CommandName {\n return COMMAND_NAMES.has(word)\n}\n\nfunction isFileEntry(entry: { directory: boolean }): entry is FileEntry {\n return !entry.directory\n}\n\nfunction requireArg(command: CommandName, args: string[], index: number): string {\n const value = args[index]\n if (!value) {\n throw new Error(`Missing argument ${index + 1} for ${command}`)\n }\n return value\n}\n\nfunction getEntry(entries: FileEntry[], filename: string): FileEntry {\n const entry = entries.find((e) => e.filename.split(/[\\\\/]/).pop() === filename)\n if (entry) {\n return entry\n } else {\n throw new Error(`${filename} not found in zip`)\n }\n}\n\nfunction parseInstruction(text: string): Instruction {\n if (text.slice(0, 8) === \"fastboot\") {\n text = text.slice(8).trim()\n }\n\n let command: CommandName | undefined\n const args: string[] = []\n const options: InstructionOptions = {}\n\n const words = text\n .split(/\\s+/)\n .map((x) => x.trim())\n .filter((x) => x !== \"\")\n\n for (const word of words) {\n if (word[0] === \"-\") {\n if (word === \"-w\" || word === \"--wipe\") {\n options.wipe = true\n } else if (word === \"--set-active=other\") {\n options.setActive = \"other\"\n } else if (word === \"--set-active=a\" || word === \"--set-active=b\") {\n const slot = word.slice(-1)\n if (slot === \"a\" || slot === \"b\") {\n options.setActive = slot\n }\n } else if (word === \"--slot-other\") {\n options.slot = \"other\"\n } else if (word.slice(0, 6) === \"--slot\") {\n const slot = word.split(\"=\")[1]\n if (!slot) {\n throw new Error(\"--slot requires a value\")\n }\n if (![\"current\", \"other\", \"a\", \"b\"].includes(slot)) {\n throw new Error(`unknown slot: ${slot}`)\n }\n options.slot = slot as SlotName\n } else if (word === \"--skip-reboot\") {\n options.skipReboot = true\n } else if (word === \"--apply-vbmeta\") {\n options.applyVbmeta = true\n } else {\n console.warn(`Unknown option: ${word}`)\n }\n } else {\n if (command) {\n args.push(word)\n } else {\n if (!isCommandName(word)) {\n throw new Error(`Unknown command: ${word}`)\n }\n command = word\n }\n }\n }\n if (!command) {\n throw new Error(\"Missing command\")\n }\n return { command, args, options }\n}\n\nfunction parseInstructions(text: string): Instruction[] {\n return text\n .split(\"\\n\")\n .map((x) => x.trim())\n .filter((x) => x !== \"\")\n .filter((x) => x[0] !== \"#\")\n .map(parseInstruction)\n}\n\nexport class FastbootFlasher {\n client: FastbootClient\n reader: ZipReader<BlobReader>\n\n constructor(client: FastbootClient, blob: Blob) {\n this.client = client\n this.reader = new ZipReader(new BlobReader(blob))\n }\n\n // parses and runs flash-all.sh. it ignores all shell commands\n // except fastboot or sleep\n async runFlashAll() {\n const entries = (await this.reader.getEntries()).filter(isFileEntry)\n const flashAllSh = await getEntry(entries, \"flash-all.sh\").getData(new TextWriter())\n\n this.client.logger.log(\"flash-all.sh\\n\" + flashAllSh)\n const instructions = flashAllSh\n .split(\"\\n\")\n .map((x) => x.trim())\n .filter((x) => x.slice(0, 9) === \"fastboot \" || x.slice(0, 5) === \"sleep\")\n .join(\"\\n\")\n\n return this.run(instructions)\n }\n\n async run(instructions: string) {\n const entries = (await this.reader.getEntries()).filter(isFileEntry) // io with factory.zip\n const commands: Instruction[] = parseInstructions(instructions)\n\n for (const command of commands) {\n this.client.logger.log(`‣ ${JSON.stringify(command)}`)\n if (command.command === \"flash\") {\n const partition = requireArg(command.command, command.args, 0)\n const filename = requireArg(command.command, command.args, 1)\n const slot = command.options.slot ?? \"current\"\n const entry = getEntry(entries, filename)\n const blob = await entry.getData(new BlobWriter(\"application/octet-stream\"))\n\n await this.client.doFlash(partition, blob, slot, Boolean(command.options.applyVbmeta))\n } else if (command.command === \"reboot-bootloader\") {\n if (command.options.setActive === \"other\") {\n await this.client.setActiveOtherSlot()\n } else if (command.options.setActive === \"a\") {\n await this.client.fd.exec(\"set_active:a\")\n } else if (command.options.setActive === \"b\") {\n await this.client.fd.exec(\"set_active:b\")\n }\n await this.client.rebootBootloader()\n } else if (command.command === \"update\") {\n const zipName = requireArg(command.command, command.args, 0)\n const nestedZipEntry = getEntry(entries, zipName)\n const zipBlob = await nestedZipEntry.getData(new BlobWriter(\"application/zip\"))\n const zipReader = new ZipReader(new BlobReader(zipBlob))\n const nestedEntries = (await zipReader.getEntries()).filter(isFileEntry)\n const fastbootInfoFile = nestedEntries.find((e) => e.filename === \"fastboot-info.txt\")\n if (!fastbootInfoFile) {\n throw new Error(\"fastboot-info.txt not found in nested zip\")\n }\n const fastbootInfoText = await fastbootInfoFile.getData(new TextWriter())\n\n this.client.logger.log(`fastboot-info.txt: ${fastbootInfoText}`)\n\n // fastboot -w update image-lynx-bp1a.250305.019.zip\n await this.client.fastbootInfo(\n nestedEntries,\n fastbootInfoText,\n Boolean(command.options.wipe),\n )\n } else if (command.command === \"flashing\") {\n if (command.args[0] === \"lock\") {\n await this.client.lock()\n } else if (command.args[0] === \"unlock\") {\n await this.client.unlock()\n } else {\n throw new Error(`Unknown command`)\n }\n } else if (command.command === \"getvar\") {\n const varName = requireArg(command.command, command.args, 0)\n const clientVar = await this.client.getVar(varName)\n this.client.logger.log(`getVar(${varName}) => ${clientVar}`)\n } else if (command.command === \"erase\") {\n const partition = requireArg(command.command, command.args, 0)\n await this.client.erase(partition)\n } else if (command.command === \"sleep\") {\n const ms = command.args[0] ? parseInt(command.args[0]) * 1000 : 5000\n await new Promise((resolve) => setTimeout(resolve, ms))\n // do_oem_command in cpp is raw command?\n } else if (command.command === \"oem\") {\n // ignore motorola oem commands that do nothing useful?\n if (command.args[0] === \"fb_mode_set\" || command.args[0] === \"fb_mode_clear\") {\n await new Promise((resolve) => setTimeout(resolve, 10))\n } else {\n throw new Error(`Fastboot oem command ${command.args[0]} not implemented`)\n }\n } else {\n throw new Error(`Fastboot command ${command.command} not implemented`)\n }\n }\n }\n}\n"],"mappings":"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,gBAAgB,KAAK;GAC5D,kBAAkB,MAAM;GACxB,KAAK;EACP;EACA,KAAK,OAAO,IAAI,uCAAuC;EACvD,MAAM,KAAK,WAAW;CACxB;AACF;;;ACrVA,MAAa,SAAS;CACpB;EACE,UAAU;EACV,UAAU;EACV,UAAU;EACV,WAAW;EACX,UAAU;EACV,MAAM;CACR;CACA;EACE,UAAU;EACV,UAAU;EACV,UAAU;EACV,WAAW;EACX,UAAU;EACV,MAAM;CACR;CACA;EACE,UAAU;EACV,UAAU;EACV,UAAU;EACV,WAAW;EACX,UAAU;EACV,MAAM;CACR;CACA;EACE,UAAU;EACV,UAAU;EACV,UAAU;EACV,WAAW;EACX,UAAU;EACV,MAAM;CACR;CACA;EACE,UAAU;EACV,UAAU;EACV,UAAU;EACV,WAAW;EACX,UAAU;EACV,MAAM;CACR;CACA;EACE,UAAU;EACV,UAAU;EACV,UAAU;EACV,WAAW;EACX,UAAU;EACV,MAAM;CACR;CACA;EACE,UAAU;EACV,UAAU;EACV,UAAU;EACV,WAAW;EACX,UAAU;EACV,MAAM;CACR;CACA;EACE,UAAU;EACV,UAAU;EACV,UAAU;EACV,WAAW;EACX,UAAU;EACV,MAAM;CACR;CACA;EACE,UAAU;EACV,UAAU;EACV,UAAU;EACV,WAAW;EACX,UAAU;EACV,MAAM;CACR;CACA;EACE,UAAU;EACV,UAAU;EACV,UAAU;EACV,WAAW;EACX,UAAU;EACV,MAAM;CACR;CACA;EACE,UAAU;EACV,UAAU;EACV,UAAU;EACV,WAAW;EACX,UAAU;EACV,MAAM;CACR;CACA;EACE,UAAU;EACV,UAAU;EACV,UAAU;EACV,WAAW;EACX,UAAU;EACV,MAAM;CACR;CACA;EACE,UAAU;EACV,UAAU;EACV,UAAU;EACV,WAAW;EACX,UAAU;EACV,MAAM;CACR;CACA;EACE,UAAU;EACV,UAAU;EACV,UAAU;EACV,WAAW;EACX,UAAU;EACV,MAAM;CACR;CACA;EACE,UAAU;EACV,UAAU;EACV,UAAU;EACV,WAAW;EACX,UAAU;EACV,MAAM;CACR;CACA;EACE,UAAU;EACV,UAAU;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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@calyxos/fastboot.ts",
|
|
3
|
+
"version": "0.0.18-rc.1",
|
|
4
|
+
"description": "Fastboot using WebUSB",
|
|
5
|
+
"keywords": [],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Ziggy, Calyx Institute",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "dist/fastboot.js",
|
|
10
|
+
"module": "dist/fastboot.js",
|
|
11
|
+
"types": "dist/fastboot.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
"types": "./dist/fastboot.d.ts",
|
|
14
|
+
"import": "./dist/fastboot.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist/*"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "npm run type-check && tsdown",
|
|
21
|
+
"format": "oxfmt .",
|
|
22
|
+
"format:check": "oxfmt --check .",
|
|
23
|
+
"lint": "oxlint --type-aware",
|
|
24
|
+
"lint:fix": "oxlint --type-aware --fix",
|
|
25
|
+
"tsc": "tsc",
|
|
26
|
+
"type-check": "tsc --noEmit"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@zip.js/zip.js": "^2.8.26"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/w3c-web-usb": "^1.0.14",
|
|
33
|
+
"oxfmt": "^0.51.0",
|
|
34
|
+
"oxlint": "^1.66.0",
|
|
35
|
+
"oxlint-tsgolint": "^0.23.0",
|
|
36
|
+
"tsdown": "^0.22.2",
|
|
37
|
+
"typescript": "^5.9.3"
|
|
38
|
+
}
|
|
39
|
+
}
|