Blue TypeScript implementation
This commit is contained in:
parent
6d496b5113
commit
b5458dbf8c
|
@ -4,6 +4,7 @@ import type { GenericEmbedded } from "./embedded";
|
||||||
import type { Value } from "./values";
|
import type { Value } from "./values";
|
||||||
import type { Encoder, Preservable } from "./encoder";
|
import type { Encoder, Preservable } from "./encoder";
|
||||||
import type { Writer, PreserveWritable } from "./writer";
|
import type { Writer, PreserveWritable } from "./writer";
|
||||||
|
import * as IO from "./iolist";
|
||||||
|
|
||||||
export interface Position {
|
export interface Position {
|
||||||
line?: number;
|
line?: number;
|
||||||
|
@ -71,14 +72,12 @@ export class Annotated<T = GenericEmbedded> implements Preservable<T>, PreserveW
|
||||||
return isAnnotated<T>(v) ? v : void 0;
|
return isAnnotated<T>(v) ? v : void 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
__preserve_on__(encoder: Encoder<T>): void {
|
__preserve_on__(encoder: Encoder<T>): IO.IOList {
|
||||||
if (encoder.includeAnnotations) {
|
if (encoder.includeAnnotations && this.annotations.length > 0) {
|
||||||
for (const a of this.annotations) {
|
return [Tag.Annotation, encoder._encodevalues([this.item, ... this.annotations])];
|
||||||
encoder.state.emitbyte(Tag.Annotation);
|
} else {
|
||||||
encoder.push(a);
|
return encoder._encode(this.item);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
encoder.push(this.item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
__preserve_text_on__(w: Writer<T>): void {
|
__preserve_text_on__(w: Writer<T>): void {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { GenericEmbedded } from './embedded';
|
||||||
import { Encoder, Preservable } from './encoder';
|
import { Encoder, Preservable } from './encoder';
|
||||||
import { Value } from './values';
|
import { Value } from './values';
|
||||||
import { Writer, PreserveWritable } from './writer';
|
import { Writer, PreserveWritable } from './writer';
|
||||||
|
import * as IO from './iolist';
|
||||||
|
|
||||||
const textEncoder = new TextEncoder();
|
const textEncoder = new TextEncoder();
|
||||||
const textDecoder = new TextDecoder();
|
const textDecoder = new TextDecoder();
|
||||||
|
@ -145,10 +146,8 @@ export class Bytes implements Preservable<any>, PreserveWritable<any> {
|
||||||
return this.toHex();
|
return this.toHex();
|
||||||
}
|
}
|
||||||
|
|
||||||
__preserve_on__(encoder: Encoder<any>) {
|
__preserve_on__<T>(_encoder: Encoder<T>): IO.IOList {
|
||||||
encoder.state.emitbyte(Tag.ByteString);
|
return [Tag.ByteString, this._view];
|
||||||
encoder.state.varint(this.length);
|
|
||||||
encoder.state.emitbytes(this._view);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
__preserve_text_on__(w: Writer<any>) {
|
__preserve_text_on__(w: Writer<any>) {
|
||||||
|
|
|
@ -1,16 +1,8 @@
|
||||||
export enum Tag {
|
export enum Tag {
|
||||||
False = 0x80,
|
False = 0xa0,
|
||||||
True,
|
True,
|
||||||
Float,
|
Float,
|
||||||
Double,
|
SignedInteger,
|
||||||
End,
|
|
||||||
Annotation,
|
|
||||||
Embedded,
|
|
||||||
|
|
||||||
SmallInteger_lo = 0x90,
|
|
||||||
MediumInteger_lo = 0xa0,
|
|
||||||
|
|
||||||
SignedInteger = 0xb0,
|
|
||||||
String,
|
String,
|
||||||
ByteString,
|
ByteString,
|
||||||
Symbol,
|
Symbol,
|
||||||
|
@ -18,4 +10,7 @@ export enum Tag {
|
||||||
Sequence,
|
Sequence,
|
||||||
Set,
|
Set,
|
||||||
Dictionary,
|
Dictionary,
|
||||||
|
Embedded,
|
||||||
|
|
||||||
|
Annotation = 0xbf,
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { DoubleFloat, SingleFloat } from "./float";
|
||||||
import { Record } from "./record";
|
import { Record } from "./record";
|
||||||
import { Bytes, BytesLike, underlying } from "./bytes";
|
import { Bytes, BytesLike, underlying } from "./bytes";
|
||||||
import { Value } from "./values";
|
import { Value } from "./values";
|
||||||
import { is } from "./is";
|
|
||||||
import { embed, GenericEmbedded, Embedded, EmbeddedTypeDecode } from "./embedded";
|
import { embed, GenericEmbedded, Embedded, EmbeddedTypeDecode } from "./embedded";
|
||||||
import { ReaderStateOptions } from "reader";
|
import { ReaderStateOptions } from "reader";
|
||||||
|
|
||||||
|
@ -47,21 +46,29 @@ export interface TypedDecoder<T> {
|
||||||
closeCompound(): boolean;
|
closeCompound(): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function asLiteral<T, E extends Exclude<Value<T>, Annotated<T>>>(
|
type DecoderStateMark = {
|
||||||
actual: Value<T>,
|
index: number;
|
||||||
expected: E): E | undefined
|
inSequence: boolean;
|
||||||
{
|
};
|
||||||
return is(actual, expected) ? expected : void 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DecoderState {
|
export class DecoderState {
|
||||||
packet: Uint8Array;
|
|
||||||
index = 0;
|
|
||||||
options: DecoderOptions;
|
options: DecoderOptions;
|
||||||
|
packet: Uint8Array;
|
||||||
|
count: number | null;
|
||||||
|
index = 0;
|
||||||
|
inSequence = false;
|
||||||
|
|
||||||
constructor(packet: BytesLike, options: DecoderOptions) {
|
constructor(packet: BytesLike, options: DecoderOptions) {
|
||||||
this.packet = underlying(packet);
|
|
||||||
this.options = options;
|
this.options = options;
|
||||||
|
this.packet = underlying(packet);
|
||||||
|
this.count = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setExpectedCount(expectedCount: number) {
|
||||||
|
if (this.count !== null) {
|
||||||
|
throw new Error(`Attempt to setExpectedCount to ${expectedCount} when count already ${this.count}`);
|
||||||
|
}
|
||||||
|
this.count = expectedCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
get includeAnnotations(): boolean {
|
get includeAnnotations(): boolean {
|
||||||
|
@ -78,15 +85,23 @@ export class DecoderState {
|
||||||
}
|
}
|
||||||
|
|
||||||
atEnd(): boolean {
|
atEnd(): boolean {
|
||||||
return this.index >= this.packet.length;
|
if (this.count === null) { // toplevel
|
||||||
|
return this.index >= this.packet.length;
|
||||||
|
} else { // nested
|
||||||
|
return this.count <= 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mark(): number {
|
mark(): DecoderStateMark {
|
||||||
return this.index;
|
return {
|
||||||
|
index: this.index,
|
||||||
|
inSequence: this.inSequence,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreMark(m: number): void {
|
restoreMark(m: DecoderStateMark): void {
|
||||||
this.index = m;
|
this.index = m.index;
|
||||||
|
this.inSequence = m.inSequence;
|
||||||
}
|
}
|
||||||
|
|
||||||
shortGuard<R>(body: () => R, short: () => R): R {
|
shortGuard<R>(body: () => R, short: () => R): R {
|
||||||
|
@ -107,59 +122,110 @@ export class DecoderState {
|
||||||
|
|
||||||
nextbyte(): number {
|
nextbyte(): number {
|
||||||
if (this.atEnd()) throw new ShortPacket("Short packet");
|
if (this.atEnd()) throw new ShortPacket("Short packet");
|
||||||
|
if (this.count !== null) this.count--;
|
||||||
return this.packet[this.index++];
|
return this.packet[this.index++];
|
||||||
}
|
}
|
||||||
|
|
||||||
nextbytes(n: number): DataView {
|
_rewind(): undefined {
|
||||||
const start = this.index;
|
this.index--;
|
||||||
|
if (this.count !== null) this.count++;
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
error(message: string, offset = 0): never {
|
||||||
|
throw new DecodeError(message, { pos: this.index + offset });
|
||||||
|
}
|
||||||
|
|
||||||
|
_ensureCounted(): number {
|
||||||
|
if (this.count === null) {
|
||||||
|
this.error("Attempt to retrieve sized object in uncounted context");
|
||||||
|
}
|
||||||
|
return this.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextbytes(): DataView {
|
||||||
|
const n = this._ensureCounted();
|
||||||
|
const c = new DataView(this.packet.buffer, this.packet.byteOffset + this.index, n);
|
||||||
this.index += n;
|
this.index += n;
|
||||||
if (this.index > this.packet.length) throw new ShortPacket("Short packet");
|
this.count = 0;
|
||||||
// ^ NOTE: greater-than, not greater-than-or-equal-to - this makes atEnd() inappropriate
|
return c;
|
||||||
return new DataView(this.packet.buffer, this.packet.byteOffset + start, n);
|
}
|
||||||
|
|
||||||
|
_checkLengthInfo(toConsume: number) {
|
||||||
|
if (this.count === null) {
|
||||||
|
if (this.index + toConsume > this.packet.length) {
|
||||||
|
throw new ShortPacket("Short packet");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (toConsume > this.count) {
|
||||||
|
this.error(`Attempt to read ${toConsume} bytes when only ${this.count} are available`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
skip(byteCount: number) {
|
||||||
|
this._checkLengthInfo(byteCount);
|
||||||
|
this.index += byteCount;
|
||||||
|
if (this.count !== null) this.count -= byteCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
varint(): number {
|
varint(): number {
|
||||||
// TODO: Bignums :-/
|
// TODO: Bignums :-/
|
||||||
const v = this.nextbyte();
|
let v = this.nextbyte();
|
||||||
if (v < 128) return v;
|
{
|
||||||
return (this.varint() << 7) + (v - 128);
|
let redundantLeadingZeroCount = 0;
|
||||||
|
while (true) {
|
||||||
|
if (v !== 0) break;
|
||||||
|
redundantLeadingZeroCount++;
|
||||||
|
if (redundantLeadingZeroCount >= 8) {
|
||||||
|
this.error("Excessively overlong varint", -1);
|
||||||
|
}
|
||||||
|
v = this.nextbyte();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let acc = 0;
|
||||||
|
while (true) {
|
||||||
|
if (v >= 128) return (acc << 7) + v - 128;
|
||||||
|
acc = (acc << 7) + v;
|
||||||
|
v = this.nextbyte();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
peekend(): boolean {
|
nextint(): number {
|
||||||
return (this.nextbyte() === Tag.End) || (this.index--, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
nextint(n: number): number {
|
|
||||||
// TODO: Bignums :-/
|
// TODO: Bignums :-/
|
||||||
if (n === 0) return 0;
|
this._ensureCounted();
|
||||||
|
if (this.count! === 0) return 0;
|
||||||
let acc = this.nextbyte();
|
let acc = this.nextbyte();
|
||||||
if (acc & 0x80) acc -= 256;
|
if (acc & 0x80) acc -= 256;
|
||||||
for (let i = 1; i < n; i++) acc = (acc * 256) + this.nextbyte();
|
while (this.count! > 0) acc = (acc * 256) + this.nextbyte();
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
nextSmallOrMediumInteger(tag: number): number | undefined {
|
|
||||||
if (tag >= Tag.SmallInteger_lo && tag <= Tag.SmallInteger_lo + 15) {
|
|
||||||
const v = tag - Tag.SmallInteger_lo;
|
|
||||||
return v > 12 ? v - 16 : v;
|
|
||||||
}
|
|
||||||
if (tag >= Tag.MediumInteger_lo && tag <= Tag.MediumInteger_lo + 15) {
|
|
||||||
const n = tag - Tag.MediumInteger_lo;
|
|
||||||
return this.nextint(n + 1);
|
|
||||||
}
|
|
||||||
return void 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
wrap<T>(v: Value<T>): Value<T> {
|
wrap<T>(v: Value<T>): Value<T> {
|
||||||
return this.includeAnnotations ? new Annotated(v) : v;
|
return this.includeAnnotations ? new Annotated(v) : v;
|
||||||
}
|
}
|
||||||
|
|
||||||
unshiftAnnotation<T>(a: Value<T>, v: Annotated<T>): Annotated<T> {
|
unshiftAnnotation<T>(anns: Value<T>[], v: Annotated<T>): Annotated<T> {
|
||||||
if (this.includeAnnotations) {
|
if (this.includeAnnotations) {
|
||||||
v.annotations.unshift(a);
|
v.annotations.unshift(... anns);
|
||||||
}
|
}
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_withCount<R>(newCount: number, f: () => R): R {
|
||||||
|
this._checkLengthInfo(newCount);
|
||||||
|
const nextCount = this.count === null ? null : this.count - newCount;
|
||||||
|
const savedInSequence = this.inSequence;
|
||||||
|
this.count = newCount;
|
||||||
|
this.inSequence = false;
|
||||||
|
try {
|
||||||
|
return f();
|
||||||
|
} finally {
|
||||||
|
this.index += this.count;
|
||||||
|
this.count = nextCount;
|
||||||
|
this.inSequence = savedInSequence;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const neverEmbeddedTypeDecode: EmbeddedTypeDecode<never> = {
|
export const neverEmbeddedTypeDecode: EmbeddedTypeDecode<never> = {
|
||||||
|
@ -172,22 +238,42 @@ export const neverEmbeddedTypeDecode: EmbeddedTypeDecode<never> = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function chopNul(bs: Bytes): Bytes {
|
||||||
|
if (bs.get(bs.length - 1) !== 0) throw new DecodeError("Missing mandatory NUL byte in string");
|
||||||
|
return bs.slice(0, bs.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
export class Decoder<T = never> implements TypedDecoder<T> {
|
export class Decoder<T = never> implements TypedDecoder<T> {
|
||||||
state: DecoderState;
|
state: DecoderState;
|
||||||
embeddedDecode: EmbeddedTypeDecode<T>;
|
embeddedDecode: EmbeddedTypeDecode<T>;
|
||||||
|
|
||||||
constructor(state: DecoderState, embeddedDecode?: EmbeddedTypeDecode<T>);
|
/* (A) */ constructor()
|
||||||
constructor(packet?: BytesLike, options?: DecoderEmbeddedOptions<T>);
|
/* (B) */ constructor(state: DecoderState, embeddedDecode?: EmbeddedTypeDecode<T>);
|
||||||
|
/* (C) */ constructor(options: DecoderEmbeddedOptions<T>);
|
||||||
|
/* (D) */ constructor(packet: BytesLike, options?: DecoderEmbeddedOptions<T>);
|
||||||
constructor(
|
constructor(
|
||||||
packet_or_state: (DecoderState | BytesLike) = new Uint8Array(0),
|
packet_or_state_or_options?: (DecoderState | BytesLike | DecoderEmbeddedOptions<T>),
|
||||||
options_or_embeddedDecode?: (DecoderEmbeddedOptions<T> | EmbeddedTypeDecode<T>))
|
options_or_embeddedDecode?: (DecoderEmbeddedOptions<T> | EmbeddedTypeDecode<T>))
|
||||||
{
|
{
|
||||||
if (packet_or_state instanceof DecoderState) {
|
if (packet_or_state_or_options === void 0) {
|
||||||
this.state = packet_or_state;
|
// (A)
|
||||||
|
this.state = new DecoderState(new Uint8Array(0), {});
|
||||||
|
this.embeddedDecode = neverEmbeddedTypeDecode;
|
||||||
|
} else if (packet_or_state_or_options instanceof DecoderState) {
|
||||||
|
// (B)
|
||||||
|
this.state = packet_or_state_or_options;
|
||||||
this.embeddedDecode = (options_or_embeddedDecode as EmbeddedTypeDecode<T>) ?? neverEmbeddedTypeDecode;
|
this.embeddedDecode = (options_or_embeddedDecode as EmbeddedTypeDecode<T>) ?? neverEmbeddedTypeDecode;
|
||||||
} else {
|
} else if ('length' in packet_or_state_or_options) {
|
||||||
|
// (D)
|
||||||
|
const packet = packet_or_state_or_options;
|
||||||
const options = (options_or_embeddedDecode as DecoderEmbeddedOptions<T>) ?? {};
|
const options = (options_or_embeddedDecode as DecoderEmbeddedOptions<T>) ?? {};
|
||||||
this.state = new DecoderState(packet_or_state, options);
|
this.state = new DecoderState(packet, options);
|
||||||
|
this.state.setExpectedCount(packet.length);
|
||||||
|
this.embeddedDecode = options.embeddedDecode ?? neverEmbeddedTypeDecode;
|
||||||
|
} else {
|
||||||
|
// (C)
|
||||||
|
const options = packet_or_state_or_options;
|
||||||
|
this.state = new DecoderState(new Uint8Array(0), options);
|
||||||
this.embeddedDecode = options.embeddedDecode ?? neverEmbeddedTypeDecode;
|
this.embeddedDecode = options.embeddedDecode ?? neverEmbeddedTypeDecode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -198,13 +284,17 @@ export class Decoder<T = never> implements TypedDecoder<T> {
|
||||||
|
|
||||||
nextvalues(): Value<T>[] {
|
nextvalues(): Value<T>[] {
|
||||||
const result = [];
|
const result = [];
|
||||||
while (!this.state.peekend()) result.push(this.next());
|
this.state.inSequence = true;
|
||||||
|
while (!this.state.atEnd()) result.push(this.next());
|
||||||
|
this.state.inSequence = false;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
static dictionaryFromArray<T>(vs: Value<T>[]): Dictionary<T> {
|
static dictionaryFromArray<T>(vs: Value<T>[]): Dictionary<T> {
|
||||||
const d = new Dictionary<T>();
|
const d = new Dictionary<T>();
|
||||||
if (vs.length % 2) throw new DecodeError("Missing dictionary value");
|
if (vs.length % 2) {
|
||||||
|
throw new DecodeError("Missing dictionary value");
|
||||||
|
}
|
||||||
for (let i = 0; i < vs.length; i += 2) {
|
for (let i = 0; i < vs.length; i += 2) {
|
||||||
d.set(vs[i], vs[i+1]);
|
d.set(vs[i], vs[i+1]);
|
||||||
}
|
}
|
||||||
|
@ -212,38 +302,47 @@ export class Decoder<T = never> implements TypedDecoder<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
next(): Value<T> {
|
next(): Value<T> {
|
||||||
|
if (this.state.inSequence) {
|
||||||
|
return this.state._withCount(this.state.varint(), () => this._next());
|
||||||
|
} else {
|
||||||
|
return this._next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_next(): Value<T> {
|
||||||
const tag = this.state.nextbyte();
|
const tag = this.state.nextbyte();
|
||||||
switch (tag) {
|
switch (tag) {
|
||||||
case Tag.False: return this.state.wrap<T>(false);
|
case Tag.False: return this.state.wrap<T>(false);
|
||||||
case Tag.True: return this.state.wrap<T>(true);
|
case Tag.True: return this.state.wrap<T>(true);
|
||||||
case Tag.Float: return this.state.wrap<T>(new SingleFloat(this.state.nextbytes(4).getFloat32(0, false)));
|
case Tag.Float: switch (this.state.count) {
|
||||||
case Tag.Double: return this.state.wrap<T>(new DoubleFloat(this.state.nextbytes(8).getFloat64(0, false)));
|
case 4: return this.state.wrap<T>(new SingleFloat(this.state.nextbytes().getFloat32(0, false)));
|
||||||
case Tag.End: throw new DecodeError("Unexpected Compound end marker");
|
case 8: return this.state.wrap<T>(new DoubleFloat(this.state.nextbytes().getFloat64(0, false)));
|
||||||
case Tag.Annotation: {
|
default: this.state.error("Bad floating-point value length " + this.state.count);
|
||||||
const a = this.next();
|
|
||||||
const v = this.next() as Annotated<T>;
|
|
||||||
return this.state.unshiftAnnotation(a, v);
|
|
||||||
}
|
}
|
||||||
case Tag.Embedded: return this.state.wrap<T>(embed(this.embeddedDecode.decode(this.state)));
|
case Tag.SignedInteger: return this.state.wrap<T>(this.state.nextint());
|
||||||
case Tag.SignedInteger: return this.state.wrap<T>(this.state.nextint(this.state.varint()));
|
case Tag.String: return this.state.wrap<T>(chopNul(Bytes.from(this.state.nextbytes())).fromUtf8());
|
||||||
case Tag.String: return this.state.wrap<T>(Bytes.from(this.state.nextbytes(this.state.varint())).fromUtf8());
|
case Tag.ByteString: return this.state.wrap<T>(Bytes.from(this.state.nextbytes()));
|
||||||
case Tag.ByteString: return this.state.wrap<T>(Bytes.from(this.state.nextbytes(this.state.varint())));
|
case Tag.Symbol: return this.state.wrap<T>(Symbol.for(Bytes.from(this.state.nextbytes()).fromUtf8()));
|
||||||
case Tag.Symbol: return this.state.wrap<T>(Symbol.for(Bytes.from(this.state.nextbytes(this.state.varint())).fromUtf8()));
|
|
||||||
case Tag.Record: {
|
case Tag.Record: {
|
||||||
const vs = this.nextvalues();
|
const vs = this.nextvalues();
|
||||||
if (vs.length === 0) throw new DecodeError("Too few elements in encoded record");
|
if (vs.length === 0) this.state.error("Too few elements in encoded record");
|
||||||
return this.state.wrap<T>(Record(vs[0], vs.slice(1)));
|
return this.state.wrap<T>(Record(vs[0], vs.slice(1)));
|
||||||
}
|
}
|
||||||
case Tag.Sequence: return this.state.wrap<T>(this.nextvalues());
|
case Tag.Sequence: return this.state.wrap<T>(this.nextvalues());
|
||||||
case Tag.Set: return this.state.wrap<T>(new Set(this.nextvalues()));
|
case Tag.Set: return this.state.wrap<T>(new Set(this.nextvalues()));
|
||||||
case Tag.Dictionary: return this.state.wrap<T>(Decoder.dictionaryFromArray(this.nextvalues()));
|
case Tag.Dictionary: return this.state.wrap<T>(Decoder.dictionaryFromArray(this.nextvalues()));
|
||||||
default: {
|
|
||||||
const v = this.state.nextSmallOrMediumInteger(tag);
|
case Tag.Embedded: return this.state.wrap<T>(embed(this.embeddedDecode.decode(this.state)));
|
||||||
if (v === void 0) {
|
|
||||||
throw new DecodeError("Unsupported Preserves tag: " + tag);
|
case Tag.Annotation: {
|
||||||
}
|
const vs = this.nextvalues();
|
||||||
return this.state.wrap<T>(v);
|
if (vs.length === 0) this.state.error("Missing value in encoded annotation");
|
||||||
|
const anns = vs.slice(1);
|
||||||
|
const v = vs[0] as Annotated<T>;
|
||||||
|
return this.state.unshiftAnnotation(anns, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default: this.state.error("Unsupported Preserves tag: " + tag, -1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,8 +363,11 @@ export class Decoder<T = never> implements TypedDecoder<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
skip(): void {
|
skip(): void {
|
||||||
// TODO: be more efficient
|
if (this.state.inSequence) {
|
||||||
this.next();
|
this.state.skip(this.state.varint());
|
||||||
|
} else {
|
||||||
|
this.next();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
withEmbeddedDecode<S, R>(
|
withEmbeddedDecode<S, R>(
|
||||||
|
@ -278,7 +380,12 @@ export class Decoder<T = never> implements TypedDecoder<T> {
|
||||||
skipAnnotations(): void {
|
skipAnnotations(): void {
|
||||||
if (!this.state.atEnd() && this.state.packet[this.state.index] === Tag.Annotation) {
|
if (!this.state.atEnd() && this.state.packet[this.state.index] === Tag.Annotation) {
|
||||||
this.state.index++;
|
this.state.index++;
|
||||||
this.skip();
|
const valueLen = this.state.varint();
|
||||||
|
this.state._checkLengthInfo(valueLen);
|
||||||
|
this.state.count = valueLen;
|
||||||
|
if (!this.state.atEnd() && this.state.packet[this.state.index] === Tag.Annotation) {
|
||||||
|
this.state.error("Immediately-nested Annotation detected");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -287,91 +394,87 @@ export class Decoder<T = never> implements TypedDecoder<T> {
|
||||||
switch (this.state.nextbyte()) {
|
switch (this.state.nextbyte()) {
|
||||||
case Tag.False: return false;
|
case Tag.False: return false;
|
||||||
case Tag.True: return true;
|
case Tag.True: return true;
|
||||||
default: return void 0;
|
default: return this.state._rewind();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nextFloat(): SingleFloat | undefined {
|
nextFloat(): SingleFloat | undefined {
|
||||||
this.skipAnnotations();
|
this.skipAnnotations();
|
||||||
switch (this.state.nextbyte()) {
|
if (this.state.nextbyte() !== Tag.Float || this.state.count !== 4) {
|
||||||
case Tag.Float: return new SingleFloat(this.state.nextbytes(4).getFloat32(0, false));
|
return this.state._rewind();
|
||||||
default: return void 0;
|
|
||||||
}
|
}
|
||||||
|
return new SingleFloat(this.state.nextbytes().getFloat32(0, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
nextDouble(): DoubleFloat | undefined {
|
nextDouble(): DoubleFloat | undefined {
|
||||||
this.skipAnnotations();
|
this.skipAnnotations();
|
||||||
switch (this.state.nextbyte()) {
|
if (this.state.nextbyte() !== Tag.Float || this.state.count !== 8) {
|
||||||
case Tag.Double: return new DoubleFloat(this.state.nextbytes(8).getFloat64(0, false));
|
return this.state._rewind();
|
||||||
default: return void 0;
|
|
||||||
}
|
}
|
||||||
|
return new DoubleFloat(this.state.nextbytes().getFloat64(0, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
nextEmbedded(): Embedded<T> | undefined {
|
nextEmbedded(): Embedded<T> | undefined {
|
||||||
this.skipAnnotations();
|
this.skipAnnotations();
|
||||||
switch (this.state.nextbyte()) {
|
if (this.state.nextbyte() !== Tag.Embedded) return this.state._rewind();
|
||||||
case Tag.Embedded: return embed(this.embeddedDecode.decode(this.state));
|
return embed(this.embeddedDecode.decode(this.state));
|
||||||
default: return void 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nextSignedInteger(): number | undefined {
|
nextSignedInteger(): number | undefined {
|
||||||
this.skipAnnotations();
|
this.skipAnnotations();
|
||||||
const b = this.state.nextbyte();
|
if (this.state.nextbyte() !== Tag.SignedInteger) return this.state._rewind();
|
||||||
switch (b) {
|
return this.state.nextint();
|
||||||
case Tag.SignedInteger: return this.state.nextint(this.state.varint());
|
|
||||||
default: return this.state.nextSmallOrMediumInteger(b);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nextString(): string | undefined {
|
nextString(): string | undefined {
|
||||||
this.skipAnnotations();
|
this.skipAnnotations();
|
||||||
switch (this.state.nextbyte()) {
|
if (this.state.nextbyte() !== Tag.String) return this.state._rewind();
|
||||||
case Tag.String: return Bytes.from(this.state.nextbytes(this.state.varint())).fromUtf8();
|
return Bytes.from(this.state.nextbytes()).fromUtf8();
|
||||||
default: return void 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nextByteString(): Bytes | undefined {
|
nextByteString(): Bytes | undefined {
|
||||||
this.skipAnnotations();
|
this.skipAnnotations();
|
||||||
switch (this.state.nextbyte()) {
|
if (this.state.nextbyte() !== Tag.ByteString) return this.state._rewind();
|
||||||
case Tag.ByteString: return Bytes.from(this.state.nextbytes(this.state.varint()));
|
return Bytes.from(this.state.nextbytes());
|
||||||
default: return void 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nextSymbol(): symbol | undefined {
|
nextSymbol(): symbol | undefined {
|
||||||
this.skipAnnotations();
|
this.skipAnnotations();
|
||||||
switch (this.state.nextbyte()) {
|
if (this.state.nextbyte() !== Tag.Symbol) return this.state._rewind();
|
||||||
case Tag.Symbol:
|
return Symbol.for(Bytes.from(this.state.nextbytes()).fromUtf8());
|
||||||
return Symbol.for(Bytes.from(this.state.nextbytes(this.state.varint())).fromUtf8());
|
}
|
||||||
default:
|
|
||||||
return void 0;
|
_openSequencelike(expectedTag: number): boolean {
|
||||||
|
this.skipAnnotations();
|
||||||
|
if (this.state.nextbyte() !== expectedTag) {
|
||||||
|
this.state._rewind();
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
this.state.inSequence = true;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
openRecord(): boolean {
|
openRecord(): boolean {
|
||||||
this.skipAnnotations();
|
return this._openSequencelike(Tag.Record);
|
||||||
return (this.state.nextbyte() === Tag.Record) || (this.state.index--, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
openSequence(): boolean {
|
openSequence(): boolean {
|
||||||
this.skipAnnotations();
|
return this._openSequencelike(Tag.Sequence);
|
||||||
return (this.state.nextbyte() === Tag.Sequence) || (this.state.index--, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
openSet(): boolean {
|
openSet(): boolean {
|
||||||
this.skipAnnotations();
|
return this._openSequencelike(Tag.Set);
|
||||||
return (this.state.nextbyte() === Tag.Set) || (this.state.index--, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
openDictionary(): boolean {
|
openDictionary(): boolean {
|
||||||
this.skipAnnotations();
|
return this._openSequencelike(Tag.Dictionary);
|
||||||
return (this.state.nextbyte() === Tag.Dictionary) || (this.state.index--, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
closeCompound(): boolean {
|
closeCompound(): boolean {
|
||||||
return this.state.peekend();
|
const r = this.state.atEnd();
|
||||||
|
if (r) this.state.inSequence = false;
|
||||||
|
return r;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { GenericEmbedded } from "./embedded";
|
||||||
import type { Preservable } from "./encoder";
|
import type { Preservable } from "./encoder";
|
||||||
import type { Writer, PreserveWritable } from "./writer";
|
import type { Writer, PreserveWritable } from "./writer";
|
||||||
import { annotations, Annotated } from "./annotated";
|
import { annotations, Annotated } from "./annotated";
|
||||||
|
import * as IO from "./iolist";
|
||||||
|
|
||||||
export type DictionaryType = 'Dictionary' | 'Set';
|
export type DictionaryType = 'Dictionary' | 'Set';
|
||||||
export const DictionaryType = Symbol.for('DictionaryType');
|
export const DictionaryType = Symbol.for('DictionaryType');
|
||||||
|
@ -43,25 +44,20 @@ export class KeyedDictionary<K extends Value<T>, V, T = GenericEmbedded> extends
|
||||||
|
|
||||||
get [Symbol.toStringTag]() { return 'Dictionary'; }
|
get [Symbol.toStringTag]() { return 'Dictionary'; }
|
||||||
|
|
||||||
__preserve_on__(encoder: Encoder<T>) {
|
__preserve_on__(encoder: Encoder<T>): IO.IOList {
|
||||||
if (encoder.canonical) {
|
if (encoder.canonical) {
|
||||||
const entries = Array.from(this);
|
const entries = Array.from(this);
|
||||||
const pieces = entries.map<[Bytes, number]>(([k, _v], i) => [canonicalEncode(k), i]);
|
const pieces = entries.map<[Bytes, number]>(([k, _v], i) => [canonicalEncode(k), i]);
|
||||||
pieces.sort((a, b) => Bytes.compare(a[0], b[0]));
|
pieces.sort((a, b) => Bytes.compare(a[0], b[0]));
|
||||||
encoder.state.emitbyte(Tag.Dictionary);
|
return [Tag.Dictionary, encoder._encodevalues(pieces.flatMap(
|
||||||
pieces.forEach(([_encodedKey, i]) => {
|
([_encodedKey, i]) => entries[i] as Value<T>[]))]; // Suuuuuuuper unsound
|
||||||
const [k, v] = entries[i];
|
|
||||||
encoder.push(k);
|
|
||||||
encoder.push(v as unknown as Value<T>); // Suuuuuuuper unsound
|
|
||||||
});
|
|
||||||
encoder.state.emitbyte(Tag.End);
|
|
||||||
} else {
|
} else {
|
||||||
encoder.state.emitbyte(Tag.Dictionary);
|
const r: IO.IOList = [Tag.Dictionary];
|
||||||
this.forEach((v, k) => {
|
this.forEach((v, k) => {
|
||||||
encoder.push(k);
|
r.push(encoder._encode(k));
|
||||||
encoder.push(v as unknown as Value<T>); // Suuuuuuuper unsound
|
r.push(encoder._encode(v as unknown as Value<T>)); // Suuuuuuuper unsound
|
||||||
});
|
});
|
||||||
encoder.state.emitbyte(Tag.End);
|
return r;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,13 +119,13 @@ export class KeyedSet<K extends Value<T>, T = GenericEmbedded> extends FlexSet<K
|
||||||
|
|
||||||
get [Symbol.toStringTag]() { return 'Set'; }
|
get [Symbol.toStringTag]() { return 'Set'; }
|
||||||
|
|
||||||
__preserve_on__(encoder: Encoder<T>) {
|
__preserve_on__(encoder: Encoder<T>): IO.IOList {
|
||||||
if (encoder.canonical) {
|
if (encoder.canonical) {
|
||||||
const pieces = Array.from(this).map<[Bytes, K]>(k => [canonicalEncode(k), k]);
|
const pieces = Array.from(this).map<[Bytes, K]>(k => [canonicalEncode(k), k]);
|
||||||
pieces.sort((a, b) => Bytes.compare(a[0], b[0]));
|
pieces.sort((a, b) => Bytes.compare(a[0], b[0]));
|
||||||
encoder.encodevalues(Tag.Set, pieces.map(e => e[1]));
|
return [Tag.Set, encoder._encodevalues(pieces.map(e => e[1]))];
|
||||||
} else {
|
} else {
|
||||||
encoder.encodevalues(Tag.Set, this);
|
return [Tag.Set, encoder._encodevalues(this)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import type { DecoderState } from "./decoder";
|
import type { DecoderState } from "./decoder";
|
||||||
import type { EncoderState } from "./encoder";
|
import type { EncoderOptions } from "./encoder";
|
||||||
import type { Value } from "./values";
|
import type { Value } from "./values";
|
||||||
import { ReaderStateOptions } from "./reader";
|
import { ReaderStateOptions } from "./reader";
|
||||||
|
import * as IO from "./iolist";
|
||||||
|
|
||||||
export type EmbeddedTypeEncode<T> = {
|
export type EmbeddedTypeEncode<T> = {
|
||||||
encode(s: EncoderState, v: T): void;
|
encode(s: EncoderOptions, v: T): IO.IOList;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EmbeddedTypeDecode<T> = {
|
export type EmbeddedTypeDecode<T> = {
|
||||||
|
|
|
@ -3,14 +3,15 @@ import { Bytes } from "./bytes";
|
||||||
import { Value } from "./values";
|
import { Value } from "./values";
|
||||||
import { EncodeError } from "./codec";
|
import { EncodeError } from "./codec";
|
||||||
import { Record, Tuple } from "./record";
|
import { Record, Tuple } from "./record";
|
||||||
import { GenericEmbedded, EmbeddedTypeEncode } from "./embedded";
|
import { EmbeddedTypeEncode } from "./embedded";
|
||||||
import type { Embedded } from "./embedded";
|
import type { Embedded } from "./embedded";
|
||||||
|
import * as IO from "./iolist";
|
||||||
|
|
||||||
export type Encodable<T> =
|
export type Encodable<T> =
|
||||||
Value<T> | Preservable<T> | Iterable<Value<T>> | ArrayBufferView;
|
Value<T> | Preservable<T> | Iterable<Value<T>> | ArrayBufferView;
|
||||||
|
|
||||||
export interface Preservable<T> {
|
export interface Preservable<T> {
|
||||||
__preserve_on__(encoder: Encoder<T>): void;
|
__preserve_on__(encoder: Encoder<T>): IO.IOList;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPreservable<T>(v: any): v is Preservable<T> {
|
export function isPreservable<T>(v: any): v is Preservable<T> {
|
||||||
|
@ -46,22 +47,54 @@ export function embeddedId(v: any): number {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const identityEmbeddedTypeEncode: EmbeddedTypeEncode<any> = {
|
export const identityEmbeddedTypeEncode: EmbeddedTypeEncode<any> = {
|
||||||
encode(s: EncoderState, v: any): void {
|
encode(s: EncoderOptions, v: any): IO.IOList {
|
||||||
new Encoder(s, this).push(embeddedId(v));
|
return new Encoder(s, this)._encode(embeddedId(v));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export class EncoderState {
|
export function encodeVarint(v: number): IO.IOList {
|
||||||
chunks: Array<Uint8Array>;
|
function wr(v: number, d: number): IO.IOList {
|
||||||
view: DataView;
|
if (v < 128) {
|
||||||
index: number;
|
return v + d;
|
||||||
options: EncoderOptions;
|
} else {
|
||||||
|
return [wr(Math.floor(v / 128), 0), (v & 127) + d];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wr(v, 128);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(options: EncoderOptions) {
|
export function encodeInt(v: number): IO.IOList {
|
||||||
this.chunks = [];
|
// TODO: Bignums :-/
|
||||||
this.view = new DataView(new ArrayBuffer(256));
|
|
||||||
this.index = 0;
|
if (v === 0) return false;
|
||||||
|
if (v === -1) return 255;
|
||||||
|
|
||||||
|
const plain_bitcount = Math.floor(Math.log2(v > 0 ? v : -(1 + v))) + 1;
|
||||||
|
const signed_bitcount = plain_bitcount + 1;
|
||||||
|
const bytecount = (signed_bitcount + 7) >> 3;
|
||||||
|
|
||||||
|
function enc(n: number, x: number): IO.IOList {
|
||||||
|
return (n > 0) && [enc(n - 1, Math.floor(x / 256)), x & 255];
|
||||||
|
};
|
||||||
|
|
||||||
|
return enc(bytecount, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Encoder<T = object> {
|
||||||
|
options: EncoderOptions;
|
||||||
|
embeddedEncode: EmbeddedTypeEncode<T>;
|
||||||
|
|
||||||
|
constructor(options: EncoderEmbeddedOptions<T>);
|
||||||
|
constructor(options: EncoderOptions, embeddedEncode?: EmbeddedTypeEncode<T>);
|
||||||
|
constructor(options: (EncoderOptions | EncoderEmbeddedOptions<T>) = {},
|
||||||
|
embeddedEncode?: EmbeddedTypeEncode<T>)
|
||||||
|
{
|
||||||
this.options = options;
|
this.options = options;
|
||||||
|
if ('embeddedEncode' in options) {
|
||||||
|
this.embeddedEncode = options.embeddedEncode ?? identityEmbeddedTypeEncode;
|
||||||
|
} else {
|
||||||
|
this.embeddedEncode = embeddedEncode ?? identityEmbeddedTypeEncode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get canonical(): boolean {
|
get canonical(): boolean {
|
||||||
|
@ -72,189 +105,47 @@ export class EncoderState {
|
||||||
return this.options.includeAnnotations ?? !this.canonical;
|
return this.options.includeAnnotations ?? !this.canonical;
|
||||||
}
|
}
|
||||||
|
|
||||||
contents(): Bytes {
|
encode(v: Encodable<T>): Bytes {
|
||||||
if (this.chunks.length === 0) {
|
return IO.ioListBytes(this._encode(v));
|
||||||
const resultLength = this.index;
|
}
|
||||||
this.index = 0;
|
|
||||||
return new Bytes(this.view.buffer.slice(0, resultLength));
|
encodeString(v: Encodable<T>): string {
|
||||||
} else {
|
return asLatin1(this.encode(v)._view);
|
||||||
this.rotatebuffer(4096);
|
}
|
||||||
const chunks = this.chunks;
|
|
||||||
this.chunks = [];
|
_encodevalues(items: Iterable<Value<T>>): IO.IOList {
|
||||||
return Bytes.concat(chunks);
|
const ios: IO.IOList = [];
|
||||||
|
for (let i of items) {
|
||||||
|
const c = IO.countIOList(this._encode(i));
|
||||||
|
ios.push(encodeVarint(c.length));
|
||||||
|
ios.push(c);
|
||||||
}
|
}
|
||||||
|
return ios;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Like contents(), but hands back a string containing binary data "encoded" via latin-1 */
|
_encode(v: Encodable<T>): IO.IOList {
|
||||||
contentsString(): string {
|
if (isPreservable<T>(v)) return v.__preserve_on__(this);
|
||||||
if (this.chunks.length === 0) {
|
if (typeof v === 'boolean') return v ? Tag.True : Tag.False;
|
||||||
const s = asLatin1(new Uint8Array(this.view.buffer, 0, this.index));
|
if (typeof v === 'number') return [Tag.SignedInteger, encodeInt(v)];
|
||||||
this.index = 0;
|
if (typeof v === 'string') return [Tag.String, new Bytes(v)._view, 0];
|
||||||
return s;
|
if (typeof v === 'symbol') {
|
||||||
} else {
|
|
||||||
this.rotatebuffer(4096);
|
|
||||||
const chunks = this.chunks;
|
|
||||||
this.chunks = [];
|
|
||||||
return chunks.map(asLatin1).join('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rotatebuffer(size: number) {
|
|
||||||
this.chunks.push(new Uint8Array(this.view.buffer, 0, this.index));
|
|
||||||
this.view = new DataView(new ArrayBuffer(size));
|
|
||||||
this.index = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
makeroom(amount: number) {
|
|
||||||
if (this.index + amount > this.view.byteLength) {
|
|
||||||
this.rotatebuffer(amount + 4096);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emitbyte(b: number) {
|
|
||||||
this.makeroom(1);
|
|
||||||
this.view.setUint8(this.index++, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
emitbytes(bs: Uint8Array) {
|
|
||||||
this.makeroom(bs.length);
|
|
||||||
(new Uint8Array(this.view.buffer)).set(bs, this.index);
|
|
||||||
this.index += bs.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
varint(v: number) {
|
|
||||||
while (v >= 128) {
|
|
||||||
this.emitbyte((v % 128) + 128);
|
|
||||||
v = Math.floor(v / 128);
|
|
||||||
}
|
|
||||||
this.emitbyte(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
encodeint(v: number) {
|
|
||||||
// TODO: Bignums :-/
|
|
||||||
const plain_bitcount = Math.floor(Math.log2(v > 0 ? v : -(1 + v))) + 1;
|
|
||||||
const signed_bitcount = plain_bitcount + 1;
|
|
||||||
const bytecount = (signed_bitcount + 7) >> 3;
|
|
||||||
if (bytecount <= 16) {
|
|
||||||
this.emitbyte(Tag.MediumInteger_lo + bytecount - 1);
|
|
||||||
} else {
|
|
||||||
this.emitbyte(Tag.SignedInteger);
|
|
||||||
this.varint(bytecount);
|
|
||||||
}
|
|
||||||
const enc = (n: number, x: number) => {
|
|
||||||
if (n > 0) {
|
|
||||||
enc(n - 1, Math.floor(x / 256));
|
|
||||||
this.emitbyte(x & 255);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
enc(bytecount, v);
|
|
||||||
}
|
|
||||||
|
|
||||||
encodebytes(tag: Tag, bs: Uint8Array) {
|
|
||||||
this.emitbyte(tag);
|
|
||||||
this.varint(bs.length);
|
|
||||||
this.emitbytes(bs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Encoder<T = object> {
|
|
||||||
state: EncoderState;
|
|
||||||
embeddedEncode: EmbeddedTypeEncode<T>;
|
|
||||||
|
|
||||||
constructor(options: EncoderEmbeddedOptions<T>);
|
|
||||||
constructor(state: EncoderState, embeddedEncode?: EmbeddedTypeEncode<T>);
|
|
||||||
constructor(
|
|
||||||
state_or_options: (EncoderState | EncoderEmbeddedOptions<T>) = {},
|
|
||||||
embeddedEncode?: EmbeddedTypeEncode<T>)
|
|
||||||
{
|
|
||||||
if (state_or_options instanceof EncoderState) {
|
|
||||||
this.state = state_or_options;
|
|
||||||
this.embeddedEncode = embeddedEncode ?? identityEmbeddedTypeEncode;
|
|
||||||
} else {
|
|
||||||
this.state = new EncoderState(state_or_options);
|
|
||||||
this.embeddedEncode = state_or_options.embeddedEncode ?? identityEmbeddedTypeEncode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
withEmbeddedEncode<S>(
|
|
||||||
embeddedEncode: EmbeddedTypeEncode<S>,
|
|
||||||
body: (e: Encoder<S>) => void): this
|
|
||||||
{
|
|
||||||
body(new Encoder(this.state, embeddedEncode));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
get canonical(): boolean {
|
|
||||||
return this.state.canonical;
|
|
||||||
}
|
|
||||||
|
|
||||||
get includeAnnotations(): boolean {
|
|
||||||
return this.state.includeAnnotations;
|
|
||||||
}
|
|
||||||
|
|
||||||
contents(): Bytes {
|
|
||||||
return this.state.contents();
|
|
||||||
}
|
|
||||||
|
|
||||||
contentsString(): string {
|
|
||||||
return this.state.contentsString();
|
|
||||||
}
|
|
||||||
|
|
||||||
encodevalues(tag: Tag, items: Iterable<Value<T>>) {
|
|
||||||
this.state.emitbyte(tag);
|
|
||||||
for (let i of items) { this.push(i); }
|
|
||||||
this.state.emitbyte(Tag.End);
|
|
||||||
}
|
|
||||||
|
|
||||||
push(v: Encodable<T>) {
|
|
||||||
if (isPreservable<unknown>(v)) {
|
|
||||||
v.__preserve_on__(this);
|
|
||||||
}
|
|
||||||
else if (isPreservable<T>(v)) {
|
|
||||||
v.__preserve_on__(this);
|
|
||||||
}
|
|
||||||
else if (typeof v === 'boolean') {
|
|
||||||
this.state.emitbyte(v ? Tag.True : Tag.False);
|
|
||||||
}
|
|
||||||
else if (typeof v === 'number') {
|
|
||||||
if (v >= -3 && v <= 12) {
|
|
||||||
this.state.emitbyte(Tag.SmallInteger_lo + ((v + 16) & 0xf));
|
|
||||||
} else {
|
|
||||||
this.state.encodeint(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (typeof v === 'string') {
|
|
||||||
this.state.encodebytes(Tag.String, new Bytes(v)._view);
|
|
||||||
}
|
|
||||||
else if (typeof v === 'symbol') {
|
|
||||||
const key = Symbol.keyFor(v);
|
const key = Symbol.keyFor(v);
|
||||||
if (key === void 0) throw new EncodeError("Cannot preserve non-global Symbol", v);
|
if (key === void 0) throw new EncodeError("Cannot preserve non-global Symbol", v);
|
||||||
this.state.encodebytes(Tag.Symbol, new Bytes(key)._view);
|
return [Tag.Symbol, new Bytes(key)._view];
|
||||||
}
|
}
|
||||||
else if (ArrayBuffer.isView(v)) {
|
if (ArrayBuffer.isView(v)) {
|
||||||
if (v instanceof Uint8Array) {
|
if (v instanceof Uint8Array) {
|
||||||
this.state.encodebytes(Tag.ByteString, v);
|
return [Tag.ByteString, v];
|
||||||
} else {
|
} else {
|
||||||
const bs = new Uint8Array(v.buffer, v.byteOffset, v.byteLength);
|
return [Tag.ByteString, new Uint8Array(v.buffer, v.byteOffset, v.byteLength)];
|
||||||
this.state.encodebytes(Tag.ByteString, bs);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (Record.isRecord<Value<T>, Tuple<Value<T>>, T>(v)) {
|
if (Record.isRecord<Value<T>, Tuple<Value<T>>, T>(v)) {
|
||||||
this.state.emitbyte(Tag.Record);
|
return [Tag.Record, this._encodevalues([v.label, ... v])];
|
||||||
this.push(v.label);
|
|
||||||
for (let i of v) { this.push(i); }
|
|
||||||
this.state.emitbyte(Tag.End);
|
|
||||||
}
|
}
|
||||||
else if (isIterable<Value<T>>(v)) {
|
if (isIterable<Value<T>>(v)) return [Tag.Sequence, this._encodevalues(v)];
|
||||||
this.encodevalues(Tag.Sequence, v);
|
return ((v: Embedded<T>) =>
|
||||||
}
|
[Tag.Embedded, this.embeddedEncode.encode(this.options, v.embeddedValue)])(v);
|
||||||
else {
|
|
||||||
((v: Embedded<T>) => {
|
|
||||||
this.state.emitbyte(Tag.Embedded);
|
|
||||||
this.embeddedEncode.encode(this.state, v.embeddedValue);
|
|
||||||
})(v);
|
|
||||||
}
|
|
||||||
return this; // for chaining
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -262,34 +153,23 @@ export function encode<T>(
|
||||||
v: Encodable<T>,
|
v: Encodable<T>,
|
||||||
options: EncoderEmbeddedOptions<T> = {}): Bytes
|
options: EncoderEmbeddedOptions<T> = {}): Bytes
|
||||||
{
|
{
|
||||||
return new Encoder(options).push(v).contents();
|
return new Encoder(options).encode(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
const _canonicalEncoder = new Encoder({ canonical: true });
|
const _canonicalEncoder = new Encoder({ canonical: true });
|
||||||
let _usingCanonicalEncoder = false;
|
|
||||||
|
|
||||||
export function canonicalEncode(v: Encodable<never>, options?: EncoderEmbeddedOptions<never>): Bytes;
|
export function canonicalEncode(v: Encodable<never>, options?: EncoderEmbeddedOptions<never>): Bytes;
|
||||||
export function canonicalEncode(v: Encodable<any>, options?: EncoderEmbeddedOptions<any>): Bytes;
|
export function canonicalEncode(v: Encodable<any>, options?: EncoderEmbeddedOptions<any>): Bytes;
|
||||||
export function canonicalEncode(v: any, options?: EncoderEmbeddedOptions<any>): Bytes {
|
export function canonicalEncode(v: any, options?: EncoderEmbeddedOptions<any>): Bytes {
|
||||||
if (options === void 0 && !_usingCanonicalEncoder) {
|
if (options === void 0) {
|
||||||
_usingCanonicalEncoder = true;
|
return _canonicalEncoder.encode(v);
|
||||||
const bs = _canonicalEncoder.push(v).contents();
|
|
||||||
_usingCanonicalEncoder = false;
|
|
||||||
return bs;
|
|
||||||
} else {
|
} else {
|
||||||
return encode(v, { ... options, canonical: true });
|
return encode(v, { ... options, canonical: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canonicalString(v: Encodable<any>): string {
|
export function canonicalString(v: Encodable<any>): string {
|
||||||
if (!_usingCanonicalEncoder) {
|
return _canonicalEncoder.encodeString(v);
|
||||||
_usingCanonicalEncoder = true;
|
|
||||||
const s = _canonicalEncoder.push(v).contentsString();
|
|
||||||
_usingCanonicalEncoder = false;
|
|
||||||
return s;
|
|
||||||
} else {
|
|
||||||
return new Encoder({ canonical: true }).push(v).contentsString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function encodeWithAnnotations<T>(v: Encodable<T>,
|
export function encodeWithAnnotations<T>(v: Encodable<T>,
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { Value } from "./values";
|
||||||
import type { GenericEmbedded } from "./embedded";
|
import type { GenericEmbedded } from "./embedded";
|
||||||
import type { Encoder, Preservable } from "./encoder";
|
import type { Encoder, Preservable } from "./encoder";
|
||||||
import type { Writer, PreserveWritable } from "./writer";
|
import type { Writer, PreserveWritable } from "./writer";
|
||||||
|
import * as IO from "./iolist";
|
||||||
|
|
||||||
export type FloatType = 'Single' | 'Double';
|
export type FloatType = 'Single' | 'Double';
|
||||||
export const FloatType = Symbol.for('FloatType');
|
export const FloatType = Symbol.for('FloatType');
|
||||||
|
@ -53,11 +54,10 @@ export class SingleFloat extends Float implements Preservable<any>, PreserveWrit
|
||||||
return Float.isSingle(v) ? v : void 0;
|
return Float.isSingle(v) ? v : void 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
__preserve_on__(encoder: Encoder<any>) {
|
__preserve_on__<T>(_encoder: Encoder<T>): IO.IOList {
|
||||||
encoder.state.emitbyte(Tag.Float);
|
const bs = new Uint8Array(4);
|
||||||
encoder.state.makeroom(4);
|
new DataView(bs.buffer).setFloat32(0, this.value, false);
|
||||||
encoder.state.view.setFloat32(encoder.state.index, this.value, false);
|
return [Tag.Float, bs];
|
||||||
encoder.state.index += 4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
__preserve_text_on__(w: Writer<any>) {
|
__preserve_text_on__(w: Writer<any>) {
|
||||||
|
@ -82,11 +82,10 @@ export class DoubleFloat extends Float implements Preservable<any>, PreserveWrit
|
||||||
return Float.isDouble(v) ? v : void 0;
|
return Float.isDouble(v) ? v : void 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
__preserve_on__(encoder: Encoder<any>) {
|
__preserve_on__<T>(_encoder: Encoder<T>): IO.IOList {
|
||||||
encoder.state.emitbyte(Tag.Double);
|
const bs = new Uint8Array(8);
|
||||||
encoder.state.makeroom(8);
|
new DataView(bs.buffer).setFloat64(0, this.value, false);
|
||||||
encoder.state.view.setFloat64(encoder.state.index, this.value, false);
|
return [Tag.Float, bs];
|
||||||
encoder.state.index += 8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
__preserve_text_on__(w: Writer<any>) {
|
__preserve_text_on__(w: Writer<any>) {
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { Bytes, BytesLike, underlying } from "./bytes";
|
||||||
|
|
||||||
|
export type IOList = number | BytesLike | IOList[] | false | CountedIOList;
|
||||||
|
|
||||||
|
class CountedIOList {
|
||||||
|
value: IOList;
|
||||||
|
length: number;
|
||||||
|
|
||||||
|
constructor(i: IOList) {
|
||||||
|
this.value = i;
|
||||||
|
this.length = iolistLength(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pushByte(i: IOList, b: number): IOList {
|
||||||
|
if (Array.isArray(i)) {
|
||||||
|
i.push(b);
|
||||||
|
return i;
|
||||||
|
} else {
|
||||||
|
return [i, b];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function append(i: IOList, j: IOList): IOList {
|
||||||
|
if (i === false) return j;
|
||||||
|
if (j === false) return i;
|
||||||
|
return [i, j];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function iolistLength(i: IOList, acc = 0): number {
|
||||||
|
if (typeof(i) === 'number') return acc + 1;
|
||||||
|
if (i === false) return acc;
|
||||||
|
if (Array.isArray(i)) return i.reduce<number>((acc, j) => iolistLength(j, acc), acc);
|
||||||
|
if (i instanceof CountedIOList) return acc + i.length;
|
||||||
|
return acc + i.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function countIOList(i: IOList): CountedIOList {
|
||||||
|
if (i instanceof CountedIOList) return i;
|
||||||
|
return new CountedIOList(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ioListBytes(i: IOList): Bytes {
|
||||||
|
if (i instanceof Bytes) return i;
|
||||||
|
const buffer = new Bytes(iolistLength(i));
|
||||||
|
|
||||||
|
function fill(i: IOList, offset: number): number {
|
||||||
|
while (i instanceof CountedIOList) i = i.value;
|
||||||
|
if (typeof(i) === 'number') {
|
||||||
|
buffer._view[offset] = i;
|
||||||
|
return offset + 1;
|
||||||
|
}
|
||||||
|
if (i === false) {
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
if (Array.isArray(i)) {
|
||||||
|
i.forEach(j => offset = fill(j, offset));
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
const bs = underlying(i);
|
||||||
|
buffer._view.set(bs, offset);
|
||||||
|
return offset + bs.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
fill(i, 0);
|
||||||
|
return buffer;
|
||||||
|
}
|
|
@ -19,3 +19,5 @@ export * from './strip';
|
||||||
export * from './text';
|
export * from './text';
|
||||||
export * from './values';
|
export * from './values';
|
||||||
export * from './writer';
|
export * from './writer';
|
||||||
|
|
||||||
|
export * as IO from './iolist';
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { isAnnotated } from './is';
|
import { isAnnotated } from './is';
|
||||||
import { Record, Tuple } from "./record";
|
import { Record, Tuple } from "./record";
|
||||||
import type { GenericEmbedded, Embedded, EmbeddedTypeEncode } from "./embedded";
|
import type { GenericEmbedded, Embedded, EmbeddedTypeEncode } from "./embedded";
|
||||||
import { Encoder, EncoderState } from "./encoder";
|
import { Encoder, EncoderOptions } from "./encoder";
|
||||||
import type { Value } from "./values";
|
import type { Value } from "./values";
|
||||||
|
import * as IO from "./iolist";
|
||||||
|
|
||||||
export type Writable<T> =
|
export type Writable<T> =
|
||||||
Value<T> | PreserveWritable<T> | Iterable<Value<T>> | ArrayBufferView;
|
Value<T> | PreserveWritable<T> | Iterable<Value<T>> | ArrayBufferView;
|
||||||
|
@ -23,8 +24,8 @@ export type EmbeddedWriter<T> =
|
||||||
{ write(s: WriterState, v: T): void } | { toValue(v: T): Value<GenericEmbedded> };
|
{ write(s: WriterState, v: T): void } | { toValue(v: T): Value<GenericEmbedded> };
|
||||||
|
|
||||||
export const genericEmbeddedTypeEncode: EmbeddedTypeEncode<GenericEmbedded> & EmbeddedWriter<GenericEmbedded> = {
|
export const genericEmbeddedTypeEncode: EmbeddedTypeEncode<GenericEmbedded> & EmbeddedWriter<GenericEmbedded> = {
|
||||||
encode(s: EncoderState, v: GenericEmbedded): void {
|
encode(s: EncoderOptions, v: GenericEmbedded): IO.IOList {
|
||||||
new Encoder(s, this).push(v.generic);
|
return new Encoder(s, this).encode(v.generic);
|
||||||
},
|
},
|
||||||
|
|
||||||
toValue(v: GenericEmbedded): Value<GenericEmbedded> {
|
toValue(v: GenericEmbedded): Value<GenericEmbedded> {
|
||||||
|
@ -33,7 +34,7 @@ export const genericEmbeddedTypeEncode: EmbeddedTypeEncode<GenericEmbedded> & Em
|
||||||
};
|
};
|
||||||
|
|
||||||
export const neverEmbeddedTypeEncode: EmbeddedTypeEncode<never> & EmbeddedWriter<never> = {
|
export const neverEmbeddedTypeEncode: EmbeddedTypeEncode<never> & EmbeddedWriter<never> = {
|
||||||
encode(_s: EncoderState, _v: never): void {
|
encode(_s: EncoderOptions, _v: never): IO.IOList {
|
||||||
throw new Error("Embeddeds not permitted encoding Preserves document");
|
throw new Error("Embeddeds not permitted encoding Preserves document");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
Constants,
|
Constants,
|
||||||
Encoder,
|
Encoder,
|
||||||
GenericEmbedded,
|
GenericEmbedded,
|
||||||
EncoderState,
|
EncoderOptions,
|
||||||
EmbeddedType,
|
EmbeddedType,
|
||||||
DecoderState,
|
DecoderState,
|
||||||
Decoder,
|
Decoder,
|
||||||
|
@ -19,6 +19,7 @@ import {
|
||||||
embed,
|
embed,
|
||||||
genericEmbeddedTypeDecode,
|
genericEmbeddedTypeDecode,
|
||||||
genericEmbeddedTypeEncode,
|
genericEmbeddedTypeEncode,
|
||||||
|
IO,
|
||||||
} from '../src/index';
|
} from '../src/index';
|
||||||
const { Tag } = Constants;
|
const { Tag } = Constants;
|
||||||
import './test-utils';
|
import './test-utils';
|
||||||
|
@ -68,7 +69,7 @@ describe('records', () => {
|
||||||
|
|
||||||
describe('parsing from subarray', () => {
|
describe('parsing from subarray', () => {
|
||||||
it('should maintain alignment of nextbytes', () => {
|
it('should maintain alignment of nextbytes', () => {
|
||||||
const u = Uint8Array.of(1, 1, 1, 1, 0xb1, 0x03, 0x33, 0x33, 0x33);
|
const u = Uint8Array.of(1, 1, 1, 1, 0xa4, 0x33, 0x33, 0x33, 0x00);
|
||||||
const bs = Bytes.from(u.subarray(4));
|
const bs = Bytes.from(u.subarray(4));
|
||||||
expect(decode(bs)).is("333");
|
expect(decode(bs)).is("333");
|
||||||
});
|
});
|
||||||
|
@ -77,13 +78,11 @@ describe('parsing from subarray', () => {
|
||||||
describe('reusing buffer space', () => {
|
describe('reusing buffer space', () => {
|
||||||
it('should be done safely, even with nested dictionaries', () => {
|
it('should be done safely, even with nested dictionaries', () => {
|
||||||
expect(canonicalEncode(fromJS(['aaa', Dictionary.fromJS({a: 1}), 'zzz'])).toHex()).is(
|
expect(canonicalEncode(fromJS(['aaa', Dictionary.fromJS({a: 1}), 'zzz'])).toHex()).is(
|
||||||
`b5
|
`a8
|
||||||
b103616161
|
85a461616100
|
||||||
b7
|
88aa
|
||||||
b10161 91
|
83a46100 82a301
|
||||||
84
|
85a47a7a7a00`.replace(/\s+/g, ''));
|
||||||
b1037a7a7a
|
|
||||||
84`.replace(/\s+/g, ''));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -99,8 +98,8 @@ describe('encoding and decoding embeddeds', () => {
|
||||||
return this.fromValue(new Decoder<GenericEmbedded>(d).next());
|
return this.fromValue(new Decoder<GenericEmbedded>(d).next());
|
||||||
}
|
}
|
||||||
|
|
||||||
encode(e: EncoderState, v: object): void {
|
encode(e: EncoderOptions, v: object): IO.IOList {
|
||||||
new Encoder(e).push(this.toValue(v));
|
return new Encoder(e)._encode(this.toValue(v));
|
||||||
}
|
}
|
||||||
|
|
||||||
equals(a: object, b: object): boolean {
|
equals(a: object, b: object): boolean {
|
||||||
|
@ -137,7 +136,7 @@ describe('encoding and decoding embeddeds', () => {
|
||||||
expect(bs1).is(bs3);
|
expect(bs1).is(bs3);
|
||||||
});
|
});
|
||||||
it('should refuse to decode embeddeds when no function has been supplied', () => {
|
it('should refuse to decode embeddeds when no function has been supplied', () => {
|
||||||
expect(() => decode(Bytes.from([Tag.Embedded, Tag.SmallInteger_lo])))
|
expect(() => decode(Bytes.from([Tag.Embedded, Tag.SignedInteger])))
|
||||||
.toThrow("Embeddeds not permitted at this point in Preserves document");
|
.toThrow("Embeddeds not permitted at this point in Preserves document");
|
||||||
});
|
});
|
||||||
it('should encode properly', () => {
|
it('should encode properly', () => {
|
||||||
|
@ -147,9 +146,8 @@ describe('encoding and decoding embeddeds', () => {
|
||||||
const B = embed({b: 2});
|
const B = embed({b: 2});
|
||||||
expect(encode([A, B], { embeddedEncode: pt })).is(
|
expect(encode([A, B], { embeddedEncode: pt })).is(
|
||||||
Bytes.from([Tag.Sequence,
|
Bytes.from([Tag.Sequence,
|
||||||
Tag.Embedded, Tag.SmallInteger_lo,
|
0x82, Tag.Embedded, Tag.SignedInteger,
|
||||||
Tag.Embedded, Tag.SmallInteger_lo + 1,
|
0x83, Tag.Embedded, Tag.SignedInteger, 1]));
|
||||||
Tag.End]));
|
|
||||||
expect(objects).toEqual([A.embeddedValue, B.embeddedValue]);
|
expect(objects).toEqual([A.embeddedValue, B.embeddedValue]);
|
||||||
});
|
});
|
||||||
it('should decode properly', () => {
|
it('should decode properly', () => {
|
||||||
|
@ -161,9 +159,8 @@ describe('encoding and decoding embeddeds', () => {
|
||||||
objects.push(Y.embeddedValue);
|
objects.push(Y.embeddedValue);
|
||||||
expect(decode(Bytes.from([
|
expect(decode(Bytes.from([
|
||||||
Tag.Sequence,
|
Tag.Sequence,
|
||||||
Tag.Embedded, Tag.SmallInteger_lo,
|
0x82, Tag.Embedded, Tag.SignedInteger,
|
||||||
Tag.Embedded, Tag.SmallInteger_lo + 1,
|
0x83, Tag.Embedded, Tag.SignedInteger, 1,
|
||||||
Tag.End
|
|
||||||
]), { embeddedDecode: pt })).is([X, Y]);
|
]), { embeddedDecode: pt })).is([X, Y]);
|
||||||
});
|
});
|
||||||
it('should store embeddeds embedded in map keys correctly', () => {
|
it('should store embeddeds embedded in map keys correctly', () => {
|
||||||
|
@ -315,8 +312,14 @@ describe('common test suite', () => {
|
||||||
case Symbol.for('DecodeShort'):
|
case Symbol.for('DecodeShort'):
|
||||||
describe(tName, () => {
|
describe(tName, () => {
|
||||||
it('should fail with ShortPacket', () => {
|
it('should fail with ShortPacket', () => {
|
||||||
expect(() => D(strip(t[0]) as Bytes))
|
expect(() => {
|
||||||
.toThrowFilter(e => ShortPacket.isShortPacket(e));
|
const d = new Decoder({
|
||||||
|
includeAnnotations: true,
|
||||||
|
embeddedDecode: genericEmbeddedTypeDecode,
|
||||||
|
});
|
||||||
|
d.write(strip(t[0]) as Bytes);
|
||||||
|
return d.next();
|
||||||
|
}).toThrowFilter(e => ShortPacket.isShortPacket(e));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
Loading…
Reference in New Issue