diff --git a/packages/exo/src/exo-tools.js b/packages/exo/src/exo-tools.js index ed0089170d..6090a6ab3f 100644 --- a/packages/exo/src/exo-tools.js +++ b/packages/exo/src/exo-tools.js @@ -6,11 +6,11 @@ import { mustMatch, M, isAwaitArgGuard, + isRawValueGuard, getAwaitArgGuardPayload, getMethodGuardPayload, getInterfaceGuardPayload, getCopyMapEntries, - // isRawValueGuard, } from '@endo/patterns'; /** @typedef {import('@endo/patterns').Method} Method */ @@ -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 = ''; /** * A method guard, for inclusion in an interface guard, that enforces only that @@ -39,28 +41,59 @@ 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; } @@ -68,6 +101,60 @@ const defendSyncArgs = (syncArgs, methodGuardPayload, label = undefined) => { 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 @@ -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; }, }; @@ -193,6 +279,7 @@ const defendMethod = (method, methodGuard, label) => { * @param {CallableFunction} behaviorMethod * @param {boolean} [thisfulMethods] * @param {MethodGuard} [methodGuard] + * @param {import('@endo/patterns').DefaultGuardType} [defaultGuards] */ const bindMethod = ( methodTag, @@ -200,6 +287,7 @@ const bindMethod = ( behaviorMethod, thisfulMethods = false, methodGuard = undefined, + defaultGuards = 'never', ) => { assert.typeof(behaviorMethod, 'function'); @@ -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: { @@ -303,18 +402,22 @@ export const defendPrototype = ( } /** @type {Record | 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); @@ -333,7 +436,6 @@ export const defendPrototype = ( interfaceGuard, ); } - for (const prop of ownKeys(behaviorMethods)) { prototype[prop] = bindMethod( `In ${q(prop)} method of (${tag})`, @@ -342,6 +444,7 @@ export const defendPrototype = ( thisfulMethods, // TODO some tool does not yet understand the `?.[` syntax methodGuards && methodGuards[prop], + defaultGuards, ); } diff --git a/packages/exo/test/test-heap-classes.js b/packages/exo/test/test-heap-classes.js index 8f5e753a92..c81f650357 100644 --- a/packages/exo/test/test-heap-classes.js +++ b/packages/exo/test/test-heap-classes.js @@ -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'), }); diff --git a/packages/patterns/src/types.js b/packages/patterns/src/types.js index 298be27029..2f875de735 100644 --- a/packages/patterns/src/types.js +++ b/packages/patterns/src/types.js @@ -531,7 +531,8 @@ export {}; * Omit & Partial<{ [K in Extract]: never }>, * symbolMethodGuards?: * CopyMap, T[Extract]>, - * defaultGuards: DefaultGuardType, + * defaultGuards?: DefaultGuardType, + * sloppy?: boolean, * }} InterfaceGuardPayload */