import { unzipSync } from "fflate"; import { describe, expect, it } from "vitest"; import { buildManifest, buildArchiveContentManifest, collectSelection, compareArchiveContentManifests, compareManifests, deterministicZip, normalizePath, parseManifest, serializeManifest, serializeArchiveContentManifest, parseArchiveContentManifest, } from "../../src/core/repro"; import { createProvenanceStatement, createReproEvidence, inspectSbomDocuments, } from "../../src/core/evidence"; function files() { const first = new File(["alpha"], "a.txt"); const second = new File(["beta"], "b.txt"); return [ { file: second, path: "b.txt" }, { file: first, path: "a.txt" }, ]; } function ownedBuffer(input: Uint8Array): ArrayBuffer { const owned = new Uint8Array(input.byteLength); owned.set(input); return owned.buffer; } describe("reproducible manifests", () => { it("hashes in deterministic path order", async () => { const manifest = await buildManifest(files(), ["SHA-512", "SHA-256"]); expect(manifest.files.map((entry) => entry.path)).toEqual([ "a.txt", "b.txt", ]); expect(manifest.operation.algorithms).toEqual(["SHA-256", "SHA-512"]); expect(manifest.operation.recordedAt).toBeNull(); expect(parseManifest(serializeManifest(manifest))).toEqual(manifest); }); it("reports changed, missing and unexpected files", async () => { const reference = await buildManifest(files(), ["SHA-256"]); const actual = await buildManifest( [ { file: new File(["changed"], "a.txt"), path: "a.txt" }, { file: new File(["new"], "c.txt"), path: "c.txt" }, ], ["SHA-256"], ); expect( compareManifests(reference, actual).map((item) => item.status), ).toEqual(["changed", "missing", "unexpected"]); }); it("creates byte-identical sorted ZIP files", async () => { const manifest = await buildManifest(files(), ["SHA-256"]); const first = await deterministicZip(files(), manifest); const second = await deterministicZip(files(), manifest); expect(first).toEqual(second); expect(Object.keys(unzipSync(first))).toEqual([ "REPRODUCIBILITY.json", "a.txt", "b.txt", ]); }); it("rejects unsafe and duplicate paths", () => { expect(() => normalizePath("../secret")).toThrow(/Unsafe/u); expect(() => collectSelection( [new File(["a"], "same"), new File(["b"], "same")], false, ), ).toThrow(/Duplicate/u); }); it("rejects ZIP and untrusted-manifest path collisions", async () => { await expect( deterministicZip([ { file: new File(["a"], "same"), path: "same" }, { file: new File(["b"], "same"), path: "same" }, ]), ).rejects.toThrow(/Duplicate/u); const manifest = await buildManifest(files(), ["SHA-256"]); await expect( deterministicZip( [ { file: new File(["user data"], "REPRODUCIBILITY.json"), path: "REPRODUCIBILITY.json", }, ], manifest, ), ).rejects.toThrow(/reserved/u); const duplicateManifest = { ...manifest, files: [manifest.files[0]!, manifest.files[0]!], }; expect(() => parseManifest(JSON.stringify(duplicateManifest))).toThrow( /duplicate path/u, ); }); it("validates manifest metadata, totals and digests", async () => { const manifest = await buildManifest(files(), ["SHA-256"]); expect(() => parseManifest(JSON.stringify({ ...manifest, totalBytes: 999 })), ).toThrow(/totalBytes/u); expect(() => parseManifest( JSON.stringify({ ...manifest, files: [ { ...manifest.files[0], digests: { "sha-256": "not-a-hash" } }, manifest.files[1], ], }), ), ).toThrow(/invalid digests/u); expect(() => normalizePath("a".repeat(4_097))).toThrow(/4096/u); }); it("streams bounded ZIP contents into comparable content manifests", async () => { const firstBytes = await deterministicZip([ { path: "nested/a.txt", file: new File(["alpha"], "a.txt") }, { path: "b.txt", file: new File(["beta"], "b.txt") }, ]); const secondBytes = await deterministicZip([ { path: "nested/a.txt", file: new File(["changed"], "a.txt") }, { path: "c.txt", file: new File(["new"], "c.txt") }, ]); const first = await buildArchiveContentManifest( new File([ownedBuffer(firstBytes)], "first.zip"), ["SHA-256"], ); const second = await buildArchiveContentManifest( new File([ownedBuffer(secondBytes)], "second.zip"), ["SHA-256"], ); expect(first.files.map((entry) => entry.path)).toEqual([ "b.txt", "nested/a.txt", ]); expect(first.files[0]?.crc32).toMatch(/^[0-9a-f]{8}$/u); expect( parseArchiveContentManifest(serializeArchiveContentManifest(first)), ).toEqual(first); expect( compareArchiveContentManifests(first, second).map((item) => item.status), ).toEqual(["missing", "unexpected", "changed"]); }); it("recognizes SBOM evidence and makes a non-conformance provenance statement", async () => { const selected = [ { path: "bom.cdx.json", file: new File( [ JSON.stringify({ bomFormat: "CycloneDX", specVersion: "1.6", serialNumber: "urn:uuid:test", metadata: { component: { name: "demo" } }, components: [{ name: "library" }], dependencies: [{ ref: "demo" }], }), ], "bom.cdx.json", ), }, ]; const manifest = await buildManifest(selected, ["SHA-256"]); const materials = await inspectSbomDocuments(selected); expect(materials[0]).toMatchObject({ status: "recognized", format: "CycloneDX JSON", specificationVersion: "1.6", componentCount: 1, }); const provenance = createProvenanceStatement(manifest, materials); expect(provenance.predicate.metadata.conformanceClaim).toBeNull(); expect(provenance.predicate.materials).toEqual(materials); expect(createReproEvidence(manifest, materials)).toMatchObject({ contractVersion: 1, provenance: { execution: "local-browser", networkRequired: false }, }); }); });