Skip to content

Commit

Permalink
feat(pass-style): generalize passable errors, throwables
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed May 6, 2024
1 parent b09f8c0 commit d06c83b
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 68 deletions.
136 changes: 73 additions & 63 deletions packages/pass-style/src/error.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
/// <reference types="ses"/>

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.
Expand Down Expand Up @@ -62,7 +63,6 @@ const checkErrorLike = (candidate, check = undefined) => {
);
};
harden(checkErrorLike);
/// <reference types="ses"/>

/**
* Validating error objects are passable raises a tension between security
Expand All @@ -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 &&
Expand All @@ -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<string,any>} */ (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({
Expand All @@ -207,5 +216,6 @@ export const ErrorHelper = harden({
canBeValid: checkErrorLike,

assertValid: (candidate, passStyleOfRecur) =>
checkRecursivelyPassableError(candidate, passStyleOfRecur, assertChecker),
checkErrorLike(candidate, assertChecker) &&
checkRecursivelyThrowable(candidate, passStyleOfRecur, assertChecker),
});
8 changes: 4 additions & 4 deletions packages/pass-style/src/passStyleOf.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/pass-style/test/test-errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)]);
Expand Down

0 comments on commit d06c83b

Please sign in to comment.