Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(patterns): Tolerate old guard format #2038

Merged
merged 1 commit into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading