1/**
2 * Parses an upload-pack result (pkt-line NAK/ACK head, then the pack either
3 * raw or in band-1 sideband frames) and verifies the pack: `PACK` magic,
4 * version 2, and the trailing SHA-1 over everything before it. Shared by
5 * the bench and the unit tests so "bytes received" never stands in for
6 * "a whole pack" (DESIGN §22.4).
7 */
8import { makeSha1 } from "@/Git/Protocol/ObjectCodec.ts";
9
10export const verifyPackResponse = (
11 raw: Uint8Array,
12 sideband: boolean,
13): { readonly objects: number; readonly error?: string } => {
14 const text = (b: Uint8Array) => new TextDecoder().decode(b);
15 let pos = 0;
16 const pieces: Array<Uint8Array> = [];
17 for (;;) {
18 if (pos + 4 > raw.length) break;
19 const four = text(raw.subarray(pos, pos + 4));
20 if (four === "PACK" && !sideband) break; // raw mode: the pack starts here
21 const len = Number.parseInt(four, 16);
22 if (Number.isNaN(len))
23 return { objects: 0, error: `bad pkt length at ${pos}` };
24 if (len === 0) {
25 pos += 4;
26 if (sideband) break; // trailing flush after the last frame
27 continue;
28 }
29 const payload = raw.subarray(pos + 4, pos + len);
30 pos += len;
31 if (sideband) {
32 if (payload[0] === 1) pieces.push(payload.subarray(1));
33 continue; // band 2/3 progress, or a NAK/ACK line before framing
34 }
35 const line = text(payload);
36 if (line.startsWith("NAK") || line.startsWith("ACK")) continue;
37 return {
38 objects: 0,
39 error: `unexpected pkt-line before pack: ${line.slice(0, 40)}`,
40 };
41 }
42 if (!sideband) pieces.push(raw.subarray(pos));
43 return verifyPack(concat(pieces));
44};
45
46/** Verifies a bare pack: magic, version, count, SHA-1 trailer. */
47export const verifyPack = (
48 pack: Uint8Array,
49): { readonly objects: number; readonly error?: string } => {
50 const text = (b: Uint8Array) => new TextDecoder().decode(b);
51 if (pack.length < 32)
52 return { objects: 0, error: `pack too short (${pack.length} bytes)` };
53 if (text(pack.subarray(0, 4)) !== "PACK")
54 return { objects: 0, error: "missing PACK magic" };
55 const view = new DataView(pack.buffer, pack.byteOffset, pack.byteLength);
56 if (view.getUint32(4) !== 2)
57 return { objects: 0, error: `pack version ${view.getUint32(4)}` };
58 const objects = view.getUint32(8);
59 const hash = makeSha1();
60 hash.update(pack.subarray(0, pack.length - 20));
61 const digest = hash.digest();
62 const trailer = pack.subarray(pack.length - 20);
63 for (let i = 0; i < 20; i++) {
64 if (digest[i] !== trailer[i]) {
65 return {
66 objects,
67 error: `trailer SHA-1 mismatch (${pack.length} bytes, ${objects} objects)`,
68 };
69 }
70 }
71 return { objects };
72};
73
74export const concat = (pieces: ReadonlyArray<Uint8Array>): Uint8Array => {
75 if (pieces.length === 1) return pieces[0]!;
76 const total = pieces.reduce((n, p) => n + p.length, 0);
77 const out = new Uint8Array(total);
78 let at = 0;
79 for (const p of pieces) {
80 out.set(p, at);
81 at += p.length;
82 }
83 return out;
84};