diff --git a/package.json b/package.json index 1b27635..dc79b6d 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,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/pnpm-lock.yaml b/pnpm-lock.yaml index b974344..6ee5b89 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,15 +5,15 @@ settings: excludeLinksFromLockfile: false dependencies: - android-fastboot: - specifier: github:commaai/fastboot.js#c3ec6fe3c96a48dab46e23d0c8c861af15b2144a - version: github.com/commaai/fastboot.js/c3ec6fe3c96a48dab46e23d0c8c861af15b2144a autoprefixer: specifier: 10.4.14 version: 10.4.14(postcss@8.4.24) comlink: specifier: ^4.4.1 version: 4.4.1 + crc-32: + specifier: ^1.2.2 + version: 1.2.2 eslint: specifier: 8.40.0 version: 8.40.0 @@ -1076,11 +1076,6 @@ packages: pretty-format: 29.6.2 dev: true - /@zip.js/zip.js@2.7.24: - resolution: {integrity: sha512-RKXojDXeJcqOLLDFYrPYD0z3YFRaLjuOIAka789VVPGcMeCDEQv08ypNThMt+u+R2b9ISyhWiz43UBrgV1ZcbA==} - engines: {bun: '>=0.7.0', deno: '>=1.0.0', node: '>=16.5.0'} - dev: false - /abab@2.0.6: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} dev: true @@ -1456,6 +1451,12 @@ packages: path-type: 4.0.0 dev: true + /crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + dev: false + /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -2974,10 +2975,6 @@ packages: p-limit: 3.1.0 dev: false - /pako@2.1.0: - resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} - dev: false - /parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -4027,12 +4024,3 @@ packages: /zod@3.21.4: resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==} dev: false - - github.com/commaai/fastboot.js/c3ec6fe3c96a48dab46e23d0c8c861af15b2144a: - resolution: {tarball: https://codeload.github.com/commaai/fastboot.js/tar.gz/c3ec6fe3c96a48dab46e23d0c8c861af15b2144a} - name: android-fastboot - version: 1.1.5-commaai - dependencies: - '@zip.js/zip.js': 2.7.24 - pako: 2.1.0 - dev: false 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 && (