Skip to content

Commit

Permalink
feat(no-trapping-shim): Ponyfill and shim for noTrapping integrity level
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Dec 30, 2024
1 parent 215eaf4 commit e807308
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 6 deletions.
1 change: 0 additions & 1 deletion packages/no-trapping-shim/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:^",
Expand Down
128 changes: 128 additions & 0 deletions packages/no-trapping-shim/src/no-trapping-pony.js
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 };
11 changes: 11 additions & 0 deletions packages/no-trapping-shim/src/no-trapping-shim.js
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;
5 changes: 0 additions & 5 deletions packages/no-trapping-shim/test/index.test.js

This file was deleted.

30 changes: 30 additions & 0 deletions packages/no-trapping-shim/test/no-trapping-pony.test.js
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);
});
13 changes: 13 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit e807308

Please sign in to comment.