diff --git a/packages/SwingSet/src/netstring.js b/packages/SwingSet/src/netstring.js new file mode 100644 index 000000000000..204af9b69a78 --- /dev/null +++ b/packages/SwingSet/src/netstring.js @@ -0,0 +1,81 @@ +// adapted from 'netstring-stream', https://github.com/tlivings/netstring-stream/ +import { assert } from '@agoric/assert'; + +const { Transform } = require('stream'); + +const COLON = 58; +const COMMA = 44; + +// input is a Buffer, output is a netstring-wrapped Buffer +export function encode(data) { + const prefix = Buffer.from(`${data.length}:`); + const suffix = Buffer.from(','); + return Buffer.concat([prefix, data, suffix]); +} + +// input is a sequence of strings, output is a byte pipe +export function createWriter() { + function transform(chunk, encoding, callback) { + assert.equal(encoding, 'buffer'); + callback(encode(chunk)); + } + return new Transform({ transform }); +} + +// Input is a Buffer containing zero or more netstrings and maybe some +// leftover bytes. Output is zero or more decoded Buffers, one per netstring, +// plus a Buffer of leftover bytes. +// +export function decode(data) { + // TODO: it would be more efficient to accumulate pending data in an array, + // rather than doing a concat each time + let start = 0; + const payloads = []; + + for (;;) { + const colon = data.indexOf(COLON, start); + if (colon === -1) { + break; // still waiting for `${LENGTH}:` + } + const sizeString = data.toString('utf-8', start, colon); + const size = parseInt(sizeString, 10); + if (!(size > -1)) { + // reject NaN, all negative numbers + throw Error(`unparseable size '${sizeString}', should be integer`); + } + if (data.length < colon + 1 + size + 1) { + break; // still waiting for `${DATA}.` + } + if (data[colon + 1 + size] !== COMMA) { + throw Error(`malformed netstring: not terminated by comma`); + } + payloads.push(data.subarray(colon + 1, colon + 1 + size)); + start = colon + 1 + size + 1; + } + + const leftover = data.subarray(start); + return { leftover, payloads }; +} + +// input is a byte pipe, output is a sequence of Buffers +export function createReader() { + let buffered = Buffer.from(''); + + function transform(chunk, encoding, callback) { + assert.equal(encoding, 'buffer'); + buffered = Buffer.concat([buffered, chunk]); + let res = null; + try { + const { leftover, payloads } = decode(buffered); + buffered = leftover; + for (let i = 0; i < payloads.length; i += 1) { + this.push(payloads[i]); + } + } catch (err) { + res = err; + } + callback(res); + } + + return new Transform({ transform }); +} diff --git a/packages/SwingSet/test/test-netstring.js b/packages/SwingSet/test/test-netstring.js new file mode 100644 index 000000000000..c9d2e7b1dbab --- /dev/null +++ b/packages/SwingSet/test/test-netstring.js @@ -0,0 +1,60 @@ +import '@agoric/install-ses'; // adds 'harden' to global +import test from 'ava'; +import { encode, decode } from '../src/netstring'; + +test('encode', t => { + function eq(input, expected) { + const encoded = encode(Buffer.from(input)); + const expBuf = Buffer.from(expected); + if (encoded.compare(expBuf) !== 0) { + console.log(`got : ${encoded}`); + console.log(`want: ${expBuf}`); + } + t.deepEqual(encoded, expBuf); + } + + eq('', '0:,'); + eq('a', '1:a,'); + eq('abc', '3:abc,'); + const umlaut = 'ümlaut'; + t.is(umlaut.length, 6); + const umlautBuffer = Buffer.from(umlaut, 'utf-8'); + t.is(umlautBuffer.length, 7); + const expectedBuffer = Buffer.from(`7:${umlaut},`, 'utf-8'); + eq(umlautBuffer, expectedBuffer); +}); + +test('decode', t => { + function eq(input, expPayloads, expLeftover) { + const encPayloads = expPayloads.map(Buffer.from); + const encLeftover = Buffer.from(expLeftover); + + const { payloads, leftover } = decode(Buffer.from(input)); + t.deepEqual(payloads, encPayloads); + t.deepEqual(leftover, encLeftover); + } + + eq('', [], ''); + eq('0', [], '0'); + eq('0:', [], '0:'); + eq('0:,', [''], ''); + eq('0:,1', [''], '1'); + eq('0:,1:', [''], '1:'); + eq('0:,1:a', [''], '1:a'); + eq('0:,1:a,', ['', 'a'], ''); + + const umlaut = 'ümlaut'; + t.is(umlaut.length, 6); + const umlautBuffer = Buffer.from(umlaut, 'utf-8'); + t.is(umlautBuffer.length, 7); + const expectedBuffer = Buffer.from(`7:${umlaut},`, 'utf-8'); + eq(expectedBuffer, [umlaut], ''); + + function bad(input, message) { + t.throws(() => decode(Buffer.from(input)), { message }); + } + + // bad('a', 'non-numeric length prefix'); + bad('a:', `unparseable size 'a', should be integer`); + bad('1:ab', 'malformed netstring: not terminated by comma'); +});