borc
Version:
Encode and parse data in the Concise Binary Object Representation (CBOR) data format (RFC7049).
624 lines (512 loc) • 14 kB
JavaScript
'use strict'
const { Buffer } = require('buffer')
const ieee754 = require('ieee754')
const Bignumber = require('bignumber.js').BigNumber
const parser = require('./decoder.asm')
const utils = require('./utils')
const c = require('./constants')
const Simple = require('./simple')
const Tagged = require('./tagged')
const { URL } = require('iso-url')
/**
* Transform binary cbor data into JavaScript objects.
*/
class Decoder {
/**
* @param {Object} [opts={}]
* @param {number} [opts.size=65536] - Size of the allocated heap.
*/
constructor (opts) {
opts = opts || {}
if (!opts.size || opts.size < 0x10000) {
opts.size = 0x10000
} else {
// Ensure the size is a power of 2
opts.size = utils.nextPowerOf2(opts.size)
}
// Heap use to share the input with the parser
this._heap = new ArrayBuffer(opts.size)
this._heap8 = new Uint8Array(this._heap)
this._buffer = Buffer.from(this._heap)
this._reset()
// Known tags
this._knownTags = Object.assign({
0: (val) => new Date(val),
1: (val) => new Date(val * 1000),
2: (val) => utils.arrayBufferToBignumber(val),
3: (val) => c.NEG_ONE.minus(utils.arrayBufferToBignumber(val)),
4: (v) => {
// const v = new Uint8Array(val)
return c.TEN.pow(v[0]).times(v[1])
},
5: (v) => {
// const v = new Uint8Array(val)
return c.TWO.pow(v[0]).times(v[1])
},
32: (val) => new URL(val),
35: (val) => new RegExp(val)
}, opts.tags)
// Initialize asm based parser
this.parser = parser(global, {
// eslint-disable-next-line no-console
log: console.log.bind(console),
pushInt: this.pushInt.bind(this),
pushInt32: this.pushInt32.bind(this),
pushInt32Neg: this.pushInt32Neg.bind(this),
pushInt64: this.pushInt64.bind(this),
pushInt64Neg: this.pushInt64Neg.bind(this),
pushFloat: this.pushFloat.bind(this),
pushFloatSingle: this.pushFloatSingle.bind(this),
pushFloatDouble: this.pushFloatDouble.bind(this),
pushTrue: this.pushTrue.bind(this),
pushFalse: this.pushFalse.bind(this),
pushUndefined: this.pushUndefined.bind(this),
pushNull: this.pushNull.bind(this),
pushInfinity: this.pushInfinity.bind(this),
pushInfinityNeg: this.pushInfinityNeg.bind(this),
pushNaN: this.pushNaN.bind(this),
pushNaNNeg: this.pushNaNNeg.bind(this),
pushArrayStart: this.pushArrayStart.bind(this),
pushArrayStartFixed: this.pushArrayStartFixed.bind(this),
pushArrayStartFixed32: this.pushArrayStartFixed32.bind(this),
pushArrayStartFixed64: this.pushArrayStartFixed64.bind(this),
pushObjectStart: this.pushObjectStart.bind(this),
pushObjectStartFixed: this.pushObjectStartFixed.bind(this),
pushObjectStartFixed32: this.pushObjectStartFixed32.bind(this),
pushObjectStartFixed64: this.pushObjectStartFixed64.bind(this),
pushByteString: this.pushByteString.bind(this),
pushByteStringStart: this.pushByteStringStart.bind(this),
pushUtf8String: this.pushUtf8String.bind(this),
pushUtf8StringStart: this.pushUtf8StringStart.bind(this),
pushSimpleUnassigned: this.pushSimpleUnassigned.bind(this),
pushTagUnassigned: this.pushTagUnassigned.bind(this),
pushTagStart: this.pushTagStart.bind(this),
pushTagStart4: this.pushTagStart4.bind(this),
pushTagStart8: this.pushTagStart8.bind(this),
pushBreak: this.pushBreak.bind(this)
}, this._heap)
}
get _depth () {
return this._parents.length
}
get _currentParent () {
return this._parents[this._depth - 1]
}
get _ref () {
return this._currentParent.ref
}
// Finish the current parent
_closeParent () {
const p = this._parents.pop()
if (p.length > 0) {
throw new Error(`Missing ${p.length} elements`)
}
switch (p.type) {
case c.PARENT.TAG:
this._push(
this.createTag(p.ref[0], p.ref[1])
)
break
case c.PARENT.BYTE_STRING:
this._push(this.createByteString(p.ref, p.length))
break
case c.PARENT.UTF8_STRING:
this._push(this.createUtf8String(p.ref, p.length))
break
case c.PARENT.MAP:
if (p.values % 2 > 0) {
throw new Error('Odd number of elements in the map')
}
this._push(this.createMap(p.ref, p.length))
break
case c.PARENT.OBJECT:
if (p.values % 2 > 0) {
throw new Error('Odd number of elements in the map')
}
this._push(this.createObject(p.ref, p.length))
break
case c.PARENT.ARRAY:
this._push(this.createArray(p.ref, p.length))
break
default:
break
}
if (this._currentParent && this._currentParent.type === c.PARENT.TAG) {
this._dec()
}
}
// Reduce the expected length of the current parent by one
_dec () {
const p = this._currentParent
// The current parent does not know the epxected child length
if (p.length < 0) {
return
}
p.length--
// All children were seen, we can close the current parent
if (p.length === 0) {
this._closeParent()
}
}
// Push any value to the current parent
_push (val, hasChildren) {
const p = this._currentParent
p.values++
switch (p.type) {
case c.PARENT.ARRAY:
case c.PARENT.BYTE_STRING:
case c.PARENT.UTF8_STRING:
if (p.length > -1) {
this._ref[this._ref.length - p.length] = val
} else {
this._ref.push(val)
}
this._dec()
break
case c.PARENT.OBJECT:
if (p.tmpKey != null) {
this._ref[p.tmpKey] = val
p.tmpKey = null
this._dec()
} else {
p.tmpKey = val
if (typeof p.tmpKey !== 'string') {
// too bad, convert to a Map
p.type = c.PARENT.MAP
p.ref = utils.buildMap(p.ref)
}
}
break
case c.PARENT.MAP:
if (p.tmpKey != null) {
this._ref.set(p.tmpKey, val)
p.tmpKey = null
this._dec()
} else {
p.tmpKey = val
}
break
case c.PARENT.TAG:
this._ref.push(val)
if (!hasChildren) {
this._dec()
}
break
default:
throw new Error('Unknown parent type')
}
}
// Create a new parent in the parents list
_createParent (obj, type, len) {
this._parents[this._depth] = {
type: type,
length: len,
ref: obj,
values: 0,
tmpKey: null
}
}
// Reset all state back to the beginning, also used for initiatlization
_reset () {
this._res = []
this._parents = [{
type: c.PARENT.ARRAY,
length: -1,
ref: this._res,
values: 0,
tmpKey: null
}]
}
// -- Interface to customize deoding behaviour
createTag (tagNumber, value) {
const typ = this._knownTags[tagNumber]
if (!typ) {
return new Tagged(tagNumber, value)
}
return typ(value)
}
createMap (obj, len) {
return obj
}
createObject (obj, len) {
return obj
}
createArray (arr, len) {
return arr
}
createByteString (raw, len) {
return Buffer.concat(raw)
}
createByteStringFromHeap (start, end) {
if (start === end) {
return Buffer.alloc(0)
}
return Buffer.from(this._heap.slice(start, end))
}
createInt (val) {
return val
}
createInt32 (f, g) {
return utils.buildInt32(f, g)
}
createInt64 (f1, f2, g1, g2) {
return utils.buildInt64(f1, f2, g1, g2)
}
createFloat (val) {
return val
}
createFloatSingle (a, b, c, d) {
return ieee754.read([a, b, c, d], 0, false, 23, 4)
}
createFloatDouble (a, b, c, d, e, f, g, h) {
return ieee754.read([a, b, c, d, e, f, g, h], 0, false, 52, 8)
}
createInt32Neg (f, g) {
return -1 - utils.buildInt32(f, g)
}
createInt64Neg (f1, f2, g1, g2) {
const f = utils.buildInt32(f1, f2)
const g = utils.buildInt32(g1, g2)
if (f > c.MAX_SAFE_HIGH) {
return c.NEG_ONE.minus(new Bignumber(f).times(c.SHIFT32).plus(g))
}
return -1 - ((f * c.SHIFT32) + g)
}
createTrue () {
return true
}
createFalse () {
return false
}
createNull () {
return null
}
createUndefined () {
return undefined
}
createInfinity () {
return Infinity
}
createInfinityNeg () {
return -Infinity
}
createNaN () {
return NaN
}
createNaNNeg () {
return -NaN
}
createUtf8String (raw, len) {
return raw.join('')
}
createUtf8StringFromHeap (start, end) {
if (start === end) {
return ''
}
return this._buffer.toString('utf8', start, end)
}
createSimpleUnassigned (val) {
return new Simple(val)
}
// -- Interface for decoder.asm.js
pushInt (val) {
this._push(this.createInt(val))
}
pushInt32 (f, g) {
this._push(this.createInt32(f, g))
}
pushInt64 (f1, f2, g1, g2) {
this._push(this.createInt64(f1, f2, g1, g2))
}
pushFloat (val) {
this._push(this.createFloat(val))
}
pushFloatSingle (a, b, c, d) {
this._push(this.createFloatSingle(a, b, c, d))
}
pushFloatDouble (a, b, c, d, e, f, g, h) {
this._push(this.createFloatDouble(a, b, c, d, e, f, g, h))
}
pushInt32Neg (f, g) {
this._push(this.createInt32Neg(f, g))
}
pushInt64Neg (f1, f2, g1, g2) {
this._push(this.createInt64Neg(f1, f2, g1, g2))
}
pushTrue () {
this._push(this.createTrue())
}
pushFalse () {
this._push(this.createFalse())
}
pushNull () {
this._push(this.createNull())
}
pushUndefined () {
this._push(this.createUndefined())
}
pushInfinity () {
this._push(this.createInfinity())
}
pushInfinityNeg () {
this._push(this.createInfinityNeg())
}
pushNaN () {
this._push(this.createNaN())
}
pushNaNNeg () {
this._push(this.createNaNNeg())
}
pushArrayStart () {
this._createParent([], c.PARENT.ARRAY, -1)
}
pushArrayStartFixed (len) {
this._createArrayStartFixed(len)
}
pushArrayStartFixed32 (len1, len2) {
const len = utils.buildInt32(len1, len2)
this._createArrayStartFixed(len)
}
pushArrayStartFixed64 (len1, len2, len3, len4) {
const len = utils.buildInt64(len1, len2, len3, len4)
this._createArrayStartFixed(len)
}
pushObjectStart () {
this._createObjectStartFixed(-1)
}
pushObjectStartFixed (len) {
this._createObjectStartFixed(len)
}
pushObjectStartFixed32 (len1, len2) {
const len = utils.buildInt32(len1, len2)
this._createObjectStartFixed(len)
}
pushObjectStartFixed64 (len1, len2, len3, len4) {
const len = utils.buildInt64(len1, len2, len3, len4)
this._createObjectStartFixed(len)
}
pushByteStringStart () {
this._parents[this._depth] = {
type: c.PARENT.BYTE_STRING,
length: -1,
ref: [],
values: 0,
tmpKey: null
}
}
pushByteString (start, end) {
this._push(this.createByteStringFromHeap(start, end))
}
pushUtf8StringStart () {
this._parents[this._depth] = {
type: c.PARENT.UTF8_STRING,
length: -1,
ref: [],
values: 0,
tmpKey: null
}
}
pushUtf8String (start, end) {
this._push(this.createUtf8StringFromHeap(start, end))
}
pushSimpleUnassigned (val) {
this._push(this.createSimpleUnassigned(val))
}
pushTagStart (tag) {
this._parents[this._depth] = {
type: c.PARENT.TAG,
length: 1,
ref: [tag]
}
}
pushTagStart4 (f, g) {
this.pushTagStart(utils.buildInt32(f, g))
}
pushTagStart8 (f1, f2, g1, g2) {
this.pushTagStart(utils.buildInt64(f1, f2, g1, g2))
}
pushTagUnassigned (tagNumber) {
this._push(this.createTag(tagNumber))
}
pushBreak () {
if (this._currentParent.length > -1) {
throw new Error('Unexpected break')
}
this._closeParent()
}
_createObjectStartFixed (len) {
if (len === 0) {
this._push(this.createObject({}))
return
}
this._createParent({}, c.PARENT.OBJECT, len)
}
_createArrayStartFixed (len) {
if (len === 0) {
this._push(this.createArray([]))
return
}
this._createParent(new Array(len), c.PARENT.ARRAY, len)
}
_decode (input) {
if (input.byteLength === 0) {
throw new Error('Input too short')
}
this._reset()
this._heap8.set(input)
const code = this.parser.parse(input.byteLength)
if (this._depth > 1) {
while (this._currentParent.length === 0) {
this._closeParent()
}
if (this._depth > 1) {
throw new Error('Undeterminated nesting')
}
}
if (code > 0) {
throw new Error('Failed to parse')
}
if (this._res.length === 0) {
throw new Error('No valid result')
}
}
// -- Public Interface
decodeFirst (input) {
this._decode(input)
return this._res[0]
}
decodeAll (input) {
this._decode(input)
return this._res
}
/**
* Decode the first cbor object.
*
* @param {Buffer|string} input
* @param {string} [enc='hex'] - Encoding used if a string is passed.
* @returns {*}
*/
static decode (input, enc) {
if (typeof input === 'string') {
input = Buffer.from(input, enc || 'hex')
}
const dec = new Decoder({ size: input.length })
return dec.decodeFirst(input)
}
/**
* Decode all cbor objects.
*
* @param {Buffer|string} input
* @param {string} [enc='hex'] - Encoding used if a string is passed.
* @returns {Array<*>}
*/
static decodeAll (input, enc) {
if (typeof input === 'string') {
input = Buffer.from(input, enc || 'hex')
}
const dec = new Decoder({ size: input.length })
return dec.decodeAll(input)
}
}
Decoder.decodeFirst = Decoder.decode
module.exports = Decoder