Run SHA-256 as WebAssembly with JS fallback; note ffmpeg.wasm as deferred
This commit is contained in:
11
CLAUDE.md
11
CLAUDE.md
@@ -36,7 +36,7 @@ JSON shapes mirror the Tauri Rust bridge exactly — don't change one side alone
|
|||||||
sends the `File` itself); cover images go to `PUT /api/admin/uploads/:id/art`. The multipart
|
sends the `File` itself); cover images go to `PUT /api/admin/uploads/:id/art`. The multipart
|
||||||
`POST /api/admin/uploads` parses the whole body in memory — scripts with small files only.
|
`POST /api/admin/uploads` parses the whole body in memory — scripts with small files only.
|
||||||
- Videos over `transcode.maxSeconds` are never re-encoded (lane would be tied up for hours).
|
- Videos over `transcode.maxSeconds` are never re-encoded (lane would be tied up for hours).
|
||||||
- Deferred ideas (Cloudflare Worker fetch, Android device download): `docs/deferred-ideas.md`.
|
- Deferred ideas (Cloudflare Worker fetch, Android device download, ffmpeg.wasm client editing): `docs/deferred-ideas.md`.
|
||||||
|
|
||||||
## Save to device + "On other devices"
|
## Save to device + "On other devices"
|
||||||
- `exportToDevice(video)` (app.js, helpers in `frontend/export.js`): puts the file in Photos / Files / Downloads.
|
- `exportToDevice(video)` (app.js, helpers in `frontend/export.js`): puts the file in Photos / Files / Downloads.
|
||||||
@@ -76,6 +76,15 @@ JSON shapes mirror the Tauri Rust bridge exactly — don't change one side alone
|
|||||||
`ignoreSearch` against the plain-URL precache. A new build = new URLs, so old ones simply get evicted.
|
`ignoreSearch` against the plain-URL precache. A new build = new URLs, so old ones simply get evicted.
|
||||||
`/fonts` and `/icons` are cached 30 days.
|
`/fonts` and `/icons` are cached 30 days.
|
||||||
|
|
||||||
|
## SHA-256 in WebAssembly (`frontend/sha256.js`)
|
||||||
|
- `Sha256.create()` runs its block function as WebAssembly (`frontend/wasm/sha256.c` → `scripts/build-sha256-wasm.sh`
|
||||||
|
→ generated `frontend/sha256-wasm.js`, base64, ~2 KB) and falls back to pure JS if WebAssembly is missing, the CSP
|
||||||
|
refuses it, or the load-time self-test ("abc") fails. ~150 MB/s vs ~65 MB/s in JS (Node/Chromium; phones are slower
|
||||||
|
but the ratio holds). It is synchronous on purpose (callers aren't async) — fine because the module is under 4 KB.
|
||||||
|
- Needs `'wasm-unsafe-eval'` in index.html's CSP `script-src`. Workers pull the blob in with `importScripts('/sha256-wasm.js')`
|
||||||
|
from inside sha256.js; the file is in the SW `SHELL` list. After editing the C file, re-run the build script and commit both.
|
||||||
|
`Sha256.engine()` says which one is active; `create({js:true})` forces JS (tests).
|
||||||
|
|
||||||
## Server media cache (`server/media-cache.js`)
|
## Server media cache (`server/media-cache.js`)
|
||||||
- Every played (`/api/streams`, LOW priority, ≤ `MEDIA_AUTO_MAX_SECONDS`, default 3 h) or saved
|
- Every played (`/api/streams`, LOW priority, ≤ `MEDIA_AUTO_MAX_SECONDS`, default 3 h) or saved
|
||||||
(`/api/download`, HIGH, ≤ 3 h) video gets ONE copy: `$MEDIA_DIR/<id>.<gen>.mp4`
|
(`/api/download`, HIGH, ≤ 3 h) video gets ONE copy: `$MEDIA_DIR/<id>.<gen>.mp4`
|
||||||
|
|||||||
@@ -32,3 +32,13 @@ yt-dlp (or an equivalent extractor) on the phone's own IP, checks the server
|
|||||||
cache first, then uploads the finished file in the background through the
|
cache first, then uploads the finished file in the background through the
|
||||||
device intake (`POST /api/p2p/intake`, `server/p2p-intake.js`). The server's
|
device intake (`POST /api/p2p/intake`, `server/p2p-intake.js`). The server's
|
||||||
yt-dlp stays as the fallback when the device fails.
|
yt-dlp stays as the fallback when the device fails.
|
||||||
|
|
||||||
|
## Client-side video editing with ffmpeg.wasm (deferred 2026-10-02)
|
||||||
|
|
||||||
|
Skipped on purpose. ffmpeg.wasm is about 25–30 MB, and fast (multi-threaded)
|
||||||
|
use needs cross-origin isolation (COOP/COEP). That would break the YouTube
|
||||||
|
thumbnails and avatars unless every image host sends CORP headers, and we do not
|
||||||
|
control `i.ytimg.com` / `ggpht.com`. The server already trims video (edit & download
|
||||||
|
runs ffmpeg there), so the browser gains nothing it needs. Revisit only if offline
|
||||||
|
editing becomes a requirement; then load it lazily from a worker and measure the
|
||||||
|
isolation fallout first.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* thread, read in 4 MiB slices (never the whole video in memory).
|
* thread, read in 4 MiB slices (never the whole video in memory).
|
||||||
*
|
*
|
||||||
* In: { name } file name under OPFS videos/ (e.g. "abc.mp4")
|
* In: { name } file name under OPFS videos/ (e.g. "abc.mp4")
|
||||||
* Out: { ok: true, sha256, size } | { ok: false, error }
|
^ * Out: { ok: true, sha256, size, engine } | { ok: false, error }
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
'use strict';
|
'use strict';
|
||||||
importScripts('/sha256.js');
|
importScripts('/sha256.js');
|
||||||
@@ -19,7 +19,7 @@ self.onmessage = async (e) => {
|
|||||||
for (let pos = 0; pos < file.size; pos += STEP) {
|
for (let pos = 0; pos < file.size; pos += STEP) {
|
||||||
h.update(new Uint8Array(await file.slice(pos, pos + STEP).arrayBuffer()));
|
h.update(new Uint8Array(await file.slice(pos, pos + STEP).arrayBuffer()));
|
||||||
}
|
}
|
||||||
self.postMessage({ ok: true, sha256: h.hex(), size: file.size });
|
self.postMessage({ ok: true, sha256: h.hex(), size: file.size, engine: self.Sha256.engine() });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
self.postMessage({ ok: false, error: err && err.message ? err.message : String(err) });
|
self.postMessage({ ok: false, error: err && err.message ? err.message : String(err) });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<meta name="apple-mobile-web-app-title" content="YT Player" />
|
<meta name="apple-mobile-web-app-title" content="YT Player" />
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; img-src 'self' https: data: asset: http://asset.localhost blob:; media-src 'self' https: blob: asset: http://asset.localhost; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:; script-src 'self'; connect-src 'self' https: ipc: http://ipc.localhost blob:; worker-src 'self';"
|
content="default-src 'self'; img-src 'self' https: data: asset: http://asset.localhost blob:; media-src 'self' https: blob: asset: http://asset.localhost; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:; script-src 'self' 'wasm-unsafe-eval'; connect-src 'self' https: ipc: http://ipc.localhost blob:; worker-src 'self';"
|
||||||
/>
|
/>
|
||||||
<title>YT Player</title>
|
<title>YT Player</title>
|
||||||
<link rel="manifest" href="/manifest.webmanifest" />
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
@@ -622,6 +622,7 @@
|
|||||||
<script src="flag-ui.js"></script>
|
<script src="flag-ui.js"></script>
|
||||||
<script src="export.js"></script>
|
<script src="export.js"></script>
|
||||||
<script src="stats-core.js"></script>
|
<script src="stats-core.js"></script>
|
||||||
|
<script src="sha256-wasm.js"></script>
|
||||||
<script src="sha256.js"></script>
|
<script src="sha256.js"></script>
|
||||||
<script src="device-db.js"></script>
|
<script src="device-db.js"></script>
|
||||||
<script src="p2p-client.js"></script>
|
<script src="p2p-client.js"></script>
|
||||||
|
|||||||
7
frontend/sha256-wasm.js
Normal file
7
frontend/sha256-wasm.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/* GENERATED by scripts/build-sha256-wasm.sh from frontend/wasm/sha256.c — do not edit.
|
||||||
|
* WebAssembly SHA-256 block compression, embedded as base64 so it loads with the
|
||||||
|
* page / worker that already loads sha256.js (no extra request, no MIME or SW change). */
|
||||||
|
(function (root) {
|
||||||
|
root.SHA256_WASM_B64 = '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';
|
||||||
|
if (typeof module !== 'undefined' && module.exports) module.exports = root.SHA256_WASM_B64;
|
||||||
|
})(typeof globalThis !== 'undefined' ? globalThis : this);
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
/* ============================================================================
|
/* ============================================================================
|
||||||
* sha256.js — incremental SHA-256 (pure JS; window.Sha256 / worker / node)
|
* sha256.js — incremental SHA-256 (pure JS; window.Sha256 / worker / node)
|
||||||
*
|
*
|
||||||
|
* The block function runs as WebAssembly (wasm/sha256.c → sha256-wasm.js) when the
|
||||||
|
* browser allows it, with the pure-JS version below as the fallback — a failed
|
||||||
|
* load or a failed self-test silently keeps hashing in JS.
|
||||||
|
*
|
||||||
* WebCrypto's digest() needs the whole input at once, which would pull a
|
* WebCrypto's digest() needs the whole input at once, which would pull a
|
||||||
* multi-hundred-MB video into memory. This hasher takes chunks as they stream
|
* multi-hundred-MB video into memory. This hasher takes chunks as they stream
|
||||||
* past (downloads, peer transfers, OPFS reads) and keeps ~100 bytes of state.
|
* past (downloads, peer transfers, OPFS reads) and keeps ~100 bytes of state.
|
||||||
@@ -22,7 +26,41 @@
|
|||||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function create() {
|
// ---- WebAssembly engine (optional) -------------------------------------
|
||||||
|
// Memory layout shared with wasm/sha256.c: state at STATE, input blocks at DATA.
|
||||||
|
const STATE = 16384, DATA = 65536, CHUNK = 1 << 20; // 1 MiB staging window
|
||||||
|
const IV = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19];
|
||||||
|
let wasm; // undefined = not tried yet, null = unavailable, else { compress, state, data }
|
||||||
|
function loadWasm() {
|
||||||
|
if (wasm !== undefined) return wasm;
|
||||||
|
wasm = null;
|
||||||
|
try {
|
||||||
|
if (typeof WebAssembly === 'undefined') return wasm;
|
||||||
|
if (!root.SHA256_WASM_B64 && typeof importScripts === 'function') importScripts('/sha256-wasm.js');
|
||||||
|
let b64 = root.SHA256_WASM_B64;
|
||||||
|
if (!b64 && typeof require === 'function') b64 = require('./sha256-wasm.js');
|
||||||
|
if (!b64) return wasm;
|
||||||
|
const bin = typeof Buffer !== 'undefined' ? Uint8Array.from(Buffer.from(b64, 'base64'))
|
||||||
|
: Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
||||||
|
const memory = new WebAssembly.Memory({ initial: (DATA + CHUNK) / 65536, maximum: (DATA + CHUNK) / 65536 });
|
||||||
|
const inst = new WebAssembly.Instance(new WebAssembly.Module(bin), { env: { memory } });
|
||||||
|
const e = {
|
||||||
|
compress: inst.exports.compress,
|
||||||
|
state: new Uint32Array(memory.buffer, STATE, 8),
|
||||||
|
data: new Uint8Array(memory.buffer, DATA, CHUNK),
|
||||||
|
};
|
||||||
|
// Self-test: one padded block of "abc" must give the well-known digest.
|
||||||
|
e.state.set(IV);
|
||||||
|
e.data.fill(0, 0, 64); e.data.set([0x61, 0x62, 0x63, 0x80], 0); e.data[63] = 24;
|
||||||
|
e.compress(STATE, DATA, 1);
|
||||||
|
if (e.state[0] !== 0xba7816bf || e.state[7] !== 0xf20015ad) return wasm;
|
||||||
|
wasm = e;
|
||||||
|
} catch { wasm = null; }
|
||||||
|
return wasm;
|
||||||
|
}
|
||||||
|
|
||||||
|
function create(opts) {
|
||||||
|
const useWasm = !(opts && opts.js) && loadWasm();
|
||||||
const H = new Uint32Array([
|
const H = new Uint32Array([
|
||||||
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
|
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
|
||||||
]);
|
]);
|
||||||
@@ -58,6 +96,21 @@
|
|||||||
H[4] = (H[4] + e) | 0; H[5] = (H[5] + f) | 0; H[6] = (H[6] + g) | 0; H[7] = (H[7] + h) | 0;
|
H[4] = (H[4] + e) | 0; H[5] = (H[5] + f) | 0; H[6] = (H[6] + g) | 0; H[7] = (H[7] + h) | 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hash n whole 64-byte blocks of buf starting at off — wasm in 1 MiB windows, else JS.
|
||||||
|
function blocks(buf, off, n) {
|
||||||
|
if (!useWasm) { for (let k = 0; k < n; k++) compress(buf, off + k * 64); return; }
|
||||||
|
const w = useWasm;
|
||||||
|
w.state.set(H);
|
||||||
|
const per = CHUNK / 64;
|
||||||
|
for (let done = 0; done < n;) {
|
||||||
|
const m = Math.min(per, n - done);
|
||||||
|
w.data.set(buf.subarray(off + done * 64, off + (done + m) * 64));
|
||||||
|
w.compress(STATE, DATA, m);
|
||||||
|
done += m;
|
||||||
|
}
|
||||||
|
H.set(w.state);
|
||||||
|
}
|
||||||
|
|
||||||
function update(data) {
|
function update(data) {
|
||||||
if (done) throw new Error('sha256: update() after digest');
|
if (done) throw new Error('sha256: update() after digest');
|
||||||
const u8 = data instanceof Uint8Array ? data : new Uint8Array(data);
|
const u8 = data instanceof Uint8Array ? data : new Uint8Array(data);
|
||||||
@@ -68,9 +121,10 @@
|
|||||||
block.set(u8.subarray(0, take), blockLen);
|
block.set(u8.subarray(0, take), blockLen);
|
||||||
blockLen += take;
|
blockLen += take;
|
||||||
i = take;
|
i = take;
|
||||||
if (blockLen === 64) { compress(block, 0); blockLen = 0; }
|
if (blockLen === 64) { blocks(block, 0, 1); blockLen = 0; }
|
||||||
}
|
}
|
||||||
for (; i + 64 <= u8.length; i += 64) compress(u8, i);
|
const whole = Math.floor((u8.length - i) / 64);
|
||||||
|
if (whole) { blocks(u8, i, whole); i += whole * 64; }
|
||||||
if (i < u8.length) { block.set(u8.subarray(i), 0); blockLen = u8.length - i; }
|
if (i < u8.length) { block.set(u8.subarray(i), 0); blockLen = u8.length - i; }
|
||||||
return api;
|
return api;
|
||||||
}
|
}
|
||||||
@@ -80,12 +134,12 @@
|
|||||||
done = true;
|
done = true;
|
||||||
const bits = total * 8;
|
const bits = total * 8;
|
||||||
block[blockLen++] = 0x80;
|
block[blockLen++] = 0x80;
|
||||||
if (blockLen > 56) { block.fill(0, blockLen); compress(block, 0); blockLen = 0; }
|
if (blockLen > 56) { block.fill(0, blockLen); blocks(block, 0, 1); blockLen = 0; }
|
||||||
block.fill(0, blockLen, 56);
|
block.fill(0, blockLen, 56);
|
||||||
const hi = Math.floor(bits / 0x100000000), lo = bits >>> 0;
|
const hi = Math.floor(bits / 0x100000000), lo = bits >>> 0;
|
||||||
block[56] = hi >>> 24; block[57] = hi >>> 16; block[58] = hi >>> 8; block[59] = hi;
|
block[56] = hi >>> 24; block[57] = hi >>> 16; block[58] = hi >>> 8; block[59] = hi;
|
||||||
block[60] = lo >>> 24; block[61] = lo >>> 16; block[62] = lo >>> 8; block[63] = lo;
|
block[60] = lo >>> 24; block[61] = lo >>> 16; block[62] = lo >>> 8; block[63] = lo;
|
||||||
compress(block, 0);
|
blocks(block, 0, 1);
|
||||||
}
|
}
|
||||||
const out = new Uint8Array(32);
|
const out = new Uint8Array(32);
|
||||||
for (let i = 0; i < 8; i++) {
|
for (let i = 0; i < 8; i++) {
|
||||||
@@ -107,6 +161,8 @@
|
|||||||
const Sha256 = {
|
const Sha256 = {
|
||||||
create,
|
create,
|
||||||
hex: (data) => create().update(data).hex(),
|
hex: (data) => create().update(data).hex(),
|
||||||
|
// Which engine create() will use: 'wasm' or 'js'.
|
||||||
|
engine: () => (loadWasm() ? 'wasm' : 'js'),
|
||||||
isHex: (s) => typeof s === 'string' && /^[0-9a-f]{64}$/.test(s),
|
isHex: (s) => typeof s === 'string' && /^[0-9a-f]{64}$/.test(s),
|
||||||
};
|
};
|
||||||
if (typeof module !== 'undefined' && module.exports) module.exports = Sha256;
|
if (typeof module !== 'undefined' && module.exports) module.exports = Sha256;
|
||||||
|
|||||||
@@ -43,3 +43,32 @@ test('update after digest throws; isHex', () => {
|
|||||||
assert.ok(Sha256.isHex(ref(Buffer.from('x'))));
|
assert.ok(Sha256.isHex(ref(Buffer.from('x'))));
|
||||||
assert.ok(!Sha256.isHex('ABC'));
|
assert.ok(!Sha256.isHex('ABC'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- WebAssembly engine ----
|
||||||
|
test('wasm engine is available in Node and matches the JS engine and node:crypto', () => {
|
||||||
|
const crypto = require('node:crypto');
|
||||||
|
assert.equal(Sha256.engine(), 'wasm');
|
||||||
|
const sizes = [0, 1, 55, 56, 63, 64, 65, 1000, (1 << 20) - 1, 1 << 20, (1 << 20) + 1, 3_000_001];
|
||||||
|
for (const n of sizes) {
|
||||||
|
const buf = Buffer.alloc(n).map((_, i) => (i * 7 + n) & 255);
|
||||||
|
const want = crypto.createHash('sha256').update(buf).digest('hex');
|
||||||
|
assert.equal(Sha256.create().update(buf).hex(), want, `wasm ${n}`);
|
||||||
|
assert.equal(Sha256.create({ js: true }).update(buf).hex(), want, `js ${n}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('wasm engine hashes streamed odd-sized chunks like one-shot', () => {
|
||||||
|
const crypto = require('node:crypto');
|
||||||
|
const buf = Buffer.alloc(2_500_003).map((_, i) => (i * 13) & 255);
|
||||||
|
const h = Sha256.create();
|
||||||
|
for (let o = 0; o < buf.length; o += 77_777) h.update(buf.subarray(o, o + 77_777));
|
||||||
|
assert.equal(h.hex(), crypto.createHash('sha256').update(buf).digest('hex'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('falls back to the JS engine when WebAssembly is unavailable', () => {
|
||||||
|
const { execFileSync } = require('node:child_process');
|
||||||
|
const out = execFileSync(process.execPath, ['-e',
|
||||||
|
"delete globalThis.WebAssembly; const S=require('./frontend/sha256.js');" +
|
||||||
|
"console.log(S.engine(), S.hex(new TextEncoder().encode('abc')))"], { cwd: require('node:path').join(__dirname, '..') }).toString().trim();
|
||||||
|
assert.equal(out, 'js ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad');
|
||||||
|
});
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ const SHELL = [
|
|||||||
'/lyrics-core.js',
|
'/lyrics-core.js',
|
||||||
'/stats-core.js',
|
'/stats-core.js',
|
||||||
'/sha256.js',
|
'/sha256.js',
|
||||||
|
'/sha256-wasm.js',
|
||||||
'/device-db.js',
|
'/device-db.js',
|
||||||
'/hash-worker.js',
|
'/hash-worker.js',
|
||||||
'/p2p-client.js',
|
'/p2p-client.js',
|
||||||
|
|||||||
46
frontend/wasm/sha256.c
Normal file
46
frontend/wasm/sha256.c
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// SHA-256 block compression for frontend/sha256.js (built to WebAssembly).
|
||||||
|
// No libc, no allocation: JS owns the memory layout.
|
||||||
|
// state (8 x u32) at STATE
|
||||||
|
// input (N x 64 B) at DATA
|
||||||
|
// Build: scripts/build-sha256-wasm.sh (writes frontend/sha256-wasm.js)
|
||||||
|
typedef unsigned int u32;
|
||||||
|
typedef unsigned char u8;
|
||||||
|
|
||||||
|
static const u32 K[64] = {
|
||||||
|
0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
|
||||||
|
0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
|
||||||
|
0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
|
||||||
|
0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
|
||||||
|
0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
|
||||||
|
0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
|
||||||
|
0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
|
||||||
|
0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2 };
|
||||||
|
|
||||||
|
#define ROR(x,n) (((x) >> (n)) | ((x) << (32 - (n))))
|
||||||
|
|
||||||
|
__attribute__((export_name("compress")))
|
||||||
|
void compress(u32 *state, const u8 *data, u32 blocks) {
|
||||||
|
u32 W[64];
|
||||||
|
while (blocks--) {
|
||||||
|
for (int i = 0; i < 16; i++, data += 4)
|
||||||
|
W[i] = ((u32)data[0] << 24) | ((u32)data[1] << 16) | ((u32)data[2] << 8) | data[3];
|
||||||
|
for (int i = 16; i < 64; i++) {
|
||||||
|
u32 s0 = ROR(W[i-15],7) ^ ROR(W[i-15],18) ^ (W[i-15] >> 3);
|
||||||
|
u32 s1 = ROR(W[i-2],17) ^ ROR(W[i-2],19) ^ (W[i-2] >> 10);
|
||||||
|
W[i] = W[i-16] + s0 + W[i-7] + s1;
|
||||||
|
}
|
||||||
|
u32 a=state[0],b=state[1],c=state[2],d=state[3],e=state[4],f=state[5],g=state[6],h=state[7];
|
||||||
|
// 8 rounds per iteration with the variables renamed instead of shuffled.
|
||||||
|
#define RND(a,b,c,d,e,f,g,h,i) do { \
|
||||||
|
u32 t1 = h + (ROR(e,6) ^ ROR(e,11) ^ ROR(e,25)) + (g ^ (e & (f ^ g))) + K[i] + W[i]; \
|
||||||
|
u32 t2 = (ROR(a,2) ^ ROR(a,13) ^ ROR(a,22)) + ((a & b) | (c & (a | b))); \
|
||||||
|
d += t1; h = t1 + t2; } while (0)
|
||||||
|
for (int i = 0; i < 64; i += 8) {
|
||||||
|
RND(a,b,c,d,e,f,g,h,i); RND(h,a,b,c,d,e,f,g,i+1);
|
||||||
|
RND(g,h,a,b,c,d,e,f,i+2); RND(f,g,h,a,b,c,d,e,i+3);
|
||||||
|
RND(e,f,g,h,a,b,c,d,i+4); RND(d,e,f,g,h,a,b,c,i+5);
|
||||||
|
RND(c,d,e,f,g,h,a,b,i+6); RND(b,c,d,e,f,g,h,a,i+7);
|
||||||
|
}
|
||||||
|
state[0]+=a; state[1]+=b; state[2]+=c; state[3]+=d; state[4]+=e; state[5]+=f; state[6]+=g; state[7]+=h;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
scripts/build-sha256-wasm.sh
Executable file
21
scripts/build-sha256-wasm.sh
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Rebuild frontend/sha256-wasm.js (the base64-embedded WebAssembly SHA-256 block
|
||||||
|
# function) from frontend/wasm/sha256.c. Needs clang with wasm32 + wasm-ld.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
|
||||||
|
clang --target=wasm32 -O3 -nostdlib -fno-builtin -ffreestanding \
|
||||||
|
-Wl,--no-entry -Wl,--export=compress -Wl,--import-memory -Wl,--initial-memory=65536 -Wl,--max-memory=1114112 \
|
||||||
|
-Wl,-z,stack-size=4096 -Wl,--strip-all \
|
||||||
|
frontend/wasm/sha256.c -o "$tmp/sha256.wasm"
|
||||||
|
b64=$(base64 -w0 "$tmp/sha256.wasm")
|
||||||
|
cat > frontend/sha256-wasm.js <<JS
|
||||||
|
/* GENERATED by scripts/build-sha256-wasm.sh from frontend/wasm/sha256.c — do not edit.
|
||||||
|
* WebAssembly SHA-256 block compression, embedded as base64 so it loads with the
|
||||||
|
* page / worker that already loads sha256.js (no extra request, no MIME or SW change). */
|
||||||
|
(function (root) {
|
||||||
|
root.SHA256_WASM_B64 = '$b64';
|
||||||
|
if (typeof module !== 'undefined' && module.exports) module.exports = root.SHA256_WASM_B64;
|
||||||
|
})(typeof globalThis !== 'undefined' ? globalThis : this);
|
||||||
|
JS
|
||||||
|
echo "wasm: $(wc -c < "$tmp/sha256.wasm") bytes"
|
||||||
Reference in New Issue
Block a user