| 1 | import { createHash } from "node:crypto"; |
| 2 | import { describe, expect, it } from "vitest"; |
| 3 | import { Sha1, concat, deflate, packStream, parsePack, utf8, writePack, type PackEntryInput } from "../src/index.js"; |
| 4 | |
| 5 | describe("streamed packs", () => { |
| 6 | it("incremental SHA-1 matches Node's for any split of the input", () => { |
| 7 | const data = new Uint8Array(1000).map((_, i) => (i * 31 + 7) & 0xff); |
| 8 | for (const len of [0, 1, 55, 56, 63, 64, 65, 119, 120, 1000]) { |
| 9 | const want = createHash("sha1").update(data.subarray(0, len)).digest(); |
| 10 | for (const step of [1, 7, 64, 1000]) { |
| 11 | const h = new Sha1(); |
| 12 | for (let i = 0; i < len; i += step) h.update(data.subarray(i, Math.min(len, i + step))); |
| 13 | expect(Buffer.from(h.digest()).equals(want)).toBe(true); |
| 14 | } |
| 15 | } |
| 16 | }); |
| 17 | |
| 18 | it("is byte for byte the pack writePack builds, and reads one entry at a time", async () => { |
| 19 | const entries: PackEntryInput[] = Array.from({ length: 50 }, (_, i) => { |
| 20 | const content = utf8(`blob ${i}\n`.repeat(i * 100 + 1)); |
| 21 | return { type: "blob", size: content.length, deflated: deflate(content) }; |
| 22 | }); |
| 23 | let pulled = 0; |
| 24 | async function* source() { |
| 25 | for (const e of entries) { |
| 26 | pulled++; |
| 27 | yield e; |
| 28 | } |
| 29 | } |
| 30 | const parts: Uint8Array[] = []; |
| 31 | let mostAhead = 0; |
| 32 | for await (const part of packStream(entries.length, source())) { |
| 33 | parts.push(part); |
| 34 | // Each entry is two parts (its header, then its data): the stream never runs ahead of its reader. |
| 35 | mostAhead = Math.max(mostAhead, pulled - Math.floor(parts.length / 2)); |
| 36 | } |
| 37 | expect(mostAhead).toBeLessThanOrEqual(1); |
| 38 | const streamed = concat(parts); |
| 39 | expect(streamed).toEqual(await writePack(entries)); |
| 40 | expect(await parsePack(streamed)).toHaveLength(50); |
| 41 | }); |
| 42 | |
| 43 | it("refuses a source that doesn't match the announced count", async () => { |
| 44 | const one: PackEntryInput = { type: "blob", size: 1, deflated: deflate(utf8("x")) }; |
| 45 | const run = async (n: number, items: PackEntryInput[]) => { |
| 46 | for await (const _ of packStream(n, (async function* () { yield* items; })())) void _; |
| 47 | }; |
| 48 | await expect(run(2, [one])).rejects.toThrow(/announced 2 objects but has 1/); |
| 49 | await expect(run(1, [one, one])).rejects.toThrow(/more than the 1 objects/); |
| 50 | }); |
| 51 | }); |