'use strict'
const stream = require('stream')
const NoFilter = require('nofilter')
const utils = require('./utils')
const constants = require('./constants')
const {
MT, NUMBYTES, SHIFT32, SIMPLE, SYMS, TAG, BI,
} = constants
const {Buffer} = require('buffer')
const HALF = (MT.SIMPLE_FLOAT << 5) | NUMBYTES.TWO
const FLOAT = (MT.SIMPLE_FLOAT << 5) | NUMBYTES.FOUR
const DOUBLE = (MT.SIMPLE_FLOAT << 5) | NUMBYTES.EIGHT
const TRUE = (MT.SIMPLE_FLOAT << 5) | SIMPLE.TRUE
const FALSE = (MT.SIMPLE_FLOAT << 5) | SIMPLE.FALSE
const UNDEFINED = (MT.SIMPLE_FLOAT << 5) | SIMPLE.UNDEFINED
const NULL = (MT.SIMPLE_FLOAT << 5) | SIMPLE.NULL
const BREAK = Buffer.from([0xff])
const BUF_NAN = Buffer.from('f97e00', 'hex')
const BUF_INF_NEG = Buffer.from('f9fc00', 'hex')
const BUF_INF_POS = Buffer.from('f97c00', 'hex')
const BUF_NEG_ZERO = Buffer.from('f98000', 'hex')
const SEMANTIC_TYPES = {}
let current_SEMANTIC_TYPES = {}
function parseDateType(str) {
if (!str) {
return 'number'
}
switch (str.toLowerCase()) {
case 'number':
return 'number'
case 'float':
return 'float'
case 'int':
case 'integer':
return 'int'
case 'string':
return 'string'
}
throw new TypeError(`dateType invalid, got "${str}"`)
}
class Encoder extends stream.Transform {
constructor(options = {}) {
const {
canonical = false,
encodeUndefined,
disallowUndefinedKeys = false,
dateType = 'number',
collapseBigIntegers = false,
detectLoops = false,
omitUndefinedProperties = false,
genTypes = [],
...superOpts
} = options
super({
...superOpts,
readableObjectMode: false,
writableObjectMode: true,
})
this.canonical = canonical
this.encodeUndefined = encodeUndefined
this.disallowUndefinedKeys = disallowUndefinedKeys
this.dateType = parseDateType(dateType)
this.collapseBigIntegers = this.canonical ? true : collapseBigIntegers
this.detectLoops = undefined
if (typeof detectLoops === 'boolean') {
if (detectLoops) {
this.detectLoops = new WeakSet()
}
} else if (detectLoops instanceof WeakSet) {
this.detectLoops = detectLoops
} else {
throw new TypeError('detectLoops must be boolean or WeakSet')
}
this.omitUndefinedProperties = omitUndefinedProperties
this.semanticTypes = {...Encoder.SEMANTIC_TYPES}
if (Array.isArray(genTypes)) {
for (let i = 0, len = genTypes.length; i < len; i += 2) {
this.addSemanticType(genTypes[i], genTypes[i + 1])
}
} else {
for (const [k, v] of Object.entries(genTypes)) {
this.addSemanticType(k, v)
}
}
}
_transform(fresh, encoding, cb) {
const ret = this.pushAny(fresh)
cb((ret === false) ? new Error('Push Error') : undefined)
}
_flush(cb) {
cb()
}
_pushUInt8(val) {
const b = Buffer.allocUnsafe(1)
b.writeUInt8(val, 0)
return this.push(b)
}
_pushUInt16BE(val) {
const b = Buffer.allocUnsafe(2)
b.writeUInt16BE(val, 0)
return this.push(b)
}
_pushUInt32BE(val) {
const b = Buffer.allocUnsafe(4)
b.writeUInt32BE(val, 0)
return this.push(b)
}
_pushFloatBE(val) {
const b = Buffer.allocUnsafe(4)
b.writeFloatBE(val, 0)
return this.push(b)
}
_pushDoubleBE(val) {
const b = Buffer.allocUnsafe(8)
b.writeDoubleBE(val, 0)
return this.push(b)
}
_pushNaN() {
return this.push(BUF_NAN)
}
_pushInfinity(obj) {
const half = (obj < 0) ? BUF_INF_NEG : BUF_INF_POS
return this.push(half)
}
_pushFloat(obj) {
if (this.canonical) {
const b2 = Buffer.allocUnsafe(2)
if (utils.writeHalf(b2, obj)) {
return this._pushUInt8(HALF) && this.push(b2)
}
}
if (Math.fround(obj) === obj) {
return this._pushUInt8(FLOAT) && this._pushFloatBE(obj)
}
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)
}
let max = Number.MAX_SAFE_INTEGER
if (mt === MT.NEG_INT) {
max--
}
if (obj <= max) {
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 (Object.is(obj, -0)) {
return this.push(BUF_NEG_ZERO)
}
if (obj < 0) {
return this._pushInt(-obj - 1, MT.NEG_INT, obj)
}
return this._pushInt(obj, MT.POS_INT)
}
_pushNumber(obj) {
if (isNaN(obj)) {
return this._pushNaN()
}
if (!isFinite(obj)) {
return this._pushInfinity(obj)
}
if (Math.round(obj) === obj) {
return this._pushIntNum(obj)
}
return this._pushFloat(obj)
}
_pushString(obj) {
const len = Buffer.byteLength(obj, 'utf8')
return this._pushInt(len, MT.UTF8_STRING) && this.push(obj, 'utf8')
}
_pushBoolean(obj) {
return this._pushUInt8(obj ? TRUE : FALSE)
}
_pushUndefined(obj) {
switch (typeof this.encodeUndefined) {
case 'undefined':
return this._pushUInt8(UNDEFINED)
case 'function':
return this.pushAny(this.encodeUndefined(obj))
case 'object': {
const buf = utils.bufferishToBuffer(this.encodeUndefined)
if (buf) {
return this.push(buf)
}
}
}
return this.pushAny(this.encodeUndefined)
}
_pushNull(obj) {
return this._pushUInt8(NULL)
}
_pushTag(tag) {
return this._pushInt(tag, MT.TAG)
}
_pushJSBigint(obj) {
let m = MT.POS_INT
let tag = TAG.POS_BIGINT
if (obj < 0) {
obj = -obj + BI.MINUS_ONE
m = MT.NEG_INT
tag = TAG.NEG_BIGINT
}
if (this.collapseBigIntegers &&
(obj <= BI.MAXINT64)) {
if (obj <= 0xffffffff) {
return this._pushInt(Number(obj), m)
}
return this._pushUInt8((m << 5) | NUMBYTES.EIGHT) &&
this._pushUInt32BE(Number(obj / BI.SHIFT32)) &&
this._pushUInt32BE(Number(obj % BI.SHIFT32))
}
let str = obj.toString(16)
if (str.length % 2) {
str = `0${str}`
}
const buf = Buffer.from(str, 'hex')
return this._pushTag(tag) && Encoder._pushBuffer(this, buf)
}
_pushObject(obj, opts) {
if (!obj) {
return this._pushNull(obj)
}
opts = {
indefinite: false,
skipTypes: false,
...opts,
}
if (!opts.indefinite) {
if (this.detectLoops) {
if (this.detectLoops.has(obj)) {
throw new Error(`\
Loop detected while CBOR encoding.
Call removeLoopDetectors before resuming.`)
} else {
this.detectLoops.add(obj)
}
}
}
if (!opts.skipTypes) {
const f = obj.encodeCBOR
if (typeof f === 'function') {
return f.call(obj, this)
}
const converter = this.semanticTypes[obj.constructor.name]
if (converter) {
return converter.call(obj, this, obj)
}
}
const keys = Object.keys(obj).filter(k => {
const tv = typeof obj[k]
return (tv !== 'function') &&
(!this.omitUndefinedProperties || (tv !== 'undefined'))
})
const cbor_keys = {}
if (this.canonical) {
keys.sort((a, b) => {
const a_cbor = cbor_keys[a] || (cbor_keys[a] = Encoder.encode(a))
const b_cbor = cbor_keys[b] || (cbor_keys[b] = Encoder.encode(b))
return a_cbor.compare(b_cbor)
})
}
if (opts.indefinite) {
if (!this._pushUInt8((MT.MAP << 5) | NUMBYTES.INDEFINITE)) {
return false
}
} else if (!this._pushInt(keys.length, MT.MAP)) {
return false
}
let ck = null
for (let j = 0, len2 = keys.length; j < len2; j++) {
const k = keys[j]
if (this.canonical && ((ck = cbor_keys[k]))) {
if (!this.push(ck)) { return false
}
} else if (!this._pushString(k)) {
return false
}
if (!this.pushAny(obj[k])) {
return false
}
}
if (opts.indefinite) {
if (!this.push(BREAK)) {
return false
}
} else if (this.detectLoops) {
this.detectLoops.delete(obj)
}
return true
}
_encodeAll(objs) {
const bs = new NoFilter({highWaterMark: this.readableHighWaterMark})
this.pipe(bs)
for (const o of objs) {
this.pushAny(o)
}
this.end()
return bs.read()
}
addSemanticType(type, fun) {
const typeName = (typeof type === 'string') ? type : type.name
const old = this.semanticTypes[typeName]
if (fun) {
if (typeof fun !== 'function') {
throw new TypeError('fun must be of type function')
}
this.semanticTypes[typeName] = fun
} else if (old) {
delete this.semanticTypes[typeName]
}
return old
}
pushAny(obj) {
switch (typeof obj) {
case 'number':
return this._pushNumber(obj)
case 'bigint':
return this._pushJSBigint(obj)
case 'string':
return this._pushString(obj)
case 'boolean':
return this._pushBoolean(obj)
case 'undefined':
return this._pushUndefined(obj)
case 'object':
return this._pushObject(obj)
case 'symbol':
switch (obj) {
case SYMS.NULL:
return this._pushNull(null)
case SYMS.UNDEFINED:
return this._pushUndefined(undefined)
default:
throw new TypeError(`Unknown symbol: ${obj.toString()}`)
}
default:
throw new TypeError(
`Unknown type: ${typeof obj}, ${(typeof obj.toString === 'function') ? obj.toString() : ''}`
)
}
}
static pushArray(gen, obj, opts) {
opts = {
indefinite: false,
...opts,
}
const len = obj.length
if (opts.indefinite) {
if (!gen._pushUInt8((MT.ARRAY << 5) | NUMBYTES.INDEFINITE)) {
return false
}
} else if (!gen._pushInt(len, MT.ARRAY)) {
return false
}
for (let j = 0; j < len; j++) {
if (!gen.pushAny(obj[j])) {
return false
}
}
if (opts.indefinite) {
if (!gen.push(BREAK)) {
return false
}
}
return true
}
removeLoopDetectors() {
if (!this.detectLoops) {
return false
}
this.detectLoops = new WeakSet()
return true
}
static _pushDate(gen, obj) {
switch (gen.dateType) {
case 'string':
return gen._pushTag(TAG.DATE_STRING) &&
gen._pushString(obj.toISOString())
case 'int':
return gen._pushTag(TAG.DATE_EPOCH) &&
gen._pushIntNum(Math.round(obj.getTime() / 1000))
case 'float':
return gen._pushTag(TAG.DATE_EPOCH) &&
gen._pushFloat(obj.getTime() / 1000)
case 'number':
default:
return gen._pushTag(TAG.DATE_EPOCH) &&
gen.pushAny(obj.getTime() / 1000)
}
}
static _pushBuffer(gen, obj) {
return gen._pushInt(obj.length, MT.BYTE_STRING) && gen.push(obj)
}
static _pushNoFilter(gen, obj) {
return Encoder._pushBuffer(gen, (obj.slice()))
}
static _pushRegexp(gen, obj) {
return gen._pushTag(TAG.REGEXP) && gen.pushAny(obj.source)
}
static _pushSet(gen, obj) {
if (!gen._pushTag(TAG.SET)) {
return false
}
if (!gen._pushInt(obj.size, MT.ARRAY)) {
return false
}
for (const x of obj) {
if (!gen.pushAny(x)) {
return false
}
}
return true
}
static _pushURL(gen, obj) {
return gen._pushTag(TAG.URI) && gen.pushAny(obj.toString())
}
static _pushBoxed(gen, obj) {
return gen.pushAny(obj.valueOf())
}
static _pushMap(gen, obj, opts) {
opts = {
indefinite: false,
...opts,
}
let entries = [...obj.entries()]
if (gen.omitUndefinedProperties) {
entries = entries.filter(([k, v]) => v !== undefined)
}
if (opts.indefinite) {
if (!gen._pushUInt8((MT.MAP << 5) | NUMBYTES.INDEFINITE)) {
return false
}
} else if (!gen._pushInt(entries.length, MT.MAP)) {
return false
}
if (gen.canonical) {
const enc = new Encoder({
genTypes: gen.semanticTypes,
canonical: gen.canonical,
detectLoops: Boolean(gen.detectLoops), dateType: gen.dateType,
disallowUndefinedKeys: gen.disallowUndefinedKeys,
collapseBigIntegers: gen.collapseBigIntegers,
})
const bs = new NoFilter({highWaterMark: gen.readableHighWaterMark})
enc.pipe(bs)
entries.sort(([a], [b]) => {
enc.pushAny(a)
const a_cbor = bs.read()
enc.pushAny(b)
const b_cbor = bs.read()
return a_cbor.compare(b_cbor)
})
for (const [k, v] of entries) {
if (gen.disallowUndefinedKeys && (typeof k === 'undefined')) {
throw new Error('Invalid Map key: undefined')
}
if (!(gen.pushAny(k) && gen.pushAny(v))) {
return false
}
}
} else {
for (const [k, v] of entries) {
if (gen.disallowUndefinedKeys && (typeof k === 'undefined')) {
throw new Error('Invalid Map key: undefined')
}
if (!(gen.pushAny(k) && gen.pushAny(v))) {
return false
}
}
}
if (opts.indefinite) {
if (!gen.push(BREAK)) {
return false
}
}
return true
}
static _pushTypedArray(gen, obj) {
let typ = 0b01000000
let sz = obj.BYTES_PER_ELEMENT
const {name} = obj.constructor
if (name.startsWith('Float')) {
typ |= 0b00010000
sz /= 2
} else if (!name.includes('U')) {
typ |= 0b00001000
}
if (name.includes('Clamped') || ((sz !== 1) && !utils.isBigEndian())) {
typ |= 0b00000100
}
typ |= {
1: 0b00,
2: 0b01,
4: 0b10,
8: 0b11,
}[sz]
if (!gen._pushTag(typ)) {
return false
}
return Encoder._pushBuffer(
gen,
Buffer.from(obj.buffer, obj.byteOffset, obj.byteLength)
)
}
static _pushArrayBuffer(gen, obj) {
return Encoder._pushBuffer(gen, Buffer.from(obj))
}
static encodeIndefinite(gen, obj, options = {}) {
if (obj == null) {
if (this == null) {
throw new Error('No object to encode')
}
obj = this
}
const {chunkSize = 4096} = options
let ret = true
const objType = typeof obj
let buf = null
if (objType === 'string') {
ret = ret && gen._pushUInt8((MT.UTF8_STRING << 5) | NUMBYTES.INDEFINITE)
let offset = 0
while (offset < obj.length) {
const endIndex = offset + chunkSize
ret = ret && gen._pushString(obj.slice(offset, endIndex))
offset = endIndex
}
ret = ret && gen.push(BREAK)
} else if ((buf = utils.bufferishToBuffer(obj))) {
ret = ret && gen._pushUInt8((MT.BYTE_STRING << 5) | NUMBYTES.INDEFINITE)
let offset = 0
while (offset < buf.length) {
const endIndex = offset + chunkSize
ret = ret && Encoder._pushBuffer(gen, buf.slice(offset, endIndex))
offset = endIndex
}
ret = ret && gen.push(BREAK)
} else if (Array.isArray(obj)) {
ret = ret && Encoder.pushArray(gen, obj, {
indefinite: true,
})
} else if (obj instanceof Map) {
ret = ret && Encoder._pushMap(gen, obj, {
indefinite: true,
})
} else {
if (objType !== 'object') {
throw new Error('Invalid indefinite encoding')
}
ret = ret && gen._pushObject(obj, {
indefinite: true,
skipTypes: true,
})
}
return ret
}
static encode(...objs) {
return new Encoder()._encodeAll(objs)
}
static encodeCanonical(...objs) {
return new Encoder({
canonical: true,
})._encodeAll(objs)
}
static encodeOne(obj, options) {
return new Encoder(options)._encodeAll([obj])
}
static encodeAsync(obj, options) {
return new Promise((resolve, reject) => {
const bufs = []
const enc = new Encoder(options)
enc.on('data', buf => bufs.push(buf))
enc.on('error', reject)
enc.on('finish', () => resolve(Buffer.concat(bufs)))
enc.pushAny(obj)
enc.end()
})
}
static get SEMANTIC_TYPES() {
return current_SEMANTIC_TYPES
}
static set SEMANTIC_TYPES(val) {
current_SEMANTIC_TYPES = val
}
static reset() {
Encoder.SEMANTIC_TYPES = {...SEMANTIC_TYPES}
}
}
Object.assign(SEMANTIC_TYPES, {
Array: Encoder.pushArray,
Date: Encoder._pushDate,
Buffer: Encoder._pushBuffer,
[Buffer.name]: Encoder._pushBuffer, Map: Encoder._pushMap,
NoFilter: Encoder._pushNoFilter,
[NoFilter.name]: Encoder._pushNoFilter, RegExp: Encoder._pushRegexp,
Set: Encoder._pushSet,
ArrayBuffer: Encoder._pushArrayBuffer,
Uint8ClampedArray: Encoder._pushTypedArray,
Uint8Array: Encoder._pushTypedArray,
Uint16Array: Encoder._pushTypedArray,
Uint32Array: Encoder._pushTypedArray,
Int8Array: Encoder._pushTypedArray,
Int16Array: Encoder._pushTypedArray,
Int32Array: Encoder._pushTypedArray,
Float32Array: Encoder._pushTypedArray,
Float64Array: Encoder._pushTypedArray,
URL: Encoder._pushURL,
Boolean: Encoder._pushBoxed,
Number: Encoder._pushBoxed,
String: Encoder._pushBoxed,
})
if (typeof BigUint64Array !== 'undefined') {
SEMANTIC_TYPES[BigUint64Array.name] = Encoder._pushTypedArray
}
if (typeof BigInt64Array !== 'undefined') {
SEMANTIC_TYPES[BigInt64Array.name] = Encoder._pushTypedArray
}
Encoder.reset()
module.exports = Encoder