UNPKG

borc

Version:

Encode and parse data in the Concise Binary Object Representation (CBOR) data format (RFC7049).

523 lines (442 loc) 12.1 kB
'use strict' const { Buffer } = require('buffer') const { URL } = require('iso-url') const Bignumber = require('bignumber.js').BigNumber const utils = require('./utils') const constants = require('./constants') const MT = constants.MT const NUMBYTES = constants.NUMBYTES const SHIFT32 = constants.SHIFT32 const SYMS = constants.SYMS const TAG = constants.TAG const HALF = (constants.MT.SIMPLE_FLOAT << 5) | constants.NUMBYTES.TWO const FLOAT = (constants.MT.SIMPLE_FLOAT << 5) | constants.NUMBYTES.FOUR const DOUBLE = (constants.MT.SIMPLE_FLOAT << 5) | constants.NUMBYTES.EIGHT const TRUE = (constants.MT.SIMPLE_FLOAT << 5) | constants.SIMPLE.TRUE const FALSE = (constants.MT.SIMPLE_FLOAT << 5) | constants.SIMPLE.FALSE const UNDEFINED = (constants.MT.SIMPLE_FLOAT << 5) | constants.SIMPLE.UNDEFINED const NULL = (constants.MT.SIMPLE_FLOAT << 5) | constants.SIMPLE.NULL const MAXINT_BN = new Bignumber('0x20000000000000') const BUF_NAN = Buffer.from('f97e00', 'hex') const BUF_INF_NEG = Buffer.from('f9fc00', 'hex') const BUF_INF_POS = Buffer.from('f97c00', 'hex') function toType (obj) { // [object Type] // --------8---1 return ({}).toString.call(obj).slice(8, -1) } /** * Transform JavaScript values into CBOR bytes * */ class Encoder { /** * @param {Object} [options={}] * @param {function(Buffer)} options.stream */ constructor (options) { options = options || {} this.streaming = typeof options.stream === 'function' this.onData = options.stream this.semanticTypes = [ [URL, this._pushUrl], [Bignumber, this._pushBigNumber] ] const addTypes = options.genTypes || [] const len = addTypes.length for (let i = 0; i < len; i++) { this.addSemanticType( addTypes[i][0], addTypes[i][1] ) } this._reset() } addSemanticType (type, fun) { const len = this.semanticTypes.length for (let i = 0; i < len; i++) { const typ = this.semanticTypes[i][0] if (typ === type) { const old = this.semanticTypes[i][1] this.semanticTypes[i][1] = fun return old } } this.semanticTypes.push([type, fun]) return null } push (val) { if (!val) { return true } this.result[this.offset] = val this.resultMethod[this.offset] = 0 this.resultLength[this.offset] = val.length this.offset++ if (this.streaming) { this.onData(this.finalize()) } return true } pushWrite (val, method, len) { this.result[this.offset] = val this.resultMethod[this.offset] = method this.resultLength[this.offset] = len this.offset++ if (this.streaming) { this.onData(this.finalize()) } return true } _pushUInt8 (val) { return this.pushWrite(val, 1, 1) } _pushUInt16BE (val) { return this.pushWrite(val, 2, 2) } _pushUInt32BE (val) { return this.pushWrite(val, 3, 4) } _pushDoubleBE (val) { return this.pushWrite(val, 4, 8) } _pushNaN () { return this.push(BUF_NAN) } _pushInfinity (obj) { const half = (obj < 0) ? BUF_INF_NEG : BUF_INF_POS return this.push(half) } _pushFloat (obj) { const b2 = Buffer.allocUnsafe(2) if (utils.writeHalf(b2, obj)) { if (utils.parseHalf(b2) === obj) { return this._pushUInt8(HALF) && this.push(b2) } } const b4 = Buffer.allocUnsafe(4) b4.writeFloatBE(obj, 0) if (b4.readFloatBE(0) === obj) { return this._pushUInt8(FLOAT) && this.push(b4) } return this._pushUInt8(DOUBLE) && this._pushDoubleBE(obj) } _pushInt (obj, mt, orig) { const m = mt << 5 if (obj < 24) { return this._pushUInt8(m | obj) } if (obj <= 0xff) { return this._pushUInt8(m | NUMBYTES.ONE) && this._pushUInt8(obj) } if (obj <= 0xffff) { return this._pushUInt8(m | NUMBYTES.TWO) && this._pushUInt16BE(obj) } if (obj <= 0xffffffff) { return this._pushUInt8(m | NUMBYTES.FOUR) && this._pushUInt32BE(obj) } if (obj <= Number.MAX_SAFE_INTEGER) { return this._pushUInt8(m | NUMBYTES.EIGHT) && this._pushUInt32BE(Math.floor(obj / SHIFT32)) && this._pushUInt32BE(obj % SHIFT32) } if (mt === MT.NEG_INT) { return this._pushFloat(orig) } return this._pushFloat(obj) } _pushIntNum (obj) { if (obj < 0) { return this._pushInt(-obj - 1, MT.NEG_INT, obj) } else { return this._pushInt(obj, MT.POS_INT) } } _pushNumber (obj) { switch (false) { case (obj === obj): // eslint-disable-line return this._pushNaN(obj) case isFinite(obj): return this._pushInfinity(obj) case ((obj % 1) !== 0): return this._pushIntNum(obj) default: return this._pushFloat(obj) } } _pushString (obj) { const len = Buffer.byteLength(obj, 'utf8') return this._pushInt(len, MT.UTF8_STRING) && this.pushWrite(obj, 5, len) } _pushBoolean (obj) { return this._pushUInt8(obj ? TRUE : FALSE) } _pushUndefined (obj) { return this._pushUInt8(UNDEFINED) } _pushArray (gen, obj) { const len = obj.length if (!gen._pushInt(len, MT.ARRAY)) { return false } for (let j = 0; j < len; j++) { if (!gen.pushAny(obj[j])) { return false } } return true } _pushTag (tag) { return this._pushInt(tag, MT.TAG) } _pushDate (gen, obj) { // Round date, to get seconds since 1970-01-01 00:00:00 as defined in // Sec. 2.4.1 and get a possibly more compact encoding. Note that it is // still allowed to encode fractions of seconds which can be achieved by // changing overwriting the encode function for Date objects. return gen._pushTag(TAG.DATE_EPOCH) && gen.pushAny(Math.round(obj / 1000)) } _pushBuffer (gen, obj) { return gen._pushInt(obj.length, MT.BYTE_STRING) && gen.push(obj) } _pushNoFilter (gen, obj) { return gen._pushBuffer(gen, obj.slice()) } _pushRegexp (gen, obj) { return gen._pushTag(TAG.REGEXP) && gen.pushAny(obj.source) } _pushSet (gen, obj) { if (!gen._pushInt(obj.size, MT.ARRAY)) { return false } for (const x of obj) { if (!gen.pushAny(x)) { return false } } return true } _pushUrl (gen, obj) { return gen._pushTag(TAG.URI) && gen.pushAny(obj.format()) } _pushBigint (obj) { let tag = TAG.POS_BIGINT if (obj.isNegative()) { obj = obj.negated().minus(1) tag = TAG.NEG_BIGINT } let str = obj.toString(16) if (str.length % 2) { str = '0' + str } const buf = Buffer.from(str, 'hex') return this._pushTag(tag) && this._pushBuffer(this, buf) } _pushBigNumber (gen, obj) { if (obj.isNaN()) { return gen._pushNaN() } if (!obj.isFinite()) { return gen._pushInfinity(obj.isNegative() ? -Infinity : Infinity) } if (obj.isInteger()) { return gen._pushBigint(obj) } if (!(gen._pushTag(TAG.DECIMAL_FRAC) && gen._pushInt(2, MT.ARRAY))) { return false } const dec = obj.decimalPlaces() const slide = obj.multipliedBy(new Bignumber(10).pow(dec)) if (!gen._pushIntNum(-dec)) { return false } if (slide.abs().isLessThan(MAXINT_BN)) { return gen._pushIntNum(slide.toNumber()) } else { return gen._pushBigint(slide) } } _pushMap (gen, obj) { if (!gen._pushInt(obj.size, MT.MAP)) { return false } return this._pushRawMap( obj.size, Array.from(obj) ) } _pushObject (obj) { if (!obj) { return this._pushUInt8(NULL) } const len = this.semanticTypes.length for (let i = 0; i < len; i++) { if (obj instanceof this.semanticTypes[i][0]) { return this.semanticTypes[i][1].call(obj, this, obj) } } const f = obj.encodeCBOR if (typeof f === 'function') { return f.call(obj, this) } const keys = Object.keys(obj) const keyLength = keys.length if (!this._pushInt(keyLength, MT.MAP)) { return false } return this._pushRawMap( keyLength, keys.map((k) => [k, obj[k]]) ) } _pushRawMap (len, map) { // Sort keys for canoncialization // 1. encode key // 2. shorter key comes before longer key // 3. same length keys are sorted with lower // byte value before higher map = map.map(function (a) { a[0] = Encoder.encode(a[0]) return a }).sort(utils.keySorter) for (let j = 0; j < len; j++) { if (!this.push(map[j][0])) { return false } if (!this.pushAny(map[j][1])) { return false } } return true } /** * Alias for `.pushAny` * * @param {*} obj * @returns {boolean} true on success */ write (obj) { return this.pushAny(obj) } /** * Push any supported type onto the encoded stream * * @param {any} obj * @returns {boolean} true on success */ pushAny (obj) { const typ = toType(obj) switch (typ) { case 'Number': return this._pushNumber(obj) case 'String': return this._pushString(obj) case 'Boolean': return this._pushBoolean(obj) case 'Object': return this._pushObject(obj) case 'Array': return this._pushArray(this, obj) case 'Uint8Array': return this._pushBuffer(this, Buffer.isBuffer(obj) ? obj : Buffer.from(obj)) case 'Null': return this._pushUInt8(NULL) case 'Undefined': return this._pushUndefined(obj) case 'Map': return this._pushMap(this, obj) case 'Set': return this._pushSet(this, obj) case 'URL': return this._pushUrl(this, obj) case 'BigNumber': return this._pushBigNumber(this, obj) case 'Date': return this._pushDate(this, obj) case 'RegExp': return this._pushRegexp(this, obj) case 'Symbol': switch (obj) { case SYMS.NULL: return this._pushObject(null) case SYMS.UNDEFINED: return this._pushUndefined(undefined) // TODO: Add pluggable support for other symbols default: throw new Error('Unknown symbol: ' + obj.toString()) } default: throw new Error('Unknown type: ' + typeof obj + ', ' + (obj ? obj.toString() : '')) } } finalize () { if (this.offset === 0) { return null } const result = this.result const resultLength = this.resultLength const resultMethod = this.resultMethod const offset = this.offset // Determine the size of the buffer let size = 0 let i = 0 for (; i < offset; i++) { size += resultLength[i] } const res = Buffer.allocUnsafe(size) let index = 0 let length = 0 // Write the content into the result buffer for (i = 0; i < offset; i++) { length = resultLength[i] switch (resultMethod[i]) { case 0: result[i].copy(res, index) break case 1: res.writeUInt8(result[i], index, true) break case 2: res.writeUInt16BE(result[i], index, true) break case 3: res.writeUInt32BE(result[i], index, true) break case 4: res.writeDoubleBE(result[i], index, true) break case 5: res.write(result[i], index, length, 'utf8') break default: throw new Error('unkown method') } index += length } const tmp = res this._reset() return tmp } _reset () { this.result = [] this.resultMethod = [] this.resultLength = [] this.offset = 0 } /** * Encode the given value * * @param {*} o * @returns {Buffer} */ static encode (o) { const enc = new Encoder() const ret = enc.pushAny(o) if (!ret) { throw new Error('Failed to encode input') } return enc.finalize() } } module.exports = Encoder