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 31, 2024
1 parent 215eaf4 commit 8f8d5cf
Show file tree
Hide file tree
Showing 7 changed files with 286 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
206 changes: 206 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,206 @@
const OriginalObject = Object;
const OriginalReflect = Reflect;
const OriginalProxy = Proxy;
const { freeze, defineProperty } = OriginalObject;
const { apply, construct, ownKeys } = OriginalReflect;

const noTrappingSet = new WeakSet();

const proxyHandlerMap = new WeakMap();

const isPrimitive = specimen => OriginalObject(specimen) !== specimen;

/**
* Corresponds to the internal function shared by `Object.isNoTrapping` and
* `Reflect.isNoTrapping`.
*
* @param {any} specimen
* @param {boolean} shouldThrow
* @returns {boolean}
*/
const isNoTrappingInternal = (specimen, shouldThrow) => {
if (noTrappingSet.has(specimen)) {
return true;
}
if (!proxyHandlerMap.has(specimen)) {
return false;
}
const [target, handler] = proxyHandlerMap.get(specimen);
if (isNoTrappingInternal(target, shouldThrow)) {
noTrappingSet.add(specimen);
return true;
}
const trap = handler.isNoTrapping;
if (trap === undefined) {
return false;
}
const result = apply(trap, handler, [target]);
const ofTarget = isNoTrappingInternal(target, shouldThrow);
if (result !== ofTarget) {
if (shouldThrow) {
throw TypeError(
`'isNoTrapping' proxy trap does not reflect 'isNoTrapping' of proxy target (which is '${ofTarget}')`,
);
}
return false;
}
if (result) {
noTrappingSet.add(specimen);
}
return result;
};

/**
* Corresponds to the internal function shared by `Object.suppressTrapping` and
* `Reflect.suppressTrapping`.
*
* @param {any} specimen
* @param {boolean} shouldThrow
* @returns {boolean}
*/
const suppressTrappingInternal = (specimen, shouldThrow) => {
if (noTrappingSet.has(specimen)) {
return true;
}
freeze(specimen);
if (!proxyHandlerMap.has(specimen)) {
noTrappingSet.add(specimen);
return true;
}
const [target, handler] = proxyHandlerMap.get(specimen);
if (isNoTrappingInternal(target, shouldThrow)) {
noTrappingSet.add(specimen);
return true;
}
const trap = handler.suppressTrapping;
if (trap === undefined) {
const result = suppressTrappingInternal(target, shouldThrow);
if (result) {
noTrappingSet.add(specimen);
}
return result;
}
const result = apply(trap, handler, [target]);
const ofTarget = isNoTrappingInternal(target, shouldThrow);
if (result !== ofTarget) {
if (shouldThrow) {
throw TypeError(
`'suppressTrapping' proxy trap does not reflect 'isNoTrapping' of proxy target (which is '${ofTarget}')`,
);
}
return false;
}
if (result) {
noTrappingSet.add(specimen);
}
return result;
};

export const extraReflectMethods = freeze({
isNoTrapping(target) {
if (isPrimitive(target)) {
throw TypeError('Reflect.isNoTrapping called on non-object');
}
return isNoTrappingInternal(target, false);
},
suppressTrapping(target) {
if (isPrimitive(target)) {
throw TypeError('Reflect.suppressTrapping called on non-object');
}
return suppressTrappingInternal(target, false);
},
});

export const extraObjectMethods = freeze({
isNoTrapping(target) {
if (isPrimitive(target)) {
return true;
}
return isNoTrappingInternal(target, true);
},
suppressTrapping(target) {
if (isPrimitive(target)) {
return target;
}
if (suppressTrappingInternal(target, true)) {
return target;
}
throw TypeError('preventExtensions trap returned falsy');
},
});

const addExtras = (base, ...extrasArgs) => {
for (const extras of extrasArgs) {
for (const key of ownKeys(extras)) {
if (base[key] !== extras[key]) {
defineProperty(base, key, {
value: extras[key],
writable: true,
enumerable: false,
configurable: true,
});
}
}
}
};

const ReflectPlus = {};
addExtras(ReflectPlus, OriginalReflect, extraReflectMethods);
export { ReflectPlus };

const ObjectPlus = function Object(...args) {
if (new.target) {
return construct(OriginalObject, args, new.target);
} else {
return apply(OriginalObject, this, args);
}
};
ObjectPlus.prototype = OriginalObject.prototype;
addExtras(ObjectPlus, OriginalObject, extraObjectMethods);
export { ObjectPlus };

const makeMetaHandler = handler =>
freeze({
get(_, trapName, _receiver) {
return freeze((target, ...rest) => {
if (
isNoTrappingInternal(target, true) ||
handler[trapName] === undefined
) {
return ReflectPlus[trapName](target, ...rest);
} else {
return handler[trapName](target, ...rest);
}
});
},
});

const makeSafeHandler = handler =>
new OriginalProxy({}, makeMetaHandler(handler));

/**
* In the shim, `ProxyPlus` should replace the global `Proxy`.
*
* @param {any} target
* @param {object} handler
*/
const ProxyPlus = function Proxy(target, handler) {
if (new.target !== ProxyPlus) {
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;
};
ProxyPlus.revocable = (target, handler) => {
const safeHandler = makeSafeHandler(handler);
const { proxy, revoke } = OriginalProxy.revocable(target, safeHandler);
proxyHandlerMap.set(proxy, [target, handler]);
return { proxy, revoke };
};

export { ProxyPlus };
12 changes: 12 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,12 @@
/* global globalThis */
import { ReflectPlus, ObjectPlus, ProxyPlus } from './no-trapping-pony.js';

globalThis.Reflect = ReflectPlus;

// @ts-expect-error Something about the type of the Object constructor
globalThis.Object = ObjectPlus;
// eslint-disable-next-line no-extend-native
Object.prototype.constructor = ObjectPlus;

// @ts-expect-error Something about the type of Proxy
globalThis.Proxy = ProxyPlus;
5 changes: 0 additions & 5 deletions packages/no-trapping-shim/test/index.test.js

This file was deleted.

26 changes: 26 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,26 @@
import test from '@endo/ses-ava/prepare-endo.js';
import { ReflectPlus, ProxyPlus } 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 ProxyPlus(specimen, sillyHandler);

t.false(ReflectPlus.isNoTrapping(specimen));
t.false(isFrozen(specimen));
t.deepEqual(safeProxy.foo, [specimen, 'foo', safeProxy]);

t.true(ReflectPlus.suppressTrapping(specimen));

t.true(ReflectPlus.isNoTrapping(specimen));
t.true(isFrozen(specimen));
t.deepEqual(safeProxy.foo, 8);
});
29 changes: 29 additions & 0 deletions packages/no-trapping-shim/test/no-trapping-shim.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import '../src/no-trapping-shim.js';
// TODO make compat with ses and uncomment
// import test from '@endo/ses-ava/prepare-endo.js';
// instead of
import test from 'ava';

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 Proxy(specimen, sillyHandler);

t.false(Reflect.isNoTrapping(specimen));
t.false(isFrozen(specimen));
t.deepEqual(safeProxy.foo, [specimen, 'foo', safeProxy]);

t.true(Reflect.suppressTrapping(specimen));

t.true(Reflect.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 8f8d5cf

Please sign in to comment.