diff --git a/packages/captp/src/captp.js b/packages/captp/src/captp.js index 0ed5983154..968d9c542b 100644 --- a/packages/captp/src/captp.js +++ b/packages/captp/src/captp.js @@ -121,6 +121,8 @@ export const makeCapTP = ( // TODO Temporary hack. // See https://github.com/Agoric/agoric-sdk/issues/2780 errorIdNum: 20000, + // TODO: fix captp to be compatible with smallcaps + serializeBodyFormat: 'capdata', }, ); @@ -611,10 +613,11 @@ export const makeCapTP = ( assert.fail(X`Trap(${target}) target cannot be a promise`); const slot = valToSlot.get(target); - (slot && slot[1] === '-') || - assert.fail(X`Trap(${target}) target was not imported`); - // @ts-expect-error TS apparently confused about `||` control flow + // TypeScript confused about `||` control flow so use `if` instead // https://github.com/microsoft/TypeScript/issues/50739 + if (!(slot && slot[1] === '-')) { + assert.fail(X`Trap(${target}) target was not imported`); + } slot[0] === 't' || assert.fail( X`Trap(${target}) imported target was not created with makeTrapHandler`, @@ -651,8 +654,6 @@ export const makeCapTP = ( // messages over the current CapTP data channel. const [isException, serialized] = trapGuest({ trapMethod: implMethod, - // @ts-expect-error TS apparently confused about `||` control flow - // https://github.com/microsoft/TypeScript/issues/50739 slot, trapArgs: implArgs, startTrap: () => { diff --git a/packages/marshal/src/dot-membrane.js b/packages/marshal/src/dot-membrane.js index 7f1f2faa3d..4dd35e65c2 100644 --- a/packages/marshal/src/dot-membrane.js +++ b/packages/marshal/src/dot-membrane.js @@ -104,7 +104,7 @@ const makeConverter = (mirrorConverter = undefined) => { break; } default: { - assert.fail(X`unrecognized passStyle ${passStyle}`); + assert.fail(X`internal: Unrecognized passStyle ${passStyle}`); } } mineToYours.set(mine, yours); diff --git a/packages/marshal/src/encodeToCapData.js b/packages/marshal/src/encodeToCapData.js index 33cbf5b98d..3b3280d0b6 100644 --- a/packages/marshal/src/encodeToCapData.js +++ b/packages/marshal/src/encodeToCapData.js @@ -202,7 +202,10 @@ export const makeEncodeToCapData = ({ return encodePromiseToCapData(passable); } default: { - assert.fail(X`unrecognized passStyle ${q(passStyle)}`, TypeError); + assert.fail( + X`internal: Unrecognized passStyle ${q(passStyle)}`, + TypeError, + ); } } }; @@ -393,9 +396,9 @@ export const makeDecodeFromCapData = ({ // @ts-expect-error This is the error case we're testing for case 'ibid': { assert.fail( - X`The capData protocol no longer supports [${q(QCLASS)}]: ${q( + X`The capData protocol no longer supports ${q(QCLASS)} ${q( qclass, - )} encoding: ${jsonEncoded}.`, + )}`, ); } default: { diff --git a/packages/marshal/src/encodeToSmallcaps.js b/packages/marshal/src/encodeToSmallcaps.js index c74a2e81e9..f88713cc5e 100644 --- a/packages/marshal/src/encodeToSmallcaps.js +++ b/packages/marshal/src/encodeToSmallcaps.js @@ -12,11 +12,7 @@ import { passStyleOf } from './passStyleOf.js'; import { ErrorHelper } from './helpers/error.js'; import { makeTagged } from './makeTagged.js'; -import { - isObject, - getTag, - hasOwnPropertyOf, -} from './helpers/passStyle-helpers.js'; +import { getTag, hasOwnPropertyOf } from './helpers/passStyle-helpers.js'; import { assertPassableSymbol, nameForPassableSymbol, @@ -32,37 +28,65 @@ import { const { ownKeys } = Reflect; const { isArray } = Array; -const { - getOwnPropertyDescriptors, - defineProperties, - is, - fromEntries, - freeze, -} = Object; +const { is, fromEntries } = Object; const { details: X, quote: q } = assert; -/** - * Special property name that indicates an encoding that needs special - * decoding. - */ -export const SCLASS = '@qclass'; - -/** - * `'` - escaped string - * `+` - non-negative bigint - * `-` - negative bigint - * `#` - constant - * `@` - symbol - * `$` - remotable - * `?` - promise - */ -const SpecialChars = `'+-#@$?`; +const BANG = '!'.charCodeAt(0); +const DASH = '-'.charCodeAt(0); /** - * @param {SmallcapsEncoding} encoded - * @returns {encoded is SmallcapsEncodingUnion} + * An `encodeToSmallcaps` function takes a passable and returns a + * JSON-representable object (i.e., round-tripping it through + * `JSON.stringify` and `JSON.parse` with no replacers or revivers + * returns an equivalent structure except for object identity). + * We call this representation a Smallcaps Encoding. + * + * A `decodeFromSmallcaps` function takes as argument what it + * *assumes* is the result of a plain `JSON.parse` with no resolver. It then + * must validate that it is a valid Smallcaps Encoding, and if it is, + * return a corresponding passable. + * + * Smallcaps considers the characters between `!` (ascii code 33, BANG) + * and `-` (ascii code 45, DASH) to be special prefixes allowing + * representation of JSON-incompatible data using strings. + * These characters, in order, are `!"#$%&'()*+,-` + * Of these, smallcaps currently uses the following: + * + * * `!` - escaped string + * * `+` - non-negative bigint + * * `-` - negative bigint + * * `#` - manifest constant + * * `%` - symbol + * * `$` - remotable + * * `&` - promise + * + * All other special characters (`"'()*,`) are reserved for future use. + * + * The manifest constants that smallcaps currently uses for values: + * * `#undefined` + * * `#NaN` + * * `#Infinity` + * * `#-Infinity` + * + * and for property names: + * * `#tag` + * * `#error` + * + * All other encoded strings beginning with `#` are reserved for + * future use. + * + * @param {string} encodedStr + * @returns {boolean} */ -const hasSClass = encoded => hasOwnPropertyOf(encoded, SCLASS); +const startsSpecial = encodedStr => { + if (encodedStr === '') { + return false; + } + // charCodeAt(0) and number compare is a bit faster. + const code = encodedStr.charCodeAt(0); + // eslint-disable-next-line yoda + return BANG <= code && code <= DASH; +}; /** * @typedef {object} EncodeToSmallcapsOptions @@ -125,23 +149,33 @@ export const makeEncodeToSmallcaps = ({ */ const encodeToSmallcapsRecur = passable => { // First we handle all primitives. Some can be represented directly as - // JSON, and some must be encoded as [SCLASS] composites. + // JSON, and some must be encoded into smallcaps strings. const passStyle = passStyleOf(passable); switch (passStyle) { case 'null': case 'boolean': { + // pass through to JSON return passable; } case 'string': { - if (passable !== '' && SpecialChars.includes(passable[0])) { - return `'${passable}`; + if (startsSpecial(passable)) { + // Strings that start with a special char are quoted with `!`. + // Since `!` is itself a special character, this trivially does + // the Hilbert hotel. Also, since the special characters are + // a continuous subrange of ascii, this quoting is sort-order + // preserving. + return `!${passable}`; } + // All other strings pass through to JSON return passable; } case 'symbol': { + // At the smallcaps level, we prefix symbol names with `%`. + // By "symbol name", we mean according to `nameForPassableSymbol` + // which does some further escaping. See comment on that function. assertPassableSymbol(passable); const name = /** @type {string} */ (nameForPassableSymbol(passable)); - return `@${name}`; + return `%${name}`; } case 'undefined': { return '#undefined'; @@ -159,38 +193,23 @@ export const makeEncodeToSmallcaps = ({ if (passable === -Infinity) { return '#-Infinity'; } + // All other numbers pass through to JSON return passable; } case 'bigint': { const str = String(passable); - return passable < 0n ? `${str}n` : `+${str}n`; + return passable < 0n ? str : `+${str}`; } case 'copyRecord': { - if (hasOwnPropertyOf(passable, SCLASS)) { - // Hilbert hotel - const { [SCLASS]: sclassValue, ...rest } = passable; - /** @type {SmallcapsEncoding} */ - const result = { - [SCLASS]: 'hilbert', - original: encodeToSmallcapsRecur(sclassValue), - }; - if (ownKeys(rest).length >= 1) { - // We harden the entire smallcaps encoding before we return it. - // `encodeToSmallcaps` requires that its input be Passable, and - // therefore hardened. - // The `freeze` here is needed anyway, because the `rest` is - // freshly constructed by the `...` above, and we're using it - // as imput in another call to `encodeToSmallcaps`. - result.rest = encodeToSmallcapsRecur(freeze(rest)); - } - return result; - } // Currently copyRecord allows only string keys so this will // work. If we allow sortable symbol keys, this will need to // become more interesting. const names = ownKeys(passable).sort(); return fromEntries( - names.map(name => [name, encodeToSmallcapsRecur(passable[name])]), + names.map(name => [ + encodeToSmallcapsRecur(name), + encodeToSmallcapsRecur(passable[name]), + ]), ); } case 'copyArray': { @@ -198,8 +217,7 @@ export const makeEncodeToSmallcaps = ({ } case 'tagged': { return { - [SCLASS]: 'tagged', - tag: getTag(passable), + '#tag': getTag(passable), payload: encodeToSmallcapsRecur(passable.payload), }; } @@ -214,24 +232,31 @@ export const makeEncodeToSmallcaps = ({ } case 'promise': { const result = encodePromiseToSmallcaps(passable); - if (typeof result === 'string' && result.startsWith('?')) { + if (typeof result === 'string' && result.startsWith('&')) { return result; } assert.fail( - X`internal: Promise encoding must start with "?": ${result}`, + X`internal: Promise encoding must start with "&": ${result}`, ); } case 'error': { const result = encodeErrorToSmallcaps(passable); - if (typeof result === 'object' && result[SCLASS] === 'error') { + if ( + typeof result === 'object' && + hasOwnPropertyOf(result, '#error') && + typeof result['#error'] === 'string' + ) { return result; } assert.fail( - X`internal: Error encoding must use ${q(SCLASS)} "error": ${result}`, + X`internal: Error encoding must have "#error" property: ${result}`, ); } default: { - assert.fail(X`unrecognized passStyle ${q(passStyle)}`, TypeError); + assert.fail( + X`internal: Unrecognized passStyle ${q(passStyle)}`, + TypeError, + ); } } }; @@ -247,7 +272,17 @@ export const makeEncodeToSmallcaps = ({ // more interested in reporting whatever diagnostic information they // carry than we are about reporting problems encountered in reporting // this information. - return harden(encodeErrorToSmallcaps(passable)); + const result = harden(encodeErrorToSmallcaps(passable)); + if ( + typeof result === 'object' && + hasOwnPropertyOf(result, '#error') && + typeof result['#error'] === 'string' + ) { + return result; + } + assert.fail( + X`internal: Error encoding must have "#error" property: ${result}`, + ); } return harden(encodeToSmallcapsRecur(passable)); }; @@ -292,169 +327,136 @@ export const makeDecodeFromSmallcaps = ({ * @param {SmallcapsEncoding} encoding must be hardened */ const decodeFromSmallcaps = encoding => { - if (typeof encoding === 'string') { - switch (encoding.charAt(0)) { - case "'": { - // un-hilbert-ify the string - return encoding.slice(1); - } - case '@': { - return passableSymbolForName(encoding.slice(1)); + switch (typeof encoding) { + case 'boolean': + case 'number': { + return encoding; + } + case 'string': { + if (!startsSpecial(encoding)) { + return encoding; } - case '#': { - switch (encoding) { - case '#undefined': { - return undefined; - } - case '#NaN': { - return NaN; - } - case '#Infinity': { - return Infinity; + const c = encoding.charAt(0); + switch (c) { + case '!': { + // un-hilbert-ify the string + return encoding.slice(1); + } + case '%': { + return passableSymbolForName(encoding.slice(1)); + } + case '#': { + switch (encoding) { + case '#undefined': { + return undefined; + } + case '#NaN': { + return NaN; + } + case '#Infinity': { + return Infinity; + } + case '#-Infinity': { + return -Infinity; + } + default: { + assert.fail(X`unknown constant "${q(encoding)}"`, TypeError); + } } - case '#-Infinity': { - return -Infinity; + } + case '+': + case '-': { + return BigInt(encoding); + } + case '$': { + const result = decodeRemotableFromSmallcaps(encoding); + if (passStyleOf(result) !== 'remotable') { + assert.fail( + X`internal: decodeRemotableFromSmallcaps option must return a remotable: ${result}`, + ); } - default: { - assert.fail(X`unknown constant "${q(encoding)}"`, TypeError); + return result; + } + case '&': { + const result = decodePromiseFromSmallcaps(encoding); + if (passStyleOf(result) !== 'promise') { + assert.fail( + X`internal: decodePromiseFromSmallcaps option must return a promise: ${result}`, + ); } + return result; } - } - case '+': - case '-': { - const last = encoding.length - 1; - assert( - encoding[last] === 'n', - X`Encoded bigint must start with "+" or "-" and end with "n": ${encoding}`, - ); - return BigInt(encoding.slice(0, last)); - } - case '$': { - const result = decodeRemotableFromSmallcaps(encoding); - if (passStyleOf(result) !== 'remotable') { + default: { assert.fail( - X`internal: decodeRemotableFromSmallcaps option must return a remotable: ${result}`, + X`Special char ${q(c)} reserved for future use: ${encoding}`, ); } - return result; } - case '?': { - const result = decodePromiseFromSmallcaps(encoding); - if (passStyleOf(result) !== 'promise') { - assert.fail( - X`internal: decodePromiseFromSmallcaps option must return a promise: ${result}`, - ); + } + case 'object': { + if (encoding === null) { + return encoding; + } + + if (isArray(encoding)) { + const result = []; + const { length } = encoding; + for (let i = 0; i < length; i += 1) { + result[i] = decodeFromSmallcaps(encoding[i]); } return result; } - default: { - return encoding; - } - } - } - if (!isObject(encoding)) { - // primitives pass through - return encoding; - } - if (isArray(encoding)) { - const result = []; - const { length } = encoding; - for (let i = 0; i < length; i += 1) { - result[i] = decodeFromSmallcaps(encoding[i]); - } - return result; - } - if (hasSClass(encoding)) { - /** @type {string} */ - const sclass = encoding[SCLASS]; - if (typeof sclass !== 'string') { - assert.fail(X`invalid sclass typeof ${q(typeof sclass)}`); - } - switch (sclass) { - // SmallcapsEncoding of primitives not handled by JSON - case 'tagged': { - // Using @ts-ignore rather than @ts-expect-error below because - // with @ts-expect-error I get a red underline in vscode, but - // without it I get errors from `yarn lint`. - // @ts-ignore inadequate type inference - // See https://github.com/endojs/endo/pull/1259#discussion_r954561901 - const { tag, payload } = encoding; + + if (hasOwnPropertyOf(encoding, '#tag')) { + const { '#tag': tag, payload, ...rest } = encoding; + typeof tag === 'string' || + assert.fail( + X`Value of "#tag", the tag, must be a string: ${encoding}`, + ); + ownKeys(rest).length === 0 || + assert.fail( + X`#tag record unexpected properties: ${q(ownKeys(rest))}`, + ); return makeTagged(tag, decodeFromSmallcaps(payload)); } - case 'error': { + if (hasOwnPropertyOf(encoding, '#error')) { const result = decodeErrorFromSmallcaps(encoding); - if (passStyleOf(result) !== 'error') { + passStyleOf(result) === 'error' || assert.fail( X`internal: decodeErrorFromSmallcaps option must return an error: ${result}`, ); - } return result; } - case 'hilbert': { - // Using @ts-ignore rather than @ts-expect-error below because - // with @ts-expect-error I get a red underline in vscode, but - // without it I get errors from `yarn lint`. - // @ts-ignore inadequate type inference - // See https://github.com/endojs/endo/pull/1259#discussion_r954561901 - const { original, rest } = encoding; - assert( - hasOwnPropertyOf(encoding, 'original'), - X`Invalid Hilbert Hotel encoding ${encoding}`, - ); - // Don't harden since we're not done mutating it - const result = { [SCLASS]: decodeFromSmallcaps(original) }; - if (hasOwnPropertyOf(encoding, 'rest')) { - assert( - typeof rest === 'object' && - rest !== null && - ownKeys(rest).length >= 1, - X`Rest encoding must be a non-empty object: ${rest}`, - ); - const restObj = decodeFromSmallcaps(rest); - // TODO really should assert that `passStyleOf(rest)` is - // `'copyRecord'` but we'd have to harden it and it is too - // early to do that. - assert( - !hasOwnPropertyOf(restObj, SCLASS), - X`Rest must not contain its own definition of ${q(SCLASS)}`, + const result = {}; + for (const encodedName of ownKeys(encoding)) { + // TypeScript confused about `||` control flow so use `if` instead + // https://github.com/microsoft/TypeScript/issues/50739 + if (typeof encodedName !== 'string') { + assert.fail( + X`Property name ${q( + encodedName, + )} of ${encoding} must be a string`, ); - defineProperties(result, getOwnPropertyDescriptors(restObj)); } - return result; - } - - case 'ibid': - case 'undefined': - case 'NaN': - case 'Infinity': - case '-Infinity': - case '@@asyncIterator': - case 'symbol': - case 'slot': { - assert.fail( - X`Unlike capData, the smallcaps protocol does not support [${q( - sclass, - )} encoding: ${encoding}.`, - ); - } - default: { - assert.fail(X`unrecognized ${q(SCLASS)}: ${q(sclass)}`, TypeError); + !encodedName.startsWith('#') || + assert.fail( + X`Unrecognized record type ${q(encodedName)}: ${encoding}`, + ); + const name = decodeFromSmallcaps(encodedName); + result[name] = decodeFromSmallcaps(encoding[encodedName]); } + return result; } - } else { - assert(typeof encoding === 'object' && encoding !== null); - const result = {}; - for (const name of ownKeys(encoding)) { - if (typeof name !== 'string') { - assert.fail( - X`Property name ${q(name)} of ${encoding} must be a string`, - ); - } - result[name] = decodeFromSmallcaps(encoding[name]); + default: { + assert.fail( + X`internal: unrecognized JSON typeof ${q( + typeof encoding, + )}: ${encoding}`, + TypeError, + ); } - return result; } }; return harden(decodeFromSmallcaps); diff --git a/packages/marshal/src/marshal-stringify.js b/packages/marshal/src/marshal-stringify.js index 2bcb35c939..c6a32061a7 100644 --- a/packages/marshal/src/marshal-stringify.js +++ b/packages/marshal/src/marshal-stringify.js @@ -29,7 +29,11 @@ const badArray = harden(new Proxy(harden([]), badArrayHandler)); const { serialize, unserialize } = makeMarshal( doNotConvertValToSlot, doNotConvertSlotToVal, - { errorTagging: 'off', useSmallcaps: false }, + { + errorTagging: 'off', + // TODO fix tests to works with smallcaps. + serializeBodyFormat: 'capdata', + }, ); /** diff --git a/packages/marshal/src/marshal.js b/packages/marshal/src/marshal.js index 4c3b5a3833..c9ef08a563 100644 --- a/packages/marshal/src/marshal.js +++ b/packages/marshal/src/marshal.js @@ -15,7 +15,6 @@ import { import { makeDecodeFromSmallcaps, makeEncodeToSmallcaps, - SCLASS, } from './encodeToSmallcaps.js'; /** @typedef {import('./types.js').MakeMarshalOptions} MakeMarshalOptions */ @@ -54,7 +53,9 @@ export const makeMarshal = ( // to be revealed when correlating with the received error. marshalSaveError = err => console.log('Temporary logging of sent error', err), - useSmallcaps = false, + // Default to 'capdata' because it was implemented first. + // Sometimes, ontogeny does recapitulate phylogeny ;) + serializeBodyFormat = 'capdata', } = {}, ) => { assert.typeof(marshalName, 'string'); @@ -148,7 +149,26 @@ export const makeMarshal = ( } }; - if (useSmallcaps) { + if (serializeBodyFormat === 'capdata') { + const encodeRemotableToCapData = val => { + const iface = getInterfaceOf(val); + // console.log(`serializeSlot: ${val}`); + return serializeSlot(val, iface); + }; + + const encodeToCapData = makeEncodeToCapData({ + encodeRemotableToCapData, + encodePromiseToCapData: serializeSlot, + encodeErrorToCapData, + }); + + const encoded = encodeToCapData(root); + const body = JSON.stringify(encoded); + return harden({ + body, + slots, + }); + } else if (serializeBodyFormat === 'smallcaps') { /** * @param {string} prefix * @param {Passable} passable @@ -186,13 +206,18 @@ export const makeMarshal = ( }; const encodePromiseToSmallcaps = promise => { - return serializeSlotToSmallcaps('?', promise); + return serializeSlotToSmallcaps('&', promise); }; - // Only under this assumption are the error encodings the same, so - // be sure. - assert(QCLASS === SCLASS); - const encodeErrorToSmallcaps = encodeErrorToCapData; + const encodeErrorToSmallcaps = err => { + // Not the most elegant way to reuse code. TODO refactor. + const capDataErr = encodeErrorToCapData(err); + const { [QCLASS]: _, message, ...rest } = capDataErr; + return harden({ + '#error': message, + ...rest, + }); + }; const encodeToSmallcaps = makeEncodeToSmallcaps({ encodeRemotableToSmallcaps, @@ -204,28 +229,14 @@ export const makeMarshal = ( const smallcapsBody = JSON.stringify(encoded); return harden({ // Valid JSON cannot begin with a '#', so this is a valid signal + // indicating smallcaps format. body: `#${smallcapsBody}`, slots, }); } else { - const encodeRemotableToCapData = val => { - const iface = getInterfaceOf(val); - // console.log(`serializeSlot: ${val}`); - return serializeSlot(val, iface); - }; - - const encodeToCapData = makeEncodeToCapData({ - encodeRemotableToCapData, - encodePromiseToCapData: serializeSlot, - encodeErrorToCapData, - }); - - const encoded = encodeToCapData(root); - const body = JSON.stringify(encoded); - return harden({ - body, - slots, - }); + assert.fail( + X`Unrecognized serializeBodyFormat: ${q(serializeBodyFormat)}`, + ); } }; @@ -295,7 +306,7 @@ export const makeMarshal = ( }; const decodePromiseFromSmallcaps = stringEncoding => { - assert(stringEncoding.startsWith('?')); + assert(stringEncoding.startsWith('&')); // slots: $slotIndex.iface or $slotIndex const i = stringEncoding.indexOf('.'); const index = Number(stringEncoding.slice(1, i)); @@ -305,7 +316,17 @@ export const makeMarshal = ( return unserializeSlot(index, iface); }; - const decodeErrorFromSmallcaps = decodeErrorFromCapData; + const decodeErrorFromSmallcaps = encoding => { + const { '#error': message, name, ...rest } = encoding; + // Not the most elegant way to reuse code. TODO refactor + const rawTree = harden({ + [QCLASS]: 'error', + message, + name, + ...rest, + }); + return decodeErrorFromCapData(rawTree); + }; const reviveFromSmallcaps = makeDecodeFromSmallcaps({ decodeRemotableFromSmallcaps, diff --git a/packages/marshal/src/types.js b/packages/marshal/src/types.js index 68921de1df..f4cd33b54e 100644 --- a/packages/marshal/src/types.js +++ b/packages/marshal/src/types.js @@ -200,7 +200,7 @@ export {}; /** * @typedef {Object} MakeMarshalOptions - * @property {'on'|'off'=} errorTagging controls whether serialized errors + * @property {'on'|'off'} [errorTagging] controls whether serialized errors * also carry tagging information, made from `marshalName` and numbers * generated (currently by counting) starting at `errorIdNum`. The * `errorTagging` option defaults to `'on'`. Serialized @@ -214,7 +214,15 @@ export {}; * that error with its errorId. Thus, if `marshalSaveError` in turn logs * to the normal console, which is the default, then the console will * show that note showing the associated errorId. - * @property {boolean} [useSmallcaps] + * @property {'capdata'|'smallcaps'} [serializeBodyFormat] + * Formatting to use in the "body" property in objects returned from + * `serialize`. The body string for each case: + * * 'capdata' - a JSON string, from an encoding of passables + * into JSON, where some values are represented as objects with a + * `'@qclass` property. + * * 'smallcaps' - a JSON string prefixed with `'#'`, which is + * an unambiguous signal since a valid JSON string cannot begin with + * `'#'`. */ // ///////////////////////////////////////////////////////////////////////////// diff --git a/packages/marshal/test/test-marshal.js b/packages/marshal/test/test-marshal-capdata.js similarity index 98% rename from packages/marshal/test/test-marshal.js rename to packages/marshal/test/test-marshal-capdata.js index d0df9f40ee..66cd468327 100644 --- a/packages/marshal/test/test-marshal.js +++ b/packages/marshal/test/test-marshal-capdata.js @@ -146,7 +146,7 @@ export const roundTripPairs = harden([ const makeTestMarshal = () => makeMarshal(undefined, undefined, { - useSmallcaps: false, + serializeBodyFormat: 'capdata', }); test('serialize unserialize round trip pairs', t => { @@ -154,7 +154,7 @@ test('serialize unserialize round trip pairs', t => { // TODO errorTagging will only be recognized once we merge with PR #2437 // We're turning it off only for the round trip test, not in general. errorTagging: 'off', - useSmallcaps: false, + serializeBodyFormat: 'capdata', }); for (const [plain, encoded] of roundTripPairs) { const { body } = serialize(plain); @@ -288,7 +288,7 @@ test('records', t => { convertValToSlot, convertSlotToVal, { - useSmallcaps: false, + serializeBodyFormat: 'capdata', }, ); diff --git a/packages/marshal/test/test-marshal-far-obj.js b/packages/marshal/test/test-marshal-far-obj.js index 58ae01e39d..845b5a1a7b 100644 --- a/packages/marshal/test/test-marshal-far-obj.js +++ b/packages/marshal/test/test-marshal-far-obj.js @@ -61,14 +61,10 @@ test('Remotable/getInterfaceOf', t => { return 'slot'; }; const m = makeMarshal(convertValToSlot, undefined, { - useSmallcaps: false, + serializeBodyFormat: 'smallcaps', }); t.deepEqual(m.serialize(p2), { - body: JSON.stringify({ - '@qclass': 'slot', - iface: 'Alleged: Thing', - index: 0, - }), + body: '#"$0.Alleged: Thing"', slots: ['slot'], }); }); @@ -172,15 +168,11 @@ test('transitional remotables', t => { return presence; } const { serialize: ser } = makeMarshal(convertValToSlot, convertSlotToVal, { - useSmallcaps: false, + serializeBodyFormat: 'smallcaps', }); const yesIface = { - body: JSON.stringify({ - '@qclass': 'slot', - iface: 'Alleged: iface', - index: 0, - }), + body: '#"$0.Alleged: iface"', slots: ['slot'], }; diff --git a/packages/marshal/test/test-marshal-justin.js b/packages/marshal/test/test-marshal-justin.js index 017498fe52..ad783683a4 100644 --- a/packages/marshal/test/test-marshal-justin.js +++ b/packages/marshal/test/test-marshal-justin.js @@ -8,7 +8,7 @@ import { decodeToJustin } from '../src/marshal-justin.js'; // this only includes the tests that do not use liveSlots /** - * Based on roundTripPairs from test-marshal.js + * Based on roundTripPairs from test-marshal-capdata.js * * A list of `[body, justinSrc]` pairs, where the body parses into * an encoding that decodes to a Justin expression that evaluates to something @@ -100,7 +100,8 @@ test('serialize decodeToJustin eval round trip pairs', t => { // We're turning `errorTagging`` off only for the round trip tests, not in // general. errorTagging: 'off', - useSmallcaps: false, + // TODO make Justin work with smallcaps + serializeBodyFormat: 'capdata', }); for (const [body, justinSrc] of jsonPairs) { const c = fakeJustinCompartment(); @@ -123,7 +124,8 @@ test('serialize decodeToJustin indented eval round trip', t => { // We're turning `errorTagging`` off only for the round trip tests, not in // general. errorTagging: 'off', - useSmallcaps: false, + // TODO make Justin work with smallcaps + serializeBodyFormat: 'capdata', }); for (const [body] of jsonPairs) { const c = fakeJustinCompartment(); diff --git a/packages/marshal/test/test-marshal-smallcaps.js b/packages/marshal/test/test-marshal-smallcaps.js index c1fedd58d0..774244c49b 100644 --- a/packages/marshal/test/test-marshal-smallcaps.js +++ b/packages/marshal/test/test-marshal-smallcaps.js @@ -2,9 +2,12 @@ import { test } from './prepare-test-env-ava.js'; +import { Far } from '../src/make-far.js'; import { makeMarshal } from '../src/marshal.js'; -import { roundTripPairs } from './test-marshal.js'; +import { roundTripPairs } from './test-marshal-capdata.js'; +import { makeTagged } from '../src/makeTagged.js'; +import { passStyleOf } from '../src/passStyleOf.js'; const { freeze, isFrozen, create, prototype: objectPrototype } = Object; @@ -13,7 +16,7 @@ const { freeze, isFrozen, create, prototype: objectPrototype } = Object; const makeTestMarshal = () => makeMarshal(undefined, undefined, { errorTagging: 'off', - useSmallcaps: true, + serializeBodyFormat: 'smallcaps', }); test('smallcaps serialize unserialize round trip half pairs', t => { @@ -66,12 +69,12 @@ test('smallcaps serialize errors', t => { const ser = val => serialize(val); t.deepEqual(ser(harden(Error())), { - body: '#{"@qclass":"error","message":"","name":"Error"}', + body: '#{"#error":"","name":"Error"}', slots: [], }); t.deepEqual(ser(harden(ReferenceError('msg'))), { - body: '#{"@qclass":"error","message":"msg","name":"ReferenceError"}', + body: '#{"#error":"msg","name":"ReferenceError"}', slots: [], }); @@ -87,8 +90,7 @@ test('smallcaps serialize errors', t => { // @ts-ignore Check dynamic consequences of type violation t.falsy(isFrozen(errExtra.foo)); t.deepEqual(ser(errExtra), { - body: - '#{"@qclass":"error","message":"has extra properties","name":"Error"}', + body: '#{"#error":"has extra properties","name":"Error"}', slots: [], }); // @ts-ignore Check dynamic consequences of type violation @@ -98,7 +100,7 @@ test('smallcaps serialize errors', t => { const nonErrorProto1 = { __proto__: Error.prototype, name: 'included' }; const nonError1 = { __proto__: nonErrorProto1, message: [] }; t.deepEqual(ser(harden(nonError1)), { - body: '#{"@qclass":"error","message":"","name":"included"}', + body: '#{"#error":"","name":"included"}', slots: [], }); }); @@ -107,18 +109,16 @@ test('smallcaps unserialize errors', t => { const { unserialize } = makeTestMarshal(); const uns = body => unserialize({ body, slots: [] }); - const em1 = uns( - '#{"@qclass":"error","message":"msg","name":"ReferenceError"}', - ); + const em1 = uns('#{"#error":"msg","name":"ReferenceError"}'); t.truthy(em1 instanceof ReferenceError); t.is(em1.message, 'msg'); t.truthy(isFrozen(em1)); - const em2 = uns('#{"@qclass":"error","message":"msg2","name":"TypeError"}'); + const em2 = uns('#{"#error":"msg2","name":"TypeError"}'); t.truthy(em2 instanceof TypeError); t.is(em2.message, 'msg2'); - const em3 = uns('#{"@qclass":"error","message":"msg3","name":"Unknown"}'); + const em3 = uns('#{"#error":"msg3","name":"Unknown"}'); t.truthy(em3 instanceof Error); t.is(em3.message, 'msg3'); }); @@ -126,7 +126,9 @@ test('smallcaps unserialize errors', t => { test('smallcaps mal-formed @qclass', t => { const { unserialize } = makeTestMarshal(); const uns = body => unserialize({ body, slots: [] }); - t.throws(() => uns('#{"@qclass": 0}'), { message: /invalid sclass/ }); + t.throws(() => uns('#{"#foo": 0}'), { + message: 'Unrecognized record type "#foo": {"#foo":0}', + }); }); test('smallcaps records', t => { @@ -142,7 +144,7 @@ test('smallcaps records', t => { convertSlotToVal, { errorTagging: 'off', - useSmallcaps: true, + serializeBodyFormat: 'smallcaps', }, ); @@ -217,3 +219,109 @@ test('smallcaps records', t => { shouldThrow(['nonenumStringData'], REC_ONLYENUM); shouldThrow(['nonenumStringData', 'enumStringData'], REC_ONLYENUM); }); + +/** + * A test case to illustrate each of the encodings + * * `!` - escaped string + * * `+` - non-negative bigint + * * `-` - negative bigint + * * `#` - manifest constant + * * `%` - symbol + * * `$` - remotable + * * `&` - promise + */ +test('smallcaps encoding examples', t => { + const { serialize } = makeMarshal(() => 'slot', undefined, { + errorTagging: 'off', + serializeBodyFormat: 'smallcaps', + }); + const assertSer = (val, body, slots, message) => + t.deepEqual(serialize(val), { body, slots }, message); + + // Numbers + assertSer(0, '#0', [], 'zero'); + assertSer(500n, '#"+500"', [], 'bigint'); + assertSer(-400n, '#"-400"', [], '-bigint'); + + // Constants + assertSer(NaN, '#"#NaN"', [], 'NaN'); + assertSer(Infinity, '#"#Infinity"', [], 'Infinity'); + assertSer(-Infinity, '#"#-Infinity"', [], '-Infinity'); + assertSer(undefined, '#"#undefined"', [], 'undefined'); + + // Strings + assertSer('unescaped', '#"unescaped"', [], 'unescaped'); + assertSer('#escaped', `#"!#escaped"`, [], 'escaped #'); + assertSer('+escaped', `#"!+escaped"`, [], 'escaped +'); + assertSer('-escaped', `#"!-escaped"`, [], 'escaped -'); + assertSer('%escaped', `#"!%escaped"`, [], 'escaped %'); + + // Symbols + assertSer(Symbol.iterator, '#"%@@iterator"', [], 'well known symbol'); + assertSer(Symbol.for('foo'), '#"%foo"', [], 'reg symbol'); + assertSer( + Symbol.for('@@foo'), + '#"%@@@@foo"', + [], + 'reg symbol that looks well known', + ); + + // Remotables + const foo = Far('foo', {}); + const bar = Far('bar', {}); + assertSer(foo, '#"$0.Alleged: foo"', ['slot'], 'Remotable object'); + assertSer( + harden([foo, bar, foo]), + '#["$0.Alleged: foo","$1.Alleged: bar","$0"]', + ['slot', 'slot'], + 'Only show iface once', + ); + + // Promises + const p = harden(Promise.resolve(null)); + assertSer(p, '#"&0"', ['slot'], 'Promise'); + + // Arrays + assertSer(harden([1, 2n]), '#[1,"+2"]', [], 'array'); + + // Records + assertSer(harden({ foo: 1, bar: 2n }), '#{"bar":"+2","foo":1}', [], 'record'); + + // Tagged + const tagged = makeTagged('foo', 'bar'); + assertSer(tagged, '#{"#tag":"foo","payload":"bar"}', [], 'tagged'); + + // Error + const err = harden(URIError('bad uri')); + + // non-passable errors alone still serialize + assertSer(err, '#{"#error":"bad uri","name":"URIError"}', [], 'error'); + const nonPassableErr = Error('foo'); + // @ts-expect-error this type error is what we're testing + nonPassableErr.extraProperty = 'something bad'; + harden(nonPassableErr); + t.throws(() => passStyleOf(nonPassableErr), { + message: + 'Passed Error has extra unpassed properties {"extraProperty":{"value":"something bad","writable":false,"enumerable":true,"configurable":false}}', + }); + assertSer( + nonPassableErr, + '#{"#error":"foo","name":"Error"}', + [], + 'non passable errors pass', + ); + + // Hilbert record + assertSer( + harden({ + '#tag': 'what', + '#error': 'me', + '#huh': 'worry', + '': 'empty', + '%sym': 'not a symbol', + }), + '#{"":"empty","!#error":"me","!#huh":"worry","!#tag":"what","!%sym":"not a symbol"}', + [], + 'hilbert property names', + ); +}); diff --git a/packages/marshal/test/test-marshal-stringify.js b/packages/marshal/test/test-marshal-stringify.js index afdce4d61d..38af7b503a 100644 --- a/packages/marshal/test/test-marshal-stringify.js +++ b/packages/marshal/test/test-marshal-stringify.js @@ -2,7 +2,7 @@ import { test } from './prepare-test-env-ava.js'; import { Far } from '../src/make-far.js'; import { stringify, parse } from '../src/marshal-stringify.js'; -import { roundTripPairs } from './test-marshal.js'; +import { roundTripPairs } from './test-marshal-capdata.js'; const { isFrozen } = Object;