bitbuffer | | | Search

The JavaScript BitView class provides a similar interface to DataView with support for bit-level reads and writes, allowing for manipulation of single bits within a specified byte offset. It offers methods to set a single bit, read a specified number of bits, and private properties for internal use, with dependencies on ArrayBuffer or Buffer objects and the DataView class.

Run example

npm run import -- "bit buffer"

bit buffer

(function (root) {

/**********************************************************
 *
 * BitView
 *
 * BitView provides a similar interface to the standard
 * DataView, but with support for bit-level reads / writes.
 *
 **********************************************************/
var BitView = function (source, byteOffset, byteLength) {
	var isBuffer = source instanceof ArrayBuffer ||
		(typeof Buffer !== 'undefined' && source instanceof Buffer);

	if (!isBuffer) {
		throw new Error('Must specify a valid ArrayBuffer or Buffer.');
	}

	byteOffset = byteOffset || 0;
	byteLength = byteLength || source.byteLength /* ArrayBuffer */ || source.length /* Buffer */;

	this._view = new Uint8Array(source, byteOffset, byteLength);

	this.bigEndian = false;
};

// Used to massage fp values so we can operate on them
// at the bit level.
BitView._scratch = new DataView(new ArrayBuffer(8));

Object.defineProperty(BitView.prototype, 'buffer', {
	get: function () { return typeof Buffer !== 'undefined' ?  Buffer.from(this._view.buffer) : this._view.buffer; },
	enumerable: true,
	configurable: false
});

Object.defineProperty(BitView.prototype, 'byteLength', {
	get: function () { return this._view.length; },
	enumerable: true,
	configurable: false
});

BitView.prototype._setBit = function (offset, on) {
	if (on) {
		this._view[offset >> 3] |= 1 << (offset & 7);
	} else {
		this._view[offset >> 3] &= ~(1 << (offset & 7));
	}
};

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 = 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;
	}

	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);

	if (bits > available) {
		throw new Error('Cannot set ' + bits + ' bit(s) from offset ' + offset + ', ' + available + ' available');
	}

	for (var i = 0; i < bits;) {
		var remaining = bits - i;
		var bitOffset = offset & 7;
		var byteOffset = offset >> 3;
		var wrote = Math.min(remaining, 8 - bitOffset);

		var mask, writeBits, destMask;
		if (this.bigEndian) {
			// create a mask with the correct bit width
			mask = ~(~0 << wrote);
			// shift the bits we want to the start of the byte and mask of the rest
			writeBits = (value >> (bits - i - wrote)) & mask;

			var destShift = 8 - bitOffset - wrote;
			// destination mask to zero all the bits we're changing first
			destMask = ~(mask << destShift);

			this._view[byteOffset] =
				(this._view[byteOffset] & destMask)
				| (writeBits << destShift);

		} else {
			// create a mask with the correct bit width
			mask = ~(0xFF << wrote);
			// shift the bits we want to the start of the byte and mask of the rest
			writeBits = value & mask;
			value >>= wrote;

			// destination mask to zero all the bits we're changing first
			destMask = ~(mask << bitOffset);

			this._view[byteOffset] =
				(this._view[byteOffset] & destMask)
				| (writeBits << bitOffset);
		}

		offset += wrote;
		i += wrote;
	}
};

BitView.prototype.getBoolean = function (offset) {
	return this.getBits(offset, 1, false) !== 0;
};
BitView.prototype.getInt8 = function (offset) {
	return this.getBits(offset, 8, true);
};
BitView.prototype.getUint8 = function (offset) {
	return this.getBits(offset, 8, false);
};
BitView.prototype.getInt16 = function (offset) {
	return this.getBits(offset, 16, true);
};
BitView.prototype.getUint16 = function (offset) {
	return this.getBits(offset, 16, false);
};
BitView.prototype.getInt32 = function (offset) {
	return this.getBits(offset, 32, true);
};
BitView.prototype.getUint32 = function (offset) {
	return this.getBits(offset, 32, false);
};
BitView.prototype.getFloat32 = function (offset) {
	BitView._scratch.setUint32(0, this.getUint32(offset));
	return BitView._scratch.getFloat32(0);
};
BitView.prototype.getFloat64 = function (offset) {
	BitView._scratch.setUint32(0, this.getUint32(offset));
	// DataView offset is in bytes.
	BitView._scratch.setUint32(4, this.getUint32(offset+32));
	return BitView._scratch.getFloat64(0);
};

BitView.prototype.setBoolean = function (offset, value) {
	this.setBits(offset, value ? 1 : 0, 1);
};
BitView.prototype.setInt8  =
BitView.prototype.setUint8 = function (offset, value) {
	this.setBits(offset, value, 8);
};
BitView.prototype.setInt16  =
BitView.prototype.setUint16 = function (offset, value) {
	this.setBits(offset, value, 16);
};
BitView.prototype.setInt32  =
BitView.prototype.setUint32 = function (offset, value) {
	this.setBits(offset, value, 32);
};
BitView.prototype.setFloat32 = function (offset, value) {
	BitView._scratch.setFloat32(0, value);
	this.setBits(offset, BitView._scratch.getUint32(0), 32);
};
BitView.prototype.setFloat64 = function (offset, value) {
	BitView._scratch.setFloat64(0, value);
	this.setBits(offset, BitView._scratch.getUint32(0), 32);
	this.setBits(offset+32, BitView._scratch.getUint32(4), 32);
};
BitView.prototype.getArrayBuffer = function (offset, byteLength) {
	var buffer = new Uint8Array(byteLength);
	for (var i = 0; i < byteLength; i++) {
		buffer[i] = this.getUint8(offset + (i * 8));
	}
	return buffer;
};

/**********************************************************
 *
 * BitStream
 *
 * Small wrapper for a BitView to maintain your position,
 * as well as to handle reading / writing of string data
 * to the underlying buffer.
 *
 **********************************************************/
var reader = function (name, size) {
	return function () {
		if (this._index + size > this._length) {
			throw new Error('Trying to read past the end of the stream');
		}
		var val = this._view[name](this._index);
		this._index += size;
		return val;
	};
};

var writer = function (name, size) {
	return function (value) {
		this._view[name](this._index, value);
		this._index += size;
	};
};

function readASCIIString(stream, bytes) {
	return readString(stream, bytes, false);
}

function readUTF8String(stream, bytes) {
	return readString(stream, bytes, true);
}

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);
	}

	// 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();

		// Stop appending chars once we hit 0x00
		if (c === 0x00) {
			append = false;

			// If we don't have a fixed length to read, break out now.
			if (!fixedLength) {
				break;
			}
		}
		if (append) {
			chars.push(c);
		}

		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;
	}
}

function writeASCIIString(stream, string, bytes) {
	var length = bytes || string.length + 1;  // + 1 for NULL

	for (var i = 0; i < length; i++) {
		stream.writeUint8(i < string.length ? string.charCodeAt(i) : 0x00);
	}
}

function writeUTF8String(stream, string, bytes) {
	var byteArray = stringToByteArray(string);

	var length = bytes || byteArray.length + 1;  // + 1 for NULL
	for (var i = 0; i < length; i++) {
		stream.writeUint8(i < byteArray.length ? byteArray[i] : 0x00);
	}
}

function stringToByteArray(str) { // https://gist.github.com/volodymyr-mykhailyk/2923227
	var b = [], i, unicode;
	for (i = 0; i < str.length; i++) {
		unicode = str.charCodeAt(i);
		// 0x00000000 - 0x0000007f -> 0xxxxxxx
		if (unicode <= 0x7f) {
			b.push(unicode);
			// 0x00000080 - 0x000007ff -> 110xxxxx 10xxxxxx
		} else if (unicode <= 0x7ff) {
			b.push((unicode >> 6) | 0xc0);
			b.push((unicode & 0x3F) | 0x80);
			// 0x00000800 - 0x0000ffff -> 1110xxxx 10xxxxxx 10xxxxxx
		} else if (unicode <= 0xffff) {
			b.push((unicode >> 12) | 0xe0);
			b.push(((unicode >> 6) & 0x3f) | 0x80);
			b.push((unicode & 0x3f) | 0x80);
			// 0x00010000 - 0x001fffff -> 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
		} else {
			b.push((unicode >> 18) | 0xf0);
			b.push(((unicode >> 12) & 0x3f) | 0x80);
			b.push(((unicode >> 6) & 0x3f) | 0x80);
			b.push((unicode & 0x3f) | 0x80);
		}
	}

	return b;
}

var BitStream = function (source, byteOffset, byteLength) {
	var isBuffer = source instanceof ArrayBuffer ||
		(typeof Buffer !== 'undefined' && source instanceof Buffer);

	if (!(source instanceof BitView) && !isBuffer) {
		throw new Error('Must specify a valid BitView, ArrayBuffer or Buffer');
	}

	if (isBuffer) {
		this._view = new BitView(source, byteOffset, byteLength);
	} else {
		this._view = source;
	}

	this._index = 0;
	this._startIndex = 0;
	this._length = this._view.byteLength * 8;
};

Object.defineProperty(BitStream.prototype, 'index', {
	get: function () { return this._index - this._startIndex; },
	set: function (val) { this._index = val + this._startIndex; },
	enumerable: true,
	configurable: true
});

Object.defineProperty(BitStream.prototype, 'length', {
	get: function () { return this._length - this._startIndex; },
	set: function (val) { this._length = val + this._startIndex; },
	enumerable  : true,
	configurable: true
});

Object.defineProperty(BitStream.prototype, 'bitsLeft', {
	get: function () { return this._length - this._index; },
	enumerable  : true,
	configurable: true
});

Object.defineProperty(BitStream.prototype, 'byteIndex', {
	// Ceil the returned value, over compensating for the amount of
	// bits written to the stream.
	get: function () { return Math.ceil(this._index / 8); },
	set: function (val) { this._index = val * 8; },
	enumerable: true,
	configurable: true
});

Object.defineProperty(BitStream.prototype, 'buffer', {
	get: function () { return this._view.buffer; },
	enumerable: true,
	configurable: false
});

Object.defineProperty(BitStream.prototype, 'view', {
	get: function () { return this._view; },
	enumerable: true,
	configurable: false
});

Object.defineProperty(BitStream.prototype, 'bigEndian', {
	get: function () { return this._view.bigEndian; },
	set: function (val) { this._view.bigEndian = val; },
	enumerable: true,
	configurable: false
});

BitStream.prototype.readBits = function (bits, signed) {
	var val = this._view.getBits(this._index, bits, signed);
	this._index += bits;
	return val;
};

BitStream.prototype.writeBits = function (value, bits) {
	this._view.setBits(this._index, value, bits);
	this._index += bits;
};

BitStream.prototype.readBoolean = reader('getBoolean', 1);
BitStream.prototype.readInt8 = reader('getInt8', 8);
BitStream.prototype.readUint8 = reader('getUint8', 8);
BitStream.prototype.readInt16 = reader('getInt16', 16);
BitStream.prototype.readUint16 = reader('getUint16', 16);
BitStream.prototype.readInt32 = reader('getInt32', 32);
BitStream.prototype.readUint32 = reader('getUint32', 32);
BitStream.prototype.readFloat32 = reader('getFloat32', 32);
BitStream.prototype.readFloat64 = reader('getFloat64', 64);

BitStream.prototype.writeBoolean = writer('setBoolean', 1);
BitStream.prototype.writeInt8 = writer('setInt8', 8);
BitStream.prototype.writeUint8 = writer('setUint8', 8);
BitStream.prototype.writeInt16 = writer('setInt16', 16);
BitStream.prototype.writeUint16 = writer('setUint16', 16);
BitStream.prototype.writeInt32 = writer('setInt32', 32);
BitStream.prototype.writeUint32 = writer('setUint32', 32);
BitStream.prototype.writeFloat32 = writer('setFloat32', 32);
BitStream.prototype.writeFloat64 = writer('setFloat64', 64);

BitStream.prototype.readASCIIString = function (bytes) {
	return readASCIIString(this, bytes);
};

BitStream.prototype.readUTF8String = function (bytes) {
	return readUTF8String(this, bytes);
};

BitStream.prototype.writeASCIIString = function (string, bytes) {
	writeASCIIString(this, string, bytes);
};

BitStream.prototype.writeUTF8String = function (string, bytes) {
	writeUTF8String(this, string, bytes);
};
BitStream.prototype.readBitStream = function(bitLength) {
	var slice = new BitStream(this._view);
	slice._startIndex = this._index;
	slice._index = this._index;
	slice.length = bitLength;
	this._index += bitLength;
	return slice;
};

BitStream.prototype.writeBitStream = function(stream, length) {
	if (!length) {
		length = stream.bitsLeft;
	}

	var bitsToWrite;
	while (length > 0) {
		bitsToWrite = Math.min(length, 32);
		this.writeBits(stream.readBits(bitsToWrite), bitsToWrite);
		length -= bitsToWrite;
	}
};

BitStream.prototype.readArrayBuffer = function(byteLength) {
	var buffer = this._view.getArrayBuffer(this._index, byteLength);
	this._index += (byteLength * 8);
	return buffer;
};

BitStream.prototype.writeArrayBuffer = function(buffer, byteLength) {
	this.writeBitStream(new BitStream(buffer), byteLength * 8);
};

// AMD / RequireJS
if (typeof define !== 'undefined' && define.amd) {
	define(function () {
		return {
			BitView: BitView,
			BitStream: BitStream
		};
	});
}
// Node.js
else if (typeof module !== 'undefined' && module.exports) {
	module.exports = {
		BitView: BitView,
		BitStream: BitStream
	};
}

}(this));

What the code could have been:

(function(root) {

/**********************************************************************
 *
 * BitView
 *
 * BitView provides a similar interface to the standard
 * DataView, but with support for bit-level reads / writes.
 *
 **********************************************************************/

class BitView {
  constructor(source, byteOffset, byteLength) {
    if (!(source instanceof ArrayBuffer || (typeof Buffer!== 'undefined' && source instanceof Buffer))) {
      throw new Error('Must specify a valid ArrayBuffer or Buffer.');
    }

    byteOffset = byteOffset || 0;
    byteLength = byteLength || source.byteLength || source.length;

    this._view = new Uint8Array(source, byteOffset, byteLength);

    this.bigEndian = false;

    this.buffer = source instanceof ArrayBuffer? source : Buffer.from(source);
    this.byteLength = byteLength;

    this._scratch = new DataView(new ArrayBuffer(8));
  }

  _setBit(offset, on) {
    if (on) {
      this._view[offset >> 3] |= 1 << (offset & 7);
    } else {
      this._view[offset >> 3] &= ~(1 << (offset & 7));
    }
  }

  getBits(offset, bits, signed) {
    const available = (this._view.length * 8 - offset);
    if (bits > available) {
      throw new Error(`Cannot get ${bits} bit(s) from offset ${offset}, ${available} available`);
    }

    let value = 0;
    for (let i = 0; i < bits;) {
      const remaining = bits - i;
      const bitOffset = offset & 7;
      const currentByte = this._view[offset >> 3];

      const read = Math.min(remaining, 8 - bitOffset);

      const mask = this.bigEndian? ~(0xFF << read) : ~0xFF;
      const readBits = (this.bigEndian? (currentByte >> (8 - read - bitOffset)) : (currentByte >> bitOffset)) & mask;

      value <<= read;
      value |= readBits;

      offset += read;
      i += read;
    }

    if (signed) {
      if (bits!== 32 && value & (1 << (bits - 1))) {
        value |= -1 ^ ((1 << bits) - 1);
      }

      return value;
    }

    return value >>> 0;
  }

  setBits(offset, value, bits) {
    const available = (this._view.length * 8 - offset);
    if (bits > available) {
      throw new Error(`Cannot set ${bits} bit(s) from offset ${offset}, ${available} available`);
    }

    for (let i = 0; i < bits;) {
      const remaining = bits - i;
      const bitOffset = offset & 7;
      const byteOffset = offset >> 3;

      const wrote = Math.min(remaining, 8 - bitOffset);

      const mask = this.bigEndian? ~(~0 << wrote) : ~(0xFF << wrote);
      const writeBits = (this.bigEndian? (value >> (bits - i - wrote)) & mask : value & mask);
      const destMask = this.bigEndian? ~(mask << (8 - bitOffset - wrote)) : ~(mask << bitOffset);

      this._view[byteOffset] = (this._view[byteOffset] & destMask) | (writeBits << (8 - bitOffset - wrote));

      offset += wrote;
      i += wrote;
    }
  }

  getBoolean(offset) {
    return this.getBits(offset, 1, false)!== 0;
  }

  getInt8(offset) {
    return this.getBits(offset, 8, true);
  }

  getUint8(offset) {
    return this.getBits(offset, 8, false);
  }

  getInt16(offset) {
    return this.getBits(offset, 16, true);
  }

  getUint16(offset) {
    return this.getBits(offset, 16, false);
  }

  getInt32(offset) {
    return this.getBits(offset, 32, true);
  }

  getUint32(offset) {
    return this.getBits(offset, 32, false);
  }

  getFloat32(offset) {
    this._scratch.setUint32(0, this.getUint32(offset));
    return this._scratch.getFloat32(0);
  }

  getFloat64(offset) {
    this._scratch.setUint32(0, this.getUint32(offset));
    this._scratch.setUint32(4, this.getUint32(offset + 32));
    return this._scratch.getFloat64(0);
  }

  setBoolean(offset, value) {
    this.setBits(offset, value? 1 : 0, 1);
  }

  setInt8(offset, value) {
    this.setBits(offset, value, 8);
  }

  setUint8(offset, value) {
    this.setBits(offset, value, 8);
  }

  setInt16(offset, value) {
    this.setBits(offset, value, 16);
  }

  setUint16(offset, value) {
    this.setBits(offset, value, 16);
  }

  setInt32(offset, value) {
    this.setBits(offset, value, 32);
  }

  setUint32(offset, value) {
    this.setBits(offset, value, 32);
  }

  setFloat32(offset, value) {
    this._scratch.setFloat32(0, value);
    this.setBits(offset, this._scratch.getUint32(0), 32);
  }

  setFloat64(offset, value) {
    this._scratch.setFloat64(0, value);
    this.setBits(offset, this._scratch.getUint32(0), 32);
    this.setBits(offset + 32, this._scratch.getUint32(4), 32);
  }

  getArrayBuffer(offset, byteLength) {
    const buffer = new Uint8Array(byteLength);
    for (let i = 0; i < byteLength; i++) {
      buffer[i] = this.getUint8(offset + (i * 8));
    }
    return buffer;
  }
}

class BitStream {
  constructor(source, byteOffset, byteLength) {
    if (!(source instanceof BitView || (source instanceof ArrayBuffer || (typeof Buffer!== 'undefined' && source instanceof Buffer)))) {
      throw new Error('Must specify a valid BitView, ArrayBuffer or Buffer');
    }

    if (source instanceof BitView) {
      this._view = source;
    } else {
      this._view = new BitView(source, byteOffset, byteLength);
    }

    this._index = 0;
    this._startIndex = 0;
    this._length = this._view.byteLength * 8;
  }

  get index() {
    return this._index - this._startIndex;
  }

  set index(value) {
    this._index = value + this._startIndex;
  }

  get length() {
    return this._length - this._startIndex;
  }

  set length(value) {
    this._length = value + this._startIndex;
  }

  get bitsLeft() {
    return this._length - this._index;
  }

  get byteIndex() {
    return Math.ceil(this._index / 8);
  }

  set byteIndex(value) {
    this._index = value * 8;
  }

  get buffer() {
    return this._view.buffer;
  }

  get view() {
    return this._view;
  }

  get bigEndian() {
    return this._view.bigEndian;
  }

  set bigEndian(value) {
    this._view.bigEndian = value;
  }

  readBits(bits, signed) {
    const val = this._view.getBits(this._index, bits, signed);
    this._index += bits;
    return val;
  }

  writeBits(value, bits) {
    this._view.setBits(this._index, value, bits);
    this._index += bits;
  }

  readBoolean() {
    return this._view.getBoolean(this._index++);
  }

  readInt8() {
    return this._view.getInt8(this._index++);
  }

  readUint8() {
    return this._view.getUint8(this._index++);
  }

  readInt16() {
    return this._view.getInt16(this._index++);
  }

  readUint16() {
    return this._view.getUint16(this._index++);
  }

  readInt32() {
    return this._view.getInt32(this._index++);
  }

  readUint32() {
    return this._view.getUint32(this._index++);
  }

  readFloat32() {
    return this._view.getFloat32(this._index++);
  }

  readFloat64() {
    return this._view.getFloat64(this._index++);
  }

  writeBoolean(value) {
    this._view.setBoolean(this._index++, value);
  }

  writeInt8(value) {
    this._view.setInt8(this._index++, value);
  }

  writeUint8(value) {
    this._view.setUint8(this._index++, value);
  }

  writeInt16(value) {
    this._view.setInt16(this._index++, value);
  }

  writeUint16(value) {
    this._view.setUint16(this._index++, value);
  }

  writeInt32(value) {
    this._view.setInt32(this._index++, value);
  }

  writeUint32(value) {
    this._view.setUint32(this._index++, value);
  }

  writeFloat32(value) {
    this._view.setFloat32(this._index++, value);
  }

  writeFloat64(value) {
    this._view.setFloat64(this._index++, value);
  }

  readASCIIString(bytes) {
    return readASCIIString(this, bytes);
  }

  readUTF8String(bytes) {
    return readUTF8String(this, bytes);
  }

  writeASCIIString(string, bytes) {
    writeASCIIString(this, string, bytes);
  }

  writeUTF8String(string, bytes) {
    writeUTF8String(this, string, bytes);
  }

  readBitStream(bitLength) {
    const slice = new BitStream(this._view);
    slice._startIndex = this._index;
    slice._index = this._index;
    slice.length = bitLength;
    this._index += bitLength;
    return slice;
  }

  writeBitStream(stream, length) {
    if (!length) {
      length = stream.bitsLeft;
    }

    while (length > 0) {
      const bitsToWrite = Math.min(length, 32);
      this.writeBits(stream.readBits(bitsToWrite), bitsToWrite);
      length -= bitsToWrite;
    }
  }

  readArrayBuffer(byteLength) {
    const buffer = this._view.getArrayBuffer(this._index, byteLength);
    this._index += (byteLength * 8);
    return buffer;
  }

  writeArrayBuffer(buffer, byteLength) {
    this.writeBitStream(new BitStream(buffer), byteLength * 8);
  }
}

// AMD / RequireJS
if (typeof define!== 'undefined' && define.amd) {
  define(function() {
    return {
      BitView: BitView,
      BitStream: BitStream
    };
  });
}
// Node.js
else if (typeof module!== 'undefined' && module.exports) {
  module.exports = {
    BitView: BitView,
    BitStream: BitStream
  };
}

}(this));

function readASCIIString(stream, bytes) {
  return readString(stream, bytes, false);
}

function readUTF8String(stream, bytes) {
  return readString(stream, bytes, true);
}

function readString(stream, bytes, utf8) {
  if (bytes === 0) {
    return '';
  }

  let i = 0;
  let chars = [];
  let append = true;
  if (!bytes) {
    bytes = Math.floor((stream._length - stream._index) / 8);
  }

  while (i < bytes) {
    const c = stream.readUint8();

    if (c === 0x00) {
      append = false;

      if (!stream.bitsLeft) {
        break;
      }
    }

    if (append) {
      chars.push(c);
    }

    i++;
  }

  const string = String.fromCharCode.apply(null, chars);
  if (utf8) {
    try {
      return decodeURIComponent(escape(string));
    } catch (e) {
      return string;
    }
  } else {
    return string;
  }
}

function writeASCIIString(stream, string, bytes) {
  const length = bytes || string.length + 1; // + 1 for NULL

  for (let i = 0; i < length; i++) {
    stream.writeUint8(i < string.length? string.charCodeAt(i) : 0x00);
  }
}

function writeUTF8String(stream, string, bytes) {
  const byteArray = stringToByteArray(string);

  const length = bytes || byteArray.length + 1; // + 1 for NULL
  for (let i = 0; i < length; i++) {
    stream.writeUint8(i < byteArray.length? byteArray[i] : 0x00);
  }
}

function stringToByteArray(str) {
  const b = [];
  for (let i = 0; i < str.length; i++) {
    const unicode = str.charCodeAt(i);
    if (unicode <= 0x7f) {
      b.push(unicode);
    } else if (unicode <= 0x7ff) {
      b.push((unicode >> 6) | 0xc0);
      b.push((unicode & 0x3F) | 0x80);
    } else if (unicode <= 0xffff) {
      b.push((unicode >> 12) | 0xe0);
      b.push(((unicode >> 6) & 0x3f) | 0x80);
      b.push((unicode & 0x3f) | 0x80);
    } else {
      b.push((unicode >> 18) | 0xf0);
      b.push(((unicode >> 12) & 0x3f) | 0x80);
      b.push(((unicode >> 6) & 0x3f) | 0x80);
      b.push((unicode & 0x3f) | 0x80);
    }
  }

  return b;
}

Code Breakdown

Overview

This code defines a JavaScript class named BitView which provides a similar interface to DataView but with support for bit-level reads and writes.

Constructor

The BitView constructor takes three arguments:

The constructor checks if the source argument is an ArrayBuffer or a Buffer, and if not, throws an error. It then creates a new Uint8Array view of the source data, starting at the specified byteOffset and with the specified byteLength.

Properties

Two properties are defined on the BitView prototype:

Methods

Three methods are defined on the BitView prototype:

Notes