import { Annotated } from "./annotated"; import { DecodeError, ShortPacket } from "./codec"; import { Tag } from "./constants"; import { Set, Dictionary } from "./dictionary"; import { DoubleFloat, SingleFloat } from "./float"; import { Record } from "./record"; import { Bytes, BytesLike, underlying } from "./bytes"; import { Value } from "./values"; export interface DecoderOptions { includeAnnotations?: boolean; decodePointer?: (v: Value) => T; } export class Decoder { packet: Uint8Array; index: number; options: DecoderOptions; constructor(packet: BytesLike = new Uint8Array(0), options: DecoderOptions = {}) { this.packet = underlying(packet); this.index = 0; this.options = options; } get includeAnnotations(): boolean { return this.options.includeAnnotations ?? false; } write(data: BytesLike) { if (this.index === this.packet.length) { this.packet = underlying(data); } else { this.packet = Bytes.concat([this.packet.slice(this.index), data])._view; } this.index = 0; } nextbyte(): number { if (this.index >= this.packet.length) throw new ShortPacket("Short packet"); // ^ NOTE: greater-than-or-equal-to, not greater-than. return this.packet[this.index++]; } nextbytes(n: number): DataView { const start = this.index; this.index += n; if (this.index > this.packet.length) throw new ShortPacket("Short packet"); // ^ NOTE: greater-than, not greater-than-or-equal-to. return new DataView(this.packet.buffer, this.packet.byteOffset + start, n); } varint(): number { // TODO: Bignums :-/ const v = this.nextbyte(); if (v < 128) return v; return (this.varint() << 7) + (v - 128); } peekend(): boolean { const matched = this.nextbyte() === Tag.End; if (!matched) this.index--; return matched; } nextvalues(): Value[] { const result = []; while (!this.peekend()) result.push(this.next()); return result; } nextint(n: number): number { // TODO: Bignums :-/ if (n === 0) return 0; let acc = this.nextbyte(); if (acc & 0x80) acc -= 256; for (let i = 1; i < n; i++) acc = (acc * 256) + this.nextbyte(); return acc; } wrap(v: Value): Value { return this.includeAnnotations ? new Annotated(v) : v; } static dictionaryFromArray(vs: Value[]): Dictionary, T> { const d = new Dictionary, T>(); if (vs.length % 2) throw new DecodeError("Missing dictionary value"); for (let i = 0; i < vs.length; i += 2) { d.set(vs[i], vs[i+1]); } return d; } unshiftAnnotation(a: Value, v: Annotated) { if (this.includeAnnotations) { v.annotations.unshift(a); } return v; } next(): Value { const tag = this.nextbyte(); switch (tag) { case Tag.False: return this.wrap(false); case Tag.True: return this.wrap(true); case Tag.Float: return this.wrap(new SingleFloat(this.nextbytes(4).getFloat32(0, false))); case Tag.Double: return this.wrap(new DoubleFloat(this.nextbytes(8).getFloat64(0, false))); case Tag.End: throw new DecodeError("Unexpected Compound end marker"); case Tag.Annotation: { const a = this.next(); const v = this.next() as Annotated; return this.unshiftAnnotation(a, v); } case Tag.Pointer: { const d = this.options.decodePointer; if (d === void 0) { throw new DecodeError("No decodePointer function supplied"); } return this.wrap(d(this.next())); } case Tag.SignedInteger: return this.wrap(this.nextint(this.varint())); case Tag.String: return this.wrap(Bytes.from(this.nextbytes(this.varint())).fromUtf8()); case Tag.ByteString: return this.wrap(Bytes.from(this.nextbytes(this.varint()))); case Tag.Symbol: return this.wrap(Symbol.for(Bytes.from(this.nextbytes(this.varint())).fromUtf8())); case Tag.Record: { const vs = this.nextvalues(); if (vs.length === 0) throw new DecodeError("Too few elements in encoded record"); return this.wrap(Record(vs[0], vs.slice(1))); } case Tag.Sequence: return this.wrap(this.nextvalues()); case Tag.Set: return this.wrap(new Set(this.nextvalues())); case Tag.Dictionary: return this.wrap(Decoder.dictionaryFromArray(this.nextvalues())); default: { if (tag >= Tag.SmallInteger_lo && tag <= Tag.SmallInteger_lo + 15) { const v = tag - Tag.SmallInteger_lo; return this.wrap(v > 12 ? v - 16 : v); } if (tag >= Tag.MediumInteger_lo && tag <= Tag.MediumInteger_lo + 15) { const n = tag - Tag.MediumInteger_lo; return this.wrap(this.nextint(n + 1)); } throw new DecodeError("Unsupported Preserves tag: " + tag); } } } try_next(): Value | undefined { const start = this.index; if (start >= this.packet.length) return void 0; // ^ important somewhat-common case optimization - avoid the exception try { return this.next(); } catch (e) { if (ShortPacket.isShortPacket(e)) { this.index = start; return void 0; } throw e; } } } export function decode(bs: BytesLike, options?: DecoderOptions) { return new Decoder(bs, options).next(); } export function decodeWithAnnotations(bs: BytesLike, options: DecoderOptions = {}): Annotated { return decode(bs, { ... options, includeAnnotations: true }) as Annotated; }