diff --git a/packages/pass-style/src/error.js b/packages/pass-style/src/error.js index d9ce1112df..8d64632d05 100644 --- a/packages/pass-style/src/error.js +++ b/packages/pass-style/src/error.js @@ -1,12 +1,13 @@ /// import { X, q } from '@endo/errors'; -import { assertChecker } from './passStyle-helpers.js'; +import { assertChecker, isObject } from './passStyle-helpers.js'; /** @import {PassStyleHelper} from './internal-types.js' */ -/** @import {Checker, PassStyle, PassStyleOf} from './types.js' */ +/** @import {Checker, PassStyle, CopyTagged, Passable} from './types.js' */ -const { getPrototypeOf, getOwnPropertyDescriptors, hasOwn, entries } = Object; +const { getPrototypeOf, getOwnPropertyDescriptors, hasOwn, entries, values } = + Object; // TODO: Maintenance hazard: Coordinate with the list of errors in the SES // whilelist. @@ -62,7 +63,6 @@ const checkErrorLike = (candidate, check = undefined) => { ); }; harden(checkErrorLike); -/// /** * Validating error objects are passable raises a tension between security @@ -86,27 +86,22 @@ export const isErrorLike = candidate => checkErrorLike(candidate); harden(isErrorLike); /** + * An own property of a passable error must be a data property whose value is + * a throwable value. + * * @param {string} propName * @param {PropertyDescriptor} desc * @param {(val: any) => PassStyle} passStyleOfRecur * @param {Checker} [check] * @returns {boolean} */ -export const checkRecursivelyPassableErrorPropertyDesc = ( +export const checkRecursivelyPassableErrorOwnPropertyDesc = ( propName, desc, passStyleOfRecur, check = undefined, ) => { const reject = !!check && ((T, ...subs) => check(false, X(T, ...subs))); - if (desc.enumerable) { - return ( - reject && - reject`Passable Error ${q( - propName, - )} own property must not be enumerable: ${desc}` - ); - } if (!hasOwn(desc, 'value')) { return ( reject && @@ -127,78 +122,92 @@ export const checkRecursivelyPassableErrorPropertyDesc = ( )} own property must be a string: ${value}`) ); } - case 'cause': { - // eslint-disable-next-line no-use-before-define - return checkRecursivelyPassableError(value, passStyleOfRecur, check); - } - case 'errors': { - if (!Array.isArray(value) || passStyleOfRecur(value) !== 'copyArray') { - return ( - reject && - reject`Passable Error ${q( - propName, - )} own property must be a copyArray: ${value}` - ); - } - return value.every(err => - // eslint-disable-next-line no-use-before-define - checkRecursivelyPassableError(err, passStyleOfRecur, check), - ); - } default: { break; } } - return ( - reject && reject`Passable Error has extra unpassed property ${q(propName)}` - ); + // eslint-disable-next-line no-use-before-define + return checkRecursivelyThrowable(value, passStyleOfRecur, check); }; -harden(checkRecursivelyPassableErrorPropertyDesc); +harden(checkRecursivelyPassableErrorOwnPropertyDesc); /** + * `candidate` is throwable if it contains only data and passable errors. + * * @param {unknown} candidate * @param {(val: any) => PassStyle} passStyleOfRecur * @param {Checker} [check] * @returns {boolean} */ -export const checkRecursivelyPassableError = ( +export const checkRecursivelyThrowable = ( candidate, passStyleOfRecur, check = undefined, ) => { const reject = !!check && ((T, ...subs) => check(false, X(T, ...subs))); - if (!checkErrorLike(candidate, check)) { - return false; - } - const proto = getPrototypeOf(candidate); - const { name } = proto; - const errConstructor = getErrorConstructor(name); - if (errConstructor === undefined || errConstructor.prototype !== proto) { - return ( - reject && - reject`Passable Error must inherit from an error class .prototype: ${candidate}` + if (checkErrorLike(candidate, undefined)) { + const proto = getPrototypeOf(candidate); + const { name } = proto; + const errConstructor = getErrorConstructor(name); + if (errConstructor === undefined || errConstructor.prototype !== proto) { + return ( + reject && + reject`Passable Error must inherit from an error class .prototype: ${candidate}` + ); + } + const descs = getOwnPropertyDescriptors(candidate); + if (!('message' in descs)) { + return ( + reject && + reject`Passable Error must have an own "message" string property: ${candidate}` + ); + } + + return entries(descs).every(([propName, desc]) => + checkRecursivelyPassableErrorOwnPropertyDesc( + propName, + desc, + passStyleOfRecur, + check, + ), ); } - const descs = getOwnPropertyDescriptors(candidate); - if (!('message' in descs)) { - return ( - reject && - reject`Passable Error must have an own "message" string property: ${candidate}` - ); + const passStyle = passStyleOfRecur(candidate); + if (!isObject(candidate)) { + // All passable primitives are throwable + return true; + } + switch (passStyle) { + case 'copyArray': { + return /** @type {Passable[]} */ (candidate).every(element => + checkRecursivelyThrowable(element, passStyleOfRecur, check), + ); + } + case 'copyRecord': { + return values(/** @type {Record} */ (candidate)).every( + value => checkRecursivelyThrowable(value, passStyleOfRecur, check), + ); + } + case 'tagged': { + return checkRecursivelyThrowable( + /** @type {CopyTagged} */ (candidate).payload, + passStyleOfRecur, + check, + ); + } + default: { + return ( + reject && + reject`A throwable cannot contain a ${q(passStyle)}: ${candidate}` + ); + } } - - return entries(descs).every(([propName, desc]) => - checkRecursivelyPassableErrorPropertyDesc( - propName, - desc, - passStyleOfRecur, - check, - ), - ); }; -harden(checkRecursivelyPassableError); +harden(checkRecursivelyThrowable); /** + * A passable error is a throwable error and contains only throwable values. + * * @type {PassStyleHelper} */ export const ErrorHelper = harden({ @@ -207,5 +216,6 @@ export const ErrorHelper = harden({ canBeValid: checkErrorLike, assertValid: (candidate, passStyleOfRecur) => - checkRecursivelyPassableError(candidate, passStyleOfRecur, assertChecker), + checkErrorLike(candidate, assertChecker) && + checkRecursivelyThrowable(candidate, passStyleOfRecur, assertChecker), }); diff --git a/packages/pass-style/src/passStyleOf.js b/packages/pass-style/src/passStyleOf.js index 7c4dd78b2e..cac034adc3 100644 --- a/packages/pass-style/src/passStyleOf.js +++ b/packages/pass-style/src/passStyleOf.js @@ -11,8 +11,8 @@ import { CopyRecordHelper } from './copyRecord.js'; import { TaggedHelper } from './tagged.js'; import { ErrorHelper, - checkRecursivelyPassableErrorPropertyDesc, - checkRecursivelyPassableError, + checkRecursivelyPassableErrorOwnPropertyDesc, + checkRecursivelyThrowable, getErrorConstructor, isErrorLike, } from './error.js'; @@ -260,7 +260,7 @@ harden(isPassable); * @returns {boolean} */ const isPassableErrorPropertyDesc = (name, desc) => - checkRecursivelyPassableErrorPropertyDesc(name, desc, passStyleOf); + checkRecursivelyPassableErrorOwnPropertyDesc(name, desc, passStyleOf); /** * After hardening, if `err` is a passable error, return it. @@ -277,7 +277,7 @@ const isPassableErrorPropertyDesc = (name, desc) => */ export const toPassableError = err => { harden(err); - if (checkRecursivelyPassableError(err, passStyleOf)) { + if (checkRecursivelyThrowable(err, passStyleOf)) { return err; } const { name, message } = err; diff --git a/packages/pass-style/test/test-errors.js b/packages/pass-style/test/test-errors.js index 2c0fd0fcce..cc9b876257 100644 --- a/packages/pass-style/test/test-errors.js +++ b/packages/pass-style/test/test-errors.js @@ -71,7 +71,7 @@ test('toPassableError, toThrowable', t => { // a throwable singleton copyArray containing a toThrowable(e), i.e., // an error like e2. t.throws(() => toThrowable(notYetCoercable), { - message: 'Passable Error has extra unpassed property "foo"', + message: 'A throwable cannot contain a "remotable": "[Alleged: Foo]"', }); const throwable = harden([e2, { e2 }, makeTagged('e2', e2)]);