Run SHA-256 as WebAssembly with JS fallback; note ffmpeg.wasm as deferred

This commit is contained in:
Claude
2026-10-02 11:10:51 +00:00
parent 883c9e7ba0
commit 0d92accd1d
10 changed files with 189 additions and 9 deletions

View File

@@ -1,6 +1,10 @@
/* ============================================================================
* 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
* 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.
@@ -22,7 +26,41 @@
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([
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;
}
// 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) {
if (done) throw new Error('sha256: update() after digest');
const u8 = data instanceof Uint8Array ? data : new Uint8Array(data);
@@ -68,9 +121,10 @@
block.set(u8.subarray(0, take), blockLen);
blockLen += 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; }
return api;
}
@@ -80,12 +134,12 @@
done = true;
const bits = total * 8;
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);
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[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);
for (let i = 0; i < 8; i++) {
@@ -107,6 +161,8 @@
const Sha256 = {
create,
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),
};
if (typeof module !== 'undefined' && module.exports) module.exports = Sha256;