393 lines
10 KiB
JavaScript
393 lines
10 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) => {
|
|
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() {
|
|
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 0: throw new DecodeError("Invalid format C start byte (0)");
|
|
case 1: return this.wrap(this.binarystream(n));
|
|
case 2: return this.wrap(this.valuestream(n));
|
|
case 3: throw new DecodeError("Invalid format C start byte (3)");
|
|
}
|
|
}
|
|
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:
|
|
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);
|
|
}
|
|
|
|
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,
|
|
});
|