Add comprehensive installation and setup documentation

- 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
This commit is contained in:
Dorian
2026-01-27 17:18:21 +00:00
parent a81f655133
commit 0d073fa89e
22658 changed files with 4494151 additions and 6 deletions
+227
View File
@@ -0,0 +1,227 @@
/* globals BigInt */
import { Token, Type } from './token.js'
import { decodeErrPrefix, assertEnoughData } from './common.js'
export const uintBoundaries = [24, 256, 65536, 4294967296, BigInt('18446744073709551616')]
/**
* @typedef {import('./bl.js').Bl} Bl
* @typedef {import('../interface').DecodeOptions} DecodeOptions
*/
/**
* @param {Uint8Array} data
* @param {number} offset
* @param {DecodeOptions} options
* @returns {number}
*/
export function readUint8 (data, offset, options) {
assertEnoughData(data, offset, 1)
const value = data[offset]
if (options.strict === true && value < uintBoundaries[0]) {
throw new Error(`${decodeErrPrefix} integer encoded in more bytes than necessary (strict decode)`)
}
return value
}
/**
* @param {Uint8Array} data
* @param {number} offset
* @param {DecodeOptions} options
* @returns {number}
*/
export function readUint16 (data, offset, options) {
assertEnoughData(data, offset, 2)
const value = (data[offset] << 8) | data[offset + 1]
if (options.strict === true && value < uintBoundaries[1]) {
throw new Error(`${decodeErrPrefix} integer encoded in more bytes than necessary (strict decode)`)
}
return value
}
/**
* @param {Uint8Array} data
* @param {number} offset
* @param {DecodeOptions} options
* @returns {number}
*/
export function readUint32 (data, offset, options) {
assertEnoughData(data, offset, 4)
const value = (data[offset] * 16777216 /* 2 ** 24 */) + (data[offset + 1] << 16) + (data[offset + 2] << 8) + data[offset + 3]
if (options.strict === true && value < uintBoundaries[2]) {
throw new Error(`${decodeErrPrefix} integer encoded in more bytes than necessary (strict decode)`)
}
return value
}
/**
* @param {Uint8Array} data
* @param {number} offset
* @param {DecodeOptions} options
* @returns {number|bigint}
*/
export function readUint64 (data, offset, options) {
// assume BigInt, convert back to Number if within safe range
assertEnoughData(data, offset, 8)
const hi = (data[offset] * 16777216 /* 2 ** 24 */) + (data[offset + 1] << 16) + (data[offset + 2] << 8) + data[offset + 3]
const lo = (data[offset + 4] * 16777216 /* 2 ** 24 */) + (data[offset + 5] << 16) + (data[offset + 6] << 8) + data[offset + 7]
const value = (BigInt(hi) << BigInt(32)) + BigInt(lo)
if (options.strict === true && value < uintBoundaries[3]) {
throw new Error(`${decodeErrPrefix} integer encoded in more bytes than necessary (strict decode)`)
}
if (value <= Number.MAX_SAFE_INTEGER) {
return Number(value)
}
if (options.allowBigInt === true) {
return value
}
throw new Error(`${decodeErrPrefix} integers outside of the safe integer range are not supported`)
}
/* not required thanks to quick[] list
const oneByteTokens = new Array(24).fill(0).map((v, i) => new Token(Type.uint, i, 1))
export function decodeUintCompact (data, pos, minor, options) {
return oneByteTokens[minor]
}
*/
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeUint8 (data, pos, _minor, options) {
return new Token(Type.uint, readUint8(data, pos + 1, options), 2)
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeUint16 (data, pos, _minor, options) {
return new Token(Type.uint, readUint16(data, pos + 1, options), 3)
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeUint32 (data, pos, _minor, options) {
return new Token(Type.uint, readUint32(data, pos + 1, options), 5)
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeUint64 (data, pos, _minor, options) {
return new Token(Type.uint, readUint64(data, pos + 1, options), 9)
}
/**
* @param {Bl} buf
* @param {Token} token
*/
export function encodeUint (buf, token) {
return encodeUintValue(buf, 0, token.value)
}
/**
* @param {Bl} buf
* @param {number} major
* @param {number|bigint} uint
*/
export function encodeUintValue (buf, major, uint) {
if (uint < uintBoundaries[0]) {
const nuint = Number(uint)
// pack into one byte, minor=0, additional=value
buf.push([major | nuint])
} else if (uint < uintBoundaries[1]) {
const nuint = Number(uint)
// pack into two byte, minor=0, additional=24
buf.push([major | 24, nuint])
} else if (uint < uintBoundaries[2]) {
const nuint = Number(uint)
// pack into three byte, minor=0, additional=25
buf.push([major | 25, nuint >>> 8, nuint & 0xff])
} else if (uint < uintBoundaries[3]) {
const nuint = Number(uint)
// pack into five byte, minor=0, additional=26
buf.push([major | 26, (nuint >>> 24) & 0xff, (nuint >>> 16) & 0xff, (nuint >>> 8) & 0xff, nuint & 0xff])
} else {
const buint = BigInt(uint)
if (buint < uintBoundaries[4]) {
// pack into nine byte, minor=0, additional=27
const set = [major | 27, 0, 0, 0, 0, 0, 0, 0]
// simulate bitwise above 32 bits
let lo = Number(buint & BigInt(0xffffffff))
let hi = Number(buint >> BigInt(32) & BigInt(0xffffffff))
set[8] = lo & 0xff
lo = lo >> 8
set[7] = lo & 0xff
lo = lo >> 8
set[6] = lo & 0xff
lo = lo >> 8
set[5] = lo & 0xff
set[4] = hi & 0xff
hi = hi >> 8
set[3] = hi & 0xff
hi = hi >> 8
set[2] = hi & 0xff
hi = hi >> 8
set[1] = hi & 0xff
buf.push(set)
} else {
throw new Error(`${decodeErrPrefix} encountered BigInt larger than allowable range`)
}
}
}
/**
* @param {Token} token
* @returns {number}
*/
encodeUint.encodedSize = function encodedSize (token) {
return encodeUintValue.encodedSize(token.value)
}
/**
* @param {number} uint
* @returns {number}
*/
encodeUintValue.encodedSize = function encodedSize (uint) {
if (uint < uintBoundaries[0]) {
return 1
}
if (uint < uintBoundaries[1]) {
return 2
}
if (uint < uintBoundaries[2]) {
return 3
}
if (uint < uintBoundaries[3]) {
return 5
}
return 9
}
/**
* @param {Token} tok1
* @param {Token} tok2
* @returns {number}
*/
encodeUint.compareTokens = function compareTokens (tok1, tok2) {
return tok1.value < tok2.value ? -1 : tok1.value > tok2.value ? 1 : /* c8 ignore next */ 0
}
+111
View File
@@ -0,0 +1,111 @@
/* eslint-env es2020 */
import { Token, Type } from './token.js'
import * as uint from './0uint.js'
import { decodeErrPrefix } from './common.js'
/**
* @typedef {import('./bl.js').Bl} Bl
* @typedef {import('../interface').DecodeOptions} DecodeOptions
*/
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeNegint8 (data, pos, _minor, options) {
return new Token(Type.negint, -1 - uint.readUint8(data, pos + 1, options), 2)
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeNegint16 (data, pos, _minor, options) {
return new Token(Type.negint, -1 - uint.readUint16(data, pos + 1, options), 3)
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeNegint32 (data, pos, _minor, options) {
return new Token(Type.negint, -1 - uint.readUint32(data, pos + 1, options), 5)
}
const neg1b = BigInt(-1)
const pos1b = BigInt(1)
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeNegint64 (data, pos, _minor, options) {
const int = uint.readUint64(data, pos + 1, options)
if (typeof int !== 'bigint') {
const value = -1 - int
if (value >= Number.MIN_SAFE_INTEGER) {
return new Token(Type.negint, value, 9)
}
}
if (options.allowBigInt !== true) {
throw new Error(`${decodeErrPrefix} integers outside of the safe integer range are not supported`)
}
return new Token(Type.negint, neg1b - BigInt(int), 9)
}
/**
* @param {Bl} buf
* @param {Token} token
*/
export function encodeNegint (buf, token) {
const negint = token.value
const unsigned = (typeof negint === 'bigint' ? (negint * neg1b - pos1b) : (negint * -1 - 1))
uint.encodeUintValue(buf, token.type.majorEncoded, unsigned)
}
/**
* @param {Token} token
* @returns {number}
*/
encodeNegint.encodedSize = function encodedSize (token) {
const negint = token.value
const unsigned = (typeof negint === 'bigint' ? (negint * neg1b - pos1b) : (negint * -1 - 1))
/* c8 ignore next 4 */
// handled by quickEncode, we shouldn't get here but it's included for completeness
if (unsigned < uint.uintBoundaries[0]) {
return 1
}
if (unsigned < uint.uintBoundaries[1]) {
return 2
}
if (unsigned < uint.uintBoundaries[2]) {
return 3
}
if (unsigned < uint.uintBoundaries[3]) {
return 5
}
return 9
}
/**
* @param {Token} tok1
* @param {Token} tok2
* @returns {number}
*/
encodeNegint.compareTokens = function compareTokens (tok1, tok2) {
// opposite of the uint comparison since we store the uint version in bytes
return tok1.value < tok2.value ? 1 : tok1.value > tok2.value ? -1 : /* c8 ignore next */ 0
}
+133
View File
@@ -0,0 +1,133 @@
import { Token, Type } from './token.js'
import { assertEnoughData, decodeErrPrefix } from './common.js'
import * as uint from './0uint.js'
import { compare, fromString, slice } from './byte-utils.js'
/**
* @typedef {import('./bl.js').Bl} Bl
* @typedef {import('../interface').DecodeOptions} DecodeOptions
*/
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} prefix
* @param {number} length
* @returns {Token}
*/
function toToken (data, pos, prefix, length) {
assertEnoughData(data, pos, prefix + length)
const buf = slice(data, pos + prefix, pos + prefix + length)
return new Token(Type.bytes, buf, prefix + length)
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} minor
* @param {DecodeOptions} _options
* @returns {Token}
*/
export function decodeBytesCompact (data, pos, minor, _options) {
return toToken(data, pos, 1, minor)
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeBytes8 (data, pos, _minor, options) {
return toToken(data, pos, 2, uint.readUint8(data, pos + 1, options))
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeBytes16 (data, pos, _minor, options) {
return toToken(data, pos, 3, uint.readUint16(data, pos + 1, options))
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeBytes32 (data, pos, _minor, options) {
return toToken(data, pos, 5, uint.readUint32(data, pos + 1, options))
}
// TODO: maybe we shouldn't support this ..
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeBytes64 (data, pos, _minor, options) {
const l = uint.readUint64(data, pos + 1, options)
if (typeof l === 'bigint') {
throw new Error(`${decodeErrPrefix} 64-bit integer bytes lengths not supported`)
}
return toToken(data, pos, 9, l)
}
/**
* `encodedBytes` allows for caching when we do a byte version of a string
* for key sorting purposes
* @param {Token} token
* @returns {Uint8Array}
*/
function tokenBytes (token) {
if (token.encodedBytes === undefined) {
token.encodedBytes = token.type === Type.string ? fromString(token.value) : token.value
}
// @ts-ignore c'mon
return token.encodedBytes
}
/**
* @param {Bl} buf
* @param {Token} token
*/
export function encodeBytes (buf, token) {
const bytes = tokenBytes(token)
uint.encodeUintValue(buf, token.type.majorEncoded, bytes.length)
buf.push(bytes)
}
/**
* @param {Token} token
* @returns {number}
*/
encodeBytes.encodedSize = function encodedSize (token) {
const bytes = tokenBytes(token)
return uint.encodeUintValue.encodedSize(bytes.length) + bytes.length
}
/**
* @param {Token} tok1
* @param {Token} tok2
* @returns {number}
*/
encodeBytes.compareTokens = function compareTokens (tok1, tok2) {
return compareBytes(tokenBytes(tok1), tokenBytes(tok2))
}
/**
* @param {Uint8Array} b1
* @param {Uint8Array} b2
* @returns {number}
*/
export function compareBytes (b1, b2) {
return b1.length < b2.length ? -1 : b1.length > b2.length ? 1 : compare(b1, b2)
}
+90
View File
@@ -0,0 +1,90 @@
import { Token, Type } from './token.js'
import { assertEnoughData, decodeErrPrefix } from './common.js'
import * as uint from './0uint.js'
import { encodeBytes } from './2bytes.js'
import { toString, slice } from './byte-utils.js'
/**
* @typedef {import('./bl.js').Bl} Bl
* @typedef {import('../interface').DecodeOptions} DecodeOptions
*/
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} prefix
* @param {number} length
* @param {DecodeOptions} options
* @returns {Token}
*/
function toToken (data, pos, prefix, length, options) {
const totLength = prefix + length
assertEnoughData(data, pos, totLength)
const tok = new Token(Type.string, toString(data, pos + prefix, pos + totLength), totLength)
if (options.retainStringBytes === true) {
tok.byteValue = slice(data, pos + prefix, pos + totLength)
}
return tok
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeStringCompact (data, pos, minor, options) {
return toToken(data, pos, 1, minor, options)
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeString8 (data, pos, _minor, options) {
return toToken(data, pos, 2, uint.readUint8(data, pos + 1, options), options)
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeString16 (data, pos, _minor, options) {
return toToken(data, pos, 3, uint.readUint16(data, pos + 1, options), options)
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeString32 (data, pos, _minor, options) {
return toToken(data, pos, 5, uint.readUint32(data, pos + 1, options), options)
}
// TODO: maybe we shouldn't support this ..
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeString64 (data, pos, _minor, options) {
const l = uint.readUint64(data, pos + 1, options)
if (typeof l === 'bigint') {
throw new Error(`${decodeErrPrefix} 64-bit integer string lengths not supported`)
}
return toToken(data, pos, 9, l, options)
}
export const encodeString = encodeBytes
+113
View File
@@ -0,0 +1,113 @@
import { Token, Type } from './token.js'
import * as uint from './0uint.js'
import { decodeErrPrefix } from './common.js'
/**
* @typedef {import('./bl.js').Bl} Bl
* @typedef {import('../interface').DecodeOptions} DecodeOptions
*/
/**
* @param {Uint8Array} _data
* @param {number} _pos
* @param {number} prefix
* @param {number} length
* @returns {Token}
*/
function toToken (_data, _pos, prefix, length) {
return new Token(Type.array, length, prefix)
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} minor
* @param {DecodeOptions} _options
* @returns {Token}
*/
export function decodeArrayCompact (data, pos, minor, _options) {
return toToken(data, pos, 1, minor)
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeArray8 (data, pos, _minor, options) {
return toToken(data, pos, 2, uint.readUint8(data, pos + 1, options))
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeArray16 (data, pos, _minor, options) {
return toToken(data, pos, 3, uint.readUint16(data, pos + 1, options))
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeArray32 (data, pos, _minor, options) {
return toToken(data, pos, 5, uint.readUint32(data, pos + 1, options))
}
// TODO: maybe we shouldn't support this ..
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeArray64 (data, pos, _minor, options) {
const l = uint.readUint64(data, pos + 1, options)
if (typeof l === 'bigint') {
throw new Error(`${decodeErrPrefix} 64-bit integer array lengths not supported`)
}
return toToken(data, pos, 9, l)
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeArrayIndefinite (data, pos, _minor, options) {
if (options.allowIndefinite === false) {
throw new Error(`${decodeErrPrefix} indefinite length items not allowed`)
}
return toToken(data, pos, 1, Infinity)
}
/**
* @param {Bl} buf
* @param {Token} token
*/
export function encodeArray (buf, token) {
uint.encodeUintValue(buf, Type.array.majorEncoded, token.value)
}
// using an array as a map key, are you sure about this? we can only sort
// by map length here, it's up to the encoder to decide to look deeper
encodeArray.compareTokens = uint.encodeUint.compareTokens
/**
* @param {Token} token
* @returns {number}
*/
encodeArray.encodedSize = function encodedSize (token) {
return uint.encodeUintValue.encodedSize(token.value)
}
+113
View File
@@ -0,0 +1,113 @@
import { Token, Type } from './token.js'
import * as uint from './0uint.js'
import { decodeErrPrefix } from './common.js'
/**
* @typedef {import('./bl.js').Bl} Bl
* @typedef {import('../interface').DecodeOptions} DecodeOptions
*/
/**
* @param {Uint8Array} _data
* @param {number} _pos
* @param {number} prefix
* @param {number} length
* @returns {Token}
*/
function toToken (_data, _pos, prefix, length) {
return new Token(Type.map, length, prefix)
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} minor
* @param {DecodeOptions} _options
* @returns {Token}
*/
export function decodeMapCompact (data, pos, minor, _options) {
return toToken(data, pos, 1, minor)
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeMap8 (data, pos, _minor, options) {
return toToken(data, pos, 2, uint.readUint8(data, pos + 1, options))
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeMap16 (data, pos, _minor, options) {
return toToken(data, pos, 3, uint.readUint16(data, pos + 1, options))
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeMap32 (data, pos, _minor, options) {
return toToken(data, pos, 5, uint.readUint32(data, pos + 1, options))
}
// TODO: maybe we shouldn't support this ..
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeMap64 (data, pos, _minor, options) {
const l = uint.readUint64(data, pos + 1, options)
if (typeof l === 'bigint') {
throw new Error(`${decodeErrPrefix} 64-bit integer map lengths not supported`)
}
return toToken(data, pos, 9, l)
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeMapIndefinite (data, pos, _minor, options) {
if (options.allowIndefinite === false) {
throw new Error(`${decodeErrPrefix} indefinite length items not allowed`)
}
return toToken(data, pos, 1, Infinity)
}
/**
* @param {Bl} buf
* @param {Token} token
*/
export function encodeMap (buf, token) {
uint.encodeUintValue(buf, Type.map.majorEncoded, token.value)
}
// using a map as a map key, are you sure about this? we can only sort
// by map length here, it's up to the encoder to decide to look deeper
encodeMap.compareTokens = uint.encodeUint.compareTokens
/**
* @param {Token} token
* @returns {number}
*/
encodeMap.encodedSize = function encodedSize (token) {
return uint.encodeUintValue.encodedSize(token.value)
}
+80
View File
@@ -0,0 +1,80 @@
import { Token, Type } from './token.js'
import * as uint from './0uint.js'
/**
* @typedef {import('./bl.js').Bl} Bl
* @typedef {import('../interface').DecodeOptions} DecodeOptions
*/
/**
* @param {Uint8Array} _data
* @param {number} _pos
* @param {number} minor
* @param {DecodeOptions} _options
* @returns {Token}
*/
export function decodeTagCompact (_data, _pos, minor, _options) {
return new Token(Type.tag, minor, 1)
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeTag8 (data, pos, _minor, options) {
return new Token(Type.tag, uint.readUint8(data, pos + 1, options), 2)
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeTag16 (data, pos, _minor, options) {
return new Token(Type.tag, uint.readUint16(data, pos + 1, options), 3)
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeTag32 (data, pos, _minor, options) {
return new Token(Type.tag, uint.readUint32(data, pos + 1, options), 5)
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeTag64 (data, pos, _minor, options) {
return new Token(Type.tag, uint.readUint64(data, pos + 1, options), 9)
}
/**
* @param {Bl} buf
* @param {Token} token
*/
export function encodeTag (buf, token) {
uint.encodeUintValue(buf, Type.tag.majorEncoded, token.value)
}
encodeTag.compareTokens = uint.encodeUint.compareTokens
/**
* @param {Token} token
* @returns {number}
*/
encodeTag.encodedSize = function encodedSize (token) {
return uint.encodeUintValue.encodedSize(token.value)
}
+308
View File
@@ -0,0 +1,308 @@
// TODO: shift some of the bytes logic to bytes-utils so we can use Buffer
// where possible
import { Token, Type } from './token.js'
import { decodeErrPrefix } from './common.js'
import { encodeUint } from './0uint.js'
/**
* @typedef {import('./bl.js').Bl} Bl
* @typedef {import('../interface').DecodeOptions} DecodeOptions
* @typedef {import('../interface').EncodeOptions} EncodeOptions
*/
const MINOR_FALSE = 20
const MINOR_TRUE = 21
const MINOR_NULL = 22
const MINOR_UNDEFINED = 23
/**
* @param {Uint8Array} _data
* @param {number} _pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeUndefined (_data, _pos, _minor, options) {
if (options.allowUndefined === false) {
throw new Error(`${decodeErrPrefix} undefined values are not supported`)
} else if (options.coerceUndefinedToNull === true) {
return new Token(Type.null, null, 1)
}
return new Token(Type.undefined, undefined, 1)
}
/**
* @param {Uint8Array} _data
* @param {number} _pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeBreak (_data, _pos, _minor, options) {
if (options.allowIndefinite === false) {
throw new Error(`${decodeErrPrefix} indefinite length items not allowed`)
}
return new Token(Type.break, undefined, 1)
}
/**
* @param {number} value
* @param {number} bytes
* @param {DecodeOptions} options
* @returns {Token}
*/
function createToken (value, bytes, options) {
if (options) {
if (options.allowNaN === false && Number.isNaN(value)) {
throw new Error(`${decodeErrPrefix} NaN values are not supported`)
}
if (options.allowInfinity === false && (value === Infinity || value === -Infinity)) {
throw new Error(`${decodeErrPrefix} Infinity values are not supported`)
}
}
return new Token(Type.float, value, bytes)
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeFloat16 (data, pos, _minor, options) {
return createToken(readFloat16(data, pos + 1), 3, options)
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeFloat32 (data, pos, _minor, options) {
return createToken(readFloat32(data, pos + 1), 5, options)
}
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} _minor
* @param {DecodeOptions} options
* @returns {Token}
*/
export function decodeFloat64 (data, pos, _minor, options) {
return createToken(readFloat64(data, pos + 1), 9, options)
}
/**
* @param {Bl} buf
* @param {Token} token
* @param {EncodeOptions} options
*/
export function encodeFloat (buf, token, options) {
const float = token.value
if (float === false) {
buf.push([Type.float.majorEncoded | MINOR_FALSE])
} else if (float === true) {
buf.push([Type.float.majorEncoded | MINOR_TRUE])
} else if (float === null) {
buf.push([Type.float.majorEncoded | MINOR_NULL])
} else if (float === undefined) {
buf.push([Type.float.majorEncoded | MINOR_UNDEFINED])
} else {
let decoded
let success = false
if (!options || options.float64 !== true) {
encodeFloat16(float)
decoded = readFloat16(ui8a, 1)
if (float === decoded || Number.isNaN(float)) {
ui8a[0] = 0xf9
buf.push(ui8a.slice(0, 3))
success = true
} else {
encodeFloat32(float)
decoded = readFloat32(ui8a, 1)
if (float === decoded) {
ui8a[0] = 0xfa
buf.push(ui8a.slice(0, 5))
success = true
}
}
}
if (!success) {
encodeFloat64(float)
decoded = readFloat64(ui8a, 1)
ui8a[0] = 0xfb
buf.push(ui8a.slice(0, 9))
}
}
}
/**
* @param {Token} token
* @param {EncodeOptions} options
* @returns {number}
*/
encodeFloat.encodedSize = function encodedSize (token, options) {
const float = token.value
if (float === false || float === true || float === null || float === undefined) {
return 1
}
if (!options || options.float64 !== true) {
encodeFloat16(float)
let decoded = readFloat16(ui8a, 1)
if (float === decoded || Number.isNaN(float)) {
return 3
}
encodeFloat32(float)
decoded = readFloat32(ui8a, 1)
if (float === decoded) {
return 5
}
}
return 9
}
const buffer = new ArrayBuffer(9)
const dataView = new DataView(buffer, 1)
const ui8a = new Uint8Array(buffer, 0)
/**
* @param {number} inp
*/
function encodeFloat16 (inp) {
if (inp === Infinity) {
dataView.setUint16(0, 0x7c00, false)
} else if (inp === -Infinity) {
dataView.setUint16(0, 0xfc00, false)
} else if (Number.isNaN(inp)) {
dataView.setUint16(0, 0x7e00, false)
} else {
dataView.setFloat32(0, inp)
const valu32 = dataView.getUint32(0)
const exponent = (valu32 & 0x7f800000) >> 23
const mantissa = valu32 & 0x7fffff
/* c8 ignore next 6 */
if (exponent === 0xff) {
// too big, Infinity, but this should be hard (impossible?) to trigger
dataView.setUint16(0, 0x7c00, false)
} else if (exponent === 0x00) {
// 0.0, -0.0 and subnormals, shouldn't be possible to get here because 0.0 should be counted as an int
dataView.setUint16(0, ((inp & 0x80000000) >> 16) | (mantissa >> 13), false)
} else { // standard numbers
// chunks of logic here borrowed from https://github.com/PJK/libcbor/blob/c78f437182533e3efa8d963ff4b945bb635c2284/src/cbor/encoding.c#L127
const logicalExponent = exponent - 127
// Now we know that 2^exponent <= 0 logically
/* c8 ignore next 6 */
if (logicalExponent < -24) {
/* No unambiguous representation exists, this float is not a half float
and is too small to be represented using a half, round off to zero.
Consistent with the reference implementation. */
// should be difficult (impossible?) to get here in JS
dataView.setUint16(0, 0)
} else if (logicalExponent < -14) {
/* Offset the remaining decimal places by shifting the significand, the
value is lost. This is an implementation decision that works around the
absence of standard half-float in the language. */
dataView.setUint16(0, ((valu32 & 0x80000000) >> 16) | /* sign bit */ (1 << (24 + logicalExponent)), false)
} else {
dataView.setUint16(0, ((valu32 & 0x80000000) >> 16) | ((logicalExponent + 15) << 10) | (mantissa >> 13), false)
}
}
}
}
/**
* @param {Uint8Array} ui8a
* @param {number} pos
* @returns {number}
*/
function readFloat16 (ui8a, pos) {
if (ui8a.length - pos < 2) {
throw new Error(`${decodeErrPrefix} not enough data for float16`)
}
const half = (ui8a[pos] << 8) + ui8a[pos + 1]
if (half === 0x7c00) {
return Infinity
}
if (half === 0xfc00) {
return -Infinity
}
if (half === 0x7e00) {
return NaN
}
const exp = (half >> 10) & 0x1f
const mant = half & 0x3ff
let val
if (exp === 0) {
val = mant * (2 ** -24)
} else if (exp !== 31) {
val = (mant + 1024) * (2 ** (exp - 25))
/* c8 ignore next 4 */
} else {
// may not be possible to get here
val = mant === 0 ? Infinity : NaN
}
return (half & 0x8000) ? -val : val
}
/**
* @param {number} inp
*/
function encodeFloat32 (inp) {
dataView.setFloat32(0, inp, false)
}
/**
* @param {Uint8Array} ui8a
* @param {number} pos
* @returns {number}
*/
function readFloat32 (ui8a, pos) {
if (ui8a.length - pos < 4) {
throw new Error(`${decodeErrPrefix} not enough data for float32`)
}
const offset = (ui8a.byteOffset || 0) + pos
return new DataView(ui8a.buffer, offset, 4).getFloat32(0, false)
}
/**
* @param {number} inp
*/
function encodeFloat64 (inp) {
dataView.setFloat64(0, inp, false)
}
/**
* @param {Uint8Array} ui8a
* @param {number} pos
* @returns {number}
*/
function readFloat64 (ui8a, pos) {
if (ui8a.length - pos < 8) {
throw new Error(`${decodeErrPrefix} not enough data for float64`)
}
const offset = (ui8a.byteOffset || 0) + pos
return new DataView(ui8a.buffer, offset, 8).getFloat64(0, false)
}
/**
* @param {Token} _tok1
* @param {Token} _tok2
* @returns {number}
*/
encodeFloat.compareTokens = encodeUint.compareTokens
/*
encodeFloat.compareTokens = function compareTokens (_tok1, _tok2) {
return _tok1
throw new Error(`${encodeErrPrefix} cannot use floats as map keys`)
}
*/
Generated Vendored Executable
+172
View File
@@ -0,0 +1,172 @@
// excluding #! from here because of ipjs compile, this is called from cli.js
import process from 'process'
import { decode, encode } from '../cborg.js'
import { tokensToDiagnostic, fromDiag } from './diagnostic.js'
import { fromHex as _fromHex, toHex } from './byte-utils.js'
/**
* @param {number} code
*/
function usage (code) {
console.error('Usage: cborg <command> <args>')
console.error('Valid commands:')
console.error('\tbin2diag [binary input]')
console.error('\tbin2hex [binary input]')
console.error('\tbin2json [--pretty] [binary input]')
console.error('\tdiag2bin [diagnostic input]')
console.error('\tdiag2hex [diagnostic input]')
console.error('\tdiag2json [--pretty] [diagnostic input]')
console.error('\thex2bin [hex input]')
console.error('\thex2diag [hex input]')
console.error('\thex2json [--pretty] [hex input]')
console.error('\tjson2bin \'[json input]\'')
console.error('\tjson2diag \'[json input]\'')
console.error('\tjson2hex \'[json input]\'')
console.error('Input may either be supplied as an argument or piped via stdin')
process.exit(code || 0)
}
async function fromStdin () {
const chunks = []
for await (const chunk of process.stdin) {
chunks.push(chunk)
}
return Buffer.concat(chunks)
}
/**
* @param {string} str
* @returns {Uint8Array}
*/
function fromHex (str) {
str = str.replace(/\r?\n/g, '') // let's be charitable
/* c8 ignore next 3 */
if (!(/^([0-9a-f]{2})*$/i).test(str)) {
throw new Error('Input string is not hexadecimal format')
}
return _fromHex(str)
}
function argvPretty () {
const argv = process.argv.filter((s) => s !== '--pretty')
const pretty = argv.length !== process.argv.length
return { argv, pretty }
}
async function run () {
const cmd = process.argv[2]
switch (cmd) {
case 'help': {
return usage(0)
}
case 'bin2diag': {
/* c8 ignore next 1 */
const bin = process.argv.length < 4 ? (await fromStdin()) : new TextEncoder().encode(process.argv[3])
for (const line of tokensToDiagnostic(bin)) {
console.log(line)
}
return
}
case 'bin2hex': {
// this is really nothing to do with cbor.. just handy
/* c8 ignore next 1 */
const bin = process.argv.length < 4 ? (await fromStdin()) : new TextEncoder().encode(process.argv[3])
return console.log(toHex(bin))
}
case 'bin2json': {
const { argv, pretty } = argvPretty()
/* c8 ignore next 1 */
const bin = argv.length < 4 ? (await fromStdin()) : new TextEncoder().encode(argv[3])
return console.log(JSON.stringify(decode(bin), undefined, pretty ? 2 : undefined))
}
case 'diag2bin': {
// no coverage on windows for non-stdin input
/* c8 ignore next 1 */
const bin = fromDiag(process.argv.length < 4 ? (await fromStdin()).toString() : process.argv[3])
return process.stdout.write(bin)
}
case 'diag2hex': {
// no coverage on windows for non-stdin input
/* c8 ignore next 1 */
const bin = fromDiag(process.argv.length < 4 ? (await fromStdin()).toString() : process.argv[3])
return console.log(toHex(bin))
}
case 'diag2json': {
const { argv, pretty } = argvPretty()
// no coverage on windows for non-stdin input
/* c8 ignore next 1 */
const bin = fromDiag(argv.length < 4 ? (await fromStdin()).toString() : argv[3])
return console.log(JSON.stringify(decode(bin), undefined, pretty ? 2 : undefined))
}
case 'hex2bin': {
// this is really nothing to do with cbor.. just handy
const bin = fromHex(process.argv.length < 4 ? (await fromStdin()).toString() : process.argv[3])
return process.stdout.write(bin)
}
case 'hex2diag': {
const bin = fromHex(process.argv.length < 4 ? (await fromStdin()).toString() : process.argv[3])
for (const line of tokensToDiagnostic(bin)) {
console.log(line)
}
return
}
case 'hex2json': {
const { argv, pretty } = argvPretty()
const bin = fromHex(argv.length < 4 ? (await fromStdin()).toString() : argv[3])
return console.log(JSON.stringify(decode(bin), undefined, pretty ? 2 : undefined))
}
case 'json2bin': {
const inp = process.argv.length < 4 ? (await fromStdin()).toString() : process.argv[3]
const obj = JSON.parse(inp)
return process.stdout.write(encode(obj))
}
case 'json2diag': {
const inp = process.argv.length < 4 ? (await fromStdin()).toString() : process.argv[3]
const obj = JSON.parse(inp)
for (const line of tokensToDiagnostic(encode(obj))) {
console.log(line)
}
return
}
case 'json2hex': {
const inp = process.argv.length < 4 ? (await fromStdin()).toString() : process.argv[3]
const obj = JSON.parse(inp)
return console.log(toHex(encode(obj)))
}
default: { // no, or unknown cmd
// this is a dirty hack to allow import of this package by the tests
// for inclusion in ipjs bundling, but to silently ignore it so we don't
// print usage and exit(1).
if (process.argv.findIndex((a) => a.endsWith('mocha')) === -1) {
if (cmd) {
console.error(`Unknown command: '${cmd}'`)
}
usage(1)
}
}
}
}
run().catch((err) => {
/* c8 ignore next 2 */
console.error(err)
process.exit(1)
})
// for ipjs, to get it to compile
export default true
+124
View File
@@ -0,0 +1,124 @@
/**
* Bl is a list of byte chunks, similar to https://github.com/rvagg/bl but for
* writing rather than reading.
* A Bl object accepts set() operations for individual bytes and copyTo() for
* inserting byte arrays. These write operations don't automatically increment
* the internal cursor so its "length" won't be changed. Instead, increment()
* must be called to extend its length to cover the inserted data.
* The toBytes() call will convert all internal memory to a single Uint8Array of
* the correct length, truncating any data that is stored but hasn't been
* included by an increment().
* get() can retrieve a single byte.
* All operations (except toBytes()) take an "offset" argument that will perform
* the write at the offset _from the current cursor_. For most operations this
* will be `0` to write at the current cursor position but it can be ahead of
* the current cursor. Negative offsets probably work but are untested.
*/
// TODO: ipjs doesn't support this, only for test files: https://github.com/mikeal/ipjs/blob/master/src/package/testFile.js#L39
import { alloc, concat, slice } from './byte-utils.js'
// the ts-ignores in this file are almost all for the `Uint8Array|number[]` duality that exists
// for perf reasons. Consider better approaches to this or removing it entirely, it is quite
// risky because of some assumptions about small chunks === number[] and everything else === Uint8Array.
const defaultChunkSize = 256
export class Bl {
/**
* @param {number} [chunkSize]
*/
constructor (chunkSize = defaultChunkSize) {
this.chunkSize = chunkSize
/** @type {number} */
this.cursor = 0
/** @type {number} */
this.maxCursor = -1
/** @type {(Uint8Array|number[])[]} */
this.chunks = []
// keep the first chunk around if we can to save allocations for future encodes
/** @type {Uint8Array|number[]|null} */
this._initReuseChunk = null
}
reset () {
this.cursor = 0
this.maxCursor = -1
if (this.chunks.length) {
this.chunks = []
}
if (this._initReuseChunk !== null) {
this.chunks.push(this._initReuseChunk)
this.maxCursor = this._initReuseChunk.length - 1
}
}
/**
* @param {Uint8Array|number[]} bytes
*/
push (bytes) {
let topChunk = this.chunks[this.chunks.length - 1]
const newMax = this.cursor + bytes.length
if (newMax <= this.maxCursor + 1) {
// we have at least one chunk and we can fit these bytes into that chunk
const chunkPos = topChunk.length - (this.maxCursor - this.cursor) - 1
// @ts-ignore
topChunk.set(bytes, chunkPos)
} else {
// can't fit it in
if (topChunk) {
// trip the last chunk to `cursor` if we need to
const chunkPos = topChunk.length - (this.maxCursor - this.cursor) - 1
if (chunkPos < topChunk.length) {
// @ts-ignore
this.chunks[this.chunks.length - 1] = topChunk.subarray(0, chunkPos)
this.maxCursor = this.cursor - 1
}
}
if (bytes.length < 64 && bytes.length < this.chunkSize) {
// make a new chunk and copy the new one into it
topChunk = alloc(this.chunkSize)
this.chunks.push(topChunk)
this.maxCursor += topChunk.length
if (this._initReuseChunk === null) {
this._initReuseChunk = topChunk
}
// @ts-ignore
topChunk.set(bytes, 0)
} else {
// push the new bytes in as its own chunk
this.chunks.push(bytes)
this.maxCursor += bytes.length
}
}
this.cursor += bytes.length
}
/**
* @param {boolean} [reset]
* @returns {Uint8Array}
*/
toBytes (reset = false) {
let byts
if (this.chunks.length === 1) {
const chunk = this.chunks[0]
if (reset && this.cursor > chunk.length / 2) {
/* c8 ignore next 2 */
// @ts-ignore
byts = this.cursor === chunk.length ? chunk : chunk.subarray(0, this.cursor)
this._initReuseChunk = null
this.chunks = []
} else {
// @ts-ignore
byts = slice(chunk, 0, this.cursor)
}
} else {
// @ts-ignore
byts = concat(this.chunks, this.cursor)
}
if (reset) {
this.reset()
}
return byts
}
}
+474
View File
@@ -0,0 +1,474 @@
// Use Uint8Array directly in the browser, use Buffer in Node.js but don't
// speak its name directly to avoid bundlers pulling in the `Buffer` polyfill
// @ts-ignore
export const useBuffer = globalThis.process &&
// @ts-ignore
!globalThis.process.browser &&
// @ts-ignore
globalThis.Buffer &&
// @ts-ignore
typeof globalThis.Buffer.isBuffer === 'function'
const textDecoder = new TextDecoder()
const textEncoder = new TextEncoder()
/**
* @param {Uint8Array} buf
* @returns {boolean}
*/
function isBuffer (buf) {
// @ts-ignore
return useBuffer && globalThis.Buffer.isBuffer(buf)
}
/**
* @param {Uint8Array|number[]} buf
* @returns {Uint8Array}
*/
export function asU8A (buf) {
/* c8 ignore next */
if (!(buf instanceof Uint8Array)) {
return Uint8Array.from(buf)
}
return isBuffer(buf) ? new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength) : buf
}
export const toString = useBuffer
? // eslint-disable-line operator-linebreak
/**
* @param {Uint8Array} bytes
* @param {number} start
* @param {number} end
*/
(bytes, start, end) => {
return end - start > 64
? // eslint-disable-line operator-linebreak
// @ts-ignore
globalThis.Buffer.from(bytes.subarray(start, end)).toString('utf8')
: utf8Slice(bytes, start, end)
}
/* c8 ignore next 11 */
: // eslint-disable-line operator-linebreak
/**
* @param {Uint8Array} bytes
* @param {number} start
* @param {number} end
*/
(bytes, start, end) => {
return end - start > 64
? textDecoder.decode(bytes.subarray(start, end))
: utf8Slice(bytes, start, end)
}
export const fromString = useBuffer
? // eslint-disable-line operator-linebreak
/**
* @param {string} string
*/
(string) => {
return string.length > 64
? // eslint-disable-line operator-linebreak
// @ts-ignore
globalThis.Buffer.from(string)
: utf8ToBytes(string)
}
/* c8 ignore next 7 */
: // eslint-disable-line operator-linebreak
/**
* @param {string} string
*/
(string) => {
return string.length > 64 ? textEncoder.encode(string) : utf8ToBytes(string)
}
/**
* Buffer variant not fast enough for what we need
* @param {number[]} arr
* @returns {Uint8Array}
*/
export const fromArray = (arr) => {
return Uint8Array.from(arr)
}
export const slice = useBuffer
? // eslint-disable-line operator-linebreak
/**
* @param {Uint8Array} bytes
* @param {number} start
* @param {number} end
*/
(bytes, start, end) => {
if (isBuffer(bytes)) {
return new Uint8Array(bytes.subarray(start, end))
}
return bytes.slice(start, end)
}
/* c8 ignore next 9 */
: // eslint-disable-line operator-linebreak
/**
* @param {Uint8Array} bytes
* @param {number} start
* @param {number} end
*/
(bytes, start, end) => {
return bytes.slice(start, end)
}
export const concat = useBuffer
? // eslint-disable-line operator-linebreak
/**
* @param {Uint8Array[]} chunks
* @param {number} length
* @returns {Uint8Array}
*/
(chunks, length) => {
// might get a stray plain Array here
/* c8 ignore next 1 */
chunks = chunks.map((c) => c instanceof Uint8Array
? c
// this case is occasionally missed during test runs so becomes coverage-flaky
/* c8 ignore next 4 */
: // eslint-disable-line operator-linebreak
// @ts-ignore
globalThis.Buffer.from(c))
// @ts-ignore
return asU8A(globalThis.Buffer.concat(chunks, length))
}
/* c8 ignore next 19 */
: // eslint-disable-line operator-linebreak
/**
* @param {Uint8Array[]} chunks
* @param {number} length
* @returns {Uint8Array}
*/
(chunks, length) => {
const out = new Uint8Array(length)
let off = 0
for (let b of chunks) {
if (off + b.length > out.length) {
// final chunk that's bigger than we need
b = b.subarray(0, out.length - off)
}
out.set(b, off)
off += b.length
}
return out
}
export const alloc = useBuffer
? // eslint-disable-line operator-linebreak
/**
* @param {number} size
* @returns {Uint8Array}
*/
(size) => {
// we always write over the contents we expose so this should be safe
// @ts-ignore
return globalThis.Buffer.allocUnsafe(size)
}
/* c8 ignore next 8 */
: // eslint-disable-line operator-linebreak
/**
* @param {number} size
* @returns {Uint8Array}
*/
(size) => {
return new Uint8Array(size)
}
export const toHex = useBuffer
? // eslint-disable-line operator-linebreak
/**
* @param {Uint8Array} d
* @returns {string}
*/
(d) => {
if (typeof d === 'string') {
return d
}
// @ts-ignore
return globalThis.Buffer.from(toBytes(d)).toString('hex')
}
/* c8 ignore next 12 */
: // eslint-disable-line operator-linebreak
/**
* @param {Uint8Array} d
* @returns {string}
*/
(d) => {
if (typeof d === 'string') {
return d
}
// @ts-ignore not smart enough to figure this out
return Array.prototype.reduce.call(toBytes(d), (p, c) => `${p}${c.toString(16).padStart(2, '0')}`, '')
}
export const fromHex = useBuffer
? // eslint-disable-line operator-linebreak
/**
* @param {string|Uint8Array} hex
* @returns {Uint8Array}
*/
(hex) => {
if (hex instanceof Uint8Array) {
return hex
}
// @ts-ignore
return globalThis.Buffer.from(hex, 'hex')
}
/* c8 ignore next 17 */
: // eslint-disable-line operator-linebreak
/**
* @param {string|Uint8Array} hex
* @returns {Uint8Array}
*/
(hex) => {
if (hex instanceof Uint8Array) {
return hex
}
if (!hex.length) {
return new Uint8Array(0)
}
return new Uint8Array(hex.split('')
.map((/** @type {string} */ c, /** @type {number} */ i, /** @type {string[]} */ d) => i % 2 === 0 ? `0x${c}${d[i + 1]}` : '')
.filter(Boolean)
.map((/** @type {string} */ e) => parseInt(e, 16)))
}
/**
* @param {Uint8Array|ArrayBuffer|ArrayBufferView} obj
* @returns {Uint8Array}
*/
function toBytes (obj) {
if (obj instanceof Uint8Array && obj.constructor.name === 'Uint8Array') {
return obj
}
if (obj instanceof ArrayBuffer) {
return new Uint8Array(obj)
}
if (ArrayBuffer.isView(obj)) {
return new Uint8Array(obj.buffer, obj.byteOffset, obj.byteLength)
}
/* c8 ignore next */
throw new Error('Unknown type, must be binary type')
}
/**
* @param {Uint8Array} b1
* @param {Uint8Array} b2
* @returns {number}
*/
export function compare (b1, b2) {
/* c8 ignore next 5 */
if (isBuffer(b1) && isBuffer(b2)) {
// probably not possible to get here in the current API
// @ts-ignore Buffer
return b1.compare(b2)
}
for (let i = 0; i < b1.length; i++) {
if (b1[i] === b2[i]) {
continue
}
return b1[i] < b2[i] ? -1 : 1
} /* c8 ignore next 3 */
return 0
}
// The below code is mostly taken from https://github.com/feross/buffer
// Licensed MIT. Copyright (c) Feross Aboukhadijeh
/**
* @param {string} string
* @param {number} [units]
* @returns {number[]}
*/
function utf8ToBytes (string, units = Infinity) {
let codePoint
const length = string.length
let leadSurrogate = null
const bytes = []
for (let i = 0; i < length; ++i) {
codePoint = string.charCodeAt(i)
// is surrogate component
if (codePoint > 0xd7ff && codePoint < 0xe000) {
// last char was a lead
if (!leadSurrogate) {
// no lead yet
/* c8 ignore next 9 */
if (codePoint > 0xdbff) {
// unexpected trail
if ((units -= 3) > -1) bytes.push(0xef, 0xbf, 0xbd)
continue
} else if (i + 1 === length) {
// unpaired lead
if ((units -= 3) > -1) bytes.push(0xef, 0xbf, 0xbd)
continue
}
// valid lead
leadSurrogate = codePoint
continue
}
// 2 leads in a row
/* c8 ignore next 5 */
if (codePoint < 0xdc00) {
if ((units -= 3) > -1) bytes.push(0xef, 0xbf, 0xbd)
leadSurrogate = codePoint
continue
}
// valid surrogate pair
codePoint = (leadSurrogate - 0xd800 << 10 | codePoint - 0xdc00) + 0x10000
/* c8 ignore next 4 */
} else if (leadSurrogate) {
// valid bmp char, but last char was a lead
if ((units -= 3) > -1) bytes.push(0xef, 0xbf, 0xbd)
}
leadSurrogate = null
// encode utf8
if (codePoint < 0x80) {
/* c8 ignore next 1 */
if ((units -= 1) < 0) break
bytes.push(codePoint)
} else if (codePoint < 0x800) {
/* c8 ignore next 1 */
if ((units -= 2) < 0) break
bytes.push(
codePoint >> 0x6 | 0xc0,
codePoint & 0x3f | 0x80
)
} else if (codePoint < 0x10000) {
/* c8 ignore next 1 */
if ((units -= 3) < 0) break
bytes.push(
codePoint >> 0xc | 0xe0,
codePoint >> 0x6 & 0x3f | 0x80,
codePoint & 0x3f | 0x80
)
/* c8 ignore next 9 */
} else if (codePoint < 0x110000) {
if ((units -= 4) < 0) break
bytes.push(
codePoint >> 0x12 | 0xf0,
codePoint >> 0xc & 0x3f | 0x80,
codePoint >> 0x6 & 0x3f | 0x80,
codePoint & 0x3f | 0x80
)
} else {
/* c8 ignore next 2 */
throw new Error('Invalid code point')
}
}
return bytes
}
/**
* @param {Uint8Array} buf
* @param {number} offset
* @param {number} end
* @returns {string}
*/
function utf8Slice (buf, offset, end) {
const res = []
while (offset < end) {
const firstByte = buf[offset]
let codePoint = null
let bytesPerSequence = (firstByte > 0xef) ? 4 : (firstByte > 0xdf) ? 3 : (firstByte > 0xbf) ? 2 : 1
if (offset + bytesPerSequence <= end) {
let secondByte, thirdByte, fourthByte, tempCodePoint
switch (bytesPerSequence) {
case 1:
if (firstByte < 0x80) {
codePoint = firstByte
}
break
case 2:
secondByte = buf[offset + 1]
if ((secondByte & 0xc0) === 0x80) {
tempCodePoint = (firstByte & 0x1f) << 0x6 | (secondByte & 0x3f)
if (tempCodePoint > 0x7f) {
codePoint = tempCodePoint
}
}
break
case 3:
secondByte = buf[offset + 1]
thirdByte = buf[offset + 2]
if ((secondByte & 0xc0) === 0x80 && (thirdByte & 0xc0) === 0x80) {
tempCodePoint = (firstByte & 0xf) << 0xc | (secondByte & 0x3f) << 0x6 | (thirdByte & 0x3f)
/* c8 ignore next 3 */
if (tempCodePoint > 0x7ff && (tempCodePoint < 0xd800 || tempCodePoint > 0xdfff)) {
codePoint = tempCodePoint
}
}
break
case 4:
secondByte = buf[offset + 1]
thirdByte = buf[offset + 2]
fourthByte = buf[offset + 3]
if ((secondByte & 0xc0) === 0x80 && (thirdByte & 0xc0) === 0x80 && (fourthByte & 0xc0) === 0x80) {
tempCodePoint = (firstByte & 0xf) << 0x12 | (secondByte & 0x3f) << 0xc | (thirdByte & 0x3f) << 0x6 | (fourthByte & 0x3f)
if (tempCodePoint > 0xffff && tempCodePoint < 0x110000) {
codePoint = tempCodePoint
}
}
}
}
/* c8 ignore next 5 */
if (codePoint === null) {
// we did not generate a valid codePoint so insert a
// replacement char (U+FFFD) and advance only 1 byte
codePoint = 0xfffd
bytesPerSequence = 1
} else if (codePoint > 0xffff) {
// encode to utf16 (surrogate pair dance)
codePoint -= 0x10000
res.push(codePoint >>> 10 & 0x3ff | 0xd800)
codePoint = 0xdc00 | codePoint & 0x3ff
}
res.push(codePoint)
offset += bytesPerSequence
}
return decodeCodePointsArray(res)
}
// Based on http://stackoverflow.com/a/22747272/680742, the browser with
// the lowest limit is Chrome, with 0x10000 args.
// We go 1 magnitude less, for safety
const MAX_ARGUMENTS_LENGTH = 0x1000
/**
* @param {number[]} codePoints
* @returns {string}
*/
export function decodeCodePointsArray (codePoints) {
const len = codePoints.length
if (len <= MAX_ARGUMENTS_LENGTH) {
return String.fromCharCode.apply(String, codePoints) // avoid extra slice()
}
/* c8 ignore next 10 */
// Decode in chunks to avoid "call stack size exceeded".
let res = ''
let i = 0
while (i < len) {
res += String.fromCharCode.apply(
String,
codePoints.slice(i, i += MAX_ARGUMENTS_LENGTH)
)
}
return res
}
+27
View File
@@ -0,0 +1,27 @@
const decodeErrPrefix = 'CBOR decode error:'
const encodeErrPrefix = 'CBOR encode error:'
const uintMinorPrefixBytes = []
uintMinorPrefixBytes[23] = 1
uintMinorPrefixBytes[24] = 2
uintMinorPrefixBytes[25] = 3
uintMinorPrefixBytes[26] = 5
uintMinorPrefixBytes[27] = 9
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} need
*/
function assertEnoughData (data, pos, need) {
if (data.length - pos < need) {
throw new Error(`${decodeErrPrefix} not enough data for type`)
}
}
export {
decodeErrPrefix,
encodeErrPrefix,
uintMinorPrefixBytes,
assertEnoughData
}
+195
View File
@@ -0,0 +1,195 @@
import { decodeErrPrefix } from './common.js'
import { Type } from './token.js'
import { jump, quick } from './jump.js'
/**
* @typedef {import('./token.js').Token} Token
* @typedef {import('../interface').DecodeOptions} DecodeOptions
* @typedef {import('../interface').DecodeTokenizer} DecodeTokenizer
*/
const defaultDecodeOptions = {
strict: false,
allowIndefinite: true,
allowUndefined: true,
allowBigInt: true
}
/**
* @implements {DecodeTokenizer}
*/
class Tokeniser {
/**
* @param {Uint8Array} data
* @param {DecodeOptions} options
*/
constructor (data, options = {}) {
this.pos = 0
this.data = data
this.options = options
}
done () {
return this.pos >= this.data.length
}
next () {
const byt = this.data[this.pos]
let token = quick[byt]
if (token === undefined) {
const decoder = jump[byt]
/* c8 ignore next 4 */
// if we're here then there's something wrong with our jump or quick lists!
if (!decoder) {
throw new Error(`${decodeErrPrefix} no decoder for major type ${byt >>> 5} (byte 0x${byt.toString(16).padStart(2, '0')})`)
}
const minor = byt & 31
token = decoder(this.data, this.pos, minor, this.options)
}
// @ts-ignore we get to assume encodedLength is set (crossing fingers slightly)
this.pos += token.encodedLength
return token
}
}
const DONE = Symbol.for('DONE')
const BREAK = Symbol.for('BREAK')
/**
* @param {Token} token
* @param {DecodeTokenizer} tokeniser
* @param {DecodeOptions} options
* @returns {any|BREAK|DONE}
*/
function tokenToArray (token, tokeniser, options) {
const arr = []
for (let i = 0; i < token.value; i++) {
const value = tokensToObject(tokeniser, options)
if (value === BREAK) {
if (token.value === Infinity) {
// normal end to indefinite length array
break
}
throw new Error(`${decodeErrPrefix} got unexpected break to lengthed array`)
}
if (value === DONE) {
throw new Error(`${decodeErrPrefix} found array but not enough entries (got ${i}, expected ${token.value})`)
}
arr[i] = value
}
return arr
}
/**
* @param {Token} token
* @param {DecodeTokenizer} tokeniser
* @param {DecodeOptions} options
* @returns {any|BREAK|DONE}
*/
function tokenToMap (token, tokeniser, options) {
const useMaps = options.useMaps === true
const obj = useMaps ? undefined : {}
const m = useMaps ? new Map() : undefined
for (let i = 0; i < token.value; i++) {
const key = tokensToObject(tokeniser, options)
if (key === BREAK) {
if (token.value === Infinity) {
// normal end to indefinite length map
break
}
throw new Error(`${decodeErrPrefix} got unexpected break to lengthed map`)
}
if (key === DONE) {
throw new Error(`${decodeErrPrefix} found map but not enough entries (got ${i} [no key], expected ${token.value})`)
}
if (useMaps !== true && typeof key !== 'string') {
throw new Error(`${decodeErrPrefix} non-string keys not supported (got ${typeof key})`)
}
if (options.rejectDuplicateMapKeys === true) {
// @ts-ignore
if ((useMaps && m.has(key)) || (!useMaps && (key in obj))) {
throw new Error(`${decodeErrPrefix} found repeat map key "${key}"`)
}
}
const value = tokensToObject(tokeniser, options)
if (value === DONE) {
throw new Error(`${decodeErrPrefix} found map but not enough entries (got ${i} [no value], expected ${token.value})`)
}
if (useMaps) {
// @ts-ignore TODO reconsider this .. maybe needs to be strict about key types
m.set(key, value)
} else {
// @ts-ignore TODO reconsider this .. maybe needs to be strict about key types
obj[key] = value
}
}
// @ts-ignore c'mon man
return useMaps ? m : obj
}
/**
* @param {DecodeTokenizer} tokeniser
* @param {DecodeOptions} options
* @returns {any|BREAK|DONE}
*/
function tokensToObject (tokeniser, options) {
// should we support array as an argument?
// check for tokenIter[Symbol.iterator] and replace tokenIter with what that returns?
if (tokeniser.done()) {
return DONE
}
const token = tokeniser.next()
if (token.type === Type.break) {
return BREAK
}
if (token.type.terminal) {
return token.value
}
if (token.type === Type.array) {
return tokenToArray(token, tokeniser, options)
}
if (token.type === Type.map) {
return tokenToMap(token, tokeniser, options)
}
if (token.type === Type.tag) {
if (options.tags && typeof options.tags[token.value] === 'function') {
const tagged = tokensToObject(tokeniser, options)
return options.tags[token.value](tagged)
}
throw new Error(`${decodeErrPrefix} tag not supported (${token.value})`)
}
/* c8 ignore next */
throw new Error('unsupported')
}
/**
* @param {Uint8Array} data
* @param {DecodeOptions} [options]
* @returns {any}
*/
function decode (data, options) {
if (!(data instanceof Uint8Array)) {
throw new Error(`${decodeErrPrefix} data to decode must be a Uint8Array`)
}
options = Object.assign({}, defaultDecodeOptions, options)
const tokeniser = options.tokenizer || new Tokeniser(data, options)
const decoded = tokensToObject(tokeniser, options)
if (decoded === DONE) {
throw new Error(`${decodeErrPrefix} did not find any content to decode`)
}
if (decoded === BREAK) {
throw new Error(`${decodeErrPrefix} got unexpected break`)
}
if (!tokeniser.done()) {
throw new Error(`${decodeErrPrefix} too many terminals, data makes no sense`)
}
return decoded
}
export { Tokeniser, tokensToObject, decode }
+154
View File
@@ -0,0 +1,154 @@
import { Tokeniser } from './decode.js'
import { toHex, fromHex } from './byte-utils.js'
import { uintBoundaries } from './0uint.js'
const utf8Encoder = new TextEncoder()
const utf8Decoder = new TextDecoder()
/**
* @param {Uint8Array} inp
* @param {number} [width]
*/
function * tokensToDiagnostic (inp, width = 100) {
const tokeniser = new Tokeniser(inp, { retainStringBytes: true, allowBigInt: true })
let pos = 0
const indent = []
/**
* @param {number} start
* @param {number} length
* @returns {string}
*/
const slc = (start, length) => {
return toHex(inp.slice(pos + start, pos + start + length))
}
while (!tokeniser.done()) {
const token = tokeniser.next()
let margin = ''.padStart(indent.length * 2, ' ')
// @ts-ignore should be safe for decode
let vLength = token.encodedLength - 1
/** @type {string|number} */
let v = String(token.value)
let outp = `${margin}${slc(0, 1)}`
const str = token.type.name === 'bytes' || token.type.name === 'string'
if (token.type.name === 'string') {
v = v.length
vLength -= v
} else if (token.type.name === 'bytes') {
v = token.value.length
// @ts-ignore
vLength -= v
}
let multilen
switch (token.type.name) {
case 'string':
case 'bytes':
case 'map':
case 'array':
// for bytes and string, we want to print out the length part of the value prefix if it
// exists - it exists for short lengths (<24) but does for longer lengths
multilen = token.type.name === 'string' ? utf8Encoder.encode(token.value).length : token.value.length
if (multilen >= uintBoundaries[0]) {
if (multilen < uintBoundaries[1]) {
outp += ` ${slc(1, 1)}`
} else if (multilen < uintBoundaries[2]) {
outp += ` ${slc(1, 2)}`
/* c8 ignore next 5 */
} else if (multilen < uintBoundaries[3]) { // sus
outp += ` ${slc(1, 4)}`
} else if (multilen < uintBoundaries[4]) { // orly?
outp += ` ${slc(1, 8)}`
}
}
break
default:
// print the value if it's not compacted into the first byte
outp += ` ${slc(1, vLength)}`
break
}
outp = outp.padEnd(width / 2, ' ')
outp += `# ${margin}${token.type.name}`
if (token.type.name !== v) {
outp += `(${v})`
}
yield outp
if (str) {
let asString = token.type.name === 'string'
margin += ' '
let repr = asString ? utf8Encoder.encode(token.value) : token.value
if (asString && token.byteValue !== undefined) {
if (repr.length !== token.byteValue.length) {
// bail on printing this as a string, it's probably not utf8, so treat it as bytes
// (you can probably blame a Go programmer for this)
repr = token.byteValue
asString = false
}
}
const wh = ((width / 2) - margin.length - 1) / 2
let snip = 0
while (repr.length - snip > 0) {
const piece = repr.slice(snip, snip + wh)
snip += piece.length
const st = asString
? utf8Decoder.decode(piece)
: piece.reduce((/** @type {string} */ p, /** @type {number} */ c) => {
if (c < 0x20 || (c >= 0x7f && c < 0xa1) || c === 0xad) {
return `${p}\\x${c.toString(16).padStart(2, '0')}`
}
return `${p}${String.fromCharCode(c)}`
}, '')
yield `${margin}${toHex(piece)}`.padEnd(width / 2, ' ') + `# ${margin}"${st}"`
}
}
if (indent.length) {
indent[indent.length - 1]--
}
if (!token.type.terminal) {
switch (token.type.name) {
case 'map':
indent.push(token.value * 2)
break
case 'array':
indent.push(token.value)
break
// TODO: test tags .. somehow
/* c8 ignore next 5 */
case 'tag':
indent.push(1)
break
default:
throw new Error(`Unknown token type '${token.type.name}'`)
}
}
while (indent.length && indent[indent.length - 1] <= 0) {
indent.pop()
}
// @ts-ignore it should be set on a decode operation
pos += token.encodedLength
}
}
/**
* Convert an input string formatted as CBOR diagnostic output into binary CBOR form.
* @param {string} input
* @returns {Uint8Array}
*/
function fromDiag (input) {
/* c8 ignore next 3 */
if (typeof input !== 'string') {
throw new TypeError('Expected string input')
}
input = input.replace(/#.*?$/mg, '').replace(/[\s\r\n]+/mg, '')
/* c8 ignore next 3 */
if (/[^a-f0-9]/i.test(input)) {
throw new TypeError('Input string was not CBOR diagnostic format')
}
return fromHex(input)
}
export { tokensToDiagnostic, fromDiag }
+117
View File
@@ -0,0 +1,117 @@
import { tokensToDiagnostic } from './diagnostic.js'
import { fromHex } from './byte-utils.js'
const inp = `
a7
62
6964
78 40
6469643a6578616d706c653a31324433
4b6f6f574d4864727a6377706a626472
5a733547477145524176636771583362
3564707550745061396f743639796577
65
70726f6f66
a4
64
74797065
74
656432353531395369676e617475726532303138
67
63726561746564
74
323032302d30352d30315430333a30303a30325a
67
63726561746f72
78 8c
6469643a6578616d706c653a31324433
4b6f6f574d4864727a6377706a626472
5a733547477145524176636771583362
3564707550745061396f743639796577
3b206578616d706c653a6b65793d6964
3d626166797265696375627478357771
6f336e6f73633463617a726b63746668
776436726577657a6770776f65347377
69726c733465626468733269
6e
7369676e617475726556616c7565
78 58
6f3972364c78676f474e38466f616565
554136456444637631324776447a4645
6d43676a577a76707572325953517941
3857327230535357554b2b6e4835744d
717a61464c756e3677775a31456f7433
37616d4744673d3d
67
63726561746564
74
323031382d31322d30315430333a30303a30305a
67
75706461746564
74
323032302d30352d30315430333a30303a30305a
68
40636f6e74657874
78 1c
68747470733a2f2f7777772e77332e6f
72672f6e732f6469642f7631
69
7075626c69634b6579
81
a5
62
6964
78 85
6261667972656963756274783577716f
336e6f73633463617a726b6374666877
6436726577657a6770776f6534737769
726c7334656264687332693b6578616d
706c653a6b65793d6964626166797265
6963756274783577716f336e6f736334
63617a726b6374666877643672657765
7a6770776f6534737769726c73346562
6468733269
64
74797065
6e
45644473615075626c69634b6579
65
6375727665
67
65643235353139
67
65787069726573
74
323031392d31322d30315430333a30303a30305a
6f
7075626c69634b6579426173653634
78 2c
716d7a3774704c4e4b4b4b646c376344
375062656a4469425670374f4e706d5a
62666d633763454b396d673d
6e
61757468656e7469636174696f6e
81
78 83
6469643a6578616d706c653a31324433
4b6f6f574d4864727a6377706a626472
5a733547477145524176636771583362
3564707550745061396f743639796577
3b6b65792d69643d6261667972656963
756274783577716f336e6f7363346361
7a726b63746668776436726577657a67
70776f6534737769726c733465626468
733269
`.replace(/[\n ]/g, '')
// tokensToDiagnostic(fromHex('861864fb4376345785d8a0002063796570428411fb3ff199999999999a'))
for (const line of tokensToDiagnostic(fromHex('851864fb4376345785d8a00020a661624284116166f46168f7616ef6617379012c7965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965707965706174f5fb3ff199999999999a'))) {
console.log(line)
}
for (const line of tokensToDiagnostic(fromHex(inp))) {
console.log(line)
}
for (const line of tokensToDiagnostic(fromHex('82410181a161318182410041ff'))) {
console.log(line)
}
+464
View File
@@ -0,0 +1,464 @@
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 }
+106
View File
@@ -0,0 +1,106 @@
// This is an unfortunate replacement for @sindresorhus/is that we need to
// re-implement for performance purposes. In particular the is.observable()
// check is expensive, and unnecessary for our purposes. The values returned
// are compatible with @sindresorhus/is, however.
const typeofs = [
'string',
'number',
'bigint',
'symbol'
]
const objectTypeNames = [
'Function',
'Generator',
'AsyncGenerator',
'GeneratorFunction',
'AsyncGeneratorFunction',
'AsyncFunction',
'Observable',
'Array',
'Buffer',
'Object',
'RegExp',
'Date',
'Error',
'Map',
'Set',
'WeakMap',
'WeakSet',
'ArrayBuffer',
'SharedArrayBuffer',
'DataView',
'Promise',
'URL',
'HTMLElement',
'Int8Array',
'Uint8Array',
'Uint8ClampedArray',
'Int16Array',
'Uint16Array',
'Int32Array',
'Uint32Array',
'Float32Array',
'Float64Array',
'BigInt64Array',
'BigUint64Array'
]
/**
* @param {any} value
* @returns {string}
*/
export function is (value) {
if (value === null) {
return 'null'
}
if (value === undefined) {
return 'undefined'
}
if (value === true || value === false) {
return 'boolean'
}
const typeOf = typeof value
if (typeofs.includes(typeOf)) {
return typeOf
}
/* c8 ignore next 4 */
// not going to bother testing this, it's not going to be valid anyway
if (typeOf === 'function') {
return 'Function'
}
if (Array.isArray(value)) {
return 'Array'
}
if (isBuffer(value)) {
return 'Buffer'
}
const objectType = getObjectType(value)
if (objectType) {
return objectType
}
/* c8 ignore next */
return 'Object'
}
/**
* @param {any} value
* @returns {boolean}
*/
function isBuffer (value) {
return value && value.constructor && value.constructor.isBuffer && value.constructor.isBuffer.call(null, value)
}
/**
* @param {any} value
* @returns {string|undefined}
*/
function getObjectType (value) {
const objectTypeName = Object.prototype.toString.call(value).slice(8, -1)
if (objectTypeNames.includes(objectTypeName)) {
return objectTypeName
}
/* c8 ignore next */
return undefined
}
+446
View File
@@ -0,0 +1,446 @@
import { decode as _decode } from '../decode.js'
import { Token, Type } from '../token.js'
import { decodeCodePointsArray } from '../byte-utils.js'
import { decodeErrPrefix } from '../common.js'
/**
* @typedef {import('../../interface').DecodeOptions} DecodeOptions
* @typedef {import('../../interface').DecodeTokenizer} DecodeTokenizer
*/
/**
* @implements {DecodeTokenizer}
*/
class Tokenizer {
/**
* @param {Uint8Array} data
* @param {DecodeOptions} options
*/
constructor (data, options = {}) {
this.pos = 0
this.data = data
this.options = options
/** @type {string[]} */
this.modeStack = ['value']
this.lastToken = ''
}
/**
* @returns {boolean}
*/
done () {
return this.pos >= this.data.length
}
/**
* @returns {number}
*/
ch () {
return this.data[this.pos]
}
/**
* @returns {string}
*/
currentMode () {
return this.modeStack[this.modeStack.length - 1]
}
skipWhitespace () {
let c = this.ch()
// @ts-ignore
while (c === 32 /* ' ' */ || c === 9 /* '\t' */ || c === 13 /* '\r' */ || c === 10 /* '\n' */) {
c = this.data[++this.pos]
}
}
/**
* @param {number[]} str
*/
expect (str) {
if (this.data.length - this.pos < str.length) {
throw new Error(`${decodeErrPrefix} unexpected end of input at position ${this.pos}`)
}
for (let i = 0; i < str.length; i++) {
if (this.data[this.pos++] !== str[i]) {
throw new Error(`${decodeErrPrefix} unexpected token at position ${this.pos}, expected to find '${String.fromCharCode(...str)}'`)
}
}
}
parseNumber () {
const startPos = this.pos
let negative = false
let float = false
/**
* @param {number[]} chars
*/
const swallow = (chars) => {
while (!this.done()) {
const ch = this.ch()
if (chars.includes(ch)) {
this.pos++
} else {
break
}
}
}
// lead
if (this.ch() === 45) { // '-'
negative = true
this.pos++
}
if (this.ch() === 48) { // '0'
this.pos++
if (this.ch() === 46) { // '.'
this.pos++
float = true
} else {
return new Token(Type.uint, 0, this.pos - startPos)
}
}
swallow([48, 49, 50, 51, 52, 53, 54, 55, 56, 57]) // DIGIT
if (negative && this.pos === startPos + 1) {
throw new Error(`${decodeErrPrefix} unexpected token at position ${this.pos}`)
}
if (!this.done() && this.ch() === 46) { // '.'
if (float) {
throw new Error(`${decodeErrPrefix} unexpected token at position ${this.pos}`)
}
float = true
this.pos++
swallow([48, 49, 50, 51, 52, 53, 54, 55, 56, 57]) // DIGIT
}
if (!this.done() && (this.ch() === 101 || this.ch() === 69)) { // '[eE]'
float = true
this.pos++
if (!this.done() && (this.ch() === 43 || this.ch() === 45)) { // '+', '-'
this.pos++
}
swallow([48, 49, 50, 51, 52, 53, 54, 55, 56, 57]) // DIGIT
}
// @ts-ignore
const numStr = String.fromCharCode.apply(null, this.data.subarray(startPos, this.pos))
const num = parseFloat(numStr)
if (float) {
return new Token(Type.float, num, this.pos - startPos)
}
if (this.options.allowBigInt !== true || Number.isSafeInteger(num)) {
return new Token(num >= 0 ? Type.uint : Type.negint, num, this.pos - startPos)
}
return new Token(num >= 0 ? Type.uint : Type.negint, BigInt(numStr), this.pos - startPos)
}
/**
* @returns {Token}
*/
parseString () {
/* c8 ignore next 4 */
if (this.ch() !== 34) { // '"'
// this would be a programming error
throw new Error(`${decodeErrPrefix} unexpected character at position ${this.pos}; this shouldn't happen`)
}
this.pos++
// check for simple fast-path, all printable ascii, no escapes
// >0x10000 elements may fail fn.apply() (http://stackoverflow.com/a/22747272/680742)
for (let i = this.pos, l = 0; i < this.data.length && l < 0x10000; i++, l++) {
const ch = this.data[i]
if (ch === 92 || ch < 32 || ch >= 128) { // '\', ' ', control-chars or non-trivial
break
}
if (ch === 34) { // '"'
// @ts-ignore
const str = String.fromCharCode.apply(null, this.data.subarray(this.pos, i))
this.pos = i + 1
return new Token(Type.string, str, l)
}
}
const startPos = this.pos
const chars = []
const readu4 = () => {
if (this.pos + 4 >= this.data.length) {
throw new Error(`${decodeErrPrefix} unexpected end of unicode escape sequence at position ${this.pos}`)
}
let u4 = 0
for (let i = 0; i < 4; i++) {
let ch = this.ch()
if (ch >= 48 && ch <= 57) { // '0' && '9'
ch -= 48
} else if (ch >= 97 && ch <= 102) { // 'a' && 'f'
ch = ch - 97 + 10
} else if (ch >= 65 && ch <= 70) { // 'A' && 'F'
ch = ch - 65 + 10
} else {
throw new Error(`${decodeErrPrefix} unexpected unicode escape character at position ${this.pos}`)
}
u4 = u4 * 16 + ch
this.pos++
}
return u4
}
// mostly taken from feross/buffer and adjusted to fit
const readUtf8Char = () => {
const firstByte = this.ch()
let codePoint = null
/* c8 ignore next 1 */
let bytesPerSequence = (firstByte > 0xef) ? 4 : (firstByte > 0xdf) ? 3 : (firstByte > 0xbf) ? 2 : 1
if (this.pos + bytesPerSequence > this.data.length) {
throw new Error(`${decodeErrPrefix} unexpected unicode sequence at position ${this.pos}`)
}
let secondByte, thirdByte, fourthByte, tempCodePoint
switch (bytesPerSequence) {
/* c8 ignore next 6 */
// this case is dealt with by the caller function
case 1:
if (firstByte < 0x80) {
codePoint = firstByte
}
break
case 2:
secondByte = this.data[this.pos + 1]
if ((secondByte & 0xc0) === 0x80) {
tempCodePoint = (firstByte & 0x1f) << 0x6 | (secondByte & 0x3f)
if (tempCodePoint > 0x7f) {
codePoint = tempCodePoint
}
}
break
case 3:
secondByte = this.data[this.pos + 1]
thirdByte = this.data[this.pos + 2]
if ((secondByte & 0xc0) === 0x80 && (thirdByte & 0xc0) === 0x80) {
tempCodePoint = (firstByte & 0xf) << 0xc | (secondByte & 0x3f) << 0x6 | (thirdByte & 0x3f)
/* c8 ignore next 3 */
if (tempCodePoint > 0x7ff && (tempCodePoint < 0xd800 || tempCodePoint > 0xdfff)) {
codePoint = tempCodePoint
}
}
break
case 4:
secondByte = this.data[this.pos + 1]
thirdByte = this.data[this.pos + 2]
fourthByte = this.data[this.pos + 3]
if ((secondByte & 0xc0) === 0x80 && (thirdByte & 0xc0) === 0x80 && (fourthByte & 0xc0) === 0x80) {
tempCodePoint = (firstByte & 0xf) << 0x12 | (secondByte & 0x3f) << 0xc | (thirdByte & 0x3f) << 0x6 | (fourthByte & 0x3f)
if (tempCodePoint > 0xffff && tempCodePoint < 0x110000) {
codePoint = tempCodePoint
}
}
}
/* c8 ignore next 5 */
if (codePoint === null) {
// we did not generate a valid codePoint so insert a
// replacement char (U+FFFD) and advance only 1 byte
codePoint = 0xfffd
bytesPerSequence = 1
} else if (codePoint > 0xffff) {
// encode to utf16 (surrogate pair dance)
codePoint -= 0x10000
chars.push(codePoint >>> 10 & 0x3ff | 0xd800)
codePoint = 0xdc00 | codePoint & 0x3ff
}
chars.push(codePoint)
this.pos += bytesPerSequence
}
// TODO: could take the approach of a quick first scan for special chars like encoding/json/decode.go#unquoteBytes
// and converting all of the ascii chars from the base array in bulk
while (!this.done()) {
const ch = this.ch()
let ch1
switch (ch) {
case 92: // '\'
this.pos++
if (this.done()) {
throw new Error(`${decodeErrPrefix} unexpected string termination at position ${this.pos}`)
}
ch1 = this.ch()
this.pos++
switch (ch1) {
case 34: // '"'
case 39: // '\''
case 92: // '\'
case 47: // '/'
chars.push(ch1)
break
case 98: // 'b'
chars.push(8)
break
case 116: // 't'
chars.push(9)
break
case 110: // 'n'
chars.push(10)
break
case 102: // 'f'
chars.push(12)
break
case 114: // 'r'
chars.push(13)
break
case 117: // 'u'
chars.push(readu4())
break
default:
throw new Error(`${decodeErrPrefix} unexpected string escape character at position ${this.pos}`)
}
break
case 34: // '"'
this.pos++
return new Token(Type.string, decodeCodePointsArray(chars), this.pos - startPos)
default:
if (ch < 32) { // ' '
throw new Error(`${decodeErrPrefix} invalid control character at position ${this.pos}`)
} else if (ch < 0x80) {
chars.push(ch)
this.pos++
} else {
readUtf8Char()
}
}
}
throw new Error(`${decodeErrPrefix} unexpected end of string at position ${this.pos}`)
}
/**
* @returns {Token}
*/
parseValue () {
switch (this.ch()) {
case 123: // '{'
this.modeStack.push('obj-start')
this.pos++
return new Token(Type.map, Infinity, 1)
case 91: // '['
this.modeStack.push('array-start')
this.pos++
return new Token(Type.array, Infinity, 1)
case 34: { // '"'
return this.parseString()
}
case 110: // 'n' / null
this.expect([110, 117, 108, 108]) // 'null'
return new Token(Type.null, null, 4)
case 102: // 'f' / // false
this.expect([102, 97, 108, 115, 101]) // 'false'
return new Token(Type.false, false, 5)
case 116: // 't' / // true
this.expect([116, 114, 117, 101]) // 'true'
return new Token(Type.true, true, 4)
case 45: // '-'
case 48: // '0'
case 49: // '1'
case 50: // '2'
case 51: // '3'
case 52: // '4'
case 53: // '5'
case 54: // '6'
case 55: // '7'
case 56: // '8'
case 57: // '9'
return this.parseNumber()
default:
throw new Error(`${decodeErrPrefix} unexpected character at position ${this.pos}`)
}
}
/**
* @returns {Token}
*/
next () {
this.skipWhitespace()
switch (this.currentMode()) {
case 'value':
this.modeStack.pop()
return this.parseValue()
case 'array-value': {
this.modeStack.pop()
if (this.ch() === 93) { // ']'
this.pos++
this.skipWhitespace()
return new Token(Type.break, undefined, 1)
}
if (this.ch() !== 44) { // ','
throw new Error(`${decodeErrPrefix} unexpected character at position ${this.pos}, was expecting array delimiter but found '${String.fromCharCode(this.ch())}'`)
}
this.pos++
this.modeStack.push('array-value')
this.skipWhitespace()
return this.parseValue()
}
case 'array-start': {
this.modeStack.pop()
if (this.ch() === 93) { // ']'
this.pos++
this.skipWhitespace()
return new Token(Type.break, undefined, 1)
}
this.modeStack.push('array-value')
this.skipWhitespace()
return this.parseValue()
}
// @ts-ignore
case 'obj-key':
if (this.ch() === 125) { // '}'
this.modeStack.pop()
this.pos++
this.skipWhitespace()
return new Token(Type.break, undefined, 1)
}
if (this.ch() !== 44) { // ','
throw new Error(`${decodeErrPrefix} unexpected character at position ${this.pos}, was expecting object delimiter but found '${String.fromCharCode(this.ch())}'`)
}
this.pos++
this.skipWhitespace()
case 'obj-start': { // eslint-disable-line no-fallthrough
this.modeStack.pop()
if (this.ch() === 125) { // '}'
this.pos++
this.skipWhitespace()
return new Token(Type.break, undefined, 1)
}
const token = this.parseString()
this.skipWhitespace()
if (this.ch() !== 58) { // ':'
throw new Error(`${decodeErrPrefix} unexpected character at position ${this.pos}, was expecting key/value delimiter ':' but found '${String.fromCharCode(this.ch())}'`)
}
this.pos++
this.modeStack.push('obj-value')
return token
}
case 'obj-value': {
this.modeStack.pop()
this.modeStack.push('obj-key')
this.skipWhitespace()
return this.parseValue()
}
/* c8 ignore next 2 */
default:
throw new Error(`${decodeErrPrefix} unexpected parse state at position ${this.pos}; this shouldn't happen`)
}
}
}
/**
* @param {Uint8Array} data
* @param {DecodeOptions} [options]
* @returns {any}
*/
function decode (data, options) {
options = Object.assign({ tokenizer: new Tokenizer(data, options) }, options)
return _decode(data, options)
}
export { decode, Tokenizer }
+311
View File
@@ -0,0 +1,311 @@
import { Type } from '../token.js'
import { encodeCustom } from '../encode.js'
import { encodeErrPrefix } from '../common.js'
import { asU8A, fromString } from '../byte-utils.js'
/**
* @typedef {import('../../interface').EncodeOptions} EncodeOptions
* @typedef {import('../token').Token} Token
* @typedef {import('../bl').Bl} Bl
*/
class JSONEncoder extends Array {
constructor () {
super()
/** @type {{type:Type,elements:number}[]} */
this.inRecursive = []
}
/**
* @param {Bl} buf
*/
prefix (buf) {
const recurs = this.inRecursive[this.inRecursive.length - 1]
if (recurs) {
if (recurs.type === Type.array) {
recurs.elements++
if (recurs.elements !== 1) { // >first
buf.push([44]) // ','
}
}
if (recurs.type === Type.map) {
recurs.elements++
if (recurs.elements !== 1) { // >first
if (recurs.elements % 2 === 1) { // key
buf.push([44]) // ','
} else {
buf.push([58]) // ':'
}
}
}
}
}
/**
* @param {Bl} buf
* @param {Token} token
*/
[Type.uint.major] (buf, token) {
this.prefix(buf)
const is = String(token.value)
const isa = []
for (let i = 0; i < is.length; i++) {
isa[i] = is.charCodeAt(i)
}
buf.push(isa)
}
/**
* @param {Bl} buf
* @param {Token} token
*/
[Type.negint.major] (buf, token) {
// @ts-ignore hack
this[Type.uint.major](buf, token)
}
/**
* @param {Bl} _buf
* @param {Token} _token
*/
[Type.bytes.major] (_buf, _token) {
throw new Error(`${encodeErrPrefix} unsupported type: Uint8Array`)
}
/**
* @param {Bl} buf
* @param {Token} token
*/
[Type.string.major] (buf, token) {
this.prefix(buf)
// buf.push(34) // '"'
// encodeUtf8(token.value, byts)
// buf.push(34) // '"'
const byts = fromString(JSON.stringify(token.value))
buf.push(byts.length > 32 ? asU8A(byts) : byts)
}
/**
* @param {Bl} buf
* @param {Token} _token
*/
[Type.array.major] (buf, _token) {
this.prefix(buf)
this.inRecursive.push({ type: Type.array, elements: 0 })
buf.push([91]) // '['
}
/**
* @param {Bl} buf
* @param {Token} _token
*/
[Type.map.major] (buf, _token) {
this.prefix(buf)
this.inRecursive.push({ type: Type.map, elements: 0 })
buf.push([123]) // '{'
}
/**
* @param {Bl} _buf
* @param {Token} _token
*/
[Type.tag.major] (_buf, _token) {}
/**
* @param {Bl} buf
* @param {Token} token
*/
[Type.float.major] (buf, token) {
if (token.type.name === 'break') {
const recurs = this.inRecursive.pop()
if (recurs) {
if (recurs.type === Type.array) {
buf.push([93]) // ']'
} else if (recurs.type === Type.map) {
buf.push([125]) // '}'
/* c8 ignore next 3 */
} else {
throw new Error('Unexpected recursive type; this should not happen!')
}
return
}
/* c8 ignore next 2 */
throw new Error('Unexpected break; this should not happen!')
}
if (token.value === undefined) {
throw new Error(`${encodeErrPrefix} unsupported type: undefined`)
}
this.prefix(buf)
if (token.type.name === 'true') {
buf.push([116, 114, 117, 101]) // 'true'
return
} else if (token.type.name === 'false') {
buf.push([102, 97, 108, 115, 101]) // 'false'
return
} else if (token.type.name === 'null') {
buf.push([110, 117, 108, 108]) // 'null'
return
}
// number
const is = String(token.value)
const isa = []
let dp = false
for (let i = 0; i < is.length; i++) {
isa[i] = is.charCodeAt(i)
if (!dp && (isa[i] === 46 || isa[i] === 101 || isa[i] === 69)) { // '[.eE]'
dp = true
}
}
if (!dp) { // need a decimal point for floats
isa.push(46) // '.'
isa.push(48) // '0'
}
buf.push(isa)
}
}
// The below code is mostly taken and modified from https://github.com/feross/buffer
// Licensed MIT. Copyright (c) Feross Aboukhadijeh
// function encodeUtf8 (string, byts) {
// let codePoint
// const length = string.length
// let leadSurrogate = null
// for (let i = 0; i < length; ++i) {
// codePoint = string.charCodeAt(i)
// // is surrogate component
// if (codePoint > 0xd7ff && codePoint < 0xe000) {
// // last char was a lead
// if (!leadSurrogate) {
// // no lead yet
// /* c8 ignore next 9 */
// if (codePoint > 0xdbff) {
// // unexpected trail
// byts.push(0xef, 0xbf, 0xbd)
// continue
// } else if (i + 1 === length) {
// // unpaired lead
// byts.push(0xef, 0xbf, 0xbd)
// continue
// }
// // valid lead
// leadSurrogate = codePoint
// continue
// }
// // 2 leads in a row
// /* c8 ignore next 5 */
// if (codePoint < 0xdc00) {
// byts.push(0xef, 0xbf, 0xbd)
// leadSurrogate = codePoint
// continue
// }
// // valid surrogate pair
// codePoint = (leadSurrogate - 0xd800 << 10 | codePoint - 0xdc00) + 0x10000
// /* c8 ignore next 4 */
// } else if (leadSurrogate) {
// // valid bmp char, but last char was a lead
// byts.push(0xef, 0xbf, 0xbd)
// }
// leadSurrogate = null
// // encode utf8
// if (codePoint < 0x80) {
// // special JSON escapes
// switch (codePoint) {
// case 8: // '\b'
// byts.push(92, 98) // '\\b'
// continue
// case 9: // '\t'
// byts.push(92, 116) // '\\t'
// continue
// case 10: // '\n'
// byts.push(92, 110) // '\\n'
// continue
// case 12: // '\f'
// byts.push(92, 102) // '\\f'
// continue
// case 13: // '\r'
// byts.push(92, 114) // '\\r'
// continue
// case 34: // '"'
// byts.push(92, 34) // '\\"'
// continue
// case 92: // '\\'
// byts.push(92, 92) // '\\\\'
// continue
// }
// byts.push(codePoint)
// } else if (codePoint < 0x800) {
// /* c8 ignore next 1 */
// byts.push(
// codePoint >> 0x6 | 0xc0,
// codePoint & 0x3f | 0x80
// )
// } else if (codePoint < 0x10000) {
// /* c8 ignore next 1 */
// byts.push(
// codePoint >> 0xc | 0xe0,
// codePoint >> 0x6 & 0x3f | 0x80,
// codePoint & 0x3f | 0x80
// )
// /* c8 ignore next 9 */
// } else if (codePoint < 0x110000) {
// byts.push(
// codePoint >> 0x12 | 0xf0,
// codePoint >> 0xc & 0x3f | 0x80,
// codePoint >> 0x6 & 0x3f | 0x80,
// codePoint & 0x3f | 0x80
// )
// } else {
// /* c8 ignore next 2 */
// throw new Error('Invalid code point')
// }
// }
// }
/**
* @param {(Token|Token[])[]} e1
* @param {(Token|Token[])[]} e2
* @returns {number}
*/
function mapSorter (e1, e2) {
if (Array.isArray(e1[0]) || Array.isArray(e2[0])) {
throw new Error(`${encodeErrPrefix} complex map keys are not supported`)
}
const keyToken1 = e1[0]
const keyToken2 = e2[0]
if (keyToken1.type !== Type.string || keyToken2.type !== Type.string) {
throw new Error(`${encodeErrPrefix} non-string map keys are not supported`)
}
if (keyToken1 < keyToken2) {
return -1
}
if (keyToken1 > keyToken2) {
return 1
}
/* c8 ignore next 1 */
throw new Error(`${encodeErrPrefix} unexpected duplicate map keys, this is not supported`)
}
const defaultEncodeOptions = { addBreakTokens: true, mapSorter }
/**
* @param {any} data
* @param {EncodeOptions} [options]
* @returns {Uint8Array}
*/
function encode (data, options) {
options = Object.assign({}, defaultEncodeOptions, options)
return encodeCustom(data, new JSONEncoder(), options)
}
export { encode }
+4
View File
@@ -0,0 +1,4 @@
import { encode } from './encode.js'
import { decode, Tokenizer } from './decode.js'
export { encode, decode, Tokenizer }
+209
View File
@@ -0,0 +1,209 @@
import { Token, Type } from './token.js'
import * as uint from './0uint.js'
import * as negint from './1negint.js'
import * as bytes from './2bytes.js'
import * as string from './3string.js'
import * as array from './4array.js'
import * as map from './5map.js'
import * as tag from './6tag.js'
import * as float from './7float.js'
import { decodeErrPrefix } from './common.js'
import { fromArray } from './byte-utils.js'
/**
* @typedef {import('../interface').DecodeOptions} DecodeOptions
*/
/**
* @param {Uint8Array} data
* @param {number} pos
* @param {number} minor
*/
function invalidMinor (data, pos, minor) {
throw new Error(`${decodeErrPrefix} encountered invalid minor (${minor}) for major ${data[pos] >>> 5}`)
}
/**
* @param {string} msg
* @returns {()=>any}
*/
function errorer (msg) {
return () => { throw new Error(`${decodeErrPrefix} ${msg}`) }
}
/** @type {((data:Uint8Array, pos:number, minor:number, options?:DecodeOptions) => any)[]} */
export const jump = []
// unsigned integer, 0x00..0x17 (0..23)
for (let i = 0; i <= 0x17; i++) {
jump[i] = invalidMinor // uint.decodeUintCompact, handled by quick[]
}
jump[0x18] = uint.decodeUint8 // unsigned integer, one-byte uint8_t follows
jump[0x19] = uint.decodeUint16 // unsigned integer, two-byte uint16_t follows
jump[0x1a] = uint.decodeUint32 // unsigned integer, four-byte uint32_t follows
jump[0x1b] = uint.decodeUint64 // unsigned integer, eight-byte uint64_t follows
jump[0x1c] = invalidMinor
jump[0x1d] = invalidMinor
jump[0x1e] = invalidMinor
jump[0x1f] = invalidMinor
// negative integer, -1-0x00..-1-0x17 (-1..-24)
for (let i = 0x20; i <= 0x37; i++) {
jump[i] = invalidMinor // negintDecode, handled by quick[]
}
jump[0x38] = negint.decodeNegint8 // negative integer, -1-n one-byte uint8_t for n follows
jump[0x39] = negint.decodeNegint16 // negative integer, -1-n two-byte uint16_t for n follows
jump[0x3a] = negint.decodeNegint32 // negative integer, -1-n four-byte uint32_t for follows
jump[0x3b] = negint.decodeNegint64 // negative integer, -1-n eight-byte uint64_t for follows
jump[0x3c] = invalidMinor
jump[0x3d] = invalidMinor
jump[0x3e] = invalidMinor
jump[0x3f] = invalidMinor
// byte string, 0x00..0x17 bytes follow
for (let i = 0x40; i <= 0x57; i++) {
jump[i] = bytes.decodeBytesCompact
}
jump[0x58] = bytes.decodeBytes8 // byte string, one-byte uint8_t for n, and then n bytes follow
jump[0x59] = bytes.decodeBytes16 // byte string, two-byte uint16_t for n, and then n bytes follow
jump[0x5a] = bytes.decodeBytes32 // byte string, four-byte uint32_t for n, and then n bytes follow
jump[0x5b] = bytes.decodeBytes64 // byte string, eight-byte uint64_t for n, and then n bytes follow
jump[0x5c] = invalidMinor
jump[0x5d] = invalidMinor
jump[0x5e] = invalidMinor
jump[0x5f] = errorer('indefinite length bytes/strings are not supported') // byte string, byte strings follow, terminated by "break"
// UTF-8 string 0x00..0x17 bytes follow
for (let i = 0x60; i <= 0x77; i++) {
jump[i] = string.decodeStringCompact
}
jump[0x78] = string.decodeString8 // UTF-8 string, one-byte uint8_t for n, and then n bytes follow
jump[0x79] = string.decodeString16 // UTF-8 string, two-byte uint16_t for n, and then n bytes follow
jump[0x7a] = string.decodeString32 // UTF-8 string, four-byte uint32_t for n, and then n bytes follow
jump[0x7b] = string.decodeString64 // UTF-8 string, eight-byte uint64_t for n, and then n bytes follow
jump[0x7c] = invalidMinor
jump[0x7d] = invalidMinor
jump[0x7e] = invalidMinor
jump[0x7f] = errorer('indefinite length bytes/strings are not supported') // UTF-8 strings follow, terminated by "break"
// array, 0x00..0x17 data items follow
for (let i = 0x80; i <= 0x97; i++) {
jump[i] = array.decodeArrayCompact
}
jump[0x98] = array.decodeArray8 // array, one-byte uint8_t for n, and then n data items follow
jump[0x99] = array.decodeArray16 // array, two-byte uint16_t for n, and then n data items follow
jump[0x9a] = array.decodeArray32 // array, four-byte uint32_t for n, and then n data items follow
jump[0x9b] = array.decodeArray64 // array, eight-byte uint64_t for n, and then n data items follow
jump[0x9c] = invalidMinor
jump[0x9d] = invalidMinor
jump[0x9e] = invalidMinor
jump[0x9f] = array.decodeArrayIndefinite // array, data items follow, terminated by "break"
// map, 0x00..0x17 pairs of data items follow
for (let i = 0xa0; i <= 0xb7; i++) {
jump[i] = map.decodeMapCompact
}
jump[0xb8] = map.decodeMap8 // map, one-byte uint8_t for n, and then n pairs of data items follow
jump[0xb9] = map.decodeMap16 // map, two-byte uint16_t for n, and then n pairs of data items follow
jump[0xba] = map.decodeMap32 // map, four-byte uint32_t for n, and then n pairs of data items follow
jump[0xbb] = map.decodeMap64 // map, eight-byte uint64_t for n, and then n pairs of data items follow
jump[0xbc] = invalidMinor
jump[0xbd] = invalidMinor
jump[0xbe] = invalidMinor
jump[0xbf] = map.decodeMapIndefinite // map, pairs of data items follow, terminated by "break"
// tags
for (let i = 0xc0; i <= 0xd7; i++) {
jump[i] = tag.decodeTagCompact
}
jump[0xd8] = tag.decodeTag8
jump[0xd9] = tag.decodeTag16
jump[0xda] = tag.decodeTag32
jump[0xdb] = tag.decodeTag64
jump[0xdc] = invalidMinor
jump[0xdd] = invalidMinor
jump[0xde] = invalidMinor
jump[0xdf] = invalidMinor
// 0xe0..0xf3 simple values, unsupported
for (let i = 0xe0; i <= 0xf3; i++) {
jump[i] = errorer('simple values are not supported')
}
jump[0xf4] = invalidMinor // false, handled by quick[]
jump[0xf5] = invalidMinor // true, handled by quick[]
jump[0xf6] = invalidMinor // null, handled by quick[]
jump[0xf7] = float.decodeUndefined // undefined
jump[0xf8] = errorer('simple values are not supported') // simple value, one byte follows, unsupported
jump[0xf9] = float.decodeFloat16 // half-precision float (two-byte IEEE 754)
jump[0xfa] = float.decodeFloat32 // single-precision float (four-byte IEEE 754)
jump[0xfb] = float.decodeFloat64 // double-precision float (eight-byte IEEE 754)
jump[0xfc] = invalidMinor
jump[0xfd] = invalidMinor
jump[0xfe] = invalidMinor
jump[0xff] = float.decodeBreak // "break" stop code
/** @type {Token[]} */
export const quick = []
// ints <24
for (let i = 0; i < 24; i++) {
quick[i] = new Token(Type.uint, i, 1)
}
// negints >= -24
for (let i = -1; i >= -24; i--) {
quick[31 - i] = new Token(Type.negint, i, 1)
}
// empty bytes
quick[0x40] = new Token(Type.bytes, new Uint8Array(0), 1)
// empty string
quick[0x60] = new Token(Type.string, '', 1)
// empty list
quick[0x80] = new Token(Type.array, 0, 1)
// empty map
quick[0xa0] = new Token(Type.map, 0, 1)
// false
quick[0xf4] = new Token(Type.false, false, 1)
// true
quick[0xf5] = new Token(Type.true, true, 1)
// null
quick[0xf6] = new Token(Type.null, null, 1)
/**
* @param {Token} token
* @returns {Uint8Array|undefined}
*/
export function quickEncodeToken (token) {
switch (token.type) {
case Type.false:
return fromArray([0xf4])
case Type.true:
return fromArray([0xf5])
case Type.null:
return fromArray([0xf6])
case Type.bytes:
if (!token.value.length) {
return fromArray([0x40])
}
return
case Type.string:
if (token.value === '') {
return fromArray([0x60])
}
return
case Type.array:
if (token.value === 0) {
return fromArray([0x80])
}
/* c8 ignore next 2 */
// shouldn't be possible if this were called when there was only one token
return
case Type.map:
if (token.value === 0) {
return fromArray([0xa0])
}
/* c8 ignore next 2 */
// shouldn't be possible if this were called when there was only one token
return
case Type.uint:
if (token.value < 24) {
return fromArray([Number(token.value)])
}
return
case Type.negint:
if (token.value >= -24) {
return fromArray([31 - Number(token.value)])
}
}
}
+61
View File
@@ -0,0 +1,61 @@
import { makeCborEncoders, objectToTokens } from './encode.js'
import { quickEncodeToken } from './jump.js'
/**
* @typedef {import('../interface').EncodeOptions} EncodeOptions
* @typedef {import('../interface').TokenTypeEncoder} TokenTypeEncoder
* @typedef {import('../interface').TokenOrNestedTokens} TokenOrNestedTokens
*/
const cborEncoders = makeCborEncoders()
/** @type {EncodeOptions} */
const defaultEncodeOptions = {
float64: false,
quickEncodeToken
}
/**
* Calculate the byte length of the given data when encoded as CBOR with the
* options provided.
* This calculation will be accurate if the same options are used as when
* performing a normal encode. Some encode options can change the encoding
* output length.
*
* @param {any} data
* @param {EncodeOptions} [options]
* @returns {number}
*/
export function encodedLength (data, options) {
options = Object.assign({}, defaultEncodeOptions, options)
options.mapSorter = undefined // won't change the length
const tokens = objectToTokens(data, options)
return tokensToLength(tokens, cborEncoders, options)
}
/**
* Calculate the byte length of the data as represented by the given tokens when
* encoded as CBOR with the options provided.
* This function is for advanced users and would not normally be called
* directly. See `encodedLength()` for appropriate use.
*
* @param {TokenOrNestedTokens} tokens
* @param {TokenTypeEncoder[]} [encoders]
* @param {EncodeOptions} [options]
*/
export function tokensToLength (tokens, encoders = cborEncoders, options = defaultEncodeOptions) {
if (Array.isArray(tokens)) {
let len = 0
for (const token of tokens) {
len += tokensToLength(token, encoders, options)
}
return len
} else {
const encoder = encoders[tokens.type.major]
/* c8 ignore next 3 */
if (encoder.encodedSize === undefined || typeof encoder.encodedSize !== 'function') {
throw new Error(`Encoder for ${tokens.type.name} does not have an encodedSize()`)
}
return encoder.encodedSize(tokens, options)
}
}
+67
View File
@@ -0,0 +1,67 @@
class Type {
/**
* @param {number} major
* @param {string} name
* @param {boolean} terminal
*/
constructor (major, name, terminal) {
this.major = major
this.majorEncoded = major << 5
this.name = name
this.terminal = terminal
}
/* c8 ignore next 3 */
toString () {
return `Type[${this.major}].${this.name}`
}
/**
* @param {Type} typ
* @returns {number}
*/
compare (typ) {
/* c8 ignore next 1 */
return this.major < typ.major ? -1 : this.major > typ.major ? 1 : 0
}
}
// convert to static fields when better supported
Type.uint = new Type(0, 'uint', true)
Type.negint = new Type(1, 'negint', true)
Type.bytes = new Type(2, 'bytes', true)
Type.string = new Type(3, 'string', true)
Type.array = new Type(4, 'array', false)
Type.map = new Type(5, 'map', false)
Type.tag = new Type(6, 'tag', false) // terminal?
Type.float = new Type(7, 'float', true)
Type.false = new Type(7, 'false', true)
Type.true = new Type(7, 'true', true)
Type.null = new Type(7, 'null', true)
Type.undefined = new Type(7, 'undefined', true)
Type.break = new Type(7, 'break', true)
// Type.indefiniteLength = new Type(0, 'indefiniteLength', true)
class Token {
/**
* @param {Type} type
* @param {any} [value]
* @param {number} [encodedLength]
*/
constructor (type, value, encodedLength) {
this.type = type
this.value = value
this.encodedLength = encodedLength
/** @type {Uint8Array|undefined} */
this.encodedBytes = undefined
/** @type {Uint8Array|undefined} */
this.byteValue = undefined
}
/* c8 ignore next 3 */
toString () {
return `Token[${this.type}].${this.value}`
}
}
export { Type, Token }