diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/transport-codec.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/transport-codec.js | 224 |
1 files changed, 224 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport-codec.js b/packages/meshbay-hub/src/meshbay_hub/static/transport-codec.js new file mode 100644 index 0000000..eba0461 --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/static/transport-codec.js @@ -0,0 +1,224 @@ +// The wire codec: the subset of msgpack MNP uses (maps, strings, integers, +// binary, arrays, null), and hex for the ids the transport mints. + +// ── Minimal msgpack encode/decode ──────────────────────────────────────────── +// Covers the subset used by MNP: maps, strings, integers, binary, arrays, null. + +function msgpack_encode(obj) { + const parts = []; + _encodeValue(obj, parts); + const total = parts.reduce((s, p) => s + p.length, 0); + const result = new Uint8Array(total); + let off = 0; + for (const p of parts) { result.set(p, off); off += p.length; } + return result; +} + +function _encodeValue(val, parts) { + if (val === null || val === undefined) { + parts.push(new Uint8Array([0xc0])); + } else if (typeof val === 'boolean') { + parts.push(new Uint8Array([val ? 0xc3 : 0xc2])); + } else if (typeof val === 'number') { + if (Number.isInteger(val)) { + if (val >= 0 && val <= 127) { + parts.push(new Uint8Array([val])); + } else if (val >= 0 && val <= 0xff) { + parts.push(new Uint8Array([0xcc, val])); + } else if (val >= 0 && val <= 0xffff) { + const b = new Uint8Array(3); b[0] = 0xcd; + new DataView(b.buffer).setUint16(1, val, false); + parts.push(b); + } else if (val >= 0 && val <= 0xffffffff) { + const b = new Uint8Array(5); b[0] = 0xce; + new DataView(b.buffer).setUint32(1, val, false); + parts.push(b); + } else if (val >= 0 && val <= Number.MAX_SAFE_INTEGER) { + // Same split as the 0xcf decoder case above, in reverse — without + // this, a value over 0xffffffff fell to the plain int32 branch + // below and silently wrapped to a wrong, unrelated number instead + // of failing loudly. + const b = new Uint8Array(9); b[0] = 0xcf; + const dv = new DataView(b.buffer); + dv.setUint32(1, Math.floor(val / 4294967296), false); + dv.setUint32(5, val % 4294967296, false); + parts.push(b); + } else if (val >= -32 && val < 0) { + parts.push(new Uint8Array([val & 0xff])); + } else if (val >= -128 && val < 0) { + const b = new Uint8Array(2); b[0] = 0xd0; b[1] = val & 0xff; + parts.push(b); + } else { + const b = new Uint8Array(5); b[0] = 0xd2; + new DataView(b.buffer).setInt32(1, val, false); + parts.push(b); + } + } else { + const b = new Uint8Array(9); b[0] = 0xcb; + new DataView(b.buffer).setFloat64(1, val, false); + parts.push(b); + } + } else if (typeof val === 'string') { + const encoded = new TextEncoder().encode(val); + if (encoded.length <= 31) { + parts.push(new Uint8Array([0xa0 | encoded.length])); + } else if (encoded.length <= 0xff) { + parts.push(new Uint8Array([0xd9, encoded.length])); + } else if (encoded.length <= 0xffff) { + const b = new Uint8Array(3); b[0] = 0xda; + new DataView(b.buffer).setUint16(1, encoded.length, false); + parts.push(b); + } else { + const b = new Uint8Array(5); b[0] = 0xdb; + new DataView(b.buffer).setUint32(1, encoded.length, false); + parts.push(b); + } + parts.push(encoded); + } else if (val instanceof Uint8Array) { + if (val.length <= 0xff) { + parts.push(new Uint8Array([0xc4, val.length])); + } else if (val.length <= 0xffff) { + const b = new Uint8Array(3); b[0] = 0xc5; + new DataView(b.buffer).setUint16(1, val.length, false); + parts.push(b); + } else { + const b = new Uint8Array(5); b[0] = 0xc6; + new DataView(b.buffer).setUint32(1, val.length, false); + parts.push(b); + } + parts.push(val); + } else if (Array.isArray(val)) { + if (val.length <= 15) { + parts.push(new Uint8Array([0x90 | val.length])); + } else if (val.length <= 0xffff) { + const b = new Uint8Array(3); b[0] = 0xdc; + new DataView(b.buffer).setUint16(1, val.length, false); + parts.push(b); + } else { + const b = new Uint8Array(5); b[0] = 0xdd; + new DataView(b.buffer).setUint32(1, val.length, false); + parts.push(b); + } + for (const item of val) _encodeValue(item, parts); + } else if (typeof val === 'object') { + const keys = Object.keys(val); + if (keys.length <= 15) { + parts.push(new Uint8Array([0x80 | keys.length])); + } else if (keys.length <= 0xffff) { + const b = new Uint8Array(3); b[0] = 0xde; + new DataView(b.buffer).setUint16(1, keys.length, false); + parts.push(b); + } else { + const b = new Uint8Array(5); b[0] = 0xdf; + new DataView(b.buffer).setUint32(1, keys.length, false); + parts.push(b); + } + for (const k of keys) { + _encodeValue(k, parts); + _encodeValue(val[k], parts); + } + } +} + +function msgpack_decode(buf) { + const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); + const [val] = _decodeValue(buf, view, 0); + return val; +} + +function _decodeValue(buf, view, offset) { + const byte = buf[offset]; + + if (byte <= 0x7f) return [byte, offset + 1]; + if ((byte & 0xe0) === 0xe0) return [byte - 256, offset + 1]; + if ((byte & 0xa0) === 0xa0) { + const len = byte & 0x1f; + return [new TextDecoder().decode(buf.slice(offset + 1, offset + 1 + len)), offset + 1 + len]; + } + if ((byte & 0xf0) === 0x90) { + const len = byte & 0x0f; + return _decodeArray(buf, view, offset + 1, len); + } + if ((byte & 0xf0) === 0x80) { + const len = byte & 0x0f; + return _decodeMap(buf, view, offset + 1, len); + } + + switch (byte) { + case 0xc0: return [null, offset + 1]; + case 0xc2: return [false, offset + 1]; + case 0xc3: return [true, offset + 1]; + case 0xc4: { const len = buf[offset + 1]; return [buf.slice(offset + 2, offset + 2 + len), offset + 2 + len]; } + case 0xc5: { const len = view.getUint16(offset + 1, false); return [buf.slice(offset + 3, offset + 3 + len), offset + 3 + len]; } + case 0xc6: { const len = view.getUint32(offset + 1, false); return [buf.slice(offset + 5, offset + 5 + len), offset + 5 + len]; } + case 0xcc: return [buf[offset + 1], offset + 2]; + case 0xcd: return [view.getUint16(offset + 1, false), offset + 3]; + case 0xce: return [view.getUint32(offset + 1, false), offset + 5]; + // uint64/int64 — never emitted by this file's own encoder (a JS number + // above 0xffffffff falls to float64 there), but the node's real msgpack + // library sends a plain uint64 for any Python int over ~4.3 billion, and + // a raw byte count crosses that easily (found live: IndexProgress. + // scanned_bytes/total_bytes in the handshake ack, indexer.py, for a + // group whose total library size exceeds ~4 GB). Split into two 32-bit + // halves rather than DataView's getBigUint64/getBigInt64 — a BigInt + // would silently poison every arithmetic use of these fields elsewhere + // (percentage math, comparisons) — and every real byte count fits in a + // plain JS number well under Number.MAX_SAFE_INTEGER (2^53). + case 0xcf: { + const hi = view.getUint32(offset + 1, false); + const lo = view.getUint32(offset + 5, false); + return [hi * 4294967296 + lo, offset + 9]; + } + case 0xd3: { + const hi = view.getInt32(offset + 1, false); + const lo = view.getUint32(offset + 5, false); + return [hi * 4294967296 + lo, offset + 9]; + } + case 0xcb: return [view.getFloat64(offset + 1, false), offset + 9]; + case 0xd0: return [view.getInt8(offset + 1), offset + 2]; + case 0xd1: return [view.getInt16(offset + 1, false), offset + 3]; + case 0xd2: return [view.getInt32(offset + 1, false), offset + 5]; + case 0xd9: { + const len = buf[offset + 1]; + return [new TextDecoder().decode(buf.slice(offset + 2, offset + 2 + len)), offset + 2 + len]; + } + case 0xda: { + const len = view.getUint16(offset + 1, false); + return [new TextDecoder().decode(buf.slice(offset + 3, offset + 3 + len)), offset + 3 + len]; + } + case 0xdb: { + const len = view.getUint32(offset + 1, false); + return [new TextDecoder().decode(buf.slice(offset + 5, offset + 5 + len)), offset + 5 + len]; + } + case 0xdc: { const len = view.getUint16(offset + 1, false); return _decodeArray(buf, view, offset + 3, len); } + case 0xdd: { const len = view.getUint32(offset + 1, false); return _decodeArray(buf, view, offset + 5, len); } + case 0xde: { const len = view.getUint16(offset + 1, false); return _decodeMap(buf, view, offset + 3, len); } + case 0xdf: { const len = view.getUint32(offset + 1, false); return _decodeMap(buf, view, offset + 5, len); } + default: throw new Error(`Unknown msgpack type: 0x${byte.toString(16)}`); + } +} + +function _decodeArray(buf, view, offset, count) { + const arr = []; + for (let i = 0; i < count; i++) { + const [val, newOff] = _decodeValue(buf, view, offset); + arr.push(val); + offset = newOff; + } + return [arr, offset]; +} + +function _decodeMap(buf, view, offset, count) { + const obj = {}; + for (let i = 0; i < count; i++) { + const [key, off1] = _decodeValue(buf, view, offset); + const [val, off2] = _decodeValue(buf, view, off1); + obj[key] = val; + offset = off2; + } + return [obj, offset]; +} + +function _hex(bytes) { + return [...bytes].map(b => b.toString(16).padStart(2, '0')).join(''); +} |