Initial JavaScript (node) implementation
This commit is contained in:
parent
97f6c1291b
commit
f30b2ae175
|
@ -0,0 +1,2 @@
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "preserves",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Data serialization format",
|
||||||
|
"homepage": "https://gitlab.com/tonyg/preserves",
|
||||||
|
"license": "MIT",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"repository": "gitlab:tonyg/preserves",
|
||||||
|
"scripts": {
|
||||||
|
"test": "mocha",
|
||||||
|
"cover": "nyc --reporter=html mocha"
|
||||||
|
},
|
||||||
|
"main": "src/index.js",
|
||||||
|
"author": "Tony Garnock-Jones <tonyg@leastfixedpoint.com>",
|
||||||
|
"devDependencies": {
|
||||||
|
"chai": "^4.2.0",
|
||||||
|
"chai-immutable": "^2.0.0-rc.2",
|
||||||
|
"mocha": "^5.2.0",
|
||||||
|
"nyc": "^13.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"immutable": "^3.8.2"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,342 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const Immutable = require('immutable');
|
||||||
|
const { List, Map, Set, fromJS } = Immutable;
|
||||||
|
|
||||||
|
const PreserveOn = Symbol('PreserveOn');
|
||||||
|
|
||||||
|
class DecodeError extends Error {}
|
||||||
|
class EncodeError extends Error {
|
||||||
|
constructor(message, irritant) {
|
||||||
|
super(message);
|
||||||
|
this.irritant = irritant;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Decoder {
|
||||||
|
constructor(packet, options) {
|
||||||
|
options = options || {};
|
||||||
|
this.packet = packet;
|
||||||
|
this.index = 0;
|
||||||
|
this.shortForms = options.shortForms || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
peekbyte() {
|
||||||
|
if (this.index >= this.packet.length) throw new DecodeError("Short packet");
|
||||||
|
// ^ NOTE: greater-than-or-equal-to, not greater-than.
|
||||||
|
return this.packet[this.index];
|
||||||
|
}
|
||||||
|
|
||||||
|
advance(count) {
|
||||||
|
const start = this.index;
|
||||||
|
this.index += (count === void 0 ? 1 : count);
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextbyte() {
|
||||||
|
const val = this.peekbyte();
|
||||||
|
this.advance();
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
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() * 128 + (v - 128);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextbytes(n) {
|
||||||
|
const start = this.advance(n);
|
||||||
|
if (this.index > this.packet.length) throw new DecodeError("Short packet");
|
||||||
|
// ^ NOTE: greater-than, not greater-than-or-equal-to.
|
||||||
|
return this.packet.slice(start, this.index); // pretend it is immutable, please
|
||||||
|
}
|
||||||
|
|
||||||
|
nextvalues(n) {
|
||||||
|
const result = [];
|
||||||
|
for (let i = 0; i < n; i++) result.push(this.next());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
peekop() {
|
||||||
|
const b = this.peekbyte();
|
||||||
|
const major = b >> 6;
|
||||||
|
const minor = (b >> 4) & 3;
|
||||||
|
const arg = b & 15;
|
||||||
|
return [major, minor, arg];
|
||||||
|
}
|
||||||
|
|
||||||
|
nextop() {
|
||||||
|
const op = this.peekop();
|
||||||
|
this.advance();
|
||||||
|
return op;
|
||||||
|
}
|
||||||
|
|
||||||
|
peekend(arg) {
|
||||||
|
const [a,i,r] = this.peekop();
|
||||||
|
return (a === 0) && (i === 3) && (r === arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
binarystream(arg, minor) {
|
||||||
|
const result = [];
|
||||||
|
while (!this.peekend(arg)) {
|
||||||
|
chunk = this.next();
|
||||||
|
if (!Buffer.isBuffer(chunk)) throw new DecodeError("Unexpected non-binary chunk");
|
||||||
|
result.push(chunk);
|
||||||
|
}
|
||||||
|
return Buffer.concat(result); // pretend it is immutable, please
|
||||||
|
}
|
||||||
|
|
||||||
|
valuestream(arg, minor, decoder) {
|
||||||
|
const result = [];
|
||||||
|
while (!this.peekend(arg)) result.push(this.next());
|
||||||
|
return decoder(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) | b[i];
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
decodebinary(minor, bs) {
|
||||||
|
switch (minor) {
|
||||||
|
case 0: return this.decodeint(bs);
|
||||||
|
case 1: return bs.toString('utf-8');
|
||||||
|
case 2: return bs; // again, pretend it is immutable, please
|
||||||
|
case 3: return Symbol.for(bs.toString('utf-8'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
decoderecord(minor, vs) {
|
||||||
|
if (minor === 3) {
|
||||||
|
if (vs.length === 0) throw new DecodeError("Too few elements in encoded record");
|
||||||
|
return new Record(vs[0], vs.slice(1));
|
||||||
|
} else {
|
||||||
|
const label = shortForms[minor];
|
||||||
|
if (label === void 0) throw new DecodeError("Use of unconfigured short form " + minor);
|
||||||
|
return new Record(label, vs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
decodecollection(minor, vs) {
|
||||||
|
switch (minor) {
|
||||||
|
case 0: return List(vs);
|
||||||
|
case 1: return Set(vs);
|
||||||
|
case 2: return mapFromArray(vs);
|
||||||
|
case 3: throw new DecodeError("Invalid collection type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next() {
|
||||||
|
const [major, minor, arg] = this.nextop();
|
||||||
|
switch (major) {
|
||||||
|
case 0:
|
||||||
|
switch (minor) {
|
||||||
|
case 0:
|
||||||
|
switch (arg) {
|
||||||
|
case 0: return false;
|
||||||
|
case 1: return true;
|
||||||
|
case 2: return Float(this.nextbytes(4).readFloatBE());
|
||||||
|
case 3: return this.nextbytes(8).readDoubleBE();
|
||||||
|
}
|
||||||
|
case 1:
|
||||||
|
return (arg > 12) ? arg - 16 : arg;
|
||||||
|
case 2: {
|
||||||
|
const t = arg >> 2;
|
||||||
|
const n = arg & 3;
|
||||||
|
switch (t) {
|
||||||
|
case 0: throw new DecodeError("Invalid format C start byte");
|
||||||
|
case 1: return this.binarystream(arg, n);
|
||||||
|
case 2: return this.valuestream(arg, n, this.decoderecord.bind(this));
|
||||||
|
case 3: return this.valuestream(arg, n, this.decodecollection.bind(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 3:
|
||||||
|
throw new DecodeError("Invalid format C end byte");
|
||||||
|
}
|
||||||
|
case 1: return this.decodebinary(minor, this.nextbytes(this.wirelength(arg)));
|
||||||
|
case 2: return this.decoderecord(minor, this.nextvalues(this.wirelength(arg)));
|
||||||
|
case 3: return this.decodecollection(minor, this.nextvalues(this.wirelength(arg)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Encoder {
|
||||||
|
constructor(options) {
|
||||||
|
options = options || {}
|
||||||
|
this.chunks = [];
|
||||||
|
this.buffer = Buffer.alloc(256);
|
||||||
|
this.index = 0;
|
||||||
|
this.shortForms = options.shortForms || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
contents() {
|
||||||
|
this.rotatebuffer(4096);
|
||||||
|
return Buffer.concat(this.chunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
rotatebuffer(size) {
|
||||||
|
this.chunks.push(this.buffer.slice(0, this.index));
|
||||||
|
this.buffer = Buffer.alloc(size);
|
||||||
|
this.index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
makeroom(amount) {
|
||||||
|
if (this.index + amount > this.buffer.length) {
|
||||||
|
this.rotatebuffer(amount + 4096);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emitbyte(b) {
|
||||||
|
this.makeroom(1);
|
||||||
|
this.buffer[this.index++] = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
emitbytes(bs) {
|
||||||
|
this.makeroom(bs.length);
|
||||||
|
this.buffer.fill(bs, this.index, this.index + bs.length);
|
||||||
|
this.index += bs.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
varint(v) {
|
||||||
|
if (v < 128) {
|
||||||
|
this.emitbyte(v);
|
||||||
|
} else {
|
||||||
|
this.emitbyte((v % 128) + 128);
|
||||||
|
this.varint(Math.floor(v / 128));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(Math.abs(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(3, 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, 3, tn);
|
||||||
|
}
|
||||||
|
|
||||||
|
push(v) {
|
||||||
|
if (typeof v === 'object' && v !== null && typeof v[PreserveOn] === 'function') {
|
||||||
|
v[PreserveOn](this);
|
||||||
|
}
|
||||||
|
if (v === false) return this.leadbyte(0, 0, 0);
|
||||||
|
if (v === true) return this.leadbyte(0, 0, 1);
|
||||||
|
if (typeof v === 'number') {
|
||||||
|
if (v >= -3 && v <= 12) return this.leadbyte(0, 1, v >= 0 ? v : v + 16);
|
||||||
|
return this.encodeint(v);
|
||||||
|
}
|
||||||
|
if (typeof v === 'string') {
|
||||||
|
const bs = Buffer.from(v);
|
||||||
|
this.header(1, 1, bs.length);
|
||||||
|
return this.emitbytes(bs);
|
||||||
|
}
|
||||||
|
if (typeof v === 'symbol') {
|
||||||
|
const key = Symbol.keyFor(v);
|
||||||
|
if (key === void 0) throw new EncodeError("Cannot preserve non-global Symbol", v);
|
||||||
|
const bs = Buffer.from(key);
|
||||||
|
this.header(1, 3, bs.length);
|
||||||
|
return this.emitbytes(bs);
|
||||||
|
}
|
||||||
|
if (Buffer.isBuffer(v)) {
|
||||||
|
this.header(1, 2, v.length);
|
||||||
|
return this.emitbytes(v);
|
||||||
|
}
|
||||||
|
if (List.isList(v)) return this.encodecollection(0, v);
|
||||||
|
if (Set.isSet(v)) return this.encodecollection(1, v);
|
||||||
|
if (Map.isMap(v)) {
|
||||||
|
const kvs = [];
|
||||||
|
v.forEach((val, key) => { kvs.push(key); kvs.push(val); });
|
||||||
|
return this.encodecollection(2, kvs);
|
||||||
|
}
|
||||||
|
if (typeof v === 'object' && v !== null && typeof v[Symbol.iterator] === 'function') {
|
||||||
|
return this.encodestream(3, 0, v);
|
||||||
|
}
|
||||||
|
throw new EncodeError("Cannot encode", v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Record {
|
||||||
|
constructor(label, fields) {
|
||||||
|
this.label = label;
|
||||||
|
this.fields = fromJS(fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
[PreserveOn](encoder) {
|
||||||
|
if (Immutable.is(encoder.shortForms[0], this.label)) {
|
||||||
|
encoder.header(2, 0, this.fields.size);
|
||||||
|
} else if (Immutable.is(encoder.shortForms[1], this.label)) {
|
||||||
|
encoder.header(2, 1, this.fields.size);
|
||||||
|
} else if (Immutable.is(encoder.shortForms[2], this.label)) {
|
||||||
|
encoder.header(2, 2, this.fields.size);
|
||||||
|
} else {
|
||||||
|
encoder.header(2, 3, this.fields.size + 1);
|
||||||
|
encoder.push(this.label);
|
||||||
|
}
|
||||||
|
for (const field in this.fields) { encoder.push(field); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Float {
|
||||||
|
constructor(value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[PreserveOn](encoder) {
|
||||||
|
encoder.leadbyte(0, 0, 2);
|
||||||
|
encoder.makeroom(4);
|
||||||
|
encoder.buffer.writeFloatBE(this.value, encoder.index);
|
||||||
|
encoder.index += 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(module.exports, {
|
||||||
|
PreserveOn,
|
||||||
|
DecodeError,
|
||||||
|
EncodeError,
|
||||||
|
Decoder,
|
||||||
|
Encoder,
|
||||||
|
Record,
|
||||||
|
Float,
|
||||||
|
});
|
Loading…
Reference in New Issue