Skip to content
File

Blob: test/object-parse.test.ts

typescript124 lines
1import { assert, test } from "vitest";
2import {
3 inflateAndParseHeader,
4 parseCommitRefs,
5 parseTreeChildOids,
6 parseTagTarget,
7} from "@/worker/git/core";
8import { encodeGitObject, type GitObjectType } from "@/worker/git/core/objects";
9import { deflate } from "@/worker/common/compression";
10 
11function hexToBytes(hex: string): Uint8Array {
12 const out = new Uint8Array(20);
13 for (let i = 0; i < 20; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
14 return out;
15}
16 
17type CommitParts = { tree: string; parents?: string[]; message?: string };
18type TagParts = {
19 targetOid: string;
20 targetType?: "commit" | "tree" | "blob" | "tag";
21 tag?: string;
22 tagger?: string;
23 message?: string;
24};
25type BlobParts = { text?: string };
26 
27function textPayloadFor(type: "commit", parts: CommitParts): Uint8Array;
28function textPayloadFor(type: "tag", parts: TagParts): Uint8Array;
29function textPayloadFor(type: "blob" | "tree", parts: BlobParts): Uint8Array;
30function textPayloadFor(
31 type: GitObjectType,
32 parts: CommitParts | TagParts | BlobParts
33): Uint8Array {
34 const enc = new TextEncoder();
35 switch (type) {
36 case "commit": {
37 const { tree, parents = [], message = "" } = parts as CommitParts;
38 const parentLines = parents.map((p) => `parent ${p}\n`).join("");
39 const body = `tree ${tree}\n${parentLines}\n${message}`;
40 return enc.encode(body);
41 }
42 case "tag": {
43 const {
44 targetOid,
45 targetType = "commit",
46 tag = "v1",
47 tagger = "You <you@example.com> 0 +0000",
48 message = "",
49 } = parts as TagParts;
50 const body =
51 `object ${targetOid}\n` +
52 `type ${targetType}\n` +
53 `tag ${tag}\n` +
54 `tagger ${tagger}\n\n${message}`;
55 return enc.encode(body);
56 }
57 default: {
58 const { text = "" } = parts as BlobParts;
59 return enc.encode(text);
60 }
61 }
62}
63 
64// Inflate-and-parse round-trip for blob, commit, tree, tag
65for (const type of ["blob", "commit", "tree", "tag"] as GitObjectType[]) {
66 test(`inflateAndParseHeader parses ${type}`, async () => {
67 const enc = new TextEncoder();
68 let payload: Uint8Array;
69 if (type === "blob") payload = enc.encode("hello");
70 else if (type === "commit") {
71 const tree = "a".repeat(40);
72 const parents = ["b".repeat(40)];
73 payload = textPayloadFor("commit", { tree, parents, message: "m" });
74 } else if (type === "tree") {
75 const oid1 = "b".repeat(40);
76 const oid2 = "c".repeat(40);
77 const e1Name = "file.txt";
78 const e2Name = "dir";
79 const parts: number[] = [];
80 parts.push(...enc.encode("100644 "));
81 parts.push(...enc.encode(e1Name));
82 parts.push(0);
83 parts.push(...hexToBytes(oid1));
84 parts.push(...enc.encode("40000 "));
85 parts.push(...enc.encode(e2Name));
86 parts.push(0);
87 parts.push(...hexToBytes(oid2));
88 payload = new Uint8Array(parts);
89 } else {
90 // tag
91 const targetOid = "d".repeat(40);
92 payload = textPayloadFor("tag", { targetOid, targetType: "commit", message: "m" });
93 }
94 const { zdata } = await encodeGitObject(type, payload);
95 const parsed = await inflateAndParseHeader(zdata);
96 assert.ok(parsed);
97 assert.strictEqual(parsed!.type, type);
98 if (type === "commit") {
99 const refs = parseCommitRefs(parsed!.payload);
100 assert.match(refs.tree || "", /^[0-9a-f]{40}$/);
101 assert.isTrue(Array.isArray(refs.parents));
102 }
103 if (type === "tree") {
104 const kids = parseTreeChildOids(parsed!.payload);
105 assert.isTrue(kids.length >= 2);
106 assert.match(kids[0], /^[0-9a-f]{40}$/);
107 assert.match(kids[1], /^[0-9a-f]{40}$/);
108 }
109 if (type === "tag") {
110 const tag = parseTagTarget(parsed!.payload);
111 assert.ok(tag);
112 assert.match(tag!.targetOid, /^[0-9a-f]{40}$/);
113 assert.strictEqual(typeof tag!.targetType, "string");
114 }
115 });
116}
117 
118// Malformed zdata should return null
119test("inflateAndParseHeader returns null on malformed data", async () => {
120 const z = await deflate(new TextEncoder().encode("garbage-without-header"));
121 const parsed = await inflateAndParseHeader(z);
122 assert.strictEqual(parsed, null);
123});