superpack
Version:
JavaScript implementation of the SuperPack extensible schemaless binary encoding format
545 lines (483 loc) • 19.4 kB
JavaScript
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.encode = undefined;
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _typeTags = require('./type-tags.js');
var _typeTags2 = _interopRequireDefault(_typeTags);
var _extendable = require('./extendable.js');
var _extendable2 = _interopRequireDefault(_extendable);
var _depthBoundExtension = require('./depth-bound-extension.js');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
/* global ArrayBuffer Uint8Array Float32Array Float64Array */
// TODO: refactor string encoding (a la superpack-java) so that this can be just Array<number>
var F16 = 0xFFFFFFFFFFFFFFFF;
var F8 = 0xFFFFFFFF;
var F4 = 0xFFFF;
var F2 = 0xFF;
function encodeUInt(value, target) {
if (value <= 63) {
target.push(value);
} else if (value <= 0x3FFF) {
target.push(_typeTags2.default.UINT14_BASE | value >> 8, value & F2);
} else if (value <= F4) {
target.push(_typeTags2.default.UINT16, value >> 8, value & F2);
} else if (value <= 0xFFFFFF) {
target.push(_typeTags2.default.UINT24, value >> 16, value >> 8 & F2, value & F2);
} else if (value <= F8) {
target.push(_typeTags2.default.UINT32);
pushUInt32(value, target);
} else {
target.push(_typeTags2.default.UINT64);
pushUInt32(value / 0x100000000 & F8, target);
pushUInt32(value & F8, target);
}
}
function encodeInteger(value, target) {
if (value === 0 && 1 / value === -Infinity) {
target.push(_typeTags2.default.NINT4_BASE);
} else if (value >= 0) {
encodeUInt(value, target);
} else {
var magnitude = -value;
if (magnitude <= 15) {
target.push(_typeTags2.default.NINT4_BASE | magnitude);
} else if (magnitude <= F2) {
target.push(_typeTags2.default.NINT8, magnitude);
} else if (magnitude <= F4) {
target.push(_typeTags2.default.NINT16, magnitude >> 8, magnitude & F2);
} else if (magnitude <= F8) {
target.push(_typeTags2.default.NINT32);
pushUInt32(magnitude, target);
} else {
target.push(_typeTags2.default.NINT64);
pushUInt32(magnitude / 0x100000000 & F8, target);
pushUInt32(magnitude & F8, target);
}
}
}
function encodeDate(value, target) {
// timestamp: same as int48, with unix timestamp in ms
var timestamp = Date.prototype.getTime.call(value);
var high = timestamp / 0x100000000;
if (timestamp < 0) --high;
target.push(_typeTags2.default.TIMESTAMP, high >>> 8 & F2, high & F2);
pushUInt32(timestamp >>> 0, target);
}
function encodeString(str, target, lut) {
var index = lut ? lut.indexOf(str) : -1;
if (index !== -1) {
// Using indexOf knowing lut.length <= 255 so it's O(1)
// todo: consider better ways to do this
target.push(_typeTags2.default.STRREF, index);
} else {
// Note: this encoding fails if value contains an unmatched surrogate half.
// utf8Ascii will be an ascii representation of UTF-8 bytes
// ref: http://monsur.hossa.in/2012/07/20/utf-8-in-javascript.html
var utf8Bytes = [];
var utf8Ascii = unescape(encodeURIComponent(str));
var containsNull = false;
for (var i = 0; i < utf8Ascii.length; ++i) {
utf8Bytes.push(utf8Ascii.charCodeAt(i));
if (utf8Bytes[i] === 0) containsNull = true;
}
var numBytes = utf8Bytes.length;
if (numBytes < 32) {
target.push(_typeTags2.default.STR5_BASE | numBytes);
} else if (!containsNull) {
target.push(_typeTags2.default.CSTRING);
utf8Bytes.push(0);
} else if (numBytes <= F2) {
target.push(_typeTags2.default.STR8, numBytes);
} else {
target.push(_typeTags2.default.STR_);
encodeUInt(numBytes, target);
}
var APPLY_CHUNK_SIZE = 0xFFFF;
if (utf8Bytes.length > APPLY_CHUNK_SIZE) {
for (var _i = 0; _i < utf8Bytes.length; _i += APPLY_CHUNK_SIZE) {
[].push.apply(target, utf8Bytes.slice(_i, _i + APPLY_CHUNK_SIZE));
}
} else {
[].push.apply(target, utf8Bytes);
}
}
}
var encodeFloat = typeof Float32Array === 'function' && typeof Float64Array === 'function' && typeof Uint8Array === 'function' ? function (value, target) {
var tag = _typeTags2.default.FLOAT32;
var f = new Float32Array(1);
f[0] = value;
if (f[0] !== value) {
tag = _typeTags2.default.DOUBLE64;
f = new Float64Array(1);
f[0] = value;
}
var u = new Uint8Array(f.buffer);
target.push(tag);
for (var i = u.length - 1; i >= 0; --i) {
target.push(u[i]);
}
} : function (value, target) {
// eBits + mBits + 1 is a multiple of 8
var eBits = 11;
var mBits = 52;
var bias = (1 << eBits - 1) - 1;
var isNegative = value < 0 || value === 0 && 1 / value < 0;
var v = Math.abs(value);
var exp = void 0,
mantissa = void 0;
if (v === 0) {
exp = bias;
mantissa = 0;
} else if (v >= Math.pow(2, 1 - bias)) {
// normal
exp = Math.min(Math.floor(Math.log(v) / Math.LN2), 1023);
var significand = v / Math.pow(2, exp);
if (significand < 1) {
exp -= 1;
significand *= 2;
}
if (significand >= 2) {
exp += 1;
significand /= 2;
}
var mMax = Math.pow(2, mBits);
mantissa = roundToEven(significand * mMax) - mMax;
exp += bias;
if (mantissa / mMax >= 1) {
exp += 1;
mantissa = 0;
}
if (exp > 2 * bias) {
// overflow
exp = (1 << eBits) - 1;
mantissa = 0;
}
} else {
// subnormal
exp = 0;
mantissa = roundToEven(v / Math.pow(2, 1 - bias - mBits));
}
var tag = _typeTags2.default.DOUBLE64;
// see if this can be represented using a FLOAT32 without dropping any significant bits
if ((mantissa & 0x1FFFFFFF) === 0 && Math.abs(exp - bias) < 256) {
tag = _typeTags2.default.FLOAT32;
eBits = 8;
mBits = 23;
mantissa /= 0x1FFFFFFF;
exp += bias;
bias = (1 << eBits - 1) - 1;
exp -= bias;
if (v < Math.pow(2, 1 - bias)) {
// subnormal
exp = 0;
mantissa = roundToEven(v / Math.pow(2, 1 - bias - mBits));
}
}
// align sign, exponent, mantissa
var bits = [];
for (var i = mBits - 1; i >= 0; --i) {
bits.unshift(mantissa & 1);
mantissa = Math.floor(mantissa / 2);
}
for (var _i2 = eBits; _i2 > 0; _i2 -= 1) {
bits.unshift(exp & 1);
exp = Math.floor(exp / 2);
}
bits.unshift(isNegative ? 1 : 0);
target.push(tag);
// pack into bytes
for (var _i3 = 0; _i3 < bits.length; _i3 += 8) {
target.push(byteFromBools(bits, _i3));
}
};
function isANaNValue(value) {
// eslint-disable-line no-shadow
return value !== value; // eslint-disable-line no-self-compare
}
function byteFromBools(bools, offset) {
return bools[offset] << 7 | bools[offset + 1] << 6 | bools[offset + 2] << 5 | bools[offset + 3] << 4 | bools[offset + 4] << 3 | bools[offset + 5] << 2 | bools[offset + 6] << 1 | bools[offset + 7];
}
function pushUInt32(n, target) {
target.push(n >>> 24, n >> 16 & F2, n >> 8 & F2, n & F2);
}
function roundToEven(n) {
var w = Math.floor(n),
f = n - w;
if (f < 0.5) return w;
if (f > 0.5) return w + 1;
return w % 2 ? w + 1 : w;
}
function generateStringLUT(hist) {
// Keep the up-to-255 keys that will save the most space, sorted by savings
return Object.keys(hist).filter(function (key) {
return hist[key] >= 2 && key.length * hist[key] >= 8;
})
// [key, expected savings]
.map(function (key) {
return [key, (key.length + 1) * hist[key] - (key.length + 1 + 2 * hist[key])];
}).sort(function (e1, e2) {
return e2[1] - e1[1];
}).slice(0, 255).map(function (elt) {
return elt[0];
});
}
function findIndex(a, predicate) {
for (var i = 0; i < a.length; ++i) {
if (predicate(a[i])) return i;
}
return -1;
}
function isSortedArrayOfThingsSameAsSortedArrayOfThings(a, b) {
return a.length === b.length && a.every(function (c, i) {
return b[i] === c;
});
}
function find(arrayLike, predicate) {
for (var i = 0; i < arrayLike.length; ++i) {
var el = arrayLike[i];
if (predicate(el)) return el;
}
return null;
}
var Encoder = function (_Extendable) {
_inherits(Encoder, _Extendable);
function Encoder(depthBound, depthBoundExtensionPoint) {
_classCallCheck(this, Encoder);
if (depthBound != null && depthBoundExtensionPoint == null || depthBound == null && depthBoundExtensionPoint != null) {
throw new TypeError('depthBound must be specified if and only if depthBoundExtensionPoint is');
}
var _this = _possibleConstructorReturn(this, (Encoder.__proto__ || Object.getPrototypeOf(Encoder)).call(this));
_this.keysets = [];
_this.stringHist = {};
_this.stringPlaceholders = true;
_this.remainingDepth = depthBound == null ? null : depthBound;
_this.depthBoundExtensionPoint = depthBoundExtensionPoint == null ? null : depthBoundExtensionPoint;
return _this;
}
_createClass(Encoder, [{
key: 'encode',
value: function encode(value) {
var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
var output = [];
if (options.keysetsToOmit != null) {
[].push.apply(this.keysets, options.keysetsToOmit);
}
var data = this.encodeValue(value, []);
if (options.keysetsToOmit != null) {
this.keysets = this.keysets.slice(options.keysetsToOmit.length);
}
var keysetData = this.encodeValue(this.keysets, []);
var strings = generateStringLUT(this.stringHist);
this.stringPlaceholders = false;
if (strings.length > 0 || this.keysets.length > 0) {
output.push(_typeTags2.default.STRLUT);
output.push(strings.length);
this.pushArrayElements(strings, output);
data = keysetData.concat(data);
}
data.forEach(function (piece) {
if (typeof piece === 'string') {
encodeString(piece, output, strings);
} else {
output.push(piece);
}
});
return output;
}
/* begin private use area */
// WARN: keys are sorted
}, {
key: 'findKeysetIndex',
value: function findKeysetIndex(keys) {
var index = findIndex(this.keysets, function (a) {
return isSortedArrayOfThingsSameAsSortedArrayOfThings(a, keys);
});
if (index < 0) {
return this.keysets.push(keys) - 1;
}
return index;
}
}, {
key: 'pushArrayElements',
value: function pushArrayElements(value, target) {
var _this2 = this;
[].forEach.call(value, function (element) {
_this2.encodeValue(element, target);
});
}
}, {
key: 'encodeValue',
value: function encodeValue(value, target) {
var _this3 = this;
var ext = find(Object.keys(this.extensions),
// $FlowFixMe: flow doesn't understand that e is an ExtensionPoint
function (e) {
return _this3.extensions[e].detector(value);
});
if (ext != null) {
target.push(_typeTags2.default.EXTENSION);
encodeUInt(+ext, target);
this.encodeValue(this.extensions[+ext].serialiser(value), target);
} else if (value === false) {
target.push(_typeTags2.default.FALSE);
} else if (value === true) {
target.push(_typeTags2.default.TRUE);
} else if (value === null) {
target.push(_typeTags2.default.NULL);
} else if (typeof value === 'undefined') {
target.push(_typeTags2.default.UNDEFINED);
} else if (typeof value === 'number') {
if (isFinite(value)) {
var v = Math.abs(value);
if (Math.floor(v) === v && v < F16 && (v !== 0 || 1 / value > 0)) {
encodeInteger(value, target);
} else {
encodeFloat(value, target);
}
} else if (value === 1 / 0) {
target.push(_typeTags2.default.FLOAT32, 0x7F, 0x80, 0x00, 0x00);
} else if (value === -1 / 0) {
target.push(_typeTags2.default.FLOAT32, 0xFF, 0x80, 0x00, 0x00);
} else if (isANaNValue(value)) {
target.push(_typeTags2.default.FLOAT32, 0x7F, 0xC0, 0x00, 0x00);
}
} else if (typeof value === 'string') {
// Push the string itself for handling later
if (this.stringPlaceholders) {
this.stringHist[value] = (this.stringHist[value] || 0) + 1;
target.push(value);
} else {
encodeString(value, target);
}
} else if ({}.toString.call(value) === '[object Date]') {
encodeDate(value, target);
} else if (typeof ArrayBuffer !== 'undefined' && value instanceof ArrayBuffer) {
target.push(_typeTags2.default.BINARY_);
this.encodeValue(value.byteLength, target);
this.pushArrayElements(new Uint8Array(value), target);
} else {
if (this.remainingDepth !== null) {
if (this.remainingDepth <= 0) {
target.push(_typeTags2.default.EXTENSION);
if (this.depthBoundExtensionPoint == null) {
throw new Error('if remainingDepth != null, depthBoundExtensionPoint must not be either');
}
encodeUInt(this.depthBoundExtensionPoint, target);
target.push(_typeTags2.default.NULL);
return target;
}
--this.remainingDepth;
}
if (Array.isArray(value)) {
var numElements = value.length;
var containsOnlyBooleans = true;
containsOnlyBooleans = value.every(function (element) {
return typeof element === 'boolean';
});
if (containsOnlyBooleans && numElements > 0) {
if (numElements <= 15) {
target.push(_typeTags2.default.BARRAY4_BASE | numElements);
} else if (numElements <= 255) {
target.push(_typeTags2.default.BARRAY8, numElements);
} else {
target.push(_typeTags2.default.BARRAY_);
encodeUInt(numElements, target);
}
for (var i = 0; i < numElements; i += 8) {
// note: there's some out of bounds going on here, but it works out like we want
target.push(byteFromBools(value, i));
}
} else {
if (numElements <= 31) {
target.push(_typeTags2.default.ARRAY5_BASE | numElements);
} else if (numElements <= 255) {
target.push(_typeTags2.default.ARRAY8, numElements);
} else {
target.push(_typeTags2.default.ARRAY_);
encodeUInt(numElements, target);
}
this.pushArrayElements(value, target);
}
} else {
// assumption: anything not in an earlier case can be treated as an object
var keys = Object.keys(value).sort();
var numKeys = keys.length;
var keysetIndex = this.findKeysetIndex(keys);
var _containsOnlyBooleans = keys.every(function (key) {
return typeof value[key] === 'boolean';
});
if (_containsOnlyBooleans) {
target.push(_typeTags2.default.BMAP_);
encodeUInt(keysetIndex, target);
var b = [0, 0, 0, 0, 0, 0, 0, 0];
for (var _i4 = 0; _i4 < numKeys; _i4 += 8) {
for (var j = 0; j < 8; ++j) {
// $FlowFixMe: flow doesn't like our fancy hacks
b[j] = _i4 + j < numKeys && value[keys[_i4 + j]];
}
target.push(byteFromBools(b, 0));
}
} else {
target.push(_typeTags2.default.MAP_);
encodeUInt(keysetIndex, target);
keys.forEach(function (key) {
return _this3.encodeValue(value[key], target);
});
}
}
if (this.remainingDepth !== null) {
++this.remainingDepth;
}
}
return target;
}
}], [{
key: 'encode',
value: function encode(value) {
var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
var depthBoundExtensionPoint = null;
if (options.depthBound != null) {
var _depthBound = options.depthBound;
if (_depthBound < 0 || Math.floor(_depthBound) !== _depthBound) {
throw new RangeError('depthBound, if specified, must be a non-negative integer');
}
if (options.extensions != null) {
Object.keys(options.extensions).forEach(function (key) {
var ext = +key;
// $FlowFixMe: flow doesn't understand that options.extensions is non-null here
var extension = options.extensions[ext];
if (extension === _depthBoundExtension.extension) {
depthBoundExtensionPoint = ext;
}
});
}
if (depthBoundExtensionPoint == null) {
throw new Error('if depthBound is used, its corresponding extension must be provided');
}
// $FlowFixMe: flow doesn't understand that options.extensions is non-null here
} else if (options.extensions != null && Object.keys(options.extensions).some(function (e) {
return options.extensions[e] === _depthBoundExtension.extension;
})) {
throw new Error('if the depthBound extension is used, a depth bound must be specified');
}
var e = new Encoder(options.depthBound, depthBoundExtensionPoint);
if (options.extensions != null) {
// $FlowFixMe: flow doesn't understand that ext is an ExtensionPoint
Object.keys(options.extensions).forEach(function (ext) {
// $FlowFixMe: flow doesn't understand that options.extensions is non-null here
var extension = options.extensions[ext];
e.extend(ext, extension.detector, extension.serialiser, extension.deserialiser);
});
}
return e.encode(value, options);
}
}]);
return Encoder;
}(_extendable2.default);
exports.default = Encoder;
var encode = exports.encode = Encoder.encode;
;