Blue TypeScript implementation

This commit is contained in:
Tony Garnock-Jones 2022-06-16 10:14:05 +02:00
parent 6d496b5113
commit b5458dbf8c
12 changed files with 430 additions and 385 deletions

View File

@ -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 {

View File

@ -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>) {

View File

@ -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,
} }

View File

@ -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;
} }
} }

View File

@ -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)];
} }
} }

View File

@ -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> = {

View File

@ -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>,

View File

@ -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>) {

View File

@ -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;
}

View File

@ -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';

View File

@ -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");
}, },

View File

@ -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;