@endo/compartment-mapper
Version:
The compartment mapper assembles Node applications in a sandbox
233 lines (214 loc) • 7.58 kB
JavaScript
/* eslint-disable no-shadow */
/**
* Provides {@link digestCompartmentMap} which creates a digest of a compartment
* map suitable for archival or further inspection.
*
* @module
*/
/**
* @import {
* CompartmentDescriptor,
* CompartmentMapDescriptor,
* DigestResult,
* ModuleDescriptor,
* Sources,
* } from './types.js'
*/
import { pathCompare } from '@endo/path-compare';
import { assertCompartmentMap, stringCompare } from './compartment-map.js';
const { create, fromEntries, entries, keys } = Object;
/**
* We attempt to produce compartment maps that are consistent regardless of
* whether the packages were originally laid out on disk for development or
* production, and other trivia like the fully qualified path of a specific
* installation.
*
* Naming compartments for the self-ascribed name and version of each Node.js
* package is insufficient because they are not guaranteed to be unique.
* Dependencies do not necessarilly come from the npm registry and may be
* for example derived from fully qualified URL's or Github org and project
* names.
* Package managers are also not required to fully deduplicate the hard
* copy of each package even when they are identical resources.
* Duplication is undesirable, but we elect to defer that problem to solutions
* in the package managers, as the alternative would be to consistently hash
* the original sources of the packages themselves, which may not even be
* available much less pristine for us.
*
* So, instead, we use the lexically least path of dependency names, delimited
* by hashes.
* The compartment maps generated by the ./node-modules.js tooling pre-compute
* these traces for our use here.
* We sort the compartments lexically on their self-ascribed name and version,
* and use the lexically least dependency name path as a tie-breaker.
* The dependency path is logical and orthogonal to the package manager's
* actual installation location, so should be orthogonal to the vagaries of the
* package manager's deduplication algorithm.
*
* @param {Record<string, CompartmentDescriptor>} compartments
* @returns {Record<string, string>} map from old to new compartment names.
*/
const renameCompartments = compartments => {
/** @type {Record<string, string>} */
const compartmentRenames = create(null);
let index = 0;
let prev = '';
// The sort below combines two comparators to avoid depending on sort
// stability, which became standard as recently as 2019.
// If that date seems quaint, please accept my regards from the distant past.
// We are very proud of you.
const compartmentsByPath = Object.entries(compartments)
.map(([name, compartment]) => ({
name,
path: compartment.path,
label: compartment.label,
}))
.sort((a, b) => {
if (a.label === b.label) {
assert(a.path !== undefined && b.path !== undefined);
return pathCompare(a.path, b.path);
}
return stringCompare(a.label, b.label);
});
for (const { name, label } of compartmentsByPath) {
if (label === prev) {
compartmentRenames[name] = `${label}-n${index}`;
index += 1;
} else {
compartmentRenames[name] = label;
prev = label;
index = 1;
}
}
return compartmentRenames;
};
/**
* @param {Record<string, CompartmentDescriptor>} compartments
* @param {Sources} sources
* @param {Record<string, string>} compartmentRenames
*/
const translateCompartmentMap = (compartments, sources, compartmentRenames) => {
const result = create(null);
for (const compartmentName of keys(compartmentRenames)) {
const compartment = compartments[compartmentName];
const { name, label, retained: compartmentRetained, policy } = compartment;
if (compartmentRetained) {
// rename module compartments
/** @type {Record<string, ModuleDescriptor>} */
const modules = create(null);
const compartmentModules = compartment.modules;
if (compartment.modules) {
for (const name of keys(compartmentModules).sort()) {
const { retained: moduleRetained, ...retainedModule } =
compartmentModules[name];
if (moduleRetained) {
if (retainedModule.compartment !== undefined) {
modules[name] = {
...retainedModule,
compartment: compartmentRenames[retainedModule.compartment],
};
} else {
modules[name] = retainedModule;
}
}
}
}
// integrate sources into modules
const compartmentSources = sources[compartmentName];
if (compartmentSources) {
for (const name of keys(compartmentSources).sort()) {
const source = compartmentSources[name];
const { location, parser, exit, sha512, deferredError } = source;
if (location !== undefined) {
modules[name] = {
location,
parser,
sha512,
};
} else if (exit !== undefined) {
modules[name] = {
exit,
};
} else if (deferredError !== undefined) {
modules[name] = {
deferredError,
};
}
}
}
result[compartmentRenames[compartmentName]] = {
name,
label,
location: compartmentRenames[compartmentName],
modules,
policy,
// `scopes`, `types`, and `parsers` are not necessary since every
// loadable module is captured in `modules`.
};
}
}
return result;
};
/**
* @param {Sources} sources
* @param {Record<string, string>} compartmentRenames
* @returns {Sources}
*/
const renameSources = (sources, compartmentRenames) => {
return fromEntries(
entries(sources).map(([name, compartmentSources]) => [
compartmentRenames[name],
compartmentSources,
]),
);
};
/**
* @param {CompartmentMapDescriptor} compartmentMap
* @param {Sources} sources
* @returns {DigestResult}
*/
export const digestCompartmentMap = (compartmentMap, sources) => {
const {
compartments,
entry: { compartment: entryCompartmentName, module: entryModuleSpecifier },
} = compartmentMap;
const oldToNewCompartmentNames = renameCompartments(compartments);
const digestCompartments = translateCompartmentMap(
compartments,
sources,
oldToNewCompartmentNames,
);
const digestEntryCompartmentName =
oldToNewCompartmentNames[entryCompartmentName];
const digestSources = renameSources(sources, oldToNewCompartmentNames);
const digestCompartmentMap = {
// TODO graceful migration from tags to conditions
// https://github.com/endojs/endo/issues/2388
tags: [],
entry: {
compartment: digestEntryCompartmentName,
module: entryModuleSpecifier,
},
compartments: digestCompartments,
};
// Cross-check:
// We assert that we have constructed a valid compartment map, not because it
// might not be, but to ensure that the assertCompartmentMap function can
// accept all valid compartment maps.
assertCompartmentMap(digestCompartmentMap);
const newToOldCompartmentNames = fromEntries(
entries(oldToNewCompartmentNames).map(([oldName, newName]) => [
newName,
oldName,
]),
);
/** @type {DigestResult} */
const digestResult = {
compartmentMap: digestCompartmentMap,
sources: digestSources,
oldToNewCompartmentNames,
newToOldCompartmentNames,
compartmentRenames: newToOldCompartmentNames,
};
return digestResult;
};