UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

269 lines • 10.5 kB
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= import { toHex } from "./hex.js"; async function sha256(bytes) { const digest = await crypto.subtle.digest('SHA-256', bytes); return new Uint8Array(digest); } export async function sha256Hex(bytes) { return toHex(await sha256(bytes)); } async function subtleHex(algo, bytes) { const digest = await crypto.subtle.digest(algo, bytes); return toHex(new Uint8Array(digest)); } export async function sha1Hex(bytes) { if (typeof crypto !== 'undefined' && typeof crypto.subtle !== 'undefined') return subtleHex('SHA-1', bytes); return yieldingDigest(new Sha1(), bytes); } /** * The 8-byte SHA-1 length trailer: the message length in bits, big-endian * across 64 bits. Split into high and low words, since the bit length passes * 32 bits at 512 MiB and a 32-bit shift would wrap it. Exported so its test * can cross that boundary without hashing 512 MiB. */ export function sha1LengthTrailer(byteLength) { const out = new Uint8Array(8); const view = new DataView(out.buffer); view.setUint32(0, Math.floor(byteLength / 0x20000000) >>> 0); view.setUint32(4, (byteLength * 8) >>> 0); return out; } /** * Incremental SHA-1, for bytes that arrive in chunks. WebCrypto hashes only * a whole buffer, and a stream cannot be buffered whole to hash at the end: * drains are bounded, so the copy would be unbounded and never cached. The * partial block a chunk leaves is carried into the next `update`; padding * happens only in `digest`. */ export class Sha1 { state = new Uint32Array([ 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0, ]); block = new Uint8Array(64); words = new Uint32Array(80); filled = 0; length = 0; update(chunk) { this.length += chunk.byteLength; let at = 0; if (this.filled > 0) { const take = Math.min(64 - this.filled, chunk.byteLength); this.block.set(chunk.subarray(0, take), this.filled); this.filled += take; at = take; if (this.filled < 64) return; this.compress(this.block, 0); this.filled = 0; } for (; at + 64 <= chunk.byteLength; at += 64) this.compress(chunk, at); if (at < chunk.byteLength) { this.block.set(chunk.subarray(at)); this.filled = chunk.byteLength - at; } } digest() { const pad = (this.filled < 56 ? 56 : 120) - this.filled; const tail = new Uint8Array(this.filled + pad + 8); tail.set(this.block.subarray(0, this.filled)); tail[this.filled] = 0x80; tail.set(sha1LengthTrailer(this.length), tail.length - 8); const state = Uint32Array.from(this.state); for (let off = 0; off < tail.length; off += 64) this.compress(tail, off, state); const out = new Uint8Array(20); const view = new DataView(out.buffer); for (let i = 0; i < 5; i++) view.setUint32(i * 4, state[i] ?? 0); return toHex(out); } compress(bytes, off, state = this.state) { const w = this.words; const view = new DataView(bytes.buffer, bytes.byteOffset + off, 64); for (let i = 0; i < 16; i++) w[i] = view.getUint32(i * 4); for (let i = 16; i < 80; i++) { w[i] = rotl((w[i - 3] ?? 0) ^ (w[i - 8] ?? 0) ^ (w[i - 14] ?? 0) ^ (w[i - 16] ?? 0), 1); } let a = state[0] ?? 0; let b = state[1] ?? 0; let c = state[2] ?? 0; let d = state[3] ?? 0; let e = state[4] ?? 0; for (let i = 0; i < 80; i++) { let f; let k; if (i < 20) { f = (b & c) | (~b & d); k = 0x5a827999; } else if (i < 40) { f = b ^ c ^ d; k = 0x6ed9eba1; } else if (i < 60) { f = (b & c) | (b & d) | (c & d); k = 0x8f1bbcdc; } else { f = b ^ c ^ d; k = 0xca62c1d6; } const t = (rotl(a, 5) + f + e + k + (w[i] ?? 0)) >>> 0; e = d; d = c; c = rotl(b, 30); b = a; a = t; } state[0] = ((state[0] ?? 0) + a) >>> 0; state[1] = ((state[1] ?? 0) + b) >>> 0; state[2] = ((state[2] ?? 0) + c) >>> 0; state[3] = ((state[3] ?? 0) + d) >>> 0; state[4] = ((state[4] ?? 0) + e) >>> 0; } } const S = [ 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, ]; const K = [ 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391, ]; function rotl(x, n) { return ((x << n) | (x >>> (32 - n))) >>> 0; } /** * Incremental MD5, for bytes that arrive in chunks, as `Sha1` is for SHA-1. * Pure JS: WebCrypto has no MD5, and core cannot import node:crypto. The * partial block a chunk leaves is carried into the next `update`; padding * happens only in `digest`. */ export class Md5 { state = new Uint32Array([0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476]); block = new Uint8Array(64); blockView = new DataView(this.block.buffer); words = new Uint32Array(16); filled = 0; length = 0; update(chunk) { this.length += chunk.byteLength; let at = 0; if (this.filled > 0) { const take = Math.min(64 - this.filled, chunk.byteLength); this.block.set(chunk.subarray(0, take), this.filled); this.filled += take; at = take; if (this.filled < 64) return; this.compress(this.blockView, 0); this.filled = 0; } const view = new DataView(chunk.buffer, chunk.byteOffset, chunk.byteLength); for (; at + 64 <= chunk.byteLength; at += 64) this.compress(view, at); if (at < chunk.byteLength) { this.block.set(chunk.subarray(at)); this.filled = chunk.byteLength - at; } } digest() { const pad = (this.filled < 56 ? 56 : 120) - this.filled; const tail = new Uint8Array(this.filled + pad + 8); tail.set(this.block.subarray(0, this.filled)); tail[this.filled] = 0x80; const view = new DataView(tail.buffer); const bits = this.length * 8; view.setUint32(tail.length - 8, bits >>> 0, true); view.setUint32(tail.length - 4, Math.floor(bits / 0x100000000) >>> 0, true); const state = Uint32Array.from(this.state); for (let off = 0; off < tail.length; off += 64) this.compress(view, off, state); const out = new Uint8Array(16); const outView = new DataView(out.buffer); for (let i = 0; i < 4; i++) outView.setUint32(i * 4, state[i] ?? 0, true); return toHex(out); } compress(view, off, state = this.state) { const M = this.words; for (let j = 0; j < 16; j++) M[j] = view.getUint32(off + j * 4, true); let A = state[0] ?? 0; let B = state[1] ?? 0; let C = state[2] ?? 0; let D = state[3] ?? 0; for (let i = 0; i < 64; i++) { let F; let g; if (i < 16) { F = (B & C) | (~B & D); g = i; } else if (i < 32) { F = (D & B) | (~D & C); g = (5 * i + 1) % 16; } else if (i < 48) { F = B ^ C ^ D; g = (3 * i + 5) % 16; } else { F = C ^ (B | ~D); g = (7 * i) % 16; } F = (F + A + (K[i] ?? 0) + (M[g] ?? 0)) >>> 0; A = D; D = C; C = B; B = (B + rotl(F, S[i] ?? 0)) >>> 0; } state[0] = ((state[0] ?? 0) + A) >>> 0; state[1] = ((state[1] ?? 0) + B) >>> 0; state[2] = ((state[2] ?? 0) + C) >>> 0; state[3] = ((state[3] ?? 0) + D) >>> 0; } } export function md5Hex(bytes) { const hash = new Md5(); hash.update(bytes); return hash.digest(); } // The yielding twin of md5Hex, for hashing bytes a caller just received. // One long pure-JS hash would block the loop a TypeScript FUSE mount is // served from, so this hands the loop back about every MiB. export async function md5HexAsync(bytes) { return yieldingDigest(new Md5(), bytes); } async function yieldingDigest(hash, bytes) { const step = 1024 * 1024; for (let offset = 0; offset < bytes.byteLength; offset += step) { hash.update(bytes.subarray(offset, offset + step)); if (offset + step < bytes.byteLength) await new Promise((resolve) => setTimeout(resolve, 0)); } return hash.digest(); } //# sourceMappingURL=hash.js.map