/** * 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 hex digest of ArrayBuffer/Uint8Array * zddc.crypto.sha256String(str) → Promise hex digest of UTF-8 encoded string * zddc.crypto.sha256File(file, onProgress?) → Promise * 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);