import type { Commit, GitObject, Identity, ObjectType, Tag, TreeEntry } from "./types.ts"; export async function inflate(data: Uint8Array): Promise { const stream = new Blob([data as BlobPart]).stream().pipeThrough(new DecompressionStream("deflate")); return new Uint8Array(await new Response(stream).arrayBuffer()); } const utf8 = new TextDecoder(); export function toHex(bytes: Uint8Array): string { return Array.from(bytes, b => b.toString(16).padStart(2, "0")).join(""); } export function oidBytes(oid: string): Uint8Array { if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(oid)) throw new Error(`invalid object id ${oid}`); return new Uint8Array(oid.match(/../g)!.map(byte => parseInt(byte, 16))); } /** ` \0`, as stored zlib-compressed in `objects/xx/` */ export function parseLoose(raw: Uint8Array): GitObject { const nul = raw.indexOf(0); if (nul === -1) throw new Error("malformed loose object"); const [type, size] = utf8.decode(raw.subarray(0, nul)).split(" "); const data = raw.subarray(nul + 1); if (data.length !== Number(size)) throw new Error("loose object size mismatch"); return { type: type as ObjectType, data }; } function parseIdentity(line: string): Identity { const lt = line.indexOf(" <"); const gt = line.indexOf("> ", lt); const [time, tz] = line.slice(gt + 2).split(" "); return { name: line.slice(0, lt), email: line.slice(lt + 2, gt), time: Number(time), tz: tz ?? "+0000", }; } export function parseCommit(oid: string, data: Uint8Array): Commit { const text = utf8.decode(data); const blank = text.indexOf("\n\n"); const head = blank === -1 ? text : text.slice(0, blank); const message = blank === -1 ? "" : text.slice(blank + 2); const commit: Commit = { oid, tree: "", parents: [], author: { name: "", email: "", time: 0, tz: "+0000" }, committer: { name: "", email: "", time: 0, tz: "+0000" }, message, }; // continuation lines (multi-line headers like gpgsig) start with a space for (const line of head.split("\n").filter(l => !l.startsWith(" "))) { const sp = line.indexOf(" "); const key = line.slice(0, sp); const value = line.slice(sp + 1); if (key === "tree") commit.tree = value; else if (key === "parent") commit.parents.push(value); else if (key === "author") commit.author = parseIdentity(value); else if (key === "committer") commit.committer = parseIdentity(value); else if (key === "change-id") commit.changeId = value; } return commit; } /** entries of ` \0` */ export function parseTree(data: Uint8Array, hashBytes: number): TreeEntry[] { if (hashBytes !== 20 && hashBytes !== 32) throw new Error("unsupported object format"); const entries: TreeEntry[] = []; let pos = 0; while (pos < data.length) { const sp = data.indexOf(0x20, pos); if (sp === -1) throw new Error("malformed tree object"); const nul = data.indexOf(0, sp); if (nul === -1 || nul + 1 + hashBytes > data.length) throw new Error("malformed tree object"); entries.push({ mode: parseInt(utf8.decode(data.subarray(pos, sp)), 8), name: utf8.decode(data.subarray(sp + 1, nul)), oid: toHex(data.subarray(nul + 1, nul + 1 + hashBytes)), }); pos = nul + 1 + hashBytes; } return entries; } export function parseTag(data: Uint8Array): Tag { const text = utf8.decode(data); const blank = text.indexOf("\n\n"); const tag: Tag = { object: "", targetType: "commit", name: "", message: text.slice(blank + 2) }; for (const line of text.slice(0, blank).split("\n")) { const sp = line.indexOf(" "); const [key, value] = [line.slice(0, sp), line.slice(sp + 1)]; if (key === "object") tag.object = value; else if (key === "type") tag.targetType = value as ObjectType; else if (key === "tag") tag.name = value; } return tag; }