Skip to content

Commit

Permalink
refactor(ses): Address all polymorphic call sites
Browse files Browse the repository at this point in the history
  • Loading branch information
kriskowal committed Aug 28, 2021
1 parent 03e8c5f commit 21a6390
Show file tree
Hide file tree
Showing 25 changed files with 473 additions and 296 deletions.
2 changes: 1 addition & 1 deletion packages/ses/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@
"parseInt",
"unescape"
],
"@endo/no-polymorphic-call": "warn"
"@endo/no-polymorphic-call": "error"
},
"overrides": [
{
Expand Down
31 changes: 25 additions & 6 deletions packages/ses/src/commons.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export const defineProperty = (object, prop, descriptor) => {
const result = originalDefineProperty(object, prop, descriptor);
if (result !== object) {
throw TypeError(
`Please report that the original defineProperty silently failed to set ${JSON.stringify(
`Please report that the original defineProperty silently failed to set ${stringifyJson(
String(prop),
)}. (SES_DEFINE_PROPERTY_FAILED_SILENTLY)`,
);
Expand Down Expand Up @@ -131,6 +131,7 @@ export const { prototype: stringPrototype } = String;
export const { prototype: weakmapPrototype } = WeakMap;
export const { prototype: weaksetPrototype } = WeakSet;
export const { prototype: functionPrototype } = Function;
export const { prototype: promisePrototype } = Promise;

/**
* uncurryThis()
Expand All @@ -151,40 +152,58 @@ export const uncurryThis = fn => (thisArg, ...args) => apply(fn, thisArg, args);

export const objectHasOwnProperty = uncurryThis(objectPrototype.hasOwnProperty);
//
export const arrayForEach = uncurryThis(arrayPrototype.forEach);
export const arrayFilter = uncurryThis(arrayPrototype.filter);
export const arrayForEach = uncurryThis(arrayPrototype.forEach);
export const arrayIncludes = uncurryThis(arrayPrototype.includes);
export const arrayJoin = uncurryThis(arrayPrototype.join);
export const arrayPush = uncurryThis(arrayPrototype.push);
export const arrayMap = uncurryThis(arrayPrototype.map);
export const arrayPop = uncurryThis(arrayPrototype.pop);
export const arrayIncludes = uncurryThis(arrayPrototype.includes);
export const arrayPush = uncurryThis(arrayPrototype.push);
export const arraySlice = uncurryThis(arrayPrototype.slice);
export const arraySome = uncurryThis(arrayPrototype.some);
export const arraySort = uncurryThis(arrayPrototype.sort);
export const iterateArray = uncurryThis(arrayPrototype[iteratorSymbol]);
//
export const mapSet = uncurryThis(mapPrototype.set);
export const mapGet = uncurryThis(mapPrototype.get);
export const mapHas = uncurryThis(mapPrototype.has);
export const iterateMap = uncurryThis(mapPrototype[iteratorSymbol]);
//
export const setAdd = uncurryThis(setPrototype.add);
export const setForEach = uncurryThis(setPrototype.forEach);
export const setHas = uncurryThis(setPrototype.has);
export const iterateSet = uncurryThis(setPrototype[iteratorSymbol]);
//
export const regexpTest = uncurryThis(regexpPrototype.test);
export const regexpExec = uncurryThis(regexpPrototype.exec);
export const matchAllRegExp = uncurryThis(regexpPrototype[matchAllSymbol]);
//
export const stringEndsWith = uncurryThis(stringPrototype.endsWith);
export const stringIncludes = uncurryThis(stringPrototype.includes);
export const stringIndexOf = uncurryThis(stringPrototype.indexOf);
export const stringMatch = uncurryThis(stringPrototype.match);
export const stringReplace = uncurryThis(stringPrototype.replace);
export const stringSearch = uncurryThis(stringPrototype.search);
export const stringSlice = uncurryThis(stringPrototype.slice);
export const stringSplit = uncurryThis(stringPrototype.split);
export const stringStartsWith = uncurryThis(stringPrototype.startsWith);
export const iterateString = uncurryThis(stringPrototype[iteratorSymbol]);
//
export const weakmapDelete = uncurryThis(weakmapPrototype.delete);
export const weakmapGet = uncurryThis(weakmapPrototype.get);
export const weakmapSet = uncurryThis(weakmapPrototype.set);
export const weakmapHas = uncurryThis(weakmapPrototype.has);
export const weakmapSet = uncurryThis(weakmapPrototype.set);
//
export const weaksetAdd = uncurryThis(weaksetPrototype.add);
export const weaksetSet = uncurryThis(weaksetPrototype.set);
export const weaksetGet = uncurryThis(weaksetPrototype.get);
export const weaksetHas = uncurryThis(weaksetPrototype.has);
//
export const functionToString = uncurryThis(functionPrototype.toString);
//
const { all } = Promise;
export const promiseAll = promises => apply(all, Promise, [promises]);
export const promiseCatch = uncurryThis(promisePrototype.catch);
export const promiseThen = uncurryThis(promisePrototype.then);

/**
* getConstructorOf()
Expand Down
72 changes: 72 additions & 0 deletions packages/ses/src/compartment-evaluate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/// <reference types="ses">
import {
TypeError,
arrayPush,
create,
defineProperties,
getOwnPropertyDescriptors,
} from './commons.js';
import {
evadeHtmlCommentTest,
evadeImportExpressionTest,
rejectSomeDirectEvalExpressions,
} from './transforms.js';
import { performEval } from './evaluate.js';

export const compartmentEvaluate = (compartmentFields, source, options) => {
// Perform this check first to avoid unecessary sanitizing.
// TODO Maybe relax string check and coerce instead:
// https://github.com/tc39/proposal-dynamic-code-brand-checks
if (typeof source !== 'string') {
throw new TypeError('first argument of evaluate() must be a string');
}

// Extract options, and shallow-clone transforms.
const {
transforms = [],
sloppyGlobalsMode = false,
__moduleShimLexicals__ = undefined,
__evadeHtmlCommentTest__ = false,
__evadeImportExpressionTest__ = false,
__rejectSomeDirectEvalExpressions__ = true, // Note default on
} = options;
const localTransforms = [...transforms];
if (__evadeHtmlCommentTest__ === true) {
arrayPush(localTransforms, evadeHtmlCommentTest);
}
if (__evadeImportExpressionTest__ === true) {
arrayPush(localTransforms, evadeImportExpressionTest);
}
if (__rejectSomeDirectEvalExpressions__ === true) {
arrayPush(localTransforms, rejectSomeDirectEvalExpressions);
}

let { globalTransforms } = compartmentFields;
const { globalObject, globalLexicals, knownScopeProxies } = compartmentFields;

let localObject = globalLexicals;
if (__moduleShimLexicals__ !== undefined) {
// When using `evaluate` for ESM modules, as should only occur from the
// module-shim's module-instance.js, we do not reveal the SES-shim's
// module-to-program translation, as this is not standardizable behavior.
// However, the `localTransforms` will come from the `__shimTransforms__`
// Compartment option in this case, which is a non-standardizable escape
// hatch so programs designed specifically for the SES-shim
// implementation may opt-in to use the same transforms for `evaluate`
// and `import`, at the expense of being tightly coupled to SES-shim.
globalTransforms = undefined;

localObject = create(null, getOwnPropertyDescriptors(globalLexicals));
defineProperties(
localObject,
getOwnPropertyDescriptors(__moduleShimLexicals__),
);
}

return performEval(source, globalObject, localObject, {
globalTransforms,
localTransforms,
sloppyGlobalsMode,
knownScopeProxies,
});
};
112 changes: 34 additions & 78 deletions packages/ses/src/compartment-shim.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,26 @@ import {
TypeError,
WeakMap,
WeakSet,
arrayFilter,
arrayJoin,
assign,
create,
defineProperties,
entries,
freeze,
getOwnPropertyDescriptors,
getOwnPropertyNames,
promiseThen,
weakmapGet,
weakmapSet,
weaksetHas,
} from './commons.js';
import { initGlobalObject } from './global-object.js';
import { performEval } from './evaluate.js';
import { isValidIdentifierName } from './scope-constants.js';
import { sharedGlobalPropertyNames } from './whitelist.js';
import {
evadeHtmlCommentTest,
evadeImportExpressionTest,
rejectSomeDirectEvalExpressions,
} from './transforms.js';
import { load } from './module-load.js';
import { link } from './module-link.js';
import { getDeferredExports } from './module-proxy.js';
import { assert } from './error/assert.js';
import { compartmentEvaluate } from './compartment-evaluate.js';

const { quote: q } = assert;

Expand Down Expand Up @@ -74,6 +70,21 @@ export const InertCompartment = function Compartment(
);
};

/**
* @param {Compartment} compartment
* @param {string} specifier
*/
const compartmentImportNow = (compartment, specifier) => {
const { execute, exportsProxy } = link(
privateFields,
moduleAliases,
compartment,
specifier,
);
execute();
return exportsProxy;
};

export const CompartmentPrototype = {
constructor: InertCompartment,

Expand All @@ -96,66 +107,8 @@ export const CompartmentPrototype = {
* @param {boolean} [options.__rejectSomeDirectEvalExpressions__]
*/
evaluate(source, options = {}) {
// Perform this check first to avoid unecessary sanitizing.
// TODO Maybe relax string check and coerce instead:
// https://github.com/tc39/proposal-dynamic-code-brand-checks
if (typeof source !== 'string') {
throw new TypeError('first argument of evaluate() must be a string');
}

// Extract options, and shallow-clone transforms.
const {
transforms = [],
sloppyGlobalsMode = false,
__moduleShimLexicals__ = undefined,
__evadeHtmlCommentTest__ = false,
__evadeImportExpressionTest__ = false,
__rejectSomeDirectEvalExpressions__ = true, // Note default on
} = options;
const localTransforms = [...transforms];
if (__evadeHtmlCommentTest__ === true) {
localTransforms.push(evadeHtmlCommentTest);
}
if (__evadeImportExpressionTest__ === true) {
localTransforms.push(evadeImportExpressionTest);
}
if (__rejectSomeDirectEvalExpressions__ === true) {
localTransforms.push(rejectSomeDirectEvalExpressions);
}

const compartmentFields = weakmapGet(privateFields, this);
let { globalTransforms } = compartmentFields;
const {
globalObject,
globalLexicals,
knownScopeProxies,
} = compartmentFields;

let localObject = globalLexicals;
if (__moduleShimLexicals__ !== undefined) {
// When using `evaluate` for ESM modules, as should only occur from the
// module-shim's module-instance.js, we do not reveal the SES-shim's
// module-to-program translation, as this is not standardizable behavior.
// However, the `localTransforms` will come from the `__shimTransforms__`
// Compartment option in this case, which is a non-standardizable escape
// hatch so programs designed specifically for the SES-shim
// implementation may opt-in to use the same transforms for `evaluate`
// and `import`, at the expense of being tightly coupled to SES-shim.
globalTransforms = undefined;

localObject = create(null, getOwnPropertyDescriptors(globalLexicals));
defineProperties(
localObject,
getOwnPropertyDescriptors(__moduleShimLexicals__),
);
}

return performEval(source, globalObject, localObject, {
globalTransforms,
localTransforms,
sloppyGlobalsMode,
knownScopeProxies,
});
return compartmentEvaluate(compartmentFields, source, options);
},

toString() {
Expand Down Expand Up @@ -192,12 +145,15 @@ export const CompartmentPrototype = {

assertModuleHooks(this);

return load(privateFields, moduleAliases, this, specifier).then(() => {
// The namespace box is a contentious design and likely to be a breaking
// change in an appropriately numbered future version.
const namespace = this.importNow(specifier);
return { namespace };
});
return promiseThen(
load(privateFields, moduleAliases, this, specifier),
() => {
// The namespace box is a contentious design and likely to be a breaking
// change in an appropriately numbered future version.
const namespace = compartmentImportNow(this, specifier);
return { namespace };
},
);
},

async load(specifier) {
Expand All @@ -217,9 +173,7 @@ export const CompartmentPrototype = {

assertModuleHooks(this);

const moduleInstance = link(privateFields, moduleAliases, this, specifier);
moduleInstance.execute();
return moduleInstance.exportsProxy;
return compartmentImportNow(this, specifier);
},
};

Expand Down Expand Up @@ -306,12 +260,14 @@ export const makeCompartmentConstructor = (

assign(globalObject, endowments);

const invalidNames = getOwnPropertyNames(globalLexicals).filter(
const invalidNames = arrayFilter(
getOwnPropertyNames(globalLexicals),
identifier => !isValidIdentifierName(identifier),
);
if (invalidNames.length) {
throw new Error(
`Cannot create compartment with invalid names for global lexicals: ${invalidNames.join(
`Cannot create compartment with invalid names for global lexicals: ${arrayJoin(
invalidNames,
', ',
)}; these names would not be lexically mentionable`,
);
Expand Down
4 changes: 3 additions & 1 deletion packages/ses/src/enable-property-overrides.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Set,
String,
TypeError,
arrayForEach,
defineProperty,
getOwnPropertyDescriptor,
getOwnPropertyDescriptors,
Expand Down Expand Up @@ -112,6 +113,7 @@ export default function enablePropertyOverrides(
this[prop] = newValue;
} else {
if (isDebug) {
// eslint-disable-next-line @endo/no-polymorphic-call
console.error(new Error(`Override property ${prop}`));
}
defineProperty(this, prop, {
Expand Down Expand Up @@ -148,7 +150,7 @@ export default function enablePropertyOverrides(
// TypeScript does not allow symbols to be used as indexes because it
// cannot recokon types of symbolized properties.
// @ts-ignore
ownKeys(descs).forEach(prop => enable(path, obj, prop, descs[prop]));
arrayForEach(ownKeys(descs), prop => enable(path, obj, prop, descs[prop]));
}

function enableProperties(path, obj, plan) {
Expand Down
Loading

0 comments on commit 21a6390

Please sign in to comment.