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);
}
}
|