UNPKG

bit-buffer

Version:

Bit-level reads and writes for ArrayBuffers

503 lines (429 loc) 14.2 kB
(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.buffer || 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));