Skip to content

Commit

Permalink
feat(compartment-mapper): more renames returned from captureFromMap /…
Browse files Browse the repository at this point in the history
… makeArchiveCompartmentMap

This change isolates the duplicated code in `archive-lite.js` and `capture-lite.js` into a new internal module, `digest.js`.

It provides `digestCompartmentMap()`, which is now consumed by both `archive-lite.js` and `capture-lite.js`.

Both `makeArchiveCompartmentMap()` and `captureFromMap()` now pass new properties from `digestCompartmentMap()` through to their consumers: `newToOldCompartmentMap` and `oldToNewCompartmentMap`.

**The `compartmentRenames` property is now soft-deprecated** (since its naming is ambiguous). The same property is now `newToOldCompartmentMap`; `compartmentRenames` should eventually be removed.

_Note_: The duplicated code drifted slightly in `archive-lite.js`; see e3b310d. These changes were kept and put into `digest.js`.
  • Loading branch information
boneskull committed Jan 16, 2025
1 parent c589ed0 commit 630592d
Show file tree
Hide file tree
Showing 5 changed files with 306 additions and 392 deletions.
206 changes: 15 additions & 191 deletions packages/compartment-mapper/src/archive-lite.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,11 @@
/**
* @import {
* ArchiveLiteOptions,
* ArchiveResult,
* ArchiveWriter,
* CaptureSourceLocationHook,
* CompartmentDescriptor,
* CompartmentMapDescriptor,
* HashPowers,
* ModuleDescriptor,
* ReadFn,
* ReadPowers,
* Sources,
Expand All @@ -54,12 +53,8 @@ import {
makeImportHookMaker,
} from './import-hook.js';
import { unpackReadPowers } from './powers.js';
import {
assertCompartmentMap,
stringCompare,
pathCompare,
} from './compartment-map.js';
import { detectAttenuators } from './policy.js';
import { digestCompartmentMap } from './digest.js';

const textEncoder = new TextEncoder();

Expand All @@ -72,160 +67,7 @@ const { assign, create, freeze } = Object;
*/
const resolveLocation = (rel, abs) => new URL(rel, abs).toString();

const { keys, entries, fromEntries } = 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,
]),
);
};
const { keys } = Object;

/**
* @param {ArchiveWriter} archive
Expand Down Expand Up @@ -269,41 +111,23 @@ const captureSourceLocations = async (sources, captureSourceLocation) => {
/**
* @param {CompartmentMapDescriptor} compartmentMap
* @param {Sources} sources
* @returns {{archiveCompartmentMap: CompartmentMapDescriptor, archiveSources: Sources}}
* @returns {ArchiveResult}
*/
export const makeArchiveCompartmentMap = (compartmentMap, sources) => {
const {
compartments,
entry: { compartment: entryCompartmentName, module: entryModuleSpecifier },
} = compartmentMap;

const compartmentRenames = renameCompartments(compartments);
const archiveCompartments = translateCompartmentMap(
compartments,
sources,
compartmentMap: archiveCompartmentMap,
sources: archiveSources,
oldToNewCompartmentNames,
newToOldCompartmentNames,
compartmentRenames,
} = digestCompartmentMap(compartmentMap, sources);
return {
archiveCompartmentMap,
archiveSources,
oldToNewCompartmentNames,
newToOldCompartmentNames,
compartmentRenames,
);
const archiveEntryCompartmentName = compartmentRenames[entryCompartmentName];
const archiveSources = renameSources(sources, compartmentRenames);

const archiveCompartmentMap = {
// TODO migrate tags to conditions
// /~https://github.com/endojs/endo/issues/2388
tags: [],
entry: {
compartment: archiveEntryCompartmentName,
module: entryModuleSpecifier,
},
compartments: archiveCompartments,
};

// 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(archiveCompartmentMap);

return { archiveCompartmentMap, archiveSources };
};

/**
Expand Down
Loading

0 comments on commit 630592d

Please sign in to comment.