Skip to content

Commit

Permalink
webusb: add zstd decompress support for wic image
Browse files Browse the repository at this point in the history
  • Loading branch information
Sarah Wang committed Jul 25, 2023
1 parent 787880f commit 08b5fd6
Show file tree
Hide file tree
Showing 7 changed files with 318 additions and 10 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ CMakeCache.txt
*.clst
*.snap
node_modules
build
build
*-lock.json
2 changes: 2 additions & 0 deletions webusb/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
window.ZstdCodec = require('zstd-codec').ZstdCodec;
require("zstd-codec/lib/module.js").run((binding) => {console.log("running", binding); window.binding = binding} )
19 changes: 10 additions & 9 deletions webusb/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,24 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"-": "^0.0.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"fs": "^0.0.1-security",
"g": "^2.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "^5.0.1",
"web-vitals": "^2.1.4"
"web-vitals": "^2.1.4",
"zstd-codec": "^0.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"build": "react-scripts build; browserify index.js -o build/bundle.js -t babelify --presets es2015; ",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
Expand All @@ -35,5 +33,8 @@
"last 1 safari version"
]
},
"proxy": "http://localhost:5000"
"devDependencies": {
"@babel/core": "^7.22.9",
"babelify": "^10.0.0"
}
}
6 changes: 6 additions & 0 deletions webusb/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,14 @@
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<script src="bundle.js"></script>
</head>

<body>
<h1>Sample Applications</h1>
<div id="container"></div>


<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
Expand Down
4 changes: 4 additions & 0 deletions webusb/src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useState } from 'react';
import Combined from "./components/Combined";
import Decompress2 from "./components/Decompress";
import usePopup from "./logic/usePopup";

import './App.css'
Expand All @@ -12,6 +13,9 @@ const App = () => {
return (
<div className="App">
<div className="u-flex u-column">
<div>
<Decompress2 />
</div>
<div className="u-row">
<span className="link-container">bootloader [optional]: </span>
<span className="file-name">{bootFile? bootFile.name:""}</span>
Expand Down
1 change: 1 addition & 0 deletions webusb/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
// import * as Z from 'zstd-codec'

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
Expand Down
293 changes: 293 additions & 0 deletions webusb/src/logic/useDecompress.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@

import {useEffect, useState} from 'react';
import {str_to_arr, ab_to_str} from '../helper/functions.js'
import {CHUNK_SZ, BLK_SZ, CHUNK_TYPE_RAW, CHUNK_TYPE_DONT_CARE, build_sparse_header, build_chunk_header} from '../helper/sparse.js'

const USBrequestParams = { filters: [{ vendorId: 8137, productId: 0x0152 }] };

const PACKET_SZ = 0x10000;
const DATA_SZ = CHUNK_SZ * BLK_SZ; // in bytes

const useDecompress = (flashFile) => {
const [USBDevice, setUSBDevice] = useState();
const [flashProgress, setFlashProgress] = useState();
const [flashTotal, setFlashTotal] = useState();

useEffect (()=> {
if (USBDevice) {
USBDevice.open().then(()=>{
console.log(`Opened USB Device: ${USBDevice.productName}`);
}, (err)=> {
console.log(err)
})
.then(()=> {
if (flashFile)
decompressFileToTransform();
})
}

}, [USBDevice])

const requestUSBDevice = () => { // first thing called with button
navigator.usb
.requestDevice(USBrequestParams)
.then((device) => {
setUSBDevice(device);
})
.catch((e) => {
console.error(`There is no device. ${e}`);
});
}

async function preBoot () {
await USBDevice.claimInterface(0);

await send_data(await str_to_arr("UCmd:setenv fastboot_dev mmc"), "OKAY");
await send_data(await str_to_arr("UCmd:setenv mmcdev ${emmc_dev}"), "OKAY");
await send_data(await str_to_arr("UCmd:mmc dev ${emmc_dev}"), "OKAY");

console.log("preboot complete")
}

const processChunk = async(data, curr_len, i) => {
console.log(data, curr_len, i);

// ignore, fill with zeroes
if (curr_len != data.length) {
let fill_len = (Math.ceil(curr_len/BLK_SZ)*BLK_SZ - curr_len);
// let fill = new Uint8Array(fill_len);
data.set(curr_len, new Uint8Array(fill_len));
data = data.slice(0, curr_len+fill_len);
}

let hex_len = (data.length + 52).toString(16); // 52 comes from headers
await send_data(await str_to_arr(`download:${hex_len}`), `DATA${hex_len}`);
await send_headers(data.length, i);

let offset=0;
while (offset < data.length) {
let packet_len = Math.min(PACKET_SZ, DATA_SZ - offset);
await USBDevice.transferOut(1, data.slice(offset, offset + packet_len));
console.log("transferout", data.slice(offset, offset + packet_len));
offset += packet_len;
}

let result = await USBDevice.transferIn(1, 1048);
if ("OKAY" !== await ab_to_str(result.data.buffer)) {
throw new Error ("failed to send data:", await ab_to_str(result.data.buffer))
}

await flash_all();
console.log("FLASH SUCCESS")
}

async function send_packet(raw_data) {
await USBDevice.transferOut(1, raw_data);
console.log("transferout", raw_data);
}

async function send_chunk_headers (chunk_len, i) {
let hex_len = (chunk_len + 52).toString(16); // 52 comes from headers
await send_data(await str_to_arr(`download:${hex_len}`), `DATA${hex_len}`);
await send_headers(chunk_len, i);
}

async function send_headers(raw_data_bytelength, i) {
let sparse = await build_sparse_header(raw_data_bytelength, i);
let dont_care = await build_chunk_header(CHUNK_TYPE_DONT_CARE, raw_data_bytelength, i);
let raw = await build_chunk_header(CHUNK_TYPE_RAW, raw_data_bytelength, i);

let headers = new Uint8Array(52);
headers.set(sparse, 0);
headers.set(dont_care, 28);
headers.set(raw, 40);

await USBDevice.transferOut(1, headers);
}

async function send_flash () {
let result = await USBDevice.transferIn(1, 1048);
if ("OKAY" !== await ab_to_str(result.data.buffer)) {
throw new Error ("failed to send data:", await ab_to_str(result.data.buffer))
}

await flash_all();
}

/*
* data: string or arraybuffer
* success_str: checks response of usb
* Throws error if USB input does not match success_str
*/
async function send_data(data, success_str) {
await USBDevice.transferOut(1, data);
let result = await USBDevice.transferIn(1, 1048);

if (success_str !== await ab_to_str(result.data.buffer)) {
throw new Error ("failed to send data:",await ab_to_str(result.data.buffer))
}
}

async function flash_all () {
await send_data(await str_to_arr("flash:all"), "OKAY");
console.log("flash");
}

const decompressFileToTransform = async(e) => {
console.log(USBDevice);

if (!flashFile) return;

const file = flashFile;
let buff = await file.arrayBuffer()
let data = new Uint8Array(buff);
let totalBytes = 0;

console.log(data)

if (!window.binding) {
console.log("no binding");
return;
}

const stream = new binding.ZstdDecompressStreamBinding();
const transform = new TransformStream();
const writer = transform.writable.getWriter();

const callback = (decompressed) => {
totalBytes += decompressed.length;
writer.write(decompressed);
}

if (!stream.begin()) {
console.log("stream.begin() error");
return null;
}

console.log("start unzipping");

let i = 0;
const size = 1024*1024*2;
while (size*i < data.length) {
let end = Math.min(size*(i+1), data.length);
let slice = data.slice(size*i, end);

if (!stream.transform(slice, callback)) {
console.log(`stream.transform() error on slice ${size*i} to ${end}`);
return null;
}

i++;
}

if (!stream.end(callback)) {
console.log("stream.end() error");
return null;
}

console.log("finishing unzipping");

const readable = transform.readable;
doFlash(readable, PACKET_SZ, totalBytes);
}

const doFlash = async(readable, size, totalSize) => {
await preBoot();

const reader = readable.getReader();

let offset = 0;
let sofar = new Uint8Array(size);
let i=0;
let totalBytes = 0;

let i_last = Math.floor(totalSize/DATA_SZ);
let last_len = totalSize - Math.floor(totalSize/DATA_SZ)*DATA_SZ;

console.log(i_last, last_len);

let isFirst = true;
let bytesChunk = 0;

reader.read().then(async function processText ({ done, value }) {
// Result objects contain two properties:
// done - true if the stream has already given you all its data.
// value - some data. Always undefined when done is true.

totalBytes += value.length;
console.log(totalBytes);

// Send chunk headers
if (isFirst) {
let send_len;
if (i==i_last) {
send_len = Math.ceil(last_len/BLK_SZ)*BLK_SZ
}
else {
send_len = DATA_SZ;
}
await send_chunk_headers(send_len, i);
console.log("Sent headers", send_len, i)

isFirst = false;
}

while (offset + value.length >= size) {
sofar.set(value.slice(0, size-offset), offset); // Whole packet is ready to send
bytesChunk += size-offset;
await send_packet(sofar);

console.log("bytes", bytesChunk, i);

if (bytesChunk == DATA_SZ) {
await send_flash();
bytesChunk = 0;
i++;
isFirst = true;
}
value = value.slice(size-offset, value.length);
offset = 0;
}

sofar.set(value, offset);
offset += value.length;
bytesChunk += value.length

if (i==i_last && bytesChunk==last_len) {
console.log("reached last send", i, bytesChunk);

// Pad last packet with zeros
let fill_len = Math.ceil(last_len/BLK_SZ)*BLK_SZ - last_len;
sofar.set(new Uint8Array(fill_len), offset);
sofar = sofar.slice(0, offset+fill_len);

console.log(sofar);

await send_packet(sofar);
await send_flash();
}

if (done) {
console.log("stream done")
return;
}
// Read some more, and call this function again
return reader.read().then(processText);
});
}

return [{
requestUSBDevice,
USBDevice,
flashProgress,
flashTotal,
preBoot,
processChunk,
send_chunk_headers,
send_packet,
send_flash,
flash_all,
}]
}

export default useDecompress;

0 comments on commit 08b5fd6

Please sign in to comment.