preserves/implementations/javascript/src/codec.js

404 lines
11 KiB
JavaScript

"use strict";
// Preserves Binary codec.
if (require('./singletonmodule.js')('leastfixedpoint.com/preserves',
require('../package.json').version,
'codec.js',
module)) return;
const Values = require('./values.js');
const Annotations = require('./annotations.js');
const { fromJS, List, Map, Set, Bytes, Record, Single, Double } = Values;
const { PreserveOn } = require('./symbols.js');
class DecodeError extends Error {}
class EncodeError extends Error {
constructor(message, irritant) {
super(message);
this.irritant = irritant;
}
}
class ShortPacket extends DecodeError {}
class Decoder {
constructor(packet, options) {
options = options || {};
this.packet = packet
? (packet._view || packet) // strip off Bytes wrapper, if any
: new Uint8Array(0);
this.index = 0;
this.placeholders = fromJS(options.placeholders || {});
this.includeAnnotations = options.includeAnnotations || false;
}
write(data) {
this.packet = Bytes.concat([this.packet.slice(this.index), data])._view;
this.index = 0;
}
nextbyte() {
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) {
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);
}
wirelength(arg) {
if (arg < 15) return arg;
return this.varint();
}
varint() {
// TODO: Bignums :-/
const v = this.nextbyte();
if (v < 128) return v;
return (this.varint() << 7) + (v - 128);
}
nextvalues(n) {
const result = [];
for (let i = 0; i < n; i++) result.push(this.next());
return result;
}
nextop() {
const b = this.nextbyte();
const major = b >> 6;
const minor = (b >> 4) & 3;
const arg = b & 15;
return [major, minor, arg];
}
peekend() {
const result = this.nextbyte() === 4;
if (!result) this.index--;
return result;
}
binarystream(minor) {
const result = [];
while (!this.peekend()) {
const chunk = this.next();
if (ArrayBuffer.isView(chunk)) {
result.push(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength));
} else if (chunk instanceof Bytes) {
result.push(chunk._view);
} else {
const e = new DecodeError("Unexpected non-binary chunk");
e.irritant = chunk;
throw e;
}
}
return this.decodebinary(minor, Bytes.concat(result));
}
valuestream(minor) {
const result = [];
while (!this.peekend()) result.push(this.next());
return this.decodecompound(minor, result);
}
decodeint(bs) {
// TODO: Bignums :-/
if (bs.length === 0) return 0;
let acc = bs[0];
if (acc & 0x80) acc -= 256;
for (let i = 1; i < bs.length; i++) acc = (acc << 8) | bs[i];
return acc;
}
decodebinary(minor, bs) {
switch (minor) {
case 0: return this.decodeint(bs._view);
case 1: return bs.fromUtf8();
case 2: return bs;
case 3: return Symbol.for(bs.fromUtf8());
}
}
decodecompound(minor, vs) {
switch (minor) {
case 0: {
if (vs.length === 0) throw new DecodeError("Too few elements in encoded record");
return new Record(vs[0], vs.slice(1));
}
case 1: return List(vs);
case 2: return Set(vs);
case 3: return this.mapFromArray(vs);
}
}
mapFromArray(vs) {
return Map().withMutations((m) => {
if (vs.length % 2) throw new DecodeError("Missing dictionary value");
for (let i = 0; i < vs.length; i += 2) {
m.set(vs[i], vs[i+1]);
}
});
}
wrap(v) {
return this.includeAnnotations ? new Annotations.Annotated(v) : v;
}
unshiftAnnotation(a, v) {
if (this.includeAnnotations) {
v.annotations.unshift(a);
}
return v;
}
next() {
while (true) { // we loop because we may need to consume an arbitrary number of no-ops
const [major, minor, arg] = this.nextop();
switch (major) {
case 0:
switch (minor) {
case 0:
switch (arg) {
case 0: return this.wrap(false);
case 1: return this.wrap(true);
case 2: return this.wrap(Single(this.nextbytes(4).getFloat32(0, false)));
case 3: return this.wrap(Double(this.nextbytes(8).getFloat64(0, false)));
case 4: throw new DecodeError("Unexpected end-of-stream marker");
case 5: {
const a = this.next();
const v = this.next();
return this.unshiftAnnotation(a, v);
}
default: throw new DecodeError("Illegal format A lead byte");
}
case 1: {
const n = this.wirelength(arg);
const v = this.placeholders.get(n, void 0);
if (typeof v === 'undefined') {
const e = new DecodeError("Invalid Preserves placeholder");
e.irritant = n;
throw e;
}
return this.wrap(v);
}
case 2: {
const t = arg >> 2;
const n = arg & 3;
switch (t) {
case 1: return this.wrap(this.binarystream(n));
case 2: return this.wrap(this.valuestream(n));
default: throw new DecodeError("Invalid format C start byte");
}
}
case 3:
return this.wrap((arg > 12) ? arg - 16 : arg);
}
case 1:
return this.wrap(this.decodebinary(minor, Bytes.from(this.nextbytes(this.wirelength(arg)))));
case 2:
return this.wrap(this.decodecompound(minor, this.nextvalues(this.wirelength(arg))));
case 3:
if (minor === 3 && arg === 15) {
// no-op.
continue;
} else {
throw new DecodeError("Invalid lead byte (major 3)");
}
}
}
}
try_next() {
const start = this.index;
try {
return this.next();
} catch (e) {
if (e instanceof ShortPacket) {
this.index = start;
return void 0;
}
throw e;
}
}
}
function decode(bs, options) {
return new Decoder(bs, options).next();
}
function decodeWithAnnotations(bs, options) {
options = options || {};
options.includeAnnotations = true;
return decode(bs, options);
}
class Encoder {
constructor(options) {
options = options || {}
this.chunks = [];
this.view = new DataView(new ArrayBuffer(256));
this.index = 0;
this.placeholders = fromJS(options.placeholders || {});
}
contents() {
this.rotatebuffer(4096);
return Bytes.concat(this.chunks);
}
rotatebuffer(size) {
this.chunks.push(new Uint8Array(this.view.buffer, 0, this.index));
this.view = new DataView(new ArrayBuffer(size));
this.index = 0;
}
makeroom(amount) {
if (this.index + amount > this.view.byteLength) {
this.rotatebuffer(amount + 4096);
}
}
emitbyte(b) {
this.makeroom(1);
this.view.setUint8(this.index++, b);
}
emitbytes(bs) {
this.makeroom(bs.length);
(new Uint8Array(this.view.buffer)).set(bs, this.index);
this.index += bs.length;
}
varint(v) {
while (v >= 128) {
this.emitbyte((v % 128) + 128);
v = Math.floor(v / 128);
}
this.emitbyte(v);
}
leadbyte(major, minor, arg) {
this.emitbyte(((major & 3) << 6) | ((minor & 3) << 4) | (arg & 15));
}
header(major, minor, wirelength) {
if (wirelength < 15) {
this.leadbyte(major, minor, wirelength);
} else {
this.leadbyte(major, minor, 15);
this.varint(wirelength);
}
}
encodeint(v) {
// TODO: Bignums :-/
const plain_bitcount = Math.floor(Math.log2(v > 0 ? v : ~v)) + 1;
const signed_bitcount = plain_bitcount + 1;
const bytecount = (signed_bitcount + 7) >> 3;
this.header(1, 0, bytecount);
const enc = (n, x) => {
if (n > 0) {
enc(n - 1, x >> 8);
this.emitbyte(x & 255);
}
};
enc(bytecount, v);
}
encodecollection(minor, items) {
this.header(2, minor, items.size);
for (const item of items) { this.push(item); }
}
encodestream(t, n, items) {
const tn = ((t & 3) << 2) | (n & 3);
this.header(0, 2, tn);
for (const item of items) { this.push(item); }
this.header(0, 0, 4);
}
encodenoop() {
this.leadbyte(3, 3, 15);
}
push(v) {
const placeholder = this.placeholders.get(v, void 0);
if (typeof placeholder !== 'undefined') {
this.header(0, 1, placeholder);
}
else if (typeof v === 'object' && v !== null && typeof v[PreserveOn] === 'function') {
v[PreserveOn](this);
}
else if (typeof v === 'boolean') {
this.leadbyte(0, 0, v ? 1 : 0);
}
else if (typeof v === 'number') {
if (v >= -3 && v <= 12) {
this.leadbyte(0, 3, v >= 0 ? v : v + 16);
} else {
this.encodeint(v);
}
}
else if (typeof v === 'string') {
const bs = Bytes(v)._view;
this.header(1, 1, bs.length);
this.emitbytes(bs);
}
else if (typeof v === 'symbol') {
const key = Symbol.keyFor(v);
if (key === void 0) throw new EncodeError("Cannot preserve non-global Symbol", v);
const bs = Bytes(key)._view;
this.header(1, 3, bs.length);
this.emitbytes(bs);
}
else if (ArrayBuffer.isView(v)) {
if (v instanceof Uint8Array) {
this.header(1, 2, v.length);
this.emitbytes(v);
} else {
const bs = new Uint8Array(v.buffer, v.byteOffset, v.byteLength);
this.header(1, 2, bs.length);
this.emitbytes(bs);
}
}
else if (List.isList(v)) {
this.encodecollection(1, v);
}
else if (Set.isSet(v)) {
this.encodecollection(2, v);
}
else if (Map.isMap(v)) {
this.encodecollection(3, List().withMutations((l) => {
v.forEach((val, key) => { l.push(key).push(val); });
}));
}
else if (typeof v === 'object' && v !== null && typeof v[Symbol.iterator] === 'function') {
this.encodestream(2, 1, v);
}
else {
throw new EncodeError("Cannot encode", v);
}
return this; // for chaining
}
}
function encode(v, options) {
return new Encoder(options).push(v).contents();
}
Object.assign(module.exports, {
DecodeError,
EncodeError,
ShortPacket,
Decoder,
decode,
decodeWithAnnotations,
Encoder,
encode,
});