-
Notifications
You must be signed in to change notification settings - Fork 74
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(no-trapping-shim): Ponyfill and shim for noTrapping integrity level
- Loading branch information
Showing
6 changed files
with
182 additions
and
6 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
const OriginalProxy = Proxy; | ||
const { freeze } = Object; | ||
const { apply } = Reflect; | ||
|
||
const noTrappingSet = new WeakSet(); | ||
|
||
const proxyHandlerMap = new WeakMap(); | ||
|
||
/** | ||
* In the shim, this should also be on `Reflect`. | ||
* TODO always return boolean vs sometimes throw | ||
* | ||
* @param {any} specimen | ||
* @returns {boolean} | ||
*/ | ||
export const isNoTrapping = specimen => { | ||
if (noTrappingSet.has(specimen)) { | ||
return true; | ||
} | ||
if (!proxyHandlerMap.has(specimen)) { | ||
return false; | ||
} | ||
const [target, handler] = proxyHandlerMap.get(specimen); | ||
if (isNoTrapping(target)) { | ||
noTrappingSet.add(specimen); | ||
return true; | ||
} | ||
const trap = handler.isNoTrapping; | ||
if (trap === undefined) { | ||
return false; | ||
} | ||
const result = apply(trap, handler, [target]); | ||
const ofTarget = isNoTrapping(target); | ||
if (result !== ofTarget) { | ||
throw TypeError( | ||
`'isNoTrapping' proxy trap does not reflect 'isNoTrapping' of proxy target (which is '${ofTarget}')`, | ||
); | ||
} | ||
if (result) { | ||
noTrappingSet.add(specimen); | ||
} | ||
return result; | ||
}; | ||
|
||
/** | ||
* In the shim, this should also be on `Reflect`. | ||
* TODO always return boolean vs sometimes throw | ||
* | ||
* @param {any} specimen | ||
* @returns {boolean} | ||
*/ | ||
export const suppressTrapping = specimen => { | ||
if (noTrappingSet.has(specimen)) { | ||
return true; | ||
} | ||
freeze(specimen); | ||
if (!proxyHandlerMap.has(specimen)) { | ||
noTrappingSet.add(specimen); | ||
return true; | ||
} | ||
const [target, handler] = proxyHandlerMap.get(specimen); | ||
if (isNoTrapping(target)) { | ||
noTrappingSet.add(specimen); | ||
return true; | ||
} | ||
const trap = handler.suppressTrapping; | ||
if (trap === undefined) { | ||
const result = suppressTrapping(target); | ||
if (result) { | ||
noTrappingSet.add(specimen); | ||
} | ||
return result; | ||
} | ||
const result = apply(trap, handler, [target]); | ||
const ofTarget = isNoTrapping(target); | ||
if (result !== ofTarget) { | ||
throw TypeError( | ||
`'suppressTrapping' proxy trap does not reflect 'isNoTrapping' of proxy target (which is '${ofTarget}')`, | ||
); | ||
} | ||
if (result) { | ||
noTrappingSet.add(specimen); | ||
} | ||
return result; | ||
}; | ||
|
||
const makeMetaHandler = handler => | ||
freeze({ | ||
get(_, trapName, _receiver) { | ||
return freeze((target, ...rest) => { | ||
if (isNoTrapping(target) || handler[trapName] === undefined) { | ||
return Reflect[trapName](target, ...rest); | ||
} else { | ||
return handler[trapName](target, ...rest); | ||
} | ||
}); | ||
}, | ||
}); | ||
|
||
const makeSafeHandler = handler => | ||
new OriginalProxy({}, makeMetaHandler(handler)); | ||
|
||
/** | ||
* In the shim, `SafeProxy` should replace the global `Proxy`. | ||
* | ||
* @param {any} target | ||
* @param {object} handler | ||
*/ | ||
const SafeProxy = function Proxy(target, handler) { | ||
if (new.target !== SafeProxy) { | ||
if (new.target === undefined) { | ||
throw TypeError('Proxy constructor requires "new"'); | ||
} | ||
throw TypeError('Safe Proxy shim does not support subclassing'); | ||
} | ||
const safeHandler = makeSafeHandler(handler); | ||
const proxy = new OriginalProxy(target, safeHandler); | ||
proxyHandlerMap.set(proxy, [target, handler]); | ||
return proxy; | ||
}; | ||
SafeProxy.revocable = (target, handler) => { | ||
const safeHandler = makeSafeHandler(handler); | ||
const { proxy, revoke } = OriginalProxy.revocable(target, safeHandler); | ||
proxyHandlerMap.set(proxy, [target, handler]); | ||
return { proxy, revoke }; | ||
}; | ||
|
||
export { SafeProxy }; |
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,11 @@ | ||
/* global globalThis */ | ||
import { | ||
isNoTrapping, | ||
suppressTrapping, | ||
SafeProxy, | ||
} from './no-trapping-pony.js'; | ||
|
||
Reflect.isNoTrapping = isNoTrapping; | ||
Reflect.suppressTrapping = suppressTrapping; | ||
// @ts-expect-error Something about the type of Proxy? | ||
globalThis.Proxy = SafeProxy; |
This file was deleted.
Oops, something went wrong.
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,30 @@ | ||
import test from '@endo/ses-ava/prepare-endo.js'; | ||
import { | ||
isNoTrapping, | ||
SafeProxy, | ||
suppressTrapping, | ||
} from '../src/no-trapping-pony.js'; | ||
|
||
const { freeze, isFrozen } = Object; | ||
|
||
test('no-trapping-pony', async t => { | ||
const specimen = { foo: 8 }; | ||
|
||
const sillyHandler = freeze({ | ||
get(target, prop, receiver) { | ||
return [target, prop, receiver]; | ||
}, | ||
}); | ||
|
||
const safeProxy = new SafeProxy(specimen, sillyHandler); | ||
|
||
t.false(isNoTrapping(specimen)); | ||
t.false(isFrozen(specimen)); | ||
t.deepEqual(safeProxy.foo, [specimen, 'foo', safeProxy]); | ||
|
||
suppressTrapping(specimen); | ||
|
||
t.true(isNoTrapping(specimen)); | ||
t.true(isFrozen(specimen)); | ||
t.deepEqual(safeProxy.foo, 8); | ||
}); |
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