Skip to content

Commit

Permalink
fix(patterns,exo): Tolerate old guard format (#2038)
Browse files Browse the repository at this point in the history
  • Loading branch information
erights authored Feb 7, 2024
1 parent 9c5eb04 commit d5b31d9
Show file tree
Hide file tree
Showing 4 changed files with 337 additions and 95 deletions.
62 changes: 40 additions & 22 deletions packages/exo/test/test-legacy-guard-tolerance.js
Original file line number Diff line number Diff line change
Expand Up @@ -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), {
Expand All @@ -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), {
Expand All @@ -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 } = {
Expand All @@ -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',
});
});
11 changes: 7 additions & 4 deletions packages/patterns/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
281 changes: 281 additions & 0 deletions packages/patterns/src/patterns/getGuardPayloads.js
Original file line number Diff line number Diff line change
@@ -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<PropertyKey, import('./types.js').MethodGuard>} [T=Record<PropertyKey, import('./types.js').MethodGuard>]
* @param {import('./types.js').InterfaceGuard<T>} interfaceGuard
* @returns {import('./types.js').InterfaceGuardPayload<T>}
*/
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);
Loading

0 comments on commit d5b31d9

Please sign in to comment.