UNPKG

binary-structures

Version:

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

246 lines (245 loc) 9.51 kB
export const hex = (value) => { return "0x" + value.toString(16).toUpperCase().padStart(2, "0"); }; export const hex_buffer = (buffer) => { 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]; const write_bit_shift = (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 = (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 = (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; let high_byte; 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 = ({ 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; 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 = (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 = ({ 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 = (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 = ({ 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 = (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 = ({ 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)); } }; //# sourceMappingURL=serialization.js.map