mirror your GitHub repos to tangled.org automatically
1

Configure Feed

Select the types of activity you want to include in your feed.

at main 5.4 kB View raw
1/** 2 * Git pkt-line framing (protocol v0). A pkt-line is a 4-hex-digit length 3 * prefix (counting the 4 prefix bytes themselves) followed by that many bytes 4 * of payload. `0000` is the flush-pkt: a section delimiter carrying no 5 * payload. Lengths `0001`-`0003` are reserved and invalid in v0. 6 * 7 * See `Documentation/gitprotocol-common.txt` in git.git. 8 */ 9 10const FLUSH = '0000' 11const MAX_DATA = 65516 12 13export const flushPkt: Buffer = Buffer.from(FLUSH, 'ascii') 14 15/** 16 * Frame a payload as a pkt-line. Accepts a string (encoded UTF-8) or raw 17 * bytes. Does NOT append a trailing newline; callers that want the 18 * conventional `\n` (command and capability lines) must include it. 19 */ 20export function encodePktLine(data: string | Buffer): Buffer { 21 const payload = typeof data === 'string' ? Buffer.from(data, 'utf8') : data 22 if (payload.length > MAX_DATA) { 23 throw new RangeError(`pkt-line payload too large: ${payload.length} > ${MAX_DATA}`) 24 } 25 const len = payload.length + 4 26 const prefix = len.toString(16).padStart(4, '0') 27 return Buffer.concat([Buffer.from(prefix, 'ascii'), payload]) 28} 29 30export type PktLine = 31 | { type: 'line', data: Buffer } 32 | { type: 'flush' } 33 34/** 35 * Incrementally decode pkt-lines from a byte source, then hand back whatever 36 * raw bytes follow the section we consumed. 37 * 38 * The git smart protocol switches from pkt-line framing to a raw packfile 39 * stream mid-response (after the NAK/ACK line on a fetch). A naive reader that 40 * buffers ahead would swallow the first chunk of the pack, so this reader 41 * tracks exactly how much it has consumed and exposes the remainder via 42 * `remaining()`. 43 */ 44export class PktLineReader { 45 private buf: Buffer = Buffer.alloc(0) 46 private done = false 47 private readonly iter: AsyncIterator<Buffer> 48 49 constructor(source: AsyncIterable<Buffer | Uint8Array>) { 50 this.iter = (async function* normalise() { 51 for await (const chunk of source) { 52 yield Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk) 53 } 54 })() 55 } 56 57 /** 58 * Read the next pkt-line, or `null` at end of stream. A flush-pkt is 59 * returned as `{ type: 'flush' }` rather than ending iteration; the wire 60 * protocol uses multiple flush-delimited sections per stream. 61 */ 62 async next(): Promise<PktLine | null> { 63 while (this.buf.length < 4) { 64 // eslint-disable-next-line no-await-in-loop -- each fill extends a shared buffer the next iteration inspects; the reads are inherently sequential 65 if (!(await this.fill())) { 66 if (this.buf.length === 0) return null 67 throw new Error('unexpected end of stream: truncated pkt-line length') 68 } 69 } 70 71 const len = Number.parseInt(this.buf.toString('ascii', 0, 4), 16) 72 if (Number.isNaN(len)) { 73 throw new Error(`invalid pkt-line length: ${JSON.stringify(this.buf.toString('ascii', 0, 4))}`) 74 } 75 if (len === 0) { 76 this.buf = this.buf.subarray(4) 77 return { type: 'flush' } 78 } 79 if (len < 4) { 80 throw new Error(`reserved pkt-line length ${len} is invalid in protocol v0`) 81 } 82 83 while (this.buf.length < len) { 84 // eslint-disable-next-line no-await-in-loop -- sequential read; see next() 85 if (!(await this.fill())) { 86 throw new Error(`unexpected end of stream: pkt-line wanted ${len} bytes, had ${this.buf.length}`) 87 } 88 } 89 90 const data = this.buf.subarray(4, len) 91 this.buf = this.buf.subarray(len) 92 return { type: 'line', data } 93 } 94 95 /** 96 * Read pkt-lines up to and including the next flush-pkt, returning the line 97 * payloads (flush excluded). Returns `null` if the stream ends before any 98 * line is read. 99 */ 100 async readUntilFlush(): Promise<Buffer[] | null> { 101 const lines: Buffer[] = [] 102 for (;;) { 103 // eslint-disable-next-line no-await-in-loop -- sequential read; see next() 104 const pkt = await this.next() 105 if (pkt === null) return lines.length > 0 ? lines : null 106 if (pkt.type === 'flush') return lines 107 lines.push(pkt.data) 108 } 109 } 110 111 /** 112 * The bytes already buffered past the last consumed pkt-line. Used to seed 113 * the raw packfile stream once negotiation framing ends. 114 */ 115 buffered(): Buffer { 116 return this.buf 117 } 118 119 /** 120 * Yield the remainder of the source as a raw byte stream: first any bytes 121 * already buffered, then the rest of the underlying iterator verbatim. After 122 * calling this, do not call `next()` again. 123 */ 124 async *remaining(): AsyncGenerator<Buffer> { 125 if (this.buf.length > 0) { 126 yield this.buf 127 this.buf = Buffer.alloc(0) 128 } 129 if (this.done) return 130 for (;;) { 131 // eslint-disable-next-line no-await-in-loop -- sequential drain of the source iterator 132 const { value, done } = await this.iter.next() 133 if (done) { 134 this.done = true 135 return 136 } 137 yield value 138 } 139 } 140 141 private async fill(): Promise<boolean> { 142 if (this.done) return false 143 const { value, done } = await this.iter.next() 144 if (done) { 145 this.done = true 146 return false 147 } 148 this.buf = this.buf.length === 0 ? value : Buffer.concat([this.buf, value]) 149 return true 150 } 151} 152 153/** Decode a single line's payload as a UTF-8 string with any trailing `\n` removed. */ 154export function lineToString(data: Buffer): string { 155 return data.length > 0 && data[data.length - 1] === 0x0A 156 ? data.toString('utf8', 0, data.length - 1) 157 : data.toString('utf8') 158}