'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;