-
Notifications
You must be signed in to change notification settings - Fork 72
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(patterns,exo): Tolerate old guard format (#2038)
- Loading branch information
Showing
4 changed files
with
337 additions
and
95 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
Oops, something went wrong.