ZDDC/shared/hash.js
2026-06-11 13:32:31 -05:00

94 lines
3.5 KiB
JavaScript

/**
* ZDDC — shared SHA-256 helpers
*
* Attaches to window.zddc.crypto. Must load AFTER shared/zddc.js (which creates
* the window.zddc object).
*
* Exports:
* zddc.crypto.sha256Hex(buffer) → Promise<string> hex digest of ArrayBuffer/Uint8Array
* zddc.crypto.sha256String(str) → Promise<string> hex digest of UTF-8 encoded string
* zddc.crypto.sha256File(file, onProgress?) → Promise<string>
* chunked streaming digest for File/Blob; for files >= 4 MB, streams 2 MB chunks
* and invokes onProgress(loaded, total) every ~8 MB.
* zddc.crypto.bytesToHex(buffer) → string (hex of ArrayBuffer/Uint8Array, no digest)
*
* Throws if Web Crypto SubtleCrypto is not available.
*/
(function (root) {
'use strict';
if (!root.zddc) {
throw new Error('shared/hash.js: window.zddc must be loaded first');
}
var HASH_CHUNK_SIZE = 2 * 1024 * 1024; // 2 MB
function requireSubtle() {
if (!root.crypto || !root.crypto.subtle || typeof root.crypto.subtle.digest !== 'function') {
throw new Error('Web Crypto SubtleCrypto is required');
}
}
function bytesToHex(buffer) {
return Array.from(new Uint8Array(buffer), function (byte) {
return byte.toString(16).padStart(2, '0');
}).join('');
}
async function sha256Hex(buffer) {
requireSubtle();
var input = (buffer instanceof Uint8Array) ? buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength) : buffer;
var hash = await root.crypto.subtle.digest('SHA-256', input);
return bytesToHex(hash);
}
async function sha256String(str) {
requireSubtle();
var bytes = new TextEncoder().encode(str);
var hash = await root.crypto.subtle.digest('SHA-256', bytes);
return bytesToHex(hash);
}
async function sha256File(file, onProgress) {
requireSubtle();
// Single-shot for small files or environments without ReadableStream
if (file.size < HASH_CHUNK_SIZE * 2 || typeof file.stream !== 'function') {
if (onProgress) { onProgress(file.size, file.size); }
var buf = await file.arrayBuffer();
var hash = await root.crypto.subtle.digest('SHA-256', buf);
return bytesToHex(hash);
}
// Chunked streaming for large files
var reader = file.stream().getReader();
var loaded = 0;
var chunks = [];
var yieldCounter = 0;
while (true) {
var result = await reader.read();
if (result.done) { break; }
chunks.push(result.value);
loaded += result.value.byteLength;
yieldCounter++;
if (onProgress && yieldCounter % 4 === 0) {
onProgress(loaded, file.size);
await new Promise(function (r) { setTimeout(r, 0); });
}
}
var total = new Uint8Array(loaded);
var offset = 0;
for (var i = 0; i < chunks.length; i++) {
total.set(chunks[i], offset);
offset += chunks[i].byteLength;
}
var digest = await root.crypto.subtle.digest('SHA-256', total.buffer);
if (onProgress) { onProgress(file.size, file.size); }
return bytesToHex(digest);
}
root.zddc.crypto = {
sha256Hex: sha256Hex,
sha256String: sha256String,
sha256File: sha256File,
bytesToHex: bytesToHex,
};
})(typeof window !== 'undefined' ? window : globalThis);