cbor-object
Version:
CBOR: deterministic encoder/decoder, diagnostic notation encoder/decoder, and utilities
1,793 lines (1,526 loc) • 69.1 kB
JavaScript
//////////////////////////////////////////////////////////////////
// //
// CBOR JavaScript API //
// //
// Author: Anders Rundgren (anders.rundgren.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;
}
#rangeBigInt(min, max) {
let value = this.getBigInt();
CBOR.#rangeCheck(value, min, max);
return value;
}
#rangeNumber(min, max) {
let value = this.#checkTypeAndGetValue(CBOR.Int);
CBOR.#rangeCheck(value, min, max);
return Number(value);
}
getInt8() {
return this.#rangeNumber(-0x80n, 0x7fn);
}
getUint8() {
return this.#rangeNumber(0n, 0xffn);
}
getInt16() {
return this.#rangeNumber(-0x8000n, 0x7fffn);
}
getUint16() {
return this.#rangeNumber(0n, 0xffffn);
}
getInt32() {
return this.#rangeNumber(-0x80000000n, 0x7fffffffn);
}
getUint32() {
return this.#rangeNumber(0n, 0xffffffffn);
}
getInt53() {
return this.#rangeNumber(-9007199254740991n, 9007199254740991n);
}
getInt64() {
return this.#rangeBigInt(-0x8000000000000000n, 0x7fffffffffffffffn);
}
getUint64() {
return this.#rangeBigInt(0n, 0xffffffffffffffffn);
}
getInt128() {
return this.#rangeBigInt(-0x80000000000000000000000000000000n,
0x7fffffffffffffffffffffffffffffffn);
}
getUint128() {
return this.#rangeBigInt(0n, 0xffffffffffffffffffffffffffffffffn);
}
getBigInt() {
if (this instanceof CBOR.Int) {
return this.#checkTypeAndGetValue(CBOR.Int);
}
return this.#checkTypeAndGetValue(CBOR.BigInt);
}
getString() {
return this.#checkTypeAndGetValue(CBOR.String);
}
getDateTime() {
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
// Truncates sub-milliseconds as well.
if (iso.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(\.\d{1,9})?((\-|\+)\d{2}:\d{2}|Z)$/m)) {
let dateTime = new Date(iso);
let time = dateTime.getTime();
if (Number.isFinite(time)) {
CBOR.#dateCheck(time, dateTime);
return dateTime;
}
}
CBOR.#error("Invalid ISO date string: " + iso);
}
getEpochTime() {
let epochMillis =
CBOR.#epochCheck((this instanceof CBOR.Int ? this.getInt53() : this.getFloat64()) * 1000);
let epochTime = new Date();
epochTime.setTime(Math.floor(epochMillis + 0.5));
return epochTime;
}
getBytes() {
return this.#checkTypeAndGetValue(CBOR.Bytes);
}
#rangeFloat(max) {
let value = this.getFloat64();
if (this.length > max) {
CBOR.#rangeError('Float' + (max * 8), this);
}
return value;
}
getFloat16() {
return this.#rangeFloat(2);
}
getFloat32() {
return this.#rangeFloat(4);
}
getFloat64() {
return this.#checkTypeAndGetValue(CBOR.Float);
}
getNonFinite64() {
return this.#checkTypeAndGetValue(CBOR.NonFinite);
}
getExtendedFloat64() {
if (this instanceof CBOR.NonFinite) {
switch (this.getNonFinite()) {
case 0x7e00n:
return Number.NaN;
case 0x7c00n:
return Number.POSITIVE_INFINITY;
case 0xfc00n:
return Number.NEGATIVE_INFINITY;
default:
CBOR.#error('getExtendedFloat64() only supports "simple" NaN (7e00)');
}
}
return this.getFloat64();
}
getBoolean() {
return this.#checkTypeAndGetValue(CBOR.Boolean);
}
isNull() {
if (this instanceof CBOR.Null) {
this.#readFlag = true;
return true;
}
return false;
}
getSimple() {
return this.#checkTypeAndGetValue(CBOR.Simple);
}
equals(object) {
if (object && object instanceof CBOR.#CborObject) {
return CBOR.compareArrays(this.encode(), object.encode()) == 0;
}
return false;
}
clone() {
return CBOR.decode(this.encode());
}
#noSuchMethod(method) {
CBOR.#error(method + '() not available in: CBOR.' + this.constructor.name);
}
get() {
this.#noSuchMethod("get");
}
toDiagnostic(prettyPrint) {
let cborPrinter = new CBOR.#CborPrinter(CBOR.#typeCheck(prettyPrint, 'boolean'));
this.internalToString(cborPrinter);
return cborPrinter.buffer;
}
toString() {
return this.toDiagnostic(true);
}
_immutableTest() {
if (this._immutableFlag) {
CBOR.#error('Map keys are immutable');
}
}
_markAsRead() {
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.toDiagnostic(false) + " with argument") +
" of type=CBOR." + this.constructor.name +
" with value=" + this.toDiagnostic(false) + " was never read");
}
} else {
this.#readFlag = true;
}
}
scan() {
this.#traverse(null, false);
return this;
}
checkForUnread() {
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(className) {
if (!(this instanceof className)) {
CBOR.#error("Expected CBOR." + className.name + ", got: CBOR." + this.constructor.name);
}
this.#readFlag = true;
return this._get();
}
}
static CborException = class extends Error {
constructor(message) {
super(message);
}
}
static #error(message) {
if (message.length > 100) {
message = message.substring(0, 100) + ' ...';
}
throw new CBOR.CborException(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 , '\\'];
static #F64_NAN = new Uint8Array([0x7f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
static #MIN_INT = -0x8000000000000000n;
static #MAX_INT = 0xffffffffffffffffn;
constructor() {
CBOR.#error("CBOR cannot be instantiated");
}
///////////////////////////
// CBOR.Int //
///////////////////////////
static Int = class extends CBOR.#CborObject {
#value;
constructor(value) {
super();
this.#value = CBOR.#unifiedInt(value);
if (this.#value < CBOR.#MIN_INT || this.#value > CBOR.#MAX_INT) {
CBOR.#error("Value is out of \"CBOR.Int\" range: " + this.#value);
}
}
encode() {
return CBOR.#encodeIntegerOrTag(CBOR.#MT_UNSIGNED, this.#value);
}
internalToString(cborPrinter) {
cborPrinter.append(this.#value.toString());
}
_get() {
return this.#value;
}
static createInt8(value) {
return CBOR.#createInt(value, -0x80n, 0x7fn);
}
static createUint8(value) {
return CBOR.#createInt(value, 0n, 0xffn);
}
static createInt16(value) {
return CBOR.#createInt(value, -0x8000n, 0x7fffn);
}
static createUint16(value) {
return CBOR.#createInt(value, 0n, 0xffffn);
}
static createInt32(value) {
return CBOR.#createInt(value, -0x80000000n, 0x7fffffffn);
}
static createUint32(value) {
return CBOR.#createInt(value, 0n, 0xffffffffn);
}
static createInt53(value) {
return CBOR.#createInt(value, -9007199254740991n, 9007199254740991n);
}
static createInt64(value) {
return CBOR.#createInt(value, -0x8000000000000000n, 0x7fffffffffffffffn);
}
static createUint64(value) {
return CBOR.#createInt(value, 0n, 0xffffffffffffffffn);
}
}
///////////////////////////
// CBOR.BigInt //
///////////////////////////
static BigInt = class extends CBOR.#CborObject {
#value;
constructor(value) {
super();
this.#value = CBOR.#unifiedInt(value);
}
encode() {
return CBOR.#encodeIntegerOrTag(CBOR.#MT_UNSIGNED, this.#value);
}
internalToString(cborPrinter) {
cborPrinter.append(this.#value.toString());
}
_get() {
return this.#value;
}
static createInt128(value) {
return CBOR.#createBigInt(value,
-0x80000000000000000000000000000000n, 0x7fffffffffffffffffffffffffffffffn);
}
static createUint128(value) {
return CBOR.#createBigInt(value, 0n, 0xffffffffffffffffffffffffffffffffn);
}
}
///////////////////////////
// CBOR.Float //
///////////////////////////
static Float = class extends CBOR.#CborObject {
#value;
#encoded;
constructor(value) {
super();
this.#value = CBOR.#typeCheck(value, 'number');
// Get the full F64 binary.
const f64b = new Uint8Array(8);
new DataView(f64b.buffer, 0, 8).setFloat64(0, value, false);
// Begin catching the forbidden cases.
if (!Number.isFinite(value)) {
CBOR.#error("Not permitted: 'NaN/Infinity'");
}
if (value == 0) { // True for -0.0 as well!
this.#encoded = f64b.slice(0, 2);
} else {
// It is apparently a regular (non-zero) number.
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 = ((f64b[0] & 0x7f) << 4) + (f64b[1] >> 4) - 0x380;
f32signif = ((f64b[1] & 0x0f) << 19) + (f64b[2] << 11) + (f64b[3] << 3) + (f64b[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.
((f64b[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.#encoded = f64b;
}
// Common F16 and F64 return point.
return;
}
// Broken loop: 32 bits are apparently needed for maintaining magnitude and precision.
let f32bin =
// Put sign bit in position. Why not << 24? JS shift doesn't work above 2^31...
((f64b[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() {
return CBOR.addArrays(new Uint8Array([(this.#encoded.length >> 2) + CBOR.#MT_FLOAT16]),
this.#encoded);
}
internalToString(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);
}
_compare(decoded) {
return CBOR.compareArrays(this.#encoded, decoded);
}
_getLength() {
return this.#encoded.length;
}
_get() {
return this.#value;
}
static createExtendedFloat(value) {
if (Number.isFinite(CBOR.#typeCheck(value, 'number'))) {
return CBOR.Float(value);
}
let f64b = new Uint8Array(8);
new DataView(f64b.buffer, 0, 8).setFloat64(0, value, false);
let nf = CBOR.NonFinite(CBOR.toBigInt(f64b));
if (!nf.isSimple()) {
CBOR.#error("createExtendedFloat() does not support NaN with payloads");
}
return nf;
}
static createFloat32(value) {
return CBOR.#returnConverted(CBOR.#reduce32(value), value, 'Float32');
}
static createFloat16(value) {
return CBOR.#returnConverted(Math.f16round(CBOR.#reduce32(value)), value, 'Float16');
}
}
///////////////////////////
// CBOR.String //
///////////////////////////
static String = class extends CBOR.#CborObject {
#textString;
constructor(textString) {
super();
this.#textString = CBOR.#typeCheck(textString, 'string');
}
encode() {
let utf8 = new TextEncoder().encode(this.#textString);
return CBOR.addArrays(CBOR.#encodeTagAndN(CBOR.#MT_STRING, utf8.length), utf8);
}
internalToString(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').append(CBOR.#twoHex(c));
} else {
cborPrinter.append(escapedCharacter);
}
continue;
}
}
cborPrinter.append(String.fromCharCode(c));
}
cborPrinter.append('"');
}
_get() {
return this.#textString;
}
}
///////////////////////////
// CBOR.Bytes //
///////////////////////////
static Bytes = class extends CBOR.#CborObject {
#byteString;
constructor(byteString) {
super();
this.#byteString = CBOR.#bytesCheck(byteString);
}
encode() {
return CBOR.addArrays(CBOR.#encodeTagAndN(CBOR.#MT_BYTES, this.#byteString.length),
this.#byteString);
}
internalToString(cborPrinter) {
cborPrinter.append("h'" + CBOR.toHex(this.#byteString) + "'");
}
_get() {
return this.#byteString;
}
}
///////////////////////////
// CBOR.Boolean //
///////////////////////////
static Boolean = class extends CBOR.#CborObject {
#value;
constructor(value) {
super();
this.#value = CBOR.#typeCheck(value, 'boolean');
}
encode() {
return new Uint8Array([this.#value ? CBOR.#MT_TRUE : CBOR.#MT_FALSE]);
}
internalToString(cborPrinter) {
cborPrinter.append(this.#value.toString());
}
_get() {
return this.#value;
}
}
///////////////////////////
// CBOR.Null //
///////////////////////////
static Null = class extends CBOR.#CborObject {
encode() {
return new Uint8Array([CBOR.#MT_NULL]);
}
internalToString(cborPrinter) {
cborPrinter.append('null');
}
}
///////////////////////////
// CBOR.Array //
///////////////////////////
static Array = class extends CBOR.#CborObject {
#objects = [];
add(object) {
CBOR.#checkArgs(arguments, 1);
this._immutableTest();
this.#objects.push(CBOR.#cborArgumentCheck(object));
return this;
}
#getIndex(index, offset) {
index = CBOR.#intCheck(index);
if (index < 0 || index >= this.#objects.length + offset) {
CBOR.#error("Array index out of range: " + index);
}
return index;
}
get(index) {
CBOR.#checkArgs(arguments, 1);
this._markAsRead();
return this.#objects[this.#getIndex(index, 0)];
}
insert(index, object) {
CBOR.#checkArgs(arguments, 2);
this._immutableTest();
this.#objects.splice(this.#getIndex(index, 1), 0, CBOR.#cborArgumentCheck(object));
return this;
}
update(index, object) {
CBOR.#checkArgs(arguments, 2);
this._immutableTest();
return this.#objects.splice(this.#getIndex(index, 0), 1, CBOR.#cborArgumentCheck(object))[0];
}
remove(index) {
CBOR.#checkArgs(arguments, 1);
this._immutableTest();
return this.#objects.splice(this.#getIndex(index, 0), 1)[0];
}
toArray() {
let array = [];
this.#objects.forEach(object => array.push(object));
return array;
}
#encodeBody(header) {
this.#objects.forEach(object => {
header = CBOR.addArrays(header, object.encode());
});
return header;
}
encodeAsSequence() {
return this.#encodeBody(new Uint8Array());
}
encode() {
return this.#encodeBody(CBOR.#encodeTagAndN(CBOR.#MT_ARRAY, this.#objects.length));
}
internalToString(cborPrinter) {
if (cborPrinter.arrayFolding(this)) {
cborPrinter.beginList('[');
let notFirst = false;
this.#objects.forEach(object => {
if (notFirst) {
cborPrinter.append(',');
}
notFirst = true;
cborPrinter.newlineAndIndent();
object.internalToString(cborPrinter);
});
cborPrinter.endList(notFirst, ']');
} else {
cborPrinter.append('[');
let notFirst = false;
this.#objects.forEach(object => {
if (notFirst) {
cborPrinter.append(',').space();
}
notFirst = true;
object.internalToString(cborPrinter);
});
cborPrinter.append(']');
}
}
_getLength() {
return this.#objects.length;
}
}
///////////////////////////
// CBOR.Map //
///////////////////////////
static Map = class extends CBOR.#CborObject {
#entries = [];
#preSortedKeys = false;
#lastLookup = 0;
static Entry = class {
constructor(key, object) {
this.key = key;
this.object = object;
this.encodedKey = key.encode();
}
compare(encodedKey) {
return CBOR.compareArrays(this.encodedKey, encodedKey);
}
compareAndTest(entry) {
let diff = this.compare(entry.encodedKey);
if (diff == 0) {
CBOR.#error("Duplicate key: " + this.key);
}
return diff > 0;
}
}
set(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(dynamic) {
return dynamic(this);
}
#getKey(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) {
this.#lastLookup = midIndex;
return entry;
}
if (diff < 0) {
startIndex = midIndex + 1;
} else {
endIndex = midIndex - 1;
}
}
if (mustExist) {
CBOR.#error("Missing key: " + key);
}
return null;
}
update(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(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(key) {
CBOR.#checkArgs(arguments, 1);
this._markAsRead();
return this.#lookup(key, true).object;
}
getConditionally(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() {
let keys = [];
this.#entries.forEach(entry => {
keys.push(entry.key);
});
return keys;
}
remove(key) {
CBOR.#checkArgs(arguments, 1);
this._immutableTest();
let targetEntry = this.#lookup(key, true);
this.#entries.splice(this.#lastLookup, 1);
return targetEntry.object;
}
_getLength() {
return this.#entries.length;
}
containsKey(key) {
CBOR.#checkArgs(arguments, 1);
return this.#lookup(key, false) != null;
}
encode() {
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(cborPrinter) {
let notFirst = false;
cborPrinter.beginList('{');
this.#entries.forEach(entry => {
if (notFirst) {
cborPrinter.append(',');
}
notFirst = true;
cborPrinter.newlineAndIndent();
entry.key.internalToString(cborPrinter);
cborPrinter.append(':').space();
entry.object.internalToString(cborPrinter);
});
cborPrinter.endList(notFirst, '}');
}
setSortingMode(preSortedKeys) {
CBOR.#checkArgs(arguments, 1);
this.#preSortedKeys = preSortedKeys;
return this;
}
#makeImmutable(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;
#cotxId;
#cotxObject;
constructor(tagNumber, object) {
super();
this.#tagNumber = CBOR.#unifiedInt(tagNumber);
this.#object = CBOR.#cborArgumentCheck(object);
if (this.#tagNumber < 0n || this.#tagNumber >= 0x10000000000000000n) {
CBOR.#error("Tag number is out of range");
}
switch (this.#tagNumber) {
case CBOR.Tag.TAG_BIGINT_POS:
case CBOR.Tag.TAG_BIGINT_NEG:
CBOR.#error("Tag number reserved for 'bigint'");
case CBOR.Tag.TAG_DATE_TIME:
// Note: clone() because we have mot read it really.
this.#dateTime = object.clone().getDateTime();
break;
case CBOR.Tag.TAG_EPOCH_TIME:
// Note: clone() because we have mot read it really.
this.#epochTime = object.clone().getEpochTime();
break;
case CBOR.Tag.TAG_COTX:
if (!(object instanceof CBOR.Array) || object.length != 2) {
this.#errorInObject(CBOR.Tag.ERR_COTX);
}
this.#cotxId = object.get(0).getString();
this.#cotxObject = object.get(1);
}
}
getDateTime() {
if (!this.#dateTime) {
this.#errorInObject(CBOR.Tag.ERR_DATE_TIME);
}
this.#object.scan();
return this.#dateTime;
}
getEpochTime() {
if (!this.#epochTime) {
this.#errorInObject(CBOR.Tag.ERR_EPOCH_TIME);
}
this.#object.scan();
return this.#epochTime;
}
#errorInObject(message) {
CBOR.#error(message + this.toDiagnostic(false));
}
encode() {
return CBOR.addArrays(CBOR.#encodeIntegerOrTag(CBOR.#MT_TAG, this.#tagNumber),
this.#object.encode());
}
internalToString(cborPrinter) {
cborPrinter.append(this.#tagNumber.toString()).append('(');
if (this.#cotxObject == null) {
this.#object.internalToString(cborPrinter);
} else {
cborPrinter.append('[');
this.#object.get(0).internalToString(cborPrinter);
cborPrinter.append(',').space();
this.#object.get(1).internalToString(cborPrinter);
cborPrinter.append(']');
}
cborPrinter.append(')');
}
getTagNumber() {
return this.#tagNumber;
}
get() {
CBOR.#checkArgs(arguments, 0);
this._markAsRead();
return this.#object;
}
_checkCotx() {
if (!this.#cotxObject) {
this.#errorInObject(CBOR.Tag.ERR_COTX);
}
}
get cotxId() {
this._checkCotx();
return this.#cotxId;
}
get cotxObject() {
return this.#cotxObject;
}
}
///////////////////////////
// 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() {
return CBOR.#encodeTagAndN(CBOR.#MT_SIMPLE, this.#value);
}
internalToString(cborPrinter) {
cborPrinter.append('simple(' + this.#value.toString() + ')');
}
_get() {
return this.#value;
}
}
///////////////////////////
// CBOR.NonFinite //
///////////////////////////
static NonFinite = class extends CBOR.#CborObject {
#value;
#original;
#ieee754;
constructor(value) {
super();
this.#original = CBOR.#typeCheck(value, 'bigint');
this.#createDetEnc(value);
}
#createDetEnc(value) {
badValue:
while (true) {
this.#ieee754 = CBOR.fromBigInt(value);
let exponent;
switch (this.#ieee754.length) {
case 2:
exponent = 0x7c00n;
break;
case 4:
exponent = 0x7f800000n;
break;
case 8:
exponent = 0x7ff0000000000000n;
break;
default:
break badValue;
}
let sign = this.#ieee754[0] > 0x7f;
if ((value & exponent) != exponent) break badValue;
switch (this.#ieee754.length) {
case 4:
if (value & ((1n << 13n) - 1n)) break;
value >>= 13n;
value &= 0x7fffn;
if (sign) {
value |= 0x8000n;
}
continue;
case 8:
if (value & ((1n << 29n) - 1n)) break;
value >>= 29n;
value &= 0x7fffffffn;
if (sign) {
value |= 0x80000000n;
}
continue;
}
this.#value = value;
return;
}
CBOR.#error("Not a non-finite number: " + this.#original);
}
isSimple() {
if (this.#ieee754.length == 2) {
switch (this.#value) {
case 0x7e00n:
case 0x7c00n:
case 0xfc00n:
return true;
}
}
return false;
}
setSign(sign) {
let mask = 1n << BigInt((this.#ieee754.length * 8) - 1);
this.#createDetEnc((this.#value & (mask - 1n)) | (sign ? mask : 0n));
return this;
}
isNaN() {
let mask;
switch (this.#ieee754.length) {
case 2:
mask = 0x3ffn;
break;
case 4:
mask = 0x7fffffn;
break;
default:
mask = 0xfffffffffffffn;
}
return (mask & this.#value) != 0n;
}
getSign() {
return this.#ieee754[0] > 0x7f;
}
static createPayload(payload) {
CBOR.#typeCheck(payload, 'bigint');
if ((payload & 0xfffffffffffffn) != payload) {
CBOR.#error("Payload out of range: " + payload);
}
return CBOR.NonFinite(0x7ff0000000000000n + CBOR.#reverseBits(payload, 52));
}
getNonFinite() {
this.scan();
return this.#value;
}
#toNonFinite64(significandLength) {
let nf64 = this.#value;
nf64 &= (1n << significandLength) - 1n;
nf64 = 0x7ff0000000000000n | (nf64 << (52n - significandLength));
if (this.getSign()) {
nf64 |= 0x8000000000000000n;
}
return nf64;
}
getPayload() {
return CBOR.#reverseBits(this.getNonFinite64() & 0xfffffffffffffn, 52);
}
encode() {
return CBOR.addArrays(new Uint8Array([0xf9 + (this.#ieee754.length >> 2)]), this.#ieee754);
}
internalToString(cborPrinter) {
if (this.isSimple()) {
cborPrinter.append(this.isNaN() ? "NaN" : this.getSign() ? "-Infinity" : "Infinity");
} else {
cborPrinter.append("float'").append(CBOR.toHex(this.#ieee754)).append("'");
}
}
_getLength() {
return this.#ieee754.length;
}
_get() {
switch (this.#ieee754.length) {
case 2:
return this.#toNonFinite64(10n);
case 4:
return this.#toNonFinite64(23n);
}
return this.#value;
}
_getValue() {
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));
static NonFinite = new Proxy(CBOR.NonFinite, 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 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);
}
eofError() {
CBOR.#error("Reading past end of buffer");
}
readByte() {
if (this.byteCount >= this.maxLength) {
if (this.sequenceMode && this.atFirstByte) {
return 0;
}
this.eofError();
}
this.atFirstByte = false;
return this.cbor[this.byteCount++];
}
readBytes(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(tag) {
CBOR.#error("Unsupported tag: " + CBOR.#twoHex(tag));
}
rangeLimitedBigInt(value) {
if (value > 0xffffffffn) {
CBOR.#error("Length limited to 0xffffffff");
}
return Number(value);
}
printFloatDetErr(decoded) {
CBOR.#error("Non-deterministic encoding of floating-point value: " +
CBOR.#twoHex((decoded.length >> 2) + CBOR.#MT_FLOAT16) +
CBOR.toHex(decoded));
}
returnFloat(decoded, f64) {
let cborFloat = CBOR.Float(f64);
if (this.strictNumbers && cborFloat._compare(decoded)) {
this.printFloatDetErr(decoded);
}
return cborFloat;
}
returnNonFinite (decoded) {
let value = CBOR.toBigInt(decoded);
let nonFinite = CBOR.NonFinite(value);
if (this.strictNumbers && nonFinite._getValue() != value) {
this.printFloatDetErr(decoded);
}
return nonFinite;
}
// 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" :)
decodeF16() {
let decoded = this.readBytes(2);
let value = CBOR.toBigInt(decoded);
let exponent = Number(value & 0x7c00n);
let significand = Number(value & 0x3ffn);
// Is it a non-finite number?
if (exponent == 0x7c00) {
// Yes, deal with it separately.
return this.returnNonFinite(decoded);
}
// It is a "regular" 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).
let f64 = significand / 0x1000000;
return this.returnFloat(decoded, decoded[0] < 128 ? f64 : -f64);
}
decodeF32() {
let decoded = this.readBytes(4);
let value = CBOR.toBigInt(decoded);
// Is it a non-finite number?
if ((decoded[0] & 0x7f) == 0x7f && (decoded[1] & 0x80) == 0x80) {
// Yes, deal with it separately.
return this.returnNonFinite(decoded);
}
// It is a "regular" number.
return this.returnFloat(decoded, new DataView(decoded.buffer, 0, 4).getFloat32(0, false));
}
decodeF64() {
let decoded = this.readBytes(8);
// Is it a non-finite number?
if ((decoded[0] & 0x7f) == 0x7f && (decoded[1] & 0xf0) == 0xf0) {
// Yes, deal with it separately.
return this.returnNonFinite(decoded);
}
// It is a "regular" number.
return this.returnFloat(decoded, new DataView(decoded.buffer, 0, 8).getFloat64(0, false));
}
selectInteger(value) {
if (value < CBOR.#MIN_INT || value > CBOR.#MAX_INT) {
return CBOR.BigInt(value);
}
return CBOR.Int(value);
}
getObject() {
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 = CBOR.toBigInt(byteArray);
return this.selectInteger(tag == CBOR.#MT_BIG_NEGATIVE ? ~value : value);
case CBOR.#MT_FLOAT16:
return this.decodeF16();
case CBOR.#MT_FLOAT32:
return this.decodeF32();
case CBOR.#MT_FLOAT64:
return this.decodeF64();
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() {
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() {
return this.byteCount;
}
}
///////////////////////////
// CBOR.decode() //
///////////////////////////
static decode(cbor) {
return CBOR.initDecoder(cbor, 0).decodeWithOptions();
}
///////////////////////////
// CBOR.initDecoder() //
///////////////////////////
static initDecoder(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(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() {
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() {
this.scanNonSignficantData();
let cborObject = this.getRawObject();
this.scanNonSignficantData();
return cborObject;
}
continueList(validStop) {
if (this.nextChar() == ',') {
this.readChar();
return true;
}
let actual = this.readChar();
if (actual != validStop) {
this.parserError(
"Expected: ',' or '" + validStop + "' actual: " + this.toReadableChar(actual));
}
this.index--;
return false;
}
getRawObject() {
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':
if (this.nextChar() == 'a') {
this.scanFor("alse");
return CBOR.Boolean(false);
}
this.scanFor('loat');
let floatBytes = this.getBytes(false).getBytes();
switch (f