From e807308885fa76dcb11a5a9ca5e9e787d9e2dff5 Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Sun, 29 Dec 2024 16:24:13 -0800 Subject: [PATCH] feat(no-trapping-shim): Ponyfill and shim for noTrapping integrity level --- packages/no-trapping-shim/package.json | 1 - .../no-trapping-shim/src/no-trapping-pony.js | 128 ++++++++++++++++++ .../no-trapping-shim/src/no-trapping-shim.js | 11 ++ packages/no-trapping-shim/test/index.test.js | 5 - .../test/no-trapping-pony.test.js | 30 ++++ yarn.lock | 13 ++ 6 files changed, 182 insertions(+), 6 deletions(-) create mode 100644 packages/no-trapping-shim/src/no-trapping-pony.js create mode 100644 packages/no-trapping-shim/src/no-trapping-shim.js delete mode 100644 packages/no-trapping-shim/test/index.test.js create mode 100644 packages/no-trapping-shim/test/no-trapping-pony.test.js diff --git a/packages/no-trapping-shim/package.json b/packages/no-trapping-shim/package.json index c76480f1b2..11b5944f1c 100644 --- a/packages/no-trapping-shim/package.json +++ b/packages/no-trapping-shim/package.json @@ -35,7 +35,6 @@ "test:c8": "c8 $C8_OPTIONS ava --config=ava-nesm.config.js", "test:xs": "exit 0" }, - "dependencies": {}, "devDependencies": { "@endo/lockdown": "workspace:^", "@endo/ses-ava": "workspace:^", diff --git a/packages/no-trapping-shim/src/no-trapping-pony.js b/packages/no-trapping-shim/src/no-trapping-pony.js new file mode 100644 index 0000000000..6a028de943 --- /dev/null +++ b/packages/no-trapping-shim/src/no-trapping-pony.js @@ -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 }; diff --git a/packages/no-trapping-shim/src/no-trapping-shim.js b/packages/no-trapping-shim/src/no-trapping-shim.js new file mode 100644 index 0000000000..c183d3b3a0 --- /dev/null +++ b/packages/no-trapping-shim/src/no-trapping-shim.js @@ -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; diff --git a/packages/no-trapping-shim/test/index.test.js b/packages/no-trapping-shim/test/index.test.js deleted file mode 100644 index bf5a26862c..0000000000 --- a/packages/no-trapping-shim/test/index.test.js +++ /dev/null @@ -1,5 +0,0 @@ -import test from '@endo/ses-ava/prepare-endo.js'; - -test('placeholder', async t => { - t.fail('TODO: add tests'); -}); diff --git a/packages/no-trapping-shim/test/no-trapping-pony.test.js b/packages/no-trapping-shim/test/no-trapping-pony.test.js new file mode 100644 index 0000000000..455fe7d4b9 --- /dev/null +++ b/packages/no-trapping-shim/test/no-trapping-pony.test.js @@ -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); +}); diff --git a/yarn.lock b/yarn.lock index a838a69480..f3ff7316c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -702,6 +702,19 @@ __metadata: languageName: unknown linkType: soft +"@endo/no-trapping-shim@workspace:packages/no-trapping-shim": + version: 0.0.0-use.local + resolution: "@endo/no-trapping-shim@workspace:packages/no-trapping-shim" + dependencies: + "@endo/lockdown": "workspace:^" + "@endo/ses-ava": "workspace:^" + ava: "npm:^6.1.3" + c8: "npm:^7.14.0" + tsd: "npm:^0.31.2" + typescript: "npm:~5.6.3" + languageName: unknown + linkType: soft + "@endo/pass-style@workspace:^, @endo/pass-style@workspace:packages/pass-style": version: 0.0.0-use.local resolution: "@endo/pass-style@workspace:packages/pass-style"