Skip to content

Commit

Permalink
feat(defendSyncMethod): implement raw exo methods
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelfig committed Oct 18, 2023
1 parent c9d9f50 commit 8109f8d
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 27 deletions.
155 changes: 129 additions & 26 deletions packages/exo/src/exo-tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import {
mustMatch,
M,
isAwaitArgGuard,
isRawValueGuard,
getAwaitArgGuardPayload,
getMethodGuardPayload,
getInterfaceGuardPayload,
getCopyMapEntries,
// isRawValueGuard,
} from '@endo/patterns';

/** @typedef {import('@endo/patterns').Method} Method */
Expand All @@ -30,7 +30,9 @@ const { defineProperties, fromEntries } = Object;
* A method guard, for inclusion in an interface guard, that does not
* enforce any constraints of incoming arguments or return results.
*/
// const RawMethodGuard = M.call().rest(M.rawValue()).returns(M.rawValue());
const RawMethodGuard = M.call().rest(M.rawValue()).returns(M.rawValue());

const REDACTED_RAW_VALUE = '<redacted rawValue>';

/**
* A method guard, for inclusion in an interface guard, that enforces only that
Expand All @@ -39,35 +41,120 @@ const { defineProperties, fromEntries } = Object;
* enforcement for a method guard, and is implied by all other
* non-raw method guards.
*/
const MinMethodGuard = M.call().rest(M.any()).returns(M.any());
const PassableMethodGuard = M.call().rest(M.any()).returns(M.any());

/**
* @typedef {object} RedactConfig
* @property {number} declaredLen
* @property {boolean} hasRestArgGuard
* @property {boolean} needRedaction
* @property {Pattern} paramsPattern
* @property {boolean} rawRestArgs
* @property {number[]} redactedIndices
*/

/**
* @param {Passable[]} syncArgs
* @param {MethodGuardPayload} methodGuardPayload
* @param {RedactConfig} redactConfig
* @param {string} [label]
* @returns {Passable[]} Returns the args that should be passed to the
* raw method
*/
const defendSyncArgs = (syncArgs, methodGuardPayload, label = undefined) => {
const { argGuards, optionalArgGuards, restArgGuard } = methodGuardPayload;
const paramsPattern = M.splitArray(
argGuards,
optionalArgGuards,
restArgGuard,
);
mustMatch(harden(syncArgs), paramsPattern, label);
if (restArgGuard !== undefined) {
const defendSyncArgs = (syncArgs, redactConfig, label = undefined) => {
const {
declaredLen,
hasRestArgGuard,
needRedaction,
paramsPattern,
rawRestArgs,
redactedIndices,
} = redactConfig;

// Only manipulate the original args if our params pattern didn't contain any
// raw values.
let redactedArgs = syncArgs;
if (rawRestArgs) {
// Don't harden the rest args.
redactedArgs = syncArgs.slice(0, Math.min(syncArgs.length, declaredLen));
} else if (needRedaction) {
// Copy the arguments array, avoiding hardening the redacted ones (which are
// trivially matched).
redactedArgs = [...syncArgs];
}

for (const i of redactedIndices) {
if (i >= redactedArgs.length) {
break;
}
redactedArgs[i] = REDACTED_RAW_VALUE;
}

mustMatch(harden(redactedArgs), paramsPattern, label);

if (hasRestArgGuard) {
return syncArgs;
}
const declaredLen =
argGuards.length + (optionalArgGuards ? optionalArgGuards.length : 0);
if (syncArgs.length <= declaredLen) {
return syncArgs;
}
// Ignore extraneous arguments, as a JS function call would do.
return syncArgs.slice(0, declaredLen);
};

/**
* @param {MethodGuardPayload} methodGuardPayload
*/
const redactRawValues = methodGuardPayload => {
const {
argGuards,
optionalArgGuards = [],
restArgGuard,
} = methodGuardPayload;

const rawArgGuards = [...argGuards, ...optionalArgGuards];

let needRedaction = false;

const redactedIndices = [];
for (let i = 0; i < rawArgGuards.length; i += 1) {
if (isRawValueGuard(rawArgGuards[i])) {
rawArgGuards[i] = REDACTED_RAW_VALUE;
redactedIndices.push(i);
needRedaction = true;
}
}

// Always match the redacted args if there is a raw rest that applies.
let rawRestArgGuard = restArgGuard;
let rawRestArgs = false;
if (isRawValueGuard(rawRestArgGuard)) {
rawRestArgGuard = undefined;
rawRestArgs = true;
}
const redactedMethodGuardPayload = harden({
...methodGuardPayload,
argGuards: rawArgGuards.slice(0, argGuards.length),
optionalArgGuards: rawArgGuards.slice(argGuards.length),
restArgGuard: rawRestArgGuard,
});

const paramsPattern = M.splitArray(
redactedMethodGuardPayload.argGuards,
redactedMethodGuardPayload.optionalArgGuards,
redactedMethodGuardPayload.restArgGuard,
);

return harden({
declaredLen: rawArgGuards.length,
hasRestArgGuard: restArgGuard !== undefined,
needRedaction,
paramsPattern,
rawRestArgs,
redactedIndices,
redactedMethodGuardPayload,
});
};

/**
* @param {Method} method
* @param {MethodGuardPayload} methodGuardPayload
Expand All @@ -76,16 +163,15 @@ const defendSyncArgs = (syncArgs, methodGuardPayload, label = undefined) => {
*/
const defendSyncMethod = (method, methodGuardPayload, label) => {
const { returnGuard } = methodGuardPayload;
const isRawReturn = isRawValueGuard(returnGuard);
const redactConfig = redactRawValues(methodGuardPayload);
const { syncMethod } = {
// Note purposeful use of `this` and concise method syntax
syncMethod(...syncArgs) {
const realArgs = defendSyncArgs(
harden(syncArgs),
methodGuardPayload,
label,
);
// Only harden args and return value if not dealing with a raw value guard.
const realArgs = defendSyncArgs(syncArgs, redactConfig, label);
const result = apply(method, this, realArgs);
mustMatch(harden(result), returnGuard, `${label}: result`);
isRawReturn || mustMatch(harden(result), returnGuard, `${label}: result`);
return result;
},
};
Expand Down Expand Up @@ -193,13 +279,15 @@ const defendMethod = (method, methodGuard, label) => {
* @param {CallableFunction} behaviorMethod
* @param {boolean} [thisfulMethods]
* @param {MethodGuard} [methodGuard]
* @param {import('@endo/patterns').DefaultGuardType} [defaultGuards]
*/
const bindMethod = (
methodTag,
contextProvider,
behaviorMethod,
thisfulMethods = false,
methodGuard = undefined,
defaultGuards = 'never',
) => {
assert.typeof(behaviorMethod, 'function');

Expand Down Expand Up @@ -235,12 +323,23 @@ const bindMethod = (
return apply(behaviorMethod, null, [context, ...args]);
},
};
if (!methodGuard && thisfulMethods) {
switch (defaultGuards) {
case 'never':
case 'passable':
methodGuard = PassableMethodGuard;
break;
case 'raw':
methodGuard = RawMethodGuard;
break;
default:
throw Fail`Unrecognized defaultGuards ${q(defaultGuards)}`;
}
}
if (methodGuard) {
method = defendMethod(method, methodGuard, methodTag);
} else if (thisfulMethods) {
// For far classes ensure that inputs and outputs are passable.
method = defendMethod(method, MinMethodGuard, methodTag);
}

defineProperties(method, {
name: { value: methodTag },
length: {
Expand Down Expand Up @@ -303,18 +402,22 @@ export const defendPrototype = (
}
/** @type {Record<PropertyKey, MethodGuard> | undefined} */
let methodGuards;
/** @type {import('@endo/patterns').DefaultGuardType} */
let defaultGuards = 'never';
if (interfaceGuard) {
const {
interfaceName,
methodGuards: mg,
symbolMethodGuards,
defaultGuards,
sloppy,
defaultGuards: dg = sloppy ? 'passable' : defaultGuards,
} = getInterfaceGuardPayload(interfaceGuard);
methodGuards = harden({
...mg,
...(symbolMethodGuards &&
fromEntries(getCopyMapEntries(symbolMethodGuards))),
});
defaultGuards = dg;
{
const methodNames = ownKeys(behaviorMethods);
assert(methodGuards);
Expand All @@ -333,7 +436,6 @@ export const defendPrototype = (
interfaceGuard,
);
}

for (const prop of ownKeys(behaviorMethods)) {
prototype[prop] = bindMethod(
`In ${q(prop)} method of (${tag})`,
Expand All @@ -342,6 +444,7 @@ export const defendPrototype = (
thisfulMethods,
// TODO some tool does not yet understand the `?.[` syntax
methodGuards && methodGuards[prop],
defaultGuards,
);
}

Expand Down
17 changes: 17 additions & 0 deletions packages/exo/test/test-heap-classes.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,23 @@ test('sloppy option', t => {
);
});

const RawGreeterI = M.interface('greeter', {}, { defaultGuards: 'raw' });

test('raw defaultGuards', t => {
const greeter = makeExo('greeter', RawGreeterI, {
sayHello(mutable) {
mutable.x = 3;
return 'hello';
},
});
const mutable = {};
t.is(greeter.sayHello(mutable), 'hello');
t.deepEqual(mutable, { x: 3 });
mutable.y = 4;
t.deepEqual(mutable, { x: 3, y: 4 });
t.deepEqual(greeter[GET_INTERFACE_GUARD](), RawGreeterI);
});

const GreeterI = M.interface('greeter', {
sayHello: M.call().returns('hello'),
});
Expand Down
3 changes: 2 additions & 1 deletion packages/patterns/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,8 @@ export {};
* Omit<T, symbol> & Partial<{ [K in Extract<keyof T, symbol>]: never }>,
* symbolMethodGuards?:
* CopyMap<Extract<keyof T, symbol>, T[Extract<keyof T, symbol>]>,
* defaultGuards: DefaultGuardType,
* defaultGuards?: DefaultGuardType,
* sloppy?: boolean,
* }} InterfaceGuardPayload
*/

Expand Down

0 comments on commit 8109f8d

Please sign in to comment.