UNPKG

binary-structures

Version:

Yet another declarative binary packer/parser, but built for modern browsers.

270 lines (247 loc) 10.4 kB
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)); } };