From 3bb7105bded6e84ef9d9db42b769dc0cd68dc77f Mon Sep 17 00:00:00 2001 From: Hoang Bui <47828508+bongbui321@users.noreply.github.com> Date: Wed, 17 Apr 2024 16:52:11 -0400 Subject: [PATCH 1/2] Transfer to QDL (#33) * connect qualcomusb * finish read write * sahara setup * js is painful * finish sahara * upload programmer * fix usblib * load programmer * time out * can reset * firehose detect partition storage info gpt * can detect partition * can write * can erase * can get active slot - haven't tested * setactive slot * remove releaseinterface * cleanup * add sparse * big cleanup * cleanup saharadefs * automate usb packet maxsize * fix getactiveslot * import only used from gpt.js * refactor * move folder * integrate * maxlun = 6 * refactor * serial * trailing white space * fix getactiveslot * auto download loader * setactiveslot * transferout with retry * add reset userdata * add semicolon * update onprogress * test * qdl update * fix sparse * works * add loader * unpack system * remove sleep at write * test * update instructions * update instructions * update instructions * move detach * cleanup sahara * update scripts * update instructions * update error message * fasttt * remove run() * update eraseuserdata * auto upload loader * throw when disconnect + resetuserdata * delete fb * cleanup + catch disconnect * update instruction * faster * fix * zadig_form update * zadig_create_new_device update to match tint * update instruction * remove * update instruction * timed out connect * increase timeout * update instruction * update instruction + update detach script * add copy button * log setactive successfully * throw during connecting if error * error diconnect while connecting * update throw error * cleanup * cleanup * cleanup * restructure + cleanup * fix * fix style * style * fix * fix * serial -> int * cleanup restructure * cleanup * cleanup sparse * cleanup sparse * change name bytes to num * clean up path * consistent var name cmd erase * cleanup sparse * write resetuserdata * remove erase cmd * cleanup sparse * update * update * cleanup style * clearer instruction * cleanup * update check gpt header consistency * faster setactiveslot * move loader into Loaders * 4x faster setactiveslot * cleanup * remove web fastboot * revert manifest test and image workers * fix downloadLoader --------- Co-authored-by: Andrei Radulescu --- bun.lockb | Bin 240008 -> 239213 bytes package.json | 2 +- src/QDL/firehose.js | 289 ++++++++++++++++++++++ src/QDL/gpt.js | 255 ++++++++++++++++++++ src/QDL/qdl.js | 318 +++++++++++++++++++++++++ src/QDL/sahara.js | 260 ++++++++++++++++++++ src/QDL/saharaDefs.js | 98 ++++++++ src/QDL/sparse.js | 261 ++++++++++++++++++++ src/QDL/usblib.js | 152 ++++++++++++ src/QDL/utils.js | 110 +++++++++ src/QDL/xmlParser.js | 53 +++++ src/app/Flash.jsx | 67 +++++- src/app/page.jsx | 33 ++- src/assets/zadig_create_new_device.png | Bin 11106 -> 13805 bytes src/assets/zadig_form.png | Bin 9582 -> 12389 bytes src/config.js | 3 + src/utils/{fastboot.js => flash.js} | 141 +++++------ 17 files changed, 1933 insertions(+), 109 deletions(-) create mode 100644 src/QDL/firehose.js create mode 100644 src/QDL/gpt.js create mode 100644 src/QDL/qdl.js create mode 100644 src/QDL/sahara.js create mode 100644 src/QDL/saharaDefs.js create mode 100644 src/QDL/sparse.js create mode 100644 src/QDL/usblib.js create mode 100644 src/QDL/utils.js create mode 100644 src/QDL/xmlParser.js rename src/utils/{fastboot.js => flash.js} (66%) diff --git a/bun.lockb b/bun.lockb index 15dd47420bed0280d322fd0ec1bd5f4e213292e4..c4dd0c46f6d2cbf3505549ce8f38415e54bb90af 100755 GIT binary patch delta 32947 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≪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 YO3?LDwUI}FuhBQizj)ptyPG}#FD{TC>i_@% diff --git a/package.json b/package.json index a424526..329cd42 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,9 @@ "node": ">=18.0.0" }, "dependencies": { - "android-fastboot": "github:commaai/fastboot.js#c3ec6fe3c96a48dab46e23d0c8c861af15b2144a", "autoprefixer": "10.4.14", "comlink": "^4.4.1", + "crc-32": "^1.2.2", "eslint": "8.40.0", "eslint-config-next": "13.4.1", "jssha": "^3.3.1", diff --git a/src/QDL/firehose.js b/src/QDL/firehose.js new file mode 100644 index 0000000..419e327 --- /dev/null +++ b/src/QDL/firehose.js @@ -0,0 +1,289 @@ +import { xmlParser } from "./xmlParser" +import { concatUint8Array, containsBytes, compareStringToBytes, sleep, readBlobAsBuffer } from "./utils" +import * as Sparse from "./sparse"; + + +class response { + constructor(resp=false, data=new Uint8Array(), error="", log=[]) { + this.resp = resp; + this.data = data; + this.error = error; + this.log = log; + } +} + + +class cfg { + constructor() { + this.ZLPAwareHost = 1; + this.SkipStorageInit = 0; + this.SkipWrite = 0; + this.MaxPayloadSizeToTargetInBytes = 1048576; + this.MaxPayloadSizeFromTargetInBytes = 4096; + this.MaxXMLSizeInBytes = 4096; + this.bit64 = true; + this.SECTOR_SIZE_IN_BYTES = 4096; + this.MemoryName = "UFS"; + this.maxlun = 6; + } +} + +export class Firehose { + constructor(cdc) { + this.cdc = cdc; + this.xml = new xmlParser(); + this.cfg = new cfg(); + this.luns = []; + } + + getStatus(resp) { + if (resp.hasOwnProperty("value")) { + let value = resp["value"]; + return (value === "ACK" || value === "true"); + } + return true; + } + + async xmlSend(data, wait=true) { + let dataToSend = new TextEncoder().encode(data).slice(0, this.cfg.MaxXMLSizeInBytes); + await this.cdc?.write(dataToSend, null, wait); + + let rData = new Uint8Array(); + let counter = 0; + let timeout = 3; + while (!(containsBytes(" timeout) { + break; + } + } + rData = concatUint8Array([rData, tmp]); + } + + const resp = this.xml.getReponse(rData); + const status = this.getStatus(resp); + if (resp.hasOwnProperty("rawmode")) { + if (resp["rawmode"] == "false") { + let log = this.xml.getLog(rData); + return new response(status, rData, "", log) + } + } else { + if (status) { + if (containsBytes("log value=", rData)) { + let log = this.xml.getLog(rData); + return new response(status, rData, "", log); + } + return new response(status, rData); + } + } + return new response(true, rData); + } + + getLuns() { + return Array.from({length: this.cfg.maxlun}, (x, i) => i) + } + + async configure() { + const connectCmd = `` + + `` + + `` + + await this.xmlSend(connectCmd, false); + this.luns = this.getLuns(); + return true; + } + + async cmdReadBuffer(physicalPartitionNumber, startSector, numPartitionSectors) { + const data = `\n` + + let rsp = await this.xmlSend(data); + let resData = new Uint8Array(); + if (!rsp.resp) { + return rsp; + } else { + let bytesToRead = this.cfg.SECTOR_SIZE_IN_BYTES * numPartitionSectors; + while (bytesToRead > 0) { + let tmp = await this.cdc.read(Math.min(this.cdc.maxSize, bytesToRead)); + const size = tmp.length; + bytesToRead -= size; + resData = concatUint8Array([resData, tmp]); + } + + const wd = await this.waitForData(); + const info = this.xml.getLog(wd); + rsp = this.xml.getReponse(wd); + if (rsp.hasOwnProperty("value")) { + if (rsp["value"] !== "ACK") { + return new response(false, resData, info); + } else if (rsp.hasOwnProperty("rawmode")) { + if (rsp["rawmode"] === "false") { + return new response(true, resData); + } + } + } else { + console.error("Failed read buffer"); + return new response(false, resData, rsp[2]); + } + } + let resp = rsp["value"] === "ACK"; + return response(resp, resData, rsp[2]); + } + + async waitForData() { + let tmp = new Uint8Array(); + let timeout = 0; + + while (!containsBytes("response value", tmp)) { + let res = await this.cdc.read(); + if (compareStringToBytes("", res)) { + timeout += 1; + if (timeout === 4) { + break; + } + await sleep(20); + } + tmp = concatUint8Array([tmp, res]); + } + return tmp; + } + + async cmdProgram(physicalPartitionNumber, startSector, blob, onProgress=()=>{}) { + let total = blob.size; + let sparseformat = false; + + let sparseHeader = await Sparse.parseFileHeader(blob.slice(0, Sparse.FILE_HEADER_SIZE)); + if (sparseHeader !== null) { + sparseformat = true; + total = await Sparse.getSparseRealSize(blob, sparseHeader); + } + + let numPartitionSectors = Math.floor(total / this.cfg.SECTOR_SIZE_IN_BYTES); + if (total % this.cfg.SECTOR_SIZE_IN_BYTES !== 0) { + numPartitionSectors += 1; + } + + const data = `\n` + + `\n`; + let i = 0; + let bytesWritten = 0; + let rsp = await this.xmlSend(data); + + if (rsp.resp) { + for await (let split of Sparse.splitBlob(blob)) { + let offset = 0; + let bytesToWriteSplit = split.size; + + while (bytesToWriteSplit > 0) { + const wlen = Math.min(bytesToWriteSplit, this.cfg.MaxPayloadSizeToTargetInBytes); + let wdata = new Uint8Array(await readBlobAsBuffer(split.slice(offset, offset + wlen))); + if (wlen % this.cfg.SECTOR_SIZE_IN_BYTES !== 0) { + let fillLen = (Math.floor(wlen/this.cfg.SECTOR_SIZE_IN_BYTES) * this.cfg.SECTOR_SIZE_IN_BYTES) + + this.cfg.SECTOR_SIZE_IN_BYTES; + const fillArray = new Uint8Array(fillLen-wlen).fill(0x00); + wdata = concatUint8Array([wdata, fillArray]); + } + await this.cdc.write(wdata); + await this.cdc.write(new Uint8Array(0), null, true); + offset += wlen; + bytesWritten += wlen; + bytesToWriteSplit -= wlen; + + // Need this for sparse image when the data.length < MaxPayloadSizeToTargetInBytes + // Add ~2.4s to total flash time + if (sparseformat && bytesWritten < total) { + await this.cdc.write(new Uint8Array(0), null, true); + } + + if (i % 10 === 0) { + onProgress(bytesWritten/total); + } + i += 1; + } + } + + const wd = await this.waitForData(); + const response = this.xml.getReponse(wd); + if (response.hasOwnProperty("value")) { + if (response["value"] !== "ACK") { + return false; + } + } else { + return false; + } + } + + onProgress(1.0); + return true; + } + + async cmdErase(physicalPartitionNumber, startSector, numPartitionSectors) { + const data = `\n` + + `\n`; + let pos = 0; + let rsp = await this.xmlSend(data) + let bytesToWrite = this.cfg.SECTOR_SIZE_IN_BYTES * numPartitionSectors; + let empty = new Uint8Array(this.cfg.MaxPayloadSizeToTargetInBytes).fill(0); + + if (rsp.resp) { + while (bytesToWrite > 0) { + let wlen = Math.min(bytesToWrite, this.cfg.MaxPayloadSizeToTargetInBytes); + await this.cdc.write(empty.slice(0, wlen)); + bytesToWrite -= wlen; + pos += wlen; + await this.cdc.write(new Uint8Array(0)); + } + + const res = await this.waitForData(); + const response = this.xml.getReponse(res); + if (response.hasOwnProperty("value")) { + if (response["value"] !== "ACK") { + throw "Failed to erase: NAK"; + } + } else { + throw "Failed to erase no return value"; + } + } + return true; + } + + async cmdSetBootLunId(lun) { + const data = `\n` + const val = await this.xmlSend(data); + if (val.resp) { + console.log(`Successfully set bootID to lun ${lun}`); + return true; + } else { + throw `Firehose - Failed to set boot lun ${lun}`; + } + } + + async cmdReset() { + let data = ""; + let val = await this.xmlSend(data); + if (val.resp) { + console.log("Reset succeeded"); + return true; + } else { + throw "Firehose - Reset failed"; + } + } +} diff --git a/src/QDL/gpt.js b/src/QDL/gpt.js new file mode 100644 index 0000000..f3f511a --- /dev/null +++ b/src/QDL/gpt.js @@ -0,0 +1,255 @@ +const { containsBytes, bytes2Number } = require("./utils"); +var CRC32 = require("crc-32"); + +export const AB_FLAG_OFFSET = 6; +export const AB_PARTITION_ATTR_SLOT_ACTIVE = (0x1 << 2); +export const PART_ATT_PRIORITY_BIT = BigInt(48) +export const PART_ATT_ACTIVE_BIT = BigInt(50) +export const PART_ATT_ACTIVE_VAL = BigInt(0x1) << PART_ATT_ACTIVE_BIT + +const efiType = { + 0x00000000 : "EFI_UNUSED", + 0xEBD0A0A2 : "EFI_BASIC_DATA", +} + + +class structHelper { + constructor(data, pos = 0) { + this.pos = pos; + this.data = data; + } + + qword(littleEndian=true) { + const view = new DataView(this.data.slice(this.pos, this.pos+=8).buffer, 0); + return Number(view.getBigUint64(0, littleEndian)); + } + + dword(littleEndian=true) { + let view = new DataView(this.data.slice(this.pos, this.pos+=4).buffer, 0); + return view.getUint32(0, littleEndian); + } + + bytes(rlen=1) { + const dat = this.data.slice(this.pos, this.pos+=rlen); + return dat; + } + + toString(rlen=1) { + const dat = this.data.slice(this.pos, this.pos+=rlen); + return dat; + } +} + + +class gptHeader { + constructor(data) { + let sh = new structHelper(data); + this.signature = sh.bytes(8); + this.revision = sh.dword(); + this.headerSize = sh.dword(); + this.crc32 = sh.dword(); + this.reserved = sh.dword(); + this.currentLba = sh.qword(); + this.backupLba = sh.qword(); + this.firstUsableLba = sh.qword(); + this.lastUsableLba = sh.qword(); + this.diskGuid = sh.bytes(16); + this.partEntryStartLba = sh.qword(); + this.numPartEntries = sh.dword(); + this.partEntrySize = sh.dword(); + this.crc32PartEntries = sh.dword(); + } +} + + +export class gptPartition { + constructor(data) { + let sh = new structHelper(data) + this.type = sh.bytes(16); + this.unique = sh.bytes(16); + this.firstLba = sh.qword(); + this.lastLba = sh.qword(); + this.flags = sh.qword(); + this.name = sh.toString(72); + } + + create() { + let buffer = new ArrayBuffer(16 + 16 + 8 + 8 + 8 + 72); + let view = new DataView(buffer); + let offset = 0; + for (let i = 0; i < this.type.length; i++) { + view.setUint8(offset++, this.type[i], true); + } + for (let i = 0; i < this.unique.length; i++) { + view.setUint8(offset++, this.unique[i], true); + } + let tmp = [BigInt(this.firstLba), BigInt(this.lastLba), BigInt(this.flags)]; + for (let i = 0; i < 3; i++) { + view.setBigUint64(offset, tmp[i], true); + offset += 8; + } + for (let i = 0; i < 72; i++) { + view.setUint8(offset++, this.name[i]); + } + return new Uint8Array(view.buffer); + } +} + + +class partf { + firstLba = 0; + lastLba = 0; + flags = 0; + sector = 0; + sectors = 0; + entryOffset = 0; + type = null; + name = ""; + unique = new Uint8Array(); +} + + +export class gpt { + constructor() { + this.header = null; + this.sectorSize = null; + this.partentries = {}; + } + + parseHeader(gptData, sectorSize=512) { + return new gptHeader(gptData.slice(sectorSize, sectorSize + 0x5C)); + } + + parse(gptData, sectorSize=512) { + this.header = new gptHeader(gptData.slice(sectorSize, sectorSize + 0x5C)); + this.sectorSize = sectorSize; + + if (!containsBytes("EFI PART", this.header.signature)) { + return false; + } + + if (this.header.revision != 0x10000) { + console.error("Unknown GPT revision."); + return false; + } + + // mbr (even for backup gpt header to ensure offset consistency) + gpt header + part_table + const start = 2 * sectorSize; + + const entrySize = this.header.partEntrySize; + this.partentries = {}; + const numPartEntries = this.header.numPartEntries; + for (let idx = 0; idx < numPartEntries; idx++) { + const data = gptData.slice(start + (idx * entrySize), start + (idx * entrySize) + entrySize); + if (new DataView(data.slice(16,32).buffer, 0).getUint32(0, true) == 0) { + break; + } + + let partentry = new gptPartition(data); + let pa = new partf(); + const guid1 = new DataView(partentry.unique.slice(0, 0x4).buffer, 0).getUint32(0, true); + const guid2 = new DataView(partentry.unique.slice(0x4, 0x6).buffer, 0).getUint16(0, true); + const guid3 = new DataView(partentry.unique.slice(0x6, 0x8).buffer, 0).getUint16(0, true); + const guid4 = new DataView(partentry.unique.slice(0x8, 0xA).buffer, 0).getUint16(0, true); + const guid5 = Array.from(partentry.unique.subarray(0xA, 0x10)) + .map(byte => byte.toString(16).padStart(2, '0')) + .join(''); + pa.unique =`${guid1.toString(16).padStart(8, '0')}- + ${guid2.toString(16).padStart(4, '0')}- + ${guid3.toString(16).padStart(4, '0')}- + ${guid4.toString(16).padStart(4, '0')}- + ${guid5}`; + pa.sector = partentry.firstLba; + pa.sectors = partentry.lastLba - partentry.firstLba + 1; + pa.flags = partentry.flags; + pa.entryOffset = start + (idx * entrySize); + const typeOfPartentry = new DataView(partentry.type.slice(0, 0x4).buffer, 0).getUint32(0, true); + if (efiType.hasOwnProperty(typeOfPartentry)) { + pa.type = efiType[typeOfPartentry]; + } else { + pa.type = typeOfPartentry.toString(16); + } + let nullIndex = Array.from(partentry.name).findIndex((element, index) => index % 2 === 0 && element === 0); + let nameWithoutNull = partentry.name.slice(0, nullIndex); + let decodedName = new TextDecoder('utf-16').decode(nameWithoutNull); + pa.name = decodedName; + if (pa.type == "EFI_UNUSED") { + continue; + } + this.partentries[pa.name] = pa; + } + return true; + } + + fixGptCrc(data) { + const headerOffset = this.sectorSize; + const partentryOffset = 2 * this.sectorSize; + const partentrySize = this.header.numPartEntries * this.header.partEntrySize; + const partdata = Uint8Array.from(data.slice(partentryOffset, partentryOffset + partentrySize)); + let headerdata = Uint8Array.from(data.slice(headerOffset, headerOffset + this.header.headerSize)); + + let view = new DataView(new ArrayBuffer(4)); + view.setInt32(0, CRC32.buf(Buffer.from(partdata)), true); + headerdata.set(new Uint8Array(view.buffer), 0x58); + view.setInt32(0, 0, true); + headerdata.set(new Uint8Array(view.buffer) , 0x10); + view.setInt32(0, CRC32.buf(Buffer.from(headerdata)), true); + headerdata.set(new Uint8Array(view.buffer), 0x10); + + data.set(headerdata, headerOffset); + return data; + } +} + + +// 0x003a for inactive and 0x006f for active boot partitions. This follows fastboot standard +export function setPartitionFlags(flags, active, isBoot) { + let newFlags = BigInt(flags); + if (active) { + if (isBoot) { + newFlags = BigInt(0x006f) << PART_ATT_PRIORITY_BIT; + } else { + newFlags |= PART_ATT_ACTIVE_VAL; + } + } else { + if (isBoot) { + newFlags = BigInt(0x003a) << PART_ATT_PRIORITY_BIT; + } else { + newFlags &= ~PART_ATT_ACTIVE_VAL; + } + } + return Number(newFlags); +} + + +function checkHeaderCrc(gptData, guidGpt) { + const headerOffset = guidGpt.sectorSize; + const headerSize = guidGpt.header.headerSize; + const testGptData = guidGpt.fixGptCrc(gptData).buffer; + const testHeader = new Uint8Array(testGptData.slice(headerOffset, headerOffset + headerSize)); + + const headerCrc = guidGpt.header.crc32; + const testHeaderCrc = bytes2Number(testHeader.slice(0x10, 0x10 + 4)); + const partTableCrc = guidGpt.header.crc32PartEntries; + const testPartTableCrc = bytes2Number(testHeader.slice(0x58, 0x58 + 4)); + + return [(headerCrc !== testHeaderCrc) || (partTableCrc !== testPartTableCrc), partTableCrc]; +} + + +export function ensureGptHdrConsistency(gptData, backupGptData, guidGpt, backupGuidGpt) { + const partTableOffset = guidGpt.sectorSize * 2; + + const [primCorrupted, primPartTableCrc] = checkHeaderCrc(gptData, guidGpt); + const [backupCorrupted, backupPartTableCrc] = checkHeaderCrc(backupGptData, backupGuidGpt); + + const headerConsistency = primPartTableCrc === backupPartTableCrc; + if (primCorrupted || !headerConsistency) { + if (backupCorrupted) { + throw "Both primary and backup gpt headers are corrupted, cannot recover"; + } + gptData.set(backupGptData.slice(partTableOffset), partTableOffset); + gptData = guidGpt.fixGptCrc(gptData); + } + return gptData; +} diff --git a/src/QDL/qdl.js b/src/QDL/qdl.js new file mode 100644 index 0000000..d673025 --- /dev/null +++ b/src/QDL/qdl.js @@ -0,0 +1,318 @@ +import * as gpt from "./gpt" +import { usbClass } from "./usblib" +import { Sahara } from "./sahara" +import { Firehose } from "./firehose" +import { concatUint8Array, runWithTimeout, containsBytes, bytes2Number } from "./utils" + + +export class qdlDevice { + constructor() { + this.mode = ""; + this.cdc = new usbClass(); + this.sahara = new Sahara(this.cdc); + this.firehose = new Firehose(this.cdc); + this._connectResolve = null; + this._connectReject = null; + } + + async waitForConnect() { + return await new Promise((resolve, reject) => { + this._connectResolve = resolve; + this._connectReject = reject; + }); + } + + async connectToSahara() { + while (!this.cdc.connected) { + await this.cdc?.connect(); + if (this.cdc.connected) { + console.log("QDL device detected"); + let resp = await runWithTimeout(this.sahara?.connect(), 10000); + if (resp.hasOwnProperty("mode")) { + this.mode = resp["mode"]; + console.log("Mode detected:", this.mode); + return resp; + } + } + } + return {"mode" : "error"}; + } + + async connect() { + try { + let resp = await this.connectToSahara(); + let mode = resp["mode"]; + if (mode === "sahara") { + await this.sahara?.uploadLoader(); + } else if (mode === "error") { + throw "Error connecting to Sahara"; + } + await this.firehose?.configure(); + this.mode = "firehose"; + } catch (error) { + if (this._connectReject !== null) { + this._connectReject(error); + this._connectResolve = null; + this._connectReject = null; + } + } + + if (this._connectResolve !== null) { + this._connectResolve(undefined); + this._connectResolve = null; + this._connectReject = null; + } + return true; + } + + async getGpt(lun, startSector=1) { + let resp; + resp = await this.firehose.cmdReadBuffer(lun, 0, 1); + if (!resp.resp) { + console.error(resp.error); + return [null, null]; + } + let data = concatUint8Array([resp.data, (await this.firehose.cmdReadBuffer(lun, startSector, 1)).data]); + let guidGpt = new gpt.gpt(); + const header = guidGpt.parseHeader(data, this.firehose.cfg.SECTOR_SIZE_IN_BYTES); + if (containsBytes("EFI PART", header.signature)) { + const partTableSize = header.numPartEntries * header.partEntrySize; + const sectors = Math.floor(partTableSize / this.firehose.cfg.SECTOR_SIZE_IN_BYTES); + data = concatUint8Array([data, (await this.firehose.cmdReadBuffer(lun, header.partEntryStartLba, sectors)).data]); + guidGpt.parse(data, this.firehose.cfg.SECTOR_SIZE_IN_BYTES); + return [data, guidGpt]; + } else { + throw "Error reading gpt header"; + } + } + + async detectPartition(partitionName, sendFull=false) { + const luns = this.firehose.luns; + for (const lun of luns) { + const [data, guidGpt] = await this.getGpt(lun); + if (guidGpt === null) { + break; + } else { + if (guidGpt.partentries.hasOwnProperty(partitionName)) { + return sendFull ? [true, lun, data, guidGpt] : [true, lun, guidGpt.partentries[partitionName]]; + } + } + } + return [false]; + } + + async flashBlob(partitionName, blob, onProgress=(_progress)=>{}) { + let startSector = 0; + let dp = await this.detectPartition(partitionName); + const found = dp[0]; + if (found) { + let lun = dp[1]; + const imgSize = blob.size; + let imgSectors = Math.floor(imgSize / this.firehose.cfg.SECTOR_SIZE_IN_BYTES); + if (imgSize % this.firehose.cfg.SECTOR_SIZE_IN_BYTES !== 0) { + imgSectors += 1; + } + if (partitionName.toLowerCase() !== "gpt") { + const partition = dp[2]; + if (imgSectors > partition.sectors) { + console.error("partition has fewer sectors compared to the flashing image"); + return false; + } + startSector = partition.sector; + console.log(`Flashing ${partitionName}...`); + if (await this.firehose.cmdProgram(lun, startSector, blob, (progress) => onProgress(progress))) { + console.log(`partition ${partitionName}: startSector ${partition.sector}, sectors ${partition.sectors}`); + } else { + throw `Errow while writing ${partitionName}`; + } + } + } else { + throw `Can't find partition ${partitionName}`; + } + return true; + } + + async erase(partitionName) { + const luns = this.firehose.luns; + for (const lun of luns) { + let [data, guidGpt] = await this.getGpt(lun); + if (guidGpt.partentries.hasOwnProperty(partitionName)) { + const partition = guidGpt.partentries[partitionName]; + console.log(`Erasing ${partitionName}...`); + await this.firehose.cmdErase(lun, partition.sector, partition.sectors); + console.log(`Erased ${partitionName} starting at sector ${partition.sector} with sectors ${partition.sectors}`); + } else { + continue; + } + } + return true; + } + + async getDevicePartitionsInfo() { + const slots = []; + const partitions = []; + const luns = this.firehose.luns; + for (const lun of luns) { + let [data, guidGpt] = await this.getGpt(lun); + if (guidGpt === null) { + throw "Error while reading device partitions"; + } + for (let partition in guidGpt.partentries) { + let slot = partition.slice(-2); + if (slot === "_a" || slot === "_b") { + partition = partition.substring(0, partition.length-2); + if (!slots.includes(slot)) { + slots.push(slot); + } + } + if (!partitions.includes(partition)) { + partitions.push(partition); + } + } + } + return [slots.length, partitions]; + } + + async getActiveSlot() { + const luns = this.firehose.luns; + for (const lun of luns) { + const [data, guidGpt] = await this.getGpt(lun); + if (guidGpt === null) { + throw "Cannot get active slot." + } + for (const partitionName in guidGpt.partentries) { + const slot = partitionName.slice(-2); + // backup gpt header is more reliable, since it would always has the non-corrupted gpt header + const [backupGptData, backupGuidGpt] = await this.getGpt(lun, guidGpt.header.backupLba); + const partition = backupGuidGpt.partentries[partitionName]; + const active = (((BigInt(partition.flags) >> (BigInt(gpt.AB_FLAG_OFFSET) * BigInt(8)))) + & BigInt(gpt.AB_PARTITION_ATTR_SLOT_ACTIVE)) === BigInt(gpt.AB_PARTITION_ATTR_SLOT_ACTIVE); + if (slot == "_a" && active) { + return "a"; + } else if (slot == "_b" && active) { + return "b"; + } + } + } + throw "Can't detect slot A or B"; + } + + patchNewGptData(gptDataA, gptDataB, guidGpt, partA, partB, slot_a_status, slot_b_status, isBoot) { + const partEntrySize = guidGpt.header.partEntrySize; + + const sdataA = gptDataA.slice(partA.entryOffset, partA.entryOffset+partEntrySize); + const sdataB = gptDataB.slice(partB.entryOffset, partB.entryOffset+partEntrySize); + + const partEntryA = new gpt.gptPartition(sdataA); + const partEntryB = new gpt.gptPartition(sdataB); + + partEntryA.flags = gpt.setPartitionFlags(partEntryA.flags, slot_a_status, isBoot); + partEntryB.flags = gpt.setPartitionFlags(partEntryB.flags, slot_b_status, isBoot); + const tmp = partEntryB.type; + partEntryB.type = partEntryA.type; + partEntryA.type = tmp; + const pDataA = partEntryA.create(), pDataB = partEntryB.create(); + + return [pDataA, partA.entryOffset, pDataB, partB.entryOffset]; + } + + async setActiveSlot(slot) { + slot = slot.toLowerCase(); + const luns = this.firehose.luns + let slot_a_status, slot_b_status; + + if (slot == "a") { + slot_a_status = true; + } else if (slot == "b") { + slot_a_status = false; + } + slot_b_status = !slot_a_status; + + for (const lunA of luns) { + let checkGptHeader = false; + let sameLun = false; + let hasPartitionA = false; + let [gptDataA, guidGptA] = await this.getGpt(lunA); + let [backupGptDataA, backupGuidGptA] = await this.getGpt(lunA, guidGptA.header.backupLba); + let lunB, gptDataB, guidGptB, backupGptDataB, backupGuidGptB; + + if (guidGptA === null) { + throw "Error while getting gpt header data"; + } + for (const partitionNameA in guidGptA.partentries) { + let slotSuffix = partitionNameA.toLowerCase().slice(-2); + if (slotSuffix !== "_a") { + continue; + } + const partitionNameB = partitionNameA.slice(0, partitionNameA.length-1) + "b"; + let sts; + if (!checkGptHeader) { + hasPartitionA = true; + if (guidGptA.partentries.hasOwnProperty(partitionNameB)) { + lunB = lunA; + sameLun = true; + gptDataB = gptDataA; + guidGptB = guidGptA; + backupGptDataB = backupGptDataA; + backupGuidGptB = backupGuidGptA; + } else { + const resp = await this.detectPartition(partitionNameB, true); + sts = resp[0]; + if (!sts) { + throw `Cannot find partition ${partitionNameB}`; + } + [sts, lunB, gptDataB, guidGptB] = resp; + [backupGptDataB, backupGuidGptB] = await this.getGpt(lunB, guidGptB.header.backupLba); + } + } + + if (!checkGptHeader && partitionNameA.slice(0, 3) !== "xbl") { // xbl partitions aren't affected by failure of changing slot, saves time + gptDataA = gpt.ensureGptHdrConsistency(gptDataA, backupGptDataA, guidGptA, backupGuidGptA); + if (!sameLun) { + gptDataB = gpt.ensureGptHdrConsistency(gptDataB, backupGptDataB, guidGptB, backupGuidGptB); + } + checkGptHeader = true; + } + + const partA = guidGptA.partentries[partitionNameA]; + const partB = guidGptB.partentries[partitionNameB]; + + let isBoot = false; + if (partitionNameA === "boot_a") { + isBoot = true; + } + const [pDataA, pOffsetA, pDataB, pOffsetB] = this.patchNewGptData( + gptDataA, gptDataB, guidGptA, partA, partB, slot_a_status, slot_b_status, isBoot + ); + + gptDataA.set(pDataA, pOffsetA) + guidGptA.fixGptCrc(gptDataA); + if (lunA === lunB) { + gptDataB = gptDataA; + } + gptDataB.set(pDataB, pOffsetB) + guidGptB.fixGptCrc(gptDataB); + } + + if (!hasPartitionA) { + continue; + } + const writeOffset = this.firehose.cfg.SECTOR_SIZE_IN_BYTES; + const gptBlobA = new Blob([gptDataA.slice(writeOffset)]); + await this.firehose.cmdProgram(lunA, 1, gptBlobA); + if (!sameLun) { + const gptBlobB = new Blob([gptDataB.slice(writeOffset)]); + await this.firehose.cmdProgram(lunB, 1, gptBlobB); + } + } + const activeBootLunId = (slot === "a") ? 1 : 2; + await this.firehose.cmdSetBootLunId(activeBootLunId); + console.log(`Successfully set slot ${slot} active`); + return true; + } + + async reset() { + await this.firehose.cmdReset(); + return true; + } +} diff --git a/src/QDL/sahara.js b/src/QDL/sahara.js new file mode 100644 index 0000000..5af8eef --- /dev/null +++ b/src/QDL/sahara.js @@ -0,0 +1,260 @@ +import { CommandHandler, cmd_t, sahara_mode_t, status_t, exec_cmd_t } from "./saharaDefs" +import { concatUint8Array, packGenerator, readBlobAsBuffer } from "./utils"; +import config from "@/config" + + +export class Sahara { + constructor(cdc) { + this.cdc = cdc; + this.ch = new CommandHandler(); + this.programmer = "6000000000010000_f8ab20526358c4fa_fhprg.bin"; + this.id = null; + this.serial = ""; + this.mode = ""; + this.rootDir = null; + } + + async connect() { + const v = await this.cdc?.read(0xC * 0x4); + if (v.length > 1) { + if (v[0] == 0x01) { + let pkt = this.ch.pkt_cmd_hdr(v); + if (pkt.cmd === cmd_t.SAHARA_HELLO_REQ) { + const rsp = this.ch.pkt_hello_req(v); + return { "mode" : "sahara", "cmd" : cmd_t.SAHARA_HELLO_REQ, "data" : rsp }; + } + } + } + throw "Sahara - Unable to connect to Sahara"; + } + + async cmdHello(mode, version=2, version_min=1, max_cmd_len=0) { + const cmd = cmd_t.SAHARA_HELLO_RSP; + const len = 0x30; + const elements = [cmd, len, version, version_min, max_cmd_len, mode, 1, 2, 3, 4, 5, 6]; + const responseData = packGenerator(elements); + await this.cdc?.write(responseData); + return true; + } + + async cmdModeSwitch(mode) { + const elements = [cmd_t.SAHARA_SWITCH_MODE, 0xC, mode]; + let data = packGenerator(elements); + await this.cdc?.write(data); + return true; + } + + async getResponse() { + try { + let data = await this.cdc?.read(); + let data_text = new TextDecoder('utf-8').decode(data.data); + if (data.length == 0) { + return {}; + } else if (data_text.includes("= 0) { + let resp = await this.getResponse(); + let cmd; + if (resp.hasOwnProperty("cmd")) { + cmd = resp["cmd"]; + } else { + throw "Sahara - Timeout while uploading loader. Wrong loader?"; + } + if (cmd == cmd_t.SAHARA_64BIT_MEMORY_READ_DATA) { + let pkt = resp["data"]; + this.id = pkt.image_id; + if (this.id >= 0xC) { + this.mode = "firehose"; + if (loop == 0) { + console.log("Firehose mode detected, uploading..."); + } + } else { + throw "Sahara - Unknown sahara id"; + } + + loop += 1; + let dataOffset = pkt.data_offset; + let dataLen = pkt.data_len; + if (dataOffset + dataLen > programmer.length) { + const fillerArray = new Uint8Array(dataOffset+dataLen-programmer.length).fill(0xff); + programmer = concatUint8Array([programmer, fillerArray]); + } + let dataToSend = programmer.slice(dataOffset, dataOffset+dataLen); + await this.cdc?.write(dataToSend); + datalen -= dataLen; + } else if (cmd == cmd_t.SAHARA_END_TRANSFER) { + let pkt = resp["data"]; + if (pkt.image_tx_status == status_t.SAHARA_STATUS_SUCCESS) { + if (await this.cmdDone()) { + console.log("Loader successfully uploaded"); + } else { + throw "Sahara - Failed to upload Loader"; + } + return this.mode; + } + } + } + return this.mode; + } + + async cmdDone() { + const toSendData = packGenerator([cmd_t.SAHARA_DONE_REQ, 0x8]); + if (await this.cdc.write(toSendData)) { + let res = await this.getResponse(); + if (res.hasOwnProperty("cmd")) { + let cmd = res["cmd"]; + if (cmd == cmd_t.SAHARA_DONE_RSP) { + return true; + } else if (cmd == cmd_t.SAHARA_END_TRANSFER) { + if (res.hasOwnProperty("data")) { + let pkt = res["data"]; + if (pkt.iamge_txt_status == status_t.SAHARA_NAK_INVALID_CMD) { + console.error("Invalid transfer command received"); + return false; + } + } + } else { + throw "Sahara - Received invalid response"; + } + } + } + return false; + } +} diff --git a/src/QDL/saharaDefs.js b/src/QDL/saharaDefs.js new file mode 100644 index 0000000..7258f74 --- /dev/null +++ b/src/QDL/saharaDefs.js @@ -0,0 +1,98 @@ +import { structHelper_io } from "./utils" + + +export const cmd_t = { + SAHARA_HELLO_REQ : 0x1, + SAHARA_HELLO_RSP : 0x2, + SAHARA_READ_DATA : 0x3, + SAHARA_END_TRANSFER : 0x4, + SAHARA_DONE_REQ : 0x5, + SAHARA_DONE_RSP : 0x6, + SAHARA_RESET_RSP : 0x8, + SAHARA_CMD_READY : 0xB, + SAHARA_SWITCH_MODE : 0xC, + SAHARA_EXECUTE_REQ : 0xD, + SAHARA_EXECUTE_RSP : 0xE, + SAHARA_EXECUTE_DATA : 0xF, + SAHARA_64BIT_MEMORY_READ_DATA : 0x12, +} + +export const exec_cmd_t = { + SAHARA_EXEC_CMD_SERIAL_NUM_READ : 0x01 +} + +export const sahara_mode_t = { + SAHARA_MODE_IMAGE_TX_PENDING : 0x0, + SAHARA_MODE_COMMAND : 0x3 +} + +export const status_t = { + SAHARA_STATUS_SUCCESS : 0x00, // Invalid command received in current state + SAHARA_NAK_INVALID_CMD : 0x01, // Protocol mismatch between host and targe +} + + +export class CommandHandler { + pkt_cmd_hdr(data) { + let st = new structHelper_io(data); + return { cmd : st.dword(), len : st.dword() } + } + + pkt_hello_req(data) { + let st = new structHelper_io(data); + return { + cmd : st.dword(), + len : st.dword(), + version : st.dword(), + version_supported : st.dword(), + cmd_packet_length : st.dword(), + mode : st.dword(), + reserved1 : st.dword(), + reserved2 : st.dword(), + reserved3 : st.dword(), + reserved4 : st.dword(), + reserved5 : st.dword(), + reserved6 : st.dword(), + } + } + + pkt_image_end(data) { + let st = new structHelper_io(data); + return { + cmd : st.dword(), + len : st.dword(), + image_id : st.dword(), + image_tx_status : st.dword(), + } + } + + pkt_done(data) { + let st = new structHelper_io(data); + return { + cmd : st.dword(), + len : st.dword(), + image_tx_status : st.dword() + } + } + + pkt_read_data_64(data) { + let st = new structHelper_io(data); + return { + cmd : st.dword(), + len : st.dword(), + image_id : Number(st.qword()), + data_offset : Number(st.qword()), + data_len : Number(st.qword()), + } + } + + pkt_execute_rsp_cmd(data) { + let st = new structHelper_io(data); + return { + cmd : st.dword(), + len : st.dword(), + client_cmd : st.dword(), + data_len : st.dword(), + } + } +} diff --git a/src/QDL/sparse.js b/src/QDL/sparse.js new file mode 100644 index 0000000..9a5cb8f --- /dev/null +++ b/src/QDL/sparse.js @@ -0,0 +1,261 @@ +import { readBlobAsBuffer } from "./utils"; + +const FILE_MAGIC = 0xed26ff3a; +export const FILE_HEADER_SIZE = 28; +const CHUNK_HEADER_SIZE = 12; +const MAX_STORE_SIZE = 1024 * 1024 * 1024; // 1 GiB + +const ChunkType = { + Raw : 0xCAC1, + Fill : 0xCAC2, + Skip : 0xCAC3, + Crc32 : 0xCAC4, +} + + +class QCSparse { + constructor(blob, header) { + this.blob = blob; + this.blockSize = header.blockSize; + this.totalChunks = header.totalChunks; + this.blobOffset = 0; + } + + async getChunkSize() { + const chunkHeader = await parseChunkHeader(this.blob.slice(this.blobOffset, this.blobOffset + CHUNK_HEADER_SIZE)); + const chunkType = chunkHeader.type; + const blocks = chunkHeader.blocks; + const dataSize = chunkHeader.dataBytes; + this.blobOffset += CHUNK_HEADER_SIZE + dataSize; + + if (chunkType == ChunkType.Raw) { + if (dataSize != (blocks * this.blockSize)) { + throw "Sparse - Chunk input size does not match output size"; + } else { + return dataSize; + } + } else if (chunkType == ChunkType.Fill) { + if (dataSize != 4) { + throw "Sparse - Fill chunk should have 4 bytes"; + } else { + return blocks * this.blockSize; + } + } else if (chunkType == ChunkType.Skip) { + return blocks * this.blockSize; + } else if (chunkType == ChunkType.Crc32) { + if (dataSize != 4) { + throw "Sparse - CRC32 chunk should have 4 bytes"; + } else { + return 0; + } + } else { + throw "Sparse - Unknown chunk type"; + } + } + + async getSize() { + this.blobOffset = FILE_HEADER_SIZE; + let length = 0, chunk = 0; + while (chunk < this.totalChunks) { + let tlen = await this.getChunkSize(); + length += tlen; + chunk += 1; + } + this.blobOffset = FILE_HEADER_SIZE; + return length; + } +} + + +export async function getSparseRealSize(blob, header) { + const sparseImage = new QCSparse(blob, header); + return await sparseImage.getSize(); +} + + +async function parseChunkHeader(blobChunkHeader) { + let chunkHeader = await readBlobAsBuffer(blobChunkHeader); + let view = new DataView(chunkHeader); + return { + type : view.getUint16(0, true), + blocks : view.getUint32(4, true), + dataBytes : view.getUint32(8, true) - CHUNK_HEADER_SIZE, + data : null, + } +} + +export async function parseFileHeader(blobHeader) { + let header = await readBlobAsBuffer(blobHeader); + let view = new DataView(header); + + let magic = view.getUint32(0, true); + let majorVersion = view.getUint16(4, true); + let minorVersion = view.getUint16(6, true); + let fileHeadrSize = view.getUint16(8, true); + let chunkHeaderSize = view.getUint16(10, true); + let blockSize = view.getUint32(12, true); + let totalBlocks = view.getUint32(16, true); + let totalChunks = view.getUint32(20, true); + let crc32 = view.getUint32(24, true); + + if (magic != FILE_MAGIC) { + return null; + } + if (fileHeadrSize != FILE_HEADER_SIZE) { + console.error(`The file header size was expected to be 28, but is ${fileHeadrSize}.`); + return null; + } + if (chunkHeaderSize != CHUNK_HEADER_SIZE) { + console.error(`The chunk header size was expected to be 12, but is ${chunkHeaderSize}.`); + return null; + } + + return { + magic : magic, + majorVersion : majorVersion, + minorVersion : minorVersion, + fileHeadrSize : fileHeadrSize, + chunkHeaderSize : chunkHeaderSize, + blockSize : blockSize, + totalBlocks : totalBlocks, + totalChunks : totalChunks, + crc32 : crc32, + } +} + +async function populate(chunks, blockSize) { + const nBlocks = calcChunksBlocks(chunks); + let ret = new Uint8Array(nBlocks * blockSize); + let offset = 0; + + for (const chunk of chunks) { + const chunkType = chunk.type; + const blocks = chunk.blocks; + const dataSize = chunk.dataBytes; + const data = chunk.data; + + if (chunkType == ChunkType.Raw) { + let rawData = new Uint8Array(await readBlobAsBuffer(data)); + ret.set(rawData, offset); + offset += blocks * blockSize; + } else if (chunkType == ChunkType.Fill) { + const fillBin = new Uint8Array(await readBlobAsBuffer(data)); + const bufferSize = blocks * blockSize; + for (let i = 0; i < bufferSize; i += dataSize) { + ret.set(fillBin, offset); + offset += dataSize; + } + } else if (chunkType == ChunkType.Skip) { + let byteToSend = blocks * blockSize; + let skipData = new Uint8Array(byteToSend).fill(0); + ret.set(skipData, offset); + offset += byteToSend; + } else if (chunkType == ChunkType.Crc32) { + continue; + } else { + throw "Sparse - Unknown chunk type"; + } + } + return new Blob([ret.buffer]); +} + + +function calcChunksRealDataBytes(chunk, blockSize) { + switch (chunk.type) { + case ChunkType.Raw: + return chunk.dataBytes; + case ChunkType.Fill: + return chunk.blocks * blockSize; + case ChunkType.Skip: + return chunk.blocks * blockSize; + case ChunkType.Crc32: + return 0; + default: + throw "Sparse - Unknown chunk type"; + } +} + + +function calcChunksSize(chunks, blockSize) { + return chunks.map((chunk) => calcChunksRealDataBytes(chunk, blockSize)).reduce((total, c) => total + c, 0); +} + + +function calcChunksBlocks(chunks) { + return chunks.map((chunk) => chunk.blocks).reduce((total, c) => total + c, 0); +} + + +export async function* splitBlob(blob, splitSize = 1048576 /* maxPayloadSizeToTarget */) { + const safeToSend = splitSize; + + let header = await parseFileHeader(blob.slice(0, FILE_HEADER_SIZE)); + if (header === null) { + yield blob; + return; + } + + header.crc32 = 0; + blob = blob.slice(FILE_HEADER_SIZE); + let splitChunks = []; + for (let i = 0; i < header.totalChunks; i++) { + let originalChunk = await parseChunkHeader(blob.slice(0, CHUNK_HEADER_SIZE)); + originalChunk.data = blob.slice(CHUNK_HEADER_SIZE, CHUNK_HEADER_SIZE + originalChunk.dataBytes); + blob = blob.slice(CHUNK_HEADER_SIZE + originalChunk.dataBytes); + + let chunksToProcess = []; + let realBytesToWrite = calcChunksRealDataBytes(originalChunk, header.blockSize) + + const isChunkTypeSkip = originalChunk.type == ChunkType.Skip; + const isChunkTypeFill = originalChunk.type == ChunkType.Fill; + + if (realBytesToWrite > safeToSend) { + let bytesToWrite = isChunkTypeSkip ? 1 : originalChunk.dataBytes; + let originalChunkData = originalChunk.data; + + while (bytesToWrite > 0) { + const toSend = Math.min(safeToSend, bytesToWrite); + let tmpChunk; + + if (isChunkTypeFill || isChunkTypeSkip) { + while (realBytesToWrite > 0) { + const realSend = Math.min(safeToSend, realBytesToWrite); + tmpChunk = { + type : originalChunk.type, + blocks : realSend / header.blockSize, + dataBytes : isChunkTypeSkip ? 0 : toSend, + data : isChunkTypeSkip ? new Blob([]) : originalChunkData.slice(0, toSend), + } + chunksToProcess.push(tmpChunk); + realBytesToWrite -= realSend; + } + } else { + tmpChunk = { + type : originalChunk.type, + blocks : toSend / header.blockSize, + dataBytes : toSend, + data : originalChunkData.slice(0, toSend), + } + chunksToProcess.push(tmpChunk); + } + bytesToWrite -= toSend; + originalChunkData = originalChunkData?.slice(toSend); + } + } else { + chunksToProcess.push(originalChunk) + } + for (const chunk of chunksToProcess) { + const remainingBytes = splitSize - calcChunksSize(splitChunks); + const realChunkBytes = calcChunksRealDataBytes(chunk); + if (remainingBytes >= realChunkBytes) { + splitChunks.push(chunk); + } else { + yield await populate(splitChunks, header.blockSize); + splitChunks = [chunk]; + } + } + } + if (splitChunks.length > 0) { + yield await populate(splitChunks, header.blockSize); + } +} diff --git a/src/QDL/usblib.js b/src/QDL/usblib.js new file mode 100644 index 0000000..5a9dbd4 --- /dev/null +++ b/src/QDL/usblib.js @@ -0,0 +1,152 @@ +import { concatUint8Array, sleep } from "./utils"; + +const vendorID = 0x05c6; +const productID = 0x9008; +const QDL_USB_CLASS = 0xff; +const BULK_TRANSFER_SIZE = 16384; + + +export class usbClass { + constructor() { + this.device = null; + this.epIn = null; + this.epOut = null; + this.maxSize = 512; + } + + get connected() { + return ( + this.device !== null && + this.device.opened && + this.device.configurations[0].interfaces[0].claimed + ); + } + + async _validateAndConnectDevice() { + let ife = this.device?.configurations[0].interfaces[0].alternates[0]; + if (ife.endpoints.length !== 2) { + throw "USB - Attempted to connect to null device"; + } + + this.epIn = null; + this.epOut = null; + + for (let endpoint of ife.endpoints) { + if (endpoint.type !== "bulk") { + throw "USB - Interface endpoint is not bulk"; + } + if (endpoint.direction === "in") { + if (this.epIn === null) { + this.epIn = endpoint; + } else { + throw "USB - Interface has multiple IN endpoints"; + } + } else if (endpoint.direction === "out") { + if (this.epOut === null) { + this.epOut = endpoint; + } else { + throw "USB - Interface has multiple OUT endpoints"; + } + } + this.maxSize = this.epIn.packetSize; + } + console.log("Endpoints: in =", this.epIn, ", out =", this.epOut); + + try { + await this.device?.open(); + await this.device?.selectConfiguration(1); + try { + await this.device?.claimInterface(0); + } catch(error) { + await this.device?.reset(); + await this.device?.forget(); + await this.device?.close(); + console.error(error); + } + } catch (error) { + throw `USB - ${error}`; + } + } + + async connect() { + this.device = await navigator.usb.requestDevice({ + filters: [ + { + vendorID : vendorID, + productID : productID, + classCode : QDL_USB_CLASS, + }, + ], + }); + console.log("Using USB device:", this.device); + + navigator.usb.addEventListener("connect", async (event) =>{ + console.log("USB device connect:", event.device); + this.device = event.device; + try { + await this._validateAndConnectDevice(); + } catch (error) { + console.log("Error while connecting to the device"); + throw error; + } + }); + await this._validateAndConnectDevice(); + } + + async read(resplen=null) { + let respData = new Uint8Array(); + let covered = 0; + if (resplen === null) { + resplen = this.epIn.packetSize; + } + + while (covered < resplen) { + try { + let respPacket = await this.device?.transferIn(this.epIn?.endpointNumber, resplen); + respData = concatUint8Array([respData, new Uint8Array(respPacket.data.buffer)]); + resplen = respData.length; + covered += respData.length; + } catch (error) { + throw error; + } + } + return respData; + } + + + async write(cmdPacket, pktSize=null, wait=true) { + if (cmdPacket.length === 0) { + try { + await this.device?.transferOut(this.epOut?.endpointNumber, cmdPacket); + } catch(error) { + try { + await this.device?.transferOut(this.epOut?.endpointNumber, cmdPacket); + } catch(error) { + throw error; + } + } + return true; + } + + let offset = 0; + if (pktSize === null) { + pktSize = BULK_TRANSFER_SIZE; + } + while (offset < cmdPacket.length) { + try { + if (wait) { + await this.device?.transferOut(this.epOut?.endpointNumber, cmdPacket.slice(offset, offset + pktSize)); + } else { + // this is a hack, webusb doesn't have timed out catching + // this only happens in sahara.configure(). The loader receive the packet but doesn't respond back (same as edl repo). + this.device?.transferOut(this.epOut?.endpointNumber, cmdPacket.slice(offset, offset + pktSize)); + await sleep(80); + } + offset += pktSize; + } catch (error) { + throw error; + } + } + return true; + } +} diff --git a/src/QDL/utils.js b/src/QDL/utils.js new file mode 100644 index 0000000..399e20e --- /dev/null +++ b/src/QDL/utils.js @@ -0,0 +1,110 @@ +export const sleep = ms => new Promise(r => setTimeout(r, ms)); + + +export class structHelper_io { + constructor(data, pos=0) { + this.pos = pos + this.data = data; + } + + dword(littleEndian=true) { + let view = new DataView(this.data.slice(this.pos, this.pos+4).buffer, 0); + this.pos += 4; + return view.getUint32(0, littleEndian); + } + + qword(littleEndian=true) { + let view = new DataView(this.data.slice(this.pos, this.pos+8).buffer, 0); + this.pos += 8; + return view.getBigUint64(0, littleEndian); + } +} + + +export function packGenerator(elements, littleEndian=true) { + let n = elements.length; + const buffer = new ArrayBuffer(n*4); + const view = new DataView(buffer); + for (let i = 0; i < n; i++) { + view.setUint32(i*4, elements[i], littleEndian); + } + return new Uint8Array(view.buffer); +} + + +export function concatUint8Array(arrays) { + let length = 0; + arrays.forEach(item => { + if (item !== null) { + length += item.length; + } + }); + let concatArray = new Uint8Array(length); + let offset = 0; + arrays.forEach( item => { + if (item !== null) { + concatArray.set(item, offset); + offset += item.length; + } + }); + return concatArray; +} + + +export function containsBytes(subString, array) { + let tArray = new TextDecoder().decode(array); + return tArray.includes(subString); +} + + +export function compareStringToBytes(compareString, array) { + let tArray = new TextDecoder().decode(array); + return compareString == tArray; +} + + +export function readBlobAsBuffer(blob) { + return new Promise((resolve, reject) => { + let reader = new FileReader(); + reader.onload = () => { + resolve(reader.result); + }; + reader.onerror = () => { + reject(reader.error); + }; + reader.readAsArrayBuffer(blob); + }); +} + + +export function bytes2Number(array) { + let view = new DataView(array.buffer, 0); + if (array.length !== 8 && array.length !== 4) { + throw "Only convert to 64 and 32 bit Number"; + } + return (array.length === 8) ? view.getBigUint64(0, true) : view.getUint32(0, true); +} + + +export function runWithTimeout(promise, timeout) { + return new Promise((resolve, reject) => { + let timedOut = false; + let tid = setTimeout(() => { + timedOut = true; + reject(new Error(`Timed out while trying to connect ${timeout}`)); + }, timeout); + promise + .then((val) => { + if (!timedOut) + resolve(val); + }) + .catch((err) => { + if (!timedOut) + reject(err); + }) + .finally(() => { + if (!timedOut) + clearTimeout(tid); + }); + }); +} \ No newline at end of file diff --git a/src/QDL/xmlParser.js b/src/QDL/xmlParser.js new file mode 100644 index 0000000..f5cd31b --- /dev/null +++ b/src/QDL/xmlParser.js @@ -0,0 +1,53 @@ +export class xmlParser { + getReponse(input) { + let tInput = new TextDecoder().decode(input); + let lines = tInput.split(" { + obj[attr.name] = attr.value; + return obj; + }, content); + } + } + return content; + } + + + getLog(input) { + let tInput = new TextDecoder().decode(input); + let lines = tInput.split(" { + if (attr.name == "value") + obj.push(attr.value); + return obj; + }, data); + } + } + return data; + } +} diff --git a/src/app/Flash.jsx b/src/app/Flash.jsx index f0b115f..774954a 100644 --- a/src/app/Flash.jsx +++ b/src/app/Flash.jsx @@ -1,8 +1,8 @@ 'use client' -import { useCallback } from 'react' +import React, { useCallback, useState } from 'react' import Image from 'next/image' -import { Step, Error, useFastboot } from '@/utils/fastboot' +import { Step, Error, useQdl } from '@/utils/flash' import bolt from '@/assets/bolt.svg' import cable from '@/assets/cable.svg' @@ -59,8 +59,9 @@ const steps = { }, [Step.DONE]: { status: 'Done', - description: 'Your device has been updated successfully. You can now unplug the USB cable from your computer. To ' + - 'complete the system reset, follow the instructions on your device.', + description: 'Your device has been updated successfully. You can now unplug the all cables from your device, ' + +'and wait for the light to stop blinking then plug the power cord in again. ' + +' To complete the system reset, follow the instructions on your device.', bgColor: 'bg-green-500', icon: done, }, @@ -69,7 +70,8 @@ const steps = { const errors = { [Error.UNKNOWN]: { status: 'Unknown error', - description: 'An unknown error has occurred. Restart your browser and try again.', + description: 'An unknown error has occurred. Unplug your device and wait for 20s. ' + + 'Restart your browser and try again.', bgColor: 'bg-red-500', icon: exclamation, }, @@ -81,12 +83,14 @@ const errors = { }, [Error.LOST_CONNECTION]: { status: 'Lost connection', - description: 'The connection to your device was lost. Check that your cables are connected properly and try again.', + description: 'The connection to your device was lost. Check that your cables are connected properly and try again. ' + + 'Unplug your device and wait for around 20s.', icon: cable, }, [Error.DOWNLOAD_FAILED]: { status: 'Download failed', - description: 'The system image could not be downloaded. Check your internet connection and try again.', + description:'The system image could not be downloaded. Unpluck your device and wait for 20s. ' + + 'Check your internet connection and try again.', icon: cloudError, }, [Error.CHECKSUM_MISMATCH]: { @@ -113,6 +117,13 @@ const errors = { }, } +const detachScript = [ + "bus=$(lsusb | grep 05c6:9008 | awk '{print $2}' | sed 's/Bus //;s/^0*//')", + "port=$(lsusb -t | grep Driver=qcserial | awk -F'Port ' '{print $2}' | cut -d ':' -f 1)", + "echo -n \"$bus-$port:1.0\" | sudo tee /sys/bus/usb/drivers/qcserial/unbind > /dev/null" +]; + +const isLinux = navigator.userAgent.toLowerCase().includes('linux'); function LinearProgress({ value, barColor }) { if (value === -1 || value > 100) value = 100 @@ -189,7 +200,7 @@ export default function Flash() { connected, serial, - } = useFastboot() + } = useQdl() const handleContinue = useCallback(() => { onContinue?.() @@ -222,6 +233,15 @@ export default function Flash() { window.removeEventListener("beforeunload", beforeUnloadListener, { capture: true }) } + const [copied, setCopied] = useState(false); + const handleCopy = () => { + setCopied(true); + setTimeout(() => { + setCopied(false); + }, 1000); + }; + + return (
{title} {description} + {(title === "Lost connection" || title === "Ready") && isLinux && ( + <> + + It seems that you're on Linux, make sure to run the script below in your terminal after plugging in your device. + +
+
+
+
+                  {detachScript.map((line, index) => (
+                    
+                      {line}
+                    
+                  ))}
+                
+
+ +
+
+
+
+ + )} {error && (