246 lines
7.1 KiB
JavaScript
246 lines
7.1 KiB
JavaScript
|
|
import { is } from './is.js';
|
||
|
|
import {
|
||
|
|
Token,
|
||
|
|
Type
|
||
|
|
} from './token.js';
|
||
|
|
import { Bl } from './bl.js';
|
||
|
|
import { encodeErrPrefix } from './common.js';
|
||
|
|
import { quickEncodeToken } from './jump.js';
|
||
|
|
import { asU8A } from './byte-utils.js';
|
||
|
|
import { encodeUint } from './0uint.js';
|
||
|
|
import { encodeNegint } from './1negint.js';
|
||
|
|
import { encodeBytes } from './2bytes.js';
|
||
|
|
import { encodeString } from './3string.js';
|
||
|
|
import { encodeArray } from './4array.js';
|
||
|
|
import { encodeMap } from './5map.js';
|
||
|
|
import { encodeTag } from './6tag.js';
|
||
|
|
import { encodeFloat } from './7float.js';
|
||
|
|
const defaultEncodeOptions = {
|
||
|
|
float64: false,
|
||
|
|
mapSorter,
|
||
|
|
quickEncodeToken
|
||
|
|
};
|
||
|
|
export function makeCborEncoders() {
|
||
|
|
const encoders = [];
|
||
|
|
encoders[Type.uint.major] = encodeUint;
|
||
|
|
encoders[Type.negint.major] = encodeNegint;
|
||
|
|
encoders[Type.bytes.major] = encodeBytes;
|
||
|
|
encoders[Type.string.major] = encodeString;
|
||
|
|
encoders[Type.array.major] = encodeArray;
|
||
|
|
encoders[Type.map.major] = encodeMap;
|
||
|
|
encoders[Type.tag.major] = encodeTag;
|
||
|
|
encoders[Type.float.major] = encodeFloat;
|
||
|
|
return encoders;
|
||
|
|
}
|
||
|
|
const cborEncoders = makeCborEncoders();
|
||
|
|
const buf = new Bl();
|
||
|
|
class Ref {
|
||
|
|
constructor(obj, parent) {
|
||
|
|
this.obj = obj;
|
||
|
|
this.parent = parent;
|
||
|
|
}
|
||
|
|
includes(obj) {
|
||
|
|
let p = this;
|
||
|
|
do {
|
||
|
|
if (p.obj === obj) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
} while (p = p.parent);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
static createCheck(stack, obj) {
|
||
|
|
if (stack && stack.includes(obj)) {
|
||
|
|
throw new Error(`${ encodeErrPrefix } object contains circular references`);
|
||
|
|
}
|
||
|
|
return new Ref(obj, stack);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const simpleTokens = {
|
||
|
|
null: new Token(Type.null, null),
|
||
|
|
undefined: new Token(Type.undefined, undefined),
|
||
|
|
true: new Token(Type.true, true),
|
||
|
|
false: new Token(Type.false, false),
|
||
|
|
emptyArray: new Token(Type.array, 0),
|
||
|
|
emptyMap: new Token(Type.map, 0)
|
||
|
|
};
|
||
|
|
const typeEncoders = {
|
||
|
|
number(obj, _typ, _options, _refStack) {
|
||
|
|
if (!Number.isInteger(obj) || !Number.isSafeInteger(obj)) {
|
||
|
|
return new Token(Type.float, obj);
|
||
|
|
} else if (obj >= 0) {
|
||
|
|
return new Token(Type.uint, obj);
|
||
|
|
} else {
|
||
|
|
return new Token(Type.negint, obj);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
bigint(obj, _typ, _options, _refStack) {
|
||
|
|
if (obj >= BigInt(0)) {
|
||
|
|
return new Token(Type.uint, obj);
|
||
|
|
} else {
|
||
|
|
return new Token(Type.negint, obj);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
Uint8Array(obj, _typ, _options, _refStack) {
|
||
|
|
return new Token(Type.bytes, obj);
|
||
|
|
},
|
||
|
|
string(obj, _typ, _options, _refStack) {
|
||
|
|
return new Token(Type.string, obj);
|
||
|
|
},
|
||
|
|
boolean(obj, _typ, _options, _refStack) {
|
||
|
|
return obj ? simpleTokens.true : simpleTokens.false;
|
||
|
|
},
|
||
|
|
null(_obj, _typ, _options, _refStack) {
|
||
|
|
return simpleTokens.null;
|
||
|
|
},
|
||
|
|
undefined(_obj, _typ, _options, _refStack) {
|
||
|
|
return simpleTokens.undefined;
|
||
|
|
},
|
||
|
|
ArrayBuffer(obj, _typ, _options, _refStack) {
|
||
|
|
return new Token(Type.bytes, new Uint8Array(obj));
|
||
|
|
},
|
||
|
|
DataView(obj, _typ, _options, _refStack) {
|
||
|
|
return new Token(Type.bytes, new Uint8Array(obj.buffer, obj.byteOffset, obj.byteLength));
|
||
|
|
},
|
||
|
|
Array(obj, _typ, options, refStack) {
|
||
|
|
if (!obj.length) {
|
||
|
|
if (options.addBreakTokens === true) {
|
||
|
|
return [
|
||
|
|
simpleTokens.emptyArray,
|
||
|
|
new Token(Type.break)
|
||
|
|
];
|
||
|
|
}
|
||
|
|
return simpleTokens.emptyArray;
|
||
|
|
}
|
||
|
|
refStack = Ref.createCheck(refStack, obj);
|
||
|
|
const entries = [];
|
||
|
|
let i = 0;
|
||
|
|
for (const e of obj) {
|
||
|
|
entries[i++] = objectToTokens(e, options, refStack);
|
||
|
|
}
|
||
|
|
if (options.addBreakTokens) {
|
||
|
|
return [
|
||
|
|
new Token(Type.array, obj.length),
|
||
|
|
entries,
|
||
|
|
new Token(Type.break)
|
||
|
|
];
|
||
|
|
}
|
||
|
|
return [
|
||
|
|
new Token(Type.array, obj.length),
|
||
|
|
entries
|
||
|
|
];
|
||
|
|
},
|
||
|
|
Object(obj, typ, options, refStack) {
|
||
|
|
const isMap = typ !== 'Object';
|
||
|
|
const keys = isMap ? obj.keys() : Object.keys(obj);
|
||
|
|
const length = isMap ? obj.size : keys.length;
|
||
|
|
if (!length) {
|
||
|
|
if (options.addBreakTokens === true) {
|
||
|
|
return [
|
||
|
|
simpleTokens.emptyMap,
|
||
|
|
new Token(Type.break)
|
||
|
|
];
|
||
|
|
}
|
||
|
|
return simpleTokens.emptyMap;
|
||
|
|
}
|
||
|
|
refStack = Ref.createCheck(refStack, obj);
|
||
|
|
const entries = [];
|
||
|
|
let i = 0;
|
||
|
|
for (const key of keys) {
|
||
|
|
entries[i++] = [
|
||
|
|
objectToTokens(key, options, refStack),
|
||
|
|
objectToTokens(isMap ? obj.get(key) : obj[key], options, refStack)
|
||
|
|
];
|
||
|
|
}
|
||
|
|
sortMapEntries(entries, options);
|
||
|
|
if (options.addBreakTokens) {
|
||
|
|
return [
|
||
|
|
new Token(Type.map, length),
|
||
|
|
entries,
|
||
|
|
new Token(Type.break)
|
||
|
|
];
|
||
|
|
}
|
||
|
|
return [
|
||
|
|
new Token(Type.map, length),
|
||
|
|
entries
|
||
|
|
];
|
||
|
|
}
|
||
|
|
};
|
||
|
|
typeEncoders.Map = typeEncoders.Object;
|
||
|
|
typeEncoders.Buffer = typeEncoders.Uint8Array;
|
||
|
|
for (const typ of 'Uint8Clamped Uint16 Uint32 Int8 Int16 Int32 BigUint64 BigInt64 Float32 Float64'.split(' ')) {
|
||
|
|
typeEncoders[`${ typ }Array`] = typeEncoders.DataView;
|
||
|
|
}
|
||
|
|
function objectToTokens(obj, options = {}, refStack) {
|
||
|
|
const typ = is(obj);
|
||
|
|
const customTypeEncoder = options && options.typeEncoders && options.typeEncoders[typ] || typeEncoders[typ];
|
||
|
|
if (typeof customTypeEncoder === 'function') {
|
||
|
|
const tokens = customTypeEncoder(obj, typ, options, refStack);
|
||
|
|
if (tokens != null) {
|
||
|
|
return tokens;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const typeEncoder = typeEncoders[typ];
|
||
|
|
if (!typeEncoder) {
|
||
|
|
throw new Error(`${ encodeErrPrefix } unsupported type: ${ typ }`);
|
||
|
|
}
|
||
|
|
return typeEncoder(obj, typ, options, refStack);
|
||
|
|
}
|
||
|
|
function sortMapEntries(entries, options) {
|
||
|
|
if (options.mapSorter) {
|
||
|
|
entries.sort(options.mapSorter);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
function mapSorter(e1, e2) {
|
||
|
|
const keyToken1 = Array.isArray(e1[0]) ? e1[0][0] : e1[0];
|
||
|
|
const keyToken2 = Array.isArray(e2[0]) ? e2[0][0] : e2[0];
|
||
|
|
if (keyToken1.type !== keyToken2.type) {
|
||
|
|
return keyToken1.type.compare(keyToken2.type);
|
||
|
|
}
|
||
|
|
const major = keyToken1.type.major;
|
||
|
|
const tcmp = cborEncoders[major].compareTokens(keyToken1, keyToken2);
|
||
|
|
if (tcmp === 0) {
|
||
|
|
console.warn('WARNING: complex key types used, CBOR key sorting guarantees are gone');
|
||
|
|
}
|
||
|
|
return tcmp;
|
||
|
|
}
|
||
|
|
function tokensToEncoded(buf, tokens, encoders, options) {
|
||
|
|
if (Array.isArray(tokens)) {
|
||
|
|
for (const token of tokens) {
|
||
|
|
tokensToEncoded(buf, token, encoders, options);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
encoders[tokens.type.major](buf, tokens, options);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
function encodeCustom(data, encoders, options) {
|
||
|
|
const tokens = objectToTokens(data, options);
|
||
|
|
if (!Array.isArray(tokens) && options.quickEncodeToken) {
|
||
|
|
const quickBytes = options.quickEncodeToken(tokens);
|
||
|
|
if (quickBytes) {
|
||
|
|
return quickBytes;
|
||
|
|
}
|
||
|
|
const encoder = encoders[tokens.type.major];
|
||
|
|
if (encoder.encodedSize) {
|
||
|
|
const size = encoder.encodedSize(tokens, options);
|
||
|
|
const buf = new Bl(size);
|
||
|
|
encoder(buf, tokens, options);
|
||
|
|
if (buf.chunks.length !== 1) {
|
||
|
|
throw new Error(`Unexpected error: pre-calculated length for ${ tokens } was wrong`);
|
||
|
|
}
|
||
|
|
return asU8A(buf.chunks[0]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
buf.reset();
|
||
|
|
tokensToEncoded(buf, tokens, encoders, options);
|
||
|
|
return buf.toBytes(true);
|
||
|
|
}
|
||
|
|
function encode(data, options) {
|
||
|
|
options = Object.assign({}, defaultEncodeOptions, options);
|
||
|
|
return encodeCustom(data, cborEncoders, options);
|
||
|
|
}
|
||
|
|
export {
|
||
|
|
objectToTokens,
|
||
|
|
encode,
|
||
|
|
encodeCustom,
|
||
|
|
Ref
|
||
|
|
};
|