binary-structures
Version:
Yet another declarative binary packer/parser, but built for modern browsers.
270 lines (247 loc) • 10.4 kB
text/typescript
export const hex = (value: number) => {
return "0x" + value.toString(16).toUpperCase().padStart(2, "0")
};
export const hex_buffer = (buffer: ArrayBuffer) => {
return Array.from(new Uint8Array(buffer), hex).join(", ")
};
const utf8_encoder = new TextEncoder();
const utf8_decoder = new TextDecoder();
export const Bits_Sizes = [1, 2, 3, 4, 5, 6, 7];
export const Uint_Sizes = Bits_Sizes.concat([8, 16, 32, 64]);
export const Int_Sizes = [8, 16, 32];
export const Float_Sizes = [32, 64];
export type Size = number;
export interface Serialization_Options {
bits: Size;
byte_offset?: number;
data_view: DataView;
little_endian?: boolean;
}
export type Numeric = number | string;
export interface Serializer<T> {
(value: T, options: Serialization_Options): Size;
}
export interface Deserializer<T> {
(options: Serialization_Options): T;
}
const write_bit_shift: (<T>(packer: Serializer<T>, value: T, options: Serialization_Options) => Size) =
(packer, value, { bits, data_view, byte_offset = 0, little_endian }) => {
/*
bit_offset = 5
buffer = 00011111
byte = xxxxxxxx
new_buffer = 000xxxxx xxx11111
*/
const bit_offset = ( byte_offset % 1 ) * 8;
byte_offset = Math.floor(byte_offset);
const bytes = new Uint8Array(Math.ceil(bits / 8));
const bit_length = packer(value, { bits, byte_offset: 0, data_view: new DataView(bytes.buffer), little_endian });
let overlap = data_view.getUint8(byte_offset) & ( 0xFF >> ( 8 - bit_offset ));
for ( const [index, byte] of bytes.entries() ) {
data_view.setUint8(byte_offset + index, (( byte << bit_offset ) & 0xFF ) | overlap);
overlap = byte >> ( 8 - bit_offset );
}
if ( bit_offset + bits > 8 ) {
data_view.setUint8(byte_offset + Math.ceil(bits / 8), overlap);
}
return bit_length;
};
const read_bit_shift: (<T>(parser: Deserializer<T>, options: Serialization_Options) => T) =
(parser, { bits, data_view, byte_offset = 0, little_endian }) => {
const bit_offset = ( byte_offset % 1 ) * 8;
byte_offset = Math.floor(byte_offset);
const bytes = new Uint8Array(Math.ceil(bits / 8));
let byte = data_view.getUint8(byte_offset);
if ( bit_offset + bits > 8 ) {
for ( const index of bytes.keys() ) {
const next = data_view.getUint8(byte_offset + index + 1);
bytes[index] = ( byte >> bit_offset ) | (( next << ( 8 - bit_offset )) & ( 0xFF >> ( bits < 8 ? ( 8 - bits ) : 0 )));
byte = next;
}
} else {
bytes[0] = byte >> bit_offset & ( 0xFF >> ( 8 - bits ));
}
return parser({ bits, byte_offset: 0, data_view: new DataView(bytes.buffer), little_endian });
};
export const uint_pack: Serializer<Numeric> = (value, { bits, data_view, byte_offset = 0, little_endian }) => {
const numeric = Number(value);
if ( numeric < 0 || numeric > 2 ** bits || !Number.isSafeInteger(numeric) ) {
throw new Error(`Unable to encode ${value} to Uint${bits}`)
}
if ( byte_offset % 1 ) {
return write_bit_shift(uint_pack, numeric, { bits, data_view, byte_offset, little_endian });
} else {
switch ( bits ) {
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
case 8:
data_view.setUint8(byte_offset, numeric);
break;
case 16:
data_view.setUint16(byte_offset, numeric, little_endian);
break;
case 32:
data_view.setUint32(byte_offset, numeric, little_endian);
break;
case 64: /* Special case to handle millisecond epoc time (from Date.now()) */
const upper = Math.floor(numeric / 2 ** 32);
const lower = numeric % 2 ** 32;
let low_byte: number;
let high_byte: number;
if ( little_endian ) {
low_byte = lower; high_byte = upper;
} else {
low_byte = upper; high_byte = lower;
}
data_view.setUint32(byte_offset, low_byte, little_endian);
data_view.setUint32(byte_offset + 4, high_byte, little_endian);
break;
default:
throw new Error(`Invalid size: ${bits}`);
}
return bits;
}
};
export const uint_parse: Deserializer<number> = ({ bits, data_view, byte_offset = 0, little_endian }) => {
if ( byte_offset % 1 ) {
return read_bit_shift(uint_parse, { bits, data_view, byte_offset, little_endian });
} else {
switch ( bits ) {
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
return data_view.getUint8(byte_offset) & ( 0xFF >> ( 8 - bits ));
case 8:
return data_view.getUint8(byte_offset);
case 16:
return data_view.getUint16(byte_offset, little_endian);
case 32:
return data_view.getUint32(byte_offset, little_endian);
case 64: /* Special case to handle millisecond epoc time (from Date.now()) */
const low_byte = data_view.getUint32(byte_offset, little_endian);
const high_byte = data_view.getUint32(byte_offset + 4, little_endian);
let value: number;
if ( little_endian ) {
value = high_byte * 2 ** 32 + low_byte;
} else {
value = low_byte * 2 ** 32 + high_byte;
}
if ( value > Number.MAX_SAFE_INTEGER ) {
throw new Error(`Uint64 out of range for Javascript: ${hex_buffer(data_view.buffer.slice(byte_offset, byte_offset + 8))}`)
}
return value;
default:
throw new Error(`Invalid size: ${bits}`);
}
}
};
export const int_pack: Serializer<Numeric> = (value, { bits, data_view, byte_offset = 0, little_endian }) => {
const numeric = Number(value);
if ( numeric < -( 2 ** ( bits - 1 )) || numeric > 2 ** ( bits - 1 ) - 1 || !Number.isSafeInteger(numeric) ) {
throw new Error(`Unable to encode ${value} to Int${bits}`)
}
if ( byte_offset % 1 ) {
return write_bit_shift(int_pack, numeric, { bits, data_view, byte_offset, little_endian });
} else {
switch ( bits ) {
case 8:
data_view.setUint8(byte_offset, numeric);
break;
case 16:
data_view.setUint16(byte_offset, numeric, little_endian);
break;
case 32:
data_view.setUint32(byte_offset, numeric, little_endian);
break;
default:
throw new Error(`Invalid size: ${bits}`);
}
return bits;
}
};
export const int_parse: Deserializer<number> = ({ bits, data_view, byte_offset = 0, little_endian }) => {
if ( byte_offset % 1 ) {
return read_bit_shift(int_parse, { bits, data_view, byte_offset, little_endian });
} else {
switch ( bits ) {
case 8:
return data_view.getInt8(byte_offset);
case 16:
return data_view.getInt16(byte_offset, little_endian);
case 32:
return data_view.getInt32(byte_offset, little_endian);
default:
throw new Error(`Invalid size: ${bits}`);
}
}
};
export const float_pack: Serializer<Numeric> = (value, { bits, data_view, byte_offset = 0, little_endian }) => {
const numeric = Number(value);
/* TODO: Input validation; NaN is a valid Float */
// if ( !Number.isFinite(numeric) ) {
// throw new Error(`Unable to encode ${value} to Float${bits}`)
// }
if ( byte_offset % 1 ) {
return write_bit_shift(float_pack, numeric, { bits, data_view, byte_offset, little_endian });
} else {
switch ( bits ) {
case 32:
data_view.setFloat32(byte_offset, numeric, little_endian);
break;
case 64:
data_view.setFloat64(byte_offset, numeric, little_endian);
break;
default:
throw new Error(`Invalid size: ${bits}`);
}
return bits;
}
};
export const float_parse: Deserializer<number> = ({ bits, data_view, byte_offset = 0, little_endian }) => {
if ( byte_offset % 1 ) {
return read_bit_shift(float_parse, { bits, data_view, byte_offset, little_endian });
} else {
switch ( bits ) {
case 32:
return data_view.getFloat32(byte_offset, little_endian);
case 64:
return data_view.getFloat64(byte_offset, little_endian);
default:
throw new Error(`Invalid size: ${bits}`);
}
}
};
export const utf8_pack: Serializer<string> = (value, { bits, data_view, byte_offset = 0 }) => {
if ( byte_offset % 1 ) {
return write_bit_shift(utf8_pack, value, { bits, data_view, byte_offset });
} else {
const byte_array = utf8_encoder.encode(value);
const byte_length = byte_array.byteLength;
if ( bits > 0 && byte_length > bits / 8 ) {
throw new Error(`Input string serializes to longer than ${bits / 8} bytes:\n${value}`);
}
if ( byte_length + byte_offset > data_view.byteLength ) {
throw new Error(`Insufficient space in ArrayBuffer to store length ${byte_length} string:\n${value}`);
}
for ( const [index, byte] of byte_array.entries() ) {
data_view.setUint8(byte_offset + index, byte);
}
return byte_length * 8;
}
};
export const utf8_parse: Deserializer<string> = ({ bits, data_view, byte_offset = 0 }) => {
if ( byte_offset % 1 ) {
return read_bit_shift(utf8_parse, { bits, data_view, byte_offset });
} else {
return utf8_decoder.decode(new DataView(data_view.buffer, byte_offset, bits ? bits / 8 : undefined));
}
};