443 lines
9.3 KiB
TypeScript
443 lines
9.3 KiB
TypeScript
import { UUID } from "./UUID";
|
|
|
|
export class BinaryReader
|
|
{
|
|
public pos = 0;
|
|
|
|
public constructor(private readonly buf: Buffer)
|
|
{
|
|
|
|
}
|
|
|
|
public seek(pos: number): void
|
|
{
|
|
if (pos < 0 || pos > this.buf.length)
|
|
{
|
|
throw new RangeError(`Invalid seek position: ${pos}`);
|
|
}
|
|
this.pos = pos;
|
|
}
|
|
|
|
public getPos(): number
|
|
{
|
|
return this.pos;
|
|
}
|
|
|
|
public peekUInt8(): number
|
|
{
|
|
this.checkBounds(1);
|
|
return this.buf.readUInt8(this.pos);
|
|
}
|
|
|
|
public peekInt8(): number
|
|
{
|
|
this.checkBounds(1);
|
|
return this.buf.readInt8(this.pos);
|
|
}
|
|
|
|
public peekUInt16LE(): number
|
|
{
|
|
this.checkBounds(2);
|
|
return this.buf.readUInt16LE(this.pos);
|
|
}
|
|
|
|
public peekInt16LE(): number
|
|
{
|
|
this.checkBounds(2);
|
|
return this.buf.readInt16LE(this.pos);
|
|
}
|
|
|
|
public peekUInt16BE(): number
|
|
{
|
|
this.checkBounds(2);
|
|
return this.buf.readUInt16BE(this.pos);
|
|
}
|
|
|
|
public peekInt16BE(): number
|
|
{
|
|
this.checkBounds(2);
|
|
return this.buf.readInt16BE(this.pos);
|
|
}
|
|
|
|
public peekUInt32LE(): number
|
|
{
|
|
this.checkBounds(4);
|
|
return this.buf.readUInt32LE(this.pos);
|
|
}
|
|
|
|
public peekInt32LE(): number
|
|
{
|
|
this.checkBounds(4);
|
|
return this.buf.readInt32LE(this.pos);
|
|
}
|
|
|
|
public peekUInt32BE(): number
|
|
{
|
|
this.checkBounds(4);
|
|
return this.buf.readUInt32BE(this.pos);
|
|
}
|
|
|
|
public peekInt32BE(): number
|
|
{
|
|
this.checkBounds(4);
|
|
return this.buf.readInt32BE(this.pos);
|
|
}
|
|
|
|
public peekUInt64LE(): bigint
|
|
{
|
|
this.checkBounds(8);
|
|
return this.buf.readBigUInt64LE(this.pos);
|
|
}
|
|
|
|
public peekInt64LE(): bigint
|
|
{
|
|
this.checkBounds(8);
|
|
return this.buf.readBigInt64LE(this.pos);
|
|
}
|
|
|
|
public peekUInt64BE(): bigint
|
|
{
|
|
this.checkBounds(8);
|
|
return this.buf.readBigUInt64BE(this.pos);
|
|
}
|
|
|
|
public peekInt64BE(): bigint
|
|
{
|
|
this.checkBounds(8);
|
|
return this.buf.readBigInt64BE(this.pos);
|
|
}
|
|
|
|
public peekFloatLE(): number
|
|
{
|
|
this.checkBounds(4);
|
|
return this.buf.readFloatLE(this.pos);
|
|
}
|
|
|
|
public peekFloatBE(): number
|
|
{
|
|
this.checkBounds(4);
|
|
return this.buf.readFloatBE(this.pos);
|
|
}
|
|
|
|
public peekDoubleLE(): number
|
|
{
|
|
this.checkBounds(8);
|
|
return this.buf.readDoubleLE(this.pos);
|
|
}
|
|
|
|
public peekDoubleBE(): number
|
|
{
|
|
this.checkBounds(8);
|
|
return this.buf.readDoubleBE(this.pos);
|
|
}
|
|
|
|
public peekUUID(): UUID
|
|
{
|
|
this.checkBounds(16);
|
|
return new UUID(this.buf, this.pos);
|
|
}
|
|
|
|
public peekDate(): Date
|
|
{
|
|
this.checkBounds(8);
|
|
return new Date(Number(this.peekUInt64LE()));
|
|
}
|
|
|
|
public peekBuffer(length: number): Buffer
|
|
{
|
|
if (this.pos + length > this.buf.length)
|
|
{
|
|
throw new RangeError("Attempt to read beyond buffer length");
|
|
}
|
|
return this.buf.subarray(this.pos, this.pos + length);
|
|
}
|
|
|
|
public peekVarInt(): { value: number | bigint; bytesRead: number }
|
|
{
|
|
let value = 0n;
|
|
let bytesRead = 0;
|
|
let shift = 0n;
|
|
|
|
while (this.pos + bytesRead < this.buf.length)
|
|
{
|
|
const byte = this.buf[this.pos + bytesRead];
|
|
bytesRead++;
|
|
|
|
const byteValue = BigInt(byte & 0x7F);
|
|
value |= (byteValue << shift);
|
|
|
|
if ((byte & 0x80) === 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
shift += 7n;
|
|
|
|
if (bytesRead > 100)
|
|
{
|
|
throw new Error('VarInt is too long');
|
|
}
|
|
}
|
|
|
|
if (bytesRead === 0 || (this.pos + bytesRead) > this.buf.length)
|
|
{
|
|
throw new Error('Incomplete VarInt');
|
|
}
|
|
|
|
const decoded = (value >> 1n) ^ -(value & 1n);
|
|
|
|
if (
|
|
decoded >= BigInt(Number.MIN_SAFE_INTEGER) &&
|
|
decoded <= BigInt(Number.MAX_SAFE_INTEGER)
|
|
)
|
|
{
|
|
return {value: Number(decoded), bytesRead};
|
|
}
|
|
else
|
|
{
|
|
return {value: decoded, bytesRead};
|
|
}
|
|
}
|
|
|
|
public peekString(metadata?: { length: number | bigint, bytesRead: number }): string
|
|
{
|
|
const {value: length, bytesRead} = this.peekVarInt();
|
|
if (length < 0)
|
|
{
|
|
throw new Error('Error reading string: Length is negative');
|
|
}
|
|
this.checkBounds(bytesRead + Number(length));
|
|
const start = this.pos + bytesRead;
|
|
const end = start + Number(length);
|
|
const stringData = this.buf.subarray(start, end);
|
|
if (metadata)
|
|
{
|
|
metadata.length = length;
|
|
metadata.bytesRead = bytesRead;
|
|
}
|
|
return new TextDecoder("utf-8").decode(stringData);
|
|
}
|
|
|
|
public peekCString(): string
|
|
{
|
|
let tempPos = this.pos;
|
|
const start = tempPos;
|
|
|
|
while (tempPos < this.buf.length && this.buf[tempPos] !== 0)
|
|
{
|
|
tempPos++;
|
|
}
|
|
|
|
if (tempPos >= this.buf.length)
|
|
{
|
|
throw new RangeError("Null-terminated string not found");
|
|
}
|
|
|
|
const stringData = this.buf.subarray(start, tempPos);
|
|
return new TextDecoder("utf-8").decode(stringData);
|
|
}
|
|
|
|
public peekFixedString(length: number): string
|
|
{
|
|
this.checkBounds(length);
|
|
return this.buf.subarray(this.pos, this.pos + length).toString('utf-8');
|
|
}
|
|
|
|
// read
|
|
|
|
public readUInt8(): number
|
|
{
|
|
const num = this.peekUInt8();
|
|
this.pos++;
|
|
return num;
|
|
}
|
|
|
|
public readInt8(): number
|
|
{
|
|
const num = this.peekInt8();
|
|
this.pos++;
|
|
return num;
|
|
}
|
|
|
|
public readUInt16LE(): number
|
|
{
|
|
const num = this.peekUInt16LE();
|
|
this.pos += 2;
|
|
return num;
|
|
}
|
|
|
|
public readInt16LE(): number
|
|
{
|
|
const num = this.peekInt16LE();
|
|
this.pos += 2;
|
|
return num;
|
|
}
|
|
|
|
public readUInt16BE(): number
|
|
{
|
|
const num = this.peekUInt16BE();
|
|
this.pos += 2;
|
|
return num;
|
|
}
|
|
|
|
public readInt16BE(): number
|
|
{
|
|
const num = this.peekInt16BE();
|
|
this.pos += 2;
|
|
return num;
|
|
}
|
|
|
|
public readUInt32LE(): number
|
|
{
|
|
const num = this.peekUInt32LE();
|
|
this.pos += 4;
|
|
return num;
|
|
}
|
|
|
|
public readInt32LE(): number
|
|
{
|
|
const num = this.peekInt32LE();
|
|
this.pos += 4;
|
|
return num;
|
|
}
|
|
|
|
public readUInt32BE(): number
|
|
{
|
|
const num = this.peekUInt32BE();
|
|
this.pos += 4;
|
|
return num;
|
|
}
|
|
|
|
public readInt32BE(): number
|
|
{
|
|
const num = this.peekInt32BE();
|
|
this.pos += 4;
|
|
return num;
|
|
}
|
|
|
|
|
|
public readUInt64LE(): bigint
|
|
{
|
|
const num = this.peekUInt64LE();
|
|
this.pos += 8;
|
|
return num;
|
|
}
|
|
|
|
public readInt64LE(): bigint
|
|
{
|
|
const num = this.peekInt64LE();
|
|
this.pos += 8;
|
|
return num;
|
|
}
|
|
|
|
public readUInt64BE(): bigint
|
|
{
|
|
const num = this.peekUInt64BE();
|
|
this.pos += 8;
|
|
return num;
|
|
}
|
|
|
|
public readInt64BE(): bigint
|
|
{
|
|
const num = this.peekInt64BE();
|
|
this.pos += 8;
|
|
return num;
|
|
}
|
|
|
|
public readFloatLE(): number
|
|
{
|
|
const num = this.peekFloatLE();
|
|
this.pos += 4;
|
|
return num;
|
|
}
|
|
|
|
public readFloatBE(): number
|
|
{
|
|
const num = this.peekFloatBE();
|
|
this.pos += 4;
|
|
return num;
|
|
}
|
|
|
|
public readDoubleLE(): number
|
|
{
|
|
const num = this.peekDoubleLE();
|
|
this.pos += 8;
|
|
return num;
|
|
}
|
|
|
|
public readDoubleBE(): number
|
|
{
|
|
const num = this.peekDoubleBE();
|
|
this.pos += 8;
|
|
return num;
|
|
}
|
|
|
|
public readUUID(): UUID
|
|
{
|
|
const uuid = this.peekUUID();
|
|
this.pos += 16;
|
|
return uuid;
|
|
}
|
|
|
|
public readDate(): Date
|
|
{
|
|
const d = this.peekDate();
|
|
this.pos += 8;
|
|
return d;
|
|
}
|
|
|
|
public readBuffer(length: number): Buffer
|
|
{
|
|
const buffer = this.peekBuffer(length);
|
|
this.pos += length;
|
|
return buffer;
|
|
}
|
|
|
|
public readCString(): string
|
|
{
|
|
const str = this.peekCString();
|
|
this.pos += Buffer.byteLength(str, "utf-8") + 1; // Include null terminator
|
|
return str;
|
|
}
|
|
|
|
public readString(): string
|
|
{
|
|
const md: {
|
|
length: number | bigint,
|
|
bytesRead: number
|
|
} = {
|
|
length: 0,
|
|
bytesRead: 0
|
|
}
|
|
const str = this.peekString(md);
|
|
this.pos += md.bytesRead + Number(md.length);
|
|
return str;
|
|
}
|
|
|
|
public readFixedString(length: number): string
|
|
{
|
|
const str = this.peekFixedString(length);
|
|
this.pos += length;
|
|
return str;
|
|
}
|
|
|
|
public length(): number
|
|
{
|
|
return this.buf.length;
|
|
}
|
|
|
|
public readVarInt(): number | bigint
|
|
{
|
|
const {value, bytesRead} = this.peekVarInt();
|
|
this.pos += bytesRead;
|
|
return value;
|
|
}
|
|
|
|
private checkBounds(length: number): void
|
|
{
|
|
if (this.pos + length > this.buf.length)
|
|
{
|
|
throw new RangeError(`Attempt to read beyond buffer length: position=${this.pos}, length=${length}, bufferSize=${this.buf.length}`);
|
|
}
|
|
}
|
|
} |