Skip to content
File

Blob: src/worker/git/core/objects.ts

typescript157 lines
1/**
2 * Git object encoding/decoding utilities
3 */
4 
5import { deflate } from "@/worker/common/compression";
6import { bytesToHex } from "@/worker/common/hex";
7import { asBufferSource, createDigestStream } from "@/worker/common/webtypes";
8 
9/**
10 * Git object types
11 */
12export type GitObjectType = "blob" | "tree" | "commit" | "tag";
13 
14const textEncoder = new TextEncoder();
15 
16/**
17 * Map Git object type to pack type code
18 */
19export function objTypeCode(type: GitObjectType): number {
20 switch (type) {
21 case "commit":
22 return 1;
23 case "tree":
24 return 2;
25 case "blob":
26 return 3;
27 case "tag":
28 return 4;
29 default:
30 throw new Error(`Unknown object type: ${type}`);
31 }
32}
33 
34/**
35 * Encode Git pack object header
36 * @param type - Object type code (1=commit, 2=tree, 3=blob, 4=tag)
37 * @param size - Object size in bytes
38 * @returns Encoded header bytes
39 */
40export function encodeObjHeader(type: number, size: number): Uint8Array {
41 let first = (type << 4) | (size & 0x0f);
42 size >>= 4;
43 const bytes: number[] = [];
44 if (size > 0) first |= 0x80;
45 bytes.push(first);
46 while (size > 0) {
47 let b = size & 0x7f;
48 size >>= 7;
49 if (size > 0) b |= 0x80;
50 bytes.push(b);
51 }
52 return new Uint8Array(bytes);
53}
54 
55function buildRawGitObject(type: GitObjectType, payload: Uint8Array): Uint8Array {
56 const header = encodeGitObjectHeader(type, payload.byteLength);
57 const raw = new Uint8Array(header.byteLength + payload.byteLength);
58 raw.set(header, 0);
59 raw.set(payload, header.byteLength);
60 return raw;
61}
62 
63function encodeGitObjectHeader(type: GitObjectType, payloadLength: number): Uint8Array {
64 return textEncoder.encode(`${type} ${payloadLength}\0`);
65}
66 
67/**
68 * Compute the raw 20-byte SHA-1 digest for a Git object payload.
69 * This avoids a hex roundtrip on hot paths that already store OIDs in binary.
70 */
71export async function computeOidBytes(
72 type: GitObjectType,
73 payload: Uint8Array
74): Promise<Uint8Array> {
75 const digestStream = createDigestStream("SHA-1");
76 const digestWriter = digestStream.getWriter();
77 await digestWriter.write(encodeGitObjectHeader(type, payload.byteLength));
78 await digestWriter.write(payload);
79 await digestWriter.close();
80 return new Uint8Array(await digestStream.digest);
81}
82 
83async function computeOidBytesFromRaw(raw: Uint8Array): Promise<Uint8Array> {
84 return new Uint8Array(await crypto.subtle.digest("SHA-1", asBufferSource(raw)));
85}
86 
87/**
88 * Create a Git object with header and compute its OID
89 * @param type - Git object type
90 * @param payload - Object content
91 * @returns Object ID and raw bytes
92 */
93export async function createGitObject(
94 type: GitObjectType,
95 payload: Uint8Array
96): Promise<{ oid: string; raw: Uint8Array }> {
97 const raw = buildRawGitObject(type, payload);
98 const oid = bytesToHex(await computeOidBytesFromRaw(raw));
99 return { oid, raw };
100}
101 
102/**
103 * Create and compress a Git object
104 * @param type - Git object type
105 * @param payload - Object content
106 * @returns Object ID, raw bytes, and compressed bytes
107 */
108export async function encodeGitObject(
109 type: GitObjectType,
110 payload: Uint8Array
111): Promise<{ oid: string; raw: Uint8Array; zdata: Uint8Array }> {
112 const { oid, raw } = await createGitObject(type, payload);
113 const zdata = await deflate(raw);
114 return { oid, raw, zdata };
115}
116 
117/**
118 * Parse a Git object to extract type and payload
119 * @param raw - Raw Git object bytes (with header)
120 * @returns Object type (GitObjectType) and payload
121 */
122export function parseGitObject(raw: Uint8Array): { type: GitObjectType; payload: Uint8Array } {
123 // Find the null byte that separates header from content
124 let nullIndex = -1;
125 for (let i = 0; i < raw.length; i++) {
126 if (raw[i] === 0) {
127 nullIndex = i;
128 break;
129 }
130 }
131 
132 if (nullIndex === -1) {
133 throw new Error("Invalid Git object: no null byte found");
134 }
135 
136 const header = new TextDecoder().decode(raw.subarray(0, nullIndex));
137 const [typeStr] = header.split(" ");
138 const payload = raw.subarray(nullIndex + 1);
139 
140 if (typeStr !== "commit" && typeStr !== "tree" && typeStr !== "blob" && typeStr !== "tag") {
141 throw new Error(`Invalid Git object type: ${typeStr}`);
142 }
143 const type = typeStr as GitObjectType;
144 
145 return { type, payload };
146}
147 
148/**
149 * Compute the OID of a Git object from its type and payload
150 * @param type - Git object type
151 * @param payload - Object content
152 * @returns SHA-1 hash as hex string
153 */
154export async function computeOid(type: GitObjectType, payload: Uint8Array): Promise<string> {
155 return bytesToHex(await computeOidBytes(type, payload));
156}