UNPKG

@atcute/cbor

Version:

lightweight DASL dCBOR42 codec library for AT Protocol

341 lines (278 loc) 7.09 kB
import { CidLinkWrapper, fromBinary, type CidLink } from '@atcute/cid'; import { decodeUtf8From } from '@atcute/uint8array'; import { toBytes, type Bytes } from './bytes.js'; interface State { b: Uint8Array; v: DataView | null; p: number; } const readArgument = (state: State, info: number): number => { if (info < 24) { return info; } let arg: number; switch (info) { case 24: { arg = readUint8(state); if (arg < 24) { throw new TypeError(`non-canonical argument encoding`); } break; } case 25: { arg = readUint16(state); if (arg < 0x100) { throw new TypeError(`non-canonical argument encoding`); } break; } case 26: { arg = readUint32(state); if (arg < 0x10000) { throw new TypeError(`non-canonical argument encoding`); } break; } case 27: { arg = readUint53(state); if (arg < 0x100000000) { throw new TypeError(`non-canonical argument encoding`); } break; } default: { throw new Error(`invalid argument encoding; got ${info}`); } } return arg; }; const readFloat64 = (state: State): number => { const view = (state.v ??= new DataView(state.b.buffer, state.b.byteOffset, state.b.byteLength)); const value = view.getFloat64(state.p); state.p += 8; return value; }; const readUint8 = (state: State): number => { return state.b[state.p++]; }; const readUint16 = (state: State): number => { let pos = state.p; const buf = state.b; const value = (buf[pos++] << 8) | buf[pos++]; state.p = pos; return value; }; const readUint32 = (state: State): number => { let pos = state.p; const buf = state.b; const value = ((buf[pos++] << 24) | (buf[pos++] << 16) | (buf[pos++] << 8) | buf[pos++]) >>> 0; state.p = pos; return value; }; const readUint53 = (state: State): number => { let pos = state.p; const buf = state.b; const hi = ((buf[pos++] << 24) | (buf[pos++] << 16) | (buf[pos++] << 8) | buf[pos++]) >>> 0; if (hi > 0x1fffff) { throw new RangeError(`can't decode integers beyond safe integer range`); } const lo = ((buf[pos++] << 24) | (buf[pos++] << 16) | (buf[pos++] << 8) | buf[pos++]) >>> 0; const value = hi * 2 ** 32 + lo; state.p = pos; return value; }; const readString = (state: State, length: number): string => { const string = decodeUtf8From(state.b, state.p, length); state.p += length; return string; }; const readBytes = (state: State, length: number): Bytes => { const slice = state.b.subarray(state.p, (state.p += length)); return toBytes(slice); }; const readCid = (state: State, length: number): CidLink => { const cid = fromBinary(state.b.subarray(state.p, (state.p += length))); return new CidLinkWrapper(cid.bytes); }; const compareKeys = (a: string, b: string): number => { return a.length - b.length || (a < b ? -1 : a > b ? 1 : 0); }; const decodeStringKey = (state: State): string => { const prelude = readUint8(state); const type = prelude >> 5; if (type !== 3) { throw new TypeError(`expected map to only have string keys; got type ${type}`); } const info = prelude & 0x1f; const length = readArgument(state, info); return readString(state, length); }; type Container = | { /** map type */ t: 0; /** container value */ c: Record<string, unknown>; /** held key (as we decode the value) */ k: string; /** remaining elements (key + value) */ r: number; /** next container in stack */ n: Container | null; } | { /** array type */ t: 1; /** container value */ c: any[]; /** held key (not used) */ k: null; /** remaining elements (values) */ r: number; /** next container in stack */ n: Container | null; }; export const decodeFirst = (buf: Uint8Array): [value: any, remainder: Uint8Array] => { const len = buf.length; const state: State = { b: buf, v: null, p: 0, }; let stack: Container | null = null; let value: any; jump: while (state.p < len) { const prelude = readUint8(state); const type = prelude >> 5; const info = prelude & 0x1f; switch (type) { case 0: { value = readArgument(state, info); break; } case 1: { value = -1 - readArgument(state, info); break; } case 2: { value = readBytes(state, readArgument(state, info)); break; } case 3: { value = readString(state, readArgument(state, info)); break; } case 4: { const len = readArgument(state, info); const arr = new Array(len); value = arr; if (len > 0) { stack = { t: 1, c: arr, k: null, r: len, n: stack }; continue jump; } break; } case 5: { const len = readArgument(state, info); const obj: Record<string, unknown> = {}; value = obj; if (len > 0) { // We'll read the key of the first item here. const first = decodeStringKey(state); stack = { t: 0, c: obj, k: first, r: len, n: stack }; continue jump; } break; } case 6: { const arg = readArgument(state, info); switch (arg) { case 42: { const prelude = readUint8(state); const type = prelude >> 5; const info = prelude & 0x1f; if (type !== 2) { throw new TypeError(`expected cid-link to be type 2 (bytes); got type ${type}`); } const len = readArgument(state, info); value = readCid(state, len); break; } default: { throw new TypeError(`unsupported tag; got ${arg}`); } } break; } case 7: { switch (info) { case 20: case 21: { value = info === 21; break; } case 22: { value = null; break; } case 27: { value = readFloat64(state); break; } default: { throw new Error(`invalid simple value; got ${info}`); } } break; } default: { throw new TypeError(`invalid type; got ${type}`); } } while (stack !== null) { switch (stack.t) { case 0: { const obj = stack.c; const key = stack.k; if (key === '__proto__') { // Guard against prototype pollution. CWE-1321 Object.defineProperty(obj, key, { enumerable: true, configurable: true, writable: true }); } obj[key] = value; break; } case 1: { const arr = stack.c; const index = arr.length - stack.r; arr[index] = value; break; } } if (--stack.r !== 0) { // We still have more values to decode, continue if (stack.t === 0) { // Read the key of the next map item const prevKey = stack.k; const nextKey = decodeStringKey(state); if (compareKeys(nextKey, prevKey) <= 0) { throw new TypeError(`map keys are not in canonical order or contain duplicates`); } stack.k = nextKey; } continue jump; } // Unwrap the stack value = stack.c; stack = stack.n; } break; } return [value, buf.subarray(state.p)]; }; export const decode = (buf: Uint8Array): any => { const [value, remainder] = decodeFirst(buf); if (remainder.length !== 0) { throw new Error(`decoded value contains remainder`); } return value; };