Prepared for release
This commit is contained in:
772
src/index.ts
Normal file
772
src/index.ts
Normal file
@@ -0,0 +1,772 @@
|
||||
/**
|
||||
* A utility class for reading various primitive types and strings from an ArrayBuffer,
|
||||
* supporting configurable byte order (endianness).
|
||||
*/
|
||||
export class Reader {
|
||||
private buffer: ArrayBuffer;
|
||||
private view: DataView;
|
||||
private offset: number;
|
||||
private littleEndian: boolean;
|
||||
|
||||
constructor(buffer: ArrayBuffer | ArrayBufferLike | Uint8Array, littleEndian: boolean = true) {
|
||||
if (buffer instanceof ArrayBuffer) {
|
||||
this.buffer = buffer;
|
||||
} else if (buffer instanceof Uint8Array || ArrayBuffer.isView(buffer)) {
|
||||
const srcBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
||||
this.buffer = srcBuffer instanceof ArrayBuffer ? srcBuffer : new ArrayBuffer(srcBuffer.byteLength);
|
||||
} else {
|
||||
throw new TypeError('Invalid buffer type. Expected ArrayBuffer, Uint8Array, or ArrayBufferView.');
|
||||
}
|
||||
this.view = new DataView(this.buffer);
|
||||
this.offset = 0;
|
||||
this.littleEndian = littleEndian;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a boolean value from the buffer. A boolean is stored as a single byte, where 0 represents
|
||||
* `false` and any non-zero value represents `true`.
|
||||
*
|
||||
* @returns A boolean value read from the buffer.
|
||||
*/
|
||||
public bool(): boolean {
|
||||
this.checkBounds(1);
|
||||
const value = this.view.getUint8(this.offset);
|
||||
this.offset += 1;
|
||||
return value !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an 8-bit signed integer from the buffer at the current offset, and advance the offset by 1 byte.
|
||||
*
|
||||
* @returns An 8-bit signed integer read from the buffer.
|
||||
*/
|
||||
public int8(): number {
|
||||
this.checkBounds(1);
|
||||
const value = this.view.getInt8(this.offset);
|
||||
this.offset += 1;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a 16-bit signed integer from the buffer at the current offset, using the specified byte order, and
|
||||
* advance the offset by 2 bytes.
|
||||
*
|
||||
* @returns A 16-bit signed integer read from the buffer.
|
||||
*/
|
||||
public int16(): number {
|
||||
this.checkBounds(2);
|
||||
const value = this.view.getInt16(this.offset, this.littleEndian);
|
||||
this.offset += 2;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a 32-bit signed integer from the buffer at the current offset, using the specified byte order, and
|
||||
* advance the offset by 4 bytes.
|
||||
*
|
||||
* @returns A 32-bit signed integer read from the buffer.
|
||||
*/
|
||||
public int32(): number {
|
||||
this.checkBounds(4);
|
||||
const value = this.view.getInt32(this.offset, this.littleEndian);
|
||||
this.offset += 4;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a 64-bit signed integer from the buffer at the current offset, using the specified byte order, and
|
||||
* advance the offset by 8 bytes.
|
||||
*
|
||||
* @returns A 64-bit signed integer read from the buffer.
|
||||
*/
|
||||
public int64(): bigint {
|
||||
this.checkBounds(8);
|
||||
const value = this.view.getBigInt64(this.offset, this.littleEndian);
|
||||
this.offset += 8;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an 8-bit unsigned integer from the buffer at the current offset, and advance the offset by 1 byte.
|
||||
*
|
||||
* @returns An 8-bit unsigned integer read from the buffer.
|
||||
*/
|
||||
public uint8(): number {
|
||||
this.checkBounds(1);
|
||||
const value = this.view.getUint8(this.offset);
|
||||
this.offset += 1;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a 16-bit unsigned integer from the buffer at the current offset, using the specified byte order, and
|
||||
* advance the offset by 2 bytes.
|
||||
*
|
||||
* @returns A 16-bit unsigned integer read from the buffer.
|
||||
*/
|
||||
public uint16(): number {
|
||||
this.checkBounds(2);
|
||||
const value = this.view.getUint16(this.offset, this.littleEndian);
|
||||
this.offset += 2;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a 32-bit unsigned integer from the buffer at the current offset, using the specified byte order, and
|
||||
* advance the offset by 4 bytes.
|
||||
*
|
||||
* @returns A 32-bit unsigned integer read from the buffer.
|
||||
*/
|
||||
public uint32(): number {
|
||||
this.checkBounds(4);
|
||||
const value = this.view.getUint32(this.offset, this.littleEndian);
|
||||
this.offset += 4;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a 64-bit unsigned integer from the buffer at the current offset, using the specified byte order, and
|
||||
* advance the offset by 8 bytes.
|
||||
*
|
||||
* @returns A 64-bit unsigned integer read from the buffer.
|
||||
*/
|
||||
public uint64(): bigint {
|
||||
this.checkBounds(8);
|
||||
const value = this.view.getBigUint64(this.offset, this.littleEndian);
|
||||
this.offset += 8;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a 32-bit floating point number from the buffer at the current offset, using the specified byte order, and
|
||||
* advance the offset by 4 bytes.
|
||||
*
|
||||
* @returns A 32-bit floating point number read from the buffer.
|
||||
*/
|
||||
public float32(): number {
|
||||
this.checkBounds(4);
|
||||
const value = this.view.getFloat32(this.offset, this.littleEndian);
|
||||
this.offset += 4;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a 64-bit floating point number from the buffer at the current offset, using the specified byte order, and
|
||||
* advance the offset by 8 bytes.
|
||||
*
|
||||
* @returns A 64-bit floating point number read from the buffer.
|
||||
*/
|
||||
public float64(): number {
|
||||
this.checkBounds(8);
|
||||
const value = this.view.getFloat64(this.offset, this.littleEndian);
|
||||
this.offset += 8;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a variable-length unsigned integer from the buffer using LEB128 encoding. The method reads
|
||||
* bytes until it encounters a byte with the most significant bit (MSB) set to 0, which indicates
|
||||
* the end of the varint.
|
||||
*
|
||||
* The value is constructed by concatenating the lower 7 bits of each byte, and shifting them
|
||||
* according to their position.
|
||||
*
|
||||
* @returns A number representing the decoded unsigned varint value.
|
||||
*/
|
||||
public varint(): number {
|
||||
let result = 0;
|
||||
let shift = 0;
|
||||
while (true) {
|
||||
this.checkBounds(1);
|
||||
const byte = this.view.getUint8(this.offset++);
|
||||
result |= (byte & 0x7F) << shift;
|
||||
if ((byte & 0x80) === 0) {
|
||||
break; // Last byte of the varint
|
||||
}
|
||||
shift += 7;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a variable-length signed integer from the buffer using LEB128 encoding. The method reads
|
||||
* bytes until it encounters a byte with the most significant bit (MSB) set to 0, which indicates
|
||||
* the end of the varint.
|
||||
*
|
||||
* The value is constructed by concatenating the lower 7 bits of each byte, and shifting them
|
||||
* according to their position. If the sign bit (the highest bit of the last byte) is set, the
|
||||
* result is sign-extended to produce a negative number.
|
||||
*
|
||||
* @returns A number representing the decoded signed varint value.
|
||||
*/
|
||||
public varsint(): number {
|
||||
let result = 0;
|
||||
let shift = 0;
|
||||
while (true) {
|
||||
this.checkBounds(1);
|
||||
const byte = this.view.getUint8(this.offset++);
|
||||
result |= (byte & 0x7F) << shift;
|
||||
if ((byte & 0x80) === 0) {
|
||||
break; // Last byte of the varint
|
||||
}
|
||||
shift += 7;
|
||||
}
|
||||
// Sign-extend the result if the sign bit is set
|
||||
if (shift < 32 && (result & (1 << (shift - 1))) !== 0) {
|
||||
result |= ~0 << shift;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a 32-bit date (stored as 32-bit seconds since epoch).
|
||||
*
|
||||
* @returns A Date object representing the date/time read from the buffer.
|
||||
*/
|
||||
public date32(): Date {
|
||||
this.checkBounds(4);
|
||||
const timestamp = this.view.getUint32(this.offset, this.littleEndian);
|
||||
this.offset += 4;
|
||||
return new Date(timestamp * 1000); // Convert seconds to milliseconds
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a 64-bit date (stored as 64-bit milliseconds since epoch).
|
||||
*
|
||||
* @returns A Date object representing the date/time read from the buffer.
|
||||
*/
|
||||
public date64(): Date {
|
||||
this.checkBounds(8);
|
||||
const timestamp = this.view.getBigUint64(this.offset, this.littleEndian);
|
||||
this.offset += 8;
|
||||
return new Date(Number(timestamp)); // Convert bigint to number (ms since epoch)
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a sequence of bytes from the buffer. If length is not provided, reads until the end of the buffer.
|
||||
*
|
||||
* @param length The number of bytes to read. If not provided, reads until the end of the buffer.
|
||||
* @returns A Uint8Array containing the bytes read from the buffer.
|
||||
*/
|
||||
public bytes(length?: number): Uint8Array {
|
||||
if (length === undefined) {
|
||||
length = this.view.byteLength - this.offset;
|
||||
}
|
||||
this.checkBounds(length);
|
||||
const bytes = new Uint8Array(this.view.buffer, this.offset, length);
|
||||
this.offset += length;
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a null-terminated C-style string from the buffer. Reads bytes until a null terminator
|
||||
* (0 byte) is found or the end of the buffer is reached, and decodes them as a UTF-8 string.
|
||||
*
|
||||
* @returns A string containing the decoded characters up to the null terminator or the end of the buffer.
|
||||
*/
|
||||
public cString(): string {
|
||||
const bytes = [];
|
||||
while (this.offset < this.view.byteLength) {
|
||||
const byte = this.view.getUint8(this.offset++);
|
||||
if (byte === 0) {
|
||||
break; // Null terminator found
|
||||
}
|
||||
bytes.push(byte);
|
||||
}
|
||||
return new TextDecoder().decode(new Uint8Array(bytes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a UTF-8 encoded string from the buffer. If length is provided, reads that many bytes and
|
||||
* decodes them as a UTF-8 string.
|
||||
*
|
||||
* @param length The number of bytes to read. If not provided, reads until the end of the buffer.
|
||||
* @returns A string containing the decoded characters.
|
||||
*/
|
||||
public utf8String(length?: number): string {
|
||||
if (length === undefined) {
|
||||
length = this.view.byteLength - this.offset;
|
||||
}
|
||||
this.checkBounds(length);
|
||||
const bytes = new Uint8Array(this.view.buffer, this.offset, length);
|
||||
this.offset += length;
|
||||
return new TextDecoder().decode(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an array of 16-bit words.
|
||||
*
|
||||
* @param length The number of 16-bit words to read.
|
||||
* @returns A Uint16Array containing the words read from the buffer.
|
||||
*/
|
||||
public words(length: number): Uint16Array {
|
||||
this.checkBounds(length * 2);
|
||||
const bytes = new Uint8Array(this.view.buffer, this.offset, length * 2);
|
||||
const copy = bytes.slice();
|
||||
const words = new Uint16Array(copy.buffer);
|
||||
this.offset += length * 2;
|
||||
return words;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an array of 32-bit double words.
|
||||
*
|
||||
* @param length The number of 32-bit double words to read.
|
||||
* @returns A Uint32Array containing the double words read from the buffer.
|
||||
*/
|
||||
public dwords(length: number): Uint32Array {
|
||||
this.checkBounds(length * 4);
|
||||
const bytes = new Uint8Array(this.view.buffer, this.offset, length * 4);
|
||||
const copy = bytes.slice();
|
||||
const dwords = new Uint32Array(copy.buffer);
|
||||
this.offset += length * 4;
|
||||
return dwords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an array of 64-bit quad words.
|
||||
*
|
||||
* @param length The number of 64-bit quad words to read.
|
||||
* @returns A BigUint64Array containing the quad words read from the buffer.
|
||||
*/
|
||||
public qwords(length: number): BigUint64Array {
|
||||
this.checkBounds(length * 8);
|
||||
const bytes = new Uint8Array(this.view.buffer, this.offset, length * 8);
|
||||
const copy = bytes.slice();
|
||||
const qwords = new BigUint64Array(copy.buffer);
|
||||
this.offset += length * 8;
|
||||
return qwords;
|
||||
}
|
||||
|
||||
public static fromBytes(bytes: Uint8Array, littleEndian: boolean = true): Reader {
|
||||
return new Reader(bytes, littleEndian);
|
||||
}
|
||||
|
||||
public static fromString(str: string, encoding: 'utf8' | 'ascii' | 'hex' | 'base64' | 'rawbase64' | 'urlbase64' | 'rawurlbase64' = 'utf8', littleEndian: boolean = true): Reader {
|
||||
let bytes: Uint8Array;
|
||||
switch (encoding) {
|
||||
case 'utf8':
|
||||
bytes = new TextEncoder().encode(str);
|
||||
break;
|
||||
case 'ascii':
|
||||
bytes = new Uint8Array(str.split('').map(c => c.charCodeAt(0)));
|
||||
break;
|
||||
case 'hex':
|
||||
bytes = new Uint8Array(str.length / 2);
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
bytes[i] = parseInt(str.substr(i * 2, 2), 16);
|
||||
}
|
||||
break;
|
||||
case 'base64':
|
||||
bytes = Uint8Array.from(atob(str), c => c.charCodeAt(0));
|
||||
break;
|
||||
case 'rawbase64':
|
||||
bytes = Uint8Array.from(atob(str.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));
|
||||
break;
|
||||
case 'urlbase64':
|
||||
bytes = Uint8Array.from(atob(str.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));
|
||||
break;
|
||||
case 'rawurlbase64':
|
||||
bytes = Uint8Array.from(atob(str.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));
|
||||
break;
|
||||
}
|
||||
return new Reader(bytes.slice().buffer, littleEndian);
|
||||
}
|
||||
|
||||
/* Aliases */
|
||||
|
||||
/* Alias for bool */
|
||||
public flag(): boolean {
|
||||
return this.bool();
|
||||
}
|
||||
|
||||
/* Alias for utf8String */
|
||||
public string(length: number): string {
|
||||
return this.utf8String(length);
|
||||
}
|
||||
|
||||
private checkBounds(length: number) {
|
||||
if (this.offset + length > this.view.byteLength) {
|
||||
throw new RangeError(`Attempt to read beyond end of buffer: offset=${this.offset}, length=${length}, bufferLength=${this.view.byteLength}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A utility class for writing various primitive types and strings into an ArrayBuffer,
|
||||
* supporting configurable byte order (endianness).
|
||||
*
|
||||
* @remarks
|
||||
* The `BufferWriter` provides methods to write signed and unsigned integers, floating-point numbers,
|
||||
* booleans, dates, raw bytes, and strings into an internal buffer. The byte order for multi-byte
|
||||
* values is determined by the provided `ByteOrder` implementation.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const writer = new BufferWriter(16, LittleEndian);
|
||||
* writer.int32(42);
|
||||
* writer.string("hello");
|
||||
* ```
|
||||
*/
|
||||
export class Writer {
|
||||
private buffer: ArrayBuffer;
|
||||
private view: DataView;
|
||||
private offset: number;
|
||||
private littleEndian: boolean;
|
||||
|
||||
constructor(size: number, littleEndian: boolean = true) {
|
||||
this.buffer = new ArrayBuffer(size);
|
||||
this.view = new DataView(this.buffer);
|
||||
this.offset = 0;
|
||||
this.littleEndian = littleEndian;
|
||||
}
|
||||
|
||||
// Methods for writing various types will go here (e.g., writeInt8, writeUInt16LE, etc.)
|
||||
public bool(value: boolean): void {
|
||||
this.checkBounds(1);
|
||||
this.view.setUint8(this.offset, value ? 1 : 0);
|
||||
this.offset += 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write an 8-bit signed integer to the buffer at the current offset, and advance the offset by
|
||||
* 1 byte.
|
||||
*
|
||||
* @param value The 8-bit signed integer to write.
|
||||
*/
|
||||
public int8(value: number): void {
|
||||
this.checkBounds(1);
|
||||
this.view.setInt8(this.offset, value);
|
||||
this.offset += 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a 16-bit signed integer to the buffer at the current offset, using the specified byte
|
||||
* order, and advance the offset by 2 bytes.
|
||||
*
|
||||
* @param value The 16-bit signed integer to write.
|
||||
*/
|
||||
public int16(value: number): void {
|
||||
this.checkBounds(2);
|
||||
this.view.setInt16(this.offset, value, this.littleEndian);
|
||||
this.offset += 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a 32-bit signed integer to the buffer at the current offset, using the specified byte
|
||||
* order, and advance the offset by 4 bytes.
|
||||
*
|
||||
* @param value The 32-bit signed integer to write.
|
||||
*/
|
||||
public int32(value: number): void {
|
||||
this.checkBounds(4);
|
||||
this.view.setInt32(this.offset, value, this.littleEndian);
|
||||
this.offset += 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a 64-bit signed integer to the buffer at the current offset, using the specified byte
|
||||
* order, and advance the offset by 8 bytes.
|
||||
*
|
||||
* @param value The 64-bit signed integer to write.
|
||||
*/
|
||||
public int64(value: number | bigint): void {
|
||||
this.checkBounds(8);
|
||||
if (typeof value === 'number') {
|
||||
value = BigInt(value);
|
||||
}
|
||||
this.view.setBigInt64(this.offset, value, this.littleEndian);
|
||||
this.offset += 8;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write an 8-bit unsigned integer to the buffer at the current offset, and advance the offset by
|
||||
* 1 byte.
|
||||
*
|
||||
* @param value The 8-bit unsigned integer to write.
|
||||
*/
|
||||
public uint8(value: number): void {
|
||||
this.checkBounds(1);
|
||||
this.view.setUint8(this.offset, value);
|
||||
this.offset += 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a 16-bit unsigned integer to the buffer at the current offset, using the specified byte
|
||||
* order, and advance the offset by 2 bytes.
|
||||
*
|
||||
* @param value The 16-bit unsigned integer to write.
|
||||
*/
|
||||
public uint16(value: number): void {
|
||||
this.checkBounds(2);
|
||||
this.view.setUint16(this.offset, value, this.littleEndian);
|
||||
this.offset += 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a 32-bit unsigned integer to the buffer at the current offset, using the specified byte
|
||||
* order, and advance the offset by 4 bytes.
|
||||
*
|
||||
* @param value The 32-bit unsigned integer to write.
|
||||
*/
|
||||
public uint32(value: number): void {
|
||||
this.checkBounds(4);
|
||||
this.view.setUint32(this.offset, value, this.littleEndian);
|
||||
this.offset += 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a 64-bit unsigned integer to the buffer at the current offset, using the specified byte
|
||||
* order, and advance the offset by 8 bytes.
|
||||
*
|
||||
* @param value The 64-bit unsigned integer to write.
|
||||
*/
|
||||
public uint64(value: number | bigint): void {
|
||||
this.checkBounds(8);
|
||||
if (typeof value === 'number') {
|
||||
value = BigInt(value);
|
||||
}
|
||||
this.view.setBigUint64(this.offset, value, this.littleEndian);
|
||||
this.offset += 8;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a 32-bit floating point number to the buffer at the current offset, using the specified byte
|
||||
* order, and advance the offset by 4 bytes.
|
||||
*
|
||||
* @param value The 32-bit floating point number to write.
|
||||
*/
|
||||
public float32(value: number): void {
|
||||
this.checkBounds(4);
|
||||
this.view.setFloat32(this.offset, value, this.littleEndian);
|
||||
this.offset += 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a 64-bit floating point number to the buffer at the current offset, using the specified byte
|
||||
* order, and advance the offset by 8 bytes.
|
||||
*
|
||||
* @param value The 64-bit floating point number to write.
|
||||
*/
|
||||
public float64(value: number): void {
|
||||
this.checkBounds(8);
|
||||
this.view.setFloat64(this.offset, value, this.littleEndian);
|
||||
this.offset += 8;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a variable-length unsigned integer to the buffer using LEB128 encoding. The method encodes the
|
||||
* value in one or more bytes, where each byte has the most significant bit (MSB) set to 1 if there
|
||||
* are more bytes to follow, and 0 if it is the last byte. The lower 7 bits of each byte contain
|
||||
* the data, and the value is constructed by concatenating these bits and shifting them according to
|
||||
* their position.
|
||||
*
|
||||
* @param value The unsigned integer to encode as a varint.
|
||||
*/
|
||||
public varint(value: number): void {
|
||||
this.checkBounds(Writer.varintSize(value));
|
||||
// Calculate the number of bytes needed to encode the varint
|
||||
// (does not actually advance the offset or write)
|
||||
// Useful for pre-sizing buffers.
|
||||
let remaining = value >>> 0; // Ensure unsigned
|
||||
while (remaining >= 0x80) {
|
||||
this.view.setUint8(this.offset++, (remaining & 0x7F) | 0x80);
|
||||
remaining >>>= 7;
|
||||
}
|
||||
this.view.setUint8(this.offset++, remaining);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a variable-length signed integer to the buffer using LEB128 encoding. The method encodes the
|
||||
* value in one or more bytes, where each byte has the most significant bit (MSB) set to 1 if there
|
||||
* are more bytes to follow, and 0 if it is the last byte. The lower 7 bits of each byte contain
|
||||
* the data, and the value is constructed by concatenating these bits and shifting them according to
|
||||
* their position. If the sign bit (the highest bit of the last byte) is set, the result is
|
||||
* sign-extended to produce a negative number.
|
||||
*
|
||||
* @param value The signed integer to encode as a varint.
|
||||
*/
|
||||
public varsint(value: number): void {
|
||||
this.checkBounds(Writer.varintSize(value));
|
||||
let remaining = value >>> 0; // Ensure unsigned
|
||||
const isNegative = value < 0;
|
||||
while (remaining >= 0x80 || (isNegative && remaining < 0x80)) {
|
||||
this.view.setUint8(this.offset++, (remaining & 0x7F) | 0x80);
|
||||
remaining >>>= 7;
|
||||
}
|
||||
this.view.setUint8(this.offset++, remaining);
|
||||
}
|
||||
|
||||
private static varintSize(value: number): number {
|
||||
let size = 1;
|
||||
let v = value >>> 0;
|
||||
while (v >= 0x80) {
|
||||
v >>>= 7;
|
||||
size++;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a 32-bit date (stored as seconds since epoch) to the buffer at the current offset, using the
|
||||
* specified byte order, and advance the offset by 4 bytes.
|
||||
*
|
||||
* @param value The Date object to write as a 32-bit timestamp.
|
||||
*/
|
||||
public date32(value: Date): void {
|
||||
const timestamp = Math.floor(value.getTime() / 1000); // Convert ms to seconds
|
||||
this.checkBounds(4);
|
||||
this.view.setUint32(this.offset, timestamp, this.littleEndian);
|
||||
this.offset += 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a 64-bit date (stored as milliseconds since epoch) to the buffer at the current offset,
|
||||
* using the specified byte order, and advance the offset by 8 bytes.
|
||||
*
|
||||
* @param value The Date object to write as a 64-bit timestamp.
|
||||
*/
|
||||
public date64(value: Date): void {
|
||||
const timestamp = BigInt(value.getTime()); // Get time in ms as bigint
|
||||
this.checkBounds(8);
|
||||
this.view.setBigUint64(this.offset, timestamp, this.littleEndian);
|
||||
this.offset += 8;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a sequence of bytes to the buffer at the current offset, and advance the offset by the
|
||||
* length of the data.
|
||||
*
|
||||
* @param data The sequence of bytes to write.
|
||||
*/
|
||||
public bytes(data: Uint8Array): void {
|
||||
this.checkBounds(data.length);
|
||||
new Uint8Array(this.buffer, this.offset, data.length).set(data);
|
||||
this.offset += data.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a null-terminated C-style string to the buffer at the current offset, and advance the
|
||||
* offset by the length of the string plus one byte for the null terminator.
|
||||
*
|
||||
* @param value The string to write as a null-terminated C-style string.
|
||||
*/
|
||||
public cString(value: string): void {
|
||||
const encoder = new TextEncoder();
|
||||
const bytes = encoder.encode(value);
|
||||
this.checkBounds(bytes.length + 1); // +1 for null terminator
|
||||
new Uint8Array(this.buffer, this.offset, bytes.length).set(bytes);
|
||||
this.view.setUint8(this.offset + bytes.length, 0); // Null terminator
|
||||
this.offset += bytes.length + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a UTF-8 encoded string to the buffer at the current offset, and advance the offset by the
|
||||
* length of the encoded string.
|
||||
*
|
||||
* @param value The string to write as a UTF-8 encoded string.
|
||||
*/
|
||||
public utf8String(value: string): void {
|
||||
const encoder = new TextEncoder();
|
||||
const bytes = encoder.encode(value);
|
||||
this.checkBounds(bytes.length);
|
||||
new Uint8Array(this.buffer, this.offset, bytes.length).set(bytes);
|
||||
this.offset += bytes.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the internal buffer to a Uint8Array containing the bytes that have been written so far.
|
||||
* The returned Uint8Array shares the same underlying ArrayBuffer, but is sliced to include only
|
||||
* the bytes up to the current offset.
|
||||
*
|
||||
* @returns A Uint8Array containing the bytes that have been written so far.
|
||||
*/
|
||||
public toBytes(): Uint8Array {
|
||||
return new Uint8Array(this.buffer, 0, this.offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the internal buffer to a string using the specified encoding. The method first converts
|
||||
* the buffer to a Uint8Array containing the bytes that have been written so far, and then encodes
|
||||
* it according to the specified encoding.
|
||||
*
|
||||
* The supported encodings are:
|
||||
* - 'utf-8': Decodes the bytes as a UTF-8 string.
|
||||
* - 'hex': Encodes the bytes as a hexadecimal string.
|
||||
* - 'base64': Encodes the bytes as a Base64 string.
|
||||
* - 'rawbase64': Encodes the bytes as a URL-safe Base64 string without padding.
|
||||
* - 'urlbase64': Encodes the bytes as a URL-safe Base64 string with padding.
|
||||
* - 'rawurlbase64': Encodes the bytes as a URL-safe Base64 string without padding.
|
||||
*
|
||||
* @param encoding The encoding to use for the string conversion.
|
||||
* @returns The encoded string.
|
||||
*/
|
||||
public toString(encoding: 'utf-8' | 'hex' | 'base64' | 'rawbase64' | 'urlbase64' | 'rawurlbase64' = 'utf-8'): string {
|
||||
const bytes = this.toBytes();
|
||||
switch (encoding) {
|
||||
case 'utf-8':
|
||||
return new TextDecoder().decode(bytes);
|
||||
case 'hex':
|
||||
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
case 'base64':
|
||||
return btoa(String.fromCharCode(...bytes));
|
||||
case 'rawbase64':
|
||||
return btoa(String.fromCharCode(...bytes)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
case 'urlbase64':
|
||||
return btoa(String.fromCharCode(...bytes)).replace(/\+/g, '-').replace(/\//g, '_');
|
||||
case 'rawurlbase64':
|
||||
return btoa(String.fromCharCode(...bytes)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
default:
|
||||
throw new Error(`Unsupported encoding: ${encoding}`);
|
||||
}
|
||||
}
|
||||
|
||||
public static fromString(value: string, encoding: 'utf-8' | 'hex' | 'base64' | 'rawbase64' | 'urlbase64' | 'rawurlbase64' = 'utf-8', littleEndian: boolean = true): Writer {
|
||||
let bytes: Uint8Array;
|
||||
switch (encoding) {
|
||||
case 'utf-8':
|
||||
bytes = new TextEncoder().encode(value);
|
||||
break;
|
||||
case 'hex':
|
||||
bytes = new Uint8Array(value.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)));
|
||||
break;
|
||||
case 'base64':
|
||||
bytes = Uint8Array.from(atob(value), c => c.charCodeAt(0));
|
||||
break;
|
||||
case 'rawbase64':
|
||||
bytes = Uint8Array.from(atob(value.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));
|
||||
break;
|
||||
case 'urlbase64':
|
||||
bytes = Uint8Array.from(atob(value.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));
|
||||
break;
|
||||
case 'rawurlbase64':
|
||||
bytes = Uint8Array.from(atob(value.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported encoding: ${encoding}`);
|
||||
}
|
||||
const writer = new Writer(bytes.length, littleEndian);
|
||||
writer.bytes(bytes);
|
||||
return writer;
|
||||
}
|
||||
|
||||
/* Aliases */
|
||||
|
||||
/* Alias for bool */
|
||||
public flag(value: boolean): void {
|
||||
this.bool(value);
|
||||
}
|
||||
|
||||
/* Alias for utf8String */
|
||||
public string(value: string): void {
|
||||
this.utf8String(value);
|
||||
}
|
||||
|
||||
private checkBounds(length: number) {
|
||||
if (this.offset + length > this.view.byteLength) {
|
||||
throw new RangeError(`Attempt to write beyond end of buffer: offset=${this.offset}, length=${length}, bufferLength=${this.view.byteLength}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { FieldType } from './types';
|
||||
Reference in New Issue
Block a user