/** * Buffer manipulation utilities for protocol detection */ /** * BufferAccumulator class for handling fragmented data */ export class BufferAccumulator { private chunks: Buffer[] = []; private totalLength = 0; /** * Append data to the accumulator */ append(data: Buffer): void { this.chunks.push(data); this.totalLength += data.length; } /** * Get the accumulated buffer */ getBuffer(): Buffer { if (this.chunks.length === 0) { return Buffer.alloc(0); } if (this.chunks.length === 1) { return this.chunks[0]; } return Buffer.concat(this.chunks, this.totalLength); } /** * Get current buffer length */ length(): number { return this.totalLength; } /** * Clear all accumulated data */ clear(): void { this.chunks = []; this.totalLength = 0; } /** * Check if accumulator has minimum bytes */ hasMinimumBytes(minBytes: number): boolean { return this.totalLength >= minBytes; } } /** * Read a big-endian 16-bit integer from buffer */ export function readUInt16BE(buffer: Buffer, offset: number): number { if (offset + 2 > buffer.length) { throw new Error('Buffer too short for UInt16BE read'); } return (buffer[offset] << 8) | buffer[offset + 1]; } /** * Read a big-endian 24-bit integer from buffer */ export function readUInt24BE(buffer: Buffer, offset: number): number { if (offset + 3 > buffer.length) { throw new Error('Buffer too short for UInt24BE read'); } return (buffer[offset] << 16) | (buffer[offset + 1] << 8) | buffer[offset + 2]; } /** * Find a byte sequence in a buffer */ export function findSequence(buffer: Buffer, sequence: Buffer, startOffset = 0): number { if (sequence.length === 0) { return startOffset; } const searchLength = buffer.length - sequence.length + 1; for (let i = startOffset; i < searchLength; i++) { let found = true; for (let j = 0; j < sequence.length; j++) { if (buffer[i + j] !== sequence[j]) { found = false; break; } } if (found) { return i; } } return -1; } /** * Extract a line from buffer (up to CRLF or LF) */ export function extractLine(buffer: Buffer, startOffset = 0): { line: string; nextOffset: number } | null { let lineEnd = -1; let skipBytes = 1; // Look for CRLF first const crlfPos = findSequence(buffer, Buffer.from('\r\n'), startOffset); if (crlfPos !== -1) { lineEnd = crlfPos; skipBytes = 2; } else { // Look for LF only for (let i = startOffset; i < buffer.length; i++) { if (buffer[i] === 0x0A) { // LF lineEnd = i; break; } } } if (lineEnd === -1) { return null; } const line = buffer.slice(startOffset, lineEnd).toString('utf8'); return { line, nextOffset: lineEnd + skipBytes }; } /** * Check if buffer starts with a string (case-insensitive) */ export function startsWithString(buffer: Buffer, str: string, offset = 0): boolean { if (offset + str.length > buffer.length) { return false; } const bufferStr = buffer.slice(offset, offset + str.length).toString('utf8'); return bufferStr.toLowerCase() === str.toLowerCase(); } /** * Safe buffer slice that doesn't throw on out-of-bounds */ export function safeSlice(buffer: Buffer, start: number, end?: number): Buffer { const safeStart = Math.max(0, Math.min(start, buffer.length)); const safeEnd = end === undefined ? buffer.length : Math.max(safeStart, Math.min(end, buffer.length)); return buffer.slice(safeStart, safeEnd); } /** * Check if buffer contains printable ASCII */ export function isPrintableAscii(buffer: Buffer, length?: number): boolean { const checkLength = length || buffer.length; for (let i = 0; i < checkLength && i < buffer.length; i++) { const byte = buffer[i]; // Check if byte is printable ASCII (0x20-0x7E) or tab/newline/carriage return if (byte < 0x20 || byte > 0x7E) { if (byte !== 0x09 && byte !== 0x0A && byte !== 0x0D) { return false; } } } return true; }