"use strict"; // Preserves Binary codec. if (require('./singletonmodule.js')('leastfixedpoint.com/preserves', require('../package.json').version, 'codec.js', module)) return; const Values = require('./values.js'); const Annotations = require('./annotations.js'); const { fromJS, 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 ShortPacket extends DecodeError {} class Decoder { constructor(packet, options) { options = options || {}; this.packet = packet ? (packet._view || packet) // strip off Bytes wrapper, if any : new Uint8Array(0); this.index = 0; this.placeholders = fromJS(options.placeholders || {}); this.includeAnnotations = options.includeAnnotations || false; } write(data) { this.packet = Bytes.concat([this.packet.slice(this.index), data])._view; this.index = 0; } nextbyte() { if (this.index >= this.packet.length) throw new ShortPacket("Short packet"); // ^ NOTE: greater-than-or-equal-to, not greater-than. return this.packet[this.index++]; } nextbytes(n) { const start = this.index; this.index += n; if (this.index > this.packet.length) throw new ShortPacket("Short packet"); // ^ NOTE: greater-than, not greater-than-or-equal-to. return new DataView(this.packet.buffer, this.packet.byteOffset + start, n); } 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); } nextvalues(n) { const result = []; for (let i = 0; i < n; i++) result.push(this.next()); return result; } nextop() { const b = this.nextbyte(); const major = b >> 6; const minor = (b >> 4) & 3; const arg = b & 15; return [major, minor, arg]; } peekend() { const result = this.nextbyte() === 4; if (!result) this.index--; return result; } binarystream(minor) { const result = []; while (!this.peekend()) { 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(minor) { const result = []; while (!this.peekend()) result.push(this.next()); return this.decodecompound(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.fromUtf8(); case 2: return bs; case 3: return Symbol.for(bs.fromUtf8()); } } decodecompound(minor, vs) { switch (minor) { case 0: { if (vs.length === 0) throw new DecodeError("Too few elements in encoded record"); return new Record(vs[0], vs.slice(1)); } case 1: return List(vs); case 2: return Set(vs); case 3: return this.mapFromArray(vs); } } mapFromArray(vs) { return Map().withMutations((m) => { if (vs.length % 2) throw new DecodeError("Missing dictionary value"); for (let i = 0; i < vs.length; i += 2) { m.set(vs[i], vs[i+1]); } }); } wrap(v) { return this.includeAnnotations ? new Annotations.Annotated(v) : v; } unshiftAnnotation(a, v) { if (this.includeAnnotations) { v.annotations.unshift(a); } return v; } next() { while (true) { // we loop because we may need to consume an arbitrary number of no-ops const [major, minor, arg] = this.nextop(); switch (major) { case 0: switch (minor) { case 0: switch (arg) { case 0: return this.wrap(false); case 1: return this.wrap(true); case 2: return this.wrap(Single(this.nextbytes(4).getFloat32(0, false))); case 3: return this.wrap(Double(this.nextbytes(8).getFloat64(0, false))); case 4: throw new DecodeError("Unexpected end-of-stream marker"); case 5: { const a = this.next(); const v = this.next(); return this.unshiftAnnotation(a, v); } default: throw new DecodeError("Illegal format A lead byte"); } case 1: { const n = this.wirelength(arg); const v = this.placeholders.get(n, void 0); if (typeof v === 'undefined') { const e = new DecodeError("Invalid Preserves placeholder"); e.irritant = n; throw e; } return this.wrap(v); } case 2: { const t = arg >> 2; const n = arg & 3; switch (t) { case 1: return this.wrap(this.binarystream(n)); case 2: return this.wrap(this.valuestream(n)); default: throw new DecodeError("Invalid format C start byte"); } } case 3: return this.wrap((arg > 12) ? arg - 16 : arg); } case 1: return this.wrap(this.decodebinary(minor, Bytes.from(this.nextbytes(this.wirelength(arg))))); case 2: return this.wrap(this.decodecompound(minor, this.nextvalues(this.wirelength(arg)))); case 3: if (minor === 3 && arg === 15) { // no-op. continue; } else { throw new DecodeError("Invalid lead byte (major 3)"); } } } } try_next() { const start = this.index; try { return this.next(); } catch (e) { if (e instanceof ShortPacket) { this.index = start; return void 0; } throw e; } } } function decode(bs, options) { return new Decoder(bs, options).next(); } function decodeWithAnnotations(bs, options) { options = options || {}; options.includeAnnotations = true; return decode(bs, options); } class Encoder { constructor(options) { options = options || {} this.chunks = []; this.view = new DataView(new ArrayBuffer(256)); this.index = 0; this.placeholders = fromJS(options.placeholders || {}); } 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(2, 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, 0, 4); } encodenoop() { this.leadbyte(3, 3, 15); } push(v) { const placeholder = this.placeholders.get(v, void 0); if (typeof placeholder !== 'undefined') { this.header(0, 1, placeholder); } else 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, 3, 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(1, v); } else if (Set.isSet(v)) { this.encodecollection(2, v); } else if (Map.isMap(v)) { this.encodecollection(3, 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(2, 1, v); } else { throw new EncodeError("Cannot encode", v); } return this; // for chaining } } function encode(v, options) { return new Encoder(options).push(v).contents(); } Object.assign(module.exports, { DecodeError, EncodeError, ShortPacket, Decoder, decode, decodeWithAnnotations, Encoder, encode, });