Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

zstd-codec support for decompressing files #382

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Prev Previous commit
Next Next commit
webusb: add zstd decompress support for wic image
  • Loading branch information
Sarah Wang committed Jul 25, 2023
commit a4c7c4ed2f82e90ee594c78ae5f0ce80ca3b570f
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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's "-" means here?

"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"fs": "^0.0.1-security",
"g": "^2.0.1",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

really need "fs" ?

"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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

supposed, need run browserify before react-script

"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'

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed commented code

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);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avoid log data, it is too big

// 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);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avoid log 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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missed ";"

let data = new Uint8Array(buff);
let totalBytes = 0;

console.log(data)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avoid log data, which just for debug

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");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am little confused here, after you decompress whole data, then trigger read. did you check memory usage here. You can stop here to check how many memory used by browser.


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;