Repair remaining cyclic dependency
This commit is contained in:
parent
6d2120989b
commit
eaff7b86d8
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "preserves",
|
||||
"version": "0.6.1",
|
||||
"version": "0.6.2",
|
||||
"description": "Experimental data serialization format",
|
||||
"homepage": "https://gitlab.com/preserves/preserves",
|
||||
"license": "Apache-2.0",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Encoder } from "./codec";
|
||||
import { Encoder } from "./encoder";
|
||||
import { Tag } from "./constants";
|
||||
import { AsPreserve, PreserveOn } from "./symbols";
|
||||
import { DefaultPointer, Value } from "./values";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Tag } from './constants';
|
||||
import { AsPreserve, PreserveOn } from './symbols';
|
||||
import { Encoder, Preservable } from './codec';
|
||||
import { Encoder, Preservable } from './encoder';
|
||||
import { DefaultPointer, Value } from './values';
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
|
|
|
@ -1,28 +1,8 @@
|
|||
// Preserves Binary codec.
|
||||
|
||||
import { Value } from './values';
|
||||
import { Tag } from './constants';
|
||||
import { PreserveOn } from './symbols';
|
||||
import { Bytes, BytesLike, underlying } from './bytes';
|
||||
import { Annotated } from './annotated';
|
||||
import { Set, Dictionary } from './dictionary';
|
||||
import { DoubleFloat, SingleFloat } from './float';
|
||||
import { Record, Tuple } from './record';
|
||||
|
||||
export type ErrorType = 'DecodeError' | 'EncodeError' | 'ShortPacket';
|
||||
export const ErrorType = Symbol.for('ErrorType');
|
||||
|
||||
export type Encodable<T extends object> =
|
||||
Value<T> | Preservable<T> | Iterable<Value<T>> | ArrayBufferView;
|
||||
|
||||
export interface Preservable<T extends object> {
|
||||
[PreserveOn](encoder: Encoder<T>): void;
|
||||
}
|
||||
|
||||
export function isPreservable<T extends object>(v: any): v is Preservable<T> {
|
||||
return typeof v === 'object' && v !== null && typeof v[PreserveOn] === 'function';
|
||||
}
|
||||
|
||||
export abstract class PreservesCodecError {
|
||||
abstract get [ErrorType](): ErrorType;
|
||||
|
||||
|
@ -61,375 +41,3 @@ export class ShortPacket extends DecodeError {
|
|||
return PreservesCodecError.isCodecError(e, 'ShortPacket');
|
||||
}
|
||||
}
|
||||
|
||||
export interface DecoderOptions<T extends object> {
|
||||
includeAnnotations?: boolean;
|
||||
decodePointer?: (v: Value<T>) => T;
|
||||
}
|
||||
|
||||
export class Decoder<T extends object> {
|
||||
packet: Uint8Array;
|
||||
index: number;
|
||||
options: DecoderOptions<T>;
|
||||
|
||||
constructor(packet: BytesLike = new Uint8Array(0), options: DecoderOptions<T> = {}) {
|
||||
this.packet = underlying(packet);
|
||||
this.index = 0;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
get includeAnnotations(): boolean {
|
||||
return this.options.includeAnnotations ?? false;
|
||||
}
|
||||
|
||||
write(data: BytesLike) {
|
||||
this.packet = Bytes.concat([this.packet.slice(this.index), data])._view;
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
nextbyte(): number {
|
||||
if (this.index >= this.packet.length) throw new ShortPacket("Short packet");
|
||||
// ^ NOTE: greater-than-or-equal-to, not greater-than.
|
||||
return this.packet[this.index++];
|
||||
}
|
||||
|
||||
nextbytes(n: number): DataView {
|
||||
const start = this.index;
|
||||
this.index += n;
|
||||
if (this.index > this.packet.length) throw new ShortPacket("Short packet");
|
||||
// ^ NOTE: greater-than, not greater-than-or-equal-to.
|
||||
return new DataView(this.packet.buffer, this.packet.byteOffset + start, n);
|
||||
}
|
||||
|
||||
varint(): number {
|
||||
// TODO: Bignums :-/
|
||||
const v = this.nextbyte();
|
||||
if (v < 128) return v;
|
||||
return (this.varint() << 7) + (v - 128);
|
||||
}
|
||||
|
||||
peekend(): boolean {
|
||||
const matched = this.nextbyte() === Tag.End;
|
||||
if (!matched) this.index--;
|
||||
return matched;
|
||||
}
|
||||
|
||||
nextvalues(): Value<T>[] {
|
||||
const result = [];
|
||||
while (!this.peekend()) result.push(this.next());
|
||||
return result;
|
||||
}
|
||||
|
||||
nextint(n: number): number {
|
||||
// TODO: Bignums :-/
|
||||
if (n === 0) return 0;
|
||||
let acc = this.nextbyte();
|
||||
if (acc & 0x80) acc -= 256;
|
||||
for (let i = 1; i < n; i++) acc = (acc * 256) + this.nextbyte();
|
||||
return acc;
|
||||
}
|
||||
|
||||
wrap(v: Value<T>): Value<T> {
|
||||
return this.includeAnnotations ? new Annotated(v) : v;
|
||||
}
|
||||
|
||||
static dictionaryFromArray<T extends object>(vs: Value<T>[]): Dictionary<Value<T>, T> {
|
||||
const d = new Dictionary<Value<T>, T>();
|
||||
if (vs.length % 2) throw new DecodeError("Missing dictionary value");
|
||||
for (let i = 0; i < vs.length; i += 2) {
|
||||
d.set(vs[i], vs[i+1]);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
unshiftAnnotation(a: Value<T>, v: Annotated<T>) {
|
||||
if (this.includeAnnotations) {
|
||||
v.annotations.unshift(a);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
next(): Value<T> {
|
||||
const tag = this.nextbyte();
|
||||
switch (tag) {
|
||||
case Tag.False: return this.wrap(false);
|
||||
case Tag.True: return this.wrap(true);
|
||||
case Tag.Float: return this.wrap(new SingleFloat(this.nextbytes(4).getFloat32(0, false)));
|
||||
case Tag.Double: return this.wrap(new DoubleFloat(this.nextbytes(8).getFloat64(0, false)));
|
||||
case Tag.End: throw new DecodeError("Unexpected Compound end marker");
|
||||
case Tag.Annotation: {
|
||||
const a = this.next();
|
||||
const v = this.next() as Annotated<T>;
|
||||
return this.unshiftAnnotation(a, v);
|
||||
}
|
||||
case Tag.Pointer: {
|
||||
const d = this.options.decodePointer;
|
||||
if (d === void 0) {
|
||||
throw new DecodeError("No decodePointer function supplied");
|
||||
}
|
||||
return this.wrap(d(this.next()));
|
||||
}
|
||||
case Tag.SignedInteger: return this.wrap(this.nextint(this.varint()));
|
||||
case Tag.String: return this.wrap(Bytes.from(this.nextbytes(this.varint())).fromUtf8());
|
||||
case Tag.ByteString: return this.wrap(Bytes.from(this.nextbytes(this.varint())));
|
||||
case Tag.Symbol: return this.wrap(Symbol.for(Bytes.from(this.nextbytes(this.varint())).fromUtf8()));
|
||||
case Tag.Record: {
|
||||
const vs = this.nextvalues();
|
||||
if (vs.length === 0) throw new DecodeError("Too few elements in encoded record");
|
||||
return this.wrap(Record(vs[0], vs.slice(1)));
|
||||
}
|
||||
case Tag.Sequence: return this.wrap(this.nextvalues());
|
||||
case Tag.Set: return this.wrap(new Set(this.nextvalues()));
|
||||
case Tag.Dictionary: return this.wrap(Decoder.dictionaryFromArray(this.nextvalues()));
|
||||
default: {
|
||||
if (tag >= Tag.SmallInteger_lo && tag <= Tag.SmallInteger_lo + 15) {
|
||||
const v = tag - Tag.SmallInteger_lo;
|
||||
return this.wrap(v > 12 ? v - 16 : v);
|
||||
}
|
||||
if (tag >= Tag.MediumInteger_lo && tag <= Tag.MediumInteger_lo + 15) {
|
||||
const n = tag - Tag.MediumInteger_lo;
|
||||
return this.wrap(this.nextint(n + 1));
|
||||
}
|
||||
throw new DecodeError("Unsupported Preserves tag: " + tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try_next(): Value<T> | undefined {
|
||||
const start = this.index;
|
||||
try {
|
||||
return this.next();
|
||||
} catch (e) {
|
||||
if (ShortPacket.isShortPacket(e)) {
|
||||
this.index = start;
|
||||
return void 0;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function decode<T extends object>(bs: BytesLike, options?: DecoderOptions<T>) {
|
||||
return new Decoder(bs, options).next();
|
||||
}
|
||||
|
||||
export function decodeWithAnnotations<T extends object>(bs: BytesLike, options: DecoderOptions<T> = {}): Annotated<T> {
|
||||
return decode(bs, { ... options, includeAnnotations: true }) as Annotated<T>;
|
||||
}
|
||||
|
||||
export interface EncoderOptions<T extends object> {
|
||||
canonical?: boolean;
|
||||
includeAnnotations?: boolean;
|
||||
encodePointer?: (v: T) => Value<T>;
|
||||
}
|
||||
|
||||
function chunkStr(bs: Uint8Array): string {
|
||||
return String.fromCharCode.apply(null, bs as any as number[]);
|
||||
}
|
||||
|
||||
function isIterable<T>(v: any): v is Iterable<T> {
|
||||
return typeof v === 'object' && v !== null && typeof v[Symbol.iterator] === 'function';
|
||||
}
|
||||
|
||||
export class Encoder<T extends object> {
|
||||
chunks: Array<Uint8Array>;
|
||||
view: DataView;
|
||||
index: number;
|
||||
options: EncoderOptions<T>;
|
||||
|
||||
constructor(options: EncoderOptions<T> = {}) {
|
||||
this.chunks = [];
|
||||
this.view = new DataView(new ArrayBuffer(256));
|
||||
this.index = 0;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
get canonical(): boolean {
|
||||
return this.options.canonical ?? true;
|
||||
}
|
||||
|
||||
get includeAnnotations(): boolean {
|
||||
return this.options.includeAnnotations ?? !this.canonical;
|
||||
}
|
||||
|
||||
contents(): Bytes {
|
||||
if (this.chunks.length === 0) {
|
||||
const resultLength = this.index;
|
||||
this.index = 0;
|
||||
return new Bytes(this.view.buffer.slice(0, resultLength));
|
||||
} else {
|
||||
this.rotatebuffer(4096);
|
||||
return Bytes.concat(this.chunks);
|
||||
}
|
||||
}
|
||||
|
||||
/* Like contents(), but hands back a string containing binary data "encoded" via latin-1 */
|
||||
contentsString(): string {
|
||||
if (this.chunks.length === 0) {
|
||||
const s = chunkStr(new Uint8Array(this.view.buffer, 0, this.index));
|
||||
this.index = 0;
|
||||
return s;
|
||||
} else {
|
||||
this.rotatebuffer(4096);
|
||||
return this.chunks.map(chunkStr).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);
|
||||
}
|
||||
|
||||
encodevalues(tag: Tag, items: Iterable<Value<T>>) {
|
||||
this.emitbyte(tag);
|
||||
for (let i of items) { this.push(i); }
|
||||
this.emitbyte(Tag.End);
|
||||
}
|
||||
|
||||
encoderawvalues(tag: Tag, items: BytesLike[]) {
|
||||
this.emitbyte(tag);
|
||||
items.forEach((i) => this.emitbytes(underlying(i)));
|
||||
this.emitbyte(Tag.End);
|
||||
}
|
||||
|
||||
push(v: Encodable<T>) {
|
||||
if (isPreservable<never>(v)) {
|
||||
v[PreserveOn](this as unknown as Encoder<never>);
|
||||
}
|
||||
else if (isPreservable<T>(v)) {
|
||||
v[PreserveOn](this);
|
||||
}
|
||||
else if (typeof v === 'boolean') {
|
||||
this.emitbyte(v ? Tag.True : Tag.False);
|
||||
}
|
||||
else if (typeof v === 'number') {
|
||||
if (v >= -3 && v <= 12) {
|
||||
this.emitbyte(Tag.SmallInteger_lo + ((v + 16) & 0xf));
|
||||
} else {
|
||||
this.encodeint(v);
|
||||
}
|
||||
}
|
||||
else if (typeof v === 'string') {
|
||||
this.encodebytes(Tag.String, new Bytes(v)._view);
|
||||
}
|
||||
else if (typeof v === 'symbol') {
|
||||
const key = Symbol.keyFor(v);
|
||||
if (key === void 0) throw new EncodeError("Cannot preserve non-global Symbol", v);
|
||||
this.encodebytes(Tag.Symbol, new Bytes(key)._view);
|
||||
}
|
||||
else if (ArrayBuffer.isView(v)) {
|
||||
if (v instanceof Uint8Array) {
|
||||
this.encodebytes(Tag.ByteString, v);
|
||||
} else {
|
||||
const bs = new Uint8Array(v.buffer, v.byteOffset, v.byteLength);
|
||||
this.encodebytes(Tag.ByteString, bs);
|
||||
}
|
||||
}
|
||||
else if (Record.isRecord<Value<T>, Tuple<Value<T>>, T>(v)) {
|
||||
this.emitbyte(Tag.Record);
|
||||
this.push(v.label);
|
||||
for (let i of v) { this.push(i); }
|
||||
this.emitbyte(Tag.End);
|
||||
}
|
||||
else if (Array.isArray(v)) {
|
||||
this.encodevalues(Tag.Sequence, v);
|
||||
}
|
||||
else if (isIterable<Value<T>>(v)) {
|
||||
this.encodevalues(Tag.Sequence, v as Iterable<Value<T>>);
|
||||
}
|
||||
else {
|
||||
const e = this.options.encodePointer ?? pointerId;
|
||||
this.emitbyte(Tag.Pointer);
|
||||
this.push(e(v));
|
||||
}
|
||||
return this; // for chaining
|
||||
}
|
||||
}
|
||||
|
||||
export function encode<T extends object>(v: Encodable<T>, options?: EncoderOptions<T>): Bytes {
|
||||
return new Encoder(options).push(v).contents();
|
||||
}
|
||||
|
||||
let _nextId = 0;
|
||||
const _registry = new WeakMap<object, number>();
|
||||
export function pointerId(v: object): number {
|
||||
let id = _registry.get(v);
|
||||
if (id === void 0) {
|
||||
id = _nextId++;
|
||||
_registry.set(v, id);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
const _canonicalEncoder = new Encoder({ canonical: true });
|
||||
let _usingCanonicalEncoder = false;
|
||||
export function canonicalEncode(v: Encodable<any>, options?: EncoderOptions<any>): Bytes {
|
||||
if (options === void 0 && !_usingCanonicalEncoder) {
|
||||
_usingCanonicalEncoder = true;
|
||||
const bs = _canonicalEncoder.push(v).contents();
|
||||
_usingCanonicalEncoder = false;
|
||||
return bs;
|
||||
} else {
|
||||
return encode(v, { ... options, canonical: true });
|
||||
}
|
||||
}
|
||||
|
||||
export function canonicalString(v: Encodable<any>): string {
|
||||
return _canonicalEncoder.push(v).contentsString();
|
||||
}
|
||||
|
||||
export function encodeWithAnnotations<T extends object>(v: Encodable<T>, options: EncoderOptions<T> = {}): Bytes {
|
||||
return encode(v, { ... options, includeAnnotations: true });
|
||||
}
|
||||
|
|
|
@ -0,0 +1,163 @@
|
|||
import { Annotated } from "./annotated";
|
||||
import { DecodeError, ShortPacket } from "./codec";
|
||||
import { Tag } from "./constants";
|
||||
import { Set, Dictionary } from "./dictionary";
|
||||
import { DoubleFloat, SingleFloat } from "./float";
|
||||
import { Record } from "./record";
|
||||
import { Bytes, BytesLike, underlying } from "./bytes";
|
||||
import { Value } from "./values";
|
||||
|
||||
export interface DecoderOptions<T extends object> {
|
||||
includeAnnotations?: boolean;
|
||||
decodePointer?: (v: Value<T>) => T;
|
||||
}
|
||||
|
||||
export class Decoder<T extends object> {
|
||||
packet: Uint8Array;
|
||||
index: number;
|
||||
options: DecoderOptions<T>;
|
||||
|
||||
constructor(packet: BytesLike = new Uint8Array(0), options: DecoderOptions<T> = {}) {
|
||||
this.packet = underlying(packet);
|
||||
this.index = 0;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
get includeAnnotations(): boolean {
|
||||
return this.options.includeAnnotations ?? false;
|
||||
}
|
||||
|
||||
write(data: BytesLike) {
|
||||
this.packet = Bytes.concat([this.packet.slice(this.index), data])._view;
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
nextbyte(): number {
|
||||
if (this.index >= this.packet.length) throw new ShortPacket("Short packet");
|
||||
// ^ NOTE: greater-than-or-equal-to, not greater-than.
|
||||
return this.packet[this.index++];
|
||||
}
|
||||
|
||||
nextbytes(n: number): DataView {
|
||||
const start = this.index;
|
||||
this.index += n;
|
||||
if (this.index > this.packet.length) throw new ShortPacket("Short packet");
|
||||
// ^ NOTE: greater-than, not greater-than-or-equal-to.
|
||||
return new DataView(this.packet.buffer, this.packet.byteOffset + start, n);
|
||||
}
|
||||
|
||||
varint(): number {
|
||||
// TODO: Bignums :-/
|
||||
const v = this.nextbyte();
|
||||
if (v < 128) return v;
|
||||
return (this.varint() << 7) + (v - 128);
|
||||
}
|
||||
|
||||
peekend(): boolean {
|
||||
const matched = this.nextbyte() === Tag.End;
|
||||
if (!matched) this.index--;
|
||||
return matched;
|
||||
}
|
||||
|
||||
nextvalues(): Value<T>[] {
|
||||
const result = [];
|
||||
while (!this.peekend()) result.push(this.next());
|
||||
return result;
|
||||
}
|
||||
|
||||
nextint(n: number): number {
|
||||
// TODO: Bignums :-/
|
||||
if (n === 0) return 0;
|
||||
let acc = this.nextbyte();
|
||||
if (acc & 0x80) acc -= 256;
|
||||
for (let i = 1; i < n; i++) acc = (acc * 256) + this.nextbyte();
|
||||
return acc;
|
||||
}
|
||||
|
||||
wrap(v: Value<T>): Value<T> {
|
||||
return this.includeAnnotations ? new Annotated(v) : v;
|
||||
}
|
||||
|
||||
static dictionaryFromArray<T extends object>(vs: Value<T>[]): Dictionary<Value<T>, T> {
|
||||
const d = new Dictionary<Value<T>, T>();
|
||||
if (vs.length % 2) throw new DecodeError("Missing dictionary value");
|
||||
for (let i = 0; i < vs.length; i += 2) {
|
||||
d.set(vs[i], vs[i+1]);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
unshiftAnnotation(a: Value<T>, v: Annotated<T>) {
|
||||
if (this.includeAnnotations) {
|
||||
v.annotations.unshift(a);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
next(): Value<T> {
|
||||
const tag = this.nextbyte();
|
||||
switch (tag) {
|
||||
case Tag.False: return this.wrap(false);
|
||||
case Tag.True: return this.wrap(true);
|
||||
case Tag.Float: return this.wrap(new SingleFloat(this.nextbytes(4).getFloat32(0, false)));
|
||||
case Tag.Double: return this.wrap(new DoubleFloat(this.nextbytes(8).getFloat64(0, false)));
|
||||
case Tag.End: throw new DecodeError("Unexpected Compound end marker");
|
||||
case Tag.Annotation: {
|
||||
const a = this.next();
|
||||
const v = this.next() as Annotated<T>;
|
||||
return this.unshiftAnnotation(a, v);
|
||||
}
|
||||
case Tag.Pointer: {
|
||||
const d = this.options.decodePointer;
|
||||
if (d === void 0) {
|
||||
throw new DecodeError("No decodePointer function supplied");
|
||||
}
|
||||
return this.wrap(d(this.next()));
|
||||
}
|
||||
case Tag.SignedInteger: return this.wrap(this.nextint(this.varint()));
|
||||
case Tag.String: return this.wrap(Bytes.from(this.nextbytes(this.varint())).fromUtf8());
|
||||
case Tag.ByteString: return this.wrap(Bytes.from(this.nextbytes(this.varint())));
|
||||
case Tag.Symbol: return this.wrap(Symbol.for(Bytes.from(this.nextbytes(this.varint())).fromUtf8()));
|
||||
case Tag.Record: {
|
||||
const vs = this.nextvalues();
|
||||
if (vs.length === 0) throw new DecodeError("Too few elements in encoded record");
|
||||
return this.wrap(Record(vs[0], vs.slice(1)));
|
||||
}
|
||||
case Tag.Sequence: return this.wrap(this.nextvalues());
|
||||
case Tag.Set: return this.wrap(new Set(this.nextvalues()));
|
||||
case Tag.Dictionary: return this.wrap(Decoder.dictionaryFromArray(this.nextvalues()));
|
||||
default: {
|
||||
if (tag >= Tag.SmallInteger_lo && tag <= Tag.SmallInteger_lo + 15) {
|
||||
const v = tag - Tag.SmallInteger_lo;
|
||||
return this.wrap(v > 12 ? v - 16 : v);
|
||||
}
|
||||
if (tag >= Tag.MediumInteger_lo && tag <= Tag.MediumInteger_lo + 15) {
|
||||
const n = tag - Tag.MediumInteger_lo;
|
||||
return this.wrap(this.nextint(n + 1));
|
||||
}
|
||||
throw new DecodeError("Unsupported Preserves tag: " + tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try_next(): Value<T> | undefined {
|
||||
const start = this.index;
|
||||
try {
|
||||
return this.next();
|
||||
} catch (e) {
|
||||
if (ShortPacket.isShortPacket(e)) {
|
||||
this.index = start;
|
||||
return void 0;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function decode<T extends object>(bs: BytesLike, options?: DecoderOptions<T>) {
|
||||
return new Decoder(bs, options).next();
|
||||
}
|
||||
|
||||
export function decodeWithAnnotations<T extends object>(bs: BytesLike, options: DecoderOptions<T> = {}): Annotated<T> {
|
||||
return decode(bs, { ... options, includeAnnotations: true }) as Annotated<T>;
|
||||
}
|
|
@ -1,6 +1,4 @@
|
|||
import type { Encoder } from "./codec";
|
||||
|
||||
import { canonicalEncode, canonicalString } from "./codec";
|
||||
import { Encoder, canonicalEncode, canonicalString } from "./encoder";
|
||||
import { Tag } from "./constants";
|
||||
import { FlexMap, FlexSet, _iterMap } from "./flex";
|
||||
import { PreserveOn } from "./symbols";
|
||||
|
|
|
@ -0,0 +1,234 @@
|
|||
import { Tag } from "./constants";
|
||||
import { Bytes, BytesLike, underlying } from "./bytes";
|
||||
import { Value } from "./values";
|
||||
import { PreserveOn } from "./symbols";
|
||||
import { EncodeError } from "./codec";
|
||||
import { Record, Tuple } from "./record";
|
||||
|
||||
export type Encodable<T extends object> =
|
||||
Value<T> | Preservable<T> | Iterable<Value<T>> | ArrayBufferView;
|
||||
|
||||
export interface Preservable<T extends object> {
|
||||
[PreserveOn](encoder: Encoder<T>): void;
|
||||
}
|
||||
|
||||
export function isPreservable<T extends object>(v: any): v is Preservable<T> {
|
||||
return typeof v === 'object' && v !== null && typeof v[PreserveOn] === 'function';
|
||||
}
|
||||
|
||||
export interface EncoderOptions<T extends object> {
|
||||
canonical?: boolean;
|
||||
includeAnnotations?: boolean;
|
||||
encodePointer?: (v: T) => Value<T>;
|
||||
}
|
||||
|
||||
function chunkStr(bs: Uint8Array): string {
|
||||
return String.fromCharCode.apply(null, bs as any as number[]);
|
||||
}
|
||||
|
||||
function isIterable<T>(v: any): v is Iterable<T> {
|
||||
return typeof v === 'object' && v !== null && typeof v[Symbol.iterator] === 'function';
|
||||
}
|
||||
|
||||
export class Encoder<T extends object> {
|
||||
chunks: Array<Uint8Array>;
|
||||
view: DataView;
|
||||
index: number;
|
||||
options: EncoderOptions<T>;
|
||||
|
||||
constructor(options: EncoderOptions<T> = {}) {
|
||||
this.chunks = [];
|
||||
this.view = new DataView(new ArrayBuffer(256));
|
||||
this.index = 0;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
get canonical(): boolean {
|
||||
return this.options.canonical ?? true;
|
||||
}
|
||||
|
||||
get includeAnnotations(): boolean {
|
||||
return this.options.includeAnnotations ?? !this.canonical;
|
||||
}
|
||||
|
||||
contents(): Bytes {
|
||||
if (this.chunks.length === 0) {
|
||||
const resultLength = this.index;
|
||||
this.index = 0;
|
||||
return new Bytes(this.view.buffer.slice(0, resultLength));
|
||||
} else {
|
||||
this.rotatebuffer(4096);
|
||||
return Bytes.concat(this.chunks);
|
||||
}
|
||||
}
|
||||
|
||||
/* Like contents(), but hands back a string containing binary data "encoded" via latin-1 */
|
||||
contentsString(): string {
|
||||
if (this.chunks.length === 0) {
|
||||
const s = chunkStr(new Uint8Array(this.view.buffer, 0, this.index));
|
||||
this.index = 0;
|
||||
return s;
|
||||
} else {
|
||||
this.rotatebuffer(4096);
|
||||
return this.chunks.map(chunkStr).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);
|
||||
}
|
||||
|
||||
encodevalues(tag: Tag, items: Iterable<Value<T>>) {
|
||||
this.emitbyte(tag);
|
||||
for (let i of items) { this.push(i); }
|
||||
this.emitbyte(Tag.End);
|
||||
}
|
||||
|
||||
encoderawvalues(tag: Tag, items: BytesLike[]) {
|
||||
this.emitbyte(tag);
|
||||
items.forEach((i) => this.emitbytes(underlying(i)));
|
||||
this.emitbyte(Tag.End);
|
||||
}
|
||||
|
||||
push(v: Encodable<T>) {
|
||||
if (isPreservable<never>(v)) {
|
||||
v[PreserveOn](this as unknown as Encoder<never>);
|
||||
}
|
||||
else if (isPreservable<T>(v)) {
|
||||
v[PreserveOn](this);
|
||||
}
|
||||
else if (typeof v === 'boolean') {
|
||||
this.emitbyte(v ? Tag.True : Tag.False);
|
||||
}
|
||||
else if (typeof v === 'number') {
|
||||
if (v >= -3 && v <= 12) {
|
||||
this.emitbyte(Tag.SmallInteger_lo + ((v + 16) & 0xf));
|
||||
} else {
|
||||
this.encodeint(v);
|
||||
}
|
||||
}
|
||||
else if (typeof v === 'string') {
|
||||
this.encodebytes(Tag.String, new Bytes(v)._view);
|
||||
}
|
||||
else if (typeof v === 'symbol') {
|
||||
const key = Symbol.keyFor(v);
|
||||
if (key === void 0) throw new EncodeError("Cannot preserve non-global Symbol", v);
|
||||
this.encodebytes(Tag.Symbol, new Bytes(key)._view);
|
||||
}
|
||||
else if (ArrayBuffer.isView(v)) {
|
||||
if (v instanceof Uint8Array) {
|
||||
this.encodebytes(Tag.ByteString, v);
|
||||
} else {
|
||||
const bs = new Uint8Array(v.buffer, v.byteOffset, v.byteLength);
|
||||
this.encodebytes(Tag.ByteString, bs);
|
||||
}
|
||||
}
|
||||
else if (Record.isRecord<Value<T>, Tuple<Value<T>>, T>(v)) {
|
||||
this.emitbyte(Tag.Record);
|
||||
this.push(v.label);
|
||||
for (let i of v) { this.push(i); }
|
||||
this.emitbyte(Tag.End);
|
||||
}
|
||||
else if (Array.isArray(v)) {
|
||||
this.encodevalues(Tag.Sequence, v);
|
||||
}
|
||||
else if (isIterable<Value<T>>(v)) {
|
||||
this.encodevalues(Tag.Sequence, v as Iterable<Value<T>>);
|
||||
}
|
||||
else {
|
||||
const e = this.options.encodePointer ?? pointerId;
|
||||
this.emitbyte(Tag.Pointer);
|
||||
this.push(e(v));
|
||||
}
|
||||
return this; // for chaining
|
||||
}
|
||||
}
|
||||
|
||||
export function encode<T extends object>(v: Encodable<T>, options?: EncoderOptions<T>): Bytes {
|
||||
return new Encoder(options).push(v).contents();
|
||||
}
|
||||
|
||||
let _nextId = 0;
|
||||
const _registry = new WeakMap<object, number>();
|
||||
export function pointerId(v: object): number {
|
||||
let id = _registry.get(v);
|
||||
if (id === void 0) {
|
||||
id = _nextId++;
|
||||
_registry.set(v, id);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
const _canonicalEncoder = new Encoder({ canonical: true });
|
||||
let _usingCanonicalEncoder = false;
|
||||
export function canonicalEncode(v: Encodable<any>, options?: EncoderOptions<any>): Bytes {
|
||||
if (options === void 0 && !_usingCanonicalEncoder) {
|
||||
_usingCanonicalEncoder = true;
|
||||
const bs = _canonicalEncoder.push(v).contents();
|
||||
_usingCanonicalEncoder = false;
|
||||
return bs;
|
||||
} else {
|
||||
return encode(v, { ... options, canonical: true });
|
||||
}
|
||||
}
|
||||
|
||||
export function canonicalString(v: Encodable<any>): string {
|
||||
return _canonicalEncoder.push(v).contentsString();
|
||||
}
|
||||
|
||||
export function encodeWithAnnotations<T extends object>(v: Encodable<T>, options: EncoderOptions<T> = {}): Bytes {
|
||||
return encode(v, { ... options, includeAnnotations: true });
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import { Encoder, Preservable } from "./encoder";
|
||||
import { Tag } from "./constants";
|
||||
import { AsPreserve, PreserveOn } from "./symbols";
|
||||
import { DefaultPointer, Value } from "./values";
|
||||
import { Encoder, Preservable } from "./codec";
|
||||
|
||||
export type FloatType = 'Single' | 'Double';
|
||||
export const FloatType = Symbol.for('FloatType');
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
export * from './annotated';
|
||||
export * from './bytes';
|
||||
export * from './codec';
|
||||
export * from './decoder';
|
||||
export * from './dictionary';
|
||||
export * from './encoder';
|
||||
export * from './flex';
|
||||
export * from './float';
|
||||
export * from './fold';
|
||||
|
|
Loading…
Reference in New Issue