ses
Version:
Hardened JavaScript for Fearless Cooperation
406 lines (366 loc) • 13.4 kB
JavaScript
/**
* @module Provides the mechanism to create a Compartment constructor that
* can provide either shim-specific or native XS features depending on
* the __native__ constructor option.
* This is necessary because a native Compartment can handle native ModuleSource
* but cannot handle shim-specific pre-compiled ModuleSources like the JSON
* representation of a module that Compartment Mapper can put in bundles.
* Pre-compiling ModuleSource during bundling helps avoid paying the cost
* of importing Babel and transforming ESM syntax to a form that can be
* confined by the shim, which is prohibitively expensive for a web runtime
* and for XS _without this adapter_.
*
* Since any invocation of the Compartment constructor may occur standing
* on a native-flavor or shim-flavor compartment, we create parallel compartment
* constructor trees for compartments created with the Compartment constructor
* of a specific compartment.
*
* A compartment's importHook, importNowHook, moduleMapHook, and the modules
* map itself may provide module descriptors that address another compartment,
* using a compartment instance as a token indicating the compartment the
* module should be loaded or initialized in.
* Consequently, the compartment instance must be a suitable token for the
* underlying native-flavor or shim-flavor compartment.
* We are not in a position to fidddle with the native compartments behavior,
* so adapted compartments use the identity of the native compartment.
* We replace all of the methods of the native compartment prototype with
* thunks that choose behavior based on whether the compartment was
* constructed with the __native__ option.
* The SES shim associates a compartment with its private fields using a weak
* map exported by ../src/compartment.js and held closely by ses by the
* enforcement of explicit exports in package.json, since Node.js 12.11.0.
*
* Evaluating ./compartment.js does not have global side-effects.
* We defer modification of the global environment until the evaluation
* of ./compartment-shim.js.
*/
// @ts-check
/* eslint-disable no-underscore-dangle */
/// <reference types="ses">
import {
Map,
TypeError,
WeakMap,
arrayFlatMap,
assign,
defineProperties,
identity,
promiseThen,
toStringTagSymbol,
weakmapGet,
weakmapSet,
} from './commons.js';
import {
setGlobalObjectSymbolUnscopables,
setGlobalObjectConstantProperties,
setGlobalObjectMutableProperties,
setGlobalObjectEvaluators,
} from './global-object.js';
import { assert, assertEqual, q } from './error/assert.js';
import { sharedGlobalPropertyNames } from './permits.js';
import { load, loadNow } from './module-load.js';
import { link } from './module-link.js';
import { getDeferredExports } from './module-proxy.js';
import { compartmentEvaluate } from './compartment-evaluate.js';
import { makeSafeEvaluator } from './make-safe-evaluator.js';
/** @import {ModuleDescriptor, ModuleExportsNamespace} from '../types.js' */
// moduleAliases associates every public module exports namespace with its
// corresponding compartment and specifier so they can be used to link modules
// across compartments.
// The mechanism to thread an alias is to use the compartment.module function
// to obtain the exports namespace of a foreign module and pass it into another
// compartment's moduleMap constructor option.
const moduleAliases = new WeakMap();
// privateFields captures the private state for each compartment.
const privateFields = new WeakMap();
export const InertCompartment = function Compartment(
_endowments = {},
_modules = {},
_options = {},
) {
throw TypeError(
'Compartment.prototype.constructor is not a valid constructor.',
);
};
/**
* @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,
get globalThis() {
return weakmapGet(privateFields, this).globalObject;
},
get name() {
return weakmapGet(privateFields, this).name;
},
/**
* @param {string} source is a JavaScript program grammar construction.
* @param {object} [options]
* @param {Array<import('./lockdown-shim').Transform>} [options.transforms]
* @param {boolean} [options.sloppyGlobalsMode]
* @param {object} [options.__moduleShimLexicals__]
* @param {boolean} [options.__evadeHtmlCommentTest__]
* @param {boolean} [options.__evadeImportExpressionTest__]
* @param {boolean} [options.__rejectSomeDirectEvalExpressions__]
*/
evaluate(source, options = {}) {
const compartmentFields = weakmapGet(privateFields, this);
return compartmentEvaluate(compartmentFields, source, options);
},
module(specifier) {
if (typeof specifier !== 'string') {
throw TypeError('first argument of module() must be a string');
}
const { exportsProxy } = getDeferredExports(
this,
weakmapGet(privateFields, this),
moduleAliases,
specifier,
);
return exportsProxy;
},
async import(specifier) {
const { noNamespaceBox } = weakmapGet(privateFields, this);
if (typeof specifier !== 'string') {
throw TypeError('first argument of import() must be a string');
}
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(
/** @type {Compartment} */ (this),
specifier,
);
if (noNamespaceBox) {
return namespace;
}
// Legacy behavior: box the namespace object so that thenable modules
// do not get coerced into a promise accidentally.
return { namespace };
},
);
},
async load(specifier) {
if (typeof specifier !== 'string') {
throw TypeError('first argument of load() must be a string');
}
return load(privateFields, moduleAliases, this, specifier);
},
importNow(specifier) {
if (typeof specifier !== 'string') {
throw TypeError('first argument of importNow() must be a string');
}
loadNow(privateFields, moduleAliases, this, specifier);
return compartmentImportNow(/** @type {Compartment} */ (this), specifier);
},
};
// This causes `String(new Compartment())` to evaluate to `[object Compartment]`.
// The descriptor follows the conventions of other globals with @@toStringTag
// properties, e.g. Math.
defineProperties(CompartmentPrototype, {
[toStringTagSymbol]: {
value: 'Compartment',
writable: false,
enumerable: false,
configurable: true,
},
});
defineProperties(InertCompartment, {
prototype: { value: CompartmentPrototype },
});
/**
* @callback MakeCompartmentConstructor
* @param {MakeCompartmentConstructor} targetMakeCompartmentConstructor
* @param {Record<string, any>} intrinsics
* @param {(object: object) => void} markVirtualizedNativeFunction
* @param {object} [options]
* @param {Compartment} [options.parentCompartment]
* @param {boolean} [options.enforceNew]
* @returns {Compartment['constructor']}
*/
// In order to facilitate migration from the deprecated signature
// of the compartment constructor,
// new Compartent(globals?, modules?, options?)
// to the new signature:
// new Compartment(options?)
// where globals and modules are expressed in the options bag instead of
// positional arguments, this function detects the temporary sigil __options__
// on the first argument and coerces compartments arguments into a single
// compartments object.
export const compartmentOptions = (...args) => {
if (args.length === 0) {
return {};
}
if (
args.length === 1 &&
typeof args[0] === 'object' &&
args[0] !== null &&
'__options__' in args[0]
) {
const { __options__, ...options } = args[0];
assert(
__options__ === true,
`Compartment constructor only supports true __options__ sigil, got ${__options__}`,
);
return options;
} else {
const [
globals = /** @type {Map<string, any>} */ ({}),
modules = /** @type {Map<string, ModuleDescriptor>} */ ({}),
options = {},
] = args;
assertEqual(
options.modules,
undefined,
`Compartment constructor must receive either a module map argument or modules option, not both`,
);
assertEqual(
options.globals,
undefined,
`Compartment constructor must receive either globals argument or option, not both`,
);
return {
...options,
globals,
modules,
};
}
};
/** @type {MakeCompartmentConstructor} */
export const makeCompartmentConstructor = (
targetMakeCompartmentConstructor,
intrinsics,
markVirtualizedNativeFunction,
{ parentCompartment = undefined, enforceNew = false } = {},
) => {
function Compartment(...args) {
if (enforceNew && new.target === undefined) {
throw TypeError(
"Class constructor Compartment cannot be invoked without 'new'",
);
}
// Extract options, and shallow-clone transforms.
const {
name = '<unknown>',
transforms = [],
__shimTransforms__ = [],
globals: endowmentsOption = {},
modules: moduleMapOption = {},
resolveHook,
importHook,
importNowHook,
moduleMapHook,
importMetaHook,
__noNamespaceBox__: noNamespaceBox = false,
} = compartmentOptions(...args);
const globalTransforms = arrayFlatMap(
[transforms, __shimTransforms__],
identity,
);
const endowments = { __proto__: null, ...endowmentsOption };
const moduleMap = { __proto__: null, ...moduleMapOption };
// Map<FullSpecifier, ModuleCompartmentRecord>
const moduleRecords = new Map();
// Map<FullSpecifier, ModuleInstance>
const instances = new Map();
// Map<FullSpecifier, {ExportsProxy, ProxiedExports, activate()}>
const deferredExports = new Map();
const globalObject = {};
setGlobalObjectSymbolUnscopables(globalObject);
// We must initialize all constant properties first because
// `makeSafeEvaluator` may use them to create optimized bindings
// in the evaluator.
// TODO: consider merging into a single initialization if internal
// evaluator is no longer eagerly created
setGlobalObjectConstantProperties(globalObject);
const { safeEvaluate } = makeSafeEvaluator({
globalObject,
globalTransforms,
sloppyGlobalsMode: false,
});
setGlobalObjectMutableProperties(globalObject, {
intrinsics,
newGlobalPropertyNames: sharedGlobalPropertyNames,
makeCompartmentConstructor: targetMakeCompartmentConstructor,
parentCompartment: this,
markVirtualizedNativeFunction,
});
// TODO: maybe add evalTaming to the Compartment constructor 3rd options?
setGlobalObjectEvaluators(
globalObject,
safeEvaluate,
markVirtualizedNativeFunction,
);
assign(globalObject, endowments);
/**
* In support dynamic import in a module source loaded by this compartment,
* like `await import(importSpecifier)`, induces this compartment to import
* a module, returning a promise for the resulting module exports
* namespace.
* Unlike `compartment.import`, never creates a box object for the
* namespace as that behavior is deprecated and inconsistent with the
* standard behavior of dynamic import.
* Obliges the caller to resolve import specifiers to their corresponding
* full specifier.
* That is, every module must have its own dynamic import function that
* closes over the surrounding module's full module specifier and calls
* through to this function.
* @param {string} fullSpecifier - A full specifier is a key in the
* compartment's module memo.
* The method `compartment.import` accepts a full specifier, but dynamic
* import accepts an import specifier and resolves it to a full specifier
* relative to the calling module's full specifier.
* @returns {Promise<ModuleExportsNamespace>}
*/
const compartmentImport = async fullSpecifier => {
if (typeof resolveHook !== 'function') {
throw TypeError(
`Compartment does not support dynamic import: no configured resolveHook for compartment ${q(name)}`,
);
}
await load(privateFields, moduleAliases, this, fullSpecifier);
const { execute, exportsProxy } = link(
privateFields,
moduleAliases,
this,
fullSpecifier,
);
execute();
return exportsProxy;
};
weakmapSet(privateFields, this, {
name: `${name}`,
globalTransforms,
globalObject,
safeEvaluate,
resolveHook,
importHook,
importNowHook,
moduleMap,
moduleMapHook,
importMetaHook,
moduleRecords,
__shimTransforms__,
deferredExports,
instances,
parentCompartment,
noNamespaceBox,
compartmentImport,
});
}
Compartment.prototype = CompartmentPrototype;
return Compartment;
};