forked from syndicate-lang/preserves
Tests and many associated repairs to the JavaScript implementation
This commit is contained in:
parent
22e2934845
commit
41ab0cf4ac
|
@ -48,7 +48,7 @@ class Decoder {
|
|||
// TODO: Bignums :-/
|
||||
const v = this.nextbyte();
|
||||
if (v < 128) return v;
|
||||
return this.varint() * 128 + (v - 128);
|
||||
return (this.varint() << 7) + (v - 128);
|
||||
}
|
||||
|
||||
nextbytes(n) {
|
||||
|
@ -86,7 +86,7 @@ class Decoder {
|
|||
binarystream(arg, minor) {
|
||||
const result = [];
|
||||
while (!this.peekend(arg)) {
|
||||
chunk = this.next();
|
||||
const chunk = this.next();
|
||||
if (!Buffer.isBuffer(chunk)) throw new DecodeError("Unexpected non-binary chunk");
|
||||
result.push(chunk);
|
||||
}
|
||||
|
@ -104,7 +104,7 @@ class Decoder {
|
|||
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];
|
||||
for (let i = 1; i < bs.length; i++) acc = (acc << 8) | bs[i];
|
||||
return acc;
|
||||
}
|
||||
|
||||
|
@ -122,7 +122,7 @@ class Decoder {
|
|||
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];
|
||||
const label = this.shortForms[minor];
|
||||
if (label === void 0) throw new DecodeError("Use of unconfigured short form " + minor);
|
||||
return new Record(label, vs);
|
||||
}
|
||||
|
@ -132,11 +132,19 @@ class Decoder {
|
|||
switch (minor) {
|
||||
case 0: return List(vs);
|
||||
case 1: return Set(vs);
|
||||
case 2: return mapFromArray(vs);
|
||||
case 2: return this.mapFromArray(vs);
|
||||
case 3: throw new DecodeError("Invalid collection type");
|
||||
}
|
||||
}
|
||||
|
||||
mapFromArray(vs) {
|
||||
return Map().withMutations((m) => {
|
||||
for (let i = 0; i < vs.length; i += 2) {
|
||||
m.set(vs[i], vs[i+1]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
next() {
|
||||
const [major, minor, arg] = this.nextop();
|
||||
switch (major) {
|
||||
|
@ -146,8 +154,8 @@ class Decoder {
|
|||
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 2: return new Single(this.nextbytes(4).readFloatBE());
|
||||
case 3: return new Double(this.nextbytes(8).readDoubleBE());
|
||||
}
|
||||
case 1:
|
||||
return (arg > 12) ? arg - 16 : arg;
|
||||
|
@ -209,12 +217,11 @@ class Encoder {
|
|||
}
|
||||
|
||||
varint(v) {
|
||||
if (v < 128) {
|
||||
this.emitbyte(v);
|
||||
} else {
|
||||
while (v >= 128) {
|
||||
this.emitbyte((v % 128) + 128);
|
||||
this.varint(Math.floor(v / 128));
|
||||
v = Math.floor(v / 128);
|
||||
}
|
||||
this.emitbyte(v);
|
||||
}
|
||||
|
||||
leadbyte(major, minor, arg) {
|
||||
|
@ -232,7 +239,7 @@ class Encoder {
|
|||
|
||||
encodeint(v) {
|
||||
// TODO: Bignums :-/
|
||||
const plain_bitcount = Math.floor(Math.log2(Math.abs(v))) + 1;
|
||||
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);
|
||||
|
@ -261,45 +268,56 @@ class Encoder {
|
|||
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);
|
||||
else if (typeof v === 'boolean') {
|
||||
this.leadbyte(0, 0, v ? 1 : 0);
|
||||
}
|
||||
if (typeof v === 'string') {
|
||||
else if (typeof v === 'number') {
|
||||
if (v >= -3 && v <= 12) {
|
||||
this.leadbyte(0, 1, v >= 0 ? v : v + 16);
|
||||
} else {
|
||||
this.encodeint(v);
|
||||
}
|
||||
}
|
||||
else if (typeof v === 'string') {
|
||||
const bs = Buffer.from(v);
|
||||
this.header(1, 1, bs.length);
|
||||
return this.emitbytes(bs);
|
||||
this.emitbytes(bs);
|
||||
}
|
||||
if (typeof v === 'symbol') {
|
||||
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 = Buffer.from(key);
|
||||
this.header(1, 3, bs.length);
|
||||
return this.emitbytes(bs);
|
||||
this.emitbytes(bs);
|
||||
}
|
||||
if (Buffer.isBuffer(v)) {
|
||||
else if (Buffer.isBuffer(v)) {
|
||||
this.header(1, 2, v.length);
|
||||
return this.emitbytes(v);
|
||||
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);
|
||||
else if (List.isList(v)) {
|
||||
this.encodecollection(0, v);
|
||||
}
|
||||
if (typeof v === 'object' && v !== null && typeof v[Symbol.iterator] === 'function') {
|
||||
return this.encodestream(3, 0, v);
|
||||
else if (Set.isSet(v)) {
|
||||
this.encodecollection(1, v);
|
||||
}
|
||||
throw new EncodeError("Cannot encode", v);
|
||||
else if (Map.isMap(v)) {
|
||||
this.encodecollection(2, 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(3, 0, v);
|
||||
}
|
||||
else {
|
||||
throw new EncodeError("Cannot encode", v);
|
||||
}
|
||||
return this; // for chaining
|
||||
}
|
||||
}
|
||||
|
||||
class Record {
|
||||
constructor(label, fields) {
|
||||
this.label = label;
|
||||
this.label = fromJS(label);
|
||||
this.fields = fromJS(fields);
|
||||
}
|
||||
|
||||
|
@ -332,11 +350,15 @@ class Record {
|
|||
encoder.header(2, 3, this.fields.size + 1);
|
||||
encoder.push(this.label);
|
||||
}
|
||||
for (const field in this.fields) { encoder.push(field); }
|
||||
for (const field of this.fields) { encoder.push(field); }
|
||||
}
|
||||
}
|
||||
|
||||
Record.makeConstructor = function (label, fieldNames) {
|
||||
Record.makeConstructor = function (labelSymbolText, fieldNames) {
|
||||
return Record.makeBasicConstructor(Symbol.for(labelSymbolText), fieldNames);
|
||||
};
|
||||
|
||||
Record.makeBasicConstructor = function (label, fieldNames) {
|
||||
const arity = fieldNames.length;
|
||||
const ctor = (...fields) => {
|
||||
if (fields.length !== arity) {
|
||||
|
@ -355,14 +377,15 @@ class Float {
|
|||
this.value = value;
|
||||
}
|
||||
|
||||
equals(other) {
|
||||
return (other instanceof Float) && (other.value === this.value);
|
||||
}
|
||||
|
||||
hashCode() {
|
||||
return this.value | 0; // TODO: something better?
|
||||
}
|
||||
}
|
||||
|
||||
class Single extends Float {
|
||||
equals(other) {
|
||||
return (other instanceof Single) && (other.value === this.value);
|
||||
}
|
||||
[PreserveOn](encoder) {
|
||||
encoder.leadbyte(0, 0, 2);
|
||||
encoder.makeroom(4);
|
||||
|
@ -371,6 +394,18 @@ class Float {
|
|||
}
|
||||
}
|
||||
|
||||
class Double extends Float {
|
||||
equals(other) {
|
||||
return (other instanceof Double) && (other.value === this.value);
|
||||
}
|
||||
[PreserveOn](encoder) {
|
||||
encoder.leadbyte(0, 0, 3);
|
||||
encoder.makeroom(8);
|
||||
encoder.buffer.writeDoubleBE(this.value, encoder.index);
|
||||
encoder.index += 8;
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(module.exports, {
|
||||
PreserveOn,
|
||||
DecodeError,
|
||||
|
@ -379,4 +414,6 @@ Object.assign(module.exports, {
|
|||
Encoder,
|
||||
Record,
|
||||
Float,
|
||||
Single,
|
||||
Double,
|
||||
});
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
023f800000
|
||||
033ff0000000000000
|
||||
03fe3cb7b759bf0426
|
||||
10
|
||||
11
|
||||
1c
|
||||
1d
|
||||
1e
|
||||
1f
|
||||
25626865626c6c6060616f35
|
||||
25626865636c6c6f35
|
||||
26626865626c6c6060616f36
|
||||
27626865626c6c6060616f37
|
||||
2c111213143c
|
||||
2cc2516111c2516212c25163133c
|
||||
410d
|
||||
417f
|
||||
4180
|
||||
4181
|
||||
41fc
|
||||
420080
|
||||
4200ff
|
||||
420100
|
||||
427fff
|
||||
42feff
|
||||
42ff00
|
||||
42ff01
|
||||
42ff02
|
||||
42ff7f
|
||||
43008000
|
||||
4300ffff
|
||||
43010000
|
||||
43020000
|
||||
5568656c6c6f
|
||||
6568656c6c6f
|
||||
7568656c6c6f
|
||||
9180
|
||||
a1b375737065616b809180
|
||||
b5c5767469746c656476706572736f6e12757468696e6711416559426c61636b77656c6cb4746461746542071d1213524472
|
||||
c411121314
|
||||
c41e1f1011
|
||||
c75568656c6c6f75746865726565776f726c64c0d00100
|
||||
e8716111516201c31112136163e27a66697273742d6e616d6559456c697a6162657468e2777375726e616d6559426c61636b77656c6c
|
|
@ -0,0 +1,106 @@
|
|||
"use strict";
|
||||
|
||||
const chai = require('chai');
|
||||
const expect = chai.expect;
|
||||
chai.use(require('chai-immutable'));
|
||||
|
||||
const Immutable = require('immutable');
|
||||
const { List, Set, Map, fromJS } = Immutable;
|
||||
|
||||
const Preserves = require('../src/index.js');
|
||||
const { Decoder, Encoder, Record, Single, Double } = Preserves;
|
||||
|
||||
const fs = require('fs');
|
||||
const util = require('util');
|
||||
|
||||
const shortForms = {
|
||||
0: Symbol.for('discard'),
|
||||
1: Symbol.for('capture'),
|
||||
2: Symbol.for('observe'),
|
||||
};
|
||||
|
||||
const Discard = Record.makeConstructor('discard', []);
|
||||
const Capture = Record.makeConstructor('capture', ['pattern']);
|
||||
const Observe = Record.makeConstructor('observe', ['pattern']);
|
||||
|
||||
describe('hex samples', () => {
|
||||
const samples = fs.readFileSync(__dirname + '/samples.txt').toString().split(/\n/)
|
||||
.filter((h) => h)
|
||||
.map((h) => Buffer.from(h, 'hex'));
|
||||
|
||||
// As new samples are added to samples.txt, we will need to update this list:
|
||||
const samplesExpected = [
|
||||
{ expected: new Single(1), },
|
||||
{ expected: new Double(1), },
|
||||
{ expected: new Double(-1.202e+300), },
|
||||
{ expected: 0, },
|
||||
{ expected: 1, },
|
||||
{ expected: 12, },
|
||||
{ expected: -3, },
|
||||
{ expected: -2, },
|
||||
{ expected: -1, },
|
||||
{ expected: Buffer.from("hello"), encodesTo: '6568656c6c6f', },
|
||||
{ expected: Buffer.from("hello"), encodesTo: '6568656c6c6f', },
|
||||
{ expected: Buffer.from("hello"), encodesTo: '6568656c6c6f', },
|
||||
{ expected: Buffer.from("hello"), encodesTo: '6568656c6c6f', },
|
||||
{ expected: List([1, 2, 3, 4]), encodesTo: 'c411121314', },
|
||||
{ expected: fromJS([["a", 1], ["b", 2], ["c", 3]]), encodesTo: 'c3c2516111c2516212c2516313', },
|
||||
{ expected: 13, },
|
||||
{ expected: 127, },
|
||||
{ expected: -128, },
|
||||
{ expected: -127, },
|
||||
{ expected: -4, },
|
||||
{ expected: 128, },
|
||||
{ expected: 255, },
|
||||
{ expected: 256, },
|
||||
{ expected: 32767, },
|
||||
{ expected: -257, },
|
||||
{ expected: -256, },
|
||||
{ expected: -255, },
|
||||
{ expected: -254, },
|
||||
{ expected: -129, },
|
||||
{ expected: 32768, },
|
||||
{ expected: 65535, },
|
||||
{ expected: 65536, },
|
||||
{ expected: 131072, },
|
||||
{ expected: "hello", },
|
||||
{ expected: Buffer.from("hello"), },
|
||||
{ expected: Symbol.for("hello"), },
|
||||
{ expected: Capture(Discard()), },
|
||||
{ expected: Observe(new Record(Symbol.for('speak'), [Discard(), Capture(Discard())])), },
|
||||
{ expected:
|
||||
new Record([Symbol.for('titled'), Symbol.for('person'), 2, Symbol.for('thing'), 1],
|
||||
[101, "Blackwell", new Record(Symbol.for('date'), [1821, 2, 3]), "Dr"]), },
|
||||
{ expected: List([1, 2, 3, 4]), },
|
||||
{ expected: List([-2, -1, 0, 1]), },
|
||||
{ expected:
|
||||
fromJS(["hello", Symbol.for('there'), Buffer.from('world'), [], Set(), true, false]), },
|
||||
{ expected:
|
||||
Map()
|
||||
.set(Symbol.for('a'), 1)
|
||||
.set('b', true)
|
||||
.set(fromJS([1, 2, 3]), Buffer.from('c'))
|
||||
.set(Map().set(Symbol.for('first-name'), 'Elizabeth'),
|
||||
Map().set(Symbol.for('surname'), 'Blackwell')), },
|
||||
];
|
||||
|
||||
samples.forEach((s, sampleIndex) => {
|
||||
it('[' + sampleIndex + '] ' + s.toString('hex') + ' should decode OK', () => {
|
||||
const actual = new Decoder(s, { shortForms }).next();
|
||||
const expected = samplesExpected[sampleIndex].expected;
|
||||
expect(Immutable.is(actual, expected),
|
||||
'[' + sampleIndex + '] actual ' + util.inspect(actual) +
|
||||
', expected ' + util.inspect(expected))
|
||||
.to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
samples.forEach((s, sampleIndex) => {
|
||||
it('[' + sampleIndex + '] ' + s.toString('hex') + ' should encode OK', () => {
|
||||
const entry = samplesExpected[sampleIndex];
|
||||
const actual = entry.encodesTo || s;
|
||||
const expected = new Encoder({ shortForms }).push(entry.expected).contents();
|
||||
expect(actual.toString('hex')).to.equal(expected.toString('hex'));
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue