import { createHash, type Hash } from "node:crypto"; import { createReadStream } from "node:fs"; import { open } from "node:fs/promises"; import { HEAD_SIZE } from "./detect.ts"; /** Hashes a byte stream as it passes: the whole content, and its first HEAD_SIZE bytes. */ export class ContentHasher { #full: Hash = createHash("sha256"); #head: Hash | null = createHash("sha256"); #headDigest: string | null = null; #seen = 0; size = 0; update(chunk: Uint8Array): void { this.#full.update(chunk); this.size += chunk.byteLength; if (this.#head) { const take = Math.min(HEAD_SIZE - this.#seen, chunk.byteLength); this.#head.update(chunk.subarray(0, take)); this.#seen += take; if (this.#seen >= HEAD_SIZE) this.#headDigest = this.#finishHead(); } } digest(): { sha256: string; head: string; size: number } { const head = this.#headDigest ?? this.#finishHead(); return { sha256: this.#full.digest("hex"), head, size: this.size }; } #finishHead(): string { const digest = this.#head!.digest("hex"); this.#head = null; return digest; } } export async function hashFile(path: string): Promise<{ sha256: string; head: string; size: number }> { const hasher = new ContentHasher(); for await (const chunk of createReadStream(path)) hasher.update(chunk as Buffer); return hasher.digest(); } export async function headOfFile(path: string): Promise { const handle = await open(path, "r"); try { const buffer = Buffer.alloc(HEAD_SIZE); let filled = 0; while (filled < HEAD_SIZE) { const { bytesRead } = await handle.read(buffer, filled, HEAD_SIZE - filled, filled); if (bytesRead === 0) break; filled += bytesRead; } return createHash("sha256").update(buffer.subarray(0, filled)).digest("hex"); } finally { await handle.close(); } } export function headOfBytes(bytes: Uint8Array): string { return createHash("sha256").update(bytes.subarray(0, HEAD_SIZE)).digest("hex"); }