diff --git a/packages/exo/test/test-legacy-guard-tolerance.js b/packages/exo/test/test-legacy-guard-tolerance.js index d2377a2246..652cc4d1c1 100644 --- a/packages/exo/test/test-legacy-guard-tolerance.js +++ b/packages/exo/test/test-legacy-guard-tolerance.js @@ -49,9 +49,8 @@ test('legacy guard tolerance', async t => { argGuard: 88, }); // @ts-expect-error Legacy adaptor can be ill typed - t.throws(() => getAwaitArgGuardPayload(laag), { - message: - 'awaitArgGuard: copyRecord {"argGuard":88,"klass":"awaitArg"} - Must be a guard:awaitArgGuard', + t.deepEqual(getAwaitArgGuardPayload(laag), { + argGuard: 88, }); t.deepEqual(getMethodGuardPayload(mg1), { @@ -69,9 +68,12 @@ test('legacy guard tolerance', async t => { returnGuard: M.any(), }); // @ts-expect-error Legacy adaptor can be ill typed - t.throws(() => getMethodGuardPayload(lmg), { - message: - 'methodGuard: copyRecord {"argGuards":[77,{"argGuard":88,"klass":"awaitArg"}],"callKind":"async","klass":"methodGuard","returnGuard":"[match:any]"} - Must be a guard:methodGuard', + t.deepEqual(getMethodGuardPayload(lmg), { + callKind: 'async', + argGuards: [77, aag], + optionalArgGuards: undefined, + restArgGuard: undefined, + returnGuard: M.any(), }); t.deepEqual(getInterfaceGuardPayload(ig1), { @@ -97,9 +99,16 @@ test('legacy guard tolerance', async t => { }, ); // @ts-expect-error Legacy adaptor can be ill typed - t.throws(() => getInterfaceGuardPayload(lig), { - message: - 'interfaceGuard: copyRecord {"interfaceName":"Foo","klass":"Interface","methodGuards":{"lmg":{"argGuards":[77,{"argGuard":88,"klass":"awaitArg"}],"callKind":"async","klass":"methodGuard","returnGuard":"[match:any]"},"mg1":"[guard:methodGuard]","mg2":"[guard:methodGuard]"}} - Must be a guard:interfaceGuard', + t.deepEqual(getInterfaceGuardPayload(lig), { + interfaceName: 'Foo', + methodGuards: { + mg1, + mg2, + lmg: M.callWhen(77, M.await(88)) + .optional() + .rest(M.any()) + .returns(M.any()), + }, }); const { meth } = { @@ -121,20 +130,29 @@ test('legacy guard tolerance', async t => { }); t.deepEqual(await f1.mg2(77, laag), [77, laag]); - t.throws( - () => - makeExo( - 'foo', - // @ts-expect-error Legacy adaptor can be ill typed - lig, - { - mg1: meth, - mg2: meth, - }, - ), + const f2 = makeExo( + 'foo', + // @ts-expect-error Legacy adaptor can be ill typed + lig, { - message: - 'interfaceGuard: copyRecord {"interfaceName":"Foo","klass":"Interface","methodGuards":{"lmg":{"argGuards":[77,{"argGuard":88,"klass":"awaitArg"}],"callKind":"async","klass":"methodGuard","returnGuard":"[match:any]"},"mg1":"[guard:methodGuard]","mg2":"[guard:methodGuard]"}} - Must be a guard:interfaceGuard', + mg1: meth, + mg2: meth, + lmg: meth, }, ); + t.deepEqual(await f2.mg1(77, 88), [77, 88]); + await t.throwsAsync(async () => f2.mg1(77, laag), { + message: + 'In "mg1" method of (foo): arg 1: {"argGuard":88,"klass":"awaitArg"} - Must be: 88', + }); + await t.throwsAsync(async () => f2.mg2(77, 88), { + message: + 'In "mg2" method of (foo): arg 1: 88 - Must be: {"argGuard":88,"klass":"awaitArg"}', + }); + t.deepEqual(await f2.mg2(77, laag), [77, laag]); + t.deepEqual(await f2.lmg(77, 88), [77, 88]); + await t.throwsAsync(async () => f2.lmg(77, laag), { + message: + 'In "lmg" method of (foo): arg 1: {"argGuard":88,"klass":"awaitArg"} - Must be: 88', + }); }); diff --git a/packages/patterns/index.js b/packages/patterns/index.js index 9dd42f741e..b32e6ea809 100644 --- a/packages/patterns/index.js +++ b/packages/patterns/index.js @@ -60,17 +60,20 @@ export { mustMatch, isAwaitArgGuard, assertAwaitArgGuard, - getAwaitArgGuardPayload, isRawGuard, assertRawGuard, assertMethodGuard, - getMethodGuardPayload, - getInterfaceMethodKeys, assertInterfaceGuard, - getInterfaceGuardPayload, kindOf, } from './src/patterns/patternMatchers.js'; +export { + getAwaitArgGuardPayload, + getMethodGuardPayload, + getInterfaceGuardPayload, + getInterfaceMethodKeys, +} from './src/patterns/getGuardPayloads.js'; + // eslint-disable-next-line import/export export * from './src/types.js'; diff --git a/packages/patterns/src/patterns/getGuardPayloads.js b/packages/patterns/src/patterns/getGuardPayloads.js new file mode 100644 index 0000000000..9b3d681b90 --- /dev/null +++ b/packages/patterns/src/patterns/getGuardPayloads.js @@ -0,0 +1,281 @@ +import { objectMap } from '@endo/common/object-map.js'; +import { + ArgGuardListShape, + AwaitArgGuardShape, + InterfaceGuardPayloadShape, + InterfaceGuardShape, + M, + MethodGuardPayloadShape, + MethodGuardShape, + RawGuardShape, + SyncValueGuardListShape, + SyncValueGuardShape, + assertAwaitArgGuard, + matches, + mustMatch, +} from './patternMatchers.js'; +import { getCopyMapKeys, makeCopyMap } from '../keys/checkKey.js'; + +// The get*GuardPayload functions exist to adapt to the worlds both +// before and after https://github.com/endojs/endo/pull/1712 . When +// given something that would be the expected guard in either world, +// it returns a *GuardPayload that is valid in the current world. Thus +// it helps new consumers of these guards cope with old code that +// would construct and send these guards. + +// Because the main use case for this legacy adaptation is in @endo/exo +// or packages that depend on it, the tests for this legacy adaptation +// are found in the @endo/exo `test-legacy-guard-tolerance.js`. + +// Unlike LegacyAwaitArgGuardShape, LegacyMethodGuardShape, +// and LegacyInterfaceGuardShape, there is no need for a +// LegacyRawGuardShape, because raw guards were introduced at +// https://github.com/endojs/endo/pull/1831 , which was merged well after +// https://github.com/endojs/endo/pull/1712 . Thus, there was never a +// `klass:` form of the raw guard. + +// TODO At such a time that we decide we no longer need to support code +// preceding https://github.com/endojs/endo/pull/1712 or guard data +// generated by that code, all the adaptation complexity in this file +// should be deleted. + +// TODO manually maintain correspondence with AwaitArgGuardPayloadShape +// because this one needs to be stable and accommodate nested legacy, +// when that's an issue. +const LegacyAwaitArgGuardShape = harden({ + klass: 'awaitArg', + argGuard: M.pattern(), +}); + +/** + * By using this abstraction rather than accessing the properties directly, + * we smooth the transition to https://github.com/endojs/endo/pull/1712, + * tolerating both the legacy and current guard shapes. + * + * Note that technically, tolerating the old LegacyAwaitArgGuardShape + * is an exploitable bug, in that a record that matches this + * shape is also a valid parameter pattern that should allow + * an argument that matches that pattern, i.e., a copyRecord argument that + * at least contains a `klass: 'awaitArgGuard'` property. + * + * @param {import('./types.js').AwaitArgGuard} awaitArgGuard + * @returns {import('./types.js').AwaitArgGuardPayload} + */ +export const getAwaitArgGuardPayload = awaitArgGuard => { + if (matches(awaitArgGuard, LegacyAwaitArgGuardShape)) { + // @ts-expect-error Legacy adaptor can be ill typed + const { klass: _, ...payload } = awaitArgGuard; + // @ts-expect-error Legacy adaptor can be ill typed + return payload; + } + assertAwaitArgGuard(awaitArgGuard); + return awaitArgGuard.payload; +}; +harden(getAwaitArgGuardPayload); + +// TODO manually maintain correspondence with SyncMethodGuardPayloadShape +// because this one needs to be stable and accommodate nested legacy, +// when that's an issue. +const LegacySyncMethodGuardShape = M.splitRecord( + { + klass: 'methodGuard', + callKind: 'sync', + argGuards: SyncValueGuardListShape, + returnGuard: SyncValueGuardShape, + }, + { + optionalArgGuards: SyncValueGuardListShape, + restArgGuard: SyncValueGuardShape, + }, +); + +// TODO manually maintain correspondence with ArgGuardShape +// because this one needs to be stable and accommodate nested legacy, +// when that's an issue. +const LegacyArgGuardShape = M.or( + RawGuardShape, + AwaitArgGuardShape, + LegacyAwaitArgGuardShape, + M.pattern(), +); +// TODO manually maintain correspondence with ArgGuardListShape +// because this one needs to be stable and accommodate nested legacy, +// when that's an issue. +const LegacyArgGuardListShape = M.arrayOf(LegacyArgGuardShape); + +// TODO manually maintain correspondence with AsyncMethodGuardPayloadShape +// because this one needs to be stable and accommodate nested legacy, +// when that's an issue. +const LegacyAsyncMethodGuardShape = M.splitRecord( + { + klass: 'methodGuard', + callKind: 'async', + argGuards: LegacyArgGuardListShape, + returnGuard: SyncValueGuardShape, + }, + { + optionalArgGuards: ArgGuardListShape, + restArgGuard: SyncValueGuardShape, + }, +); + +// TODO manually maintain correspondence with MethodGuardPayloadShape +// because this one needs to be stable and accommodate nested legacy, +// when that's an issue. +const LegacyMethodGuardShape = M.or( + LegacySyncMethodGuardShape, + LegacyAsyncMethodGuardShape, +); + +const adaptLegacyArgGuard = argGuard => + matches(argGuard, LegacyAwaitArgGuardShape) + ? M.await(getAwaitArgGuardPayload(argGuard).argGuard) + : argGuard; + +/** + * By using this abstraction rather than accessing the properties directly, + * we smooth the transition to https://github.com/endojs/endo/pull/1712, + * tolerating both the legacy and current guard shapes. + * + * Unlike LegacyAwaitArgGuardShape, tolerating LegacyMethodGuardShape + * does not seem like a currently exploitable bug, because there is not + * currently any context where either a methodGuard or a copyRecord would + * both be meaningful. + * + * @param {import('./types.js').MethodGuard} methodGuard + * @returns {import('./types.js').MethodGuardPayload} + */ +export const getMethodGuardPayload = methodGuard => { + if (matches(methodGuard, MethodGuardShape)) { + return methodGuard.payload; + } + mustMatch(methodGuard, LegacyMethodGuardShape, 'legacyMethodGuard'); + const { + // @ts-expect-error Legacy adaptor can be ill typed + klass: _, + // @ts-expect-error Legacy adaptor can be ill typed + callKind, + // @ts-expect-error Legacy adaptor can be ill typed + returnGuard, + // @ts-expect-error Legacy adaptor can be ill typed + restArgGuard, + } = methodGuard; + let { + // @ts-expect-error Legacy adaptor can be ill typed + argGuards, + // @ts-expect-error Legacy adaptor can be ill typed + optionalArgGuards, + } = methodGuard; + if (callKind === 'async') { + argGuards = argGuards.map(adaptLegacyArgGuard); + optionalArgGuards = + optionalArgGuards && optionalArgGuards.map(adaptLegacyArgGuard); + } + const payload = harden({ + callKind, + argGuards, + optionalArgGuards, + restArgGuard, + returnGuard, + }); + // ensure the adaptation succeeded. + mustMatch(payload, MethodGuardPayloadShape, 'internalMethodGuardAdaptor'); + return payload; +}; +harden(getMethodGuardPayload); + +// TODO manually maintain correspondence with InterfaceGuardPayloadShape +// because this one needs to be stable and accommodate nested legacy, +// when that's an issue. +const LegacyInterfaceGuardShape = M.splitRecord( + { + klass: 'Interface', + interfaceName: M.string(), + methodGuards: M.recordOf( + M.string(), + M.or(MethodGuardShape, LegacyMethodGuardShape), + ), + }, + { + defaultGuards: M.or(M.undefined(), 'passable', 'raw'), + sloppy: M.boolean(), + // There is no need to accommodate LegacyMethodGuardShape in + // this position, since `symbolMethodGuards happened + // after https://github.com/endojs/endo/pull/1712 + symbolMethodGuards: M.mapOf(M.symbol(), MethodGuardShape), + }, +); + +const adaptMethodGuard = methodGuard => { + if (matches(methodGuard, LegacyMethodGuardShape)) { + const { + callKind, + argGuards, + optionalArgGuards = [], + restArgGuard = M.any(), + returnGuard, + } = getMethodGuardPayload(methodGuard); + const mCall = callKind === 'sync' ? M.call : M.callWhen; + return mCall(...argGuards) + .optional(...optionalArgGuards) + .rest(restArgGuard) + .returns(returnGuard); + } + return methodGuard; +}; + +/** + * By using this abstraction rather than accessing the properties directly, + * we smooth the transition to https://github.com/endojs/endo/pull/1712, + * tolerating both the legacy and current guard shapes. + * + * Unlike LegacyAwaitArgGuardShape, tolerating LegacyInterfaceGuardShape + * does not seem like a currently exploitable bug, because there is not + * currently any context where either an interfaceGuard or a copyRecord would + * both be meaningful. + * + * @template {Record} [T=Record] + * @param {import('./types.js').InterfaceGuard} interfaceGuard + * @returns {import('./types.js').InterfaceGuardPayload} + */ +export const getInterfaceGuardPayload = interfaceGuard => { + if (matches(interfaceGuard, InterfaceGuardShape)) { + return interfaceGuard.payload; + } + mustMatch(interfaceGuard, LegacyInterfaceGuardShape, 'legacyInterfaceGuard'); + // @ts-expect-error Legacy adaptor can be ill typed + // eslint-disable-next-line prefer-const + let { klass: _, interfaceName, methodGuards, ...rest } = interfaceGuard; + methodGuards = objectMap(methodGuards, adaptMethodGuard); + const payload = harden({ + interfaceName, + methodGuards, + ...rest, + }); + mustMatch( + payload, + InterfaceGuardPayloadShape, + 'internalInterfaceGuardAdaptor', + ); + return payload; +}; +harden(getInterfaceGuardPayload); + +const emptyCopyMap = makeCopyMap([]); + +/** + * @param {import('./types.js').InterfaceGuard} interfaceGuard + * @returns {(string | symbol)[]} + */ +export const getInterfaceMethodKeys = interfaceGuard => { + const { methodGuards, symbolMethodGuards = emptyCopyMap } = + getInterfaceGuardPayload(interfaceGuard); + /** @type {(string | symbol)[]} */ + // TODO at-ts-expect-error works locally but not from @endo/exo + // @ts-ignore inference is too weak to see this is ok + return harden([ + ...Reflect.ownKeys(methodGuards), + ...getCopyMapKeys(symbolMethodGuards), + ]); +}; +harden(getInterfaceMethodKeys); diff --git a/packages/patterns/src/patterns/patternMatchers.js b/packages/patterns/src/patterns/patternMatchers.js index aafeb33413..dae54dc48d 100644 --- a/packages/patterns/src/patterns/patternMatchers.js +++ b/packages/patterns/src/patterns/patternMatchers.js @@ -31,7 +31,6 @@ import { checkCopyBag, getCopyMapEntryArray, makeCopyMap, - getCopyMapKeys, } from '../keys/checkKey.js'; import { generateCollectionPairEntries } from '../keys/keycollection-operators.js'; @@ -1738,7 +1737,7 @@ const AwaitArgGuardPayloadShape = harden({ argGuard: M.pattern(), }); -const AwaitArgGuardShape = M.kind('guard:awaitArgGuard'); +export const AwaitArgGuardShape = M.kind('guard:awaitArgGuard'); /** * @param {any} specimen @@ -1757,19 +1756,6 @@ export const assertAwaitArgGuard = specimen => { }; harden(assertAwaitArgGuard); -/** - * By using this abstraction rather than accessing the properties directly, - * we smooth the transition to https://github.com/endojs/endo/pull/1712 - * - * @param {import('./types.js').AwaitArgGuard} awaitArgGuard - * @returns {import('./types.js').AwaitArgGuardPayload} - */ -export const getAwaitArgGuardPayload = awaitArgGuard => { - assertAwaitArgGuard(awaitArgGuard); - return awaitArgGuard.payload; -}; -harden(getAwaitArgGuardPayload); - /** * @param {import('./types.js').Pattern} argPattern * @returns {import('./types.js').AwaitArgGuard} @@ -1787,7 +1773,7 @@ const makeAwaitArgGuard = argPattern => { const RawGuardPayloadShape = M.record(); -const RawGuardShape = M.kind('guard:rawGuard'); +export const RawGuardShape = M.kind('guard:rawGuard'); export const isRawGuard = specimen => matches(specimen, RawGuardShape); @@ -1802,12 +1788,12 @@ const makeRawGuard = () => makeTagged('guard:rawGuard', {}); // M.call(...) // M.callWhen(...) -const SyncValueGuardShape = M.or(RawGuardShape, M.pattern()); +export const SyncValueGuardShape = M.or(RawGuardShape, M.pattern()); -const SyncValueGuardListShape = M.arrayOf(SyncValueGuardShape); +export const SyncValueGuardListShape = M.arrayOf(SyncValueGuardShape); const ArgGuardShape = M.or(RawGuardShape, AwaitArgGuardShape, M.pattern()); -const ArgGuardListShape = M.arrayOf(ArgGuardShape); +export const ArgGuardListShape = M.arrayOf(ArgGuardShape); const SyncMethodGuardPayloadShape = harden({ callKind: 'sync', @@ -1825,12 +1811,12 @@ const AsyncMethodGuardPayloadShape = harden({ returnGuard: SyncValueGuardShape, }); -const MethodGuardPayloadShape = M.or( +export const MethodGuardPayloadShape = M.or( SyncMethodGuardPayloadShape, AsyncMethodGuardPayloadShape, ); -const MethodGuardShape = M.kind('guard:methodGuard'); +export const MethodGuardShape = M.kind('guard:methodGuard'); /** * @param {any} specimen @@ -1841,19 +1827,6 @@ export const assertMethodGuard = specimen => { }; harden(assertMethodGuard); -/** - * By using this abstraction rather than accessing the properties directly, - * we smooth the transition to https://github.com/endojs/endo/pull/1712 - * - * @param {import('./types.js').MethodGuard} methodGuard - * @returns {import('./types.js').MethodGuardPayload} - */ -export const getMethodGuardPayload = methodGuard => { - assertMethodGuard(methodGuard); - return methodGuard.payload; -}; -harden(getMethodGuardPayload); - /** * @param {'sync'|'async'} callKind * @param {import('./types.js').ArgGuard[]} argGuards @@ -1898,7 +1871,7 @@ const makeMethodGuardMaker = ( }, }); -const InterfaceGuardPayloadShape = M.splitRecord( +export const InterfaceGuardPayloadShape = M.splitRecord( { interfaceName: M.string(), methodGuards: M.recordOf(M.string(), MethodGuardShape), @@ -1910,7 +1883,7 @@ const InterfaceGuardPayloadShape = M.splitRecord( }, ); -const InterfaceGuardShape = M.kind('guard:interfaceGuard'); +export const InterfaceGuardShape = M.kind('guard:interfaceGuard'); /** * @param {any} specimen @@ -1921,39 +1894,6 @@ export const assertInterfaceGuard = specimen => { }; harden(assertInterfaceGuard); -/** - * By using this abstraction rather than accessing the properties directly, - * we smooth the transition to https://github.com/endojs/endo/pull/1712 - * - * @template {Record} [T=Record] - * @param {import('./types.js').InterfaceGuard} interfaceGuard - * @returns {import('./types.js').InterfaceGuardPayload} - */ -export const getInterfaceGuardPayload = interfaceGuard => { - assertInterfaceGuard(interfaceGuard); - return interfaceGuard.payload; -}; -harden(getInterfaceGuardPayload); - -const emptyCopyMap = makeCopyMap([]); - -/** - * @param {import('./types.js').InterfaceGuard} interfaceGuard - * @returns {(string | symbol)[]} - */ -export const getInterfaceMethodKeys = interfaceGuard => { - const { methodGuards, symbolMethodGuards = emptyCopyMap } = - getInterfaceGuardPayload(interfaceGuard); - /** @type {(string | symbol)[]} */ - // TODO at-ts-expect-error works locally but not from @endo/exo - // @ts-ignore inference is too weak to see this is ok - return harden([ - ...Reflect.ownKeys(methodGuards), - ...getCopyMapKeys(symbolMethodGuards), - ]); -}; -harden(getInterfaceMethodKeys); - /** * @template {Record} [M = Record] * @param {string} interfaceName