// flick.js
// ========
// Copyright (c) 2013 Serge Aleynikov <saleyn@gmail.com>
// See BSD for licensing information.
// This project originated from Bert (https://github.com/rustyio/BERT-JS)
// but ended up being a rewrite.
//
// Vendored from erlb.js (https://github.com/saleyn/erlb.js) for use with
// the flick library's binary (ETF) WebSocket transport for Phoenix.
// erlb.js is archived to be deprecated in favor of this project being its
// successor.
//
// flick.js is a Javascript implementation of Erlang Binary External Term
// Format. http://github.com/saleyn/erlb.js
//
// For future integration:
// BigInteger: https://github.com/silentmatt/javascript-biginteger
//
//-----------------------------------------------------------------------------
// - CLASSES -
//-----------------------------------------------------------------------------
function Flick() {}
function ErlObject() {}
ErlObject.prototype.type = 'erl';
ErlObject.prototype.toString = function() { return this.type; }
ErlObject.prototype.extend = function(child, type) {
const f = function() {};
f.prototype = ErlObject.prototype;
child.prototype = new f();
child.prototype.constructor = child;
child.prototype.type = type;
}
function ErlAtom(s) { this.value = s; }
ErlObject.prototype.extend(ErlAtom, 'atom');
ErlAtom.prototype.equals = function(a) { return a instanceof ErlAtom && this.value === a.value; }
ErlAtom.prototype.encodeSize = function(a) { return 3 + Math.min(255, (a || this.value).length); }
ErlAtom.prototype.toString = function() {
return (!this.value.length || this.value[0] < "a" || this.value[0] > "z")
? "'" + this.value + "'" : this.value;
}
function ErlBinary(arr) {
if (typeof(arr) === 'string')
this.value = Array.from(arr);
else if (arr instanceof Array)
this.value = arr;
else
throw new Error("Unsupported binary data type: " + Flick.getClassName(arr));
}
ErlObject.prototype.extend(ErlBinary, 'binary');
ErlBinary.prototype.equals = function(a) { return a instanceof ErlBinary && this.value.equals(a.value); }
ErlBinary.prototype.encodeSize = function(a) { return 1 + 4 + (a || this.value).length; }
ErlBinary.prototype.toString = function(opts = {}) {
const a = this.value;
const printable = a.length > 0 && a.every((i) => i > 30 && i < 127);
const body = printable ? a.map(i => String.fromCharCode(i)).join('') : a.join(',');
const cmp = opts.compact === true;
const beg = cmp ? "`" : `<<${printable ? '"':''}`
const end = cmp ? "`" : `${printable ? '"':''}>>`
return `${beg}${body}${end}`
}
function ErlTuple(arr) {
this.value = arr;
this.length = arr === undefined ? 0 : arr.length;
}
ErlObject.prototype.extend(ErlTuple, 'tuple');
ErlTuple.prototype.equals = function(a) { return a instanceof ErlTuple && this.value.equals(a.value); }
ErlTuple.prototype.encodeSize = function() {
return Flick.encode_tuple_size(this.value);
//return this.value.reduce(
// (s,i) => s + Flick.encode_size(i),
// 1 + (this.length < 256 ? 1 : 4));
}
ErlTuple.prototype.toString = function() {
return "{" + this.value.map((e) => Flick.toString(e)).join(',') + "}";
}
ErlTuple.prototype.toDate = function() { return new Date(this.toTimestamp()); }
ErlTuple.prototype.toTimestamp = function() {
if (length !== 3) return -1;
const n = value[0] * 1000000000 + value[1] * 1000 + value[2] / 1000;
return isNaN(n) ? -1 : n;
}
function ErlMap(obj) {
if (obj instanceof ErlMap)
this.value = Flick.objectDeepClone(obj.value)
else if (obj instanceof Object)
this.value = obj
else
throw new Error('ErlMap must be given an object!');
}
ErlObject.prototype.extend(ErlMap, 'map');
ErlMap.prototype.equals = function(a) {
return a instanceof ErlMap && Flick.objectsEqual(this.value, a.value);
}
ErlMap.prototype.encodeSize = function(opts={}) { return Flick.encode_map_size(this.value, opts); }
ErlMap.prototype.toString = function() {
return "#{" + Object.keys(this.value).map(k => `${k} => ${Flick.toString(this.value[k])}`).join(',') + "}";
}
function ErlPid(node, id, serial, creation) {
if (typeof(node) === 'string')
node = new ErlAtom(node);
else if (!(node instanceof ErlAtom))
throw new Error("Node argument must be an atom!");
this.node = node;
this.id = id >>> 0;
this.serial = serial >>> 0;
this.creation = creation >>> 0;
}
ErlObject.prototype.extend(ErlPid, 'pid');
ErlPid.prototype.equals = function(a) {
return a instanceof ErlPid && this.node.equals(a.node)
&& this.id === a.id
&& this.serial === a.serial
&& this.creation === a.creation;
}
// Encoded using NEW_PID_EXT: tag(1) + node + id(4) + serial(4) + creation(4)
ErlPid.prototype.encodeSize = function() { return 1 + this.node.encodeSize() + 12; }
ErlPid.prototype.toString = function() {
return "#pid{" + this.node + "," +
this.id + "," +
this.serial + "}";
}
function ErlRef(Node, creation, IDs) {
if (typeof(Node) === 'string')
Node = new ErlAtom(Node);
else if (!(Node instanceof ErlAtom))
throw new Error("Node argument must be an atom!");
if (!(IDs instanceof Array) || IDs.length > 3)
throw new Error("Reference IDs must be an array of length <= 3!");
this.node = Node;
this.creation = creation >>> 0;
this.ids = IDs;
}
ErlObject.prototype.extend(ErlRef, 'ref');
ErlRef.prototype.equals = function(a) {
return a instanceof ErlRef && this.node.equals(a.node)
&& this.creation === a.creation
&& this.ids.equals(a.ids);
}
// Encoded using NEWER_REFERENCE_EXT: tag(1) + len(2) + node + creation(4) + 4*len
ErlRef.prototype.encodeSize = function() {
return 1 + 2 + this.node.encodeSize() + 4 + 4*this.ids.length;
}
ErlRef.prototype.toString = function() {
return `#ref{${this.node.toString() + (this.ids.length ? this.ids.map(i => `,${i}`).join('') : '')}}`;
}
function ErlVar(Name, type) {
this.valueType = type;
this.name = Name;
}
ErlObject.prototype.extend(ErlVar, 'binary');
ErlVar.prototype.equals = function(a) { return false; }
ErlVar.prototype.encodeSize = function() { throw new Error("Cannot encode variables!"); }
ErlVar.prototype.toString = function() {
let tp;
switch (this.valueType) {
case Flick.Enum.ATOM: tp = "::atom()"; break;
case Flick.Enum.BINARY: tp = "::binary()"; break;
case Flick.Enum.ErlBoolean: tp = "::bool()"; break;
case Flick.Enum.ErlByte: tp = "::byte()"; break;
case Flick.Enum.ErlDouble: tp = "::double()"; break;
case Flick.Enum.ErlLong: tp = "::int()"; break;
case Flick.Enum.ErlList: tp = "::list()"; break;
case Flick.Enum.ErlMap: tp = "::map()"; break;
case Flick.Enum.ErlPid: tp = "::pid()"; break;
case Flick.Enum.ErlPort: tp = "::port()"; break;
case Flick.Enum.ErlRef: tp = "::ref()"; break;
case Flick.Enum.ErlString: tp = "::string()"; break;
case Flick.Enum.ErlTuple: tp = "::tuple()"; break;
case Flick.Enum.ErlVar: tp = "::var()"; break;
default: tp = ""; break;
}
return this.name + tp;
}
//-----------------------------------------------------------------------------
// - INTERFACE -
//-----------------------------------------------------------------------------
Flick.prototype.encode = function (obj, opts = {}) {
var n = 1 + this.encode_size(obj, opts);
var b = new ArrayBuffer(n);
var d = new DataView(b)
d.setUint8(0, this.Enum.VERSION);
var v = this.encode_inner(obj, d, 1, opts);
if (v.offset !== n)
throw new Error("Invalid size of encoded buffer: " + v.offset + " expected: " + n);
return b;
}
Flick.prototype.decode = function (buffer) {
var dv = new DataView(buffer, 0);
if (dv.getUint8(0) !== this.Enum.VERSION) {
throw new Error("Not a valid Erlang term.");
}
var obj = this.decode_inner({data: dv, offset: 1});
if (obj.offset !== buffer.byteLength) {
throw new Error("Erlang term buffer has unused " +
buffer.byteLength - obj.offset + " bytes");
}
return obj.value;
}
Flick.prototype.equals = function () {
var a = arguments[0];
var b = arguments.length > 1 ? arguments[1] : this;
if (a === b)
return true;
if (ErlObject.prototype.isPrototypeOf(a))
return a.equals(b)
|| (a instanceof ErlTuple && b instanceof Date && a.toTimestamp() === b.getTime());
if (a instanceof Date && b instanceof ErlTuple)
return b.toTimestamp() === a.getTime();
if (a instanceof Array)
return b instanceof Array && a.equals(b);
// Compare two objects for equality
if (a instanceof Object != b instanceof Object)
return false;
for (let k in a) if (!(k in b)) return false;
for (let k in b) if (!(k in a)) return false;
for (let k in a) {
var av = a[k];
var bv = b[k];
if (!Flick.equals(av, bv))
return false;
}
return true;
}
Flick.prototype.toString = function(obj, opts = {}) {
if (obj === undefined) return "undefined";
if (obj === null) return "null";
switch (typeof(obj)) {
case 'number': return obj.toString();
case 'boolean': return obj.toString();
case 'string': return `"${obj.toString()}"`;
}
if (ErlObject.prototype.isPrototypeOf(obj))
return obj.toString(opts);
if (obj instanceof Array)
return `[${obj.map((e) => Flick.toString(e, opts)).join(",")}]`;
const body = Object.keys(obj).map((k) => `{${k},${Flick.toString(obj[k], opts)}}`).join(",");
return `[${body}]`;
}
Flick.prototype.atom = function(obj) { return new ErlAtom(obj); }
Flick.prototype.binary = function(obj) { return new ErlBinary(obj); }
Flick.prototype.tuple = function() {
var a = new Array(arguments.length);
for (let i=0, n = arguments.length; i < n; ++i)
a[i] = arguments[i];
return new ErlTuple(a);
}
Flick.prototype.pid = function(Node, Id, Serial, creation) { return new ErlPid(Node, Id, Serial, creation); }
Flick.prototype.ref = function(Node, creation, IDs) { return new ErlRef(Node, creation, IDs); }
Flick.prototype.map = function(obj) { return new ErlMap(obj); }
Flick.prototype.toArrayBuffer = function(a) {
var b = new ArrayBuffer(a.length);
var d = new DataView(b);
for (let i=0, n=a.length; i < n; ++i)
d.setUint8(i, a[i]);
return b;
}
Flick.prototype.bufferToArray = function(b) {
var d = new DataView(b);
var r = new Array(d.byteLength);
for (let i=0, n=d.byteLength; i < n; ++i)
r[i] = d.getUint8(i);
return r;
}
//-----------------------------------------------------------------------------
// - ENCODING -
//-----------------------------------------------------------------------------
// See: https://www.erlang.org/doc/apps/erts/erl_ext_dist.html
// See: https://github.com/erlang/otp/blob/master/lib/erl_interface/include/ei.h#L137
Flick.prototype.Enum = {
VERSION : 131,
SMALL_ATOM : 115, // 's'
ATOM : 100, // 'd'
ATOM_UTF8 : 118, // 'v'
SMALL_ATOM_UTF8 : 119, // 'w'
BINARY : 109, // 'm'
BIT_BINARY : 77, // 'M'
SMALL_INTEGER : 97 , // 'a'
INTEGER : 98 , // 'b'
SMALL_BIG : 110, // 'n'
LARGE_BIG : 111, // 'o'
FLOAT : 99 , // 'c'
NEW_FLOAT : 70 , // 'F'
STRING : 107, // 'k'
PORT : 102, // 'f'
V4_PORT : 120, // 'x'
NEW_PORT : 89, // 'Y'
PID : 103, // 'g'
NEW_PID : 88, // 'X'
SMALL_TUPLE : 104, // 'h'
LARGE_TUPLE : 105, // 'i'
LIST : 108, // 'l'
MAP : 116, // 't'
REFERENCE : 101, // 'e'
NEW_REFERENCE : 114, // 'r'
NEWER_REFERENCE : 90, // 'Z'
NIL : 106, // 'j'
//---- Custom --------
TUPLE : 104, // SMALL_TUPLE
DOUBLE : 70, // NEW_FLOAT,
BYTE : 97, // SMALL_INTEGER,
BOOLEAN : 254,
VAR : 255,
ZERO : 0
}
Flick.prototype.Encoding = {
ASCII : 1,
LATIN1 : 2,
UTF8 : 4,
}
Flick.prototype.encode_size = function(obj, opts = {}) {
if (obj === null)
return this.atom("nil").encodeSize();
if (obj === undefined)
return this.atom(String(obj)).encodeSize();
switch (typeof(obj)) {
case "number": return this.encode_number_size(obj);
case "string": return this.encode_string_size(obj);
case "boolean": return obj ? 7 : 8; // Atom "true" or "false"
}
switch (obj.type) {
case "atom": return obj.encodeSize();
case "tuple": return obj.encodeSize();
case "binary": return obj.encodeSize();
case "pid": return obj.encodeSize();
case "ref": return obj.encodeSize();
case "map": return obj.encodeSize();
}
if (obj instanceof Array)
return this.is_assoc_array(obj) ? this.encode_assoc_array_size(obj)
: this.encode_array_size(obj)
if (obj instanceof Object)
return Flick.encode_map_size(obj, opts);
throw new Error(`Cannot determine type of object: ${JSON.stringify(obj)}`)
}
Flick.prototype.encode_inner = function (obj, dataView, offset, opts = {}) {
let sfx = (obj instanceof Array)
? (this.is_assoc_array(obj) ? 'assoc_array' : 'array')
: typeof(obj);
var fun = 'encode_' + sfx;
return this[fun](obj, dataView, offset, opts);
};
Flick.prototype.encode_object = function(obj, dv, offset, opts = {}) {
if (obj === null)
return this.encode_atom("nil", dv, offset);
if (obj === undefined)
return this.encode_atom(String(obj), dv, offset);
switch (obj.type) {
case "atom": return this.encode_atom(obj.value, dv, offset);
case "binary": return this.encode_binary(obj.value, dv, offset);
case "tuple": return this.encode_tuple(obj, dv, offset);
case "ref": return this.encode_ref(obj, dv, offset);
case "pid": return this.encode_pid(obj, dv, offset);
case "map": return this.encode_map(obj, dv, offset, opts);
}
if (Flick.is_assoc_array(obj))
return this.encode_assoc_array(obj, dv, offset);
else if (obj instanceof Array)
return this.encode_array(obj, dv, offset);
//var s = this.getClassName(obj);
//if (Array.isArray(obj) || s.indexOf("Array") != -1) return this.encode_array(obj, dv, offset);
// Treat it as a tuple
return this.encode_tuple_or_map(obj, dv, offset, opts);
}
Flick.prototype.encode_undefined = function(obj, dv, offset) {
if (obj !== undefined && obj !== null)
throw new Error('Object value must by undefined or null. Found: ' + String(obj));
return this.encode_atom(String(obj), dv, offset);
}
Flick.prototype.encode_string_size = function(obj) {
return 1 + 2 + obj.length; // FIXME: implement encoding for length > 0xFF
}
Flick.prototype.encode_string = function(obj, dv, offset) {
dv.setUint8(offset++, this.Enum.STRING);
dv.setUint16(offset, obj.length); // FIXME: check length > 0xFF
offset += 2;
for (let i = 0, n = obj.length; i < n; ++i)
dv.setUint8(offset++, obj.charCodeAt(i));
return { data: dv, offset: offset };
}
Flick.prototype.encode_boolean = function(obj, dv, offset) {
return this.encode_atom(obj ? "true" : "false", dv, offset);
}
Flick.prototype.encode_number_size = function(obj) {
var s, isInteger = this.isInt(obj);
// Handle floats
if (!isInteger) return 1 + 8;
// Small int
if (obj >= 0 && obj < 256) return 1 + 1;
// 4 byte int
if (obj >= -2147483648 && obj <= 2147483647) return 1 + 4;
// Bignum
var n = 0;
if (obj < 0) obj = -obj;
for (; obj; ++n, obj = Math.floor(obj / 256));
return 1 + 2 + n;
}
Flick.prototype.encode_number = function(obj, dv, offset) {
// assuming that obj is numeric, otherwise need to check that: obj === +obj
// Handle floats
if (!this.isInt(obj)) return this.encode_float(obj, dv, offset);
// Small int...
if (obj >= 0 && obj < 256) {
dv.setUint8(offset++, this.Enum.SMALL_INTEGER);
dv.setUint8(offset++, obj);
return { data: dv, offset: offset };
}
// 4 byte int
if (obj >= -2147483648 && obj <= 2147483647) {
dv.setUint8(offset++, this.Enum.INTEGER);
dv.setUint32(offset, obj);
return { data: dv, offset: offset+4 };
}
// Bignum
var pos = offset;
offset += 2; // code, arity
dv.setUint8(offset++, obj < 0 ? 1 : 0); // Sign
if (obj < 0) obj = -obj;
var n = 0;
for (; obj; ++n, obj = Math.floor(obj / 256)) {
var i = obj % 256;
dv.setUint8(offset++, i);
}
var code = n < 256 ? this.Enum.SMALL_BIG : this.Enum.LARGE_BIG;
dv.setUint8(pos++, code);
dv.setUint8(pos, n);
return { data: dv, offset: offset };
}
Flick.prototype.encode_float = function(obj, dv, offset) {
// NaN/Infinity/-Infinity would encode fine here but Erlang's
// binary_to_term rejects non-finite NEW_FLOAT values with badarg.
if (!Number.isFinite(obj))
throw new Error(`Cannot encode non-finite float: ${obj}`);
dv.setUint8(offset++, this.Enum.NEW_FLOAT);
dv.setFloat64(offset, obj);
return { data: dv, offset: offset+8 };
}
Flick.prototype.encode_atom = function(obj, dv, offset) {
dv.setUint8(offset++, this.Enum.ATOM);
dv.setUint16(offset, obj.length);
offset += 2;
for (let i = 0, n = obj.length; i < n; ++i)
dv.setUint8(offset++, obj.charCodeAt(i));
return { data: dv, offset: offset };
}
Flick.prototype.encode_binary = function(obj, dv, offset) {
dv.setUint8(offset++, this.Enum.BINARY);
dv.setUint32(offset, obj.length);
offset += 4;
if (obj instanceof Array)
for (let i = 0, n = obj.length; i < n; ++i)
dv.setUint8(offset++, obj[i]);
else if (typeof(obj) === 'string')
for (let i = 0, n = obj.length; i < n; ++i)
dv.setUint8(offset++, obj.charCodeAt(i));
else
throw new Error(`Invalid ErlBinary data type: ${typeof(obj)}`)
return { data: dv, offset: offset };
}
Flick.prototype.encode_tuple_size = function(obj, dv, offset) {
if (obj instanceof Array) {
return obj.reduce(
(s,i) => s + Flick.encode_size(i),
1 + (obj.length < 256 ? 1 : 4));
} else if (obj instanceof Object) {
let len = 0, sz = 0;
for (const p in obj) if (obj.hasOwnProperty(p)) {
sz += Flick.encode_size(p) + Flick.encode_size(obj[p]);
if (++len > 1)
throw new Error(`Invalid size of a tuple that is likely part of a proplist: ${JSON.stringify(obj)}`);
}
return 1 + (len < 256 ? 1 : 4) + sz;
} else
throw new Error(`Invalid type of tuple data: ${typeof(obj)}`)
}
Flick.prototype.encode_tuple_or_map = function(obj, dv, offset, opts = {}) {
if (!(obj instanceof ErlTuple) && obj instanceof Object) {
let len = 0, out = [];
for (const p in obj) if (obj.hasOwnProperty(p)) {
out.push(p);
out.push(obj[p]);
if (++len > 1)
return this.encode_map(obj, dv, offset, opts);
}
obj = new ErlTuple(out);
}
return this.encode_tuple(obj, dv, offset)
}
Flick.prototype.encode_tuple = function(obj, dv, offset) {
if (obj instanceof ErlTuple) {
var n = obj.length;
if (n < 256) {
dv.setUint8(offset++, this.Enum.SMALL_TUPLE);
dv.setUint8(offset++, n);
} else {
dv.setUint8(offset++, this.Enum.LARGE_TUPLE);
dv.setUint32(offset, n);
offset += 4;
}
return obj.value.reduce(
(a, e) => Flick.encode_inner(e, a.data, a.offset),
{data: dv, offset: offset});
} else
throw new Error(`Invalid type of tuple object: ${JSON.stringify(obj)}`)
}
Flick.prototype.encode_pid = function(obj, dv, offset) {
dv.setUint8(offset++, this.Enum.NEW_PID);
var r = this.encode_atom(obj.node.value, dv, offset);
offset = r.offset;
dv.setUint32(offset, obj.id); offset += 4;
dv.setUint32(offset, obj.serial); offset += 4;
dv.setUint32(offset, obj.creation); offset += 4;
return { data: dv, offset: offset };
}
Flick.prototype.encode_ref = function(obj, dv, offset) {
dv.setUint8(offset++, this.Enum.NEWER_REFERENCE);
dv.setUint16(offset, obj.ids.length); offset += 2;
var r = this.encode_atom(obj.node.value, dv, offset);
offset = r.offset;
dv.setUint32(offset, obj.creation); offset += 4;
offset = obj.ids.reduce((n,i) => { dv.setUint32(n, i); return n+4; }, offset);
return { data: dv, offset: offset };
}
Flick.prototype.encode_map_size = function(obj, opts) {
if (!(obj instanceof Object))
throw new Error(`Invalid data type for encoding map size: ${typeof(obj)}`);
const mapKeyType = opts.mapKeyType || 'binary'
const mapAtomKey = mapKeyType == 'atom'
const mapBinKey = mapKeyType == 'binary'
const sz = Object.entries(obj).reduce((a,kv) => {
const k = (typeof(kv[0]) === 'string') ? (mapBinKey ? ErlBinary.prototype.encodeSize(kv[0]) :
mapAtomKey ? ErlAtom.prototype.encodeSize(kv[0]) :
this.encode_string_size(kv[0]))
: Flick.encode_size(kv[0]);
return a += k + Flick.encode_size(kv[1]);
}, 5) // Flick.Enum.MAP + arity
return sz;
}
Flick.prototype.encode_map = function(obj, dv, offset, opts = {}) {
dv.setUint8(offset++, this.Enum.MAP);
let len = 0;
if (obj instanceof ErlMap)
obj = obj.value
else if (!typeof(obj) == 'object')
throw new Error(`ErlMap encoding expects an object, got: ${typeof(obj)}`);
for(let prop in obj) if (obj.hasOwnProperty(prop)) ++len;
dv.setUint32(offset, len); offset += 4;
const mapKeyType = opts.mapKeyType || 'binary'
const mapAtomKey = mapKeyType == 'atom'
const mapBinKey = mapKeyType == 'binary'
return Object.entries(obj).reduce((o,kv) => {
o = (typeof(kv[0]) === 'string') ? (mapBinKey ? this.encode_binary(kv[0], o.data, o.offset) :
mapAtomKey ? this.encode_atom(kv[0], o.data, o.offset) :
this.encode_string(kv[0], o.data, o.offset))
: Flick.encode_inner(kv[0], o.data, o.offset);
return this.encode_inner(kv[1], o.data, o.offset);
}, {data: dv, offset: offset}) // Flick.Enum.MAP + arity
}
//Flick.prototype.encode_object = function(obj, dv, offset) { return this.encode_map(obj, dv, offset); }
Flick.prototype.encode_array_size = function(obj) {
return obj.reduce((a,e) => a + Flick.encode_size(e), obj.length ? 6 : 1)
}
Flick.prototype.encode_array_size = function(obj) {
return obj.reduce((a,e) => a + Flick.encode_size(e), obj.length ? 6 : 1)
}
Flick.prototype.encode_array = function(obj, dv, offset) {
if (obj.length > 0) {
dv.setUint8(offset++, this.Enum.LIST);
dv.setUint32(offset, obj.length); offset += 4;
offset = obj.reduce(
(n,e) => { var r = Flick.encode_inner(e, dv, n); return r.offset; },
offset
);
}
dv.setUint8(offset++, this.Enum.NIL);
return { data: dv, offset: offset };
}
Flick.prototype.is_assoc_array = function(obj) {
return (obj instanceof Array) &&
obj.length > 0 &&
obj.every(e => {
if (e instanceof ErlTuple && e.length === 2 && e.value[0] instanceof ErlAtom)
return true;
if (!(e instanceof Object)) return false;
const keys = Object.keys(e);
if (keys.length !== 1) return false;
return keys[0] instanceof ErlAtom || typeof(keys[0]) == 'string';
})
}
Flick.prototype.encode_assoc_array_size = function(obj) {
if (!obj instanceof Array)
throw new Error(`Invalid type of data in assoc array: ${typeof(obj)} (expected array)`)
const sz = obj.reduce(
(a,p) => {
let len = 0; let tmp;
for (const k in p)
if (p.hasOwnProperty(k)) {
if (++len > 1) { tmp = p; break; }
a += 2 // tuple
+ this.atom(k).encodeSize() + this.encode_size(p[k]);
}
if (len != 1)
throw new Error(`Invalid size of tuple inside a proplist: ${JSON.stringify(tmp)}`);
return a;
},
6 // list begin/end
)
return sz
}
Flick.prototype.encode_assoc_array = function(obj, dv, offset) {
if (!obj instanceof Array)
throw new Error(`Invalid type of data in assoc array: ${typeof(obj)} (expected array)`)
for (let i=0; i < obj.length; ++i) {
let item=obj[i], key, val, len = 0;
for (const k in item)
if (item.hasOwnProperty(k)) {
if (++len > 1) break;
key = k;
val = item[k];
}
if (len != 1)
throw new Error(`Invalid size of tuple inside a proplist: ${JSON.stringify(item)}`);
obj[i] = this.tuple(this.atom(key), val);
}
return this.encode_array(obj, dv, offset);
}
//-----------------------------------------------------------------------------
// - DECODING -
//-----------------------------------------------------------------------------
Flick.prototype.decode_inner = function(obj) {
const dv = obj.data;
const type = dv.getUint8(obj.offset);
switch (type) {
case this.Enum.SMALL_ATOM: return this.decode_atom(obj);
case this.Enum.ATOM: return this.decode_atom(obj);
case this.Enum.ATOM_UTF8: return this.decode_atom(obj);
case this.Enum.SMALL_ATOM_UTF8: return this.decode_atom(obj);
case this.Enum.STRING: return this.decode_string(obj);
case this.Enum.SMALL_INTEGER: return this.decode_integer(obj);
case this.Enum.INTEGER:
case this.Enum.SMALL_BIG:
case this.Enum.LARGE_BIG: return this.decode_integer(obj);
case this.Enum.FLOAT:
case this.Enum.NEW_FLOAT: return this.decode_float(obj);
case this.Enum.LIST: return this.decode_list(obj);
case this.Enum.MAP: return this.decode_map(obj);
case this.Enum.NIL: return { value: [], offset: obj.offset+1 };
case this.Enum.SMALL_TUPLE:
case this.Enum.LARGE_TUPLE: return this.decode_tuple(obj);
case this.Enum.BINARY: return this.decode_binary(obj);
case this.Enum.PID:
case this.Enum.NEW_PID: return this.decode_pid(obj);
case this.Enum.NEW_REFERENCE:
case this.Enum.NEWER_REFERENCE: return this.decode_ref(obj);
default: throw new Error("Unexpected Erlang type: " +
type + " at offset " + obj.offset);
}
}
Flick.prototype.decode_atom = function(obj) {
var dv = obj.data;
var offset = obj.offset;
var n, type = dv.getUint8(offset++);
switch (type) {
case this.Enum.ATOM:
case this.Enum.ATOM_UTF8:
n = dv.getUint16(offset); offset += 2;
break;
case this.Enum.SMALL_ATOM:
case this.Enum.SMALL_ATOM_UTF8:
n = dv.getUint8(offset++);
break;
default:
throw new Error("Invalid Erlang atom: " +
type + " at offset " + offset);
}
var a = new Uint8Array(dv.buffer, offset, n);
offset += n;
var s = String.fromCharCode.apply(String, a);
var v;
switch (s) {
case "true": v = true; break;
case "false": v = false; break;
case "undefined": v = undefined; break;
case "null":
case "nil": v = null; break;
default: v = this.atom(s);
}
return { value: v, offset: offset };
}
Flick.prototype.decode_binary = function(obj) {
var dv = obj.data;
var offset = obj.offset;
var type = dv.getUint8(offset++);
if (type !== this.Enum.BINARY)
throw new Error("Invalid Erlang binary: " + type + " at offset " + offset);
var n = dv.getUint32(offset); offset += 4;
var a = new Array(n);
for (let i=offset, j=0, m = offset+n; i < m; ++i, ++j) a[j] = dv.getUint8(i);
return { value: this.binary(a), offset: offset+n };
}
Flick.prototype.decode_integer = function(obj) {
var dv = obj.data;
var offset = obj.offset;
var type = dv.getUint8(offset++);
var v, arity, sign;
switch (type) {
case this.Enum.SMALL_INTEGER:
v = dv.getUint8(offset++);
break;
case this.Enum.INTEGER:
v = dv.getInt32(offset); offset += 4;
break;
case this.Enum.SMALL_BIG:
arity = dv.getUint8(offset++);
// Deliverately falling through
case this.Enum.LARGE_BIG:
if (type != this.Enum.SMALL_BIG) {
arity = dv.getUint32(offset); offset += 4;
}
if (arity > 8)
throw new Error("Integer value too large for type: " +
type + " arity " + arity);
sign = dv.getUint8(offset++);
v = 0;
for (let i = 0, n = 1; i < arity; ++i, n *= 256)
v += dv.getUint8(offset++) * n;
if (sign) v = -v;
break;
default:
throw new Error("Invalid Erlang integer type: " +
type + " at offset " + offset);
}
return { value: v, offset: offset };
}
Flick.prototype.decode_float = function(obj) {
var dv = obj.data;
var offset = obj.offset;
var type = dv.getUint8(offset++);
var v, n;
switch (type) {
case this.Enum.FLOAT:
n = 31;
var A = new Uint8Array(dv.buffer, offset, n);
offset += n;
var S = String.fromCharCode.apply(String, A);
v = parseFloat(S);
break;
case this.Enum.NEW_FLOAT:
v = dv.getFloat64(offset); offset += 8;
break;
default:
throw new Error("Invalid Erlang float type: " +
type + " at offset " + offset);
}
return { value: v, offset: offset };
}
Flick.prototype.decode_string = function(obj) {
var dv = obj.data;
var offset = obj.offset;
var n, s, type = dv.getUint8(offset++);
switch (type) {
case this.Enum.STRING:
n = dv.getUint16(offset); offset += 2;
var a = new Uint8Array(dv.buffer, offset, n);
offset += n;
s = String.fromCharCode.apply(String, a);
break;
case this.Enum.LIST:
n = dv.getUint32(offset); offset += 4;
var r = [];
for (let i = 0; i < n; i++) {
if (dv.getUint8(offset++) !== this.SMALL_INTEGER)
throw new Error("Error decoding string.");
var c = dv.getUint8(offset++);
r.push(c);
}
s = String.fromCharCode.apply(String, r);
break;
case this.Enum.NIL:
s = "";
break;
default:
throw new Error("Invalid Erlang string type: " +
type + " at offset " + offset);
}
return { value: s, offset: offset };
}
Flick.prototype.decode_list = function(obj) {
var dv = obj.data;
var offset = obj.offset;
var n,r,type = dv.getUint8(offset++);
switch (type) {
case this.Enum.STRING:
n = dv.getUint16(offset); offset += 2;
var a = new Uint8Array(dv.buffer, offset, n);
offset += n;
r = String.fromCharCode.apply(String, a);
break;
case this.Enum.LIST:
n = dv.getUint32(offset);
obj.offset = offset + 4;
r = new Array(n);
for (let i = 0; i < n; ++i) {
var res = Flick.decode_inner(obj);
r[i] = res.value;
obj.offset = res.offset;
}
offset = obj.offset;
if (dv.byteLength > offset && dv.getUint8(offset) === this.Enum.NIL)
offset++;
// Check if the list is an associative array
//if (this.is_assoc_array(r))
// // Try to convert the associative array to an object
// for (let i=0; i < r.length; ++i)
// if (r[i] instanceof ErlTuple && r[i].length === 2) {
// const k = r[i].value[0], v = r[i].value[1];
// r[i] = {}
// r[i][k] = v
// }
break;
case this.Enum.NIL:
r = [];
break;
default:
throw new Error("Invalid Erlang list type: " +
type + " at offset " + offset);
}
return { value: r, offset: offset };
}
Flick.prototype.decode_map = function(obj) {
const dv = obj.data;
let offset = obj.offset;
const type = dv.getUint8(offset++);
if (type !== this.Enum.MAP)
throw new Error("Invalid map type: " + type);
const arity = dv.getUint32(offset);
obj.offset = offset + 4;
let res = {}
for (let i=0; i < arity; ++i) {
const key = Flick.decode_inner(obj);
obj.offset = key.offset;
const val = Flick.decode_inner(obj);
obj.offset = val.offset;
const kv = key.value;
const k = (kv instanceof ErlAtom) ? kv.value
: (kv instanceof ErlBinary) ? String.fromCharCode.apply(String, kv.value)
: (typeof(kv) === 'string') ? kv
: kv.hasOwnProperty('value')? kv.value
: kv;
res[k] = val.value;
}
return {value: res, offset: obj.offset};
}
Flick.prototype.decode_tuple = function(obj) {
var dv = obj.data;
var offset = obj.offset;
var n, type = dv.getUint8(offset++);
switch (type) {
case this.Enum.SMALL_TUPLE:
n = dv.getUint8(offset);
obj.offset = offset + 1;
break;
case this.Enum.LARGE_TUPLE:
n = dv.getUint32(offset);
obj.offset += 4;
break;
default:
throw new Error("Invalid Erlang tuple type: " +
type + " at offset " + offset);
}
var r = new Array(n);
for (let i = 0; i < n; i++) {
var res = Flick.decode_inner(obj);
r[i] = res.value;
obj.offset = res.offset;
}
return { value: this.tuple.apply(this, r), offset: obj.offset };
}
Flick.prototype.decode_pid = function(obj) {
var dv = obj.data;
var offset = obj.offset;
var type = dv.getUint8(offset++);
if (type !== this.Enum.PID && type !== this.Enum.NEW_PID)
throw new Error("Invalid pid type: " + type);
obj.offset = offset;
var r = this.decode_atom(obj);
offset = r.offset;
var Id = dv.getUint32(offset); offset += 4;
var Sn = dv.getUint32(offset); offset += 4;
var cr;
if (type === this.Enum.NEW_PID) {
cr = dv.getUint32(offset); offset += 4;
} else {
cr = dv.getUint8(offset++);
}
return { value: this.pid(r.value, Id, Sn, cr), offset: offset };
}
Flick.prototype.decode_ref = function(obj) {
var dv = obj.data;
var offset = obj.offset;
var type = dv.getUint8(offset++);
if (type !== this.Enum.NEW_REFERENCE && type !== this.Enum.NEWER_REFERENCE)
throw new Error("Invalid ref type: " + type);
var n = dv.getUint16(offset); offset += 2;
var ids = new Array(n);
obj.offset = offset;
var r = this.decode_atom(obj);
offset = r.offset;
var cr;
if (type === this.Enum.NEWER_REFERENCE) {
cr = dv.getUint32(offset); offset += 4;
} else {
cr = dv.getUint8(offset++);
}
for (let i = 0; i < n; ++i, offset += 4)
ids[i] = dv.getUint32(offset);
return { value: this.ref(r.value, cr, ids), offset: offset };
}
//-----------------------------------------------------------------------------
// - UTILITY FUNCTIONS -
//-----------------------------------------------------------------------------
Flick.prototype.getClassName = function(obj) {
const funcNameRegex = /(.{1,}) => \(/;
const results = (funcNameRegex).exec(obj.constructor.toString());
return (results && results.length > 1) ? results[1] : "";
};
Flick.prototype.isInt = function(x) { return parseFloat(x) == parseInt(x) && !isNaN(x); }
Flick.prototype.timestampToTuple = function(n) {
var Ms = Math.floor(n / 1000000000); n -= Ms*1000000000;
var s = Math.floor(n / 1000); n -= s*1000;
var ms = n;
return new ErlTuple([Ms, s, ms]);
}
Flick.prototype.dateToTuple = function(d) {
var n = d.getTime();
var Ms = Math.floor(n / 1000000000); n -= Ms*1000000000;
var s = Math.floor(n / 1000); n -= s*1000;
var ms = n;
return new ErlTuple([Ms, s, ms]);
}
var Flick = new Flick();
// Override console log to display Flick objects friendly
//(() => {
// var cl = console.log;
// console.log = () => {
// cl.apply(console, [].slice.call(arguments).map((el) => {
// return typeof el === 'object' // {}.toString.call(el) === '[object Object]'
// && typeof el.toString === 'function'
// && el.toString !== Object.prototype.toString ? el.toString() : el;
// }));
// };
// console.oldlog = cl;
//}());
// attach the .equals method to Array's prototype to call it on any array
Array.prototype.equals = function(rhs) {
// if the other rhs is undefined or null return a false value
if (!rhs)
return false;
// compare lengths - can save a lot of time
if (this.length != rhs.length)
return false;
for (let i = 0, l=this.length; i < l; i++) {
// Check if we have nested arrays
if ( (this[i] instanceof Array && rhs[i] instanceof Array)
|| (this[i] instanceof ErlTuple && rhs[i] instanceof ErlTuple)
|| (this[i].equals !== undefined))
{
// recurse into the nested arrays
if (!this[i].equals(rhs[i]))
return false;
} else if (this[i] !== rhs[i])
return false;
}
return true;
}
ArrayBuffer.prototype.equals = function(rhs) {
if (!rhs)
return false;
const a = new Uint8Array(this);
const b = rhs instanceof ArrayBuffer ? new Uint8Array(rhs): rhs;
// compare lengths - can save a lot of time
if (a.length != b.length)
return false;
for (let i = 0, l=a.length; i < l; ++i)
if (a[i] !== b[i])
return false;
return true;
}
Flick.objectEquals = function(lhs, rhs) {
'use strict';
if (lhs === null || lhs === undefined ||
rhs === null || rhs === undefined) return lhs === rhs;
// after lhs just checking type of one would be enough
if (lhs.constructor !== rhs.constructor) return false;
// if they are functions, they should exactly refer to same one (because of closures)
if (lhs instanceof Function) return lhs === rhs;
// if they are regexps, they should exactly refer to same one (it is hard to better equality check on current ES)
if (lhs instanceof RegExp) return lhs === rhs;
if (lhs === rhs || lhs.valueOf() === rhs.valueOf()) return true;
if (Array.isArray(lhs)) return lhs.equals(rhs);
// if they are dates, they must had equal valueOf
if (lhs instanceof Date) return false;
// if they are strictly equal, they both need to be object at least
if (!(lhs instanceof Object)) return false;
if (!(rhs instanceof Object)) return false;
// recursive object equality check
return Object.keys(rhs).every(i => lhs[i] !== undefined) &&
Object.keys(lhs).every(i => objectEquals(lhs[i], rhs[i]));
}
Flick.objectDeepClone = function(obj, override = undefined, filterKeys = () => true) {
let res = {}
function doMerge(dst, src, path) {
for (const [key, val] of Object.entries(src)) {
if (!filterKeys(path, key)) continue
if (Array.isArray(val)) {
dst[key] = val.slice() // Clone the array
continue
} else if (val === null || typeof val !== "object") {
dst[key] = val
continue
}
if (dst[key] === undefined)
dst[key] = new val.__proto__.constructor();
const p = [...path, key]
doMerge(dst[key], val, p);
}
}
doMerge(res, obj, [])
if (override !== undefined)
doMerge(res, override, [])
return res
}