UNPKG

@endo/compartment-mapper

Version:

The compartment mapper assembles Node applications in a sandbox

233 lines (214 loc) 7.58 kB
/* 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; };