extract-base-iterator
Version:
Base iterator for extract iterators like tar-iterator and zip-iterator
243 lines (242 loc) • 8.24 kB
JavaScript
/**
* 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 };