UNPKG

binary-structures

Version:

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

418 lines 17.2 kB
import 'improved-map'; import { Bits_Sizes, Uint_Sizes, Int_Sizes, Float_Sizes, uint_pack, int_pack, float_pack, uint_parse, int_parse, float_parse, utf8_pack, utf8_parse } from './serialization'; export const Parent = Symbol("Parent"); const set_context = (data, context) => { if (context !== undefined) { data[Parent] = context; } return data; }; const remove_context = (data, delete_flag) => { if (delete_flag) { delete data[Parent]; } return data; }; export const inspect_transcoder = (data, context) => { console.log({ data, context }); return data; }; export const inspect = { encode: inspect_transcoder, decode: inspect_transcoder, }; const fetch_and_encode = ({ source, encode, context }) => { let decoded; if (typeof source === 'function') { decoded = source(); } else { decoded = source; } if (typeof encode === 'function') { return encode(decoded, context); } else { return decoded; } }; const decode_and_deliver = ({ encoded, decode, context, deliver }) => { let decoded; if (typeof decode === 'function') { decoded = decode(encoded, context); } else { decoded = encoded; } if (typeof deliver === 'function') { deliver(decoded); } return decoded; }; const factory = (serializer, deserializer, verify_size) => { return ((bits, transcoders = {}) => { if (!verify_size(bits)) { throw new Error(`Invalid size: ${bits}`); } const { encode, decode, little_endian: LE } = transcoders; const pack = (source, options = {}) => { const { data_view = new DataView(new ArrayBuffer(Math.ceil(bits / 8))), byte_offset = 0, little_endian = LE, context } = options; const encoded = fetch_and_encode({ source, encode, context }); const size = (serializer(encoded, { bits, data_view, byte_offset, little_endian }) / 8); return { size, buffer: data_view.buffer }; }; const parse = (data_view, options = {}, deliver) => { const { byte_offset = 0, little_endian = LE, context } = options; const encoded = deserializer({ bits, data_view, byte_offset, little_endian }); const data = decode_and_deliver({ encoded, context, decode, deliver }); return { data, size: bits / 8 }; }; return { pack, parse }; }); }; export const Bits = factory(uint_pack, uint_parse, (s) => Bits_Sizes.includes(s)); export const Uint = factory(uint_pack, uint_parse, (s) => Uint_Sizes.includes(s)); export const Int = factory(int_pack, int_parse, (s) => Int_Sizes.includes(s)); export const Float = factory(float_pack, float_parse, (s) => Float_Sizes.includes(s)); export const Utf8 = factory(utf8_pack, utf8_parse, (s) => s % 8 === 0 && s >= 0); const numeric = (n, context, type = 'B') => { if (typeof n === 'object') { let { bits = 0, bytes = 0 } = n; n = type === 'B' ? bits / 8 + bytes : bits + bytes * 8; } else if (typeof n === 'function') { n = n(context); } else if (typeof n !== 'number') { throw new Error(`Invalid numeric input ${n}`); } if (n < 0) { throw new Error(`Invalid size: ${n} bytes`); } return n; }; /** Byte_Buffer doesn't do any serialization, but just copies bytes to/from an ArrayBuffer that's a subset of the * serialized buffer. Byte_Buffer only works on byte-aligned data. * * @param {Numeric} length * @param {Transcoders<ArrayBuffer, any>} transcoders */ export const Byte_Buffer = (length, transcoders = {}) => { const { encode, decode } = transcoders; const pack = (source, options = {}) => { const { data_view, byte_offset = 0, context } = options; const size = numeric(length, context); const buffer = fetch_and_encode({ source, encode, context }); if (size !== buffer.byteLength) { throw new Error(`Length miss-match. Expected length: ${size}, actual bytelength: ${buffer.byteLength}`); } if (data_view === undefined) { return { size, buffer }; } new Uint8Array(buffer).forEach((value, index) => { data_view.setUint8(byte_offset + index, value); }); return { size, buffer: data_view.buffer }; }; const parse = (data_view, options = {}, deliver) => { const { byte_offset = 0, context } = options; const size = numeric(length, context); const buffer = data_view.buffer.slice(byte_offset, byte_offset + size); const data = decode_and_deliver({ encoded: buffer, context, decode, deliver }); return { data, size }; }; return { pack, parse }; }; export const Padding = (bits, transcoders = {}) => { const { encode, decode } = transcoders; const pack = (source, options = {}) => { let { data_view, byte_offset = 0, context } = options; const size = numeric(bits, context, 'b'); if (data_view === undefined) { data_view = new DataView(new ArrayBuffer(Math.ceil(size / 8))); } if (encode !== undefined) { let fill = encode(null, options.context); let i = 0; while (i < Math.floor(size / 8)) { data_view.setUint8(byte_offset + i, fill); fill >>= 8; i++; } const remainder = size % 8; if (remainder) { data_view.setUint8(byte_offset + i, fill & (2 ** remainder - 1)); } } return { size: size / 8, buffer: data_view.buffer }; }; const parse = (data_view, options = {}, deliver) => { const { context } = options; const size = numeric(bits, context, 'b'); let data = null; if (decode !== undefined) { data = decode(data, context); if (deliver !== undefined) { deliver(data); } } return { size: size / 8, data }; }; return { pack, parse }; }; export const Branch = ({ chooser, choices, default_choice }) => { const choose = (source) => { let choice = chooser(source); if (choices.hasOwnProperty(choice)) { return choices[choice]; } else { if (default_choice !== undefined) { return default_choice; } else { throw new Error(`Choice ${choice} not in ${Object.keys(choices)}`); } } }; const pack = (source, options = {}) => { return choose(options.context).pack(source, options); }; const parse = (data_view, options = {}, deliver) => { return choose(options.context).parse(data_view, options, deliver); }; return { parse, pack }; }; export const Embed = (embedded) => { const pack = (source, { byte_offset, data_view, little_endian, context } = {}) => { if (context !== undefined) { const parent = context[Parent]; if (embedded instanceof Array) { return embedded .pack(context, { byte_offset, data_view, little_endian, context: parent }, source); } else if (embedded instanceof Map) { return embedded .pack(context, { byte_offset, data_view, little_endian, context: parent }, context); } } return embedded.pack(source, { byte_offset, data_view, little_endian, context }); }; const parse = (data_view, { byte_offset, little_endian, context } = {}, deliver) => { if (context !== undefined) { const parent = context[Parent]; if (embedded instanceof Array) { return embedded .parse(data_view, { byte_offset, little_endian, context: parent }, undefined, context); } else if (embedded instanceof Map) { return embedded .parse(data_view, { byte_offset, little_endian, context: parent }, undefined, context); } } return embedded.parse(data_view, { byte_offset, little_endian, context }, deliver); }; return { pack, parse }; }; const concat_buffers = (packed, byte_length) => { const data_view = new DataView(new ArrayBuffer(Math.ceil(byte_length))); let byte_offset = 0; for (const { size, buffer } of packed) { /* Copy all the data from the returned buffers into one grand buffer. */ const bytes = Array.from(new Uint8Array(buffer)); /* Create a Byte Array with the appropriate number of Uint(8)s, possibly with a trailing Bits. */ const array = Binary_Array(); for (let i = 0; i < Math.floor(size); i++) { array.push(Uint(8)); } if (size % 1) { array.push(Bits((size % 1) * 8)); } /* Pack the bytes into the buffer */ array.pack(bytes, { data_view, byte_offset }); byte_offset += size; } return data_view; }; export function Binary_Map(transcoders = {}, iterable) { if (transcoders instanceof Array) { [transcoders, iterable] = [iterable, transcoders]; } const { encode, decode, little_endian: LE } = transcoders; const map = new Map((iterable || [])); map.pack = (source, options = {}, encoded) => { const packed = []; let { data_view, byte_offset = 0, little_endian = LE, context } = options; if (encoded === undefined) { encoded = fetch_and_encode({ source, encode, context }); set_context(encoded, context); } /* Need to return a function to the `pack` chain to enable Embed with value checking. */ const fetcher = (key) => () => { const value = encoded.get(key); if (value === undefined) { throw new Error(`Insufficient data for serialization: ${key} not in ${encoded}`); } return value; }; let offset = 0; for (const [key, item] of map) { const { size, buffer } = item.pack(fetcher(key), { data_view, byte_offset: data_view === undefined ? 0 : byte_offset + offset, little_endian, context: encoded }); if (data_view === undefined) { packed.push({ size, buffer }); } offset += size; } if (data_view === undefined) { data_view = concat_buffers(packed, offset); } return { size: offset, buffer: data_view.buffer }; }; map.parse = (data_view, options = {}, deliver, results) => { const { byte_offset = 0, little_endian = LE, context } = options; let remove_parent_symbol = false; if (results === undefined) { results = set_context(new Map(), context); remove_parent_symbol = true; } let offset = 0; for (const [key, item] of map) { const { data, size } = item.parse(data_view, { byte_offset: byte_offset + offset, little_endian, context: results }, (data) => results.set(key, data)); offset += size; } const data = decode_and_deliver({ encoded: results, decode, context, deliver }); remove_context(results, remove_parent_symbol); return { data, size: offset }; }; return map; } (function (Binary_Map) { Binary_Map.object_encoder = (obj) => Map.fromObject(obj); Binary_Map.object_decoder = (map) => map.toObject(); Binary_Map.object_transcoders = { encode: Binary_Map.object_encoder, decode: Binary_Map.object_decoder }; })(Binary_Map || (Binary_Map = {})); /* This would be much cleaner if JavaScript had interfaces. Or I could make everything subclass Struct... */ const extract_array_options = (elements = []) => { if (elements.length > 0) { const first = elements[0]; if (!first.hasOwnProperty('pack') && !first.hasOwnProperty('parse')) { return elements.shift(); } const last = elements[elements.length - 1]; if (!last.hasOwnProperty('pack') && !last.hasOwnProperty('parse')) { return elements.pop(); } } return {}; }; export const Binary_Array = (...elements) => { const { encode, decode, little_endian: LE } = extract_array_options(elements); const array = new Array(...elements); array.pack = (source, options = {}, fetcher) => { let { data_view, byte_offset = 0, little_endian = LE, context } = options; const encoded = fetch_and_encode({ source, encode, context }); const packed = []; if (fetcher === undefined) { set_context(encoded, context); const iterator = encoded[Symbol.iterator](); fetcher = () => { const value = iterator.next().value; if (value === undefined) { throw new Error(`Insufficient data for serialization: ${encoded}`); } return value; }; } const store = (result) => { if (data_view === undefined) { packed.push(result); } }; const size = array.__pack_loop(fetcher, { data_view, byte_offset, little_endian, context: encoded }, store, context); if (data_view === undefined) { data_view = concat_buffers(packed, size); } return { size, buffer: data_view.buffer }; }; array.__pack_loop = (fetcher, { data_view, byte_offset = 0, little_endian, context }, store) => { let offset = 0; for (const item of array) { const { size, buffer } = item.pack(fetcher, { data_view, byte_offset: data_view === undefined ? 0 : byte_offset + offset, little_endian, context }); store({ size, buffer }); offset += size; } return offset; }; array.parse = (data_view, options = {}, deliver, results) => { const { byte_offset = 0, little_endian = LE, context } = options; let remove_parent_symbol = false; if (results === undefined) { results = set_context(new Array(), context); remove_parent_symbol = true; } const size = array.__parse_loop(data_view, { byte_offset, little_endian, context: results }, (data) => results.push(data), context); const data = decode_and_deliver({ encoded: remove_context(results, remove_parent_symbol), context, decode, deliver }); return { data, size }; }; array.__parse_loop = (data_view, { byte_offset = 0, little_endian, context }, deliver) => { let offset = 0; for (const item of array) { const { data, size } = item.parse(data_view, { byte_offset: byte_offset + offset, little_endian, context }, deliver); offset += size; } return offset; }; return array; }; export const Repeat = (...elements) => { const { count, bytes, encode, decode, little_endian } = extract_array_options(elements); const array = Binary_Array({ encode, decode, little_endian }, ...elements); const pack_loop = array.__pack_loop; const parse_loop = array.__parse_loop; array.__pack_loop = (fetcher, { data_view, byte_offset = 0, little_endian, context }, store, parent) => { let offset = 0; if (count !== undefined) { const repeat = numeric(count, parent); for (let i = 0; i < repeat; i++) { offset += pack_loop(fetcher, { data_view, byte_offset: byte_offset + offset, little_endian, context }, store); } } else if (bytes !== undefined) { const repeat = numeric(bytes, parent); while (offset < repeat) { offset += pack_loop(fetcher, { data_view, byte_offset: byte_offset + offset, little_endian, context }, store); } if (offset > repeat) { throw new Error(`Cannot pack into ${repeat} bytes.`); } } else { throw new Error("One of count or bytes must specified in options."); } return offset; }; array.__parse_loop = (data_view, { byte_offset = 0, little_endian, context }, deliver, parent) => { let offset = 0; if (count !== undefined) { const repeat = numeric(count, parent); for (let i = 0; i < repeat; i++) { offset += parse_loop(data_view, { byte_offset: byte_offset + offset, little_endian, context }, deliver); } } else if (bytes !== undefined) { const repeat = numeric(bytes, parent); while (offset < repeat) { offset += parse_loop(data_view, { byte_offset: byte_offset + offset, little_endian, context }, deliver); } if (offset > repeat) { throw new Error(`Cannot parse exactly ${repeat} bytes.`); } } else { throw new Error("One of count or bytes must specified in options."); } return offset; }; return array; }; //# sourceMappingURL=transcode.js.map