| // Licensed to the Apache Software Foundation (ASF) under one | |
| // or more contributor license agreements. See the NOTICE file | |
| // distributed with this work for additional information | |
| // regarding copyright ownership. The ASF licenses this file | |
| // to you under the Apache License, Version 2.0 (the | |
| // "License"); you may not use this file except in compliance | |
| // with the License. You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, | |
| // software distributed under the License is distributed on an | |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| // KIND, either express or implied. See the License for the | |
| // specific language governing permissions and limitations | |
| // under the License. | |
| import { | |
| HEADER_SIZE, | |
| readSize | |
| } from '../wire/vsr/header.js'; | |
| export class ProtocolFrameError extends Error { | |
| constructor(message: string) { | |
| super(message); | |
| this.name = 'ProtocolFrameError'; | |
| } | |
| } | |
| export type ExtractedFrames = { | |
| frames: Buffer[], | |
| remainder: Buffer | |
| }; | |
| const declaredFrameSize = ( | |
| header: Buffer, | |
| maximumFrameSize: number | |
| ): number => { | |
| const declaredSize = readSize(header); | |
| if (declaredSize < HEADER_SIZE) | |
| throw new ProtocolFrameError( | |
| `declared frame size ${declaredSize} is below header size` | |
| ); | |
| if (declaredSize > maximumFrameSize) | |
| throw new ProtocolFrameError( | |
| `declared frame size ${declaredSize} exceeds ` + | |
| `the ${maximumFrameSize} byte limit` | |
| ); | |
| return declaredSize; | |
| }; | |
| export const extractResponseFrames = ( | |
| buffer: Buffer, | |
| maximumFrameSize: number | |
| ): ExtractedFrames => { | |
| const frames: Buffer[] = []; | |
| let offset = 0; | |
| while (buffer.length - offset >= HEADER_SIZE) { | |
| const available = buffer.length - offset; | |
| const declaredSize = declaredFrameSize( | |
| buffer.subarray(offset, offset + HEADER_SIZE), | |
| maximumFrameSize | |
| ); | |
| if (available < declaredSize) | |
| break; | |
| frames.push(buffer.subarray(offset, offset + declaredSize)); | |
| offset += declaredSize; | |
| } | |
| return { | |
| frames, | |
| remainder: offset === buffer.length | |
| ? Buffer.alloc(0) | |
| : buffer.subarray(offset) | |
| }; | |
| }; | |
| /** | |
| * Incrementally extracts response frames without repeatedly copying an | |
| * incomplete frame as new socket chunks arrive. | |
| */ | |
| export class ResponseFrameDecoder { | |
| private readonly maximumFrameSize: number; | |
| private chunks: Buffer[]; | |
| private chunkIndex: number; | |
| private chunkOffset: number; | |
| private bufferedLength: number; | |
| private expectedFrameSize?: number; | |
| constructor(maximumFrameSize: number) { | |
| this.maximumFrameSize = maximumFrameSize; | |
| this.chunks = []; | |
| this.chunkIndex = 0; | |
| this.chunkOffset = 0; | |
| this.bufferedLength = 0; | |
| this.expectedFrameSize = undefined; | |
| } | |
| get hasBufferedData(): boolean { | |
| return this.bufferedLength > 0; | |
| } | |
| clear(): void { | |
| this.chunks = []; | |
| this.chunkIndex = 0; | |
| this.chunkOffset = 0; | |
| this.bufferedLength = 0; | |
| this.expectedFrameSize = undefined; | |
| } | |
| push(data: Buffer): Buffer[] { | |
| if (data.length > 0) { | |
| this.chunks.push(data); | |
| this.bufferedLength += data.length; | |
| } | |
| const frames: Buffer[] = []; | |
| while (true) { | |
| if (this.expectedFrameSize === undefined) { | |
| if (this.bufferedLength < HEADER_SIZE) | |
| break; | |
| this.expectedFrameSize = declaredFrameSize( | |
| this.peek(HEADER_SIZE), | |
| this.maximumFrameSize | |
| ); | |
| } | |
| if (this.bufferedLength < this.expectedFrameSize) | |
| break; | |
| frames.push(this.consume(this.expectedFrameSize)); | |
| this.expectedFrameSize = undefined; | |
| } | |
| this.compact(); | |
| return frames; | |
| } | |
| private peek(length: number): Buffer { | |
| const first = this.chunks[this.chunkIndex]; | |
| const firstAvailable = first.length - this.chunkOffset; | |
| if (firstAvailable >= length) | |
| return first.subarray(this.chunkOffset, this.chunkOffset + length); | |
| const result = Buffer.allocUnsafe(length); | |
| let resultOffset = 0; | |
| for (let index = this.chunkIndex; resultOffset < length; index += 1) { | |
| const chunk = this.chunks[index]; | |
| const offset = index === this.chunkIndex ? this.chunkOffset : 0; | |
| const copyLength = Math.min(chunk.length - offset, length - resultOffset); | |
| chunk.copy(result, resultOffset, offset, offset + copyLength); | |
| resultOffset += copyLength; | |
| } | |
| return result; | |
| } | |
| private consume(length: number): Buffer { | |
| const first = this.chunks[this.chunkIndex]; | |
| const firstAvailable = first.length - this.chunkOffset; | |
| if (firstAvailable >= length) { | |
| const frame = first.subarray( | |
| this.chunkOffset, | |
| this.chunkOffset + length | |
| ); | |
| this.chunkOffset += length; | |
| this.bufferedLength -= length; | |
| if (this.chunkOffset === first.length) { | |
| this.chunkIndex += 1; | |
| this.chunkOffset = 0; | |
| } | |
| return frame; | |
| } | |
| const frame = Buffer.allocUnsafe(length); | |
| let frameOffset = 0; | |
| while (frameOffset < length) { | |
| const chunk = this.chunks[this.chunkIndex]; | |
| const available = chunk.length - this.chunkOffset; | |
| const copyLength = Math.min(available, length - frameOffset); | |
| chunk.copy( | |
| frame, | |
| frameOffset, | |
| this.chunkOffset, | |
| this.chunkOffset + copyLength | |
| ); | |
| frameOffset += copyLength; | |
| this.chunkOffset += copyLength; | |
| if (this.chunkOffset === chunk.length) { | |
| this.chunkIndex += 1; | |
| this.chunkOffset = 0; | |
| } | |
| } | |
| this.bufferedLength -= length; | |
| return frame; | |
| } | |
| private compact(): void { | |
| if (this.chunkIndex === 0) | |
| return; | |
| if (this.chunkIndex === this.chunks.length) { | |
| this.chunks = []; | |
| this.chunkIndex = 0; | |
| return; | |
| } | |
| this.chunks = this.chunks.slice(this.chunkIndex); | |
| this.chunkIndex = 0; | |
| } | |
| } |