UNPKG

extract-base-iterator

Version:

Base iterator for extract iterators like tar-iterator and zip-iterator

243 lines (242 loc) 8.24 kB
/** * Buffer List for Streaming * * Simple linked list for accumulating buffer chunks during streaming. * Provides efficient append, consume, and slice operations. */ import { allocBuffer } from 'extract-base-iterator'; let BufferList = class BufferList { /** * Append a buffer to the end of the list */ append(buf) { if (buf.length === 0) return; const node = { data: buf, next: null }; if (this.tail) { this.tail.next = node; this.tail = node; } else { this.head = this.tail = node; } this.length += buf.length; } /** * Prepend a buffer to the front of the list */ prepend(buf) { if (buf.length === 0) return; const node = { data: buf, next: this.head }; if (!this.tail) { this.tail = node; } this.head = node; this.length += buf.length; } /** * Consume n bytes from the front of the list * Returns a new buffer containing the consumed bytes */ consume(n) { if (n <= 0) return allocBuffer(0); if (n > this.length) n = this.length; const result = allocBuffer(n); let offset = 0; while(offset < n && this.head){ const chunk = this.head.data; const needed = n - offset; if (chunk.length <= needed) { // Use entire chunk chunk.copy(result, offset); offset += chunk.length; this.head = this.head.next; if (!this.head) this.tail = null; } else { // Use partial chunk chunk.copy(result, offset, 0, needed); this.head.data = chunk.slice(needed); offset = n; } } this.length -= n; return result; } /** * Get a slice of the buffer without consuming * Returns a new buffer containing the bytes */ slice(start, end) { const len = end - start; if (len <= 0) return allocBuffer(0); if (start >= this.length) return allocBuffer(0); const result = allocBuffer(Math.min(len, this.length - start)); let resultOffset = 0; let bufOffset = 0; let node = this.head; // Skip to start position while(node && bufOffset + node.data.length <= start){ bufOffset += node.data.length; node = node.next; } // Copy data while(node && resultOffset < result.length){ const chunk = node.data; const chunkStart = Math.max(0, start - bufOffset); const chunkEnd = Math.min(chunk.length, end - bufOffset); const toCopy = chunkEnd - chunkStart; if (toCopy > 0) { chunk.copy(result, resultOffset, chunkStart, chunkEnd); resultOffset += toCopy; } bufOffset += chunk.length; node = node.next; } return result; } /** * Read a single byte at offset without consuming */ readByte(offset) { if (offset < 0 || offset >= this.length) return -1; let bufOffset = 0; let node = this.head; while(node){ if (offset < bufOffset + node.data.length) { return node.data[offset - bufOffset]; } bufOffset += node.data.length; node = node.next; } return -1; } /** * Search for a byte sequence in the buffer * Returns offset of first match, or -1 if not found */ indexOf(signature, startOffset = 0) { if (signature.length === 0) return startOffset; if (startOffset + signature.length > this.length) return -1; // Simple byte-by-byte search // Could be optimized with KMP/Boyer-Moore for larger signatures for(let i = startOffset; i <= this.length - signature.length; i++){ let match = true; for(let j = 0; j < signature.length; j++){ if (this.readByte(i + j) !== signature[j]) { match = false; break; } } if (match) return i; } return -1; } /** * Skip (consume) n bytes without returning them */ skip(n) { if (n <= 0) return; if (n >= this.length) { this.clear(); return; } let remaining = n; while(remaining > 0 && this.head){ const chunk = this.head.data; if (chunk.length <= remaining) { remaining -= chunk.length; this.head = this.head.next; if (!this.head) this.tail = null; } else { this.head.data = chunk.slice(remaining); remaining = 0; } } this.length -= n; } /** * Clear all buffers */ clear() { this.head = null; this.tail = null; this.length = 0; } /** * Check if buffer has at least n bytes available */ has(n) { return this.length >= n; } /** * Check if the buffer starts with a signature at offset 0 */ startsWith(signature) { if (signature.length > this.length) return false; for(let i = 0; i < signature.length; i++){ if (this.readByte(i) !== signature[i]) return false; } return true; } /** * Get a consolidated buffer of the entire contents * Note: This creates a copy, so use sparingly for large buffers */ toBuffer() { if (this.length === 0) return allocBuffer(0); return this.slice(0, this.length); } /** * Read UInt16 (little-endian) at offset without consuming * Returns null if not enough data */ readUInt16LEAt(offset) { if (offset < 0 || offset + 2 > this.length) return null; const bytes = this.readBytesAt(offset, 2); if (bytes.length < 2) return null; return bytes.readUInt16LE(0); } /** * Read UInt32 (little-endian) at offset without consuming * Returns null if not enough data */ readUInt32LEAt(offset) { if (offset < 0 || offset + 4 > this.length) return null; const bytes = this.readBytesAt(offset, 4); if (bytes.length < 4) return null; return bytes.readUInt32LE(0); } /** * Read bytes at offset without consuming. * Returns a slice (zero-copy) when data fits within a single chunk, * otherwise allocates and copies from multiple chunks. */ readBytesAt(offset, length) { if (length <= 0) return allocBuffer(0); if (offset < 0 || offset >= this.length) return allocBuffer(0); // Clamp length to available data const available = this.length - offset; if (length > available) length = available; // Find the node containing the offset let bufOffset = 0; let node = this.head; while(node && bufOffset + node.data.length <= offset){ bufOffset += node.data.length; node = node.next; } if (!node) return allocBuffer(0); const startInChunk = offset - bufOffset; // Single-buffer optimization: zero-copy slice if (startInChunk + length <= node.data.length) { return node.data.slice(startInChunk, startInChunk + length); } // Multi-buffer case: must allocate and copy const result = allocBuffer(length); let resultOffset = 0; while(node && resultOffset < length){ const chunk = node.data; const chunkStart = resultOffset === 0 ? startInChunk : 0; const chunkAvailable = chunk.length - chunkStart; const toCopy = Math.min(chunkAvailable, length - resultOffset); chunk.copy(result, resultOffset, chunkStart, chunkStart + toCopy); resultOffset += toCopy; node = node.next; } return result; } constructor(){ this.head = null; this.tail = null; /** Total bytes in the buffer list */ this.length = 0; } }; export { BufferList as default };