Permalink
Cannot retrieve contributors at this time
1101 lines (1027 sloc)
30.9 KB
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?
codeql-action/node_modules/cbor/lib/encoder.js
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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 |