@endo/compartment-mapper
Version:
The compartment mapper assembles Node applications in a sandbox
298 lines (273 loc) • 7.73 kB
JavaScript
/**
* Provides adapters for Compartment Mapper I/O to the corresponding
* Node.js implementations of those behaviors.
*
* The Compartment Mapper generalizes its I/O interface to allow for a wide
* variety of I/O providers, but especially for reading and writing from
* virtualized file systems like zip files.
*
* @module
*/
/**
* @import {
* CryptoInterface,
* FsInterface,
* PathInterface,
* UrlInterface,
* } from './types/node-powers.js'
* @import {FileUrlString} from './types/external.js'
* @import {
* CanonicalFn,
* FileURLToPathFn,
* HashFn,
* IsAbsoluteFn,
* MaybeReadFn,
* MaybeReadNowFn,
* MaybeReadPowers,
* PathToFileURLFn,
* ReadFn,
* ReadNowPowers,
* ReadPowers,
* RequireResolveFn,
* WritePowers,
* } from './types/powers.js'
*/
import { createRequire } from 'module';
/**
* @type {FileURLToPathFn}
*/
const fakeFileURLToPath = location => {
const url = new URL(location);
if (url.protocol !== 'file:') {
throw Error(`Cannot convert URL to file path: ${location}`);
}
return url.pathname;
};
/**
* @type {PathToFileURLFn} path
*/
const fakePathToFileURL = path => {
return new URL(path, 'file://');
};
/**
* @type {IsAbsoluteFn}
*/
const fakeIsAbsolute = () => false;
/**
* The implementation of `makeReadPowers` and the deprecated
* `makeNodeReadPowers` handles the case when the `url` power is not provided,
* but `makeReadPowers` presents a type that requires `url`.
*
* @param {object} args
* @param {FsInterface} args.fs
* @param {UrlInterface} [args.url]
* @param {CryptoInterface} [args.crypto]
* @param {PathInterface} [args.path]
* @returns {MaybeReadPowers<FileUrlString>}
*/
const makeReadPowersSloppy = ({
fs,
url = undefined,
crypto = undefined,
path = undefined,
}) => {
const fileURLToPath =
url === undefined ? fakeFileURLToPath : url.fileURLToPath;
const pathToFileURL =
url === undefined ? fakePathToFileURL : url.pathToFileURL;
const isAbsolute = path === undefined ? fakeIsAbsolute : path.isAbsolute;
let readMutex = Promise.resolve(undefined);
/**
* @type {ReadFn}
*/
const read = async location => {
const promise = readMutex;
let release = Function.prototype;
readMutex = new Promise(resolve => {
release = resolve;
});
await promise;
const filepath = fileURLToPath(location);
try {
// We await here to ensure that we release the mutex only after
// completing the read.
return await fs.promises.readFile(filepath);
} finally {
release(undefined);
}
};
/**
* @type {MaybeReadFn}
*/
const maybeRead = location =>
read(location).catch(error => {
if (
error.message.startsWith('ENOENT: ') ||
error.message.startsWith('EISDIR: ')
) {
return undefined;
}
throw error;
});
/** @type {RequireResolveFn} */
const requireResolve = (from, specifier, options) =>
createRequire(from).resolve(specifier, options);
/**
* There are two special things about the canonical function the compartment
* mapper needs. It needs to use URL's instead of posix paths to avoid
* bundling up a bunch of cruft when we port this around. It needs to use
* promises. URL's can and must logically distinguish a directory from a
* file, by the final slash, whereas paths must never have a final slash
* because that implies a blank file name within the enclosing directory. The
* canonical function must also return the logical path instead of the real
* path if there is no real path. This is merely a convenience for the
* compartment mapper because it will fail to read the package.json in the
* non-existent directory on the next step after canonicalizing the package
* location.
*
* @type {CanonicalFn<FileUrlString>}
*/
const canonical = async location => {
await null;
try {
if (location.endsWith('/')) {
const realPath = await fs.promises.realpath(
fileURLToPath(location).replace(/\/$/, ''),
);
return /** @type {FileUrlString} */ (
`${pathToFileURL(realPath).href}/`
);
} else {
const realPath = await fs.promises.realpath(fileURLToPath(location));
return /** @type {FileUrlString} */ (pathToFileURL(realPath).href);
}
} catch {
return location;
}
};
/** @type {HashFn | undefined} */
const computeSha512 = crypto
? bytes => {
const hash = crypto.createHash('sha512');
hash.update(bytes);
return hash.digest().toString('hex');
}
: undefined;
return {
read,
maybeRead,
fileURLToPath,
pathToFileURL,
canonical,
computeSha512,
requireResolve,
isAbsolute,
};
};
/**
* Creates {@link ReadPowers} for dynamic module support
*
* @param {object} args
* @param {FsInterface} args.fs
* @param {UrlInterface} [args.url]
* @param {CryptoInterface} [args.crypto]
* @param {PathInterface} [args.path]
* @returns {ReadNowPowers<FileUrlString>}
*/
export const makeReadNowPowers = ({
fs,
url = undefined,
crypto = undefined,
path = undefined,
}) => {
const powers = makeReadPowersSloppy({ fs, url, crypto, path });
const fileURLToPath = powers.fileURLToPath || fakeFileURLToPath;
const isAbsolute = powers.isAbsolute || fakeIsAbsolute;
/**
* @type {MaybeReadNowFn}
*/
const maybeReadNow = location => {
const filePath = fileURLToPath(location);
try {
return fs.readFileSync(filePath);
} catch (error) {
if (
'code' in error &&
(error.code === 'ENOENT' || error.code === 'EISDIR')
) {
return undefined;
}
throw error;
}
};
return {
...powers,
maybeReadNow,
fileURLToPath,
isAbsolute,
};
};
/**
* The implementation of `makeWritePowers` and the deprecated
* `makeNodeWritePowers` handles the case when the `url` power is not provided,
* but `makeWritePowers` presents a type that requires `url`.
*
* @param {object} args
* @param {FsInterface} args.fs
* @param {UrlInterface} [args.url]
*/
const makeWritePowersSloppy = ({ fs, url = undefined }) => {
const fileURLToPath =
url === undefined ? fakeFileURLToPath : url.fileURLToPath;
/**
* @param {string} location
* @param {Uint8Array} data
*/
const write = async (location, data) => {
await null;
try {
return await fs.promises.writeFile(fileURLToPath(location), data);
} catch (error) {
throw Error(error.message);
}
};
return { write };
};
/**
* @param {object} args
* @param {FsInterface} args.fs
* @param {UrlInterface} args.url
* @param {CryptoInterface} [args.crypto]
*/
export const makeReadPowers = makeReadPowersSloppy;
/**
* @param {object} args
* @param {FsInterface} args.fs
* @param {UrlInterface} args.url
*/
export const makeWritePowers = makeWritePowersSloppy;
/**
* Deprecated in favor of {@link makeReadPowers}.
* It transpires that positional arguments needed to become an arguments bag to
* reasonably expand to multiple optional dependencies.
*
* @param {FsInterface} fs
* @param {CryptoInterface} [crypto]
* @returns {ReadPowers<FileUrlString>}
* @deprecated
*/
export const makeNodeReadPowers = (fs, crypto = undefined) => {
return makeReadPowersSloppy({ fs, crypto });
};
/**
* Deprecated in favor of {@link makeWritePowers}.
* It transpires that positional arguments needed to become an arguments bag to
* reasonably expand to multiple optional dependencies.
*
* @param {FsInterface} fs
* @returns {WritePowers}
* @deprecated
*/
export const makeNodeWritePowers = fs => {
return makeWritePowersSloppy({ fs });
};