'use strict'
const stream = require('stream')
const utils = require('./utils')
const Decoder = require('./decoder')
const NoFilter = require('nofilter')
const {MT, NUMBYTES, SYMS} = require('./constants')
const {Buffer} = require('buffer')
function plural(c) {
if (c > 1) {
return 's'
}
return ''
}
function normalizeOptions(opts, cb) {
switch (typeof opts) {
case 'function':
return {options: {}, cb: (opts)}
case 'string':
return {options: {encoding: (opts)}, cb}
case 'number':
return {options: {max_depth: opts}, cb}
case 'object':
return {options: opts || {}, cb}
default:
throw new TypeError('Unknown option type')
}
}
class Commented extends stream.Transform {
constructor(options = {}) {
const {
depth = 1,
max_depth = 10,
no_summary = false,
tags = {},
preferWeb,
encoding,
...superOpts
} = options
super({
...superOpts,
readableObjectMode: false,
writableObjectMode: false,
})
this.depth = depth
this.max_depth = max_depth
this.all = new NoFilter()
if (!tags[24]) {
tags[24] = this._tag_24.bind(this)
}
this.parser = new Decoder({
tags,
max_depth,
preferWeb,
encoding,
})
this.parser.on('value', this._on_value.bind(this))
this.parser.on('start', this._on_start.bind(this))
this.parser.on('start-string', this._on_start_string.bind(this))
this.parser.on('stop', this._on_stop.bind(this))
this.parser.on('more-bytes', this._on_more.bind(this))
this.parser.on('error', this._on_error.bind(this))
if (!no_summary) {
this.parser.on('data', this._on_data.bind(this))
}
this.parser.bs.on('read', this._on_read.bind(this))
}
_tag_24(v) {
const c = new Commented({depth: this.depth + 1, no_summary: true})
c.on('data', b => this.push(b))
c.on('error', er => this.emit('error', er))
c.end(v)
}
_transform(fresh, encoding, cb) {
this.parser.write(fresh, encoding, cb)
}
_flush(cb) {
return this.parser._flush(cb)
}
static comment(input, options = {}, cb = null) {
if (input == null) {
throw new Error('input required')
}
({options, cb} = normalizeOptions(options, cb))
const bs = new NoFilter()
const {encoding = 'hex', ...opts} = options
const d = new Commented(opts)
let p = null
if (typeof cb === 'function') {
d.on('end', () => {
cb(null, bs.toString('utf8'))
})
d.on('error', cb)
} else {
p = new Promise((resolve, reject) => {
d.on('end', () => {
resolve(bs.toString('utf8'))
})
d.on('error', reject)
})
}
d.pipe(bs)
utils.guessEncoding(input, encoding).pipe(d)
return p
}
_on_error(er) {
this.push('ERROR: ')
this.push(er.toString())
this.push('\n')
}
_on_read(buf) {
this.all.write(buf)
const hex = buf.toString('hex')
this.push(new Array(this.depth + 1).join(' '))
this.push(hex)
let ind = ((this.max_depth - this.depth) * 2) - hex.length
if (ind < 1) {
ind = 1
}
this.push(new Array(ind + 1).join(' '))
this.push('-- ')
}
_on_more(mt, len, parent_mt, pos) {
let desc = ''
this.depth++
switch (mt) {
case MT.POS_INT:
desc = 'Positive number,'
break
case MT.NEG_INT:
desc = 'Negative number,'
break
case MT.ARRAY:
desc = 'Array, length'
break
case MT.MAP:
desc = 'Map, count'
break
case MT.BYTE_STRING:
desc = 'Bytes, length'
break
case MT.UTF8_STRING:
desc = 'String, length'
break
case MT.SIMPLE_FLOAT:
if (len === 1) {
desc = 'Simple value,'
} else {
desc = 'Float,'
}
break
}
this.push(`${desc} next ${len} byte${plural(len)}\n`)
}
_on_start_string(mt, len, parent_mt, pos) {
let desc = ''
this.depth++
switch (mt) {
case MT.BYTE_STRING:
desc = `Bytes, length: ${len}`
break
case MT.UTF8_STRING:
desc = `String, length: ${len.toString()}`
break
}
this.push(`${desc}\n`)
}
_on_start(mt, tag, parent_mt, pos) {
this.depth++
switch (parent_mt) {
case MT.ARRAY:
this.push(`[${pos}], `)
break
case MT.MAP:
if (pos % 2) {
this.push(`{Val:${Math.floor(pos / 2)}}, `)
} else {
this.push(`{Key:${Math.floor(pos / 2)}}, `)
}
break
}
switch (mt) {
case MT.TAG:
this.push(`Tag #${tag}`)
if (tag === 24) {
this.push(' Encoded CBOR data item')
}
break
case MT.ARRAY:
if (tag === SYMS.STREAM) {
this.push('Array (streaming)')
} else {
this.push(`Array, ${tag} item${plural(tag)}`)
}
break
case MT.MAP:
if (tag === SYMS.STREAM) {
this.push('Map (streaming)')
} else {
this.push(`Map, ${tag} pair${plural(tag)}`)
}
break
case MT.BYTE_STRING:
this.push('Bytes (streaming)')
break
case MT.UTF8_STRING:
this.push('String (streaming)')
break
}
this.push('\n')
}
_on_stop(mt) {
this.depth--
}
_on_value(val, parent_mt, pos, ai) {
if (val !== SYMS.BREAK) {
switch (parent_mt) {
case MT.ARRAY:
this.push(`[${pos}], `)
break
case MT.MAP:
if (pos % 2) {
this.push(`{Val:${Math.floor(pos / 2)}}, `)
} else {
this.push(`{Key:${Math.floor(pos / 2)}}, `)
}
break
}
}
const str = utils.cborValueToString(val, -Infinity)
if ((typeof val === 'string') ||
(Buffer.isBuffer(val))) {
if (val.length > 0) {
this.push(str)
this.push('\n')
}
this.depth--
} else {
this.push(str)
this.push('\n')
}
switch (ai) {
case NUMBYTES.ONE:
case NUMBYTES.TWO:
case NUMBYTES.FOUR:
case NUMBYTES.EIGHT:
this.depth--
}
}
_on_data() {
this.push('0x')
this.push(this.all.read().toString('hex'))
this.push('\n')
}
}
module.exports = Commented