| 1 | /** |
| 2 | * `inflateEntry` (src/Git/Protocol/Zlib.ts): the exact-span inflate the pack |
| 3 | * parser runs per entry. Pack entries are back-to-back zlib streams with no |
| 4 | * length prefix, so the consumed-input count is as important as the output. |
| 5 | */ |
| 6 | import { inflateEntry } from "@/Git/Protocol/Zlib.ts"; |
| 7 | import { describe, expect, test } from "alchemy-test"; |
| 8 | import * as Effect from "effect/Effect"; |
| 9 | import zlib from "node:zlib"; |
| 10 | |
| 11 | const content = (n: number, seed: number) => { |
| 12 | const out = new Uint8Array(n); |
| 13 | for (let i = 0; i < n; i++) out[i] = (i * 31 + seed * 7) & 0xff; |
| 14 | return out; |
| 15 | }; |
| 16 | |
| 17 | describe("inflateEntry", { tags: ["unit", "local"] }, () => { |
| 18 | test("returns the content and the exact compressed span, ignoring trailing bytes", async () => { |
| 19 | const a = content(5000, 1); |
| 20 | const b = content(300, 2); |
| 21 | const za = new Uint8Array(zlib.deflateSync(a)); |
| 22 | const zb = new Uint8Array(zlib.deflateSync(b)); |
| 23 | const pack = new Uint8Array(za.length + zb.length + 3); |
| 24 | pack.set(za, 0); |
| 25 | pack.set(zb, za.length); |
| 26 | pack.set([9, 9, 9], za.length + zb.length); |
| 27 | const first = await Effect.runPromise( |
| 28 | inflateEntry(pack, 0, { expectedSize: a.length }), |
| 29 | ); |
| 30 | expect(first.bytesConsumed).toBe(za.length); |
| 31 | expect(Array.from(first.content)).toEqual(Array.from(a)); |
| 32 | const second = await Effect.runPromise( |
| 33 | inflateEntry(pack, first.bytesConsumed, { expectedSize: b.length }), |
| 34 | ); |
| 35 | expect(second.bytesConsumed).toBe(zb.length); |
| 36 | expect(Array.from(second.content)).toEqual(Array.from(b)); |
| 37 | }); |
| 38 | |
| 39 | test("a wrong expectedSize is not trusted from the fast path: the result is still exact", async () => { |
| 40 | const a = content(2000, 3); |
| 41 | const za = new Uint8Array(zlib.deflateSync(a)); |
| 42 | const buf = new Uint8Array(za.length + 5); |
| 43 | buf.set(za, 0); |
| 44 | // The fast paths refuse (output != expectedSize) and the streaming path |
| 45 | // answers; either way the caller gets the real span and content. |
| 46 | const out = await Effect.runPromise( |
| 47 | inflateEntry(buf, 0, { expectedSize: a.length + 1 }), |
| 48 | ); |
| 49 | expect(out.bytesConsumed).toBe(za.length); |
| 50 | expect(out.content.length).toBe(a.length); |
| 51 | }); |
| 52 | |
| 53 | test("an incompressible 1 MiB entry round-trips (multi-chunk output)", async () => { |
| 54 | const big = new Uint8Array(1 << 20); |
| 55 | crypto.getRandomValues(big.subarray(0, 65536)); |
| 56 | for (let at = 65536; at < big.length; at += 65536) |
| 57 | big.copyWithin(at, 0, 65536); |
| 58 | for (let i = 0; i < big.length; i += 4099) big[i] ^= 0x5a; |
| 59 | const z = new Uint8Array(zlib.deflateSync(big)); |
| 60 | const buf = new Uint8Array(z.length + 2); |
| 61 | buf.set(z, 0); |
| 62 | const out = await Effect.runPromise( |
| 63 | inflateEntry(buf, 0, { expectedSize: big.length }), |
| 64 | ); |
| 65 | expect(out.bytesConsumed).toBe(z.length); |
| 66 | expect(out.content.length).toBe(big.length); |
| 67 | expect(out.content[4099]).toBe(big[4099]); |
| 68 | }); |
| 69 | |
| 70 | test("corrupt input fails with a typed ZlibError", async () => { |
| 71 | const junk = new Uint8Array([0x78, 0x9c, 1, 2, 3, 4, 5, 6, 7, 8]); |
| 72 | const result = await Effect.runPromise( |
| 73 | Effect.result(inflateEntry(junk, 0, { expectedSize: 10 })), |
| 74 | ); |
| 75 | expect(result._tag).toBe("Failure"); |
| 76 | }); |
| 77 | }); |