Prepared for release

This commit is contained in:
2026-03-12 17:07:09 +01:00
commit 99f0ff64e1
14 changed files with 5644 additions and 0 deletions

67
src/index.test.ts Normal file
View 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
View 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
View 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)
}