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}
*/