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