|
1 | 1 | import { |
2 | 2 | GitPktLine, |
3 | | -} from "./GitPktLine.mjs" |
| 3 | +} from './GitPktLine.mjs' |
4 | 4 |
|
| 5 | +class GitPktLineDecoder implements Transformer<Uint8Array, GitPktLine> { |
| 6 | + private static readonly SPECIALS_LEN_TBL: Record<number, GitPktLine> = |
| 7 | + GitPktLine.SPECIAL_LINES.reduce((acc: Record<number, GitPktLine>, line) => { |
| 8 | + acc[line.rawLength] = line |
| 9 | + return acc |
| 10 | + }, {}) |
5 | 11 |
|
6 | | -class GitPktLineDecoder implements Transformer<string, GitPktLine> { |
7 | | - private static readonly SPECIALS_RAW_LEN_TBL = GitPktLine.SPECIAL_LINES.reduce((acc: Record<number, GitPktLine>, line) => { |
8 | | - acc[line.rawLength] = line |
9 | | - return acc |
10 | | - }, {}) |
| 12 | + // Internal byte buffer |
| 13 | + private buffer: Uint8Array = new Uint8Array(0) |
| 14 | + private offset = 0 // start index into buffer |
11 | 15 |
|
12 | | - private buffer: string = '' |
| 16 | + public async transform(chunk: Uint8Array, controller: TransformStreamDefaultController<GitPktLine>) { |
| 17 | + this.appendChunk(chunk) |
13 | 18 |
|
14 | | - public async transform(chunk: string, controller: TransformStreamDefaultController) { |
15 | | - this.buffer += chunk |
| 19 | + // Process as many complete pkt-lines as possible |
| 20 | + // We always require at least 4 bytes for the header. |
| 21 | + while (this.availableBytes() >= GitPktLine.HEX_LENGTH) { |
| 22 | + const headerView = this.buffer.subarray(this.offset, this.offset + GitPktLine.HEX_LENGTH) |
| 23 | + const length = GitPktLineDecoder.parseHeaderLength(headerView) |
16 | 24 |
|
17 | | - while ( this.buffer.length >= GitPktLine.HEX_LENGTH ) { |
18 | | - const hexLengthStr = this.buffer.slice(0, GitPktLine.HEX_LENGTH) |
19 | | - const pktLineLength = parseInt(hexLengthStr, 16) |
20 | | - |
21 | | - if ( pktLineLength > GitPktLine.MAX_RAW_LENGTH ) { |
22 | | - throw new Error(`Invalid packet: Length \`${pktLineLength}\` exceeds the maximum limit`) |
| 25 | + // Enforce application max early |
| 26 | + if (length > GitPktLine.MAX_RAW_LENGTH) { |
| 27 | + throw new Error(`Invalid packet: length ${length} exceeds maximum limit ${GitPktLine.MAX_RAW_LENGTH}`) |
23 | 28 | } |
24 | 29 |
|
25 | | - if ( pktLineLength < GitPktLine.HEX_LENGTH ) { |
26 | | - const pktLine = GitPktLineDecoder.SPECIALS_RAW_LEN_TBL[pktLineLength] |
27 | | - if ( pktLine ) { |
28 | | - controller.enqueue(pktLine) |
29 | | - } else { |
30 | | - throw new Error("Invalid pkt line length") |
| 30 | + // Control packets: 0000, 0001, 0002 |
| 31 | + if (length < GitPktLine.HEX_LENGTH) { |
| 32 | + const special = GitPktLineDecoder.SPECIALS_LEN_TBL[length] |
| 33 | + if (!special) { |
| 34 | + throw new Error(`Invalid pkt-line control length: ${length}`) |
31 | 35 | } |
32 | 36 |
|
33 | | - this.buffer = this.buffer.slice(GitPktLine.HEX_LENGTH) |
| 37 | + // Control packets are just the 4-byte header; no payload, no newline. |
| 38 | + this.consumeBytes(GitPktLine.HEX_LENGTH) |
| 39 | + controller.enqueue(special) |
34 | 40 | continue |
35 | 41 | } |
36 | 42 |
|
37 | | - if ( this.buffer.length < pktLineLength ) { |
| 43 | + // Data packets: need the full frame before we can decode |
| 44 | + if (this.availableBytes() < length) { |
| 45 | + // Wait for more data |
38 | 46 | break |
39 | 47 | } |
40 | 48 |
|
41 | | - const end = this.buffer.charAt(pktLineLength - 1) === '\n' ? pktLineLength - 1 : pktLineLength |
42 | | - controller.enqueue(new GitPktLine({ |
43 | | - content: this.buffer.slice(GitPktLine.HEX_LENGTH, end), |
44 | | - rawLine: this.buffer.slice(0, pktLineLength), |
45 | | - })) |
46 | | - this.buffer = this.buffer.slice(pktLineLength) |
| 49 | + const raw = this.buffer.subarray(this.offset, this.offset + length) |
| 50 | + |
| 51 | + // Let GitPktLine do full validation (newline, protocol max, etc.) |
| 52 | + const pkt = GitPktLine.fromRaw(raw, /* precompile */ true) |
| 53 | + controller.enqueue(pkt) |
| 54 | + |
| 55 | + this.consumeBytes(length) |
| 56 | + } |
| 57 | + } |
| 58 | + |
| 59 | + // ---- Buffer management ---- |
| 60 | + |
| 61 | + private availableBytes(): number { |
| 62 | + return this.buffer.length - this.offset |
| 63 | + } |
| 64 | + |
| 65 | + private appendChunk(chunk: Uint8Array): void { |
| 66 | + if (chunk.length === 0) return |
| 67 | + |
| 68 | + if (this.buffer.length === 0) { |
| 69 | + // Fast path: no existing data |
| 70 | + this.buffer = chunk |
| 71 | + this.offset = 0 |
| 72 | + return |
| 73 | + } |
| 74 | + |
| 75 | + // Compact if we've consumed a lot |
| 76 | + if (this.offset > 0) { |
| 77 | + const remaining = this.buffer.subarray(this.offset) |
| 78 | + const merged = new Uint8Array(remaining.length + chunk.length) |
| 79 | + merged.set(remaining, 0) |
| 80 | + merged.set(chunk, remaining.length) |
| 81 | + this.buffer = merged |
| 82 | + this.offset = 0 |
| 83 | + } else { |
| 84 | + // No consumed prefix; just append |
| 85 | + const merged = new Uint8Array(this.buffer.length + chunk.length) |
| 86 | + merged.set(this.buffer, 0) |
| 87 | + merged.set(chunk, this.buffer.length) |
| 88 | + this.buffer = merged |
47 | 89 | } |
48 | 90 | } |
| 91 | + |
| 92 | + private consumeBytes(count: number): void { |
| 93 | + this.offset += count |
| 94 | + if (this.offset >= this.buffer.length) { |
| 95 | + // Fully consumed; reset |
| 96 | + this.buffer = new Uint8Array(0) |
| 97 | + this.offset = 0 |
| 98 | + } |
| 99 | + } |
| 100 | + |
| 101 | + // ---- Header parsing ---- |
| 102 | + |
| 103 | + private static parseHeaderLength(header: Uint8Array): number { |
| 104 | + if (header.length !== GitPktLine.HEX_LENGTH) { |
| 105 | + throw new Error('Pkt-line header must be exactly 4 bytes') |
| 106 | + } |
| 107 | + |
| 108 | + let hex = '' |
| 109 | + for (let i = 0; i < GitPktLine.HEX_LENGTH; i++) { |
| 110 | + hex += String.fromCharCode(header[i]) |
| 111 | + } |
| 112 | + |
| 113 | + if (!/^[0-9a-fA-F]{4}$/.test(hex)) { |
| 114 | + throw new Error(`Invalid pkt-line length header: "${hex}"`) |
| 115 | + } |
| 116 | + |
| 117 | + const length = parseInt(hex, 16) |
| 118 | + if (!Number.isFinite(length) || length < 0 || length > 0xffff) { |
| 119 | + throw new Error(`Invalid pkt-line length value: ${length}`) |
| 120 | + } |
| 121 | + |
| 122 | + return length |
| 123 | + } |
49 | 124 | } |
50 | 125 |
|
51 | | -class GitPktLineDecoderStream extends TransformStream<string, GitPktLine> { |
52 | | - constructor(_?: unknown, writableStrategy?: QueuingStrategy<string>, readableStrategy?: QueuingStrategy<GitPktLine>) { |
| 126 | +class GitPktLineDecoderStream extends TransformStream<Uint8Array, GitPktLine> { |
| 127 | + constructor( |
| 128 | + _?: unknown, |
| 129 | + writableStrategy?: QueuingStrategy<Uint8Array>, |
| 130 | + readableStrategy?: QueuingStrategy<GitPktLine>, |
| 131 | + ) { |
53 | 132 | super(new GitPktLineDecoder(), writableStrategy, readableStrategy) |
54 | 133 | } |
55 | 134 | } |
56 | 135 |
|
57 | | - |
58 | 136 | export { |
59 | 137 | GitPktLineDecoder, |
60 | 138 | GitPktLineDecoderStream, |
|
0 commit comments