diff --git a/packages/pass-style/src/error.js b/packages/pass-style/src/error.js index 89aa72c0a6..9a65cc9230 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, PassStyleOf} from './types.js' */ +/** @import {Checker, PassStyleOf, 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 {PassStyleOf} 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 && @@ -116,89 +111,88 @@ export const checkRecursivelyPassableErrorPropertyDesc = ( ); } const { value } = desc; - switch (propName) { - case 'message': - case 'stack': { - return ( - typeof value === 'string' || - (reject && - reject`Passable Error ${q( - propName, - )} 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 {PassStyleOf} 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, check)) { + 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 +201,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 1b6f497ba2..13740ca420 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, } from './error.js'; import { RemotableHelper } from './remotable.js'; @@ -264,7 +264,7 @@ harden(isPassable); * @returns {boolean} */ const isPassableErrorPropertyDesc = (name, desc) => - checkRecursivelyPassableErrorPropertyDesc(name, desc, passStyleOf); + checkRecursivelyPassableErrorOwnPropertyDesc(name, desc, passStyleOf); /** * Return a passable error that propagates the diagnostic info of the @@ -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; @@ -309,3 +309,11 @@ export const toPassableError = err => { return newError; }; harden(toPassableError); + +export const toThrowable = candidate => { + harden(candidate); + if (ErrorHelper.canBeValid(candidate)) { + return toPassableError(candidate); + } + throw Fail`TODO oops`; +}