File size: 5,561 Bytes
80d7f0c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
const FRAME_HEADER_LENGTH = 4;
const MAX_UINT32 = 0xffff_ffff;
const PAYLOAD_BLOCK_SIZE = 64 * 1024;

/** Default upper bound for one framed CBOR payload. */
export const DEFAULT_MAX_FRAME_LENGTH = 16 * 1024 * 1024;

export interface FrameDecoderOptions {
	maxFrameLength?: number;
}

export class FrameError extends Error {
	constructor(message: string) {
		super(message);
		this.name = "FrameError";
	}
}

function resolveMaxFrameLength(options: FrameDecoderOptions | undefined): number {
	const value = options?.maxFrameLength ?? DEFAULT_MAX_FRAME_LENGTH;
	if (!Number.isSafeInteger(value) || value < 0 || value > MAX_UINT32) {
		throw new RangeError(`maxFrameLength must be an integer between 0 and ${MAX_UINT32}`);
	}
	return value;
}

/** Prefixes a payload with its unsigned 32-bit big-endian byte length. */
export function encodeFrame(payload: Uint8Array): Uint8Array {
	if (!(payload instanceof Uint8Array)) throw new TypeError("Frame payload must be a Uint8Array");
	if (payload.byteLength > MAX_UINT32) throw new RangeError("Frame payload exceeds the unsigned 32-bit length limit");
	const frame = new Uint8Array(FRAME_HEADER_LENGTH + payload.byteLength);
	const length = payload.byteLength;
	frame[0] = length >>> 24;
	frame[1] = length >>> 16;
	frame[2] = length >>> 8;
	frame[3] = length;
	frame.set(payload, FRAME_HEADER_LENGTH);
	return frame;
}

type DecoderState = "open" | "ended" | "failed";

/** Incrementally splits arbitrary byte chunks into length-prefixed payloads. */
export class FrameDecoder {
	private readonly header = new Uint8Array(FRAME_HEADER_LENGTH);
	private headerLength = 0;
	private readonly maxFrameLength: number;
	private payloadBlocks: Uint8Array[] = [];
	private currentPayloadBlock: Uint8Array | undefined;
	private currentPayloadBlockLength = 0;
	private expectedPayloadLength: number | undefined;
	private payloadLength = 0;
	private state: DecoderState = "open";

	constructor(options?: FrameDecoderOptions) {
		this.maxFrameLength = resolveMaxFrameLength(options);
	}

	push(chunk: Uint8Array): Uint8Array[] {
		if (this.state === "ended") throw new FrameError("Frame decoder has ended");
		if (this.state === "failed") throw new FrameError("Frame decoder has failed");
		if (!(chunk instanceof Uint8Array)) throw new TypeError("Frame chunk must be a Uint8Array");

		const frames: Uint8Array[] = [];
		let chunkOffset = 0;
		while (chunkOffset < chunk.byteLength) {
			if (this.expectedPayloadLength === undefined) {
				const headerBytes = Math.min(FRAME_HEADER_LENGTH - this.headerLength, chunk.byteLength - chunkOffset);
				this.header.set(chunk.subarray(chunkOffset, chunkOffset + headerBytes), this.headerLength);
				this.headerLength += headerBytes;
				chunkOffset += headerBytes;
				if (this.headerLength < FRAME_HEADER_LENGTH) continue;

				const frameLength =
					this.header[0]! * 0x1_000_000 + this.header[1]! * 0x1_0000 + this.header[2]! * 0x100 + this.header[3]!;
				this.headerLength = 0;
				if (frameLength > this.maxFrameLength) {
					this.fail(`Frame length ${frameLength} exceeds configured limit of ${this.maxFrameLength}`);
				}
				if (frameLength === 0) {
					frames.push(new Uint8Array());
					continue;
				}
				this.expectedPayloadLength = frameLength;
				this.payloadBlocks = [];
				this.currentPayloadBlock = undefined;
				this.currentPayloadBlockLength = 0;
				this.payloadLength = 0;
			}

			const expectedPayloadLength = this.expectedPayloadLength;
			if (expectedPayloadLength === undefined) continue;
			while (chunkOffset < chunk.byteLength && this.payloadLength < expectedPayloadLength) {
				let block = this.currentPayloadBlock;
				if (!block || this.currentPayloadBlockLength === block.byteLength) {
					block = new Uint8Array(Math.min(PAYLOAD_BLOCK_SIZE, expectedPayloadLength - this.payloadLength));
					this.payloadBlocks.push(block);
					this.currentPayloadBlock = block;
					this.currentPayloadBlockLength = 0;
				}
				const payloadBytes = Math.min(
					block.byteLength - this.currentPayloadBlockLength,
					chunk.byteLength - chunkOffset,
				);
				block.set(chunk.subarray(chunkOffset, chunkOffset + payloadBytes), this.currentPayloadBlockLength);
				this.currentPayloadBlockLength += payloadBytes;
				this.payloadLength += payloadBytes;
				chunkOffset += payloadBytes;
			}
			if (this.payloadLength === expectedPayloadLength) {
				if (this.payloadBlocks.length === 1) {
					frames.push(this.payloadBlocks[0]!);
				} else {
					const payload = new Uint8Array(expectedPayloadLength);
					let offset = 0;
					for (const payloadBlock of this.payloadBlocks) {
						payload.set(payloadBlock, offset);
						offset += payloadBlock.byteLength;
					}
					frames.push(payload);
				}
				this.payloadBlocks = [];
				this.currentPayloadBlock = undefined;
				this.currentPayloadBlockLength = 0;
				this.expectedPayloadLength = undefined;
				this.payloadLength = 0;
			}
		}
		return frames;
	}

	end(): void {
		if (this.state === "ended") throw new FrameError("Frame decoder has ended");
		if (this.state === "failed") throw new FrameError("Frame decoder has failed");
		if (this.headerLength !== 0 || this.expectedPayloadLength !== undefined) {
			this.fail("Truncated frame at end of stream");
		}
		this.state = "ended";
	}

	private fail(message: string): never {
		this.state = "failed";
		this.headerLength = 0;
		this.payloadBlocks = [];
		this.currentPayloadBlock = undefined;
		this.currentPayloadBlockLength = 0;
		this.expectedPayloadLength = undefined;
		this.payloadLength = 0;
		throw new FrameError(message);
	}
}