import { Annotated } from "./annotated"; import { DecodeError, ShortPacket } from "./codec"; import { Tag } from "./constants"; import { Set, Dictionary } from "./dictionary"; import { DoubleFloat } from "./float"; import { Record } from "./record"; import { Bytes, BytesLike, underlying, hexDigit } from "./bytes"; import { Value } from "./values"; import { is } from "./is"; import { embed, GenericEmbedded, Embedded, EmbeddedTypeDecode } from "./embedded"; import { ReaderStateOptions } from "reader"; export interface DecoderOptions { includeAnnotations?: boolean; } export interface DecoderEmbeddedOptions extends DecoderOptions { embeddedDecode?: EmbeddedTypeDecode; } export interface TypedDecoder { atEnd(): boolean; mark(): any; restoreMark(m: any): undefined; skip(): void; next(): Value; withEmbeddedDecode( embeddedDecode: EmbeddedTypeDecode, body: (d: TypedDecoder) => R): R; nextBoolean(): boolean | undefined; nextDouble(): DoubleFloat | undefined; nextEmbedded(): Embedded | undefined; nextSignedInteger(): number | bigint | undefined; nextString(): string | undefined; nextByteString(): Bytes | undefined; nextSymbol(): symbol | undefined; openRecord(): boolean; openSequence(): boolean; openSet(): boolean; openDictionary(): boolean; closeCompound(): boolean; } export function asLiteral, Annotated>>( actual: Value, expected: E): E | undefined { return is(actual, expected) ? expected : void 0; } export class DecoderState { packet: Uint8Array; index = 0; options: DecoderOptions; constructor(packet: BytesLike, options: DecoderOptions) { this.packet = underlying(packet); 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; } atEnd(): boolean { return this.index >= this.packet.length; } mark(): number { return this.index; } restoreMark(m: number): undefined { this.index = m; return void 0; } shortGuard(body: () => R, short: () => R): R { if (this.atEnd()) return short(); // ^ important somewhat-common case optimization - avoid the exception const start = this.mark(); try { return body(); } catch (e) { if (ShortPacket.isShortPacket(e)) { this.restoreMark(start); return short(); } throw e; } } nextbyte(): number { if (this.atEnd()) throw new ShortPacket("Short packet"); 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 - this makes atEnd() inappropriate 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 { return (this.nextbyte() === Tag.End) || (this.index--, false); } nextint(n: number): number | bigint { const start = this.index; if (n === 0) return 0; if (n > 7) return this.nextbigint(n); if (n === 7) { const highByte = this.packet[this.index]; if ((highByte >= 0x20) && (highByte < 0xe0)) { return this.nextbigint(n); } // if highByte is 0xe0, we still might have a value // equal to (Number.MIN_SAFE_INTEGER-1). } let acc = this.nextbyte(); if (acc & 0x80) acc -= 256; for (let i = 1; i < n; i++) acc = (acc * 256) + this.nextbyte(); if (!Number.isSafeInteger(acc)) { this.index = start; return this.nextbigint(n); } return acc; } nextbigint(n: number): bigint { if (n === 0) return BigInt(0); const bs = Bytes.from(this.nextbytes(n)); if (bs.get(0) >= 128) { // negative const hex = bs.toHex(d => hexDigit(15 - d)); return ~BigInt('0x' + hex); } else { // (strictly) positive const hex = bs.toHex(); return BigInt('0x' + hex); } } wrap(v: Value): Value { return this.includeAnnotations ? new Annotated(v) : v; } unshiftAnnotation(a: Value, v: Annotated): Annotated { if (this.includeAnnotations) { v.annotations.unshift(a); } return v; } } export const neverEmbeddedTypeDecode: EmbeddedTypeDecode = { decode(_s: DecoderState): never { throw new Error("Embeddeds not permitted at this point in Preserves document"); }, fromValue(_v: Value, _options: ReaderStateOptions): never { throw new Error("Embeddeds not permitted at this point in Preserves document"); }, }; export class Decoder implements TypedDecoder { state: DecoderState; embeddedDecode: EmbeddedTypeDecode; constructor(state: DecoderState, embeddedDecode?: EmbeddedTypeDecode); constructor(packet?: BytesLike, options?: DecoderEmbeddedOptions); constructor( packet_or_state: (DecoderState | BytesLike) = new Uint8Array(0), options_or_embeddedDecode?: (DecoderEmbeddedOptions | EmbeddedTypeDecode)) { if (packet_or_state instanceof DecoderState) { this.state = packet_or_state; this.embeddedDecode = (options_or_embeddedDecode as EmbeddedTypeDecode) ?? neverEmbeddedTypeDecode; } else { const options = (options_or_embeddedDecode as DecoderEmbeddedOptions) ?? {}; this.state = new DecoderState(packet_or_state, options); this.embeddedDecode = options.embeddedDecode ?? neverEmbeddedTypeDecode; } } write(data: BytesLike) { this.state.write(data); } nextvalues(): Value[] { const result = []; while (!this.state.peekend()) result.push(this.next()); return result; } static dictionaryFromArray(vs: Value[]): Dictionary { const d = new Dictionary(); 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; } next(): Value { const tag = this.state.nextbyte(); switch (tag) { case Tag.False: return this.state.wrap(false); case Tag.True: return this.state.wrap(true); 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.state.unshiftAnnotation(a, v); } case Tag.Embedded: return this.state.wrap(embed(this.embeddedDecode.decode(this.state))); case Tag.Ieee754: switch (this.state.varint()) { case 8: return this.state.wrap(DoubleFloat.fromBytes(this.state.nextbytes(8))); default: throw new DecodeError("Invalid IEEE754 size"); } case Tag.SignedInteger: return this.state.wrap(this.state.nextint(this.state.varint())); case Tag.String: return this.state.wrap(Bytes.from(this.state.nextbytes(this.state.varint())).fromUtf8()); case Tag.ByteString: return this.state.wrap(Bytes.from(this.state.nextbytes(this.state.varint()))); case Tag.Symbol: return this.state.wrap(Symbol.for(Bytes.from(this.state.nextbytes(this.state.varint())).fromUtf8())); case Tag.Record: { const vs = this.nextvalues(); if (vs.length === 0) throw new DecodeError("Too few elements in encoded record"); return this.state.wrap(Record(vs[0], vs.slice(1))); } case Tag.Sequence: return this.state.wrap(this.nextvalues()); case Tag.Set: return this.state.wrap(new Set(this.nextvalues())); case Tag.Dictionary: return this.state.wrap(Decoder.dictionaryFromArray(this.nextvalues())); default: throw new DecodeError("Unsupported Preserves tag: " + tag); } } try_next(): Value | undefined { return this.state.shortGuard(() => this.next(), () => void 0); } atEnd(): boolean { return this.state.atEnd(); } mark(): any { return this.state.mark(); } restoreMark(m: any): undefined { return this.state.restoreMark(m); } skip(): void { // TODO: be more efficient this.next(); } withEmbeddedDecode( embeddedDecode: EmbeddedTypeDecode, body: (d: TypedDecoder) => R): R { return body(new Decoder(this.state, embeddedDecode)); } skipAnnotations(f: (reset: () => undefined) => R): R { const m = this.mark(); while (!this.state.atEnd() && this.state.packet[this.state.index] === Tag.Annotation) { this.state.index++; this.skip(); } return f(() => this.restoreMark(m)); } nextBoolean(): boolean | undefined { return this.skipAnnotations((reset) => { switch (this.state.nextbyte()) { case Tag.False: return false; case Tag.True: return true; default: return reset(); } }); } nextDouble(): DoubleFloat | undefined { return this.skipAnnotations((reset) => { if (this.state.nextbyte() !== Tag.Ieee754) return reset(); if (this.state.nextbyte() !== 8) return reset(); return DoubleFloat.fromBytes(this.state.nextbytes(8)); }); } nextEmbedded(): Embedded | undefined { return this.skipAnnotations((reset) => { switch (this.state.nextbyte()) { case Tag.Embedded: return embed(this.embeddedDecode.decode(this.state)); default: return reset(); } }); } nextSignedInteger(): number | bigint | undefined { return this.skipAnnotations((reset) => { switch (this.state.nextbyte()) { case Tag.SignedInteger: return this.state.nextint(this.state.varint()); default: return reset(); } }); } nextString(): string | undefined { return this.skipAnnotations((reset) => { switch (this.state.nextbyte()) { case Tag.String: return Bytes.from(this.state.nextbytes(this.state.varint())).fromUtf8(); default: return reset(); } }); } nextByteString(): Bytes | undefined { return this.skipAnnotations((reset) => { switch (this.state.nextbyte()) { case Tag.ByteString: return Bytes.from(this.state.nextbytes(this.state.varint())); default: return reset(); } }); } nextSymbol(): symbol | undefined { return this.skipAnnotations((reset) => { switch (this.state.nextbyte()) { case Tag.Symbol: return Symbol.for(Bytes.from(this.state.nextbytes(this.state.varint())).fromUtf8()); default: return reset(); } }); } openRecord(): boolean { return this.skipAnnotations((reset) => (this.state.nextbyte() === Tag.Record) || (reset(), false)); } openSequence(): boolean { return this.skipAnnotations((reset) => (this.state.nextbyte() === Tag.Sequence) || (reset(), false)); } openSet(): boolean { return this.skipAnnotations((reset) => (this.state.nextbyte() === Tag.Set) || (reset(), false)); } openDictionary(): boolean { return this.skipAnnotations((reset) => (this.state.nextbyte() === Tag.Dictionary) || (reset(), false)); } closeCompound(): boolean { return this.state.peekend(); } } export function decode(bs: BytesLike, options: DecoderEmbeddedOptions = {}): Value { return new Decoder(bs, options).next(); } export function decodeWithAnnotations(bs: BytesLike, options: DecoderEmbeddedOptions = {}): Annotated { return decode(bs, { ... options, includeAnnotations: true }) as Annotated; }