borc
Version:
Encode and parse data in the Concise Binary Object Representation (CBOR) data format (RFC7049).
523 lines (442 loc) • 12.1 kB
JavaScript
'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