Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

105 lines · 4.8 KBRaw History
1import { gunzipSync, inflateRawSync } from "node:zlib";
2import { describe, expect, it } from "vitest";
3import { crc32, gzip, tar, zip, type ArchiveEntry } from "../../src/lib/archive.js";
4
5const enc = new TextEncoder();
6const dec = new TextDecoder();
7const MTIME = Date.UTC(2024, 4, 6, 7, 8, 10) / 1000;
8
9const entries: ArchiveEntry[] = [
10 { path: "repo-abc1234/", dir: true },
11 { path: "repo-abc1234/README.md", mode: "100644", content: enc.encode("# hello\n".repeat(50)) },
12 { path: "repo-abc1234/bin/run", mode: "100755", content: enc.encode("#!/bin/sh\necho hi\n") },
13 { path: `repo-abc1234/${"deep/".repeat(30)}file.txt`, mode: "100644", content: enc.encode("long path") },
14 { path: "repo-abc1234/link", mode: "120000", content: enc.encode("README.md") },
15];
16
17/** Read a tar archive's entries (ustar plus pax `path` records). */
18function untar(data: Uint8Array): Array<{ name: string; type: string; mode: number; body: string; link: string }> {
19 const out: Array<{ name: string; type: string; mode: number; body: string; link: string }> = [];
20 let off = 0;
21 let paxPath: string | null = null;
22 while (off + 512 <= data.length) {
23 const h = data.subarray(off, off + 512);
24 if (h.every((b) => b === 0)) break;
25 const str = (a: number, n: number) => dec.decode(h.subarray(a, a + n)).replace(/\0.*$/s, "");
26 const size = parseInt(str(124, 12), 8);
27 const type = str(156, 1);
28 const body = data.subarray(off + 512, off + 512 + size);
29 // Header checksum: the sum of all header bytes with the checksum field as spaces.
30 let sum = 0;
31 for (let i = 0; i < 512; i++) sum += i >= 148 && i < 156 ? 32 : h[i]!;
32 expect(parseInt(str(148, 8), 8)).toBe(sum);
33 if (type === "x") {
34 paxPath = /\d+ path=(.*)\n/.exec(dec.decode(body))?.[1] ?? null;
35 } else if (type !== "g") {
36 out.push({ name: paxPath ?? str(0, 100), type, mode: parseInt(str(100, 8), 8), body: dec.decode(body), link: str(157, 100) });
37 paxPath = null;
38 }
39 off += 512 + Math.ceil(size / 512) * 512;
40 }
41 return out;
42}
43
44/** Read a zip archive's central directory and inflate each entry. */
45function unzip(data: Uint8Array): Array<{ name: string; body: string; unixMode: number }> {
46 const v = new DataView(data.buffer, data.byteOffset, data.byteLength);
47 let eocd = data.length - 22;
48 while (eocd >= 0 && v.getUint32(eocd, true) !== 0x06054b50) eocd--;
49 expect(eocd).toBeGreaterThanOrEqual(0);
50 const count = v.getUint16(eocd + 10, true);
51 let p = v.getUint32(eocd + 16, true);
52 const out: Array<{ name: string; body: string; unixMode: number }> = [];
53 for (let i = 0; i < count; i++) {
54 expect(v.getUint32(p, true)).toBe(0x02014b50);
55 const method = v.getUint16(p + 10, true);
56 const crc = v.getUint32(p + 16, true);
57 const csize = v.getUint32(p + 20, true);
58 const nlen = v.getUint16(p + 28, true);
59 const xlen = v.getUint16(p + 30, true);
60 const clen = v.getUint16(p + 32, true);
61 const attrs = v.getUint32(p + 38, true);
62 const local = v.getUint32(p + 42, true);
63 const name = dec.decode(data.subarray(p + 46, p + 46 + nlen));
64 const start = local + 30 + v.getUint16(local + 26, true) + v.getUint16(local + 28, true);
65 const raw = data.subarray(start, start + csize);
66 const body = method === 8 ? new Uint8Array(inflateRawSync(raw)) : raw;
67 expect(crc32(body)).toBe(crc);
68 out.push({ name, body: dec.decode(body), unixMode: attrs >>> 16 });
69 p += 46 + nlen + xlen + clen;
70 }
71 return out;
72}
73
74describe("archives", () => {
75 it("computes the standard CRC-32", () => {
76 expect(crc32(enc.encode("123456789"))).toBe(0xcbf43926);
77 expect(crc32(new Uint8Array(0))).toBe(0);
78 });
79
80 it("writes a tar with modes, symlinks, long names and a commit comment", async () => {
81 const t = tar(entries, MTIME, "abc1234");
82 expect(t.length % 512).toBe(0);
83 const files = untar(t);
84 expect(files.map((f) => f.name)).toEqual(entries.map((e) => ("dir" in e && !e.path.endsWith("/") ? `${e.path}/` : e.path)));
85 expect(files[1]!.body).toBe("# hello\n".repeat(50));
86 // Like `git archive` (tar.umask 0002), which GitHub tarballs come from.
87 expect(files[2]!.mode).toBe(0o775);
88 expect(files[1]!.mode).toBe(0o664);
89 expect(files[3]!.body).toBe("long path");
90 expect(files[4]!.type).toBe("2");
91 expect(files[4]!.link).toBe("README.md");
92 // gzip round trip
93 expect(new Uint8Array(gunzipSync(await gzip(t)))).toEqual(t);
94 });
95
96 it("writes a zip that inflates back to the same files", async () => {
97 const z = await zip(entries, MTIME, "abc1234");
98 const files = unzip(z);
99 expect(files.map((f) => f.name)).toEqual(entries.map((e) => e.path));
100 expect(files[1]!.body).toBe("# hello\n".repeat(50));
101 expect(files[2]!.unixMode & 0o777).toBe(0o775);
102 expect(files[4]!.unixMode & 0o170000).toBe(0o120000);
103 expect(dec.decode(z.subarray(z.length - "abc1234".length))).toBe("abc1234");
104 });
105});