diff --git a/.changeset/fix-invalid-string-length.md b/.changeset/fix-invalid-string-length.md new file mode 100644 index 0000000..3f696e0 --- /dev/null +++ b/.changeset/fix-invalid-string-length.md @@ -0,0 +1,12 @@ +--- +"@maastrich/hashup": patch +--- + +Fix `Error: Invalid string length` when hashing very large dependency graphs. + +`combineHashes` used `hashes.join("")` to feed the sha256 hasher. With +millions of entries (each a 64-char hash) the joined string exceeds +V8's maximum string length (~512 MB) and the join itself throws. Feed +each hash to the hasher with `update()` instead — sha256 is incremental, +so the output hash is byte-for-byte identical to the old implementation +on inputs that used to succeed. diff --git a/src/lib/combine-hashes.ts b/src/lib/combine-hashes.ts index 661d540..89603a2 100644 --- a/src/lib/combine-hashes.ts +++ b/src/lib/combine-hashes.ts @@ -1,5 +1,9 @@ import { createHash } from "node:crypto"; -export function combineHashes(hashes: string[]): string { - return createHash("sha256").update(hashes.join("")).digest("hex"); +export function combineHashes(hashes: readonly string[]): string { + const hasher = createHash("sha256"); + for (let i = 0; i < hashes.length; i++) { + hasher.update(hashes[i] as string); + } + return hasher.digest("hex"); } diff --git a/tests/combine-hashes.test.ts b/tests/combine-hashes.test.ts new file mode 100644 index 0000000..1923650 --- /dev/null +++ b/tests/combine-hashes.test.ts @@ -0,0 +1,28 @@ +import { createHash } from "node:crypto"; +import { describe, expect, test } from "vite-plus/test"; +import { combineHashes } from "../src/lib/combine-hashes.js"; + +describe("combineHashes", () => { + test("matches the reference sha256(join)", () => { + const hashes = ["a", "b", "c", "deadbeef"]; + const expected = createHash("sha256").update(hashes.join("")).digest("hex"); + + expect(combineHashes(hashes)).toBe(expected); + }); + + test("does not throw `Invalid string length` on very large inputs", () => { + // A single 64-char hash × 10M = 640M chars, past V8's max string length. + // Guards against anyone reintroducing `hashes.join('')`. + const oneHash = "a".repeat(64); + const hashes = new Array(10_000_000).fill(oneHash); + + expect(() => combineHashes(hashes)).not.toThrow(); + }); + + test("reference `hashes.join('')` would throw on the same input", () => { + const oneHash = "a".repeat(64); + const hashes = new Array(10_000_000).fill(oneHash); + + expect(() => hashes.join("")).toThrow(/Invalid string length/); + }); +});