UNPKG

cbor

Version:

Encode and parse data in the Concise Binary Object Representation (CBOR) data format (RFC8949).

379 lines (349 loc) 10.9 kB
'use strict'; const constants = require('./constants'); const utils = require('./utils'); const INTERNAL_JSON = Symbol('INTERNAL_JSON'); function setBuffersToJSON(obj, fn) { // The data item tagged can be a byte string or any other data item. In the // latter case, the tag applies to all of the byte string data items // contained in the data item, except for those contained in a nested data // item tagged with an expected conversion. if (utils.isBufferish(obj)) { obj.toJSON = fn; } else if (Array.isArray(obj)) { for (const v of obj) { setBuffersToJSON(v, fn); } } else if (obj && (typeof obj === 'object')) { // FFS, complexity in the protocol. // There's some circular dependency in here. // eslint-disable-next-line no-use-before-define if (!(obj instanceof Tagged) || (obj.tag < 21) || (obj.tag > 23)) { for (const v of Object.values(obj)) { setBuffersToJSON(v, fn); } } } } function b64this() { // eslint-disable-next-line no-invalid-this return utils.base64(this); } function b64urlThis() { // eslint-disable-next-line no-invalid-this return utils.base64url(this); } function hexThis() { // eslint-disable-next-line no-invalid-this return this.toString('hex'); } function swapEndian(ab, size, byteOffset, byteLength) { const dv = new DataView(ab); const [getter, setter] = { 2: [dv.getUint16, dv.setUint16], 4: [dv.getUint32, dv.setUint32], 8: [dv.getBigUint64, dv.setBigUint64], }[size]; const end = byteOffset + byteLength; for (let offset = byteOffset; offset < end; offset += size) { setter.call(dv, offset, getter.call(dv, offset, true)); } } /** * Convert a tagged value to a more interesting JavaScript type. Errors * thrown in this function will be captured into the "err" property of the * original Tagged instance. * * @callback TagFunction * @param {any} value The value inside the tag. * @param {Tagged} tag The enclosing Tagged instance; useful if you want to * modify it and return it. Also available as "this". * @returns {any} The transformed value. */ /** * A mapping from tag number to a tag decoding function. * * @typedef {Object.<string, TagFunction>} TagMap */ /** * @type {TagMap} * @private */ const TAGS = { // Standard date/time string; see Section 3.4.1 0: v => new Date(v), // Epoch-based date/time; see Section 3.4.2 1: v => new Date(v * 1000), // Positive bignum; see Section 3.4.3 2: v => utils.bufferToBigInt(v), // Negative bignum; see Section 3.4.3 3: v => constants.BI.MINUS_ONE - utils.bufferToBigInt(v), // Expected conversion to base64url encoding; see Section 3.4.5.2 21: (v, tag) => { if (utils.isBufferish(v)) { tag[INTERNAL_JSON] = b64urlThis; } else { setBuffersToJSON(v, b64urlThis); } return tag; }, // Expected conversion to base64 encoding; see Section 3.4.5.2 22: (v, tag) => { if (utils.isBufferish(v)) { tag[INTERNAL_JSON] = b64this; } else { setBuffersToJSON(v, b64this); } return tag; }, // Expected conversion to base16 encoding; see Section Section 3.4.5.2 23: (v, tag) => { if (utils.isBufferish(v)) { tag[INTERNAL_JSON] = hexThis; } else { setBuffersToJSON(v, hexThis); } return tag; }, // URI; see Section 3.4.5.3 32: v => new URL(v), // Base64url; see Section 3.4.5.3 33: (v, tag) => { // If any of the following apply: // - the encoded text string contains non-alphabet characters or // only 1 alphabet character in the last block of 4 (where // alphabet is defined by Section 5 of [RFC4648] for tag number 33 // and Section 4 of [RFC4648] for tag number 34), or if (!v.match(/^[a-zA-Z0-9_-]+$/)) { throw new Error('Invalid base64url characters'); } const last = v.length % 4; if (last === 1) { throw new Error('Invalid base64url length'); } // - the padding bits in a 2- or 3-character block are not 0, or if (last === 2) { // The last 4 bits of the last character need to be zero. if ('AQgw'.indexOf(v[v.length - 1]) === -1) { throw new Error('Invalid base64 padding'); } } else if (last === 3) { // The last 2 bits of the last character need to be zero. if ('AEIMQUYcgkosw048'.indexOf(v[v.length - 1]) === -1) { throw new Error('Invalid base64 padding'); } } // Or // - the base64url encoding has padding characters, // (caught above) // the string is invalid. return tag; }, // Base64; see Section 3.4.5.3 34: (v, tag) => { // If any of the following apply: // - the encoded text string contains non-alphabet characters or // only 1 alphabet character in the last block of 4 (where // alphabet is defined by Section 5 of [RFC4648] for tag number 33 // and Section 4 of [RFC4648] for tag number 34), or const m = v.match(/^[a-zA-Z0-9+/]+(?<padding>={0,2})$/); if (!m) { throw new Error('Invalid base64 characters'); } if ((v.length % 4) !== 0) { throw new Error('Invalid base64 length'); } // - the padding bits in a 2- or 3-character block are not 0, or if (m.groups.padding === '=') { // The last 4 bits of the last character need to be zero. if ('AQgw'.indexOf(v[v.length - 2]) === -1) { throw new Error('Invalid base64 padding'); } } else if (m.groups.padding === '==') { // The last 2 bits of the last character need to be zero. if ('AEIMQUYcgkosw048'.indexOf(v[v.length - 3]) === -1) { throw new Error('Invalid base64 padding'); } } // - the base64 encoding has the wrong number of padding characters, // (caught above) // the string is invalid. return tag; }, // Regular expression; see Section 2.4.4.3 35: v => new RegExp(v), // https://github.com/input-output-hk/cbor-sets-spec/blob/master/CBOR_SETS.md 258: v => new Set(v), }; const TYPED_ARRAY_TAGS = { 64: Uint8Array, 65: Uint16Array, 66: Uint32Array, // 67: BigUint64Array, Safari doesn't implement 68: Uint8ClampedArray, 69: Uint16Array, 70: Uint32Array, // 71: BigUint64Array, Safari doesn't implement 72: Int8Array, 73: Int16Array, 74: Int32Array, // 75: BigInt64Array, Safari doesn't implement // 76: reserved 77: Int16Array, 78: Int32Array, // 79: BigInt64Array, Safari doesn't implement // 80: not implemented, float16 array 81: Float32Array, 82: Float64Array, // 83: not implemented, float128 array // 84: not implemented, float16 array 85: Float32Array, 86: Float64Array, // 87: not implemented, float128 array }; // Safari if (typeof BigUint64Array !== 'undefined') { TYPED_ARRAY_TAGS[67] = BigUint64Array; TYPED_ARRAY_TAGS[71] = BigUint64Array; } if (typeof BigInt64Array !== 'undefined') { TYPED_ARRAY_TAGS[75] = BigInt64Array; TYPED_ARRAY_TAGS[79] = BigInt64Array; } function toTypedArray(val, tagged) { if (!utils.isBufferish(val)) { throw new TypeError('val not a buffer'); } const {tag} = tagged; // See https://tools.ietf.org/html/rfc8746 const TypedClass = TYPED_ARRAY_TAGS[tag]; if (!TypedClass) { throw new Error(`Invalid typed array tag: ${tag}`); } const little = tag & 0b00000100; const float = (tag & 0b00010000) >> 4; const sz = 2 ** (float + (tag & 0b00000011)); if ((!little !== utils.isBigEndian()) && (sz > 1)) { swapEndian(val.buffer, sz, val.byteOffset, val.byteLength); } const ab = val.buffer.slice(val.byteOffset, val.byteOffset + val.byteLength); return new TypedClass(ab); } for (const n of Object.keys(TYPED_ARRAY_TAGS)) { TAGS[n] = toTypedArray; } /** * @type {TagMap} * @private */ let current_TAGS = {}; /** * A CBOR tagged item, where the tag does not have semantics specified at the * moment, or those semantics threw an error during parsing. Typically this will * be an extension point you're not yet expecting. */ class Tagged { /** * Creates an instance of Tagged. * * @param {number} tag The number of the tag. * @param {any} value The value inside the tag. * @param {Error} [err] The error that was thrown parsing the tag, or null. */ constructor(tag, value, err) { this.tag = tag; this.value = value; this.err = err; if (typeof this.tag !== 'number') { throw new Error(`Invalid tag type (${typeof this.tag})`); } if ((this.tag < 0) || ((this.tag | 0) !== this.tag)) { throw new Error(`Tag must be a positive integer: ${this.tag}`); } } toJSON() { if (this[INTERNAL_JSON]) { return this[INTERNAL_JSON].call(this.value); } const ret = { tag: this.tag, value: this.value, }; if (this.err) { ret.err = this.err; } return ret; } /** * Convert to a String. * * @returns {string} String of the form '1(2)'. */ toString() { return `${this.tag}(${JSON.stringify(this.value)})`; } /** * Push the simple value onto the CBOR stream. * * @param {object} gen The generator to push onto. * @returns {boolean} True on success. */ encodeCBOR(gen) { gen._pushTag(this.tag); return gen.pushAny(this.value); } /** * If we have a converter for this type, do the conversion. Some converters * are built-in. Additional ones can be passed in. If you want to remove * a built-in converter, pass a converter in whose value is 'null' instead * of a function. * * @param {object} converters Keys in the object are a tag number, the value * is a function that takes the decoded CBOR and returns a JavaScript value * of the appropriate type. Throw an exception in the function on errors. * @returns {any} The converted item. */ convert(converters) { let f = (converters == null) ? undefined : converters[this.tag]; if (f === null) { // === is intentional. null has semantic meaning as above return this; } if (typeof f !== 'function') { f = Tagged.TAGS[this.tag]; if (typeof f !== 'function') { return this; } } try { return f.call(this, this.value, this); } catch (error) { if (error && error.message && (error.message.length > 0)) { this.err = error.message; } else { this.err = error; } return this; } } /** * The current set of supported tags. May be modified by plugins. * * @type {TagMap} * @static */ static get TAGS() { return current_TAGS; } static set TAGS(val) { current_TAGS = val; } /** * Reset the supported tags to the original set, before any plugins modified * the list. */ static reset() { Tagged.TAGS = {...TAGS}; } } Tagged.INTERNAL_JSON = INTERNAL_JSON; Tagged.reset(); module.exports = Tagged;