@jsprismarine/jsbinaryutils
Version:
Basic binary data managing tool written in TypeScript.
671 lines (670 loc) • 20.6 kB
JavaScript
import assert from 'assert';
export default class BinaryStream {
/**
* Creates a new BinaryStream instance.
* @param {Buffer|null|undefined} buffer - The array or Buffer containing binary data.
* @param {number} offset - The initial pointer position.
*/
constructor(buffer, offset = 0) {
this.binary = [];
this.buffer = null;
this.writeIndex = 0;
this.buffer = buffer ?? null; // Keep this instance for reading
this.readIndex = offset;
}
/**
* Reads a slice of buffer by the given length.
* @param {number} len
*/
read(len) {
this.doReadAssertions(len);
return this.buffer.slice(this.readIndex, (this.readIndex += len));
}
write(buf) {
this.binary = [...this.binary, ...buf];
this.writeIndex += buf.byteLength;
}
/**
* Reads an unsigned byte (0 to 255).
* @returns {number}
*/
readByte() {
this.doReadAssertions(1);
return this.buffer.readUInt8(this.readIndex++);
}
/**
* Writes an unsigned byte (0 to 255).
* @param {number} v
*/
writeByte(v) {
v &= 0xff;
this.binary[this.writeIndex++] = v;
}
/**
* Reads a signed byte (-128 to 127).
* @returns {number}
*/
readSignedByte() {
this.doReadAssertions(1);
return this.buffer.readInt8(this.readIndex++);
}
/**
* Writes a signed byte (-128 to 127).
* @param {number} v
*/
writeSignedByte(v) {
if (v < 0)
v = 0xff + v + 1;
this.binary[this.writeIndex++] = v & 0xff;
}
/**
* Reads a boolean (true or false).
* @returns {boolean}
*/
readBoolean() {
this.doReadAssertions(1);
return !!this.readByte();
}
/**
* Writes a boolean (true or false).
* @param {boolean} v
*/
writeBoolean(v) {
this.writeByte(+v);
}
/**
* Reads a 16 bit (2 bytes) signed big-endian number.
* @returns {number}
*/
readShort() {
this.doReadAssertions(2);
return this.buffer.readInt16BE(this.addOffset(2));
}
/**
* Writes a 16 bit (2 bytes) signed big-endian number.
* @param {number} v
*/
writeShort(v) {
this.doWriteAssertions(v, -32768, 32767);
this.writeByte(v >> 8);
this.writeByte(v);
}
/**
* Reads a 16 bit (2 bytes) signed little-endian number.
* @returns {number}
*/
readShortLE() {
this.doReadAssertions(2);
return this.buffer.readInt16LE(this.addOffset(2));
}
/**
* Writes a 16 bit (2 bytes) signed big-endian number.
* @param {number} v
*/
writeShortLE(v) {
this.doWriteAssertions(v, -32768, 32767);
this.writeByte(v);
this.writeByte(v >> 8);
}
/**
* Reads a 16 bit (2 bytes) unsigned big-endian number.
* @returns {number}
*/
readUnsignedShort() {
this.doReadAssertions(2);
return this.buffer.readUInt16BE(this.addOffset(2));
}
/**
* Writes a 16 bit (2 bytes) unsigned big-endian number.
* @param {number} v
*/
writeUnsignedShort(v) {
this.doWriteAssertions(v, 0, 65535);
this.writeByte(v >>> 8);
this.writeByte(v);
}
/**
* Reads a 16 bit (2 bytes) unsigned little-endian number.
* @returns {number}
*/
readUnsignedShortLE() {
this.doReadAssertions(2);
return this.buffer.readUInt16LE(this.addOffset(2));
}
/**
* Writes a 16 bit (2 bytes) unsigned little-endian number.
* @param {number} v
*/
writeUnsignedShortLE(v) {
this.doWriteAssertions(v, 0, 65535);
this.writeByte(v);
this.writeByte(v >>> 8);
}
/**
* Reads a 24 bit (3 bytes) signed big-endian number.
* @returns {number}
*/
readTriad() {
this.doReadAssertions(3);
return this.buffer.readIntBE(this.addOffset(3), 3);
}
/**
* Writes a 24 bit (3 bytes) signed big-endian number.
* @param {number} v
*/
writeTriad(v) {
this.doWriteAssertions(v, -8388608, 8388607);
this.writeByte((v & 0xff0000) >> 16); // msb
this.writeByte((v & 0x00ff00) >> 8); // mib
this.writeByte(v & 0x0000ff); // lsb
}
/**
* Reads a 24 bit (3 bytes) little-endian number.
* @returns {number}
*/
readTriadLE() {
this.doReadAssertions(3);
return this.buffer.readIntLE(this.addOffset(3), 3);
}
/**
* Writes a 24 bit (3 bytes) signed little-endian number.
* @param {number} v
*/
writeTriadLE(v) {
this.doWriteAssertions(v, -8388608, 8388607);
this.writeByte(v & 0x0000ff);
this.writeByte((v & 0x00ff00) >> 8);
this.writeByte((v & 0xff0000) >> 16);
}
/**
* Reads a 24 bit (3 bytes) unsigned big-endian number.
* @returns {number}
*/
readUnsignedTriad() {
this.doReadAssertions(3);
return this.buffer.readUIntBE(this.addOffset(3), 3);
}
/**
* Writes a 24 bit (3 bytes) unsigned big-endian number.
* @param {number} v
*/
writeUnsignedTriad(v) {
this.doWriteAssertions(v, 0, 16777215);
this.writeByte((v & 0xff0000) >>> 16); // msb
this.writeByte((v & 0x00ff00) >>> 8); // mib
this.writeByte(v & 0x0000ff); // lsb
}
/**
* Reads a 24 bit (3 bytes) unsigned little-endian number.
* @returns {number}
*/
readUnsignedTriadLE() {
this.doReadAssertions(3);
return this.buffer.readUIntLE(this.addOffset(3), 3);
}
/**
* Writes a 24 bit (3 bytes) unsigned little-endian number.
* @param {number} v
*/
writeUnsignedTriadLE(v) {
this.doWriteAssertions(v, 0, 16777215);
this.writeByte(v & 0x0000ff);
this.writeByte((v & 0x00ff00) >>> 8);
this.writeByte((v & 0xff0000) >>> 16);
}
/**
* Reads a 32 bit (4 bytes) big-endian signed number.
* @returns {number}
*/
readInt() {
this.doReadAssertions(4);
return this.buffer.readInt32BE(this.addOffset(4));
}
/**
* Writes a 32 bit (4 bytes) big-endian signed number.
* @param {number} v
*/
writeInt(v) {
if (v < 0)
v = v & (0xffffffff + v + 1);
this.doWriteAssertions(v, -2147483648, 2147483647);
this.writeByte(v >> 24);
this.writeByte(v >> 16);
this.writeByte(v >> 8);
this.writeByte(v);
}
/**
* Reads a 32 bit (4 bytes) signed number.
* @returns {number}
*/
readIntLE() {
this.doReadAssertions(4);
return this.buffer.readIntLE(this.addOffset(4), 4);
}
/**
* Writes a 32 bit (4 bytes) little-endian signed number.
* @param {number} v
*/
writeIntLE(v) {
if (v < 0)
v = v & (0xffffffff + v + 1);
this.doWriteAssertions(v, -2147483648, 2147483647);
this.writeByte(v);
this.writeByte(v >> 8);
this.writeByte(v >> 16);
this.writeByte(v >> 24);
}
/**
* Reads a 32 bit (4 bytes) big-endian unsigned number.
* @returns {number}
*/
readUnsignedInt() {
this.doReadAssertions(4);
return this.buffer.readUInt32BE(this.addOffset(4));
}
/**
* Writes a 32 bit (4 bytes) big-endian unsigned number.
* @param {number} v
*/
writeUnsignedInt(v) {
this.doWriteAssertions(v, 0, 4294967295);
this.writeByte(v >>> 24);
this.writeByte(v >>> 16);
this.writeByte(v >>> 8);
this.writeByte(v);
}
/**
* Reads a 32 bit (4 bytes) little-endian unsigned number.
* @returns {number}
*/
readUnsignedIntLE() {
this.doReadAssertions(4);
return this.buffer.readUInt32LE(this.addOffset(4));
}
/**
* Writes a 32 bit (4 bytes) little-endian unsigned number.
* @param {number} v
*/
writeUnsignedIntLE(v) {
this.doWriteAssertions(v, 0, 4294967295);
this.writeByte(v);
this.writeByte(v >>> 8);
this.writeByte(v >>> 16);
this.writeByte(v >>> 24);
}
/**
* Returns a 32 bit (4 bytes) big-endian flating point number.
* @returns {number}
*/
readFloat() {
this.doReadAssertions(4);
return this.buffer.readFloatBE(this.addOffset(4));
}
/**
* Writes a 32 bit (4 bytes) big-endian floating point number.
* @param {number} v
*/
writeFloat(v) {
this.doWriteAssertions(v, -3.4028234663852886e38, +3.4028234663852886e38);
this.write(new Uint8Array(new Float32Array([v]).buffer).reverse());
}
/**
* Returns a 32 bit (4 bytes) big-endian flating point number.
* @returns {number}
*/
readFloatLE() {
this.doReadAssertions(4);
return this.buffer.readFloatLE(this.addOffset(4));
}
/**
* Writes a 32 bit (4 bytes) little-endian floating point number.
* @param {number} v
*/
writeFloatLE(v) {
this.doWriteAssertions(v, -3.4028234663852886e38, +3.4028234663852886e38);
this.write(new Uint8Array(new Float32Array([v]).buffer));
}
/**
* Returns a 64 bit (8 bytes) big-endian flating point number.
* @returns {number}
*/
readDouble() {
this.doReadAssertions(8);
return this.buffer.readDoubleBE(this.addOffset(8));
}
/**
* Writes a 64 bit (8 bytes) big-endian floating point number.
* @param {number} v
*/
writeDouble(v) {
this.doWriteAssertions(v, -1.7976931348623157e308, +1.7976931348623157e308);
this.write(new Uint8Array(new Float64Array([v]).buffer).reverse());
}
/**
* Returns a 64 bit (8 bytes) little-endian flating point number.
* @returns {number}
*/
readDoubleLE() {
this.doReadAssertions(8);
return this.buffer.readDoubleLE(this.addOffset(8));
}
/**
* Writes a 64 bit (8 bytes) little-endian floating point number.
* @param {number} v
*/
writeDoubleLE(v) {
this.doWriteAssertions(v, -1.7976931348623157e308, +1.7976931348623157e308);
this.write(new Uint8Array(new Float64Array([v]).buffer));
}
/**
* Returns a 64 bit (8 bytes) signed big-endian number.
* @returns {bigint}
*/
readLong() {
this.doReadAssertions(8);
return this.buffer.readBigInt64BE(this.addOffset(8));
}
/**
* Writes a 64 bit (8 bytes) signed big-endian number.
* @param {bigint} v
*/
writeLong(v) {
const lo = Number(v & BigInt(0xffffffff));
this.binary[this.writeIndex + 7] = lo;
this.binary[this.writeIndex + 6] = lo >> 8;
this.binary[this.writeIndex + 5] = lo >> 16;
this.binary[this.writeIndex + 4] = lo >> 24;
const hi = Number((v >> BigInt(32)) & BigInt(0xffffffff));
this.binary[this.writeIndex + 3] = hi;
this.binary[this.writeIndex + 2] = hi >> 8;
this.binary[this.writeIndex + 1] = hi >> 16;
this.binary[this.writeIndex] = hi >> 24;
this.writeIndex += 8;
}
/**
* Returns a 64 bit (8 bytes) signed little-endian number.
* @returns {bigint}
*/
readLongLE() {
this.doReadAssertions(8);
return this.buffer.readBigInt64LE(this.addOffset(8));
}
/**
* Writes a 64 bit (8 bytes) signed big-endian number.
* @param {bigint} v
*/
writeLongLE(v) {
const lo = Number(v & BigInt(0xffffffff));
this.binary[this.writeIndex++] = lo;
this.binary[this.writeIndex++] = lo >> 8;
this.binary[this.writeIndex++] = lo >> 16;
this.binary[this.writeIndex++] = lo >> 24;
const hi = Number((v >> BigInt(32)) & BigInt(0xffffffff));
this.binary[this.writeIndex++] = hi;
this.binary[this.writeIndex++] = hi >> 8;
this.binary[this.writeIndex++] = hi >> 16;
this.binary[this.writeIndex++] = hi >> 24;
}
/**
* Returns a 64 bit (8 bytes) unsigned big-endian number.
* @returns {bigint}
*/
readUnsignedLong() {
this.doReadAssertions(8);
return this.buffer.readBigUInt64BE(this.addOffset(8));
}
/**
* Writes a 64 bit (8 bytes) unsigned big-endian number.
* @param {bigint} v
*/
writeUnsignedLong(v) {
const lo = Number(v & BigInt(0xffffffff));
this.binary[this.writeIndex + 7] = lo;
this.binary[this.writeIndex + 6] = lo >> 8;
this.binary[this.writeIndex + 5] = lo >> 16;
this.binary[this.writeIndex + 4] = lo >> 24;
const hi = Number((v >> BigInt(32)) & BigInt(0xffffffff));
this.binary[this.writeIndex + 3] = hi;
this.binary[this.writeIndex + 2] = hi >> 8;
this.binary[this.writeIndex + 1] = hi >> 16;
this.binary[this.writeIndex] = hi >> 24;
this.writeIndex += 8;
}
/**
* Returns a 64 bit (8 bytes) unsigned little-endian number.
* @returns {bigint}
*/
readUnsignedLongLE() {
this.doReadAssertions(8);
return this.buffer.readBigUInt64LE(this.addOffset(8));
}
/**
* Writes a 64 bit (8 bytes) unsigned big-endian number.
* @param {bigint} v
*/
writeUnsignedLongLE(v) {
const lo = Number(v & BigInt(0xffffffff));
this.binary[this.writeIndex++] = lo;
this.binary[this.writeIndex++] = lo >> 8;
this.binary[this.writeIndex++] = lo >> 16;
this.binary[this.writeIndex++] = lo >> 24;
const hi = Number((v >> BigInt(32)) & BigInt(0xffffffff));
this.binary[this.writeIndex++] = hi;
this.binary[this.writeIndex++] = hi >> 8;
this.binary[this.writeIndex++] = hi >> 16;
this.binary[this.writeIndex++] = hi >> 24;
}
/**
* Reads a 32 bit (4 bytes) zigzag-encoded number.
* @returns {number}
*/
readVarInt() {
const raw = this.readUnsignedVarInt();
const temp = (((raw << 63) >> 63) ^ raw) >> 1;
return temp ^ (raw & (1 << 63));
}
/**
* Writes a 32 bit (4 bytes) zigzag-encoded number.
* @param {number} v
*/
writeVarInt(v) {
v = (v << 32) >> 32;
return this.writeUnsignedVarInt((v << 1) ^ (v >> 31));
}
/**
* Reads a 32 bit unsigned number.
* @returns {number}
*/
readUnsignedVarInt() {
assert(this.buffer != null, 'Reading on empty buffer!');
let value = 0;
for (let i = 0; i <= 28; i += 7) {
if (typeof this.buffer[this.readIndex] === 'undefined') {
throw new Error('No bytes left in buffer');
}
let b = this.readByte();
value |= (b & 0x7f) << i;
if ((b & 0x80) === 0) {
return value;
}
}
throw new Error('VarInt did not terminate after 5 bytes!');
}
/**
* Writes a 32 bit unsigned number with variable-length.
* @param {number} v
*/
writeUnsignedVarInt(v) {
while ((v & 0xffffff80) !== 0) {
this.writeByte((v & 0x7f) | 0x80);
v >>>= 7;
}
this.writeByte(v & 0x7f);
}
/**
* Reads a 64 bit zigzag-encoded variable-length number.
* @returns {bigint}
*/
readVarLong() {
const raw = this.readUnsignedVarLong();
return raw >> 1n;
}
/**
* Writes a 64 bit unsigned zigzag-encoded number.
* @param {bigint} v
*/
writeVarLong(v) {
return this.writeUnsignedVarLong((v << 1n) ^ (v >> 63n));
}
/**
* Reads a 64 bit unsigned variable-length number.
* @returns {bigint}
*/
readUnsignedVarLong() {
let value = 0n;
for (let i = 0; i <= 63; i += 7) {
if (this.feof()) {
throw new Error('No bytes left in buffer');
}
const b = this.readByte();
value |= (BigInt(b) & 0x7fn) << BigInt(i);
if ((b & 0x80) === 0) {
return value;
}
}
throw new Error('VarLong did not terminate after 10 bytes!');
}
/**
* Writes a 64 bit unsigned variable-length number.
* @param {bigint} v
*/
writeUnsignedVarLong(v) {
for (let i = 0; i < 10; ++i) {
if (v >> 7n !== 0n) {
this.writeByte(Number(v | 0x80n));
}
else {
this.writeByte(Number(v & 0x7fn));
break;
}
v >>= 7n;
}
}
/**
* Increases the write offset by the given length.
* @param {number} length
*/
addOffset(length) {
return (this.readIndex += length) - length;
}
/**
* Returns whatever or not the read offset is at end of line.
* @returns {number}
*/
feof() {
if (!this.buffer)
throw new Error('Buffer is write only!');
return typeof this.buffer[this.readIndex] === 'undefined';
}
/**
* Reads the remaining bytes and returns the buffer slice.
* @returns {Buffer}
*/
readRemaining() {
if (!this.buffer)
throw new Error('Buffer is write only!');
const buf = this.buffer.slice(this.readIndex);
this.readIndex = this.buffer.byteLength;
return buf;
}
/**
* Skips len bytes on the buffer.
* @param {number} len
*/
skip(len) {
assert(Number.isInteger(len), 'Cannot skip a float amount of bytes');
this.readIndex += len;
}
/**
* Returns the encoded buffer.
* @returns {Buffer}
*/
getBuffer() {
return this.buffer !== null ? this.buffer : Buffer.from(this.binary);
}
/**
* Sets the buffer for reading.
* make sure to reset the reading index!
* @param buf - The new Buffer.
*/
setBuffer(buf) {
this.buffer = buf;
}
/**
* Clears the whole BinaryStream instance.
*/
clear() {
this.buffer = null;
this.binary = [];
this.readIndex = 0;
this.writeIndex = 0;
}
/**
* Conventional method to reuse the stream
* without having to create a new BinaryStream instance.
* @param buf - The new buffer instance.
*/
reuse(buf) {
this.buffer = buf;
this.binary = [];
this.readIndex = 0;
this.writeIndex = 0;
}
/**
* Sets the reading index.
* @param index - The new read index.
*/
setReadIndex(index) {
assert(index > 0, 'Index must be a positive integer');
this.readIndex = index;
}
/**
* Sets the new writing index.
* @param index - The new write index.
*/
setWriteIndex(index) {
assert(index > 0, 'Index must be a positive integer');
this.writeIndex = index;
}
/**
* Retuns the read index.
* @returns {number}
*/
getReadIndex() {
return this.readIndex;
}
/**
* Returns the write index.
* @returns {number}
*/
getWriteIndex() {
return this.writeIndex;
}
/**
* Do read assertions, check if the read buffer is null.
* @param {number} byteLength
*/
doReadAssertions(byteLength) {
assert(this.buffer !== null, 'Cannot read without buffer data!');
assert(this.buffer.byteLength >= byteLength, 'Cannot read without buffer data!');
}
/**
* Do read assertions, check if the read buffer is null.
* @param {number|bigint} num
* @param {number|bigint} minVal
* @param {number|bigint} maxVal
*/
doWriteAssertions(num, minVal, maxVal) {
assert(num >= minVal && num <= maxVal, `Value out of bounds: value=${num}, min=${minVal}, max=${maxVal}`);
}
}