UNPKG

cbor-object

Version:

CBOR: deterministic encoder/decoder, diagnostic notation encoder/decoder, and utilities

1,756 lines (1,502 loc) 57.3 kB
////////////////////////////////////////////////////////////////// // // // CBOR JavaScript API // // // // Author: Anders Rundgren (anders.rundgrn.net@gmail.com) // // Repository: https://github.com/cyberphone/CBOR.js#cborjs // ////////////////////////////////////////////////////////////////// 'use strict'; // Single global static object. class CBOR { // Super class for all CBOR wrappers. static #CborObject = class { #readFlag; _immutableFlag; constructor() { this.#readFlag = false; this._immutableFlag = false; } getInt = function() { if (this instanceof CBOR.BigInt) { // During decoding, integers outside of Number.MAX_SAFE_INTEGER // automatically get "BigInt" representation. CBOR.#error("Integer is outside of Number.MAX_SAFE_INTEGER, use getBigInt()"); } return this.#checkTypeAndGetValue(CBOR.Int); } #rangeInt = function(min, max) { let value = this.getInt(); if (value < min || value > max) { CBOR.#error("Value out of range: " + value); } return value; } getInt8 = function() { return this.#rangeInt(-0x80, 0x7f); } getUint8 = function() { return this.#rangeInt(0, 0xff); } getInt16 = function() { return this.#rangeInt(-0x8000, 0x7fff); } getUint16 = function() { return this.#rangeInt(0, 0xffff); } getInt32 = function() { return this.#rangeInt(-0x80000000, 0x7fffffff); } getUint32 = function() { return this.#rangeInt(0, 0xffffffff); } getString = function() { return this.#checkTypeAndGetValue(CBOR.String); } getDateTime = function() { let iso = this.getString(); // Fails on https://www.rfc-editor.org/rfc/rfc3339.html#section-5.8 // Leap second 1990-12-31T15:59:60-08:00 if (iso.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(\.\d+)?((\-|\+)\d{2}:\d{2}|Z)$/m)) { let dateTime = new Date(iso); if (Number.isFinite(dateTime.getTime())) { return dateTime; } } CBOR.#error("Invalid ISO date string: " + iso); } getEpochTime = function() { let time = this instanceof CBOR.Int ? this.getInt() * 1000 : Math.round(this.getFloat64() * 1000); let epochTime = new Date(); epochTime.setTime(time); return epochTime; } getBytes = function() { return this.#checkTypeAndGetValue(CBOR.Bytes); } #rangeFloat = function(max) { let value = this.getFloat64(); if (this.length > max) { CBOR.#error("Value out of range: " + this.toString()); } return value; } getFloat16 = function() { return this.#rangeFloat(2); } getFloat32 = function() { return this.#rangeFloat(4); } getFloat64 = function() { return this.#checkTypeAndGetValue(CBOR.Float); } getBoolean = function() { return this.#checkTypeAndGetValue(CBOR.Boolean); } isNull = function() { if (this instanceof CBOR.Null) { this.#readFlag = true; return true; } return false; } getBigInt = function() { if (this instanceof CBOR.Int) { return BigInt(this.getInt()); } return this.#checkTypeAndGetValue(CBOR.BigInt); } #rangeBigInt(min, max) { let value = this.getBigInt(); if (value < min || value > max) { CBOR.#error("Value out of range: " + value); } return value; } getInt64 = function() { return this.#rangeBigInt(-0x8000000000000000n, 0x7fffffffffffffffn); } getUint64 = function() { return this.#rangeBigInt(0n, 0xffffffffffffffffn); } getSimple = function() { return this.#checkTypeAndGetValue(CBOR.Simple); } equals = function(object) { if (object && object instanceof CBOR.#CborObject) { return CBOR.compareArrays(this.encode(), object.encode()) == 0; } return false; } clone = function() { return CBOR.decode(this.encode()); } #noSuchMethod = function(method) { CBOR.#error(method + '() not available in: CBOR.' + this.constructor.name); } get = function() { this.#noSuchMethod("get"); } toDiag = function(prettyPrint) { let cborPrinter = new CBOR.#CborPrinter(CBOR.#typeCheck(prettyPrint, 'boolean')); this.internalToString(cborPrinter); return cborPrinter.buffer; } toString = function() { return this.toDiag(true); } _immutableTest = function() { if (this._immutableFlag) { CBOR.#error('Map keys are immutable'); } } _markAsRead = function() { this.#readFlag = true; } #traverse = function(holderObject, check) { switch (this.constructor.name) { case "Map": this.getKeys().forEach(key => { this.get(key).#traverse(key, check); }); break; case "Array": this.toArray().forEach(object => { object.#traverse(this, check); }); break; case "Tag": this.get().#traverse(this, check); break; } if (check) { if (!this.#readFlag) { CBOR.#error((holderObject == null ? "Data" : holderObject instanceof CBOR.Array ? "Array element" : holderObject instanceof CBOR.Tag ? "Tagged object " + holderObject.getTagNumber().toString() : "Map key " + holderObject.toDiag(false) + " with argument") + " of type=CBOR." + this.constructor.name + " with value=" + this.toDiag(false) + " was never read"); } } else { this.#readFlag = true; } } scan = function() { this.#traverse(null, false); return this; } checkForUnread = function() { this.#traverse(null, true); } get length() { if (!this._getLength) { CBOR.#error("CBOR." + this.constructor.name + " does not have a 'length' property"); } return this._getLength(); } #checkTypeAndGetValue = function(className) { if (!(this instanceof className)) { CBOR.#error("Expected CBOR." + className.name + ", got: CBOR." + this.constructor.name); } this.#readFlag = true; return this._get(); } } static CborError = class extends Error { constructor(message) { super(message); } } static #error = function(message) { if (message.length > 100) { message = message.substring(0, 100) + ' ...'; } throw new CBOR.CborError(message); } static #MT_UNSIGNED = 0x00; static #MT_NEGATIVE = 0x20; static #MT_BYTES = 0x40; static #MT_STRING = 0x60; static #MT_ARRAY = 0x80; static #MT_MAP = 0xa0; static #MT_SIMPLE = 0xe0; static #MT_TAG = 0xc0; static #MT_BIG_UNSIGNED = 0xc2; static #MT_BIG_NEGATIVE = 0xc3; static #MT_FALSE = 0xf4; static #MT_TRUE = 0xf5; static #MT_NULL = 0xf6; static #MT_FLOAT16 = 0xf9; static #MT_FLOAT32 = 0xfa; static #MT_FLOAT64 = 0xfb; static #ESCAPE_CHARACTERS = [ // 0 1 2 3 4 5 6 7 8 9 A B C D E F 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 'b', 't', 'n', 1 , 'f', 'r', 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 0 , 0 , '"', 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , '\\']; constructor() { CBOR.#error("CBOR cannot be instantiated"); } /////////////////////////// // CBOR.Int // /////////////////////////// static Int = class extends CBOR.#CborObject { #value; // Integers with a magnitude above 2^53 - 1, must use CBOR.BigInt. constructor(value) { super(); this.#value = CBOR.#intCheck(value); } encode = function() { let tag; let n = this.#value; if (n < 0) { tag = CBOR.#MT_NEGATIVE; n = -n - 1; } else { tag = CBOR.#MT_UNSIGNED; } return CBOR.#encodeTagAndN(tag, n); } internalToString = function(cborPrinter) { cborPrinter.append(this.#value.toString()); } _get = function() { return this.#value; } } /////////////////////////// // CBOR.BigInt // /////////////////////////// static BigInt = class extends CBOR.#CborObject { #value; // The CBOR.BigInt wrapper object implements the CBOR integer reduction algorithm. The // JavaScript "BigInt" object is used for maintaining lossless represention of bignums. constructor(value) { super(); this.#value = CBOR.#typeCheck(value, 'bigint'); } encode = function() { let tag; let value = this.#value if (value < 0) { tag = CBOR.#MT_NEGATIVE; value = ~value; } else { tag = CBOR.#MT_UNSIGNED; } return CBOR.#finishBigIntAndTag(tag, value); } internalToString = function(cborPrinter) { cborPrinter.append(this.#value.toString()); } _get = function() { return this.#value; } } /////////////////////////// // CBOR.Float // /////////////////////////// static Float = class extends CBOR.#CborObject { #value; #encoded; #tag; constructor(value) { super(); this.#value = CBOR.#typeCheck(value, 'number'); // Begin catching the F16 edge cases. this.#tag = CBOR.#MT_FLOAT16; if (Number.isNaN(value)) { this.#encoded = CBOR.#int16ToByteArray(0x7e00); } else if (!Number.isFinite(value)) { this.#encoded = CBOR.#int16ToByteArray(value < 0 ? 0xfc00 : 0x7c00); } else if (value == 0) { // True for -0.0 as well! this.#encoded = CBOR.#int16ToByteArray(Object.is(value,-0) ? 0x8000 : 0x0000); } else { // It is apparently a genuine (non-zero) number. // Get the full F64 binary. const buffer = new ArrayBuffer(8); new DataView(buffer).setFloat64(0, value, false); const u8 = new Uint8Array(buffer) let f32exp; let f32signif; while (true) { // "goto" surely beats quirky loop/break/return/flag constructs... // The following code depends on that Math.fround works as expected. if (value == Math.fround(value)) { // Nothing was lost during the conversion, F32 or F16 is on the menu. f32exp = ((u8[0] & 0x7f) << 4) + ((u8[1] & 0xf0) >> 4) - 0x380; f32signif = ((u8[1] & 0x0f) << 19) + (u8[2] << 11) + (u8[3] << 3) + (u8[4] >> 5); // Very small F32 numbers may require subnormal representation. if (f32exp <= 0) { // The implicit "1" becomes explicit using subnormal representation. f32signif += 0x800000; // Denormalize by shifting right 1-23 positions. f32signif >>= (1 - f32exp); f32exp = 0; // Subnormal F32 cannot be represented by F16, stick to F32. break; } // If F16 would lose precision, stick to F32. if (f32signif & 0x1fff) { break; } // Setup for F16. let f16exp = f32exp - 0x70; // Too small or too big for F16, or running into F16 NaN/Infinity space. if (f16exp <= -10 || f16exp > 30) { break; } let f16signif = f32signif >> 13; // Finally, check if we need to denormalize F16. if (f16exp <= 0) { if (f16signif & (1 << (1 - f16exp)) - 1) { // Losing bits is not an option, stick to F32. break; } // The implicit "1" becomes explicit using subnormal representation. f16signif += 0x400; // Put significand in position. f16signif >>= (1 - f16exp); // Valid and denormalized F16 have exponent = 0. f16exp = 0; } // A rarity, 16 bits turned out being sufficient for representing value. this.#encoded = CBOR.#int16ToByteArray( // Put sign bit in position. ((u8[0] & 0x80) << 8) + // Exponent. Put it in front of significand. (f16exp << 10) + // Significand. f16signif); } else { // Converting value to F32 returned a truncated result. // Full 64-bit representation is required. this.#tag = CBOR.#MT_FLOAT64; this.#encoded = u8; } // Common F16 and F64 return point. return; } // Broken loop: 32 bits are apparently needed for maintaining magnitude and precision. this.#tag = CBOR.#MT_FLOAT32; let f32bin = // Put sign bit in position. Why not << 24? JS shift doesn't work above 2^31... ((u8[0] & 0x80) * 0x1000000) + // Exponent. Put it in front of significand (<< 23). (f32exp * 0x800000) + // Significand. f32signif; this.#encoded = CBOR.addArrays(CBOR.#int16ToByteArray(f32bin / 0x10000), CBOR.#int16ToByteArray(f32bin % 0x10000)); } } encode = function() { return CBOR.addArrays(new Uint8Array([this.#tag]), this.#encoded); } internalToString = function(cborPrinter) { let floatString = Object.is(this.#value,-0) ? '-0.0' : this.#value.toString(); // Diagnostic Notation support. if (floatString.indexOf('.') < 0) { let matches = floatString.match(/\-?\d+/g); if (matches) { floatString = matches[0] + '.0' + floatString.substring(matches[0].length); } } cborPrinter.append(floatString); } _isBadFloat = function() { return this.#encoded.length == 2 && (this.#encoded[0] & 0x7c) == 0x7c; } _compare = function(decoded) { return CBOR.compareArrays(this.#encoded, decoded); } _getLength = function() { return this.#encoded.length; } _get = function() { return this.#value; } } /////////////////////////// // CBOR.String // /////////////////////////// static String = class extends CBOR.#CborObject { #textString; constructor(textString) { super(); this.#textString = CBOR.#typeCheck(textString, 'string'); } encode = function() { let utf8 = new TextEncoder().encode(this.#textString); return CBOR.addArrays(CBOR.#encodeTagAndN(CBOR.#MT_STRING, utf8.length), utf8); } internalToString = function(cborPrinter) { cborPrinter.append('"'); for (let q = 0; q < this.#textString.length; q++) { let c = this.#textString.charCodeAt(q); if (c <= 0x5c) { let escapedCharacter; if (escapedCharacter = CBOR.#ESCAPE_CHARACTERS[c]) { cborPrinter.append('\\'); if (escapedCharacter == 1) { cborPrinter.append('u00'); cborPrinter.append(CBOR.#twoHex(c)); } else { cborPrinter.append(escapedCharacter); } continue; } } cborPrinter.append(String.fromCharCode(c)); } cborPrinter.append('"'); } _get = function() { return this.#textString; } } /////////////////////////// // CBOR.Bytes // /////////////////////////// static Bytes = class extends CBOR.#CborObject { #byteString; constructor(byteString) { super(); this.#byteString = CBOR.#bytesCheck(byteString); } encode = function() { return CBOR.addArrays(CBOR.#encodeTagAndN(CBOR.#MT_BYTES, this.#byteString.length), this.#byteString); } internalToString = function(cborPrinter) { cborPrinter.append("h'" + CBOR.toHex(this.#byteString) + "'"); } _get = function() { return this.#byteString; } } /////////////////////////// // CBOR.Boolean // /////////////////////////// static Boolean = class extends CBOR.#CborObject { #value; constructor(value) { super(); this.#value = CBOR.#typeCheck(value, 'boolean'); } encode = function() { return new Uint8Array([this.#value ? CBOR.#MT_TRUE : CBOR.#MT_FALSE]); } internalToString = function(cborPrinter) { cborPrinter.append(this.#value.toString()); } _get = function() { return this.#value; } } /////////////////////////// // CBOR.Null // /////////////////////////// static Null = class extends CBOR.#CborObject { encode = function() { return new Uint8Array([CBOR.#MT_NULL]); } internalToString = function(cborPrinter) { cborPrinter.append('null'); } } /////////////////////////// // CBOR.Array // /////////////////////////// static Array = class extends CBOR.#CborObject { #objects = []; add = function(object) { CBOR.#checkArgs(arguments, 1); this._immutableTest(); this.#objects.push(CBOR.#cborArgumentCheck(object)); return this; } get = function(index) { CBOR.#checkArgs(arguments, 1); this._markAsRead(); index = CBOR.#intCheck(index); if (index < 0 || index >= this.#objects.length) { CBOR.#error("Array index out of range: " + index); } return this.#objects[index]; } update = function(index, object) { CBOR.#checkArgs(arguments, 2); this._immutableTest(); index = CBOR.#intCheck(index); if (index < 0 || index >= this.#objects.length) { CBOR.#error("Array index out of range: " + index); } return this.#objects.splice(index, 1, CBOR.#cborArgumentCheck(object))[0]; } toArray = function() { let array = []; this.#objects.forEach(object => array.push(object)); return array; } encode = function() { let encoded = CBOR.#encodeTagAndN(CBOR.#MT_ARRAY, this.#objects.length); this.#objects.forEach(object => { encoded = CBOR.addArrays(encoded, object.encode()); }); return encoded; } internalToString = function(cborPrinter) { cborPrinter.append('['); let notFirst = false; this.#objects.forEach(object => { if (notFirst) { cborPrinter.append(','); cborPrinter.space(); } notFirst = true; object.internalToString(cborPrinter); }); cborPrinter.append(']'); } _getLength = function() { return this.#objects.length; } } /////////////////////////// // CBOR.Map // /////////////////////////// static Map = class extends CBOR.#CborObject { #entries = []; #preSortedKeys = false; static Entry = class { constructor(key, object) { this.key = key; this.object = object; this.encodedKey = key.encode(); } compare = function(encodedKey) { return CBOR.compareArrays(this.encodedKey, encodedKey); } compareAndTest = function(entry) { let diff = this.compare(entry.encodedKey); if (diff == 0) { CBOR.#error("Duplicate key: " + this.key); } return diff > 0; } } set = function(key, object) { CBOR.#checkArgs(arguments, 2); this._immutableTest(); let newEntry = new CBOR.Map.Entry(this.#getKey(key), CBOR.#cborArgumentCheck(object)); this.#makeImmutable(key); let insertIndex = this.#entries.length; if (insertIndex) { let endIndex = insertIndex - 1; if (this.#preSortedKeys) { // Normal case for deterministic decoding. if (this.#entries[endIndex].compareAndTest(newEntry)) { CBOR.#error("Non-deterministic order for key: " + key); } } else { // Programmatically created key or the result of unconstrained decoding. // Then we need to test and sort (always produce deterministic CBOR). // The algorithm is based on binary sort and insertion. insertIndex = 0; let startIndex = 0; while (startIndex <= endIndex) { let midIndex = (endIndex + startIndex) >> 1; if (newEntry.compareAndTest(this.#entries[midIndex])) { // New key is bigger than the looked up entry. // Preliminary assumption: this is the one, but continue. insertIndex = startIndex = midIndex + 1; } else { // New key is smaller, search lower parts of the array. endIndex = midIndex - 1; } } } } // If insertIndex == this.#entries.length, the key will be appended. // If insertIndex == 0, the key will be first in the list. this.#entries.splice(insertIndex, 0, newEntry); return this; } setDynamic = function(dynamic) { return dynamic(this); } #getKey = function(key) { return CBOR.#cborArgumentCheck(key); } #lookup(key, mustExist) { let encodedKey = this.#getKey(key).encode(); let startIndex = 0; let endIndex = this.#entries.length - 1; while (startIndex <= endIndex) { let midIndex = (endIndex + startIndex) >> 1; let entry = this.#entries[midIndex]; let diff = entry.compare(encodedKey); if (diff == 0) { return entry; } if (diff < 0) { startIndex = midIndex + 1; } else { endIndex = midIndex - 1; } } if (mustExist) { CBOR.#error("Missing key: " + key); } return null; } update = function(key, object, existing) { CBOR.#checkArgs(arguments, 3); this._immutableTest(); let entry = this.#lookup(key, existing); let previous; if (entry) { previous = entry.object; entry.object = CBOR.#cborArgumentCheck(object); } else { previous = null; this.set(key, object); } return previous; } merge = function(map) { CBOR.#checkArgs(arguments, 1); this._immutableTest(); if (!(map instanceof CBOR.Map)) { CBOR.#error("Argument must be of type CBOR.Map"); } map.#entries.forEach(entry => { this.set(entry.key, entry.object); }); return this; } get = function(key) { CBOR.#checkArgs(arguments, 1); this._markAsRead(); return this.#lookup(key, true).object; } getConditionally = function(key, defaultObject) { CBOR.#checkArgs(arguments, 2); let entry = this.#lookup(key, false); // Note: defaultValue may be 'null' defaultObject = defaultObject ? CBOR.#cborArgumentCheck(defaultObject) : null; return entry ? entry.object : defaultObject; } getKeys = function() { let keys = []; this.#entries.forEach(entry => { keys.push(entry.key); }); return keys; } remove = function(key) { CBOR.#checkArgs(arguments, 1); this._immutableTest(); let targetEntry = this.#lookup(key, true); for (let i = 0; i < this.#entries.length; i++) { if (this.#entries[i] == targetEntry) { this.#entries.splice(i, 1); return targetEntry.object; } } } _getLength = function() { return this.#entries.length; } containsKey = function(key) { CBOR.#checkArgs(arguments, 1); return this.#lookup(key, false) != null; } encode = function() { let encoded = CBOR.#encodeTagAndN(CBOR.#MT_MAP, this.#entries.length); this.#entries.forEach(entry => { encoded = CBOR.addArrays(encoded, CBOR.addArrays(entry.encodedKey, entry.object.encode())); }); return encoded; } internalToString = function(cborPrinter) { let notFirst = false; cborPrinter.beginMap(); this.#entries.forEach(entry => { if (notFirst) { cborPrinter.append(','); } notFirst = true; cborPrinter.newlineAndIndent(); entry.key.internalToString(cborPrinter); cborPrinter.append(':'); cborPrinter.space(); entry.object.internalToString(cborPrinter); }); cborPrinter.endMap(notFirst); } setSortingMode = function(preSortedKeys) { CBOR.#checkArgs(arguments, 1); this.#preSortedKeys = preSortedKeys; return this; } #makeImmutable = function(object) { object._immutableFlag = true; if (object instanceof CBOR.Map) { object.getKeys().forEach(key => { this.#makeImmutable(object.get(key)); }); } else if (object instanceof CBOR.Array) { object.toArray().forEach(value => { this.#makeImmutable(value); }); } } } /////////////////////////// // CBOR.Tag // /////////////////////////// static Tag = class extends CBOR.#CborObject { static TAG_DATE_TIME = 0n; static TAG_EPOCH_TIME = 1n; static TAG_COTX = 1010n; static TAG_BIGINT_POS = 2n; static TAG_BIGINT_NEG = 3n; static ERR_COTX = "Invalid COTX object: "; static ERR_DATE_TIME = "Invalid ISO date/time object: "; static ERR_EPOCH_TIME = "Invalid Epoch time object: "; #tagNumber; #object; #dateTime; #epochTime; constructor(tagNumber, object) { super(); this.#tagNumber = CBOR.#typeCheck(tagNumber, 'bigint'); this.#object = CBOR.#cborArgumentCheck(object); if (tagNumber < 0n || tagNumber >= 0x10000000000000000n) { CBOR.#error("Tag number is out of range"); } if (tagNumber == CBOR.Tag.TAG_BIGINT_POS || tagNumber == CBOR.Tag.TAG_BIGINT_NEG) { CBOR.#error("Tag number reserved for 'bigint'"); } if (tagNumber == CBOR.Tag.TAG_DATE_TIME) { // Note: clone() because we have mot read it really. this.#dateTime = object.clone().getDateTime(); } else if (tagNumber == CBOR.Tag.TAG_EPOCH_TIME) { // Note: clone() because we have mot read it really. this.#epochTime = object.clone().getEpochTime(); } else if (tagNumber == CBOR.Tag.TAG_COTX) { if (!(object instanceof CBOR.Array) || object.length != 2 || !(object.get(0) instanceof CBOR.String)) { this.#errorInObject(CBOR.Tag.ERR_COTX); } } } getDateTime = function() { if (!this.#dateTime) { this.#errorInObject(CBOR.Tag.ERR_DATE_TIME); } this.#object.scan(); return this.#dateTime; } getEpochTime = function() { if (!this.#epochTime) { this.#errorInObject(CBOR.Tag.ERR_EPOCH_TIME); } this.#object.scan(); return this.#epochTime; } #errorInObject = function(message) { CBOR.#error(message + this.toDiag(false)); } encode = function() { return CBOR.addArrays(CBOR.#finishBigIntAndTag(CBOR.#MT_TAG, this.#tagNumber), this.#object.encode()); } internalToString = function(cborPrinter) { cborPrinter.append(this.#tagNumber.toString()); cborPrinter.append('('); this.#object.internalToString(cborPrinter); cborPrinter.append(')'); } getTagNumber = function() { return this.#tagNumber; } update = function(object) { CBOR.#checkArgs(arguments, 1); this._immutableTest(); let previous = this.#object; this.#object = CBOR.#cborArgumentCheck(object); return previous; } get = function() { CBOR.#checkArgs(arguments, 0); this._markAsRead(); return this.#object; } } /////////////////////////// // CBOR.Simple // /////////////////////////// static Simple = class extends CBOR.#CborObject { #value; constructor(value) { super(); this.#value = CBOR.#intCheck(value); if (value < 0 || value > 255 || (value > 23 && value < 32)) { CBOR.#error("Simple value out of range: " + value); } } encode = function() { return CBOR.#encodeTagAndN(CBOR.#MT_SIMPLE, this.#value); } internalToString = function(cborPrinter) { cborPrinter.append('simple(' + this.#value.toString() + ')'); } _get = function() { return this.#value; } } /////////////////////////// // Proxy // /////////////////////////// // The Proxy concept enables checks for invocation by "new" and number of arguments. static #handler = class { constructor(numberOfArguments) { this.numberOfArguments = numberOfArguments; } apply(target, thisArg, argumentsList) { if (argumentsList.length != this.numberOfArguments) { CBOR.#error("CBOR." + target.name + " expects " + this.numberOfArguments + " argument(s)"); } return new target(...argumentsList); } construct(target, args) { CBOR.#error("CBOR." + target.name + " does not permit \"new\""); } } static Int = new Proxy(CBOR.Int, new CBOR.#handler(1)); static BigInt = new Proxy(CBOR.BigInt, new CBOR.#handler(1)); static Float = new Proxy(CBOR.Float, new CBOR.#handler(1)); static String = new Proxy(CBOR.String, new CBOR.#handler(1)); static Bytes = new Proxy(CBOR.Bytes, new CBOR.#handler(1)); static Boolean = new Proxy(CBOR.Boolean, new CBOR.#handler(1)); static Null = new Proxy(CBOR.Null, new CBOR.#handler(0)); static Array = new Proxy(CBOR.Array, new CBOR.#handler(0)); static Map = new Proxy(CBOR.Map, new CBOR.#handler(0)); static Tag = new Proxy(CBOR.Tag, new CBOR.#handler(2)); static Simple = new Proxy(CBOR.Simple, new CBOR.#handler(1)); /////////////////////////// // Decoder Core // /////////////////////////// static get SEQUENCE_MODE() { return 0x1; } static get LENIENT_MAP_DECODING() { return 0x2; } static get LENIENT_NUMBER_DECODING() { return 0x4; } static get REJECT_INVALID_FLOATS() { return 0x8; } static Decoder = class { constructor(cbor, options) { this.cbor = CBOR.#bytesCheck(cbor); this.maxLength = cbor.length; this.byteCount = 0; this.sequenceMode = options & CBOR.SEQUENCE_MODE; this.strictMaps = !(options & CBOR.LENIENT_MAP_DECODING); this.strictNumbers = !(options & CBOR.LENIENT_NUMBER_DECODING); this.rejectNanInfinity = options & CBOR.REJECT_INVALID_FLOATS; } eofError = function() { CBOR.#error("Reading past end of buffer"); } readByte = function() { if (this.byteCount >= this.maxLength) { if (this.sequenceMode && this.atFirstByte) { return 0; } this.eofError(); } this.atFirstByte = false; return this.cbor[this.byteCount++]; } readBytes = function(length) { if (this.byteCount + length > this.maxLength) { this.eofError(); } let result = new Uint8Array(length); let q = -1; while (++q < length) { result[q] = this.cbor[this.byteCount++]; } return result; } unsupportedTag = function(tag) { CBOR.#error("Unsupported tag: " + CBOR.#twoHex(tag)); } rangeLimitedBigInt = function(value) { if (value > 0xffffffffn) { CBOR.#error("Length limited to 0xffffffff"); } return Number(value); } compareAndReturn = function(decoded, f64) { let cborFloat = CBOR.Float(f64); if (this.strictNumbers && cborFloat._compare(decoded)) { CBOR.#error("Non-deterministic encoding of: " + f64); } if (this.rejectNanInfinity && cborFloat._isBadFloat()) { CBOR.#error('"NaN" and "Infinity" support is disabled'); } return cborFloat; } // Interesting algorithm... // 1. Read the F16 byte string. // 2. Convert the F16 byte string to its F64 IEEE-754 equivalent (JavaScript Number). // 3. Create a CBOR.Float object using the F64 Number as input. This causes CBOR.Float to // create an '#encoded' byte string holding the deterministic IEEE-754 representation. // 4. Optionally verify that '#encoded' is equal to the byte string read at step 1. // Maybe not the most performant solution, but hey, this is a "Reference Implementation" :) decompressF16AndReturn = function() { let f64; let decoded = this.readBytes(2); let f16Binary = (decoded[0] << 8) + decoded[1]; let exponent = f16Binary & 0x7c00; let significand = f16Binary & 0x3ff; // Catch the three cases of special/reserved numbers. if (exponent == 0x7c00) { f64 = significand ? Number.NaN : Number.POSITIVE_INFINITY; } else { // It is a genuine number. if (exponent) { // Normal representation, add the implicit "1.". significand += 0x400; // -1: Keep fractional point in line with subnormal numbers. significand *= (1 << ((exponent / 0x400) - 1)); } // Divide with: 2 ^ (Exponent offset + Size of significand - 1). f64 = significand / 0x1000000; } return this.compareAndReturn(decoded, f16Binary >= 0x8000 ? -f64 : f64); } selectInteger = function(value) { if (value > BigInt(Number.MAX_SAFE_INTEGER) || value < BigInt(Number.MIN_SAFE_INTEGER)) { return CBOR.BigInt(value); } return CBOR.Int(Number(value)); } getObject = function() { let tag = this.readByte(); // Begin with CBOR types that are uniquely defined by the tag byte. switch (tag) { case CBOR.#MT_BIG_NEGATIVE: case CBOR.#MT_BIG_UNSIGNED: let byteArray = this.getObject().getBytes(); if (this.strictNumbers && (byteArray.length <= 8 || !byteArray[0])) { CBOR.#error("Non-deterministic bignum encoding"); } let value = 0n; byteArray.forEach(byte => { value <<= 8n; value += BigInt(byte); }); return this.selectInteger(tag == CBOR.#MT_BIG_NEGATIVE ? ~value : value); case CBOR.#MT_FLOAT16: return this.decompressF16AndReturn(); case CBOR.#MT_FLOAT32: let f32bytes = this.readBytes(4); const f32buffer = new ArrayBuffer(4); new Uint8Array(f32buffer).set(f32bytes); return this.compareAndReturn(f32bytes, new DataView(f32buffer).getFloat32(0, false)); case CBOR.#MT_FLOAT64: let f64bytes = this.readBytes(8); const f64buffer = new ArrayBuffer(8); new Uint8Array(f64buffer).set(f64bytes); return this.compareAndReturn(f64bytes, new DataView(f64buffer).getFloat64(0, false)); case CBOR.#MT_NULL: return CBOR.Null(); case CBOR.#MT_TRUE: case CBOR.#MT_FALSE: return CBOR.Boolean(tag == CBOR.#MT_TRUE); } // Then decode CBOR types that blend length of data in the tag byte. let n = tag & 0x1f; let bigN = BigInt(n); if (n > 27) { this.unsupportedTag(tag); } if (n > 23) { // For 1, 2, 4, and 8 byte N. let q = 1 << (n - 24); let mask = 0xffffffffn << BigInt((q >> 1) * 8); bigN = 0n; while (--q >= 0) { bigN <<= 8n; bigN += BigInt(this.readByte()); } // If the upper half (for 2, 4, 8 byte N) of N or a single byte // N is zero, a shorter variant should have been used. // In addition, N must be > 23. if (this.strictNumbers && (bigN < 24n || !(mask & bigN))) { CBOR.#error("Non-deterministic N encoding for tag: 0x" + CBOR.#twoHex(tag)); } } // N successfully decoded, now switch on major type (upper three bits). switch (tag & 0xe0) { case CBOR.#MT_SIMPLE: return CBOR.Simple(this.rangeLimitedBigInt(bigN)); case CBOR.#MT_TAG: return CBOR.Tag(bigN, this.getObject()); case CBOR.#MT_UNSIGNED: return this.selectInteger(bigN); case CBOR.#MT_NEGATIVE: return this.selectInteger(~bigN); case CBOR.#MT_BYTES: return CBOR.Bytes(this.readBytes(this.rangeLimitedBigInt(bigN))); case CBOR.#MT_STRING: return CBOR.String(new TextDecoder('utf-8', {fatal: true}).decode( this.readBytes(this.rangeLimitedBigInt(bigN)))); case CBOR.#MT_ARRAY: let cborArray = CBOR.Array(); for (let q = this.rangeLimitedBigInt(bigN); --q >= 0;) { cborArray.add(this.getObject()); } return cborArray; case CBOR.#MT_MAP: let cborMap = CBOR.Map().setSortingMode(this.strictMaps); for (let q = this.rangeLimitedBigInt(bigN); --q >= 0;) { cborMap.set(this.getObject(), this.getObject()); } // Programmatically added elements sort automatically. return cborMap.setSortingMode(false); default: this.unsupportedTag(tag); } } ////////////////////////////// // Decoder.* public methods // ////////////////////////////// decodeWithOptions = function() { this.atFirstByte = true; let object = this.getObject(); if (this.sequenceMode) { if (this.atFirstByte) { return null; } } else if (this.byteCount < this.maxLength) { CBOR.#error("Unexpected data encountered after CBOR object"); } return object; } getByteCount = function() { return this.byteCount; } } /////////////////////////// // CBOR.decode() // /////////////////////////// static decode = function(cbor) { return CBOR.initDecoder(cbor, 0).decodeWithOptions(); } /////////////////////////// // CBOR.initDecoder() // /////////////////////////// static initDecoder = function(cbor, options) { return new CBOR.Decoder(cbor, options); } //================================// // Diagnostic Notation Support // //================================// static DiagnosticNotation = class { static ParserError = class extends Error { constructor(message) { super(message); } } cborText; index; sequence; constructor(cborText, sequenceMode) { this.cborText = cborText; this.sequenceMode = sequenceMode; this.index = 0; } parserError = function(error) { // Unsurprisingly, error handling turned out to be the most complex part... let start = this.index - 100; if (start < 0) { start = 0; } let linePos = 0; while (start < this.index - 1) { if (this.cborText[start++] == '\n') { linePos = start; } } let complete = ''; if (this.index > 0 && this.cborText[this.index - 1] == '\n') { this.index--; } let endLine = this.index; while (endLine < this.cborText.length) { if (this.cborText[endLine] == '\n') { break; } endLine++; } for (let q = linePos; q < endLine; q++) { complete += this.cborText[q]; } complete += '\n'; for (let q = linePos; q < this.index; q++) { complete += '-'; } let lineNumber = 1; for (let q = 0; q < this.index - 1; q++) { if (this.cborText[q] == '\n') { lineNumber++; } } throw new CBOR.DiagnosticNotation.ParserError("\n" + complete + "^\n\nError in line " + lineNumber + ". " + error); } readSequenceToEOF = function() { try { let sequence = []; this.scanNonSignficantData(); while (this.index < this.cborText.length) { if (sequence.length) { if (this.sequenceMode) { this.scanFor(","); } else { this.readChar(); this.parserError("Unexpected data after token"); } } sequence.push(this.getObject()); } if (!sequence.length && !this.sequenceMode) { this.readChar(); } return sequence; } catch (e) { if (e instanceof CBOR.DiagnosticNotation.ParserError) { throw e; } // The exception apparently came from a deeper layer. // Make it a parser error and remove the original error name. this.parserError(e.toString().replace(/.*Error\: ?/g, '')); } } getObject = function() { this.scanNonSignficantData(); let cborObject = this.getRawObject(); this.scanNonSignficantData(); return cborObject; } continueList = function(validStop) { if (this.nextChar() == ',') { this.readChar(); return true; } this.scanFor(validStop); this.index--; return false; } getRawObject = function() { switch (this.readChar()) { case '<': this.scanFor("<"); let sequence = new Uint8Array(); this.scanNonSignficantData(); while (this.readChar() != '>') { this.index--; do { sequence = CBOR.addArrays(sequence, this.getObject().encode()); } while (this.continueList('>')); } this.scanFor(">"); return CBOR.Bytes(sequence); case '[': let array = CBOR.Array(); this.scanNonSignficantData(); while (this.readChar() != ']') { this.index--; do { array.add(this.getObject()); } while (this.continueList(']')); } return array; case '{': let map = CBOR.Map(); this.scanNonSignficantData(); while (this.readChar() != '}') { this.index--; do { let key = this.getObject(); this.scanFor(":"); map.set(key, this.getObject()); } while (this.continueList('}')); } return map; case '\'': return this.getString(true); case '"': return this.getString(false); case 'h': return this.getBytes(false); case 'b': if (this.nextChar() == '3') { this.scanFor("32'"); this.parserError("b32 not implemented"); } this.scanFor("64"); return this.getBytes(true); case 't': this.scanFor("rue"); return CBOR.Boolean(true); case 'f': this.scanFor("alse"); return CBOR.Boolean(false); case 'n': this.scanFor("ull"); return CBOR.Null(); case 's': this.scanFor("imple("); return this.simpleType(); case '-': if (this.readChar() == 'I') { this.scanFor("nfinity"); return CBOR.Float(Number.NEGATIVE_INFINITY); } return this.getNumberOrTag(true); case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': return this.getNumberOrTag(false); case 'N': this.scanFor("aN"); return CBOR.Float(Number.NaN); case 'I': this.scanFor("nfinity"); return CBOR.Float(Number.POSITIVE_INFINITY); default: this.index--; this.parserError("Unexpected character: " + this.toChar(this.readChar())); } } simpleType = function() { let token = ''; while (true) { switch (this.nextChar()) { case ')': break; case '+': case '-': case 'e': case '.': this.parserError("Syntax error"); default: token += this.readChar(); continue; } break; } this.readChar(); return CBOR.Simple(Number(token.trim())).clone(); } getNumberOrTag = function(negative) { let token = ''; this.index--; let prefix = null; if (this.readChar() == '0') { switch (this.nextChar()) { case 'b': case 'o': case 'x': prefix = '0' + this.readChar(); break; } } if (prefix == null) { this.index--; } let floatingPoint = false; while (true) { token += this.readChar(); switch (this.nextChar()) { case '\u0000': case ' ': case '\n': case '\r': case '\t': case ',': case ':': case '>': case ']': case '}': case '/': case '#': case '(': case ')': break; case '.': case 'e': if (!prefix) { floatingPoint = true; } continue; case '_': if (!prefix) { this.parserError("'_' is only permitted for 0b, 0o, and 0x numbers"); } this.readChar(); default: continue; } break; } if (floatingPoint) { this.testForNonDecimal(prefix); let value = Number(token); // Implicit overflow is not permitted if (!Number.isFinite(value)) { this.parserError("Floating point value out of range"); } return CBOR.Float(negative ? -value : value); } if (this.nextChar() == '(') { // Do not accept '-', 0xhhh, or leading zeros this.testForNonDecimal(prefix); if (negative || (token.length > 1 && token.charAt(0) == '0')) { this.parserError("Tag syntax error"); } this.readChar(); let tagNumber = BigInt(token); let cborTag = CBOR.Tag(tagNumber, this.getObject()); this.scanFor(")"); return cborTag; } let bigInt = BigInt((prefix == null ? '' : prefix) + token); // Clone: slight quirk to get the optimal CBOR integer type return CBOR.BigInt(negative ? -bigInt : bigInt).clone(); } testForNonDecimal = function(nonDecimal) { if (nonDecimal) { this.parserError("0b, 0o, and 0x prefixes are only permited for integers"); } } nextChar = function() { if (this.index == this.cborText.length) return String.fromCharCode(0); let c = this.readChar(); this.index--; return c; } toChar = function(c) { let charCode = c.charCodeAt(0); return charCode < 0x20 ? "\\u00" + CBOR.#twoHex(charCode) : "'" + c + "'"; } scanFor = function(expected) { [...expected].forEach(c => { let actual = this.readChar(); if (c != actual) { this.parserError("Expected: '" + c + "' actual: " + this.toChar(actual)); } }); } getString = function(byteString) { let s = ''; while (true) { let c; switch (c = this.readChar()) { // Control character handling case '\r': if (this.nextChar() == '\n') { continue; } c = '\n'; break; case '\n': case '\t': break; case '\\': switch (c = this.readChar()) { case '\n': continue; case '\'': case '"': case '\\': break; case 'b': c = '\b'; break; case 'f': c = '\f'; break; case 'n': c = '\n'; break; case 'r': c = '\r'; break; case 't': c = '\t'; break; case 'u': let u16 = 0; for (let i = 0; i < 4; i++) { u16 = (u16 << 4) + CBOR.#decodeOneHex(this.readChar().charCodeAt(0)); } c = String.fromCharCode(u16); break; default: this.parserError("Invalid escape character " + this.toChar(c)); } break; case '"': if (!byteString) { return CBOR.String(s); } break; case '\'': if (byteString) { return CBOR.Bytes(new TextEncoder().encode(s)); } break; default: if (c.charCodeAt(0) < 0x20) { this.parserError("Unexpected control character: " + this.toChar(c)); } } s += c; } } getBytes = function(b64) { let token = ''; this.scanFor("'"); while(true) { let c; switch (c = this.readChar()) { case '\'': break; case ' ': case '\r': case '\n':