Prepared for release
This commit is contained in:
67
src/index.test.ts
Normal file
67
src/index.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Reader, Writer } from '.';
|
||||
|
||||
describe('Reader/Writer round-trip', () => {
|
||||
it('writes then reads a variety of types correctly', () => {
|
||||
const writer = new Writer(1024);
|
||||
|
||||
// values to write
|
||||
const bBool = true;
|
||||
const bInt8 = -42;
|
||||
const bUint8 = 200;
|
||||
const bUint16 = 0xBEEF;
|
||||
const bUint32 = 0xDEADBEEF >>> 0;
|
||||
const bUint64 = 0x1122334455667788n;
|
||||
const bFloat32 = 3.1415927;
|
||||
const bFloat64 = 1.23456789e5;
|
||||
const bCString = 'hello';
|
||||
const bUtf8 = 'π≈3.14';
|
||||
const words = [0x1234, 0x5678, 0x9ABC];
|
||||
const dwords = [0x11223344 >>> 0, 0x55667788 >>> 0];
|
||||
const qwords = [0x8000000000000000n, 0xAABBCCDDEEFF0011n];
|
||||
const rawBytes = new Uint8Array([1, 2, 3, 4]);
|
||||
|
||||
// write sequence
|
||||
writer.bool(bBool);
|
||||
writer.int8(bInt8);
|
||||
writer.uint8(bUint8);
|
||||
writer.uint16(bUint16);
|
||||
writer.uint32(bUint32);
|
||||
writer.uint64(bUint64);
|
||||
writer.float32(bFloat32);
|
||||
writer.float64(bFloat64);
|
||||
writer.cString(bCString);
|
||||
writer.utf8String(bUtf8);
|
||||
for (const w of words) writer.uint16(w);
|
||||
for (const d of dwords) writer.uint32(d);
|
||||
for (const q of qwords) writer.uint64(q);
|
||||
writer.bytes(rawBytes);
|
||||
|
||||
// extract buffer (private, access for test)
|
||||
// Narrow the writer internals with a local typed view instead of `any`.
|
||||
const writerInternals = writer as unknown as { buffer: ArrayBuffer; offset: number };
|
||||
const buf: ArrayBuffer = writerInternals.buffer.slice(0, writerInternals.offset);
|
||||
const reader = new Reader(buf);
|
||||
|
||||
// read back in same order
|
||||
expect(reader.bool()).toBe(bBool);
|
||||
expect(reader.int8()).toBe(bInt8);
|
||||
expect(reader.uint8()).toBe(bUint8);
|
||||
expect(reader.uint16()).toBe(bUint16);
|
||||
expect(reader.uint32()).toBe(bUint32);
|
||||
expect(reader.uint64()).toBe(bUint64);
|
||||
expect(reader.float32()).toBeCloseTo(bFloat32, 6);
|
||||
expect(reader.float64()).toBeCloseTo(bFloat64, 9);
|
||||
expect(reader.cString()).toBe(bCString);
|
||||
|
||||
// utf8String requires length - calculate encoded length
|
||||
const utf8len = new TextEncoder().encode(bUtf8).length;
|
||||
expect(reader.utf8String(utf8len)).toBe(bUtf8);
|
||||
|
||||
for (const w of words) expect(reader.words(1)[0]).toBe(w);
|
||||
for (const d of dwords) expect(reader.dwords(1)[0]).toBe(d);
|
||||
for (const q of qwords) expect(reader.qwords(1)[0]).toBe(q);
|
||||
const outBytes = reader.bytes(rawBytes.length);
|
||||
expect(Array.from(outBytes)).toEqual(Array.from(rawBytes));
|
||||
});
|
||||
});
|
||||
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';
|
||||
136
src/types.ts
Normal file
136
src/types.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
|
||||
/**
|
||||
* Type definitions for the packet parsing library.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Enumeration of supported field types for dissecting packets.
|
||||
*
|
||||
* Each field type corresponds to a specific way of interpreting bytes in a packet, such as boolean
|
||||
* values, integers of various sizes and endianness, floating point numbers, date/time values, and
|
||||
* array buffers for strings and byte arrays.
|
||||
*/
|
||||
export const FieldType = {
|
||||
// Boolean types
|
||||
BOOL: 'BOOL', // Boolean value (stored as a byte, 0 for false, non-zero for true)
|
||||
|
||||
// Number types
|
||||
BITS: 'BITS', // 1-bit values stored in a number (1 bit per value, packed into bytes)
|
||||
INT8: 'INT8', // 8-bit signed integer (1 byte)
|
||||
INT16_LE: 'INT16_LE', // 16-bit signed integer (little-endian)
|
||||
INT16_BE: 'INT16_BE', // 16-bit signed integer (big-endian)
|
||||
INT32_LE: 'INT32_LE', // 32-bit signed integer (little-endian)
|
||||
INT32_BE: 'INT32_BE', // 32-bit signed integer (big-endian)
|
||||
INT64_LE: 'INT64_LE', // 64-bit signed integer (little-endian)
|
||||
INT64_BE: 'INT64_BE', // 64-bit signed integer (big-endian)
|
||||
UINT8: 'UINT8', // 8-bit unsigned integer
|
||||
UINT16_LE: 'UINT16_LE', // 16-bit unsigned integer (little-endian)
|
||||
UINT16_BE: 'UINT16_BE', // 16-bit unsigned integer (big-endian)
|
||||
UINT32_LE: 'UINT32_LE', // 32-bit unsigned integer (little-endian)
|
||||
UINT32_BE: 'UINT32_BE', // 32-bit unsigned integer (big-endian)
|
||||
UINT64_LE: 'UINT64_LE', // 64-bit unsigned integer (little-endian)
|
||||
UINT64_BE: 'UINT64_BE', // 64-bit unsigned integer (big-endian)
|
||||
FLOAT32_LE: 'FLOAT32_LE', // 32-bit IEEE floating point (little-endian)
|
||||
FLOAT32_BE: 'FLOAT32_BE', // 32-bit IEEE floating point (big-endian)
|
||||
FLOAT64_LE: 'FLOAT64_LE', // 64-bit IEEE floating point (little-endian)
|
||||
FLOAT64_BE: 'FLOAT64_BE', // 64-bit IEEE floating point (big-endian)
|
||||
VARINT: 'VARINT', // Variable-length integer (unsigned, LEB128 encoding)
|
||||
VARSINT: 'VARSINT', // Variable-length integer (signed, LEB128 encoding)
|
||||
|
||||
// Date/time types (stored as integer timestamps)
|
||||
DATE32_LE: 'DATE32_LE', // 32-bit integer date (e.g., seconds since epoch) little-endian
|
||||
DATE32_BE: 'DATE32_BE', // 32-bit integer date big-endian
|
||||
DATE64_LE: 'DATE64_LE', // 64-bit integer date (e.g., ms since epoch) little-endian
|
||||
DATE64_BE: 'DATE64_BE', // 64-bit integer date big-endian
|
||||
|
||||
// Array buffer types
|
||||
BYTES: 'BYTES', // 8-bits per value array (Uint8Array)
|
||||
C_STRING: 'C_STRING', // Null-terminated string (C-style) (Uint8Array)
|
||||
UTF8_STRING: 'UTF8_STRING', // UTF-8 encoded string (Uint8Array)
|
||||
WORDS: 'WORDS', // 16-bits per value array (Uint16Array)
|
||||
DWORDS: 'DWORDS', // 32-bits per value array (Uint32Array)
|
||||
QWORDS: 'QWORDS', // 64-bits per value array (BigUint64Array)
|
||||
|
||||
// Aliases
|
||||
FLAG: 'BOOL', // alternate name for boolean/flag fields
|
||||
STRING: 'UTF8_STRING', // alias for UTF8 encoded strings
|
||||
} as const;
|
||||
|
||||
export type FieldType = typeof FieldType[keyof typeof FieldType];
|
||||
|
||||
/**
|
||||
* Interface for a packet, which can be dissected into segments and fields. This is a placeholder
|
||||
* for any additional properties or methods that may be needed for representing a packet in the future.
|
||||
*/
|
||||
export interface Packet {
|
||||
data: string | Uint8Array | ArrayBuffer; // Raw packet data as an ArrayBuffer
|
||||
snr?: number; // Optional signal-to-noise ratio (for radio packets)
|
||||
rssi?: number; // Optional received signal strength indicator (for radio packets)
|
||||
parsed?: unknown; // Optional parsed representation of the packet (e.g., a structured object)
|
||||
dissected?: Dissected; // Optional dissected representation of the packet (array of segments)
|
||||
|
||||
/**
|
||||
* Method to dissect the packet into segments and fields, returning an array of segments.
|
||||
*
|
||||
* This method can be implemented by classes that represent specific packet types, and can
|
||||
* use the defined field types to parse the raw data into a structured format.
|
||||
*/
|
||||
dissect?(): Dissected; // Method to dissect the packet into segments and fields
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for a protocol, which defines how to encode and decode packets. This can be used to
|
||||
* represent different radio protocols (e.g., AX.25, LoRaWAN) and their specific encoding/decoding
|
||||
* logic.
|
||||
*/
|
||||
export interface Protocol {
|
||||
// Name of the protocol (e.g., "AX.25", "LoRaWAN", etc.)
|
||||
name: string;
|
||||
|
||||
// Method to encode a packet into an ArrayBuffer (for serialization)
|
||||
encode: (packet: Packet) => ArrayBuffer;
|
||||
|
||||
// Method to decode a packet into a dissected representation (array of segments)
|
||||
decode: (data: ArrayBuffer, dissect?: boolean) => Packet;
|
||||
|
||||
// Method to return a string representation of the protocol (e.g., for debugging or logging)
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a dissected packet, which is an array of segments.
|
||||
*/
|
||||
export type Dissected = Segment[];
|
||||
|
||||
/**
|
||||
* Represents a segment of bytes in a packet, defined by a name and an array of fields.
|
||||
* Each field specifies the type and length of data it represents.
|
||||
*/
|
||||
export interface Segment {
|
||||
name: string;
|
||||
data?: ArrayBuffer; // Optional raw data for the segment (if needed for parsing / serialization)
|
||||
fields: Field[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a field within a segment, defined by its type, name, and optional properties
|
||||
* for bit fields and array lengths.
|
||||
*/
|
||||
export interface Field {
|
||||
type: FieldType;
|
||||
name: string;
|
||||
value?: unknown; // Optional value for the field (used for serialization or as a default value)
|
||||
bits?: BitField[]; // Optional array of bit field definitions (for BITS type)
|
||||
length?: number; // Optional length for array types (e.g., BYTES, WORDS)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a bit field definition, specifying the name and size of the field in bits,
|
||||
* and an optional flag indicating if it is the least significant bit in a byte (for BITS type).
|
||||
*/
|
||||
export interface BitField {
|
||||
name: string;
|
||||
size: number; // Number of bits for this field (must be > 0)
|
||||
lsb?: boolean; // Optional flag indicating if this field is the least significant bit in the byte (for BITS type)
|
||||
}
|
||||
Reference in New Issue
Block a user