lib/ByteBuffer.mjs
/* eslint-disable lines-between-class-members, no-param-reassign, prefer-spread */
import { extractBuffer } from './utils';
class ByteBuffer {
// Creates a new ByteBuffer
// - from given source (assumed to be number of bytes when numeric)
// - with given byte order (defaults to big-endian)
// - with given implicit growth strategy (defaults to false)
constructor(source = 0, order = this.constructor.BIG_ENDIAN, implicitGrowth = false) {
// Holds buffer
this._buffer = null;
// Holds raw buffer
this._raw = null;
// Holds internal view for reading/writing
this._view = null;
// Holds byte order
this._order = !!order;
// Holds implicit growth strategy
this._implicitGrowth = !!implicitGrowth;
// Holds read/write index
this._index = 0;
// Attempt to extract a buffer from given source
let buffer = extractBuffer(source, true);
// On failure, assume source is a primitive indicating the number of bytes
if (!buffer) {
buffer = new ArrayBuffer(source);
}
// Assign new buffer
this.buffer = buffer;
}
// Sanitizes read/write index
_sanitizeIndex() {
if (this._index < 0) {
this._index = 0;
}
if (this._index > this.length) {
this._index = this.length;
}
}
// Retrieves buffer
get buffer() {
return this._buffer;
}
// Sets new buffer and sanitizes read/write index
set buffer(buffer) {
this._buffer = buffer;
this._raw = new Uint8Array(this._buffer);
this._view = new DataView(this._buffer);
this._sanitizeIndex();
}
// Retrieves raw buffer
get raw() {
return this._raw;
}
// Retrieves view
get view() {
return this._view;
}
// Retrieves number of bytes
get length() {
return this._buffer.byteLength;
}
// Retrieves number of bytes
// Note: This allows for ByteBuffer to be detected as a proper source by its own constructor
get byteLength() {
return this.length;
}
// Retrieves byte order
get order() {
return this._order;
}
// Sets byte order
set order(order) {
this._order = !!order;
}
// Retrieves implicit growth strategy
get implicitGrowth() {
return this._implicitGrowth;
}
// Sets implicit growth strategy
set implicitGrowth(implicitGrowth) {
this._implicitGrowth = !!implicitGrowth;
}
// Retrieves read/write index
get index() {
return this._index;
}
// Sets read/write index
set index(index) {
if (index < 0 || index > this.length) {
throw new RangeError(`Invalid index ${index}, should be between 0 and ${this.length}`);
}
this._index = index;
}
// Retrieves number of available bytes
get available() {
return this.length - this._index;
}
// Sets index to front of the buffer
front() {
this._index = 0;
return this;
}
// Sets index to end of the buffer
end() {
this._index = this.length;
return this;
}
// Seeks given number of bytes
// Note: Backwards seeking is supported
seek(bytes = 1) {
this.index += bytes;
return this;
}
// Reads sequence of given number of bytes (defaults to number of bytes available)
read(bytes = this.available) {
if (bytes > this.available) {
throw new Error(`Cannot read ${bytes} byte(s), ${this.available} available`);
}
if (bytes <= 0) {
throw new RangeError(`Invalid number of bytes ${bytes}`);
}
const value = new ByteBuffer(this._buffer.slice(this._index, this._index + bytes), this.order);
this._index += bytes;
return value;
}
// Writes sequence of bytes
write(sequence) {
let view;
// Ensure we're dealing with a Uint8Array view
if (!(sequence instanceof Uint8Array)) {
// Extract the buffer from the sequence
const buffer = extractBuffer(sequence);
if (!buffer) {
throw new TypeError(`Cannot write ${sequence}, not a sequence`);
}
// And create a new Uint8Array view for it
view = new Uint8Array(buffer);
} else {
view = sequence;
}
const { available } = this;
if (view.byteLength > available) {
if (this._implicitGrowth) {
this.append(view.byteLength - available);
} else {
throw new Error(`Cannot write ${sequence} using ${view.byteLength} byte(s), ${this.available} available`);
}
}
this._raw.set(view, this._index);
this._index += view.byteLength;
return this;
}
// Reads UTF-8 encoded string of given number of bytes (defaults to number of bytes available)
//
// Based on David Flanagan's BufferView (https://github.com/davidflanagan/BufferView/blob/master/BufferView.js//L195)
readString(bytes = this.available) {
if (bytes > this.available) {
throw new Error(`Cannot read ${bytes} byte(s), ${this.available} available`);
}
if (bytes <= 0) {
throw new RangeError(`Invalid number of bytes ${bytes}`);
}
// Local reference
const raw = this._raw;
// Holds decoded characters
const codepoints = [];
// Index into codepoints
let c = 0;
// Bytes
let b1 = null;
let b2 = null;
let b3 = null;
let b4 = null;
// Target index
const target = this._index + bytes;
while (this._index < target) {
b1 = raw[this._index];
if (b1 < 128) {
// One byte sequence
codepoints[c++] = b1;
this._index++;
} else if (b1 < 194) {
throw new Error('Unexpected continuation byte');
} else if (b1 < 224) {
// Two byte sequence
b2 = raw[this._index + 1];
if (b2 < 128 || b2 > 191) {
throw new Error('Bad continuation byte');
}
codepoints[c++] = ((b1 & 0x1F) << 6) + (b2 & 0x3F);
this._index += 2;
} else if (b1 < 240) {
// Three byte sequence
b2 = raw[this._index + 1];
if (b2 < 128 || b2 > 191) {
throw new Error('Bad continuation byte');
}
b3 = raw[this._index + 2];
if (b3 < 128 || b3 > 191) {
throw new Error('Bad continuation byte');
}
codepoints[c++] = ((b1 & 0x0F) << 12) + ((b2 & 0x3F) << 6) + (b3 & 0x3F);
this._index += 3;
} else if (b1 < 245) {
// Four byte sequence
b2 = raw[this._index + 1];
if (b2 < 128 || b2 > 191) {
throw new Error('Bad continuation byte');
}
b3 = raw[this._index + 2];
if (b3 < 128 || b3 > 191) {
throw new Error('Bad continuation byte');
}
b4 = raw[this._index + 3];
if (b4 < 128 || b4 > 191) {
throw new Error('Bad continuation byte');
}
let cp = ((b1 & 0x07) << 18) + ((b2 & 0x3F) << 12) + ((b3 & 0x3F) << 6) + (b4 & 0x3F);
cp -= 0x10000;
// Turn code point into two surrogate pairs
codepoints[c++] = 0xD800 + ((cp & 0x0FFC00) >>> 10);
codepoints[c++] = 0xDC00 + (cp & 0x0003FF);
this._index += 4;
} else {
throw new Error('Illegal byte');
}
}
// Browsers may have hardcoded or implicit limits on the array length when applying a function
// See: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/apply//apply_and_built-in_functions
const limit = 1 << 16;
const { length } = codepoints;
if (length < limit) {
return String.fromCharCode.apply(String, codepoints);
}
const chars = [];
let i = 0;
while (i < length) {
chars.push(String.fromCharCode.apply(String, codepoints.slice(i, i + limit)));
i += limit;
}
return chars.join('');
}
// Writes UTF-8 encoded string
// Note: Does not write string length or terminator
//
// Based on David Flanagan's BufferView (https://github.com/davidflanagan/BufferView/blob/master/BufferView.js//L264)
writeString(string) {
// Encoded UTF-8 bytes
const bytes = [];
// String length, offset and byte offset
const { length } = string;
let i = 0;
let b = 0;
while (i < length) {
const c = string.charCodeAt(i);
if (c <= 0x7F) {
// One byte sequence
bytes[b++] = c;
} else if (c <= 0x7FF) {
// Two byte sequence
bytes[b++] = 0xC0 | ((c & 0x7C0) >>> 6);
bytes[b++] = 0x80 | (c & 0x3F);
} else if (c <= 0xD7FF || (c >= 0xE000 && c <= 0xFFFF)) {
// Three byte sequence
// Source character is not a UTF-16 surrogate
bytes[b++] = 0xE0 | ((c & 0xF000) >>> 12);
bytes[b++] = 0x80 | ((c & 0x0FC0) >>> 6);
bytes[b++] = 0x80 | (c & 0x3F);
} else {
// Four byte sequence
if (i === length - 1) {
throw new Error(`Unpaired surrogate ${string[i]} (index ${i})`);
}
// Retrieve surrogate
const d = string.charCodeAt(++i);
if (c < 0xD800 || c > 0xDBFF || d < 0xDC00 || d > 0xDFFF) {
throw new Error(`Unpaired surrogate ${string[i]} (index ${i})`);
}
const cp = ((c & 0x03FF) << 10) + (d & 0x03FF) + 0x10000;
bytes[b++] = 0xF0 | ((cp & 0x1C0000) >>> 18);
bytes[b++] = 0x80 | ((cp & 0x03F000) >>> 12);
bytes[b++] = 0x80 | ((cp & 0x000FC0) >>> 6);
bytes[b++] = 0x80 | (cp & 0x3F);
}
++i;
}
this.write(bytes);
return bytes.length;
}
// Aliases for reading/writing UTF-8 encoded strings
// readUTFChars: this.::readString
// writeUTFChars: this.::writeString
// Reads UTF-8 encoded C-string (excluding the actual NULL-byte)
readCString() {
const bytes = this._raw;
let { length } = bytes;
let i = this._index;
while (bytes[i] !== 0x00 && i < length) {
++i;
}
length = i - this._index;
if (length > 0) {
const string = this.readString(length);
this.readByte();
return string;
}
return null;
}
// Writes UTF-8 encoded C-string (NULL-terminated)
writeCString(string) {
let bytes = this.writeString(string);
this.writeByte(0x00);
return ++bytes;
}
// Prepends given number of bytes
prepend(bytes) {
if (bytes <= 0) {
throw new RangeError(`Invalid number of bytes ${bytes}`);
}
const view = new Uint8Array(this.length + bytes);
view.set(this._raw, bytes);
this._index += bytes;
this.buffer = view.buffer;
return this;
}
// Appends given number of bytes
append(bytes) {
if (bytes <= 0) {
throw new RangeError(`Invalid number of bytes ${bytes}`);
}
const view = new Uint8Array(this.length + bytes);
view.set(this._raw, 0);
this.buffer = view.buffer;
return this;
}
// Clips this buffer
clip(begin = this._index, end = this.length) {
if (begin < 0) {
begin = this.length + begin;
}
const buffer = this._buffer.slice(begin, end);
this._index -= begin;
this.buffer = buffer;
return this;
}
// Slices this buffer
slice(begin = 0, end = this.length) {
const slice = new ByteBuffer(this._buffer.slice(begin, end), this.order);
return slice;
}
// Clones this buffer
clone() {
const clone = new ByteBuffer(this._buffer.slice(0), this.order, this.implicitGrowth);
clone.index = this._index;
return clone;
}
// Reverses this buffer
reverse() {
Array.prototype.reverse.call(this._raw);
this._index = 0;
return this;
}
// Array of bytes in this buffer
toArray() {
return Array.prototype.slice.call(this._raw, 0);
}
// Hex representation of this buffer with given spacer
toHex(spacer = ' ') {
return Array.prototype.map.call(this._raw, (byte) => (
`00${byte.toString(16).toUpperCase()}`.slice(-2)
)).join(spacer);
}
// ASCII representation of this buffer with given spacer and optional byte alignment
toASCII(spacer = ' ', align = true, unknown = '\uFFFD') {
const prefix = (align) ? ' ' : '';
return Array.prototype.map.call(this._raw, (byte) => (
(byte < 0x20 || byte > 0x7E) ? prefix + unknown : prefix + String.fromCharCode(byte)
)).join(spacer);
}
}
// Generic reader
const reader = function (method, bytes) {
return function (order = this._order) {
if (bytes > this.available) {
throw new Error(`Cannot read ${bytes} byte(s), ${this.available} available`);
}
const value = this._view[method](this._index, order);
this._index += bytes;
return value;
};
};
// Generic writer
const writer = function (method, bytes) {
return function (value, order = this._order) {
const { available } = this;
if (bytes > available) {
if (this._implicitGrowth) {
this.append(bytes - available);
} else {
throw new Error(`Cannot write ${value} using ${bytes} byte(s), ${available} available`);
}
}
this._view[method](this._index, value, order);
this._index += bytes;
return this;
};
};
// Byte order constants
ByteBuffer.LITTLE_ENDIAN = true;
ByteBuffer.BIG_ENDIAN = false;
// Readers for bytes, shorts, integers, floats and doubles
ByteBuffer.prototype.readByte = reader('getInt8', 1);
ByteBuffer.prototype.readUnsignedByte = reader('getUint8', 1);
ByteBuffer.prototype.readShort = reader('getInt16', 2);
ByteBuffer.prototype.readUnsignedShort = reader('getUint16', 2);
ByteBuffer.prototype.readInt = reader('getInt32', 4);
ByteBuffer.prototype.readUnsignedInt = reader('getUint32', 4);
ByteBuffer.prototype.readFloat = reader('getFloat32', 4);
ByteBuffer.prototype.readDouble = reader('getFloat64', 8);
// Writers for bytes, shorts, integers, floats and doubles
ByteBuffer.prototype.writeByte = writer('setInt8', 1);
ByteBuffer.prototype.writeUnsignedByte = writer('setUint8', 1);
ByteBuffer.prototype.writeShort = writer('setInt16', 2);
ByteBuffer.prototype.writeUnsignedShort = writer('setUint16', 2);
ByteBuffer.prototype.writeInt = writer('setInt32', 4);
ByteBuffer.prototype.writeUnsignedInt = writer('setUint32', 4);
ByteBuffer.prototype.writeFloat = writer('setFloat32', 4);
ByteBuffer.prototype.writeDouble = writer('setFloat64', 8);
export default ByteBuffer;