Skip to content
File

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

typescript144 lines
1// Minimal pkt-line encoder/decoder utilities for Git protocol v2
2// See https://git-scm.com/docs/pack-protocol and protocol-v2 docs
3 
4export const FLUSH = "0000"; // flush-pkt
5export const DELIM = "0001"; // delim-pkt
6export const RESPONSE_END = "0002"; // response-end-pkt
7 
8function toHex4(n: number): string {
9 // 4 hex digits, lowercase
10 return n.toString(16).padStart(4, "0");
11}
12 
13// Parse a single pkt-line section and return lines plus the offset after the terminating flush (0000).
14// If parsing fails, returns undefined.
15/**
16 * Parses a section of pkt-line data until a flush packet.
17 * @param buf - Buffer containing pkt-line data
18 * @returns Object with parsed lines and offset after the section, or undefined if invalid
19 */
20export function parsePktSection(buf: Uint8Array): { lines: string[]; offset: number } | undefined {
21 const td = new TextDecoder();
22 let off = 0;
23 const lines: string[] = [];
24 while (off + 4 <= buf.byteLength) {
25 const hdr = td.decode(buf.subarray(off, off + 4));
26 off += 4;
27 if (hdr === FLUSH) {
28 // finished section
29 return { lines, offset: off };
30 }
31 if (hdr === DELIM || hdr === RESPONSE_END) {
32 // For command sections in receive-pack we don't expect delim/response-end, treat as error
33 return undefined;
34 }
35 const len = parseInt(hdr, 16);
36 if (!Number.isFinite(len) || len < 4 || off + (len - 4) > buf.byteLength) {
37 return undefined;
38 }
39 const payLen = len - 4;
40 const payload = buf.subarray(off, off + payLen);
41 off += payLen;
42 lines.push(td.decode(payload).replace(/\r?\n$/, ""));
43 }
44 return undefined;
45}
46 
47/**
48 * Creates a pkt-line formatted packet from a string or byte array.
49 * Prepends a 4-byte hex length prefix to the data.
50 * @param s - Input string or byte array
51 * @returns Pkt-line formatted byte array
52 */
53export function pktLine(s: string | Uint8Array): Uint8Array {
54 const payload = typeof s === "string" ? new TextEncoder().encode(s) : s;
55 const len = payload.byteLength + 4; // length includes the 4-byte header
56 const header = new TextEncoder().encode(toHex4(len));
57 const out = new Uint8Array(header.byteLength + payload.byteLength);
58 out.set(header, 0);
59 out.set(payload, header.byteLength);
60 return out;
61}
62 
63/**
64 * Creates a flush packet (0000) to signal end of a pkt-line stream.
65 * @returns Flush packet byte array
66 */
67export function flushPkt(): Uint8Array {
68 return new TextEncoder().encode(FLUSH);
69}
70 
71/**
72 * Creates a delimiter packet (0001) for protocol v2.
73 * @returns Delimiter packet byte array
74 */
75export function delimPkt(): Uint8Array {
76 return new TextEncoder().encode(DELIM);
77}
78 
79export function responseEndPkt(): Uint8Array {
80 return new TextEncoder().encode(RESPONSE_END);
81}
82 
83/**
84 * Concatenates multiple byte arrays into a single array.
85 * @param chunks - Array of byte arrays to concatenate
86 * @returns Single concatenated byte array
87 */
88export function concatChunks(chunks: Uint8Array[]): Uint8Array {
89 const total = chunks.reduce((acc, c) => acc + c.byteLength, 0);
90 const out = new Uint8Array(total);
91 let offset = 0;
92 for (const c of chunks) {
93 out.set(c, offset);
94 offset += c.byteLength;
95 }
96 return out;
97}
98 
99export type PktItem =
100 | { type: "line"; text: string; raw: Uint8Array }
101 | { type: "flush" }
102 | { type: "delim" }
103 | { type: "response-end" };
104 
105// Decode a pkt-line framed buffer into items. Does not attempt to interpret lines.
106/**
107 * Decodes a buffer containing pkt-line formatted data.
108 * Parses the stream into individual lines, flush, and delimiter packets.
109 * @param buf - Buffer containing pkt-line data
110 * @returns Array of decoded packets with type and content
111 */
112export function decodePktLines(buf: Uint8Array): PktItem[] {
113 const td = new TextDecoder();
114 let off = 0;
115 const out: PktItem[] = [];
116 while (off + 4 <= buf.byteLength) {
117 const hdr = td.decode(buf.subarray(off, off + 4));
118 off += 4;
119 if (hdr === FLUSH) {
120 out.push({ type: "flush" });
121 continue;
122 }
123 if (hdr === DELIM) {
124 out.push({ type: "delim" });
125 continue;
126 }
127 if (hdr === RESPONSE_END) {
128 out.push({ type: "response-end" });
129 continue;
130 }
131 const len = parseInt(hdr, 16);
132 if (!Number.isFinite(len) || len < 4 || off + (len - 4) > buf.byteLength) {
133 // Malformed; stop parsing to avoid infinite loop
134 break;
135 }
136 const payLen = len - 4;
137 const payload = buf.subarray(off, off + payLen);
138 off += payLen;
139 const text = td.decode(payload);
140 out.push({ type: "line", text, raw: payload });
141 }
142 return out;
143}