From 63e87ecb0cfff30ccf00e5f6ae60b2869e899ad2 Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Wed, 17 Apr 2024 16:55:18 -0700 Subject: [PATCH] feat(pass-style): generalize passable errors, throwables --- packages/pass-style/src/error.js | 143 +++++++++++++----------- packages/pass-style/src/passStyleOf.js | 8 +- packages/pass-style/test/errors.test.js | 2 +- 3 files changed, 80 insertions(+), 73 deletions(-) diff --git a/packages/pass-style/src/error.js b/packages/pass-style/src/error.js index 969f256bcf..3855249a8f 100644 --- a/packages/pass-style/src/error.js +++ b/packages/pass-style/src/error.js @@ -1,12 +1,13 @@ /// import { q } from '@endo/errors'; -import { assertChecker, CX } from './passStyle-helpers.js'; +import { assertChecker, isObject, CX } 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. @@ -69,7 +70,6 @@ const checkErrorLike = (candidate, check = undefined) => { ); }; harden(checkErrorLike); -/// /** * Validating error objects are passable raises a tension between security @@ -93,26 +93,21 @@ 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, ) => { - if (desc.enumerable) { - return ( - !!check && - CX(check)`Passable Error ${q( - propName, - )} own property must not be enumerable: ${desc}` - ); - } if (!hasOwn(desc, 'value')) { return ( !!check && @@ -133,84 +128,95 @@ export const checkRecursivelyPassableErrorPropertyDesc = ( )} own property must be a string: ${value}`) ); } - case 'cause': - case 'error': - case 'suppressed': { - // eslint-disable-next-line no-use-before-define - return checkRecursivelyPassableError(value, passStyleOfRecur, check); - } - case 'errors': { - if (!Array.isArray(value) || passStyleOfRecur(value) !== 'copyArray') { - return ( - !!check && - CX(check)`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 ( - !!check && - CX(check)`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, ) => { - if (!checkErrorLike(candidate, check)) { - return false; - } - const proto = getPrototypeOf(candidate); - const { name } = proto; - const errConstructor = getErrorConstructor(name); - if (errConstructor === undefined || errConstructor.prototype !== proto) { - return ( - !!check && - CX( + if (checkErrorLike(candidate, undefined)) { + const proto = getPrototypeOf(candidate); + const { name } = proto; + const errConstructor = getErrorConstructor(name); + if (errConstructor === undefined || errConstructor.prototype !== proto) { + return ( + !!check && + CX( + check, + )`Passable Error must inherit from an error class .prototype: ${candidate}` + ); + } + const descs = getOwnPropertyDescriptors(candidate); + if (!('message' in descs)) { + return ( + !!check && + CX( + check, + )`Passable Error must have an own "message" string property: ${candidate}` + ); + } + + return entries(descs).every(([propName, desc]) => + checkRecursivelyPassableErrorOwnPropertyDesc( + propName, + desc, + passStyleOfRecur, check, - )`Passable Error must inherit from an error class .prototype: ${candidate}` + ), ); } - const descs = getOwnPropertyDescriptors(candidate); - if (!('message' in descs)) { - return ( - !!check && - CX( + 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, - )`Passable Error must have an own "message" string property: ${candidate}` - ); + ); + } + default: { + return ( + !!check && + CX(check)`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({ @@ -219,5 +225,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 ae9eb40152..5b6b6ec21c 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/errors.test.js b/packages/pass-style/test/errors.test.js index c94e3a67b6..98216c0e1e 100644 --- a/packages/pass-style/test/errors.test.js +++ b/packages/pass-style/test/errors.test.js @@ -76,7 +76,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)]);