const encoder = new TextEncoder(); const decoder = new TextDecoder(); export const FLUSH_PKT = encoder.encode("0000"); const LENGTH_BYTES = 4; const MAX_PKT_DATA = 65516; export function pktLine(data: string | Uint8Array): Uint8Array { const payload = typeof data === "string" ? encoder.encode(data) : data; if (payload.byteLength > MAX_PKT_DATA) { throw new Error("pkt-line payload too large"); } const header = encoder.encode((payload.byteLength + LENGTH_BYTES).toString(16).padStart(LENGTH_BYTES, "0")); return concatBytes([header, payload]); } export function concatBytes(chunks: Uint8Array[]): Uint8Array { const total = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0); const out = new Uint8Array(total); let offset = 0; for (const chunk of chunks) { out.set(chunk, offset); offset += chunk.byteLength; } return out; } export type Packet = { kind: "data"; data: Uint8Array } | { kind: "flush" } | { kind: "delim" } | { kind: "end" }; export function parsePacket(buffer: Uint8Array, offset: number): { packet: Packet; next: number } | null { if (buffer.byteLength - offset < LENGTH_BYTES) return null; const length = Number.parseInt(decoder.decode(buffer.subarray(offset, offset + LENGTH_BYTES)), 16); if (Number.isNaN(length)) throw new Error("Invalid pkt-line length"); if (length === 0) return { packet: { kind: "flush" }, next: offset + LENGTH_BYTES }; if (length === 1) return { packet: { kind: "delim" }, next: offset + LENGTH_BYTES }; if (length === 2) return { packet: { kind: "end" }, next: offset + LENGTH_BYTES }; if (buffer.byteLength - offset < length) return null; return { packet: { kind: "data", data: buffer.subarray(offset + LENGTH_BYTES, offset + length) }, next: offset + length, }; } export function parseAllPackets(buffer: Uint8Array): Packet[] { const packets: Packet[] = []; let offset = 0; while (offset < buffer.byteLength) { const parsed = parsePacket(buffer, offset); if (!parsed) break; packets.push(parsed.packet); offset = parsed.next; } return packets; } export function packetText(packet: Packet): string | null { return packet.kind === "data" ? decoder.decode(packet.data).replace(/\n$/, "") : null; } /** Reads pkt-lines from a stream until the first flush packet, returning the decoded lines. */ export async function readUntilFlush(stream: ReadableStream, maxBytes: number): Promise { const reader = stream.getReader(); let buffer = new Uint8Array(0); let offset = 0; const lines: string[] = []; try { while (true) { const parsed = parsePacket(buffer, offset); if (parsed) { offset = parsed.next; if (parsed.packet.kind === "flush") return lines; const text = packetText(parsed.packet); if (text !== null) lines.push(text); continue; } const { value, done } = await reader.read(); if (done) return lines; buffer = concatBytes([buffer.subarray(offset), value]); offset = 0; if (buffer.byteLength > maxBytes) throw new Error("Command section too large"); } } finally { await reader.cancel().catch(() => undefined); } } const SIDEBAND_DATA = 1; const SIDEBAND_PROGRESS = 2; export function sidebandPacket(band: number, data: string | Uint8Array): Uint8Array { const payload = typeof data === "string" ? encoder.encode(data) : data; return pktLine(concatBytes([new Uint8Array([band]), payload])); } export function reportStatusResponse( results: Array<{ ref: string; error: string | null }>, useSideband: boolean, message: string | null, ): Uint8Array { const report = concatBytes([ pktLine("unpack ok\n"), ...results.map((result) => pktLine(result.error ? `ng ${result.ref} ${result.error}\n` : `ok ${result.ref}\n`)), FLUSH_PKT, ]); if (!useSideband) return report; const chunks: Uint8Array[] = []; if (message) chunks.push(sidebandPacket(SIDEBAND_PROGRESS, `${message}\n`)); chunks.push(sidebandPacket(SIDEBAND_DATA, report)); chunks.push(FLUSH_PKT); return concatBytes(chunks); }