Skip to content
Permalink
9bfb9ba527
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time
670 lines (623 sloc) 20.1 KB
'use strict'
const BinaryParseStream = require('../vendor/binary-parse-stream')
const Tagged = require('./tagged')
const Simple = require('./simple')
const utils = require('./utils')
const NoFilter = require('nofilter')
const stream = require('stream')
const constants = require('./constants')
const {MT, NUMBYTES, SYMS, BI} = constants
const {Buffer} = require('buffer')
const COUNT = Symbol('count')
const MAJOR = Symbol('major type')
const ERROR = Symbol('error')
const NOT_FOUND = Symbol('not found')
function parentArray(parent, typ, count) {
const a = []
a[COUNT] = count
a[SYMS.PARENT] = parent
a[MAJOR] = typ
return a
}
function parentBufferStream(parent, typ) {
const b = new NoFilter()
b[COUNT] = -1
b[SYMS.PARENT] = parent
b[MAJOR] = typ
return b
}
class UnexpectedDataError extends Error {
constructor(byte, value) {
super(`Unexpected data: 0x${byte.toString(16)}`)
this.name = 'UnexpectedDataError'
this.byte = byte
this.value = value
}
}
/**
* Things that can act as inputs, from which a NoFilter can be created.
*
* @typedef {string|Buffer|ArrayBuffer|Uint8Array|Uint8ClampedArray
* |DataView|stream.Readable} BufferLike
*/
/**
* @typedef ExtendedResults
* @property {any} value The value that was found.
* @property {number} length The number of bytes of the original input that
* were read.
* @property {Buffer} bytes The bytes of the original input that were used
* to produce the value.
* @property {Buffer} [unused] The bytes that were left over from the original
* input. This property only exists if {@linkcode Decoder.decodeFirst} or
* {@linkcode Decoder.decodeFirstSync} was called.
*/
/**
* @typedef DecoderOptions
* @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 {Tagged.TagMap} [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 the input.
* Ignored if input is a Buffer.
* @property {boolean} [required=false] Should an error be thrown when no
* data is in the input?
* @property {boolean} [extendedResults=false] If true, emit extended
* results, which will be an object with shape {@link ExtendedResults}.
* The value will already have been null-checked.
* @property {boolean} [preventDuplicateKeys=false] If true, error is
* thrown if a map has duplicate keys.
*/
/**
* @callback decodeCallback
* @param {Error} [error] If one was generated.
* @param {any} [value] The decoded value.
* @returns {void}
*/
/**
* @param {DecoderOptions|decodeCallback|string} opts Options,
* the callback, or input incoding.
* @param {decodeCallback} [cb] Called on completion.
* @returns {{options: DecoderOptions, cb: decodeCallback}} Normalized.
* @throws {TypeError} On unknown option type.
* @private
*/
function normalizeOptions(opts, cb) {
switch (typeof opts) {
case 'function':
return {options: {}, cb: /** @type {decodeCallback} */ (opts)}
case 'string':
return {options: {encoding: /** @type {BufferEncoding} */ (opts)}, cb}
case 'object':
return {options: opts || {}, cb}
default:
throw new TypeError('Unknown option type')
}
}
/**
* Decode a stream of CBOR bytes by transforming them into equivalent
* JavaScript data. Because of the limitations of Node object streams,
* special symbols are emitted instead of NULL or UNDEFINED. Fix those
* up by calling {@link Decoder.nullcheck}.
*
* @extends BinaryParseStream
*/
class Decoder extends BinaryParseStream {
/**
* Create a parsing stream.
*
* @param {DecoderOptions} [options={}] Options.
*/
constructor(options = {}) {
const {
tags = {},
max_depth = -1,
preferWeb = false,
required = false,
encoding = 'hex',
extendedResults = false,
preventDuplicateKeys = false,
...superOpts
} = options
super({defaultEncoding: encoding, ...superOpts})
this.running = true
this.max_depth = max_depth
this.tags = tags
this.preferWeb = preferWeb
this.extendedResults = extendedResults
this.required = required
this.preventDuplicateKeys = preventDuplicateKeys
if (extendedResults) {
this.bs.on('read', this._onRead.bind(this))
this.valueBytes = /** @type {NoFilter} */ (new NoFilter())
}
}
/**
* Check the given value for a symbol encoding a NULL or UNDEFINED value in
* the CBOR stream.
*
* @static
* @param {any} val The value to check.
* @returns {any} The corrected value.
* @throws {Error} Nothing was found.
* @example
* myDecoder.on('data', val => {
* val = Decoder.nullcheck(val)
* // ...
* })
*/
static nullcheck(val) {
switch (val) {
case SYMS.NULL:
return null
case SYMS.UNDEFINED:
return undefined
// Leaving this in for now as belt-and-suspenders, but I'm pretty sure
// it can't happen.
/* istanbul ignore next */
case NOT_FOUND:
/* istanbul ignore next */
throw new Error('Value not found')
default:
return val
}
}
/**
* Decode the first CBOR item in the input, synchronously. This will throw
* an exception if the input is not valid CBOR, or if there are more bytes
* left over at the end (if options.extendedResults is not true).
*
* @static
* @param {BufferLike} input If a Readable stream, must have
* received the `readable` event already, or you will get an error
* claiming "Insufficient data".
* @param {DecoderOptions|string} [options={}] Options or encoding for input.
* @returns {ExtendedResults|any} The decoded value.
* @throws {UnexpectedDataError} Data is left over after decoding.
* @throws {Error} Insufficient data.
*/
static decodeFirstSync(input, options = {}) {
if (input == null) {
throw new TypeError('input required')
}
({options} = normalizeOptions(options))
const {encoding = 'hex', ...opts} = options
const c = new Decoder(opts)
const s = utils.guessEncoding(input, encoding)
// For/of doesn't work when you need to call next() with a value
// generator created by parser will be "done" after each CBOR entity
// parser will yield numbers of bytes that it wants
const parser = c._parse()
let state = parser.next()
while (!state.done) {
const b = s.read(state.value)
if ((b == null) || (b.length !== state.value)) {
throw new Error('Insufficient data')
}
if (c.extendedResults) {
c.valueBytes.write(b)
}
state = parser.next(b)
}
let val = null
if (c.extendedResults) {
val = state.value
val.unused = s.read()
} else {
val = Decoder.nullcheck(state.value)
if (s.length > 0) {
const nextByte = s.read(1)
s.unshift(nextByte)
throw new UnexpectedDataError(nextByte[0], val)
}
}
return val
}
/**
* Decode all of the CBOR items in the input into an array. This will throw
* an exception if the input is not valid CBOR; a zero-length input will
* return an empty array.
*
* @static
* @param {BufferLike} input What to parse?
* @param {DecoderOptions|string} [options={}] Options or encoding
* for input.
* @returns {Array<ExtendedResults>|Array<any>} Array of all found items.
* @throws {TypeError} No input provided.
* @throws {Error} Insufficient data provided.
*/
static decodeAllSync(input, options = {}) {
if (input == null) {
throw new TypeError('input required')
}
({options} = normalizeOptions(options))
const {encoding = 'hex', ...opts} = options
const c = new Decoder(opts)
const s = utils.guessEncoding(input, encoding)
const res = []
while (s.length > 0) {
const parser = c._parse()
let state = parser.next()
while (!state.done) {
const b = s.read(state.value)
if ((b == null) || (b.length !== state.value)) {
throw new Error('Insufficient data')
}
if (c.extendedResults) {
c.valueBytes.write(b)
}
state = parser.next(b)
}
res.push(Decoder.nullcheck(state.value))
}
return res
}
/**
* Decode the first CBOR item in the input. This will error if there are
* more bytes left over at the end (if options.extendedResults is not true),
* and optionally if there were no valid CBOR bytes in the input. Emits the
* {Decoder.NOT_FOUND} Symbol in the callback if no data was found and the
* `required` option is false.
*
* @static
* @param {BufferLike} input What to parse?
* @param {DecoderOptions|decodeCallback|string} [options={}] Options, the
* callback, or input encoding.
* @param {decodeCallback} [cb] Callback.
* @returns {Promise<ExtendedResults|any>} Returned even if callback is
* specified.
* @throws {TypeError} No input provided.
*/
static decodeFirst(input, options = {}, cb = null) {
if (input == null) {
throw new TypeError('input required')
}
({options, cb} = normalizeOptions(options, cb))
const {encoding = 'hex', required = false, ...opts} = options
const c = new Decoder(opts)
let v = /** @type {any} */ (NOT_FOUND)
const s = utils.guessEncoding(input, encoding)
const p = new Promise((resolve, reject) => {
c.on('data', val => {
v = Decoder.nullcheck(val)
c.close()
})
c.once('error', er => {
if (c.extendedResults && (er instanceof UnexpectedDataError)) {
v.unused = c.bs.slice()
return resolve(v)
}
if (v !== NOT_FOUND) {
// Typescript work-around
// eslint-disable-next-line dot-notation
er['value'] = v
}
v = ERROR
c.close()
return reject(er)
})
c.once('end', () => {
switch (v) {
case NOT_FOUND:
if (required) {
return reject(new Error('No CBOR found'))
}
return resolve(v)
// Pretty sure this can't happen, but not *certain*.
/* istanbul ignore next */
case ERROR:
/* istanbul ignore next */
return undefined
default:
return resolve(v)
}
})
})
if (typeof cb === 'function') {
p.then(val => cb(null, val), cb)
}
s.pipe(c)
return p
}
/**
* @callback decodeAllCallback
* @param {Error} error If one was generated.
* @param {Array<ExtendedResults>|Array<any>} value All of the decoded
* values, wrapped in an Array.
*/
/**
* Decode all of the CBOR items in the input. This will error if there are
* more bytes left over at the end.
*
* @static
* @param {BufferLike} input What to parse?
* @param {DecoderOptions|decodeAllCallback|string} [options={}]
* Decoding options, the callback, or the input encoding.
* @param {decodeAllCallback} [cb] Callback.
* @returns {Promise<Array<ExtendedResults>|Array<any>>} Even if callback
* is specified.
* @throws {TypeError} No input specified.
*/
static decodeAll(input, options = {}, cb = null) {
if (input == null) {
throw new TypeError('input required')
}
({options, cb} = normalizeOptions(options, cb))
const {encoding = 'hex', ...opts} = options
const c = new Decoder(opts)
const vals = []
c.on('data', val => vals.push(Decoder.nullcheck(val)))
const p = new Promise((resolve, reject) => {
c.on('error', reject)
c.on('end', () => resolve(vals))
})
if (typeof cb === 'function') {
p.then(v => cb(undefined, v), er => cb(er, undefined))
}
utils.guessEncoding(input, encoding).pipe(c)
return p
}
/**
* Stop processing.
*/
close() {
this.running = false
this.__fresh = true
}
/**
* Only called if extendedResults is true.
*
* @ignore
*/
_onRead(data) {
this.valueBytes.write(data)
}
/**
* @yields {number} Number of bytes to read.
* @returns {Generator<number, any, Buffer>} Yields a number of bytes,
* returns anything, next returns a Buffer.
* @throws {Error} Maximum depth exceeded.
* @ignore
*/
*_parse() {
let parent = null
let depth = 0
let val = null
while (true) {
if ((this.max_depth >= 0) && (depth > this.max_depth)) {
throw new Error(`Maximum depth ${this.max_depth} exceeded`)
}
const [octet] = yield 1
if (!this.running) {
this.bs.unshift(Buffer.from([octet]))
throw new UnexpectedDataError(octet)
}
const mt = octet >> 5
const ai = octet & 0x1f
const parent_major = (parent == null) ? undefined : parent[MAJOR]
const parent_length = (parent == null) ? undefined : parent.length
switch (ai) {
case NUMBYTES.ONE:
this.emit('more-bytes', mt, 1, parent_major, parent_length)
;[val] = yield 1
break
case NUMBYTES.TWO:
case NUMBYTES.FOUR:
case NUMBYTES.EIGHT: {
const numbytes = 1 << (ai - 24)
this.emit('more-bytes', mt, numbytes, parent_major, parent_length)
const buf = yield numbytes
val = (mt === MT.SIMPLE_FLOAT) ?
buf :
utils.parseCBORint(ai, buf)
break
}
case 28:
case 29:
case 30:
this.running = false
throw new Error(`Additional info not implemented: ${ai}`)
case NUMBYTES.INDEFINITE:
switch (mt) {
case MT.POS_INT:
case MT.NEG_INT:
case MT.TAG:
throw new Error(`Invalid indefinite encoding for MT ${mt}`)
}
val = -1
break
default:
val = ai
}
switch (mt) {
case MT.POS_INT:
// Val already decoded
break
case MT.NEG_INT:
if (val === Number.MAX_SAFE_INTEGER) {
val = BI.NEG_MAX
} else {
val = (typeof val === 'bigint') ? BI.MINUS_ONE - val : -1 - val
}
break
case MT.BYTE_STRING:
case MT.UTF8_STRING:
switch (val) {
case 0:
this.emit('start-string', mt, val, parent_major, parent_length)
if (mt === MT.UTF8_STRING) {
val = ''
} else {
val = this.preferWeb ? new Uint8Array(0) : Buffer.allocUnsafe(0)
}
break
case -1:
this.emit('start', mt, SYMS.STREAM, parent_major, parent_length)
parent = parentBufferStream(parent, mt)
depth++
continue
default:
this.emit('start-string', mt, val, parent_major, parent_length)
val = yield val
if (mt === MT.UTF8_STRING) {
val = utils.utf8(val)
} else if (this.preferWeb) {
val = new Uint8Array(val.buffer, val.byteOffset, val.length)
}
}
break
case MT.ARRAY:
case MT.MAP:
switch (val) {
case 0:
val = (mt === MT.MAP) ? {} : []
break
case -1:
this.emit('start', mt, SYMS.STREAM, parent_major, parent_length)
parent = parentArray(parent, mt, -1)
depth++
continue
default:
this.emit('start', mt, val, parent_major, parent_length)
parent = parentArray(parent, mt, val * (mt - 3))
depth++
continue
}
break
case MT.TAG:
this.emit('start', mt, val, parent_major, parent_length)
parent = parentArray(parent, mt, 1)
parent.push(val)
depth++
continue
case MT.SIMPLE_FLOAT:
if (typeof val === 'number') {
if ((ai === NUMBYTES.ONE) && (val < 32)) {
throw new Error(
`Invalid two-byte encoding of simple value ${val}`
)
}
const hasParent = (parent != null)
val = Simple.decode(
val,
hasParent,
hasParent && (parent[COUNT] < 0)
)
} else {
val = utils.parseCBORfloat(val)
}
}
this.emit('value', val, parent_major, parent_length, ai)
let again = false
while (parent != null) {
if (val === SYMS.BREAK) {
parent[COUNT] = 1
} else if (Array.isArray(parent)) {
parent.push(val)
} else {
// Assert: parent instanceof NoFilter
const pm = parent[MAJOR]
if ((pm != null) && (pm !== mt)) {
this.running = false
throw new Error('Invalid major type in indefinite encoding')
}
parent.write(val)
}
if ((--parent[COUNT]) !== 0) {
again = true
break
}
--depth
delete parent[COUNT]
if (Array.isArray(parent)) {
switch (parent[MAJOR]) {
case MT.ARRAY:
val = parent
break
case MT.MAP: {
let allstrings = true
if ((parent.length % 2) !== 0) {
throw new Error(`Invalid map length: ${parent.length}`)
}
for (let i = 0, len = parent.length; i < len; i += 2) {
if ((typeof parent[i] !== 'string') ||
(parent[i] === '__proto__')) {
allstrings = false
break
}
}
if (allstrings) {
val = {}
for (let i = 0, len = parent.length; i < len; i += 2) {
if (this.preventDuplicateKeys &&
Object.prototype.hasOwnProperty.call(val, parent[i])) {
throw new Error('Duplicate keys in a map')
}
val[parent[i]] = parent[i + 1]
}
} else {
val = new Map()
for (let i = 0, len = parent.length; i < len; i += 2) {
if (this.preventDuplicateKeys && val.has(parent[i])) {
throw new Error('Duplicate keys in a map')
}
val.set(parent[i], parent[i + 1])
}
}
break
}
case MT.TAG: {
const t = new Tagged(parent[0], parent[1])
val = t.convert(this.tags)
break
}
}
} else /* istanbul ignore else */ if (parent instanceof NoFilter) {
// Only parent types are Array and NoFilter for (Array/Map) and
// (bytes/string) respectively.
switch (parent[MAJOR]) {
case MT.BYTE_STRING:
val = parent.slice()
if (this.preferWeb) {
val = new Uint8Array(
/** @type {Buffer} */ (val).buffer,
/** @type {Buffer} */ (val).byteOffset,
/** @type {Buffer} */ (val).length
)
}
break
case MT.UTF8_STRING:
val = parent.toString('utf-8')
break
}
}
this.emit('stop', parent[MAJOR])
const old = parent
parent = parent[SYMS.PARENT]
delete old[SYMS.PARENT]
delete old[MAJOR]
}
if (!again) {
if (this.extendedResults) {
const bytes = this.valueBytes.slice()
const ret = {
value: Decoder.nullcheck(val),
bytes,
length: bytes.length,
}
this.valueBytes = new NoFilter()
return ret
}
return val
}
}
}
}
Decoder.NOT_FOUND = NOT_FOUND
module.exports = Decoder