@endo/compartment-mapper
Version:
The compartment mapper assembles Node applications in a sandbox
157 lines (141 loc) • 5.16 kB
JavaScript
/**
* Provides functions for interacting with Node.js module specifiers in
* their canonical form.
* This is a kind of path math that is platform-agnostic.
*
* @module
*/
// q, as in quote, for error messages.
const q = JSON.stringify;
/**
* Advances a partial module specifier solution by following the path
* components in the given problem.
* The problem may not produce a path that escapes the solution, that is, the
* problem may not traverse up from an empty solution.
* `Solve` returns false if the problem attempts to escape.
* Advanding a partial solution is the core of `resolve`, `join`, and
* `relativize`, which have different invariants.
*
* @param {Array<string>} solution - fully resolved path components, including
* any from a prior path resolution initially.
* @param {Array<string>} problem - partially resolved path components, that
* is, including '.' and '..' components.
* @returns {boolean} whether the solver terminated early because of a
* nonsensical attempt to traverse above the root directory.
*/
const solve = (solution, problem) => {
for (const part of problem) {
if (part === '.' || part === '') {
// no-op
} else if (part === '..') {
if (solution.length === 0) {
return false;
}
solution.pop();
} else {
solution.push(part);
}
}
return true;
};
/**
* `Resolve` computes the full module specifier for a given imported module specifier
* relative to the referrer module specifier.
* In Node.js compartments, the referrer must be an internal module specifier
* in the context of a compartment, and all internal module specifiers begin
* with a "." path component.
* The referent may be either internal or external.
* In Node.js, fully resolved paths are valid module specifiers, but these
* paths that begin with / are disallowed as they could be used to defeat
* compartment containment.
*
* @param {string} spec - a path to resolve.
* @param {string} referrer - the fully resolved path of referrer module.
* @returns {string} the fully resolved path.
*/
export const resolve = (spec, referrer) => {
spec = String(spec || '');
referrer = String(referrer || '');
if (spec.startsWith('/')) {
throw Error(`Module specifier ${q(spec)} must not begin with "/"`);
}
if (!referrer.startsWith('./')) {
throw Error(`Module referrer ${q(referrer)} must begin with "./"`);
}
const specParts = spec.split('/');
const solution = [];
const problem = [];
if (specParts[0] === '.' || specParts[0] === '..') {
const referrerParts = referrer.split('/');
problem.push(...referrerParts);
problem.pop();
solution.push('.');
}
problem.push(...specParts);
if (!solve(solution, problem)) {
throw Error(
`Module specifier ${q(spec)} via referrer ${q(
referrer,
)} must not traverse behind an empty path`,
);
}
return solution.join('/');
};
/**
* To construct a module map from a node_modules package, inter-package linkage
* requires connecting a full base module specifier like "dependency-package"
* to the other package's full internal module specifier like "." or
* "./utility", to form a local full module specifier like "dependency-package"
* or "dependency-package/utility".
* This type of join may assert that the base is absolute and the referrent is
* relative.
*
* @param {string} base - the fully resolved path of a module.
* @param {string} spec - the partially resolved path of another module.
* @returns {string} the fully resolved path of the specified module.
*/
export const join = (base, spec) => {
spec = String(spec || '');
base = String(base || '');
const specParts = spec.split('/');
const baseParts = base.split('/');
if (specParts.length > 1 && specParts[0] === '') {
throw Error(`Module specifier ${q(spec)} must not start with "/"`);
}
if (baseParts[0] === '.' || baseParts[0] === '..') {
throw Error(`External module specifier ${q(base)} must be absolute`);
}
if (specParts[0] !== '.') {
throw Error(`Internal module specifier ${q(spec)} must be relative`);
}
const solution = [];
if (!solve(solution, specParts)) {
throw Error(
`Module specifier ${q(spec)} via base ${q(
base,
)} must not refer to a module outside of the base`,
);
}
return [base, ...solution].join('/');
};
/**
* Relativize turns absolute identifiers into relative identifiers.
* In package.json, internal module identifiers can be either relative or
* absolute, but compartments backed by node_modules always use relative module
* specifiers for internal linkage.
*
* @param {string} spec - a module specifier that of a local module, that might
* be erroneously framed without an initial '.' path component.
* @returns {string} the idempotent module specifier, ensured to begin with
* '.'.
*/
export const relativize = spec => {
spec = String(spec || '');
const solution = [];
if (!solve(solution, spec.split('/'))) {
throw Error(
`Module specifier ${q(spec)} must not traverse behind an empty path`,
);
}
return ['.', ...solution].join('/');
};