'use strict';
const stream = require('stream');
const Decoder = require('./decoder');
const utils = require('./utils');
const NoFilter = require('nofilter');
const {MT, SYMS} = require('./constants');
/**
* Things that can act as inputs, from which a NoFilter can be created.
*
* @typedef {string|Buffer|ArrayBuffer|ArrayBufferView
* |DataView|stream.Readable} BufferLike
*/
/**
* @typedef DiagnoseOptions
* @property {string} [separator='\n'] Output between detected objects.
* @property {boolean} [stream_errors=false] Put error info into the
* output stream.
* @property {number} [max_depth=-1] The maximum depth to parse.
* Use -1 for "until you run out of memory". Set this to a finite
* positive number for un-trusted inputs. Most standard inputs won't nest
* more than 100 or so levels; I've tested into the millions before
* running out of memory.
* @property {object} [tags] Mapping from tag number to function(v),
* where v is the decoded value that comes after the tag, and where the
* function returns the correctly-created value for that tag.
* @property {boolean} [preferWeb=false] If true, prefer Uint8Arrays to
* be generated instead of node Buffers. This might turn on some more
* changes in the future, so forward-compatibility is not guaranteed yet.
* @property {BufferEncoding} [encoding='hex'] The encoding of input, ignored if
* input is not string.
*/
/**
* @callback diagnoseCallback
* @param {Error} [error] If one was generated.
* @param {string} [value] The diagnostic value.
* @returns {void}
*/
/**
* @param {DiagnoseOptions|diagnoseCallback|string} opts Options,
* the callback, or input incoding.
* @param {diagnoseCallback} [cb] Called on completion.
* @returns {{options: DiagnoseOptions, cb: diagnoseCallback}} Normalized.
* @throws {TypeError} Unknown option type.
* @private
*/
function normalizeOptions(opts, cb) {
switch (typeof opts) {
case 'function':
return {options: {}, cb: /** @type {diagnoseCallback} */ (opts)};
case 'string':
return {options: {encoding: /** @type {BufferEncoding} */ (opts)}, cb};
case 'object':
return {options: opts || {}, cb};
default:
throw new TypeError('Unknown option type');
}
}
/**
* Output the diagnostic format from a stream of CBOR bytes.
*
* @extends stream.Transform
*/
class Diagnose extends stream.Transform {
/**
* Creates an instance of Diagnose.
*
* @param {DiagnoseOptions} [options={}] Options for creation.
*/
constructor(options = {}) {
const {
separator = '\n',
stream_errors = false,
// Decoder options
tags,
max_depth,
preferWeb,
encoding,
// Stream.Transform options
...superOpts
} = options;
super({
...superOpts,
readableObjectMode: false,
writableObjectMode: false,
});
this.float_bytes = -1;
this.separator = separator;
this.stream_errors = stream_errors;
this.parser = new Decoder({
tags,
max_depth,
preferWeb,
encoding,
});
this.parser.on('more-bytes', this._on_more.bind(this));
this.parser.on('value', this._on_value.bind(this));
this.parser.on('start', this._on_start.bind(this));
this.parser.on('stop', this._on_stop.bind(this));
this.parser.on('data', this._on_data.bind(this));
this.parser.on('error', this._on_error.bind(this));
}
/**
* Transforming.
*
* @param {any} fresh Buffer to transcode.
* @param {BufferEncoding} encoding Name of encoding.
* @param {stream.TransformCallback} cb Callback when done.
* @ignore
*/
_transform(fresh, encoding, cb) {
this.parser.write(fresh, encoding, cb);
}
/**
* Flushing.
*
* @param {stream.TransformCallback} cb Callback when done.
* @ignore
*/
_flush(cb) {
this.parser._flush(er => {
if (this.stream_errors) {
if (er) {
this._on_error(er);
}
return cb();
}
return cb(er);
});
}
/**
* Convenience function to return a string in diagnostic format.
*
* @param {BufferLike} input The CBOR bytes to format.
* @param {DiagnoseOptions |diagnoseCallback|string} [options={}]
* Options, the callback, or the input encoding.
* @param {diagnoseCallback} [cb] Callback.
* @returns {Promise} If callback not specified.
* @throws {TypeError} Input not provided.
*/
static diagnose(input, options = {}, cb = null) {
if (input == null) {
throw new TypeError('input required');
}
({options, cb} = normalizeOptions(options, cb));
const {encoding = 'hex', ...opts} = options;
const bs = new NoFilter();
const d = new Diagnose(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;
}
/**
* @ignore
*/
_on_error(er) {
if (this.stream_errors) {
this.push(er.toString());
} else {
this.emit('error', er);
}
}
/** @private */
_on_more(mt, len, _parent_mt, _pos) {
if (mt === MT.SIMPLE_FLOAT) {
this.float_bytes = {
2: 1,
4: 2,
8: 3,
}[len];
}
}
/** @private */
_fore(parent_mt, pos) {
switch (parent_mt) {
case MT.BYTE_STRING:
case MT.UTF8_STRING:
case MT.ARRAY:
if (pos > 0) {
this.push(', ');
}
break;
case MT.MAP:
if (pos > 0) {
if (pos % 2) {
this.push(': ');
} else {
this.push(', ');
}
}
}
}
/** @private */
_on_value(val, parent_mt, pos) {
if (val === SYMS.BREAK) {
return;
}
this._fore(parent_mt, pos);
const fb = this.float_bytes;
this.float_bytes = -1;
this.push(utils.cborValueToString(val, fb));
}
/** @private */
_on_start(mt, tag, parent_mt, pos) {
this._fore(parent_mt, pos);
switch (mt) {
case MT.TAG:
this.push(`${tag}(`);
break;
case MT.ARRAY:
this.push('[');
break;
case MT.MAP:
this.push('{');
break;
case MT.BYTE_STRING:
case MT.UTF8_STRING:
this.push('(');
break;
}
if (tag === SYMS.STREAM) {
this.push('_ ');
}
}
/** @private */
_on_stop(mt) {
switch (mt) {
case MT.TAG:
this.push(')');
break;
case MT.ARRAY:
this.push(']');
break;
case MT.MAP:
this.push('}');
break;
case MT.BYTE_STRING:
case MT.UTF8_STRING:
this.push(')');
break;
}
}
/** @private */
_on_data() {
this.push(this.separator);
}
}
module.exports = Diagnose;