export const Types = { BERT_START: 131, SMALL_ATOM: 115, ATOM: 100, BINARY: 109, SMALL_INTEGER: 97, INTEGER: 98, SMALL_BIG: 110, LARGE_BIG: 111, FLOAT: 99, STRING: 107, LIST: 108, SMALL_TUPLE: 104, LARGE_TUPLE: 105, // TODO: Implement. MAP: 116, NIL: 106, NEW_FLOAT: 70, ZERO: 0, }; export const Lang = { ELIXIR: 0, ERLANG: 1, }; export class Bert { allBinariesAsString; mapKeyAsAtom; decodeUndefinedValues; convention; outputBuffer; constructor(allBinariesAsString = false, mapKeyAsAtom = true, decodeUndefinedValues = true, convention = Lang.ELIXIR) { this.allBinariesAsString = allBinariesAsString; this.mapKeyAsAtom = mapKeyAsAtom; this.decodeUndefinedValues = decodeUndefinedValues; this.convention = convention; this.outputBuffer = Buffer.alloc(10000000); this.outputBuffer[0] = Types.BERT_START; } toAtom = toAtom; toTuple = toTuple; #encode = (obj, buffer) => this[`encode_${typeof obj}`](obj, buffer); encode = (obj, copy = true) => { const tailBuffer = this.#encode(obj, this.outputBuffer.subarray(1)); process.stderr.write(`tailbuffer length: ${tailBuffer.length}\n`); if (tailBuffer.length === 0) { throw new Error("Bert encode a too big term, encoding buffer overflow"); } else if (copy) { const ret = Buffer.alloc(tailBuffer.length + 1); this.outputBuffer.copy(ret, 0, 0, ret.length + 1); return ret; } else { return this.outputBuffer.subarray(0, tailBuffer.length + 1); } }; #decode = (buffer) => { const t = buffer[0]; buffer = buffer.subarray(1); switch (t) { case Types.SMALL_ATOM: return this.decode_atom(buffer, 1); case Types.ATOM: return this.decode_atom(buffer, 2); case Types.BINARY: return this.decode_binary(buffer); case Types.SMALL_INTEGER: return this.decode_integer(buffer, 1, true); case Types.INTEGER: return this.decode_integer(buffer, 4); case Types.SMALL_BIG: return this.decode_big(buffer, 1); case Types.LARGE_BIG: return this.decode_big(buffer, 4); case Types.FLOAT: return this.decode_float(buffer); case Types.NEW_FLOAT: return this.decode_new_float(buffer); case Types.STRING: return this.decode_string(buffer); case Types.LIST: return this.decode_list(buffer); case Types.SMALL_TUPLE: return this.decode_tuple(buffer, 1); // case Types.LARGE_TUPLE: // return this.decode_large_tuple(buffer, 4) case Types.NIL: return this.decode_nil(buffer); case Types.MAP: return this.decode_map(buffer); default: throw new Error(`Unexpected BERT type: ${t}`); } }; decode = (buffer) => { if (buffer[0] !== Types.BERT_START) { throw new Error("Invalid BERT start magic"); } const obj = this.#decode(buffer.subarray(1)); if (obj.rest.length !== 0) { throw new Error(`Invalid BERT, remainder was: ${obj.rest.length}`); } return obj.value; }; encode_string = (obj, buffer) => { if (this.convention === Lang.ELIXIR) { process.stderr.write(`encode string as elixir\n`); return this.encode_binary(Buffer.from(obj), buffer); } else { buffer[0] = Types.STRING; buffer.writeUInt16BE(Buffer.byteLength(obj, "utf-8"), 1); const len = buffer.write(obj, 3); return buffer.subarray(0, 3 + len); } }; encode_boolean = (obj, buffer) => { if (obj) { return this.#encode(this.toAtom("true"), buffer); } else { return this.#encode(this.toAtom("false"), buffer); } }; encode_number = (obj, buffer) => { const isInteger = obj % 1 === 0; // Handle floats... if (!isInteger) { return this.encode_float(obj, buffer); } // Small int... if (isInteger && obj >= 0 && obj < 256) { buffer[0] = Types.SMALL_INTEGER; buffer.writeUInt8(obj, 1); return buffer.subarray(0, 2); } // 4 byte int... if (isInteger && obj >= -134217728 && obj <= 134217727) { buffer[0] = Types.INTEGER; buffer.writeInt32BE(obj, 1); return buffer.subarray(0, 5); } // Bignum... const numBuffer = Buffer.alloc(buffer.length); if (obj < 0) { obj *= -1; numBuffer[0] = 1; } else { numBuffer[0] = 0; } let offset = 1; while (obj !== 0) { numBuffer[offset] = obj % 256; obj = Math.floor(obj / 256); offset++; } if (offset < 256) { buffer[0] = Types.SMALL_BIG; buffer.writeUInt8(offset - 1, 1); numBuffer.copy(buffer, 2, 0, offset); return buffer.subarray(0, 2 + offset); } else { buffer[0] = Types.LARGE_BIG; buffer.writeUInt32BE(offset - 1, 1); numBuffer.copy(buffer, 5, 0, offset); return buffer.subarray(0, 5 + offset); } }; encode_float = (obj, buffer) => { // float... buffer[0] = Types.NEW_FLOAT; buffer.writeDoubleBE(obj, 1); return buffer.subarray(0, 9); }; encode_object = (obj, buffer) => { // Check if it's an atom, binary, or tuple... if (obj === null) { const undefinedAtom = this.convention === Lang.ELIXIR ? "nil" : "undefined"; return this.#encode(this.toAtom(undefinedAtom), buffer); } if (Buffer.isBuffer(obj)) { return this.encode_binary(obj, buffer); } if (Array.isArray(obj)) { return this.encode_array(obj, buffer); } if (obj.type === "Atom") { return this.encode_atom(obj, buffer); } if (obj.type === "Tuple") { return this.encode_tuple(obj, buffer); } // Treat the object as an associative array... return this.encode_map(obj, buffer); }; encode_atom = (obj, buffer) => { buffer[0] = Types.ATOM; buffer.writeUInt16BE(obj.value.length, 1); const len = buffer.write(obj.value, 3); return buffer.subarray(0, 3 + len); }; encode_binary = (obj, buffer) => { process.stderr.write(`encode binary\n`); buffer[0] = Types.BINARY; buffer.writeUInt32BE(obj.length, 1); const len = obj.copy(buffer, 5); return buffer.subarray(0, 5 + len); }; encode_undefined = (_obj, buffer) => { return this.#encode(null, buffer); }; encode_tuple = (obj, buffer) => { if (obj.length < 256) { buffer[0] = Types.SMALL_TUPLE; buffer.writeUInt8(obj.length, 1); buffer = buffer.subarray(0, 2); } else { buffer[0] = Types.LARGE_TUPLE; buffer.writeUInt32BE(obj.length, 1); buffer = buffer.subarray(0, 5); } for (let i = 0; i < obj.length; ++i) { buffer = this.#encode(obj[i], buffer); } return buffer; }; encode_array = (obj, buffer) => { if (obj.length === 0) { buffer[0] = Types.NIL; return buffer.subarray(0, 1); } buffer[0] = Types.LIST; buffer.writeUInt32BE(obj.length, 1); buffer = buffer.subarray(0, 5); for (let i = 0; i < obj.length; ++i) { buffer = this.#encode(obj[i], buffer); } buffer[0] = Types.NIL; return buffer.subarray(0, 1); }; encode_map = (obj, buffer) => { const keys = Object.keys(obj); buffer[0] = Types.MAP; buffer.writeUInt32BE(keys.length, 1); buffer = buffer.subarray(0, 5); for (let i = 0; i < keys.length; ++i) { const key = this.mapKeyAsAtom ? this.toAtom(keys[i]) : keys[i]; buffer = this.#encode(key, buffer); buffer = this.#encode(obj[keys[i]], buffer); } return buffer; }; decode_atom = (buffer, count) => { const size = this.bytesToInt(buffer, count, true); buffer = Buffer.from(buffer, count); let value = buffer.toString("utf8", 0, size); if (value === "true") { value = true; } else if (value === "false") { value = false; } else if (this.decodeUndefinedValues && this.convention === Lang.ELIXIR && value === "nil") { value = null; } else if (this.decodeUndefinedValues && this.convention === Lang.ERLANG && value === "undefined") { value = null; } else { value = this.toAtom(value); } return { value, rest: buffer.subarray(size), }; }; decode_binary = (buffer) => { const size = this.bytesToInt(buffer, 4, true); buffer = Buffer.from(buffer, 4); const bin = Buffer.alloc(size); buffer.copy(bin, 0, 0, size); return { value: this.convention === Lang.ELIXIR && this.allBinariesAsString ? bin.toString() : bin, rest: buffer.subarray(size), }; }; decode_integer = (buffer, count, unsigned = false) => { return { value: this.bytesToInt(buffer, count, unsigned), rest: buffer.subarray(count), }; }; decode_big = (buffer, count) => { const size = this.bytesToInt(buffer, count, false); buffer = buffer.subarray(count); let num = 0; const isNegative = buffer[0] === 1; buffer = buffer.subarray(1); for (let i = size - 1; i >= 0; --i) { const n = buffer[i]; if (num === 0) { num = n; } else { num = num * 256 + n; } } if (isNegative) { num = num * -1; } return { value: num, rest: buffer.subarray(size), }; }; decode_float = (buffer) => { const size = 31; return { value: parseFloat(buffer.toString("utf8", 0, size)), rest: buffer.subarray(size), }; }; decode_new_float = (buffer) => { return { value: buffer.readDoubleBE(0), rest: buffer.subarray(8), }; }; decode_string = (buffer) => { const sizeLen = this.convention === Lang.ELIXIR ? 4 : 2; const size = this.bytesToInt(buffer, sizeLen, true); buffer = buffer.subarray(sizeLen); return { value: buffer.toString("utf8", 0, size), rest: buffer.subarray(size), }; }; decode_list = (buffer) => { const arr = []; const size = this.bytesToInt(buffer, 4, true); buffer = buffer.subarray(4); for (let i = 0; i < size; ++i) { const el = this.#decode(buffer); arr.push(el.value); buffer = el.rest; } const lastChar = buffer[0]; if (lastChar !== Types.NIL) { throw new Error("List does not end with NIL"); } buffer = buffer.subarray(1); return { value: arr, rest: buffer, }; }; decode_map = (buffer) => { const map = {}; const size = this.bytesToInt(buffer, 4, true); buffer = buffer.subarray(4); for (let i = 0; i < size; ++i) { let el = this.#decode(buffer); const key = el.value; el = this.#decode(el.rest); const value = el.value; map[key] = value; buffer = el.rest; } return { value: map, rest: buffer, }; }; decode_tuple = (buffer, count) => { const arr = []; const size = this.bytesToInt(buffer, count, true); buffer = buffer.subarray(count); for (let i = 0; i < size; ++i) { const el = this.#decode(buffer); arr.push(el.value); buffer = el.rest; } return { value: this.toTuple(arr), rest: buffer, }; }; decode_nil = (buffer) => { // nil is an empty list return { value: [], rest: buffer, }; }; bytesToInt = (buffer, length, unsigned) => { switch (length) { case 1: return unsigned ? buffer.readUInt8(0) : buffer.readInt8(0); case 2: return unsigned ? buffer.readUInt16BE(0) : buffer.readInt16BE(0); case 4: return unsigned ? buffer.readUInt32BE(0) : buffer.readInt32BE(0); } }; binary_to_list = (str) => { const ret = []; for (let i = 0; i < str.length; ++i) ret.push(str[i]); return ret; }; } /** * Convert object to atom. * */ export const toAtom = (str) => ({ type: "Atom", value: str, toString: () => str, }); /** * Convert array of items to tuple. * */ export const toTuple = (arr) => ({ ...arr, type: "Tuple", length: arr.length, value: arr, toString: () => "{" + arr .filter((i) => i !== "") .map((i) => i.toString()) .join(", ") + "}", });