diff --git a/implementations/javascript/src/index.js b/implementations/javascript/src/index.js index ef44782..0deae66 100644 --- a/implementations/javascript/src/index.js +++ b/implementations/javascript/src/index.js @@ -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, }); diff --git a/implementations/javascript/test/samples.txt b/implementations/javascript/test/samples.txt new file mode 100644 index 0000000..33f6704 --- /dev/null +++ b/implementations/javascript/test/samples.txt @@ -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 diff --git a/implementations/javascript/test/test-decode.js b/implementations/javascript/test/test-decode.js new file mode 100644 index 0000000..358d5af --- /dev/null +++ b/implementations/javascript/test/test-decode.js @@ -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')); + }); + }); +});