UNPKG

ox

Version:

Ethereum Standard Library

913 lines (795 loc) 25.7 kB
import * as Bytes from './Bytes.js' import * as Errors from './Errors.js' import * as Hex from './Hex.js' import * as Cursor from './internal/cursor.js' /** * Encodes a value into CBOR (Concise Binary Object Representation) format. * * @example * ```ts twoslash * import { Cbor } from 'ox' * * Cbor.encode([1, 2, 3]) * // @log: '0x83010203' * * Cbor.encode({ foo: 'bar', baz: [1, 2, 3] }) * // @log: '0xa263666f6f636261726362617a83010203' * * Cbor.encode('hello', { as: 'Bytes' }) * // @log: Uint8Array(6) [ 101, 104, 101, 108, 108, 111 ] * ``` * * @param data - The value to encode. * @param options - Encoding options. * @returns The CBOR-encoded value. */ export function encode<as extends 'Hex' | 'Bytes' = 'Hex'>( data: unknown, options: encode.Options<as> = {}, ): encode.ReturnType<as> { const { as = 'Hex' } = options const encodable = getEncodable(data) const cursor = Cursor.create(new Uint8Array(encodable.length)) encodable.encode(cursor) if (as === 'Hex') return Hex.fromBytes(cursor.bytes) as encode.ReturnType<as> return cursor.bytes as encode.ReturnType<as> } export declare namespace encode { type Options<as extends 'Hex' | 'Bytes' = 'Hex' | 'Bytes'> = { /** The format to return the encoded value in. */ as?: as | 'Hex' | 'Bytes' | undefined } type ReturnType<as extends 'Hex' | 'Bytes' = 'Hex' | 'Bytes'> = | (as extends 'Hex' ? Hex.Hex : never) | (as extends 'Bytes' ? Bytes.Bytes : never) type ErrorType = | Cursor.create.ErrorType | Hex.fromBytes.ErrorType | UnsupportedBigIntError | UnexpectedTokenError | NumberTooLargeError | StringTooLargeError | ByteStringTooLargeError | ArrayTooLargeError | ObjectTooLargeError | Errors.GlobalErrorType } /** * Decodes CBOR (Concise Binary Object Representation) data into a JavaScript value. * * @example * ```ts twoslash * import { Cbor } from 'ox' * * Cbor.decode('0x83010203') * // @log: [1, 2, 3] * * Cbor.decode('0xa263666f6f636261726362617a83010203') * // @log: { foo: 'bar', baz: [1, 2, 3] } * * Cbor.decode(new Uint8Array([101, 104, 101, 108, 108, 111])) * // @log: 'hello' * ``` * * @param data - The CBOR-encoded data to decode. * @param options - Decoding options. * @returns The decoded value. */ export function decode<type = unknown>(data: Hex.Hex | Bytes.Bytes): type { const bytes = (() => { if (typeof data === 'string') { if (data.length > 3 && data.length % 2 !== 0) throw new Hex.InvalidLengthError(data) return Bytes.fromHex(data) } return data })() const cursor = Cursor.create(bytes) return decodeCursor(cursor) as type } export declare namespace decode { type ErrorType = | Bytes.fromHex.ErrorType | Cursor.create.ErrorType | Hex.InvalidLengthError | decodeCursor.ErrorType | Errors.GlobalErrorType } export class InvalidMajorTypeError extends Errors.BaseError { override readonly name = 'Cbor.InvalidMajorTypeError' constructor({ majorType }: { majorType: number }) { super(`Invalid CBOR major type: ${majorType}`) } } export class InvalidAdditionalInfoError extends Errors.BaseError { override readonly name = 'Cbor.InvalidAdditionalInfoError' constructor({ additionalInfo }: { additionalInfo: number }) { super(`Invalid CBOR additional info: ${additionalInfo}`) } } export class Unsupported64BitIntegerError extends Errors.BaseError { override readonly name = 'Cbor.Unsupported64BitIntegerError' constructor() { super('64-bit integers are not supported in CBOR decoding.') } } export class UnsupportedTagError extends Errors.BaseError { override readonly name = 'Cbor.UnsupportedTagError' constructor({ tag }: { tag: number }) { super(`CBOR tagged data (tag ${tag}) is not yet supported.`) } } export class InvalidIndefiniteLengthChunkError extends Errors.BaseError { override readonly name = 'Cbor.InvalidIndefiniteLengthChunkError' constructor({ type }: { type: string }) { super(`Invalid chunk type in indefinite-length ${type}`) } } export class InvalidSimpleValueError extends Errors.BaseError { override readonly name = 'Cbor.InvalidSimpleValueError' constructor({ value }: { value: number }) { super(`Invalid CBOR simple value: ${value}`) } } export class UnsupportedBigIntError extends Errors.BaseError { override readonly name = 'Cbor.UnsupportedBigIntError' constructor() { super('BigInt values are not supported in CBOR encoding.') } } export class UnexpectedTokenError extends Errors.BaseError { override readonly name = 'Cbor.UnexpectedTokenError' constructor({ token }: { token: string }) { super(`Unexpected token: ${token}`) } } export class NumberTooLargeError extends Errors.BaseError { override readonly name = 'Cbor.NumberTooLargeError' constructor({ number }: { number: string }) { super( `Number exceeds maximum safe integer (${Number.MAX_SAFE_INTEGER}): ${number}`, ) } } export class StringTooLargeError extends Errors.BaseError { override readonly name = 'Cbor.StringTooLargeError' constructor({ size }: { size: number }) { super(`String length exceeds maximum (4294967295): ${size}`) } } export class ArrayTooLargeError extends Errors.BaseError { override readonly name = 'Cbor.ArrayTooLargeError' constructor({ size }: { size: number }) { super(`Array length exceeds maximum (4294967295): ${size}`) } } export class ObjectTooLargeError extends Errors.BaseError { override readonly name = 'Cbor.ObjectTooLargeError' constructor({ size }: { size: number }) { super(`Object size exceeds maximum (4294967295): ${size}`) } } export class ByteStringTooLargeError extends Errors.BaseError { override readonly name = 'Cbor.ByteStringTooLargeError' constructor({ size }: { size: number }) { super(`Byte string length exceeds maximum (4294967295): ${size}`) } } ///////////////////////////////////////////////////////////////////////////////// // Internal ///////////////////////////////////////////////////////////////////////////////// type Encodable = { length: number encode(cursor: Cursor.Cursor): void } /** @internal */ // biome-ignore lint/correctness/noUnusedVariables: _ function getEncodable(value: unknown): Encodable { if (typeof value === 'undefined') return { length: 1, encode: (cursor) => cursor.pushUint8(0xf7) } if (value === null) return { length: 1, encode: (cursor) => cursor.pushUint8(0xf6) } if (typeof value === 'boolean') return { length: 1, encode: (cursor) => cursor.pushUint8(value ? 0xf5 : 0xf4), } if (typeof value === 'number') return getEncodable.number(value as number) if (typeof value === 'bigint') throw new UnsupportedBigIntError() if (typeof value === 'string') return getEncodable.string(value as string) if (Array.isArray(value)) return getEncodable.array(value) if (value instanceof Uint8Array) return getEncodable.byteString(value) if (value instanceof ArrayBuffer) return getEncodable.byteString(new Uint8Array(value)) if (ArrayBuffer.isView(value)) return getEncodable.byteString( new Uint8Array(value.buffer, value.byteOffset, value.byteLength), ) if (typeof value === 'object') return getEncodable.object(value as Record<string, unknown>) throw new UnexpectedTokenError({ token: String(value) }) } /** @internal */ namespace getEncodable { /** @internal */ export function number(value: number): Encodable { // Handle non-safe integers (floats, NaN, Infinity) if (!Number.isSafeInteger(value)) { // Use Float32 if the value can be represented without precision loss // This creates smaller encodings when possible (5 bytes vs 9 bytes) const float32 = Math.fround(value) if (Number.isNaN(value) || value === float32) return { length: 5, // 1 byte prefix + 4 bytes float32 encode(cursor) { cursor.pushUint8(0xfa) cursor.dataView.setFloat32(cursor.position, value, false) cursor.position += 4 }, } return { length: 9, // 1 byte prefix + 8 bytes float64 encode(cursor) { cursor.pushUint8(0xfb) cursor.dataView.setFloat64(cursor.position, value, false) cursor.position += 8 }, } } // Handle positive integers if (value >= 0) { if (value <= 0x17) return { length: 1, encode: (cursor) => cursor.pushUint8(value) } if (value <= 0xff) return { length: 2, // 1 byte prefix + 1 byte uint8 encode: (cursor) => { cursor.pushUint8(0x18) cursor.pushUint8(value) }, } if (value <= 0xffff) return { length: 3, // 1 byte prefix + 2 bytes uint16 encode: (cursor) => { cursor.pushUint8(0x19) cursor.pushUint16(value) }, } if (value <= 0xffffffff) return { length: 5, // 1 byte prefix + 4 bytes uint32 encode: (cursor) => { cursor.pushUint8(0x1a) cursor.pushUint32(value) }, } throw new NumberTooLargeError({ number: value.toString(10) }) } // Handle negative integers // CBOR encodes -n as (n-1) const positiveNumber = -1 - value if (value >= -24) return { length: 1, encode: (cursor) => cursor.pushUint8(0x20 + positiveNumber), } if (positiveNumber <= 0xff) return { length: 2, // 1 byte prefix + 1 byte uint8 encode: (cursor) => { cursor.pushUint8(0x38) cursor.pushUint8(positiveNumber) }, } if (positiveNumber <= 0xffff) return { length: 3, // 1 byte prefix + 2 bytes uint16 encode: (cursor) => { cursor.pushUint8(0x39) cursor.pushUint16(positiveNumber) }, } if (positiveNumber <= 0xffffffff) return { length: 5, // 1 byte prefix + 4 bytes uint32 encode: (cursor) => { cursor.pushUint8(0x3a) cursor.pushUint32(positiveNumber) }, } throw new NumberTooLargeError({ number: value.toString(10) }) } /** @internal */ export function string(value: string): Encodable { const encoded = Bytes.fromString(value) const size = encoded.length if (size <= 0x17) return { length: 1 + size, // 1 byte prefix + size bytes encode(cursor) { cursor.pushUint8(0x60 + size) if (size > 0) cursor.pushBytes(encoded) }, } if (size <= 0xff) return { length: 2 + size, // 1 byte prefix + 1 byte uint8 + size bytes encode(cursor) { cursor.pushUint8(0x78) cursor.pushUint8(size) cursor.pushBytes(encoded) }, } if (size <= 0xffff) return { length: 3 + size, // 1 byte prefix + 2 bytes uint16 + size bytes encode(cursor) { cursor.pushUint8(0x79) cursor.pushUint16(size) cursor.pushBytes(encoded) }, } if (size <= 0xffffffff) return { length: 5 + size, // 1 byte prefix + 4 bytes uint32 + size bytes encode(cursor) { cursor.pushUint8(0x7a) cursor.pushUint32(size) cursor.pushBytes(encoded) }, } throw new StringTooLargeError({ size }) } /** @internal */ export function array(value: unknown[]): Encodable { const items = value.map((item) => getEncodable(item)) const bodyLength = items.reduce((acc, item) => acc + item.length, 0) const size = value.length if (size <= 0x17) return { length: 1 + bodyLength, // 1 byte prefix + body length encode(cursor) { cursor.pushUint8(0x80 + size) for (const item of items) item.encode(cursor) }, } if (size <= 0xff) return { length: 2 + bodyLength, // 1 byte prefix + 1 byte uint8 + body length encode(cursor) { cursor.pushUint8(0x98) cursor.pushUint8(size) for (const item of items) item.encode(cursor) }, } if (size <= 0xffff) return { length: 3 + bodyLength, // 1 byte prefix + 2 bytes uint16 + body length encode(cursor) { cursor.pushUint8(0x99) cursor.pushUint16(size) for (const item of items) item.encode(cursor) }, } if (size <= 0xffffffff) return { length: 5 + bodyLength, // 1 byte prefix + 4 bytes uint32 + body length encode(cursor) { cursor.pushUint8(0x9a) cursor.pushUint32(size) for (const item of items) item.encode(cursor) }, } throw new ArrayTooLargeError({ size }) } /** @internal */ export function byteString(value: Uint8Array): Encodable { const size = value.byteLength if (size <= 0x17) return { length: 1 + size, // 1 byte prefix + size bytes encode(cursor) { cursor.pushUint8(0x40 + size) cursor.pushBytes(value) }, } if (size <= 0xff) return { length: 2 + size, // 1 byte prefix + 1 byte uint8 + size bytes encode(cursor) { cursor.pushUint8(0x58) cursor.pushUint8(size) cursor.pushBytes(value) }, } if (size <= 0xffff) return { length: 3 + size, // 1 byte prefix + 2 bytes uint16 + size bytes encode(cursor) { cursor.pushUint8(0x59) cursor.pushUint16(size) cursor.pushBytes(value) }, } if (size <= 0xffffffff) return { length: 5 + size, // 1 byte prefix + 4 bytes uint32 + size bytes encode(cursor) { cursor.pushUint8(0x5a) cursor.pushUint32(size) cursor.pushBytes(value) }, } throw new ByteStringTooLargeError({ size }) } /** @internal */ export function object(value: Record<string, unknown>): Encodable { const keys = Object.keys(value) const entries = keys.map((key) => ({ key: getEncodable(key), value: getEncodable(value[key]), })) const bodyLength = entries.reduce( (acc, entry) => acc + entry.key.length + entry.value.length, 0, ) const size = keys.length if (size <= 0x17) return { length: 1 + bodyLength, // 1 byte prefix + body length encode(cursor) { cursor.pushUint8(0xa0 + size) for (const entry of entries) { entry.key.encode(cursor) entry.value.encode(cursor) } }, } if (size <= 0xff) return { length: 2 + bodyLength, // 1 byte prefix + 1 byte uint8 + body length encode(cursor) { cursor.pushUint8(0xb8) cursor.pushUint8(size) for (const entry of entries) { entry.key.encode(cursor) entry.value.encode(cursor) } }, } if (size <= 0xffff) return { length: 3 + bodyLength, // 1 byte prefix + 2 bytes uint16 + body length encode(cursor) { cursor.pushUint8(0xb9) cursor.pushUint16(size) for (const entry of entries) { entry.key.encode(cursor) entry.value.encode(cursor) } }, } if (size <= 0xffffffff) return { length: 5 + bodyLength, // 1 byte prefix + 4 bytes uint32 + body length encode(cursor) { cursor.pushUint8(0xba) cursor.pushUint32(size) for (const entry of entries) { entry.key.encode(cursor) entry.value.encode(cursor) } }, } throw new ObjectTooLargeError({ size }) } } /** @internal */ // biome-ignore lint/correctness/noUnusedVariables: _ function decodeCursor(cursor: Cursor.Cursor): unknown { const initialByte = cursor.readUint8() const majorType = initialByte >> 5 const additionalInfo = initialByte & 0b00011111 switch (majorType) { // Major type 0: Unsigned integer case 0: return decodeCursor.readUnsignedInteger(cursor, additionalInfo) // Major type 1: Negative integer case 1: return decodeCursor.readNegativeInteger(cursor, additionalInfo) // Major type 2: Byte string case 2: return decodeCursor.readByteString(cursor, additionalInfo) // Major type 3: Text string case 3: return decodeCursor.readTextString(cursor, additionalInfo) // Major type 4: Array case 4: return decodeCursor.readArray(cursor, additionalInfo) // Major type 5: Map case 5: return decodeCursor.readMap(cursor, additionalInfo) // Major type 6: Tagged data (not yet supported) case 6: throw new UnsupportedTagError({ tag: additionalInfo }) // Major type 7: Simple values and floats case 7: return decodeCursor.readSimpleOrFloat(cursor, additionalInfo) default: throw new InvalidMajorTypeError({ majorType }) } } /** @internal */ namespace decodeCursor { export type ErrorType = | readUnsignedInteger.ErrorType | readNegativeInteger.ErrorType | readByteString.ErrorType | readTextString.ErrorType | readArray.ErrorType | readMap.ErrorType | readSimpleOrFloat.ErrorType | UnsupportedTagError | InvalidMajorTypeError | Errors.GlobalErrorType /** @internal */ // biome-ignore lint/correctness/noUnusedVariables: _ function readLength(cursor: Cursor.Cursor, additionalInfo: number): number { if (additionalInfo < 24) return additionalInfo if (additionalInfo === 24) return cursor.readUint8() if (additionalInfo === 25) return cursor.readUint16() if (additionalInfo === 26) return cursor.readUint32() if (additionalInfo === 27) throw new Unsupported64BitIntegerError() throw new InvalidAdditionalInfoError({ additionalInfo }) } /** @internal */ export declare namespace readLength { type ErrorType = | Unsupported64BitIntegerError | InvalidAdditionalInfoError | Errors.GlobalErrorType } /** @internal */ export function readUnsignedInteger( cursor: Cursor.Cursor, additionalInfo: number, ): number { return readLength(cursor, additionalInfo) } /** @internal */ export declare namespace readUnsignedInteger { type ErrorType = readLength.ErrorType | Errors.GlobalErrorType } /** @internal */ export function readNegativeInteger( cursor: Cursor.Cursor, additionalInfo: number, ): number { const value = readLength(cursor, additionalInfo) return -1 - value } /** @internal */ export declare namespace readNegativeInteger { type ErrorType = readLength.ErrorType | Errors.GlobalErrorType } /** @internal */ export function readByteString( cursor: Cursor.Cursor, additionalInfo: number, ): Bytes.Bytes { // Indefinite-length byte string if (additionalInfo === 31) { const chunks: Bytes.Bytes[] = [] let totalLength = 0 while (true) { const byte = cursor.inspectUint8() if (byte === 0xff) { cursor.readUint8() // consume the break byte break } const chunk = decodeCursor(cursor) as Bytes.Bytes if (!(chunk instanceof Uint8Array)) throw new InvalidIndefiniteLengthChunkError({ type: 'byte string' }) chunks.push(chunk) totalLength += chunk.length } // Concatenate chunks const result = new Uint8Array(totalLength) let offset = 0 for (const chunk of chunks) { result.set(chunk, offset) offset += chunk.length } return result } const length = readLength(cursor, additionalInfo) return cursor.readBytes(length) } /** @internal */ export declare namespace readByteString { type ErrorType = | readLength.ErrorType | InvalidIndefiniteLengthChunkError | Errors.GlobalErrorType } /** @internal */ export function readTextString( cursor: Cursor.Cursor, additionalInfo: number, ): string { // Indefinite-length text string if (additionalInfo === 31) { const chunks: string[] = [] while (true) { const byte = cursor.inspectUint8() if (byte === 0xff) { cursor.readUint8() // consume the break byte break } const chunk = decodeCursor(cursor) if (typeof chunk !== 'string') throw new InvalidIndefiniteLengthChunkError({ type: 'text string' }) chunks.push(chunk) } return chunks.join('') } const length = readLength(cursor, additionalInfo) const bytes = cursor.readBytes(length) return Bytes.toString(bytes) } /** @internal */ export declare namespace readTextString { type ErrorType = | readLength.ErrorType | Bytes.toString.ErrorType | InvalidIndefiniteLengthChunkError | Errors.GlobalErrorType } /** @internal */ export function readArray( cursor: Cursor.Cursor, additionalInfo: number, ): unknown[] { // Indefinite-length array if (additionalInfo === 31) { const result: unknown[] = [] while (true) { const byte = cursor.inspectUint8() if (byte === 0xff) { cursor.readUint8() // consume the break byte break } result.push(decodeCursor(cursor)) } return result } const length = readLength(cursor, additionalInfo) const result: unknown[] = [] for (let i = 0; i < length; i++) { result.push(decodeCursor(cursor)) } return result } /** @internal */ export declare namespace readArray { type ErrorType = readLength.ErrorType | Errors.GlobalErrorType } /** @internal */ export function readMap( cursor: Cursor.Cursor, additionalInfo: number, ): Record<string, unknown> { // Indefinite-length map if (additionalInfo === 31) { const result: Record<string, unknown> = {} while (true) { const byte = cursor.inspectUint8() if (byte === 0xff) { cursor.readUint8() // consume the break byte break } const key = decodeCursor(cursor) // Support both string and number keys (for COSE_Key with integer keys) const keyStr = typeof key === 'string' ? key : typeof key === 'number' ? String(key) : String(key) const value = decodeCursor(cursor) result[keyStr] = value } return result } const length = readLength(cursor, additionalInfo) const result: Record<string, unknown> = {} for (let i = 0; i < length; i++) { const key = decodeCursor(cursor) // Support both string and number keys (for COSE_Key with integer keys) const keyStr = typeof key === 'string' ? key : typeof key === 'number' ? String(key) : String(key) const value = decodeCursor(cursor) result[keyStr] = value } return result } /** @internal */ export declare namespace readMap { type ErrorType = readLength.ErrorType | Errors.GlobalErrorType } /** @internal */ export function readSimpleOrFloat( cursor: Cursor.Cursor, additionalInfo: number, ): unknown { // Simple values if (additionalInfo === 20) return false if (additionalInfo === 21) return true if (additionalInfo === 22) return null if (additionalInfo === 23) return undefined // Float16 (half-precision) if (additionalInfo === 25) { const bits = cursor.readUint16() return getFloat16(bits) } // Float32 if (additionalInfo === 26) { const value = cursor.dataView.getFloat32(cursor.position, false) cursor.position += 4 return value } // Float64 if (additionalInfo === 27) { const value = cursor.dataView.getFloat64(cursor.position, false) cursor.position += 8 return value } // Simple value (additional byte) if (additionalInfo === 24) { const simpleValue = cursor.readUint8() // Simple values 0-19 are assigned, 20-23 are in the initial byte // 24-31 are reserved, 32-255 are unassigned if (simpleValue < 32) throw new InvalidSimpleValueError({ value: simpleValue }) // For now, treat unassigned simple values as undefined return undefined } throw new InvalidAdditionalInfoError({ additionalInfo }) } /** @internal */ export declare namespace readSimpleOrFloat { type ErrorType = | InvalidSimpleValueError | InvalidAdditionalInfoError | Errors.GlobalErrorType } /** @internal */ function getFloat16(bits: number): number { // IEEE 754 half-precision (16-bit) float decoding // Format: 1 sign bit, 5 exponent bits, 10 fraction bits const sign = (bits >> 15) & 0x1 const exponent = (bits >> 10) & 0x1f const fraction = bits & 0x3ff // Handle special cases if (exponent === 0) { // Subnormal numbers or zero if (fraction === 0) return sign ? -0 : 0 // Subnormal: (-1)^sign × 2^(-14) × (0 + fraction/1024) const value = 2 ** -14 * (fraction / 1024) return sign ? -value : value } if (exponent === 0x1f) { // Infinity or NaN if (fraction === 0) return sign ? -Infinity : Infinity return NaN } // Normal numbers: (-1)^sign × 2^(exponent-15) × (1 + fraction/1024) const value = 2 ** (exponent - 15) * (1 + fraction / 1024) return sign ? -value : value } }