-
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
7 changed files
with
286 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,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 }; |
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,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; |
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,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); | ||
}); |
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,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); | ||
}); |
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