Skip to content
File

Blob: test/util/git-pack.ts

typescript205 lines
1import { asBufferSource, deflate, hexToBytes, zeroOid } from "@/worker/common";
2import type { GitObjectType } from "@/worker/git";
3import { concatChunks, encodeObjHeader, encodeOfsDeltaDistance, objTypeCode } from "@/worker/git";
4 
5type PackObjectEntry = {
6 type: GitObjectType;
7 payload: Uint8Array;
8};
9 
10type OfsDeltaEntry = {
11 type: "ofs-delta";
12 baseIndex: number;
13 delta: Uint8Array;
14};
15 
16type RefDeltaEntry = {
17 type: "ref-delta";
18 baseOid: string;
19 delta: Uint8Array;
20};
21 
22type PackBuildEntry = PackObjectEntry | OfsDeltaEntry | RefDeltaEntry;
23 
24function encodeDeltaVarint(value: number): Uint8Array {
25 if (!Number.isInteger(value) || value < 0) {
26 throw new Error(`invalid delta varint value: ${value}`);
27 }
28 const out: number[] = [];
29 do {
30 let byte = value & 0x7f;
31 value >>>= 7;
32 if (value > 0) byte |= 0x80;
33 out.push(byte);
34 } while (value > 0);
35 return Uint8Array.from(out);
36}
37 
38function encodeDeltaCopy(offset: number, size: number): Uint8Array {
39 if (!Number.isInteger(offset) || offset < 0) {
40 throw new Error(`invalid delta copy offset: ${offset}`);
41 }
42 if (!Number.isInteger(size) || size <= 0 || size > 0x10000) {
43 throw new Error(`invalid delta copy size: ${size}`);
44 }
45 
46 let opcode = 0x80;
47 const bytes: number[] = [];
48 
49 if (offset & 0xff) {
50 opcode |= 0x01;
51 bytes.push(offset & 0xff);
52 }
53 if (offset & 0xff00) {
54 opcode |= 0x02;
55 bytes.push((offset >>> 8) & 0xff);
56 }
57 if (offset & 0xff0000) {
58 opcode |= 0x04;
59 bytes.push((offset >>> 16) & 0xff);
60 }
61 if (offset & 0xff000000) {
62 opcode |= 0x08;
63 bytes.push((offset >>> 24) & 0xff);
64 }
65 
66 if (size !== 0x10000) {
67 if (size & 0xff) {
68 opcode |= 0x10;
69 bytes.push(size & 0xff);
70 }
71 if (size & 0xff00) {
72 opcode |= 0x20;
73 bytes.push((size >>> 8) & 0xff);
74 }
75 if (size & 0xff0000) {
76 opcode |= 0x40;
77 bytes.push((size >>> 16) & 0xff);
78 }
79 }
80 
81 return Uint8Array.from([opcode, ...bytes]);
82}
83 
84/**
85 * Build a simple append-only git delta. The result is the full base payload
86 * followed by the provided suffix bytes.
87 */
88export function buildAppendOnlyDelta(base: Uint8Array, suffix: Uint8Array): Uint8Array {
89 const parts: Uint8Array[] = [
90 encodeDeltaVarint(base.length),
91 encodeDeltaVarint(base.length + suffix.length),
92 ];
93 
94 if (base.length > 0) parts.push(encodeDeltaCopy(0, base.length));
95 
96 for (let offset = 0; offset < suffix.length; offset += 0x7f) {
97 const chunk = suffix.subarray(offset, offset + 0x7f);
98 parts.push(Uint8Array.from([chunk.length]));
99 parts.push(chunk);
100 }
101 
102 return concatChunks(parts);
103}
104 
105/**
106 * Build a delta whose result is the leading `prefixLength` bytes of `base`.
107 */
108export function buildCopyPrefixDelta(base: Uint8Array, prefixLength: number): Uint8Array {
109 if (!Number.isInteger(prefixLength) || prefixLength < 0 || prefixLength > base.length) {
110 throw new Error(`invalid delta prefix length: ${prefixLength}`);
111 }
112 
113 const parts: Uint8Array[] = [encodeDeltaVarint(base.length), encodeDeltaVarint(prefixLength)];
114 if (prefixLength > 0) parts.push(encodeDeltaCopy(0, prefixLength));
115 return concatChunks(parts);
116}
117 
118/**
119 * Build a Git pack file from objects, including delta entries used by pack-only tests.
120 */
121export async function buildPack(objects: PackBuildEntry[]): Promise<Uint8Array> {
122 const header = new Uint8Array(12);
123 header.set(new TextEncoder().encode("PACK"), 0);
124 const view = new DataView(header.buffer);
125 view.setUint32(4, 2);
126 view.setUint32(8, objects.length);
127 
128 const parts: Uint8Array[] = [header];
129 const entryOffsets: number[] = [];
130 let bodyLength = header.byteLength;
131 
132 for (const object of objects) {
133 const entryOffset = bodyLength;
134 let objectHeader: Uint8Array;
135 let objectPayload: Uint8Array;
136 
137 if (object.type === "ofs-delta") {
138 const baseOffset = entryOffsets[object.baseIndex];
139 if (baseOffset === undefined) {
140 throw new Error(`missing OFS_DELTA base index: ${object.baseIndex}`);
141 }
142 objectHeader = concatChunks([
143 encodeObjHeader(6, object.delta.byteLength),
144 encodeOfsDeltaDistance(entryOffset - baseOffset),
145 ]);
146 objectPayload = await deflate(object.delta);
147 } else if (object.type === "ref-delta") {
148 objectHeader = concatChunks([
149 encodeObjHeader(7, object.delta.byteLength),
150 hexToBytes(object.baseOid),
151 ]);
152 objectPayload = await deflate(object.delta);
153 } else {
154 objectHeader = encodeObjHeader(objTypeCode(object.type), object.payload.byteLength);
155 objectPayload = await deflate(object.payload);
156 }
157 
158 parts.push(objectHeader);
159 parts.push(objectPayload);
160 entryOffsets.push(entryOffset);
161 bodyLength += objectHeader.byteLength + objectPayload.byteLength;
162 }
163 
164 const body = concatChunks(parts);
165 const sha = new Uint8Array(await crypto.subtle.digest("SHA-1", asBufferSource(body)));
166 const out = new Uint8Array(body.byteLength + 20);
167 out.set(body, 0);
168 out.set(sha, body.byteLength);
169 return out;
170}
171 
172export async function makeCommit(treeOid: string, msg: string) {
173 const author = `You <you@example.com> 0 +0000`;
174 const payload = new TextEncoder().encode(
175 `tree ${treeOid}\n` + `author ${author}\n` + `committer ${author}\n\n${msg}`
176 );
177 const header = new TextEncoder().encode(`commit ${payload.byteLength}\0`);
178 const raw = new Uint8Array(header.length + payload.length);
179 raw.set(header, 0);
180 raw.set(payload, header.length);
181 const hash = await crypto.subtle.digest("SHA-1", raw);
182 const oid = Array.from(new Uint8Array(hash))
183 .map((byte) => byte.toString(16).padStart(2, "0"))
184 .join("");
185 return { oid, payload };
186}
187 
188export async function makeTree(): Promise<{ oid: string; payload: Uint8Array }> {
189 const payload = new Uint8Array(0);
190 const header = new TextEncoder().encode(`tree ${payload.byteLength}\0`);
191 const raw = new Uint8Array(header.length + payload.length);
192 raw.set(header, 0);
193 raw.set(payload, header.length);
194 const oid = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-1", raw)))
195 .map((byte) => byte.toString(16).padStart(2, "0"))
196 .join("");
197 return { oid, payload };
198}
199 
200export function zero40(): string {
201 return zeroOid();
202}
203 
204export { encodeObjHeader } from "@/worker/git";