Skip to content

Commit f08ae39

Browse files
committed
chore(compartment-mapper): breakout bundling into safe, unsafe, and util modules
1 parent 56d3159 commit f08ae39

File tree

6 files changed

+364
-268
lines changed

6 files changed

+364
-268
lines changed

packages/compartment-mapper/bundle.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export { makeBundle, writeBundle } from './src/bundle.js';
1+
export { makeBundle, writeBundle } from './src/bundle-unsafe.js';

packages/compartment-mapper/index.js

+2-3
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@ export {
1414
} from './src/import-archive.js';
1515
export { search } from './src/search.js';
1616
export { compartmentMapForNodeModules } from './src/node-modules.js';
17+
export { makeBundle, writeBundle } from './src/bundle-unsafe.js';
1718
export {
18-
makeBundle,
1919
makeSecureBundle,
2020
makeSecureBundleFromArchive,
21-
writeBundle,
22-
} from './src/bundle.js';
21+
} from './src/bundle-safe.js';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
/** @typedef {import('./types.js').CompartmentMapDescriptor} CompartmentMapDescriptor */
2+
/** @typedef {import('./types.js').ReadFn} ReadFn */
3+
/** @typedef {import('./types.js').ModuleTransforms} ModuleTransforms */
4+
/** @typedef {import('./types.js').Sources} Sources */
5+
6+
import fs from 'fs';
7+
import url from 'url';
8+
import { ZipReader } from '@endo/zip';
9+
import { transforms } from 'ses/tools.js';
10+
import { makeArchiveCompartmentMap, locationsForSources } from './archive.js';
11+
import { parseLocatedJson } from './json.js';
12+
import { assertCompartmentMap } from './compartment-map.js';
13+
import { unpackReadPowers } from './powers.js';
14+
import { makeReadPowers } from './node-powers.js';
15+
import { makeBundle } from './bundle-unsafe.js';
16+
import { prepareToBundle, resolveLocation } from './bundle-util.js';
17+
18+
// quote strings
19+
const q = JSON.stringify;
20+
21+
const { evadeImportExpressionTest, mandatoryTransforms } = transforms;
22+
23+
const textDecoder = new TextDecoder();
24+
25+
function wrapFunctorInPrecompiledModule(unsafeFunctorSrc, compartmentName) {
26+
// the mandatory ses transforms will reject import expressions and html comments
27+
const safeFunctorSrc = mandatoryTransforms(unsafeFunctorSrc);
28+
const wrappedSrc = `() => (function(){
29+
with (this.scopeTerminator) {
30+
with (this.globalThis) {
31+
return function() {
32+
'use strict';
33+
return (
34+
${safeFunctorSrc}
35+
);
36+
};
37+
}
38+
}
39+
}).call(getEvalKitForCompartment(${q(compartmentName)}))()`;
40+
return wrappedSrc;
41+
}
42+
43+
// This function is serialized and references variables from its destination scope.
44+
45+
function renderFunctorTable(functorTable) {
46+
const entries = Object.entries(functorTable);
47+
const lines = entries.map(([key, value]) => `${q(key)}: ${value}`);
48+
return `{\n${lines.map(line => ` ${line}`).join(',\n')}\n};`;
49+
}
50+
51+
/**
52+
* @param {CompartmentMapDescriptor} compartmentMap
53+
* @param {Sources} sources
54+
* @returns {Promise<string>}
55+
*/
56+
export const makeSecureBundleFromAppContainer = async (
57+
compartmentMap,
58+
sources,
59+
) => {
60+
const moduleFunctors = {};
61+
const moduleRegistry = {};
62+
63+
for (const {
64+
path,
65+
module,
66+
compartment: compartmentName,
67+
} of locationsForSources(sources)) {
68+
const { bytes, parser } = module;
69+
const textModule = textDecoder.decode(bytes);
70+
const moduleData = JSON.parse(textModule);
71+
const { __syncModuleProgram__, source, ...otherModuleData } = moduleData;
72+
// record module data
73+
moduleRegistry[path] = otherModuleData;
74+
// record functor
75+
switch (parser) {
76+
case 'pre-mjs-json': {
77+
moduleFunctors[path] = wrapFunctorInPrecompiledModule(
78+
__syncModuleProgram__,
79+
compartmentName,
80+
);
81+
// eslint-disable-next-line no-continue
82+
continue;
83+
}
84+
case 'pre-cjs-json': {
85+
moduleFunctors[path] = wrapFunctorInPrecompiledModule(
86+
source,
87+
compartmentName,
88+
);
89+
// eslint-disable-next-line no-continue
90+
continue;
91+
}
92+
default: {
93+
throw new Error(`Unknown parser ${q(parser)}`);
94+
}
95+
}
96+
}
97+
98+
const bundleRuntimeLocation = new URL(
99+
'./bundle-runtime.js',
100+
import.meta.url,
101+
).toString();
102+
// these read powers must refer to the disk as we are bundling the runtime from
103+
// this package's sources. The user-provided read powers used elsewhere refer
104+
// to the user's application source code.
105+
const { read } = makeReadPowers({ fs, url });
106+
const runtimeBundle = evadeImportExpressionTest(
107+
await makeBundle(read, bundleRuntimeLocation),
108+
).replace(`'use strict';\n(() => `, `'use strict';\nreturn (() => `);
109+
110+
const bundle = `\
111+
// START BUNDLE RUNTIME ================================
112+
const { loadApplication } = (function(){
113+
${runtimeBundle}
114+
})();
115+
// END BUNDLE RUNTIME ================================
116+
117+
// START MODULE REGISTRY ================================
118+
const compartmentMap = ${JSON.stringify(compartmentMap, null, 2)};
119+
const moduleRegistry = ${JSON.stringify(moduleRegistry, null, 2)}
120+
121+
const loadModuleFunctors = (getEvalKitForCompartment) => {
122+
return ${renderFunctorTable(moduleFunctors)}
123+
}
124+
125+
// END MODULE REGISTRY ==================================
126+
127+
const { execute } = loadApplication(
128+
compartmentMap,
129+
moduleRegistry,
130+
loadModuleFunctors,
131+
'App',
132+
)
133+
134+
execute()
135+
`;
136+
137+
return bundle;
138+
};
139+
140+
/**
141+
* @param {ReadFn} read
142+
* @param {string} moduleLocation
143+
* @param {object} [options]
144+
* @param {ModuleTransforms} [options.moduleTransforms]
145+
* @param {boolean} [options.dev]
146+
* @param {Set<string>} [options.tags]
147+
* @param {Array<string>} [options.searchSuffixes]
148+
* @param {object} [options.commonDependencies]
149+
* @returns {Promise<string>}
150+
*/
151+
export const makeSecureBundle = async (read, moduleLocation, options) => {
152+
const { compartmentMap, sources } = await prepareToBundle(
153+
read,
154+
moduleLocation,
155+
{
156+
linkOptions: { archiveOnly: true },
157+
...options,
158+
},
159+
);
160+
161+
const { archiveCompartmentMap, archiveSources } = makeArchiveCompartmentMap(
162+
compartmentMap,
163+
sources,
164+
);
165+
166+
return makeSecureBundleFromAppContainer(
167+
archiveCompartmentMap,
168+
archiveSources,
169+
);
170+
};
171+
172+
/**
173+
* @param {import('./types.js').ReadPowers} readPowers
174+
* @param {string} archiveLocation
175+
* @param {object} [options]
176+
* @returns {Promise<string>}
177+
*/
178+
export const makeSecureBundleFromArchive = async (
179+
readPowers,
180+
archiveLocation,
181+
options = {},
182+
) => {
183+
const { expectedSha512 = undefined } = options;
184+
185+
const { read, computeSha512 } = unpackReadPowers(readPowers);
186+
const archiveBytes = await read(archiveLocation);
187+
const archive = new ZipReader(archiveBytes, { name: archiveLocation });
188+
const get = path => archive.read(path);
189+
190+
const compartmentMapBytes = get('compartment-map.json');
191+
192+
let sha512;
193+
if (computeSha512 !== undefined) {
194+
sha512 = computeSha512(compartmentMapBytes);
195+
}
196+
if (expectedSha512 !== undefined) {
197+
if (sha512 === undefined) {
198+
throw new Error(
199+
`Cannot verify expectedSha512 without also providing computeSha512, for archive ${archiveLocation}`,
200+
);
201+
}
202+
if (sha512 !== expectedSha512) {
203+
throw new Error(
204+
`Archive compartment map failed a SHA-512 integrity check, expected ${expectedSha512}, got ${sha512}, for archive ${archiveLocation}`,
205+
);
206+
}
207+
}
208+
const compartmentMapText = textDecoder.decode(compartmentMapBytes);
209+
const compartmentMap = parseLocatedJson(
210+
compartmentMapText,
211+
'compartment-map.json',
212+
);
213+
assertCompartmentMap(compartmentMap, archiveLocation);
214+
215+
// build sources object from archive
216+
/** @type {Sources} */
217+
const sources = {};
218+
for (const [compartmentName, { modules }] of Object.entries(
219+
compartmentMap.compartments,
220+
)) {
221+
const compartmentLocation = resolveLocation(
222+
`${compartmentName}/`,
223+
'file:///',
224+
);
225+
let compartmentSources = sources[compartmentName];
226+
if (compartmentSources === undefined) {
227+
compartmentSources = {};
228+
sources[compartmentName] = compartmentSources;
229+
}
230+
for (const { location, parser } of Object.values(modules)) {
231+
// ignore alias records
232+
if (location === undefined) {
233+
// eslint-disable-next-line no-continue
234+
continue;
235+
}
236+
const moduleLocation = resolveLocation(location, compartmentLocation);
237+
const path = new URL(moduleLocation).pathname.slice(1); // skip initial "/"
238+
const bytes = get(path);
239+
compartmentSources[location] = { bytes, location, parser };
240+
}
241+
}
242+
243+
return makeSecureBundleFromAppContainer(compartmentMap, sources);
244+
};

0 commit comments

Comments
 (0)