preserves/implementations/javascript/packages/core/src/decoder.ts

172 lines
6.1 KiB
TypeScript

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<T extends object> {
includeAnnotations?: boolean;
decodePointer?: (v: Value<T>) => T;
}
export class Decoder<T extends object> {
packet: Uint8Array;
index: number;
options: DecoderOptions<T>;
constructor(packet: BytesLike = new Uint8Array(0), options: DecoderOptions<T> = {}) {
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<T>[] {
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<T>): Value<T> {
return this.includeAnnotations ? new Annotated(v) : v;
}
static dictionaryFromArray<T extends object>(vs: Value<T>[]): Dictionary<Value<T>, T> {
const d = new Dictionary<Value<T>, 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<T>, v: Annotated<T>) {
if (this.includeAnnotations) {
v.annotations.unshift(a);
}
return v;
}
next(): Value<T> {
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<T>;
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<T> | 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<T extends object>(bs: BytesLike, options?: DecoderOptions<T>) {
return new Decoder(bs, options).next();
}
export function decodeWithAnnotations<T extends object>(bs: BytesLike, options: DecoderOptions<T> = {}): Annotated<T> {
return decode(bs, { ... options, includeAnnotations: true }) as Annotated<T>;
}