diff --git a/packages/marshal/src/encodePassable.js b/packages/marshal/src/encodePassable.js index 09ea1ef750..33c5ca87d1 100644 --- a/packages/marshal/src/encodePassable.js +++ b/packages/marshal/src/encodePassable.js @@ -252,9 +252,20 @@ const stringEscapes = Array(0x22) stringEscapes['^'.charCodeAt(0)] = '_@'; stringEscapes['_'.charCodeAt(0)] = '__'; -const encodeStringWithEscapes = str => +/** + * Encodes a string with escape sequences for use in the "compactOrdered" format. + * + * @type {(str: string) => string} + */ +const encodeCompactString = str => `s${str.replace(/[\0-!^_]/g, ch => stringEscapes[ch.charCodeAt(0)])}`; -const decodeStringWithEscapes = encoded => { + +/** + * Decodes a string from the "compactOrdered" format. + * + * @type {(encoded: string) => string} + */ +const decodeCompactString = encoded => { return encoded.slice(1).replace(/([!_])(.|\n)?/g, (esc, prefix, suffix) => { switch (esc) { case '!_': @@ -277,18 +288,30 @@ const decodeStringWithEscapes = encoded => { }); }; -const encodeStringWithoutEscapes = str => `s${str}`; -const decodeStringWithoutEscapes = encoded => encoded.slice(1); +/** + * Encodes a string by simple prefixing for use in the "legacyOrdered" format. + * + * @type {(str: string) => string} + */ +const encodeLegacyString = str => `s${str}`; + +/** + * Decodes a string from the "legacyOrdered" format. + * + * @type {(encoded: string) => string} + */ +const decodeLegacyString = encoded => encoded.slice(1); /** - * Encodes an array into a sequence of encoded elements, each terminated by a - * space (which is part of the escaped range in encoded strings). + * Encodes an array into a sequence of encoded elements for use in the "compactOrdered" + * format, each terminated by a space (which is part of the escaped range in + * "compactOrdered" encoded strings). * * @param {unknown[]} array * @param {(p: Passable) => string} encodePassable * @returns {string} */ -const encodeArrayWithoutEscapes = (array, encodePassable) => { +const encodeCompactArray = (array, encodePassable) => { const chars = ['^']; for (const element of array) { const enc = encodePassable(element); @@ -302,7 +325,7 @@ const encodeArrayWithoutEscapes = (array, encodePassable) => { * @param {(encoded: string) => Passable} decodePassable * @returns {Array} */ -const decodeArrayWithoutEscapes = (encoded, decodePassable) => { +const decodeCompactArray = (encoded, decodePassable) => { encoded.startsWith('^') || Fail`Encoded array expected: ${encoded}`; const tail = encoded.slice(1); const elements = []; @@ -343,9 +366,9 @@ const decodeArrayWithoutEscapes = (encoded, decodePassable) => { }; /** - * Performs the original array encoding, which escapes all array elements rather - * than just strings (`\u0000` as the element terminator and `\u0001` as the - * escape prefix for `\u0000` or `\u0001`). + * Performs the original array encoding, which escapes all encoded array + * elements rather than just strings (`\u0000` as the element terminator and + * `\u0001` as the escape prefix for `\u0000` or `\u0001`). * This necessitated an undesirable amount of iteration and expansion; see * https://github.com/endojs/endo/pull/1260#discussion_r960369826 * @@ -353,7 +376,7 @@ const decodeArrayWithoutEscapes = (encoded, decodePassable) => { * @param {(p: Passable) => string} encodePassable * @returns {string} */ -const encodeArrayWithEscapes = (array, encodePassable) => { +const encodeLegacyArray = (array, encodePassable) => { const chars = ['[']; for (const element of array) { const enc = encodePassable(element); @@ -373,7 +396,7 @@ const encodeArrayWithEscapes = (array, encodePassable) => { * @param {(encoded: string) => Passable} decodePassable * @returns {Array} */ -const decodeArrayWithEscapes = (encoded, decodePassable) => { +const decodeLegacyArray = (encoded, decodePassable) => { encoded.startsWith('[') || Fail`Encoded array expected: ${encoded}`; const elements = []; const elemChars = []; @@ -465,7 +488,7 @@ const assertEncodedError = encoding => { * error: Error, * encodeRecur: (p: Passable) => string, * ) => string} [encodeError] - * @property {boolean} [xxx] + * @property {'legacyOrdered' | 'compactOrdered'} [format] */ /** @@ -477,13 +500,23 @@ export const makeEncodePassable = (encodeOptions = {}) => { encodeRemotable = (rem, _) => Fail`remotable unexpected: ${rem}`, encodePromise = (prom, _) => Fail`promise unexpected: ${prom}`, encodeError = (err, _) => Fail`error unexpected: ${err}`, - xxx = false, + format = 'legacyOrdered', } = encodeOptions; - const encodeString = xxx - ? encodeStringWithEscapes - : encodeStringWithoutEscapes; - const encodeArray = xxx ? encodeArrayWithoutEscapes : encodeArrayWithEscapes; + let formatPrefix; + let encodeString; + let encodeArray; + if (format === 'legacyOrdered') { + formatPrefix = ''; + encodeString = encodeLegacyString; + encodeArray = encodeLegacyArray; + } else if (format === 'compactOrdered') { + formatPrefix = '~'; + encodeString = encodeCompactString; + encodeArray = encodeCompactArray; + } else { + Fail`Unrecognized format: ${q(format)}`; + } const innerEncode = passable => { if (isErrorLike(passable)) { @@ -556,10 +589,7 @@ export const makeEncodePassable = (encodeOptions = {}) => { } } }; - const encodePassable = xxx - ? // A leading "~" indicates the v2 encoding (with escaping in strings rather than arrays). - passable => `~${innerEncode(passable)}` - : innerEncode; + const encodePassable = passable => `${formatPrefix}${innerEncode(passable)}`; return harden(encodePassable); }; harden(makeEncodePassable); @@ -648,15 +678,12 @@ export const makeDecodePassable = (decodeOptions = {}) => { // A leading "~" indicates the v2 encoding (with escaping in strings rather than arrays). if (encoded.startsWith('~')) { const innerDecode = makeInnerDecode( - decodeStringWithEscapes, - decodeArrayWithoutEscapes, + decodeCompactString, + decodeCompactArray, ); return innerDecode(encoded.slice(1)); } - const innerDecode = makeInnerDecode( - decodeStringWithoutEscapes, - decodeArrayWithEscapes, - ); + const innerDecode = makeInnerDecode(decodeLegacyString, decodeLegacyArray); return innerDecode(encoded); }; return harden(decodePassable); diff --git a/packages/marshal/test/test-encodePassable.js b/packages/marshal/test/test-encodePassable.js index ee464131e3..5e51e85582 100644 --- a/packages/marshal/test/test-encodePassable.js +++ b/packages/marshal/test/test-encodePassable.js @@ -70,7 +70,7 @@ const encodePassableInternal2 = makeEncodePassable({ encodeRemotable: r => encodeThing('r', r), encodePromise: p => encodeThing('?', p), encodeError: er => encodeThing('!', er), - xxx: true, + format: 'compactOrdered', }); const encodePassable = passable => { @@ -93,6 +93,24 @@ const decodePassable = encoded => { return decodePassableInternal(encoded); }; +test('makeEncodePassable argument validation', t => { + t.notThrows(() => makeEncodePassable(), 'must accept zero arguments'); + t.notThrows(() => makeEncodePassable({}), 'must accept empty options'); + t.notThrows( + () => makeEncodePassable({ format: 'legacyOrdered' }), + 'must accept format: "legacyOrdered"', + ); + t.notThrows( + () => makeEncodePassable({ format: 'compactOrdered' }), + 'must accept format: "compactOrdered"', + ); + t.throws( + () => makeEncodePassable({ format: 'newHotness' }), + { message: /^Unrecognized format\b/ }, + 'must reject unknown format', + ); +}); + const { comparator: compareFull } = makeComparatorKit(compareRemotables); const asNumber = new Float64Array(1);