UNPKG

superpack

Version:

JavaScript implementation of the SuperPack extensible schemaless binary encoding format

545 lines (483 loc) 19.4 kB
'use strict'; 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;