- Add GETTING_STARTED.md with quick start guide and development modes - Add INSTALL.sh automated installation script - Add INSTALLATION_CHECKLIST.md, INSTALLATION_SUCCESS.md, and INSTALLATION_SUMMARY.md - Add QUICK_REFERENCE.md for common commands - Add SETUP_GUIDE.md with detailed setup instructions - Update README.md with improved project overview - Add did-wallet app dependencies and node_modules
465 lines
14 KiB
JavaScript
465 lines
14 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'
|
|
|
|
/**
|
|
* @typedef {import('../interface').EncodeOptions} EncodeOptions
|
|
* @typedef {import('../interface').OptionalTypeEncoder} OptionalTypeEncoder
|
|
* @typedef {import('../interface').Reference} Reference
|
|
* @typedef {import('../interface').StrictTypeEncoder} StrictTypeEncoder
|
|
* @typedef {import('../interface').TokenTypeEncoder} TokenTypeEncoder
|
|
* @typedef {import('../interface').TokenOrNestedTokens} TokenOrNestedTokens
|
|
*/
|
|
|
|
/** @type {EncodeOptions} */
|
|
const defaultEncodeOptions = {
|
|
float64: false,
|
|
mapSorter,
|
|
quickEncodeToken
|
|
}
|
|
|
|
/** @returns {TokenTypeEncoder[]} */
|
|
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()
|
|
|
|
/** @implements {Reference} */
|
|
class Ref {
|
|
/**
|
|
* @param {object|any[]} obj
|
|
* @param {Reference|undefined} parent
|
|
*/
|
|
constructor (obj, parent) {
|
|
this.obj = obj
|
|
this.parent = parent
|
|
}
|
|
|
|
/**
|
|
* @param {object|any[]} obj
|
|
* @returns {boolean}
|
|
*/
|
|
includes (obj) {
|
|
/** @type {Reference|undefined} */
|
|
let p = this
|
|
do {
|
|
if (p.obj === obj) {
|
|
return true
|
|
}
|
|
} while (p = p.parent) // eslint-disable-line
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* @param {Reference|undefined} stack
|
|
* @param {object|any[]} obj
|
|
* @returns {Reference}
|
|
*/
|
|
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)
|
|
}
|
|
|
|
/** @type {{[typeName: string]: StrictTypeEncoder}} */
|
|
const typeEncoders = {
|
|
/**
|
|
* @param {any} obj
|
|
* @param {string} _typ
|
|
* @param {EncodeOptions} _options
|
|
* @param {Reference} [_refStack]
|
|
* @returns {TokenOrNestedTokens}
|
|
*/
|
|
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)
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @param {any} obj
|
|
* @param {string} _typ
|
|
* @param {EncodeOptions} _options
|
|
* @param {Reference} [_refStack]
|
|
* @returns {TokenOrNestedTokens}
|
|
*/
|
|
bigint (obj, _typ, _options, _refStack) {
|
|
if (obj >= BigInt(0)) {
|
|
return new Token(Type.uint, obj)
|
|
} else {
|
|
return new Token(Type.negint, obj)
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @param {any} obj
|
|
* @param {string} _typ
|
|
* @param {EncodeOptions} _options
|
|
* @param {Reference} [_refStack]
|
|
* @returns {TokenOrNestedTokens}
|
|
*/
|
|
Uint8Array (obj, _typ, _options, _refStack) {
|
|
return new Token(Type.bytes, obj)
|
|
},
|
|
|
|
/**
|
|
* @param {any} obj
|
|
* @param {string} _typ
|
|
* @param {EncodeOptions} _options
|
|
* @param {Reference} [_refStack]
|
|
* @returns {TokenOrNestedTokens}
|
|
*/
|
|
string (obj, _typ, _options, _refStack) {
|
|
return new Token(Type.string, obj)
|
|
},
|
|
|
|
/**
|
|
* @param {any} obj
|
|
* @param {string} _typ
|
|
* @param {EncodeOptions} _options
|
|
* @param {Reference} [_refStack]
|
|
* @returns {TokenOrNestedTokens}
|
|
*/
|
|
boolean (obj, _typ, _options, _refStack) {
|
|
return obj ? simpleTokens.true : simpleTokens.false
|
|
},
|
|
|
|
/**
|
|
* @param {any} _obj
|
|
* @param {string} _typ
|
|
* @param {EncodeOptions} _options
|
|
* @param {Reference} [_refStack]
|
|
* @returns {TokenOrNestedTokens}
|
|
*/
|
|
null (_obj, _typ, _options, _refStack) {
|
|
return simpleTokens.null
|
|
},
|
|
|
|
/**
|
|
* @param {any} _obj
|
|
* @param {string} _typ
|
|
* @param {EncodeOptions} _options
|
|
* @param {Reference} [_refStack]
|
|
* @returns {TokenOrNestedTokens}
|
|
*/
|
|
undefined (_obj, _typ, _options, _refStack) {
|
|
return simpleTokens.undefined
|
|
},
|
|
|
|
/**
|
|
* @param {any} obj
|
|
* @param {string} _typ
|
|
* @param {EncodeOptions} _options
|
|
* @param {Reference} [_refStack]
|
|
* @returns {TokenOrNestedTokens}
|
|
*/
|
|
ArrayBuffer (obj, _typ, _options, _refStack) {
|
|
return new Token(Type.bytes, new Uint8Array(obj))
|
|
},
|
|
|
|
/**
|
|
* @param {any} obj
|
|
* @param {string} _typ
|
|
* @param {EncodeOptions} _options
|
|
* @param {Reference} [_refStack]
|
|
* @returns {TokenOrNestedTokens}
|
|
*/
|
|
DataView (obj, _typ, _options, _refStack) {
|
|
return new Token(Type.bytes, new Uint8Array(obj.buffer, obj.byteOffset, obj.byteLength))
|
|
},
|
|
|
|
/**
|
|
* @param {any} obj
|
|
* @param {string} _typ
|
|
* @param {EncodeOptions} options
|
|
* @param {Reference} [refStack]
|
|
* @returns {TokenOrNestedTokens}
|
|
*/
|
|
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]
|
|
},
|
|
|
|
/**
|
|
* @param {any} obj
|
|
* @param {string} typ
|
|
* @param {EncodeOptions} options
|
|
* @param {Reference} [refStack]
|
|
* @returns {TokenOrNestedTokens}
|
|
*/
|
|
Object (obj, typ, options, refStack) {
|
|
// could be an Object or a Map
|
|
const isMap = typ !== 'Object'
|
|
// it's slightly quicker to use Object.keys() than Object.entries()
|
|
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)
|
|
/** @type {TokenOrNestedTokens[]} */
|
|
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
|
|
}
|
|
|
|
/**
|
|
* @param {any} obj
|
|
* @param {EncodeOptions} [options]
|
|
* @param {Reference} [refStack]
|
|
* @returns {TokenOrNestedTokens}
|
|
*/
|
|
function objectToTokens (obj, options = {}, refStack) {
|
|
const typ = is(obj)
|
|
const customTypeEncoder = (options && options.typeEncoders && /** @type {OptionalTypeEncoder} */ 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)
|
|
}
|
|
|
|
/*
|
|
CBOR key sorting is a mess.
|
|
|
|
The canonicalisation recommendation from https://tools.ietf.org/html/rfc7049#section-3.9
|
|
includes the wording:
|
|
|
|
> The keys in every map must be sorted lowest value to highest.
|
|
> Sorting is performed on the bytes of the representation of the key
|
|
> data items without paying attention to the 3/5 bit splitting for
|
|
> major types.
|
|
> ...
|
|
> * If two keys have different lengths, the shorter one sorts
|
|
earlier;
|
|
> * If two keys have the same length, the one with the lower value
|
|
in (byte-wise) lexical order sorts earlier.
|
|
|
|
1. It is not clear what "bytes of the representation of the key" means: is it
|
|
the CBOR representation, or the binary representation of the object itself?
|
|
Consider the int and uint difference here.
|
|
2. It is not clear what "without paying attention to" means: do we include it
|
|
and compare on that? Or do we omit the special prefix byte, (mostly) treating
|
|
the key in its plain binary representation form.
|
|
|
|
The FIDO 2.0: Client To Authenticator Protocol spec takes the original CBOR
|
|
wording and clarifies it according to their understanding.
|
|
https://fidoalliance.org/specs/fido-v2.0-rd-20170927/fido-client-to-authenticator-protocol-v2.0-rd-20170927.html#message-encoding
|
|
|
|
> The keys in every map must be sorted lowest value to highest. Sorting is
|
|
> performed on the bytes of the representation of the key data items without
|
|
> paying attention to the 3/5 bit splitting for major types. The sorting rules
|
|
> are:
|
|
> * If the major types are different, the one with the lower value in numerical
|
|
> order sorts earlier.
|
|
> * If two keys have different lengths, the shorter one sorts earlier;
|
|
> * If two keys have the same length, the one with the lower value in
|
|
> (byte-wise) lexical order sorts earlier.
|
|
|
|
Some other implementations, such as borc, do a full encode then do a
|
|
length-first, byte-wise-second comparison:
|
|
https://github.com/dignifiedquire/borc/blob/b6bae8b0bcde7c3976b0f0f0957208095c392a36/src/encoder.js#L358
|
|
https://github.com/dignifiedquire/borc/blob/b6bae8b0bcde7c3976b0f0f0957208095c392a36/src/utils.js#L143-L151
|
|
|
|
This has the benefit of being able to easily handle arbitrary keys, including
|
|
complex types (maps and arrays).
|
|
|
|
We'll opt for the FIDO approach, since it affords some efficies since we don't
|
|
need a full encode of each key to determine order and can defer to the types
|
|
to determine how to most efficiently order their values (i.e. int and uint
|
|
ordering can be done on the numbers, no need for byte-wise, for example).
|
|
|
|
Recommendation: stick to single key types or you'll get into trouble, and prefer
|
|
string keys because it's much simpler that way.
|
|
*/
|
|
|
|
/*
|
|
(UPDATE, Dec 2020)
|
|
https://tools.ietf.org/html/rfc8949 is the updated CBOR spec and clarifies some
|
|
of the questions above with a new recommendation for sorting order being much
|
|
closer to what would be expected in other environments (i.e. no length-first
|
|
weirdness).
|
|
This new sorting order is not yet implemented here but could be added as an
|
|
option. "Determinism" (canonicity) is system dependent and it's difficult to
|
|
change existing systems that are built with existing expectations. So if a new
|
|
ordering is introduced here, the old needs to be kept as well with the user
|
|
having the option.
|
|
*/
|
|
|
|
/**
|
|
* @param {TokenOrNestedTokens[]} entries
|
|
* @param {EncodeOptions} options
|
|
*/
|
|
function sortMapEntries (entries, options) {
|
|
if (options.mapSorter) {
|
|
entries.sort(options.mapSorter)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {(Token|Token[])[]} e1
|
|
* @param {(Token|Token[])[]} e2
|
|
* @returns {number}
|
|
*/
|
|
function mapSorter (e1, e2) {
|
|
// the key position ([0]) could have a single token or an array
|
|
// almost always it'll be a single token but complex key might get involved
|
|
/* c8 ignore next 2 */
|
|
const keyToken1 = Array.isArray(e1[0]) ? e1[0][0] : e1[0]
|
|
const keyToken2 = Array.isArray(e2[0]) ? e2[0][0] : e2[0]
|
|
|
|
// different key types
|
|
if (keyToken1.type !== keyToken2.type) {
|
|
return keyToken1.type.compare(keyToken2.type)
|
|
}
|
|
|
|
const major = keyToken1.type.major
|
|
// TODO: handle case where cmp === 0 but there are more keyToken e. complex type)
|
|
const tcmp = cborEncoders[major].compareTokens(keyToken1, keyToken2)
|
|
/* c8 ignore next 5 */
|
|
if (tcmp === 0) {
|
|
// duplicate key or complex type where the first token matched,
|
|
// i.e. a map or array and we're only comparing the opening token
|
|
console.warn('WARNING: complex key types used, CBOR key sorting guarantees are gone')
|
|
}
|
|
return tcmp
|
|
}
|
|
|
|
/**
|
|
* @param {Bl} buf
|
|
* @param {TokenOrNestedTokens} tokens
|
|
* @param {TokenTypeEncoder[]} encoders
|
|
* @param {EncodeOptions} options
|
|
*/
|
|
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)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {any} data
|
|
* @param {TokenTypeEncoder[]} encoders
|
|
* @param {EncodeOptions} options
|
|
* @returns {Uint8Array}
|
|
*/
|
|
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)
|
|
/* c8 ignore next 4 */
|
|
// this would be a problem with encodedSize() functions
|
|
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)
|
|
}
|
|
|
|
/**
|
|
* @param {any} data
|
|
* @param {EncodeOptions} [options]
|
|
* @returns {Uint8Array}
|
|
*/
|
|
function encode (data, options) {
|
|
options = Object.assign({}, defaultEncodeOptions, options)
|
|
return encodeCustom(data, cborEncoders, options)
|
|
}
|
|
|
|
export { objectToTokens, encode, encodeCustom, Ref }
|