"use strict"; // Preserves Binary codec. const Values = require('./values.js'); const { 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 Decoder { constructor(packet, options) { options = options || {}; this.packet = packet._view || packet; // strip off Bytes wrapper, if any 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() << 7) + (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 new DataView(this.packet.buffer, this.packet.byteOffset + start, n); } 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)) { 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(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) | bs[i]; return acc; } decodebinary(minor, bs) { switch (minor) { case 0: return this.decodeint(bs._view); case 1: return bs.toString(); case 2: return bs; case 3: return Symbol.for(bs.toString()); } } 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 = this.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 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) { case 0: switch (minor) { case 0: switch (arg) { case 0: return false; case 1: return true; case 2: return Single(this.nextbytes(4).getFloat32(0, false)); case 3: return Double(this.nextbytes(8).getFloat64(0, false)); } 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, Bytes.from(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.view = new DataView(new ArrayBuffer(256)); this.index = 0; this.shortForms = options.shortForms || {}; } 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(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); } 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, 1, 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(0, v); } else if (Set.isSet(v)) { this.encodecollection(1, 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 } } Object.assign(module.exports, { DecodeError, EncodeError, Decoder, Encoder, });