"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, });