179 lines
5.7 KiB
TypeScript
179 lines
5.7 KiB
TypeScript
import { Tag } from "./constants";
|
|
import { Bytes } from "./bytes";
|
|
import { Value } from "./values";
|
|
import { EncodeError } from "./codec";
|
|
import { Record, Tuple } from "./record";
|
|
import { EmbeddedTypeEncode } from "./embedded";
|
|
import type { Embedded } from "./embedded";
|
|
import * as IO from "./iolist";
|
|
|
|
export type Encodable<T> =
|
|
Value<T> | Preservable<T> | Iterable<Value<T>> | ArrayBufferView;
|
|
|
|
export interface Preservable<T> {
|
|
__preserve_on__(encoder: Encoder<T>): IO.IOList;
|
|
}
|
|
|
|
export function isPreservable<T>(v: any): v is Preservable<T> {
|
|
return typeof v === 'object' && v !== null && '__preserve_on__' in v && typeof v.__preserve_on__ === 'function';
|
|
}
|
|
|
|
export interface EncoderOptions {
|
|
canonical?: boolean;
|
|
includeAnnotations?: boolean;
|
|
}
|
|
|
|
export interface EncoderEmbeddedOptions<T> extends EncoderOptions {
|
|
embeddedEncode?: EmbeddedTypeEncode<T>;
|
|
}
|
|
|
|
export function asLatin1(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';
|
|
}
|
|
|
|
let _nextId = 0;
|
|
const _registry = new WeakMap<object, number>();
|
|
export function embeddedId(v: any): number {
|
|
let id = _registry.get(v);
|
|
if (id === void 0) {
|
|
id = _nextId++;
|
|
_registry.set(v, id);
|
|
}
|
|
return id;
|
|
}
|
|
|
|
export const identityEmbeddedTypeEncode: EmbeddedTypeEncode<any> = {
|
|
encode(s: EncoderOptions, v: any): IO.IOList {
|
|
return new Encoder(s, this)._encode(embeddedId(v));
|
|
}
|
|
};
|
|
|
|
export function encodeVarint(v: number): IO.IOList {
|
|
function wr(v: number, d: number): IO.IOList {
|
|
if (v < 128) {
|
|
return v + d;
|
|
} else {
|
|
return [wr(Math.floor(v / 128), 0), (v & 127) + d];
|
|
}
|
|
}
|
|
return wr(v, 128);
|
|
}
|
|
|
|
export function encodeInt(v: number): IO.IOList {
|
|
// TODO: Bignums :-/
|
|
|
|
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;
|
|
if ('embeddedEncode' in options) {
|
|
this.embeddedEncode = options.embeddedEncode ?? identityEmbeddedTypeEncode;
|
|
} else {
|
|
this.embeddedEncode = embeddedEncode ?? identityEmbeddedTypeEncode;
|
|
}
|
|
}
|
|
|
|
get canonical(): boolean {
|
|
return this.options.canonical ?? true;
|
|
}
|
|
|
|
get includeAnnotations(): boolean {
|
|
return this.options.includeAnnotations ?? !this.canonical;
|
|
}
|
|
|
|
encode(v: Encodable<T>): Bytes {
|
|
return IO.ioListBytes(this._encode(v));
|
|
}
|
|
|
|
encodeString(v: Encodable<T>): string {
|
|
return asLatin1(this.encode(v)._view);
|
|
}
|
|
|
|
_encodevalues(items: Iterable<Value<T>>): IO.IOList {
|
|
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;
|
|
}
|
|
|
|
_encode(v: Encodable<T>): IO.IOList {
|
|
if (isPreservable<T>(v)) return v.__preserve_on__(this);
|
|
if (typeof v === 'boolean') return v ? Tag.True : Tag.False;
|
|
if (typeof v === 'number') return [Tag.SignedInteger, encodeInt(v)];
|
|
if (typeof v === 'string') return [Tag.String, new Bytes(v)._view, 0];
|
|
if (typeof v === 'symbol') {
|
|
const key = Symbol.keyFor(v);
|
|
if (key === void 0) throw new EncodeError("Cannot preserve non-global Symbol", v);
|
|
return [Tag.Symbol, new Bytes(key)._view];
|
|
}
|
|
if (ArrayBuffer.isView(v)) {
|
|
if (v instanceof Uint8Array) {
|
|
return [Tag.ByteString, v];
|
|
} else {
|
|
return [Tag.ByteString, new Uint8Array(v.buffer, v.byteOffset, v.byteLength)];
|
|
}
|
|
}
|
|
if (Record.isRecord<Value<T>, Tuple<Value<T>>, T>(v)) {
|
|
return [Tag.Record, this._encodevalues([v.label, ... v])];
|
|
}
|
|
if (isIterable<Value<T>>(v)) return [Tag.Sequence, this._encodevalues(v)];
|
|
return ((v: Embedded<T>) =>
|
|
[Tag.Embedded, this.embeddedEncode.encode(this.options, v.embeddedValue)])(v);
|
|
}
|
|
}
|
|
|
|
export function encode<T>(
|
|
v: Encodable<T>,
|
|
options: EncoderEmbeddedOptions<T> = {}): Bytes
|
|
{
|
|
return new Encoder(options).encode(v);
|
|
}
|
|
|
|
const _canonicalEncoder = new Encoder({ canonical: true });
|
|
|
|
export function canonicalEncode(v: Encodable<never>, options?: EncoderEmbeddedOptions<never>): Bytes;
|
|
export function canonicalEncode(v: Encodable<any>, options?: EncoderEmbeddedOptions<any>): Bytes;
|
|
export function canonicalEncode(v: any, options?: EncoderEmbeddedOptions<any>): Bytes {
|
|
if (options === void 0) {
|
|
return _canonicalEncoder.encode(v);
|
|
} else {
|
|
return encode(v, { ... options, canonical: true });
|
|
}
|
|
}
|
|
|
|
export function canonicalString(v: Encodable<any>): string {
|
|
return _canonicalEncoder.encodeString(v);
|
|
}
|
|
|
|
export function encodeWithAnnotations<T>(v: Encodable<T>,
|
|
options: EncoderEmbeddedOptions<T> = {}): Bytes {
|
|
return encode(v, { ... options, includeAnnotations: true });
|
|
}
|