-
Notifications
You must be signed in to change notification settings - Fork 215
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: more missing Fars. kill "this" (#3746)
- Loading branch information
Showing
16 changed files
with
410 additions
and
35 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
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
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
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,135 @@ | ||
// @ts-check | ||
|
||
// eslint-disable-next-line spaced-comment | ||
/// <reference types="ses"/> | ||
|
||
import { Far } from './make-far.js'; | ||
import { passStyleOf } from './passStyleOf.js'; | ||
|
||
const { ownKeys, construct, apply } = Reflect; | ||
const { | ||
defineProperty, | ||
defineProperties, | ||
getPrototypeOf, | ||
setPrototypeOf, | ||
} = Object; | ||
|
||
const reservedNames = ['constructor', Symbol.toStringTag]; | ||
const reserved = name => reservedNames.includes(name); | ||
|
||
/** | ||
* @callback Guard | ||
* @param {any} val | ||
* @returns {any} | ||
*/ | ||
|
||
/** @type {Guard} */ | ||
const guard = val => { | ||
harden(val); | ||
passStyleOf(val); // asserts that it is passable | ||
return val; | ||
}; | ||
|
||
/** | ||
* @typedef {Object} Interface | ||
* @property {(name: string | symbol) => Guard} getArgsGuard | ||
* @property {(name: string | symbol) => Guard} getResultGuard | ||
*/ | ||
|
||
/** | ||
* @param {string} farName | ||
* @returns {Interface} | ||
*/ | ||
export const makeInterface = farName => | ||
Far('interface', { | ||
toString: () => farName, | ||
getArgsGuard: _name => guard, | ||
getResultGuard: _name => guard, | ||
}); | ||
harden(makeInterface); | ||
|
||
/** | ||
* Returns a class like rawClass that implements the interface. The interface | ||
* comes first because it is typically a variable name, whereas the | ||
* `rawClass` should be the class definition itself inline, so that there are | ||
* no other references to the original rawClass. | ||
* | ||
* Does surgery on `rawClass` in place so its instances are also instances | ||
* of the returned maker, where all the methods are defensive wrappers of | ||
* the original raw methods | ||
* | ||
* @param {Interface} iface Will be a source of schema for checking the arguments | ||
* going in to each method, and the result being returned. | ||
* @param {Object} rawClass Assuming this is the only reference, afterwards the | ||
* rawClass itself should be inaccessible, encapsulated only within the maker. | ||
* @returns {Object} A function that can be used either as a replacement class | ||
* or as a maker. It can be used with or without `new`, and can be extended | ||
* by subclasses. | ||
*/ | ||
export const impl = (iface, rawClass) => { | ||
const proto = rawClass.prototype; | ||
const instances = new WeakSet(); | ||
const suspectMakerArgs = iface.getArgsGuard('constructor'); | ||
|
||
function maker(...args) { | ||
const instance = construct(rawClass, suspectMakerArgs(args), new.target); | ||
instances.add(instance); | ||
if (new.target === undefined || new.target === maker) { | ||
harden(instance); | ||
} | ||
return instance; | ||
} | ||
setPrototypeOf(maker, getPrototypeOf(rawClass)); | ||
defineProperties(maker, { | ||
prototype: { | ||
// Shares the same proto, so instanceof works. | ||
value: proto, | ||
}, | ||
name: { | ||
value: rawClass.name, | ||
}, | ||
length: { | ||
value: rawClass.length, | ||
}, | ||
}); | ||
|
||
defineProperties(proto, { | ||
[Symbol.toStringTag]: { | ||
value: `Alleged: ${iface}`, | ||
}, | ||
constructor: { | ||
// Points back at the maker as the constructor, hopefully making rawClass, | ||
// the original constructor, inaccessible. | ||
value: maker, | ||
}, | ||
}); | ||
|
||
for (const name of ownKeys(proto)) { | ||
if (!reserved(name)) { | ||
const rawMethod = proto[name]; | ||
if (typeof rawMethod === 'function') { | ||
const suspectArgs = iface.getArgsGuard(name); | ||
const protectResult = iface.getResultGuard(name); | ||
defineProperty(proto, name, { | ||
get() { | ||
assert(instances.has(this)); // fail fast in the getter! | ||
// We return an arrow function so its lexical `this` is | ||
// the dynamic `this` of the getter, which must be an object | ||
// made by this maker! The getter returns a method safely | ||
// bound to the instance, in which the args and result are | ||
// themselves safely insulated. | ||
return (...args) => | ||
protectResult(apply(rawMethod, this, suspectArgs(args))); | ||
}, | ||
}); | ||
} | ||
} | ||
} | ||
// @ts-ignore iface will coerce to the right string. Far will eventually be | ||
// changed to accept an Interface. | ||
// eslint-disable-next-line no-func-assign | ||
maker = Far(iface, maker); | ||
|
||
return maker; | ||
}; | ||
harden(impl); |
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,82 @@ | ||
// @ts-check | ||
|
||
// eslint-disable-next-line spaced-comment | ||
/// <reference types="ses"/> | ||
|
||
import { passStyleOf } from './passStyleOf'; | ||
|
||
const { ownKeys } = Reflect; | ||
const { defineProperty } = Object; | ||
|
||
const insulate = val => { | ||
harden(val); | ||
passStyleOf(val); // asserts that it is passable | ||
return val; | ||
}; | ||
|
||
const reservedNames = ['constructor', 'self', '__perform__']; | ||
const reserved = name => reservedNames.includes(name); | ||
|
||
/** | ||
* As long as we're wrapping the methods of our objects-as-closures anyway, | ||
* can we provide a version that avoids the method-per-instance static | ||
* allocation overhead which | ||
* * still being essentially as safe against this-rebinding attacks | ||
* * still supports the convenience of `instance.meth` to get a method | ||
* bound to the instance. | ||
* However, without using a proxy, we cannot make these methods appear to | ||
* be own properties of the instance, and so cannot support `...` for | ||
* trait composition. | ||
* | ||
* @param {Object} InnerClass | ||
*/ | ||
export const makeFarClass = InnerClass => { | ||
const innerProto = InnerClass.prototype; | ||
|
||
class FarClass { | ||
#inner; | ||
|
||
constructor(...args) { | ||
this.#inner = new InnerClass(...args); | ||
this.#inner.self = this; | ||
} | ||
|
||
// Name puposely suggests meta-level. Safe to expose. | ||
// eslint-disable-next-line no-underscore-dangle | ||
__perform__(name, args) { | ||
this.#inner; // fail fast if inappropriate this | ||
assert(!reserved(name)); | ||
const hardArgs = args.map(arg => insulate(arg)); | ||
// Gives the inner method access to the inner instance for | ||
// accessing its own private state. | ||
const result = this.#inner[name](...hardArgs); | ||
return insulate(result); | ||
} | ||
} | ||
const farProto = FarClass.prototype; | ||
|
||
// TODO I think we can move this loop into the FarClass scope, so | ||
// we can inline __perform__ and get rid of it as a separate method. | ||
for (const name of ownKeys(innerProto)) { | ||
if (!reserved(name)) { | ||
const meth = innerProto[name]; | ||
if (typeof meth === 'function') { | ||
defineProperty(farProto, name, { | ||
get() { | ||
// We return an arrow function so its lexical `this` is | ||
// the dynamic this of the getter, which will normally be | ||
// an object that inherits this access for farProto, i.e., | ||
// typically an instance of this FarClass. | ||
return (...args) => | ||
// eslint-disable-next-line no-underscore-dangle | ||
this.__perform__(name, args); | ||
}, | ||
enumerable: false, | ||
configurable: false, | ||
}); | ||
} | ||
} | ||
} | ||
return FarClass; | ||
}; | ||
harden(makeFarClass); |
Oops, something went wrong.