From 1e3b7fcfffcb435c2c6b82c7c24e2c75382f3a18 Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Thu, 25 Aug 2022 16:09:12 -0700 Subject: [PATCH] fix: split marshal from its encoder (#1259) --- packages/marshal/index.js | 3 +- packages/marshal/src/encodeToCapData.js | 431 ++++++++++++++++++++++++ packages/marshal/src/marshal-justin.js | 2 +- packages/marshal/src/marshal.js | 377 ++++----------------- 4 files changed, 492 insertions(+), 321 deletions(-) create mode 100644 packages/marshal/src/encodeToCapData.js diff --git a/packages/marshal/index.js b/packages/marshal/index.js index 955dfa0ed1..66feb4e782 100644 --- a/packages/marshal/index.js +++ b/packages/marshal/index.js @@ -22,7 +22,8 @@ export { deeplyFulfilled } from './src/deeplyFulfilled.js'; export { makeTagged } from './src/makeTagged.js'; export { Remotable, Far, ToFarFunction } from './src/make-far.js'; -export { QCLASS, makeMarshal } from './src/marshal.js'; +export { QCLASS } from './src/encodeToCapData.js'; +export { makeMarshal } from './src/marshal.js'; export { stringify, parse } from './src/marshal-stringify.js'; export { decodeToJustin } from './src/marshal-justin.js'; diff --git a/packages/marshal/src/encodeToCapData.js b/packages/marshal/src/encodeToCapData.js new file mode 100644 index 0000000000..47cf3a6326 --- /dev/null +++ b/packages/marshal/src/encodeToCapData.js @@ -0,0 +1,431 @@ +// @ts-check + +/// + +// This module is based on the `encodePassable.js` in `@agoric/store`, +// which may migrate here. The main external difference is that +// `encodePassable` goes directly to string, whereas `encodeToCapData` +// encodes to CapData, a JSON-representable data structure, and leaves it to +// the caller (`marshal.js`) to stringify it. + +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 { + assertPassableSymbol, + nameForPassableSymbol, + passableSymbolForName, +} from './helpers/symbol.js'; + +/** @typedef {import('./types.js').MakeMarshalOptions} MakeMarshalOptions */ +/** @template Slot @typedef {import('./types.js').ConvertSlotToVal} ConvertSlotToVal */ +/** @template Slot @typedef {import('./types.js').ConvertValToSlot} ConvertValToSlot */ +/** @template Slot @typedef {import('./types.js').Serialize} Serialize */ +/** @template Slot @typedef {import('./types.js').Unserialize} Unserialize */ +/** @typedef {import('./types.js').Passable} Passable */ +/** @typedef {import('./types.js').InterfaceSpec} InterfaceSpec */ +/** @typedef {import('./types.js').Encoding} Encoding */ +/** @typedef {import('./types.js').Remotable} Remotable */ +/** @typedef {import('./types.js').EncodingUnion} EncodingUnion */ + +const { ownKeys } = Reflect; +const { isArray } = Array; +const { + getOwnPropertyDescriptors, + defineProperties, + is, + fromEntries, + freeze, +} = Object; +const { details: X, quote: q } = assert; + +/** + * Special property name that indicates an encoding that needs special + * decoding. + */ +const QCLASS = '@qclass'; +export { QCLASS }; + +/** + * @param {Encoding} encoded + * @returns {encoded is EncodingUnion} + */ +const hasQClass = encoded => hasOwnPropertyOf(encoded, QCLASS); + +/** + * @typedef {object} EncodeToCapDataOptions + * @property {(remotable: object) => Encoding} [encodeRemotableToCapData] + * @property {(promise: object) => Encoding} [encodePromiseToCapData] + * @property {(error: object) => Encoding} [encodeErrorToCapData] + */ + +const dontEncodeRemotableToCapData = rem => + assert.fail(X`remotable unexpected: ${rem}`); + +const dontEncodePromiseToCapData = prom => + assert.fail(X`promise unexpected: ${prom}`); + +const dontEncodeErrorToCapData = err => + assert.fail(X`error object unexpected: ${err}`); + +/** + * @param {EncodeToCapDataOptions=} encodeOptions + * @returns {(passable: Passable) => Encoding} + */ +export const makeEncodeToCapData = ({ + encodeRemotableToCapData = dontEncodeRemotableToCapData, + encodePromiseToCapData = dontEncodePromiseToCapData, + encodeErrorToCapData = dontEncodeErrorToCapData, +} = {}) => { + /** + * Must encode `val` into plain JSON data *canonically*, such that + * `JSON.stringify(encode(v1)) === JSON.stringify(encode(v1))`. For most + * encodings, the order of properties of each node of the output + * structure is determined by the algorithm below without special + * arrangement, usually by being expressed directly as an object literal. + * The exception is copyRecords, whose natural enumeration order + * can differ between copyRecords that our distributed object semantics + * considers to be equivalent. + * Since, for each copyRecord, we only accept string property names, + * not symbols, we can canonically sort the names first. + * JSON.stringify will then visit these in that sorted order. + * + * Encoding with a canonical-JSON encoder would also solve this canonicalness + * problem in a more modular and encapsulated manner. Note that the + * actual order produced here, though it agrees with canonical-JSON on + * copyRecord property ordering, differs from canonical-JSON as a whole + * in that the other record properties are visited in the order in which + * they are literally written below. TODO perhaps we should indeed switch + * to a canonical JSON encoder, and not delicatetly depend on the order + * in which these object literals are written. + * + * Readers must not care about this order anyway. We impose this requirement + * mainly to reduce non-determinism exposed outside a vat. + * + * @param {Passable} passable + * @returns {Encoding} except that `encodeToCapData` does not generally + * `harden` this result before returning. Rather, `encodeToCapData` is not + * directly exposed. + * What's exposed instead is a wrapper that freezes the output before + * returning. If this turns out to impede static analysis for `harden` safety, + * we can always put the (now redundant) hardens back in. They don't hurt. + */ + const encodeToCapDataRecur = passable => { + // First we handle all primitives. Some can be represented directly as + // JSON, and some must be encoded as [QCLASS] composites. + const passStyle = passStyleOf(passable); + switch (passStyle) { + case 'null': { + return null; + } + case 'undefined': { + return { [QCLASS]: 'undefined' }; + } + case 'string': + case 'boolean': { + return passable; + } + case 'number': { + if (Number.isNaN(passable)) { + return { [QCLASS]: 'NaN' }; + } + if (is(passable, -0)) { + return 0; + } + if (passable === Infinity) { + return { [QCLASS]: 'Infinity' }; + } + if (passable === -Infinity) { + return { [QCLASS]: '-Infinity' }; + } + return passable; + } + case 'bigint': { + return { + [QCLASS]: 'bigint', + digits: String(passable), + }; + } + case 'symbol': { + assertPassableSymbol(passable); + const name = /** @type {string} */ (nameForPassableSymbol(passable)); + return { + [QCLASS]: 'symbol', + name, + }; + } + case 'copyRecord': { + if (hasOwnPropertyOf(passable, QCLASS)) { + // Hilbert hotel + const { [QCLASS]: qclassValue, ...rest } = passable; + /** @type {Encoding} */ + const result = { + [QCLASS]: 'hilbert', + original: encodeToCapDataRecur(qclassValue), + }; + if (ownKeys(rest).length >= 1) { + // We harden the entire capData encoding before we return it. + // `encodeToCapData` 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 `encodeToCapData`. + result.rest = encodeToCapDataRecur(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, encodeToCapDataRecur(passable[name])]), + ); + } + case 'copyArray': { + return passable.map(encodeToCapDataRecur); + } + case 'tagged': { + return { + [QCLASS]: 'tagged', + tag: getTag(passable), + payload: encodeToCapDataRecur(passable.payload), + }; + } + case 'remotable': { + return encodeRemotableToCapData(passable); + } + case 'error': { + return encodeErrorToCapData(passable); + } + case 'promise': { + return encodePromiseToCapData(passable); + } + default: { + assert.fail(X`unrecognized passStyle ${q(passStyle)}`, TypeError); + } + } + }; + const encodeToCapData = passable => { + if (ErrorHelper.canBeValid(passable)) { + // We pull out this special case to accommodate errors that are not + // valid Passables. For example, because they're not frozen. + // The special case can only ever apply at the root, and therefore + // outside the recursion, since an error could only be deeper in + // a passable structure if it were passable. + // + // We pull out this special case because, for these errors, we're much + // more interested in reporting whatever diagnostic information they + // carry than we are about reporting problems encountered in reporting + // this information. + return harden(encodeErrorToCapData(passable)); + } + return harden(encodeToCapDataRecur(passable)); + }; + return harden(encodeToCapData); +}; +harden(makeEncodeToCapData); + +/** + * @typedef {object} DecodeOptions + * @property {(encodedRemotable: Encoding) => (Promise|Remotable)} [decodeRemotableFromCapData] + * @property {(encodedPromise: Encoding) => (Promise|Remotable)} [decodePromiseFromCapData] + * @property {(encodedError: Encoding) => Error} [decodeErrorFromCapData] + */ + +const dontDecodeRemotableOrPromiseFromCapData = slotEncoding => + assert.fail(X`remotable or promise unexpected: ${slotEncoding}`); +const dontDecodeErrorFromCapData = errorEncoding => + assert.fail(X`error unexpected: ${errorEncoding}`); + +/** + * The current encoding does not give the decoder enough into to distinguish + * whether a slot represents a promise or a remotable. As an implementation + * restriction until this is fixed, if either is provided, both must be + * provided and they must be the same. + * + * This seems like the best starting point to incrementally evolve to an + * API where these can reliably differ. + * See https://github.com/Agoric/agoric-sdk/issues/4334 + * + * @param {DecodeOptions=} decodeOptions + * @returns {(encoded: Encoding) => Passable} + */ +export const makeDecodeFromCapData = ({ + decodeRemotableFromCapData = dontDecodeRemotableOrPromiseFromCapData, + decodePromiseFromCapData = dontDecodeRemotableOrPromiseFromCapData, + decodeErrorFromCapData = dontDecodeErrorFromCapData, +} = {}) => { + assert( + decodeRemotableFromCapData === decodePromiseFromCapData, + X`An implementation restriction for now: If either decodeRemotableFromCapData or decodePromiseFromCapData is provided, both must be provided and they must be the same: ${q( + decodeRemotableFromCapData, + )} vs ${q(decodePromiseFromCapData)}`, + ); + + /** + * `decodeFromCapData` may rely on `jsonEncoded` being the result of a + * plain call to JSON.parse. However, it *cannot* rely on `jsonEncoded` + * having been produced by JSON.stringify on the output of `encodeToCapData` + * above, i.e., `decodeFromCapData` cannot rely on `jsonEncoded` being a + * valid marshalled representation. Rather, `decodeFromCapData` must + * validate that. + * + * @param {Encoding} jsonEncoded must be hardened + */ + const decodeFromCapData = jsonEncoded => { + if (!isObject(jsonEncoded)) { + // primitives pass through + return jsonEncoded; + } + // Assertions of the above to narrow the type. + assert(isObject(jsonEncoded)); + if (hasQClass(jsonEncoded)) { + const qclass = jsonEncoded[QCLASS]; + assert.typeof( + qclass, + 'string', + X`invalid qclass typeof ${q(typeof qclass)}`, + ); + assert(!isArray(jsonEncoded)); + switch (qclass) { + // Encoding of primitives not handled by JSON + case 'undefined': { + return undefined; + } + case 'NaN': { + return NaN; + } + case 'Infinity': { + return Infinity; + } + case '-Infinity': { + return -Infinity; + } + case 'bigint': { + // 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 { digits } = jsonEncoded; + assert.typeof( + digits, + 'string', + X`invalid digits typeof ${q(typeof digits)}`, + ); + return BigInt(digits); + } + case '@@asyncIterator': { + // Deprecated qclass. TODO make conditional + // on environment variable. Eventually remove, but after confident + // that there are no more supported senders. + // + // 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 + return Symbol.asyncIterator; + } + case 'symbol': { + // 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 { name } = jsonEncoded; + return passableSymbolForName(name); + } + + 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 } = jsonEncoded; + return makeTagged(tag, decodeFromCapData(payload)); + } + + case 'error': { + return decodeErrorFromCapData(jsonEncoded); + } + + case 'slot': { + // See note above about how the current encoding cannot reliably + // distinguish which we should call, so in the non-default case + // both must be the same and it doesn't matter which we call. + return decodeRemotableFromCapData(jsonEncoded); + } + + 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 } = jsonEncoded; + assert( + hasOwnPropertyOf(jsonEncoded, 'original'), + X`Invalid Hilbert Hotel encoding ${jsonEncoded}`, + ); + // Don't harden since we're not done mutating it + const result = { [QCLASS]: decodeFromCapData(original) }; + if (hasOwnPropertyOf(jsonEncoded, 'rest')) { + assert( + typeof rest === 'object' && + rest !== null && + ownKeys(rest).length >= 1, + X`Rest encoding must be a non-empty object: ${rest}`, + ); + const restObj = decodeFromCapData(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, QCLASS), + X`Rest must not contain its own definition of ${q(QCLASS)}`, + ); + defineProperties(result, getOwnPropertyDescriptors(restObj)); + } + return result; + } + + default: { + assert( + qclass !== 'ibid', + X`The protocol no longer supports ibid encoding: ${jsonEncoded}.`, + ); + assert.fail(X`unrecognized ${q(QCLASS)} ${q(qclass)}`, TypeError); + } + } + } else if (isArray(jsonEncoded)) { + const result = []; + const { length } = jsonEncoded; + for (let i = 0; i < length; i += 1) { + result[i] = decodeFromCapData(jsonEncoded[i]); + } + return result; + } else { + assert(typeof jsonEncoded === 'object' && jsonEncoded !== null); + const result = {}; + for (const name of ownKeys(jsonEncoded)) { + assert.typeof( + name, + 'string', + X`Property ${name} of ${jsonEncoded} must be a string`, + ); + result[name] = decodeFromCapData(jsonEncoded[name]); + } + return result; + } + }; + return harden(decodeFromCapData); +}; diff --git a/packages/marshal/src/marshal-justin.js b/packages/marshal/src/marshal-justin.js index 28d99297a3..fa482a97b6 100644 --- a/packages/marshal/src/marshal-justin.js +++ b/packages/marshal/src/marshal-justin.js @@ -3,7 +3,7 @@ /// import { Nat } from '@endo/nat'; -import { QCLASS } from './marshal.js'; +import { QCLASS } from './encodeToCapData.js'; import { getErrorConstructor } from './helpers/error.js'; import { isObject } from './helpers/passStyle-helpers.js'; diff --git a/packages/marshal/src/marshal.js b/packages/marshal/src/marshal.js index e2e893a5c8..b3d6138429 100644 --- a/packages/marshal/src/marshal.js +++ b/packages/marshal/src/marshal.js @@ -3,17 +3,15 @@ /// import { Nat } from '@endo/nat'; -import { assertPassable, passStyleOf } from './passStyleOf.js'; +import { assertPassable } from './passStyleOf.js'; import { getInterfaceOf } from './helpers/remotable.js'; -import { ErrorHelper, getErrorConstructor } from './helpers/error.js'; -import { makeTagged } from './makeTagged.js'; -import { isObject, getTag } from './helpers/passStyle-helpers.js'; +import { getErrorConstructor } from './helpers/error.js'; import { - assertPassableSymbol, - nameForPassableSymbol, - passableSymbolForName, -} from './helpers/symbol.js'; + QCLASS, + makeEncodeToCapData, + makeDecodeFromCapData, +} from './encodeToCapData.js'; /** @typedef {import('./types.js').MakeMarshalOptions} MakeMarshalOptions */ /** @template Slot @typedef {import('./types.js').ConvertSlotToVal} ConvertSlotToVal */ @@ -24,24 +22,9 @@ import { /** @typedef {import('./types.js').InterfaceSpec} InterfaceSpec */ /** @typedef {import('./types.js').Encoding} Encoding */ -const { ownKeys } = Reflect; const { isArray } = Array; -const { - getOwnPropertyDescriptors, - defineProperties, - is, - fromEntries, - freeze, -} = Object; const { details: X, quote: q } = assert; -/** - * Special property name that indicates an encoding that needs special - * decoding. - */ -const QCLASS = '@qclass'; -export { QCLASS }; - /** @type {ConvertValToSlot} */ const defaultValToSlotFn = x => x; /** @type {ConvertSlotToVal} */ @@ -129,7 +112,7 @@ export const makeMarshal = ( * @param {Error} err * @returns {Encoding} */ - const encodeError = err => { + const encodeErrorToCapData = err => { // Must encode `cause`, `errors`. // nested non-passable errors must be ok from here. if (errorTagging === 'on') { @@ -158,123 +141,18 @@ export const makeMarshal = ( } }; - /** - * Must encode `val` into plain JSON data *canonically*, such that - * `JSON.stringify(encode(v1)) === JSON.stringify(encode(v1))` - * For each copyRecord, we only accept string property names, - * not symbols. The encoded form the sort - * order of these names must be the same as their enumeration - * order, so a `JSON.stringify` of the encoded form agrees with - * a canonical-json stringify of the encoded form. - * - * @param {Passable} val - * @returns {Encoding} - */ - const encode = val => { - if (ErrorHelper.canBeValid(val)) { - return encodeError(val); - } - // First we handle all primitives. Some can be represented directly as - // JSON, and some must be encoded as [QCLASS] composites. - const passStyle = passStyleOf(val); - switch (passStyle) { - case 'null': { - return null; - } - case 'undefined': { - return harden({ [QCLASS]: 'undefined' }); - } - case 'string': - case 'boolean': { - return val; - } - case 'number': { - if (Number.isNaN(val)) { - return harden({ [QCLASS]: 'NaN' }); - } - if (is(val, -0)) { - return 0; - } - if (val === Infinity) { - return harden({ [QCLASS]: 'Infinity' }); - } - if (val === -Infinity) { - return harden({ [QCLASS]: '-Infinity' }); - } - return val; - } - case 'bigint': { - return harden({ - [QCLASS]: 'bigint', - digits: String(val), - }); - } - case 'symbol': { - assertPassableSymbol(val); - const name = /** @type {string} */ (nameForPassableSymbol(val)); - return harden({ - [QCLASS]: 'symbol', - name, - }); - } - case 'copyRecord': { - if (QCLASS in val) { - // Hilbert hotel - const { [QCLASS]: qclassValue, ...rest } = val; - if (ownKeys(rest).length === 0) { - /** @type {Encoding} */ - const result = harden({ - [QCLASS]: 'hilbert', - original: encode(qclassValue), - }); - return result; - } else { - /** @type {Encoding} */ - const result = harden({ - [QCLASS]: 'hilbert', - original: encode(qclassValue), - // See https://github.com/Agoric/agoric-sdk/issues/4313 - rest: encode(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(val).sort(); - return fromEntries(names.map(name => [name, encode(val[name])])); - } - case 'copyArray': { - return val.map(encode); - } - case 'tagged': { - /** @type {Encoding} */ - const result = harden({ - [QCLASS]: 'tagged', - tag: getTag(val), - payload: encode(val.payload), - }); - return result; - } - case 'remotable': { - const iface = getInterfaceOf(val); - // console.log(`serializeSlot: ${val}`); - return serializeSlot(val, iface); - } - case 'error': { - return encodeError(val); - } - case 'promise': { - // console.log(`serializeSlot: ${val}`); - return serializeSlot(val); - } - default: { - assert.fail(X`unrecognized passStyle ${q(passStyle)}`, TypeError); - } - } + const encodeRemotableToCapData = val => { + const iface = getInterfaceOf(val); + // console.log(`serializeSlot: ${val}`); + return serializeSlot(val, iface); }; + const encode = makeEncodeToCapData({ + encodeRemotableToCapData, + encodePromiseToCapData: serializeSlot, + encodeErrorToCapData, + }); + const encoded = encode(root); return harden({ @@ -299,190 +177,51 @@ export const makeMarshal = ( return val; } - /** - * We stay close to the algorithm at - * https://tc39.github.io/ecma262/#sec-json.parse , where - * fullRevive(harden(JSON.parse(str))) is like JSON.parse(str, revive)) - * for a similar reviver. But with the following differences: - * - * Rather than pass a reviver to JSON.parse, we first call a plain - * (one argument) JSON.parse to get rawTree, and then post-process - * the rawTree with fullRevive. The kind of revive function - * handled by JSON.parse only does one step in post-order, with - * JSON.parse doing the recursion. By contrast, fullParse does its - * own recursion in the same pre-order in which the replacer visited them. - * - * In order to break cycles, the potentially cyclic objects are - * not frozen during the recursion. Rather, the whole graph is - * hardened before being returned. Error objects are not - * potentially recursive, and so may be harmlessly hardened when - * they are produced. - * - * fullRevive can produce properties whose value is undefined, - * which a JSON.parse on a reviver cannot do. If a reviver returns - * undefined to JSON.parse, JSON.parse will delete the property - * instead. - * - * fullRevive creates and returns a new graph, rather than - * modifying the original tree in place. - * - * fullRevive may rely on rawTree being the result of a plain call - * to JSON.parse. However, it *cannot* rely on it having been - * produced by JSON.stringify on the replacer above, i.e., it - * cannot rely on it being a valid marshalled - * representation. Rather, fullRevive must validate that. - * - * @param {Encoding} rawTree must be hardened - */ - function fullRevive(rawTree) { - if (!isObject(rawTree)) { - // primitives pass through - return rawTree; - } - // Assertions of the above to narrow the type. - assert.typeof(rawTree, 'object'); - assert(rawTree !== null); - if (QCLASS in rawTree) { - const qclass = rawTree[QCLASS]; - assert.typeof( - qclass, - 'string', - X`invalid qclass typeof ${q(typeof qclass)}`, - ); - assert(!isArray(rawTree)); - // Switching on `rawTree[QCLASS]` (or anything less direct, like - // `qclass`) does not discriminate rawTree in typescript@4.2.3 and - // earlier. - switch (rawTree['@qclass']) { - // Encoding of primitives not handled by JSON - case 'undefined': { - return undefined; - } - case 'NaN': { - return NaN; - } - case 'Infinity': { - return Infinity; - } - case '-Infinity': { - return -Infinity; - } - case 'bigint': { - const { digits } = rawTree; - assert.typeof( - digits, - 'string', - X`invalid digits typeof ${q(typeof digits)}`, - ); - return BigInt(digits); - } - case '@@asyncIterator': { - // Deprectated qclass. TODO make conditional - // on environment variable. Eventually remove, but after confident - // that there are no more supported senders. - return Symbol.asyncIterator; - } - case 'symbol': { - const { name } = rawTree; - return passableSymbolForName(name); - } - - case 'tagged': { - const { tag, payload } = rawTree; - return makeTagged(tag, fullRevive(payload)); - } - - case 'error': { - // Must decode `cause` and `errors` properties - const { name, message, errorId } = rawTree; - assert.typeof( - name, - 'string', - X`invalid error name typeof ${q(typeof name)}`, - ); - assert.typeof( - message, - 'string', - X`invalid error message typeof ${q(typeof message)}`, - ); - const EC = getErrorConstructor(`${name}`) || Error; - // errorId is a late addition so be tolerant of its absence. - const errorName = - errorId === undefined - ? `Remote${EC.name}` - : `Remote${EC.name}(${errorId})`; - // Due to a defect in the SES type definition, the next line is - // fails a type check. - // Pending https://github.com/endojs/endo/issues/977 - // @ts-ignore-next-line - const error = assert.error(`${message}`, EC, { errorName }); - return error; - } - - case 'slot': { - const { index, iface } = rawTree; - const val = unserializeSlot(index, iface); - return val; - } + const decodeErrorFromCapData = rawTree => { + // Must decode `cause` and `errors` properties + const { name, message, errorId } = rawTree; + assert.typeof( + name, + 'string', + X`invalid error name typeof ${q(typeof name)}`, + ); + assert.typeof( + message, + 'string', + X`invalid error message typeof ${q(typeof message)}`, + ); + const EC = getErrorConstructor(`${name}`) || Error; + // errorId is a late addition so be tolerant of its absence. + const errorName = + errorId === undefined + ? `Remote${EC.name}` + : `Remote${EC.name}(${errorId})`; + // Due to a defect in the SES type definition, the next line is + // fails a type check. + // Pending https://github.com/endojs/endo/issues/977 + // @ts-ignore-next-line + const error = assert.error(`${message}`, EC, { errorName }); + return error; + }; - case 'hilbert': { - const { original, rest } = rawTree; - assert( - 'original' in rawTree, - X`Invalid Hilbert Hotel encoding ${rawTree}`, - ); - // Don't harden since we're not done mutating it - const result = { [QCLASS]: fullRevive(original) }; - if ('rest' in rawTree) { - assert( - rest !== undefined, - X`Rest encoding must not be undefined`, - ); - const restObj = fullRevive(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( - !(QCLASS in restObj), - X`Rest must not contain its own definition of ${q(QCLASS)}`, - ); - defineProperties(result, getOwnPropertyDescriptors(restObj)); - } - return result; - } + // The current encoding does not give the decoder enough into to distinguish + // whether a slot represents a promise or a remotable. As an implementation + // restriction until this is fixed, if either is provided, both must be + // provided and they must be the same. + // See https://github.com/Agoric/agoric-sdk/issues/4334 + const decodeRemotableOrPromiseFromCapData = rawTree => { + const { index, iface } = rawTree; + const val = unserializeSlot(index, iface); + return val; + }; - default: { - assert( - // @ts-expect-error exhaustive check should make condition true - qclass !== 'ibid', - X`The protocol no longer supports ibid encoding: ${rawTree}.`, - ); - assert.fail(X`unrecognized ${q(QCLASS)} ${q(qclass)}`, TypeError); - } - } - } else if (isArray(rawTree)) { - const result = []; - const { length } = rawTree; - for (let i = 0; i < length; i += 1) { - result[i] = fullRevive(rawTree[i]); - } - return result; - } else { - const result = {}; - for (const name of ownKeys(rawTree)) { - assert.typeof( - name, - 'string', - X`Property ${name} of ${rawTree} must be a string`, - ); - result[name] = fullRevive(rawTree[name]); - } - return result; - } - } + const fullRevive = makeDecodeFromCapData({ + decodeRemotableFromCapData: decodeRemotableOrPromiseFromCapData, + decodePromiseFromCapData: decodeRemotableOrPromiseFromCapData, + decodeErrorFromCapData, + }); return fullRevive; }; - /** * @type {Unserialize} */