diff --git a/bit-buffer.js b/bit-buffer.js index 1ba68bd..32f6b38 100644 --- a/bit-buffer.js +++ b/bit-buffer.js @@ -17,41 +17,13 @@ const NORMAL_DENOMINATOR = (1 << NORMAL_FRACTIONAL_BITS) - 1; const NORMAL_RESOLUTION = 1.0 / NORMAL_DENOMINATOR; const MAX_VAR_INT32_BYTES = 5; -const MASKS = [ - 0, - 0xFFFFFFFF >> 31, - 0xFFFFFFFF >> 30, - 0xFFFFFFFF >> 29, - 0xFFFFFFFF >> 28, - 0xFFFFFFFF >> 27, - 0xFFFFFFFF >> 26, - 0xFFFFFFFF >> 25, - 0xFFFFFFFF >> 24, - 0xFFFFFFFF >> 23, - 0xFFFFFFFF >> 22, - 0xFFFFFFFF >> 21, - 0xFFFFFFFF >> 20, - 0xFFFFFFFF >> 19, - 0xFFFFFFFF >> 18, - 0xFFFFFFFF >> 17, - 0xFFFFFFFF >> 16, - 0xFFFFFFFF >> 15, - 0xFFFFFFFF >> 14, - 0xFFFFFFFF >> 13, - 0xFFFFFFFF >> 12, - 0xFFFFFFFF >> 11, - 0xFFFFFFFF >> 10, - 0xFFFFFFFF >> 9, - 0xFFFFFFFF >> 8, - 0xFFFFFFFF >> 7, - 0xFFFFFFFF >> 6, - 0xFFFFFFFF >> 5, - 0xFFFFFFFF >> 4, - 0xFFFFFFFF >> 3, - 0xFFFFFFFF >> 2, - 0xFFFFFFFF >> 1, - 0xFFFFFFFF, -]; +// MASKS[n] = n low bits set. Must use unsigned shift: 0xFFFFFFFF passes +// through ToInt32 as -1, so the old signed `>>` produced -1 for every entry. +const MASKS = new Array(33); +MASKS[0] = 0; +for (let i = 1; i <= 32; i++) { + MASKS[i] = 0xFFFFFFFF >>> (32 - i); +} /********************************************************** * * BitView @@ -72,6 +44,7 @@ var BitView = function (source, byteOffset, byteLength) { byteLength = byteLength || source.byteLength /* ArrayBuffer */ || source.length /* Buffer */; this._view = new Uint8Array(source.buffer || source, byteOffset, byteLength); + this._dv = new DataView(this._view.buffer, this._view.byteOffset, this._view.byteLength); this.bigEndian = false; }; @@ -100,58 +73,68 @@ BitView.prototype._setBit = function (offset, on) { } }; -BitView.prototype.getBits = function (offset, bits, signed) { - var available = (this._view.length * 8 - offset); - - if (bits > available) { - throw new Error('Cannot get ' + bits + ' bit(s) from offset ' + offset + ', ' + available + ' available'); - } - +// The original per-byte loop, verbatim, minus the bounds check (caller did it) +// and minus the sign handling (caller does it). Handles bigEndian and buffer tails. +BitView.prototype._getBitsSlow = function (offset, bits) { var value = 0; for (var i = 0; i < bits;) { var remaining = bits - i; var bitOffset = offset & 7; var currentByte = this._view[offset >> 3]; - - // the max number of bits we can read from the current byte var read = Math.min(remaining, 8 - bitOffset); var mask, readBits; if (this.bigEndian) { - // create a mask with the correct bit width mask = ~(0xFF << read); - // shift the bits we want to the start of the byte and mask of the rest readBits = (currentByte >> (8 - read - bitOffset)) & mask; - value <<= read; value |= readBits; } else { - // create a mask with the correct bit width mask = ~(0xFF << read); - // shift the bits we want to the start of the byte and mask off the rest readBits = (currentByte >> bitOffset) & mask; - value |= readBits << i; } offset += read; i += read; } + return value; +}; + +BitView.prototype.getBits = function (offset, bits, signed) { + var available = (this._view.length * 8 - offset); + if (bits > available) { + throw new Error('Cannot get ' + bits + ' bit(s) from offset ' + offset + ', ' + available + ' available'); + } + + var value; + var byteOffset = offset >> 3; + var bitOffset = offset & 7; + + if (!this.bigEndian && byteOffset + 4 <= this._view.length) { + // Little-endian fast path: one unaligned 32-bit load covers bitOffset + bits <= 32. + var word = this._dv.getUint32(byteOffset, true); + if (bitOffset + bits <= 32) { + value = (word >>> bitOffset) & MASKS[bits]; + } else { + // Need up to 7 spill bits from the 5th byte (bounds guaranteed by the available check). + var spill = this._view[byteOffset + 4]; + value = ((word >>> bitOffset) | (spill << (32 - bitOffset))) & MASKS[bits]; + } + } else { + value = this._getBitsSlow(offset, bits); // the existing loop, renamed + } if (signed) { - // If we're not working with a full 32 bits, check the - // imaginary MSB for this bit count and convert to a - // valid 32-bit signed value if set. if (bits !== 32 && value & (1 << (bits - 1))) { value |= -1 ^ ((1 << bits) - 1); } - return value; } - return value >>> 0; }; + BitView.prototype.setBits = function (offset, value, bits) { var available = (this._view.length * 8 - offset); @@ -222,6 +205,11 @@ BitView.prototype.getUint32 = function (offset) { return this.getBits(offset, 32, false); }; BitView.prototype.getFloat32 = function (offset) { + // Byte-aligned fast path: read the IEEE754 bits directly (little-endian, + // same byte order the unaligned path assembles via getUint32). + if ((offset & 7) === 0 && (offset >> 3) + 4 <= this._view.length) { + return this._dv.getFloat32(offset >> 3, true); + } BitView._scratch.setUint32(0, this.getUint32(offset)); return BitView._scratch.getFloat32(0); }; @@ -303,45 +291,54 @@ function readString(stream, bytes, utf8) { if (bytes === 0) { return ''; } - var i = 0; - var chars = []; - var append = true; var fixedLength = !!bytes; - if (!bytes) { - bytes = Math.floor((stream._length - stream._index) / 8); + var available = (stream._length - stream._index) >> 3; + var maxBytes = fixedLength ? bytes : available; + if (maxBytes <= 0) { + return ''; + } + if (fixedLength && bytes > available) { + throw new Error('Trying to read past the end of the stream'); } - // Read while we still have space available, or until we've - // hit the fixed byte length passed in. - while (i < bytes) { - var c = stream.readUint8(); + var u8 = stream._view._view; + var startByte = stream._index >> 3; + var bitOffset = stream._index & 7; - // Stop appending chars once we hit 0x00 - if (c === 0x00) { - append = false; + // Locate the NUL terminator (if any) without copying, then decode + // only the string bytes. Fixed-length reads truncate at the NUL but + // still consume all `bytes` bytes; variable reads consume through the NUL. + var strLen = maxBytes; + var sawNul = false; + var data; - // If we don't have a fixed length to read, break out now. - if (!fixedLength) { + if (bitOffset === 0) { + var nulIdx = u8.indexOf(0, startByte); + if (nulIdx !== -1 && nulIdx - startByte < maxBytes) { + strLen = nulIdx - startByte; + sawNul = true; + } + // Zero-copy window over the source; decoded immediately below, never retained. + data = Buffer.from(u8.buffer, u8.byteOffset + startByte, strLen); + } else { + var inv = 8 - bitOffset; + for (var i = 0; i < maxBytes; i++) { + var c = ((u8[startByte + i] >>> bitOffset) | (u8[startByte + i + 1] << inv)) & 0xFF; + if (c === 0) { + strLen = i; + sawNul = true; break; } } - if (append) { - chars.push(c); + data = Buffer.allocUnsafe(strLen); + for (var j = 0; j < strLen; j++) { + data[j] = ((u8[startByte + j] >>> bitOffset) | (u8[startByte + j + 1] << inv)) & 0xFF; } - - i++; } - var string = String.fromCharCode.apply(null, chars); - if (utf8) { - try { - return decodeURIComponent(escape(string)); // https://stackoverflow.com/a/17192845 - } catch (e) { - return string; - } - } else { - return string; - } + stream._index += (fixedLength ? bytes : strLen + (sawNul ? 1 : 0)) * 8; + + return data.toString(utf8 ? 'utf8' : 'latin1', 0, strLen); } function writeASCIIString(stream, string, bytes) { @@ -467,9 +464,32 @@ BitStream.prototype.writeBits = function (value, bits) { this._index += bits; }; -BitStream.prototype.readBoolean = reader('getBoolean', 1); +// Dedicated implementations for the two hottest single-value reads +// (huffman walks / field-path ops for readBoolean, varints for readUint8). +// They index the byte array directly instead of going reader() -> getBoolean/getUint8 -> getBits. +BitStream.prototype.readBoolean = function () { + if (this._index >= this._length) { + throw new Error('Trying to read past the end of the stream'); + } + var v = (this._view._view[this._index >> 3] >>> (this._index & 7)) & 1; + this._index++; + return v === 1; +}; BitStream.prototype.readInt8 = reader('getInt8', 8); -BitStream.prototype.readUint8 = reader('getUint8', 8); +BitStream.prototype.readUint8 = function () { + var i = this._index; + if (i + 8 > this._length) { + throw new Error('Trying to read past the end of the stream'); + } + var u8 = this._view._view; + var byteOffset = i >> 3; + var bitOffset = i & 7; + var v = bitOffset === 0 + ? u8[byteOffset] + : ((u8[byteOffset] >>> bitOffset) | (u8[byteOffset + 1] << (8 - bitOffset))) & 0xFF; + this._index = i + 8; + return v; +}; BitStream.prototype.readInt16 = reader('getInt16', 16); BitStream.prototype.readUint16 = reader('getUint16', 16); BitStream.prototype.readInt32 = reader('getInt32', 32); @@ -550,15 +570,31 @@ BitStream.prototype.readBitsAsBytes = function (bits) { }; BitStream.prototype.readBytes = function (bytes) { - const arr = new Array(bytes); - for (let i = 0; i < bytes; ++i) { - arr[i] = this.readUint8(); + var bits = bytes * 8; + if (this._index + bits > this._length) { + throw new Error('Trying to read past the end of the stream'); + } + var u8 = this._view._view; + var byteOffset = this._index >> 3; + var bitOffset = this._index & 7; + var result = Buffer.allocUnsafe(bytes); + + if (bitOffset === 0) { + // Byte-aligned: single memcpy. + result.set(u8.subarray(byteOffset, byteOffset + bytes)); + } else { + var inv = 8 - bitOffset; + for (var i = 0; i < bytes; i++) { + result[i] = ((u8[byteOffset + i] >>> bitOffset) | (u8[byteOffset + i + 1] << inv)) & 0xFF; + } } - return Buffer.from(arr); + + this._index += bits; + return result; }; BitStream.prototype.readOneBit = function () { - return this.readBits(1, false) === 1; + return this.readBoolean(); }; BitStream.prototype.readArrayBuffer = function (bits) { @@ -597,11 +633,6 @@ BitStream.prototype.readnBits = function (n) { return bits & MASKS[n]; }; -BitStream.prototype.readnBytes = function (n) { - let bytes = []; - bytes = this.readBytes(n); -}; - BitStream.prototype.readUBits = BitStream.prototype.readBits; BitStream.prototype.readSBits = function (bits) { @@ -673,22 +704,30 @@ BitStream.prototype.readBitCoordPrecise = function () { return result; }; +// Unrolled; the single-byte case (the vast majority of varints) is one +// masked load and one branch. NOTE: intentionally returns a signed int32 +// for values >= 2^31, matching the original implementation — consumers +// (zigzag readVarInt32, handle decoders) rely on ToInt32 semantics anyway. BitStream.prototype.readUVarInt32 = function () { - let result = 0; - let count = 0; - let bytes; - - do { - bytes = this.readBits(8); - result |= (bytes & 127) << (7 * count); - ++count; - } while (count < MAX_VAR_INT32_BYTES && (bytes & 0x80) !== 0); - + var b = this.readUint8(); + var result = b & 0x7F; + if ((b & 0x80) === 0) return result; + b = this.readUint8(); + result |= (b & 0x7F) << 7; + if ((b & 0x80) === 0) return result; + b = this.readUint8(); + result |= (b & 0x7F) << 14; + if ((b & 0x80) === 0) return result; + b = this.readUint8(); + result |= (b & 0x7F) << 21; + if ((b & 0x80) === 0) return result; + b = this.readUint8(); + result |= (b & 0x7F) << 28; return result; }; BitStream.prototype.readOneByte = function () { - return this.readBytes(1)[0]; + return this.readUint8(); }; BitStream.prototype.readVarInt32 = function () { @@ -830,7 +869,7 @@ BitStream.prototype.readCString = function () { let s = ''; while (true) { - const c = this.readUInt8(); + const c = this.readUint8(); // Stop appending chars once we hit 0x00 if (c === 0x00) { @@ -844,24 +883,37 @@ BitStream.prototype.readCString = function () { }; BitStream.prototype.readLeUint64 = function () { - const bytes = this.readBytes(8); - return bytes.readBigUint64LE(); + var lo = this.readBits(32); + var hi = this.readBits(32); + return (BigInt(hi) << 32n) | BigInt(lo); }; BitStream.prototype.readVarUint64 = function () { - // Copyright 2023 Skye van Boheemen. All rights reserved. MIT license. - let value = 0n; - let length = 0; - while (true) { - const currentByte = BigInt(this.readOneByte()); - value |= (currentByte & 0x7Fn) << 7n * BigInt(length); - length++; - if (length > 10) throw new Error("Max Length Reached"); - - if ((currentByte & 0x80n) !== 0x80n) break; + var lo = 0, hi = 0, b; + + // Bytes 0..3 fill lo bits 0..27. + for (var shift = 0; shift < 28; shift += 7) { + b = this.readUint8(); + lo |= (b & 0x7F) << shift; + if ((b & 0x80) === 0) return BigInt(lo >>> 0); + } + + // Byte 4 straddles: low 4 bits -> lo[28..31], next 3 bits -> hi[0..2]. + b = this.readUint8(); + lo |= (b & 0x0F) << 28; + hi = (b & 0x7F) >>> 4; + + if ((b & 0x80) !== 0) { + // Bytes 5..9 fill hi. + for (var shift2 = 3; ; shift2 += 7) { + if (shift2 > 31) throw new Error('Max Length Reached'); + b = this.readUint8(); + hi |= (b & 0x7F) << shift2; + if ((b & 0x80) === 0) break; + } } - return value; + return (BigInt(hi >>> 0) << 32n) | BigInt(lo >>> 0); }; BitStream.from = function from(array) {