172 lines
6.1 KiB
TypeScript
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>;
|
|
}
|