mirror of
https://github.com/github/codeql-action.git
synced 2025-12-27 01:30:10 +08:00
1102 lines
31 KiB
JavaScript
1102 lines
31 KiB
JavaScript
'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')
|
|
|
|
/**
|
|
* Generate the CBOR for a value. If you are using this, you'll either need
|
|
* to call {@link Encoder.write} with a Buffer, or look into the internals of
|
|
* Encoder to reuse existing non-documented behavior.
|
|
*
|
|
* @callback EncodeFunction
|
|
* @param {Encoder} enc The encoder to use.
|
|
* @param {any} val The value to encode.
|
|
* @returns {boolean} True on success.
|
|
*/
|
|
|
|
/* eslint-disable jsdoc/check-types */
|
|
/**
|
|
* A mapping from tag number to a tag decoding function.
|
|
*
|
|
* @typedef {Object.<string, EncodeFunction>} SemanticMap
|
|
*/
|
|
/* eslint-enable jsdoc/check-types */
|
|
|
|
/**
|
|
* @type {SemanticMap}
|
|
* @private
|
|
*/
|
|
const SEMANTIC_TYPES = {}
|
|
|
|
/**
|
|
* @type {SemanticMap}
|
|
* @private
|
|
*/
|
|
let current_SEMANTIC_TYPES = {}
|
|
|
|
/**
|
|
* @param {string} str String to normalize.
|
|
* @returns {"number"|"float"|"int"|"string"} Normalized.
|
|
* @throws {TypeError} Invalid input.
|
|
* @private
|
|
*/
|
|
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}"`)
|
|
}
|
|
|
|
/**
|
|
* @typedef EncodingOptions
|
|
* @property {any[]|object} [genTypes=[]] Array of pairs of
|
|
* `type`, `function(Encoder)` for semantic types to be encoded. Not
|
|
* needed for Array, Date, Buffer, Map, RegExp, Set, or URL.
|
|
* If an object, the keys are the constructor names for the types.
|
|
* @property {boolean} [canonical=false] Should the output be
|
|
* canonicalized.
|
|
* @property {boolean|WeakSet} [detectLoops=false] Should object loops
|
|
* be detected? This will currently add memory to track every part of the
|
|
* object being encoded in a WeakSet. Do not encode
|
|
* the same object twice on the same encoder, without calling
|
|
* `removeLoopDetectors` in between, which will clear the WeakSet.
|
|
* You may pass in your own WeakSet to be used; this is useful in some
|
|
* recursive scenarios.
|
|
* @property {("number"|"float"|"int"|"string")} [dateType="number"] -
|
|
* how should dates be encoded? "number" means float or int, if no
|
|
* fractional seconds.
|
|
* @property {any} [encodeUndefined=undefined] How should an
|
|
* "undefined" in the input be encoded. By default, just encode a CBOR
|
|
* undefined. If this is a buffer, use those bytes without re-encoding
|
|
* them. If this is a function, the function will be called (which is a
|
|
* good time to throw an exception, if that's what you want), and the
|
|
* return value will be used according to these rules. Anything else will
|
|
* be encoded as CBOR.
|
|
* @property {boolean} [disallowUndefinedKeys=false] Should
|
|
* "undefined" be disallowed as a key in a Map that is serialized? If
|
|
* this is true, encode(new Map([[undefined, 1]])) will throw an
|
|
* exception. Note that it is impossible to get a key of undefined in a
|
|
* normal JS object.
|
|
* @property {boolean} [collapseBigIntegers=false] Should integers
|
|
* that come in as ECMAscript bigint's be encoded
|
|
* as normal CBOR integers if they fit, discarding type information?
|
|
* @property {number} [chunkSize=4096] Number of characters or bytes
|
|
* for each chunk, if obj is a string or Buffer, when indefinite encoding.
|
|
* @property {boolean} [omitUndefinedProperties=false] When encoding
|
|
* objects or Maps, do not include a key if its corresponding value is
|
|
* `undefined`.
|
|
*/
|
|
|
|
/**
|
|
* Transform JavaScript values into CBOR bytes. The `Writable` side of
|
|
* the stream is in object mode.
|
|
*
|
|
* @extends stream.Transform
|
|
*/
|
|
class Encoder extends stream.Transform {
|
|
/**
|
|
* Creates an instance of Encoder.
|
|
*
|
|
* @param {EncodingOptions} [options={}] Options for the encoder.
|
|
*/
|
|
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
|
|
|
|
/** @type {WeakSet?} */
|
|
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)
|
|
// Old transformers might not return bool. undefined !== false
|
|
return cb((ret === false) ? new Error('Push Error') : undefined)
|
|
}
|
|
|
|
// eslint-disable-next-line class-methods-use-this
|
|
_flush(cb) {
|
|
return cb()
|
|
}
|
|
|
|
/**
|
|
* @param {number} val Number(0-255) to encode.
|
|
* @returns {boolean} True on success.
|
|
* @ignore
|
|
*/
|
|
_pushUInt8(val) {
|
|
const b = Buffer.allocUnsafe(1)
|
|
b.writeUInt8(val, 0)
|
|
return this.push(b)
|
|
}
|
|
|
|
/**
|
|
* @param {number} val Number(0-65535) to encode.
|
|
* @returns {boolean} True on success.
|
|
* @ignore
|
|
*/
|
|
_pushUInt16BE(val) {
|
|
const b = Buffer.allocUnsafe(2)
|
|
b.writeUInt16BE(val, 0)
|
|
return this.push(b)
|
|
}
|
|
|
|
/**
|
|
* @param {number} val Number(0..2**32-1) to encode.
|
|
* @returns {boolean} True on success.
|
|
* @ignore
|
|
*/
|
|
_pushUInt32BE(val) {
|
|
const b = Buffer.allocUnsafe(4)
|
|
b.writeUInt32BE(val, 0)
|
|
return this.push(b)
|
|
}
|
|
|
|
/**
|
|
* @param {number} val Number to encode as 4-byte float.
|
|
* @returns {boolean} True on success.
|
|
* @ignore
|
|
*/
|
|
_pushFloatBE(val) {
|
|
const b = Buffer.allocUnsafe(4)
|
|
b.writeFloatBE(val, 0)
|
|
return this.push(b)
|
|
}
|
|
|
|
/**
|
|
* @param {number} val Number to encode as 8-byte double.
|
|
* @returns {boolean} True on success.
|
|
* @ignore
|
|
*/
|
|
_pushDoubleBE(val) {
|
|
const b = Buffer.allocUnsafe(8)
|
|
b.writeDoubleBE(val, 0)
|
|
return this.push(b)
|
|
}
|
|
|
|
/**
|
|
* @returns {boolean} True on success.
|
|
* @ignore
|
|
*/
|
|
_pushNaN() {
|
|
return this.push(BUF_NAN)
|
|
}
|
|
|
|
/**
|
|
* @param {number} obj Positive or negative infinity.
|
|
* @returns {boolean} True on success.
|
|
* @ignore
|
|
*/
|
|
_pushInfinity(obj) {
|
|
const half = (obj < 0) ? BUF_INF_NEG : BUF_INF_POS
|
|
return this.push(half)
|
|
}
|
|
|
|
/**
|
|
* Choose the best float representation for a number and encode it.
|
|
*
|
|
* @param {number} obj A number that is known to be not-integer, but not
|
|
* how many bytes of precision it needs.
|
|
* @returns {boolean} True on success.
|
|
* @ignore
|
|
*/
|
|
_pushFloat(obj) {
|
|
if (this.canonical) {
|
|
// TODO: is this enough slower to hide behind canonical?
|
|
// It's certainly enough of a hack (see utils.parseHalf)
|
|
|
|
// From section 3.9:
|
|
// If a protocol allows for IEEE floats, then additional canonicalization
|
|
// rules might need to be added. One example rule might be to have all
|
|
// floats start as a 64-bit float, then do a test conversion to a 32-bit
|
|
// float; if the result is the same numeric value, use the shorter value
|
|
// and repeat the process with a test conversion to a 16-bit float. (This
|
|
// rule selects 16-bit float for positive and negative Infinity as well.)
|
|
|
|
// which seems pretty much backwards to me.
|
|
const b2 = Buffer.allocUnsafe(2)
|
|
if (utils.writeHalf(b2, obj)) {
|
|
// I have convinced myself that there are no cases where writeHalf
|
|
// will return true but `utils.parseHalf(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)
|
|
}
|
|
|
|
/**
|
|
* Choose the best integer representation for a postive number and encode
|
|
* it. If the number is over MAX_SAFE_INTEGER, fall back on float (but I
|
|
* don't remember why).
|
|
*
|
|
* @param {number} obj A positive number that is known to be an integer,
|
|
* but not how many bytes of precision it needs.
|
|
* @param {number} mt The Major Type number to combine with the integer.
|
|
* Not yet shifted.
|
|
* @param {number} [orig] The number before it was transformed to positive.
|
|
* If the mt is NEG_INT, and the positive number is over MAX_SAFE_INT,
|
|
* then we'll encode this as a float rather than making the number
|
|
* negative again and losing precision.
|
|
* @returns {boolean} True on success.
|
|
* @ignore
|
|
*/
|
|
_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) {
|
|
// Special case for Number.MIN_SAFE_INTEGER - 1
|
|
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)
|
|
}
|
|
|
|
/**
|
|
* Choose the best integer representation for a number and encode it.
|
|
*
|
|
* @param {number} obj A number that is known to be an integer,
|
|
* but not how many bytes of precision it needs.
|
|
* @returns {boolean} True on success.
|
|
* @ignore
|
|
*/
|
|
_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)
|
|
}
|
|
|
|
/**
|
|
* @param {number} obj Plain JS number to encode.
|
|
* @returns {boolean} True on success.
|
|
* @ignore
|
|
*/
|
|
_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)
|
|
}
|
|
|
|
/**
|
|
* @param {string} obj String to encode.
|
|
* @returns {boolean} True on success.
|
|
* @ignore
|
|
*/
|
|
_pushString(obj) {
|
|
const len = Buffer.byteLength(obj, 'utf8')
|
|
return this._pushInt(len, MT.UTF8_STRING) && this.push(obj, 'utf8')
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} obj Bool to encode.
|
|
* @returns {boolean} True on success.
|
|
* @ignore
|
|
*/
|
|
_pushBoolean(obj) {
|
|
return this._pushUInt8(obj ? TRUE : FALSE)
|
|
}
|
|
|
|
/**
|
|
* @param {undefined} obj Ignored.
|
|
* @returns {boolean} True on success.
|
|
* @ignore
|
|
*/
|
|
_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)
|
|
}
|
|
|
|
/**
|
|
* @param {null} obj Ignored.
|
|
* @returns {boolean} True on success.
|
|
* @ignore
|
|
*/
|
|
_pushNull(obj) {
|
|
return this._pushUInt8(NULL)
|
|
}
|
|
|
|
/**
|
|
* @param {number} tag Tag number to encode.
|
|
* @returns {boolean} True on success.
|
|
* @ignore
|
|
*/
|
|
_pushTag(tag) {
|
|
return this._pushInt(tag, MT.TAG)
|
|
}
|
|
|
|
/**
|
|
* @param {bigint} obj BigInt to encode.
|
|
* @returns {boolean} True on success.
|
|
* @ignore
|
|
*/
|
|
_pushJSBigint(obj) {
|
|
let m = MT.POS_INT
|
|
let tag = TAG.POS_BIGINT
|
|
// BigInt doesn't have -0
|
|
if (obj < 0) {
|
|
obj = -obj + BI.MINUS_ONE
|
|
m = MT.NEG_INT
|
|
tag = TAG.NEG_BIGINT
|
|
}
|
|
|
|
if (this.collapseBigIntegers &&
|
|
(obj <= BI.MAXINT64)) {
|
|
// Special handiling for 64bits
|
|
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)
|
|
}
|
|
|
|
/**
|
|
* @param {object} obj Object to encode.
|
|
* @returns {boolean} True on success.
|
|
* @throws {Error} Loop detected.
|
|
* @ignore
|
|
*/
|
|
_pushObject(obj, opts) {
|
|
if (!obj) {
|
|
return this._pushNull(obj)
|
|
}
|
|
opts = {
|
|
indefinite: false,
|
|
skipTypes: false,
|
|
...opts,
|
|
}
|
|
if (!opts.indefinite) {
|
|
// This will only happen the first time through for indefinite encoding
|
|
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) {
|
|
// Note: this can't be a normal sort, because 'b' needs to sort before
|
|
// 'aa'
|
|
keys.sort((a, b) => {
|
|
// Always strings, so don't bother to pass options.
|
|
// hold on to the cbor versions, since there's no need
|
|
// to encode more than once
|
|
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)) { // Already a Buffer
|
|
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
|
|
}
|
|
|
|
/**
|
|
* @param {any[]} objs Array of supported things.
|
|
* @returns {Buffer} Concatenation of encodings for the supported things.
|
|
* @ignore
|
|
*/
|
|
_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()
|
|
}
|
|
|
|
/**
|
|
* Add an encoding function to the list of supported semantic types. This
|
|
* is useful for objects for which you can't add an encodeCBOR method.
|
|
*
|
|
* @param {string|Function} type The type to encode.
|
|
* @param {EncodeFunction} fun The encoder to use.
|
|
* @returns {EncodeFunction?} The previous encoder or undefined if there
|
|
* wasn't one.
|
|
* @throws {TypeError} Invalid function.
|
|
*/
|
|
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
|
|
}
|
|
|
|
/**
|
|
* Push any supported type onto the encoded stream.
|
|
*
|
|
* @param {any} obj The thing to encode.
|
|
* @returns {boolean} True on success.
|
|
* @throws {TypeError} Unknown type for obj.
|
|
*/
|
|
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)
|
|
// TODO: Add pluggable support for other symbols
|
|
default:
|
|
throw new TypeError(`Unknown symbol: ${obj.toString()}`)
|
|
}
|
|
default:
|
|
throw new TypeError(
|
|
`Unknown type: ${typeof obj}, ${(typeof obj.toString === 'function') ? obj.toString() : ''}`
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Encode an array and all of its elements.
|
|
*
|
|
* @param {Encoder} gen Encoder to use.
|
|
* @param {any[]} obj Array to encode.
|
|
* @param {object} [opts] Options.
|
|
* @param {boolean} [opts.indefinite=false] Use indefinite encoding?
|
|
* @returns {boolean} True on success.
|
|
*/
|
|
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
|
|
}
|
|
|
|
/**
|
|
* Remove the loop detector WeakSet for this Encoder.
|
|
*
|
|
* @returns {boolean} True when the Encoder was reset, else false.
|
|
*/
|
|
removeLoopDetectors() {
|
|
if (!this.detectLoops) {
|
|
return false
|
|
}
|
|
this.detectLoops = new WeakSet()
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* @param {Encoder} gen Encoder.
|
|
* @param {Date} obj Date to encode.
|
|
* @returns {boolean} True on success.
|
|
* @ignore
|
|
*/
|
|
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':
|
|
// Force float
|
|
return gen._pushTag(TAG.DATE_EPOCH) &&
|
|
gen._pushFloat(obj.getTime() / 1000)
|
|
case 'number':
|
|
default:
|
|
// If we happen to have an integral number of seconds,
|
|
// use integer. Otherwise, use float.
|
|
return gen._pushTag(TAG.DATE_EPOCH) &&
|
|
gen.pushAny(obj.getTime() / 1000)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Encoder} gen Encoder.
|
|
* @param {Buffer} obj Buffer to encode.
|
|
* @returns {boolean} True on success.
|
|
* @ignore
|
|
*/
|
|
static _pushBuffer(gen, obj) {
|
|
return gen._pushInt(obj.length, MT.BYTE_STRING) && gen.push(obj)
|
|
}
|
|
|
|
/**
|
|
* @param {Encoder} gen Encoder.
|
|
* @param {NoFilter} obj Buffer to encode.
|
|
* @returns {boolean} True on success.
|
|
* @ignore
|
|
*/
|
|
static _pushNoFilter(gen, obj) {
|
|
return Encoder._pushBuffer(gen, /** @type {Buffer} */ (obj.slice()))
|
|
}
|
|
|
|
/**
|
|
* @param {Encoder} gen Encoder.
|
|
* @param {RegExp} obj RegExp to encode.
|
|
* @returns {boolean} True on success.
|
|
* @ignore
|
|
*/
|
|
static _pushRegexp(gen, obj) {
|
|
return gen._pushTag(TAG.REGEXP) && gen.pushAny(obj.source)
|
|
}
|
|
|
|
/**
|
|
* @param {Encoder} gen Encoder.
|
|
* @param {Set} obj Set to encode.
|
|
* @returns {boolean} True on success.
|
|
* @ignore
|
|
*/
|
|
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
|
|
}
|
|
|
|
/**
|
|
* @param {Encoder} gen Encoder.
|
|
* @param {URL} obj URL to encode.
|
|
* @returns {boolean} True on success.
|
|
* @ignore
|
|
*/
|
|
static _pushURL(gen, obj) {
|
|
return gen._pushTag(TAG.URI) && gen.pushAny(obj.toString())
|
|
}
|
|
|
|
/**
|
|
* @param {Encoder} gen Encoder.
|
|
* @param {object} obj Boxed String, Number, or Boolean object to encode.
|
|
* @returns {boolean} True on success.
|
|
* @ignore
|
|
*/
|
|
static _pushBoxed(gen, obj) {
|
|
return gen.pushAny(obj.valueOf())
|
|
}
|
|
|
|
/**
|
|
* @param {Encoder} gen Encoder.
|
|
* @param {Map} obj Map to encode.
|
|
* @returns {boolean} True on success.
|
|
* @throws {Error} Map key that is undefined.
|
|
* @ignore
|
|
*/
|
|
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
|
|
}
|
|
// Memoizing the cbor only helps in certain cases, and hurts in most
|
|
// others. Just avoid it.
|
|
if (gen.canonical) {
|
|
// Keep the key/value pairs together, so we don't have to do odd
|
|
// gets with object keys later
|
|
const enc = new Encoder({
|
|
genTypes: gen.semanticTypes,
|
|
canonical: gen.canonical,
|
|
detectLoops: Boolean(gen.detectLoops), // Give enc its own loop detector
|
|
dateType: gen.dateType,
|
|
disallowUndefinedKeys: gen.disallowUndefinedKeys,
|
|
collapseBigIntegers: gen.collapseBigIntegers,
|
|
})
|
|
const bs = new NoFilter({highWaterMark: gen.readableHighWaterMark})
|
|
enc.pipe(bs)
|
|
entries.sort(([a], [b]) => {
|
|
// Both a and b are the keys
|
|
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
|
|
}
|
|
|
|
/**
|
|
* @param {Encoder} gen Encoder.
|
|
* @param {NodeJS.TypedArray} obj Array to encode.
|
|
* @returns {boolean} True on success.
|
|
* @ignore
|
|
*/
|
|
static _pushTypedArray(gen, obj) {
|
|
// See https://tools.ietf.org/html/rfc8746
|
|
|
|
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)
|
|
)
|
|
}
|
|
|
|
/**
|
|
* @param {Encoder} gen Encoder.
|
|
* @param { ArrayBuffer } obj Array to encode.
|
|
* @returns {boolean} True on success.
|
|
* @ignore
|
|
*/
|
|
static _pushArrayBuffer(gen, obj) {
|
|
return Encoder._pushBuffer(gen, Buffer.from(obj))
|
|
}
|
|
|
|
/**
|
|
* Encode the given object with indefinite length. There are apparently
|
|
* some (IMO) broken implementations of poorly-specified protocols that
|
|
* REQUIRE indefinite-encoding. See the example for how to add this as an
|
|
* `encodeCBOR` function to an object or class to get indefinite encoding.
|
|
*
|
|
* @param {Encoder} gen The encoder to use.
|
|
* @param {string|Buffer|Array|Map|object} [obj] The object to encode. If
|
|
* null, use "this" instead.
|
|
* @param {EncodingOptions} [options={}] Options for encoding.
|
|
* @returns {boolean} True on success.
|
|
* @throws {Error} No object to encode or invalid indefinite encoding.
|
|
* @example <caption>Force indefinite encoding:</caption>
|
|
* const o = {
|
|
* a: true,
|
|
* encodeCBOR: cbor.Encoder.encodeIndefinite,
|
|
* }
|
|
* const m = []
|
|
* m.encodeCBOR = cbor.Encoder.encodeIndefinite
|
|
* cbor.encodeOne([o, m])
|
|
*/
|
|
static encodeIndefinite(gen, obj, options = {}) {
|
|
if (obj == null) {
|
|
if (this == null) {
|
|
throw new Error('No object to encode')
|
|
}
|
|
obj = this
|
|
}
|
|
|
|
// TODO: consider other options
|
|
const {chunkSize = 4096} = options
|
|
|
|
let ret = true
|
|
const objType = typeof obj
|
|
let buf = null
|
|
if (objType === 'string') {
|
|
// TODO: make sure not to split surrogate pairs at the edges of chunks,
|
|
// since such half-surrogates cannot be legally encoded as UTF-8.
|
|
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
|
|
}
|
|
|
|
/**
|
|
* Encode one or more JavaScript objects, and return a Buffer containing the
|
|
* CBOR bytes.
|
|
*
|
|
* @param {...any} objs The objects to encode.
|
|
* @returns {Buffer} The encoded objects.
|
|
*/
|
|
static encode(...objs) {
|
|
return new Encoder()._encodeAll(objs)
|
|
}
|
|
|
|
/**
|
|
* Encode one or more JavaScript objects canonically (slower!), and return
|
|
* a Buffer containing the CBOR bytes.
|
|
*
|
|
* @param {...any} objs The objects to encode.
|
|
* @returns {Buffer} The encoded objects.
|
|
*/
|
|
static encodeCanonical(...objs) {
|
|
return new Encoder({
|
|
canonical: true,
|
|
})._encodeAll(objs)
|
|
}
|
|
|
|
/**
|
|
* Encode one JavaScript object using the given options.
|
|
*
|
|
* @static
|
|
* @param {any} obj The object to encode.
|
|
* @param {EncodingOptions} [options={}] Passed to the Encoder constructor.
|
|
* @returns {Buffer} The encoded objects.
|
|
*/
|
|
static encodeOne(obj, options) {
|
|
return new Encoder(options)._encodeAll([obj])
|
|
}
|
|
|
|
/**
|
|
* Encode one JavaScript object using the given options in a way that
|
|
* is more resilient to objects being larger than the highWaterMark
|
|
* number of bytes. As with the other static encode functions, this
|
|
* will still use a large amount of memory. Use a stream-based approach
|
|
* directly if you need to process large and complicated inputs.
|
|
*
|
|
* @param {any} obj The object to encode.
|
|
* @param {EncodingOptions} [options={}] Passed to the Encoder constructor.
|
|
* @returns {Promise<Buffer>} A promise for the encoded buffer.
|
|
*/
|
|
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()
|
|
})
|
|
}
|
|
|
|
/**
|
|
* The currently supported set of semantic types. May be modified by plugins.
|
|
*
|
|
* @type {SemanticMap}
|
|
*/
|
|
static get SEMANTIC_TYPES() {
|
|
return current_SEMANTIC_TYPES
|
|
}
|
|
|
|
static set SEMANTIC_TYPES(val) {
|
|
current_SEMANTIC_TYPES = val
|
|
}
|
|
|
|
/**
|
|
* Reset the supported semantic types to the original set, before any
|
|
* plugins modified the list.
|
|
*/
|
|
static reset() {
|
|
Encoder.SEMANTIC_TYPES = {...SEMANTIC_TYPES}
|
|
}
|
|
}
|
|
|
|
Object.assign(SEMANTIC_TYPES, {
|
|
Array: Encoder.pushArray,
|
|
Date: Encoder._pushDate,
|
|
Buffer: Encoder._pushBuffer,
|
|
[Buffer.name]: Encoder._pushBuffer, // Might be mangled
|
|
Map: Encoder._pushMap,
|
|
NoFilter: Encoder._pushNoFilter,
|
|
[NoFilter.name]: Encoder._pushNoFilter, // Mßight be mangled
|
|
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,
|
|
})
|
|
|
|
// Safari needs to get better.
|
|
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
|