'use strict';
const {
ArrayPrototypeForEach,
MapPrototypeForEach,
ObjectKeys,
PromiseResolve,
SafeMap,
String,
StringPrototypeEndsWith,
StringPrototypeStartsWith,
} = primordials;
const { Buffer } = require('buffer');
const { isArrayBufferView } = require('internal/util/types');
const { dirname, join, sep } = require('path');
const { fileURLToPath, pathToFileURL, URL } = require('internal/url');
const { kEmptyObject } = require('internal/util');
const { validateObject } = require('internal/validators');
const {
codes: {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_STATE,
ERR_MODULE_NOT_FOUND,
},
} = require('internal/errors');
const { createENOENT, createEROFS, createEXDEV } = require('internal/vfs/errors');
const {
VirtualFileSystem,
kLayerId,
kReservedRoot,
normalizeMountedPath,
} = require('internal/vfs/file_system');
const {
getLayerIdFromPath,
getNormalizedVfsRoot,
} = require('internal/vfs/router');
const { ReservedRootProvider } = require('internal/vfs/root');
const { getVirtualFd, closeVirtualFd, createVfsFileHandle } = require('internal/vfs/fd');
const {
assertEncoding,
encodeRealpathResult,
getOptions,
setVfsHandlers,
} = require('internal/fs/utils');
const permission = require('internal/process/permission');
const { getOptionValue } = require('internal/options');
const nativeModulesBinding = internalBinding('modules');
const { UV_ENOENT } = internalBinding('uv');
let debug = require('internal/util/debuglog').debuglog('vfs', (fn) => {
debug = fn;
});
function toPathStr(pathOrUrl) {
if (typeof pathOrUrl === 'string') return pathOrUrl;
if (pathOrUrl instanceof URL) return fileURLToPath(pathOrUrl);
if (Buffer.isBuffer(pathOrUrl)) return pathOrUrl.toString();
return null;
}
function noopFdSync(fd) {
if (getVirtualFd(fd)) return true;
return undefined;
}
const noopFdPromise = PromiseResolve(true);
function noopFd(fd) {
if (getVirtualFd(fd)) return noopFdPromise;
return undefined;
}
function toWriteBuffer(data, options) {
if (Buffer.isBuffer(data)) return data;
if (isArrayBufferView(data)) {
return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
}
const encoding = typeof options === 'string' ? options : options?.encoding;
return Buffer.from(data, encoding || 'utf8');
}
function getMkdtempOptions(prefix, options) {
options = getOptions(options);
if (Buffer.isBuffer(prefix)) {
options = { __proto__: null, ...options, encoding: 'buffer' };
}
return options;
}
function writeFileSyncFd(fd, data, options) {
const vfd = getVirtualFd(fd);
if (vfd === undefined) return undefined;
const buffer = toWriteBuffer(data, options);
let offset = 0;
let length = buffer.byteLength;
while (length > 0) {
const written = vfd.entry.writeSync(buffer, offset, length, null);
offset += written;
length -= written;
}
return true;
}
const activeVFSLayers = new SafeMap();
// Serves the reserved root directory itself, while any layer is mounted.
let rootVFS = null;
let hooksInstalled = false;
let vfsHandlerObj;
// Lazy: os.devNull may not be available at snapshot time.
let normalizedVfsRoot = null;
let normalizedVfsRootPrefix = null;
function registerVFS(vfs) {
// TODO(rafaelgss): publish a diagnostics channel message in audit mode.
if (permission.isEnabled() && !permission.isAuditMode() &&
!getOptionValue('--allow-fs-vfs')) {
throw new ERR_INVALID_STATE(
'VFS cannot be used when the permission model is enabled. ' +
'Use --allow-fs-vfs to allow it.',
);
}
if (activeVFSLayers.has(vfs[kLayerId])) return;
activeVFSLayers.set(vfs[kLayerId], vfs);
debug('register layer=%d mount=%s active=%d',
vfs[kLayerId], vfs.mountPoint, activeVFSLayers.size);
if (!hooksInstalled) {
installHooks();
}
}
function deregisterVFS(vfs) {
if (!activeVFSLayers.delete(vfs[kLayerId])) return;
debug('deregister layer=%d active=%d', vfs[kLayerId], activeVFSLayers.size);
purgeLoaderCachesForVFS(vfs);
if (activeVFSLayers.size === 0) {
uninstallHooks();
}
}
/**
* Resolves a path string to the reserved VFS root, or null for a path
* other than the root itself and those under it. Ownership is decidable
* from the path alone: all mount points live under the reserved
* `${os.devNull}/vfs/<id>` namespace, so a single prefix comparison
* rejects every real-file-system path and a map lookup finds the owning
* layer. The normalized path is returned
* alongside the layer so downstream helpers can skip renormalization.
*
* The root itself, and a path under it that no active layer owns, come
* back with `vfs: null` rather than as `null`, because the two cases must not be
* treated alike by the module loader. The loader manufactures such
* paths itself: resolving a mount point as a directory first probes the
* sibling names `<mount>.js`, `<mount>.json`, ..., and a package.json
* walk-up passes the parents of the mount point. They cannot name
* anything real, but on Windows the root sits under `\\.\nul`, and
* `\\.\nul\<anything>` opens the NUL device, which stats as a character
* device and reads as empty. Handed to the native loader, such a probe
* "finds" a file and the walk-up above it rejects the empty device as an
* invalid package.json, so the loader must answer for the whole root.
* @param {string} inputPath
* @returns {{ vfs: object|null, normalized: string }|null}
*/
function findVFSOrRoot(inputPath) {
const normalized = normalizeMountedPath(inputPath);
if (normalized !== normalizedVfsRoot &&
!StringPrototypeStartsWith(normalized, normalizedVfsRootPrefix)) {
return null;
}
const layerId = getLayerIdFromPath(normalized);
const vfs = layerId === -1 ? undefined : activeVFSLayers.get(layerId);
if (vfs === undefined || !vfs.shouldHandleNormalized(normalized)) {
return { vfs: null, normalized };
}
return { vfs, normalized };
}
/**
* Resolves a path string to the VFS that serves it for the fs functions,
* or null for a path outside the reserved VFS root. Unlike the loader, the
* fs functions see the root as a directory, so a path under the root that
* no layer serves is the root file system's to answer.
* @param {string} inputPath
* @returns {{ vfs: object, normalized: string }|null}
*/
function findVFS(inputPath) {
const r = findVFSOrRoot(inputPath);
if (r === null) return null;
if (r.vfs === null) r.vfs = rootVFS;
return r;
}
/**
* Drop the cache entries under `vfs`'s mount point from the
* JS-reachable loader caches. Real-fs entries and other-VFS entries
* are left in place. Because mount points cannot collide with real
* paths - and symlink resolution inside a VFS always yields paths
* under the same mount point - a prefix scan is exact.
* @param {object} vfs The VFS being deregistered
*/
function purgeLoaderCachesForVFS(vfs) {
const mountPoint = vfs.mountPoint;
const cjsLoader = require('internal/modules/cjs/loader');
cjsLoader.purgeModuleCachesForPrefix(mountPoint);
const helpers = require('internal/modules/helpers');
helpers.purgeRealpathCacheForPrefix(mountPoint);
const pkgReader = require('internal/modules/package_json_reader');
pkgReader.purgePackageJSONCacheForPrefix(mountPoint);
const esmLoader = require('internal/modules/esm/loader');
if (esmLoader.isCascadedLoaderInitialized()) {
const loader = esmLoader.getOrInitializeCascadedLoader();
const loadCache = loader.loadCache;
const mountURL = pathToFileURL(mountPoint).href;
const mountURLPrefix = mountURL + '/';
// Iterate via MapPrototypeForEach (not for-of map.keys()) so a
// polluted Map iterator can't break the cleanup path.
if (loadCache && typeof loadCache.delete === 'function') {
MapPrototypeForEach(loadCache, (variants, url) => {
if (typeof url === 'string' &&
(url === mountURL ||
StringPrototypeStartsWith(url, mountURLPrefix)) &&
variants) {
ArrayPrototypeForEach(ObjectKeys(variants), (type) => {
loadCache.delete(url, type);
});
}
});
}
}
}
/**
* Returns the stat result code for a VFS path.
* @param {object} vfs The VFS instance
* @param {string} filePath The path to check
* @returns {number} 0 for file, 1 for directory, -2 for not found
*/
function vfsStat(vfs, filePath) {
try {
const stats = vfs.statSync(filePath);
if (stats.isDirectory()) return 1;
return 0;
} catch (err) {
if (typeof err?.errno === 'number') return err.errno;
throw err;
}
}
/**
* Finds the VFS owning `filename` and stats it.
* @param {string} filename The absolute path to check
* @returns {{ vfs: object, result: number }|null}
*/
function findVFSForStat(filename) {
const r = findVFS(filename);
if (r === null) return null;
return { vfs: r.vfs, result: vfsStat(r.vfs, filename) };
}
/**
* Reads `filename` from the VFS that owns it, reporting a missing file
* or a directory the way the native loader's read does.
* @param {object} vfs The VFS owning filename
* @param {string} filename The absolute path to read
* @param {string|object} options Read options
* @returns {Buffer|string}
*/
function readVFS(vfs, filename, options) {
try {
return vfs.readFileSync(filename, options);
} catch (e) {
const code = e?.code;
if (code === 'ENOENT' || code === 'EISDIR') {
throw createENOENT('open', filename);
}
throw e;
}
}
/**
* Walk up directories inside `vfs`'s mount looking for package.json.
* Always returns an object. When a package.json is found `.tuple` is
* populated with the C++ binding's serialized shape
* (`[name, main, type, imports, exports, filePath]`); otherwise
* `.sentinel` is the last candidate path checked - matches the "not
* found" marker the C++ binding returns for getPackageScopeConfig.
* Syntactically malformed package.json aborts the walk with
* ERR_INVALID_PACKAGE_CONFIG (thrown by the native parser), matching
* native GetPackageJSON.
* @param {object} vfs The VFS that owns startPath
* @param {string} startPath Absolute path to start from
* @param {string} normalizedStart Same as startPath but passed through
* `normalizeMountedPath`. Used to walk the directory chain so the
* containment check is a plain string prefix rather than a fresh
* `toNamespacedPath(resolve(...))` on every iteration.
* @returns {{ vfs?: object, pjsonPath?: string, tuple?: Array, sentinel: string }}
*/
function findVFSPackageJSON(vfs, startPath, normalizedStart) {
let currentDir = dirname(startPath);
let currentNorm = dirname(normalizedStart);
let lastDir;
let sentinel = join(currentDir, 'package.json');
while (currentDir !== lastDir) {
if (StringPrototypeEndsWith(currentDir, '/node_modules') ||
StringPrototypeEndsWith(currentDir, '\\node_modules')) {
break;
}
if (!vfs.shouldHandleNormalized(currentNorm)) {
break;
}
const pjsonPath = join(currentDir, 'package.json');
sentinel = pjsonPath;
if (vfsStat(vfs, pjsonPath) === 0) {
let content;
try {
content = vfs.readFileSync(pjsonPath);
} catch {
content = null;
}
if (content !== null) {
const tuple = nativeModulesBinding.parsePackageJSON(
content, pjsonPath);
return { vfs, pjsonPath, tuple, sentinel: pjsonPath };
}
}
lastDir = currentDir;
currentDir = dirname(currentDir);
currentNorm = dirname(currentNorm);
}
return { sentinel };
}
function findVFSForExists(filename) {
const r = findVFS(filename);
if (r === null) return null;
return { vfs: r.vfs, exists: r.vfs.existsSync(filename) };
}
function findVFSForPath(filename) {
const r = findVFS(filename);
if (r === null) return null;
return { vfs: r.vfs, path: filename };
}
function findVFSWith(filename, syscall, fn) {
const r = findVFS(filename);
if (r === null) return undefined;
if (r.vfs.existsSync(filename)) {
if (fn === undefined) return true;
return fn(r.vfs, filename);
}
throw createENOENT(syscall, filename);
}
function vfsRead(path, syscall, fn) {
const pathStr = toPathStr(path);
if (pathStr === null) return undefined;
return findVFSWith(pathStr, syscall, fn);
}
function vfsOp(path, fn) {
const pathStr = toPathStr(path);
if (pathStr !== null) {
const r = findVFSForPath(pathStr);
if (r !== null) return fn(r.vfs, r.path);
}
return undefined;
}
function vfsOpVoid(path, fn) {
const pathStr = toPathStr(path);
if (pathStr !== null) {
const r = findVFSForPath(pathStr);
if (r !== null) { fn(r.vfs, r.path); return true; }
}
return undefined;
}
function checkSameVFS(srcPath, destPath, syscall, srcVfs) {
if (findVFS(destPath)?.vfs !== srcVfs) {
throw createEXDEV(syscall, srcPath);
}
}
// Nothing in the root directory can be removed, and a recursive removal of
// it would walk into the layers' mount points, which only the layers serve,
// so it is refused before it starts.
function rmRootSync(path, options) {
try {
rootVFS.lstatSync(path);
} catch (err) {
if (options?.force === true && err?.code === 'ENOENT') return;
throw err;
}
throw createEROFS('rm', path);
}
function createVfsHandlers() {
return {
__proto__: null,
existsSync(path) {
const pathStr = toPathStr(path);
if (pathStr === null) return undefined;
const r = findVFSForExists(pathStr);
return r !== null ? r.exists : undefined;
},
readFileSync(path, options) {
if (typeof path === 'number') {
const vfd = getVirtualFd(path);
if (vfd) {
const enc = typeof options === 'string' ? options : options?.encoding;
if (enc && enc !== 'buffer') assertEncoding(enc);
return vfd.entry.readFileSync(options);
}
return undefined;
}
const pathStr = toPathStr(path);
if (pathStr === null) return undefined;
const enc = typeof options === 'string' ? options : options?.encoding;
if (enc && enc !== 'buffer') assertEncoding(enc);
return findVFSWith(pathStr, 'open', (vfs, n) => vfs.readFileSync(n, options));
},
readdirSync(path, options) {
const result = vfsRead(path, 'scandir', (vfs, n) => vfs.readdirSync(n, options));
if (result !== undefined && options?.encoding === 'buffer' && !options?.withFileTypes) {
for (let i = 0; i < result.length; i++) {
if (typeof result[i] === 'string') result[i] = Buffer.from(result[i]);
}
}
return result;
},
lstatSync(path, options) {
const pathStr = toPathStr(path);
if (pathStr === null) return undefined;
const r = findVFS(pathStr);
if (r === null) return undefined;
return r.vfs.lstatSync(pathStr, options);
},
statSync(path, options) {
return vfsRead(path, 'stat', (vfs, n) => vfs.statSync(n, options));
},
realpathSync(path, options) {
return vfsRead(path, 'realpath', (vfs, n) => vfs.realpathSync(n, options));
},
accessSync(path, mode) {
const pathStr = toPathStr(path);
if (pathStr !== null) {
const r = findVFSForPath(pathStr);
if (r !== null) {
if (mode != null && typeof mode !== 'number') {
throw new ERR_INVALID_ARG_TYPE('mode', 'integer', mode);
}
r.vfs.accessSync(r.path, mode);
return true;
}
}
return undefined;
},
readlinkSync(path, options) {
const pathStr = toPathStr(path);
if (pathStr === null) return undefined;
const r = findVFS(pathStr);
if (r === null) return undefined;
return r.vfs.readlinkSync(pathStr, options);
},
statfsSync(path, options) {
const pathStr = toPathStr(path);
if (pathStr !== null && findVFSWith(pathStr, 'statfs')) {
if (options?.bigint) {
return {
type: 0n, bsize: 4096n, blocks: 0n,
bfree: 0n, bavail: 0n, files: 0n, ffree: 0n,
};
}
return { type: 0, bsize: 4096, blocks: 0, bfree: 0, bavail: 0, files: 0, ffree: 0 };
}
return undefined;
},
writeFileSync(path, data, options) {
if (typeof path === 'number') return writeFileSyncFd(path, data, options);
return vfsOpVoid(path, (vfs, n) => vfs.writeFileSync(n, data, options));
},
appendFileSync(path, data, options) {
if (typeof path === 'number') return writeFileSyncFd(path, data, options);
return vfsOpVoid(path, (vfs, n) => vfs.appendFileSync(n, data, options));
},
mkdirSync: (path, options) =>
vfsOp(path, (vfs, n) => ({ result: vfs.mkdirSync(n, options) })),
rmdirSync: (path) => vfsOpVoid(path, (vfs, n) => vfs.rmdirSync(n)),
rmSync(path, options) {
return vfsOpVoid(path, (vfs, n) => {
if (vfs === rootVFS) rmRootSync(n, options);
else vfs.rmSync(n, options);
});
},
unlinkSync: (path) => vfsOpVoid(path, (vfs, n) => vfs.unlinkSync(n)),
renameSync(oldPath, newPath) {
return vfsOpVoid(oldPath, (vfs, n) => {
const destStr = toPathStr(newPath);
checkSameVFS(n, destStr, 'rename', vfs);
vfs.renameSync(n, destStr);
});
},
copyFileSync(src, dest, mode) {
return vfsOpVoid(src, (vfs, n) => {
const destStr = toPathStr(dest);
checkSameVFS(n, destStr, 'copyfile', vfs);
vfs.copyFileSync(n, destStr, mode);
});
},
symlinkSync: (target, path, type) =>
vfsOpVoid(path, (vfs, n) => vfs.symlinkSync(target, n, type)),
chmodSync: (path, mode) => vfsOpVoid(path, (vfs, n) => vfs.chmodSync(n, mode)),
chownSync: (path, uid, gid) => vfsOpVoid(path, (vfs, n) => vfs.chownSync(n, uid, gid)),
lchownSync: (path, uid, gid) => vfsOpVoid(path, (vfs, n) => vfs.lchownSync(n, uid, gid)),
utimesSync: (path, atime, mtime) =>
vfsOpVoid(path, (vfs, n) => vfs.utimesSync(n, atime, mtime)),
lutimesSync: (path, atime, mtime) =>
vfsOpVoid(path, (vfs, n) => vfs.lutimesSync(n, atime, mtime)),
truncateSync: (path, len) => vfsOpVoid(path, (vfs, n) => vfs.truncateSync(n, len)),
linkSync(existingPath, newPath) {
return vfsOpVoid(existingPath, (vfs, n) => {
const destStr = toPathStr(newPath);
checkSameVFS(n, destStr, 'link', vfs);
vfs.linkSync(n, destStr);
});
},
mkdtempSync(prefix, options) {
return vfsOp(prefix, (vfs, n) => {
options = getMkdtempOptions(prefix, options);
return encodeRealpathResult(vfs.mkdtempSync(n), options);
});
},
mkstempSync(prefix, options) {
const result = vfsOp(prefix, (vfs, n) => vfs.mkstempSync(n));
if (result !== undefined && options?.encoding === 'buffer') {
result.path = Buffer.from(result.path);
}
return result;
},
opendirSync: (path, options) => vfsOp(path, (vfs, n) => vfs.opendirSync(n, options)),
openAsBlob(path, options) {
const pathStr = toPathStr(path);
if (pathStr !== null) {
const r = findVFS(pathStr);
if (r !== null && r.vfs.existsSync(pathStr)) {
return r.vfs.openAsBlob(pathStr, options);
}
}
return undefined;
},
openAsBlobSync: (path, options) =>
vfsRead(path, 'stat', (vfs, n) => vfs.openAsBlob(n, options)),
openSync: (path, flags, mode) => vfsOp(path, (vfs, n) => vfs.openSync(n, flags, mode)),
closeSync(fd) {
const vfd = getVirtualFd(fd);
if (vfd) { vfd.entry.closeSync(); closeVirtualFd(fd); return true; }
return undefined;
},
readSync(fd, buffer, offset, length, position) {
const vfd = getVirtualFd(fd);
if (vfd) return vfd.entry.readSync(buffer, offset, length, position);
return undefined;
},
writeSync(fd, buffer, offset, length, position) {
const vfd = getVirtualFd(fd);
if (vfd) return vfd.entry.writeSync(buffer, offset, length, position);
return undefined;
},
fstatSync(fd, options) {
const vfd = getVirtualFd(fd);
if (vfd) return vfd.entry.statSync(options);
return undefined;
},
ftruncateSync(fd, len) {
const vfd = getVirtualFd(fd);
if (vfd) { vfd.entry.truncateSync(len); return true; }
return undefined;
},
fchmodSync(fd, mode) {
const vfd = getVirtualFd(fd);
if (vfd) { vfd.entry.chmodSync(mode); return true; }
return undefined;
},
fchownSync: noopFdSync,
futimesSync(fd, atime, mtime) {
const vfd = getVirtualFd(fd);
if (vfd) { vfd.entry.utimesSync(atime, mtime); return true; }
return undefined;
},
fdatasyncSync: noopFdSync,
fsyncSync: noopFdSync,
readvSync(fd, buffers, position) {
const vfd = getVirtualFd(fd);
if (!vfd) return undefined;
let totalRead = 0;
for (let i = 0; i < buffers.length; i++) {
const buf = buffers[i];
const pos = position != null ? position + totalRead : position;
const bytesRead = vfd.entry.readSync(buf, 0, buf.byteLength, pos);
totalRead += bytesRead;
if (bytesRead < buf.byteLength) break;
}
return totalRead;
},
writevSync(fd, buffers, position) {
const vfd = getVirtualFd(fd);
if (!vfd) return undefined;
let totalWritten = 0;
for (let i = 0; i < buffers.length; i++) {
const buf = buffers[i];
const pos = position != null ? position + totalWritten : position;
const bytesWritten = vfd.entry.writeSync(buf, 0, buf.byteLength, pos);
totalWritten += bytesWritten;
if (bytesWritten < buf.byteLength) break;
}
return totalWritten;
},
close(fd) {
const vfd = getVirtualFd(fd);
if (!vfd) return undefined;
return vfd.entry.close().then(() => { closeVirtualFd(fd); return true; });
},
read(fd, buffer, offset, length, position) {
const vfd = getVirtualFd(fd);
if (!vfd) return undefined;
return vfd.entry.read(buffer, offset, length, position)
.then(({ bytesRead }) => bytesRead);
},
write(fd, buffer, offset, length, position) {
const vfd = getVirtualFd(fd);
if (!vfd) return undefined;
return vfd.entry.write(buffer, offset, length, position)
.then(({ bytesWritten }) => bytesWritten);
},
fstat(fd, options) {
const vfd = getVirtualFd(fd);
if (!vfd) return undefined;
return vfd.entry.stat(options);
},
ftruncate(fd, len) {
const vfd = getVirtualFd(fd);
if (!vfd) return undefined;
return vfd.entry.truncate(len).then(() => true);
},
fchmod(fd, mode) {
const vfd = getVirtualFd(fd);
if (!vfd) return undefined;
return vfd.entry.chmod(mode).then(() => true);
},
fchown: noopFd,
futimes(fd, atime, mtime) {
const vfd = getVirtualFd(fd);
if (!vfd) return undefined;
return vfd.entry.utimes(atime, mtime).then(() => true);
},
fdatasync: noopFd,
fsync: noopFd,
createReadStream(path, options) {
const pathStr = toPathStr(path);
if (pathStr !== null) {
const r = findVFSForPath(pathStr);
if (r !== null) return r.vfs.createReadStream(r.path, options);
}
return undefined;
},
createWriteStream(path, options) {
const pathStr = toPathStr(path);
if (pathStr !== null) {
const r = findVFSForPath(pathStr);
if (r !== null) return r.vfs.createWriteStream(r.path, options);
}
return undefined;
},
watch(filename, options, listener) {
if (typeof options === 'function') {
listener = options;
options = kEmptyObject;
} else if (options != null) {
validateObject(options, 'options');
} else {
options = kEmptyObject;
}
const pathStr = toPathStr(filename);
if (pathStr !== null) {
const r = findVFSForPath(pathStr);
if (r !== null) {
if (!r.vfs.existsSync(pathStr)) throw createENOENT('watch', pathStr);
return r.vfs.watch(pathStr, options, listener);
}
}
return undefined;
},
watchFile(filename, options, listener) {
const pathStr = toPathStr(filename);
if (pathStr === null) return undefined;
const r = findVFSForPath(pathStr);
if (r === null) return undefined;
if (options === null || typeof options !== 'object') {
listener = options;
options = kEmptyObject;
}
return r.vfs.watchFile(pathStr, options, listener);
},
unwatchFile(filename, listener) {
const pathStr = toPathStr(filename);
if (pathStr === null) return undefined;
const r = findVFSForPath(pathStr);
if (r === null) return undefined;
r.vfs.unwatchFile(pathStr, listener);
return true;
},
promisesWatch(filename, options) {
const pathStr = toPathStr(filename);
if (pathStr === null) return undefined;
const r = findVFSForPath(pathStr);
if (r === null) return undefined;
if (!r.vfs.existsSync(pathStr)) throw createENOENT('watch', pathStr);
return r.vfs.promises.watch(pathStr, options);
},
readdir(path, options) {
const promise = vfsOp(path, (vfs, n) => vfs.promises.readdir(n, options));
if (promise !== undefined && options?.encoding === 'buffer' && !options?.withFileTypes) {
return promise.then((result) => {
for (let i = 0; i < result.length; i++) {
if (typeof result[i] === 'string') result[i] = Buffer.from(result[i]);
}
return result;
});
}
return promise;
},
lstat(path, options) {
const pathStr = toPathStr(path);
if (pathStr === null) return undefined;
const r = findVFS(pathStr);
if (r === null) return undefined;
return r.vfs.promises.lstat(pathStr, options);
},
stat(path, options) {
const promise = vfsOp(path, (vfs, n) => vfs.promises.stat(n, options));
if (promise !== undefined && options?.throwIfNoEntry === false) {
return promise.catch((err) => {
if (err?.code === 'ENOENT') return undefined;
throw err;
});
}
return promise;
},
readFile(path, options) {
if (typeof path === 'number') {
const vfd = getVirtualFd(path);
if (vfd) {
const enc = typeof options === 'string' ? options : options?.encoding;
if (enc && enc !== 'buffer') assertEncoding(enc);
return vfd.entry.readFile(options);
}
return undefined;
}
const enc = typeof options === 'string' ? options : options?.encoding;
if (enc && enc !== 'buffer') assertEncoding(enc);
return vfsOp(path, (vfs, n) => vfs.promises.readFile(n, options));
},
realpath(path, options) {
return vfsOp(path, (vfs, n) => vfs.promises.realpath(n, options));
},
access(path, mode) {
return vfsOp(path, (vfs, n) => {
if (mode != null && typeof mode !== 'number') {
throw new ERR_INVALID_ARG_TYPE('mode', 'integer', mode);
}
return vfs.promises.access(n, mode).then(() => true);
});
},
readlink(path, options) {
const pathStr = toPathStr(path);
if (pathStr === null) return undefined;
const r = findVFS(pathStr);
if (r === null) return undefined;
return r.vfs.promises.readlink(pathStr, options);
},
chown: (path, uid, gid) =>
vfsOp(path, (vfs, n) => vfs.promises.chown(n, uid, gid).then(() => true)),
lchown: (path, uid, gid) =>
vfsOp(path, (vfs, n) => vfs.promises.lchown(n, uid, gid).then(() => true)),
lutimes: (path, atime, mtime) =>
vfsOp(path, (vfs, n) => vfs.promises.lutimes(n, atime, mtime).then(() => true)),
statfs(path, options) {
const pathStr = toPathStr(path);
if (pathStr !== null && findVFSWith(pathStr, 'statfs')) {
if (options?.bigint) {
return {
__proto__: null,
type: 0n, bsize: 4096n, blocks: 0n,
bfree: 0n, bavail: 0n, files: 0n, ffree: 0n,
};
}
return {
__proto__: null,
type: 0, bsize: 4096, blocks: 0,
bfree: 0, bavail: 0, files: 0, ffree: 0,
};
}
return undefined;
},
writeFile(path, data, options) {
return vfsOp(path, (vfs, n) => vfs.promises.writeFile(n, data, options).then(() => true));
},
appendFile(path, data, options) {
return vfsOp(path, (vfs, n) => vfs.promises.appendFile(n, data, options).then(() => true));
},
mkdir(path, options) {
return vfsOp(path, (vfs, n) =>
vfs.promises.mkdir(n, options).then((result) => ({ __proto__: null, result })));
},
rmdir: (path) => vfsOp(path, (vfs, n) => vfs.promises.rmdir(n).then(() => true)),
rm(path, options) {
return vfsOp(path, async (vfs, n) => {
if (vfs === rootVFS) rmRootSync(n, options);
else await vfs.promises.rm(n, options);
return true;
});
},
unlink: (path) => vfsOp(path, (vfs, n) => vfs.promises.unlink(n).then(() => true)),
rename(oldPath, newPath) {
return vfsOp(oldPath, (vfs, n) => {
const destStr = toPathStr(newPath);
checkSameVFS(n, destStr, 'rename', vfs);
return vfs.promises.rename(n, destStr).then(() => true);
});
},
copyFile(src, dest, mode) {
return vfsOp(src, (vfs, n) => {
const destStr = toPathStr(dest);
checkSameVFS(n, destStr, 'copyfile', vfs);
return vfs.promises.copyFile(n, destStr, mode).then(() => true);
});
},
symlink(target, path, type) {
return vfsOp(path, (vfs, n) => vfs.promises.symlink(target, n, type).then(() => true));
},
truncate: (path, len) =>
vfsOp(path, (vfs, n) => vfs.promises.truncate(n, len).then(() => true)),
link(existingPath, newPath) {
return vfsOp(existingPath, (vfs, n) => {
const destStr = toPathStr(newPath);
checkSameVFS(n, destStr, 'link', vfs);
return vfs.promises.link(n, destStr).then(() => true);
});
},
mkdtemp(prefix, options) {
return vfsOp(prefix, (vfs, n) => {
options = getMkdtempOptions(prefix, options);
return vfs.promises.mkdtemp(n).then((result) => encodeRealpathResult(result, options));
});
},
// Wrapped in async functions so provider throws become rejections.
mkstemp(prefix, options) {
return vfsOp(prefix, async (vfs, n) => {
const { path, fd } = vfs.mkstempSync(n);
return {
__proto__: null,
path: options?.encoding === 'buffer' ? Buffer.from(path) : path,
fd,
};
});
},
promisesMkstemp(prefix, options) {
return vfsOp(prefix, async (vfs, n) => {
const { path, fd } = vfs.mkstempSync(n);
return {
__proto__: null,
path: options?.encoding === 'buffer' ? Buffer.from(path) : path,
handle: createVfsFileHandle(getVirtualFd(fd)),
};
});
},
chmod: (path, mode) =>
vfsOp(path, (vfs, n) => vfs.promises.chmod(n, mode).then(() => true)),
utimes: (path, atime, mtime) =>
vfsOp(path, (vfs, n) => vfs.promises.utimes(n, atime, mtime).then(() => true)),
open(path, flags, mode) {
// Wrap openSync in an async fn so provider throws become rejections
// instead of escaping past fs.open's callback.
return vfsOp(path, async (vfs, n) => vfs.openSync(n, flags, mode));
},
promisesOpen(path, flags, mode) {
const pathStr = toPathStr(path);
if (pathStr !== null) {
const r = findVFSForPath(pathStr);
if (r !== null) {
const fd = r.vfs.openSync(r.path, flags, mode);
return PromiseResolve(createVfsFileHandle(getVirtualFd(fd)));
}
}
return undefined;
},
lchmod: (path, mode) =>
vfsOp(path, (vfs, n) => vfs.promises.lchmod(n, mode).then(() => true)),
};
}
/**
* Install toggleable loader overrides so that the module loader's
* internal fs operations (stat, readFile, realpath) are redirected
* to VFS when appropriate.
*/
function installModuleLoaderOverrides() {
const {
legacyMainResolveExtensions,
legacyMainResolveExtensionsIndexes,
kLoaderOverrideNoResult,
setLoaderOverrides,
} = require('internal/modules/helpers');
const { kResolvedByMainIndexNode } = legacyMainResolveExtensionsIndexes;
const { internal: internalConstants } = internalBinding('constants');
// Overrides return `undefined` for paths the VFS does not own;
// wrapLoaderMethod then falls through to the native binding.
setLoaderOverrides({
internalModuleStat(filename) {
const r = findVFSOrRoot(filename);
if (r === null) return undefined;
return r.vfs === null ? UV_ENOENT : vfsStat(r.vfs, filename);
},
readFileSync(filename, options) {
const pathStr = typeof filename === 'string' ? filename :
(filename instanceof URL ? fileURLToPath(filename) : String(filename));
const r = findVFSOrRoot(pathStr);
if (r === null) return undefined;
if (r.vfs === null) throw createENOENT('open', pathStr);
return readVFS(r.vfs, pathStr, options);
},
realpathSync(filename) {
const r = findVFSOrRoot(filename);
if (r === null) return undefined;
if (r.vfs === null || !r.vfs.existsSync(filename)) {
throw createENOENT('realpath', filename);
}
return r.vfs.realpathSync(filename);
},
getResolutionRoot(pathStr) {
const r = findVFSOrRoot(pathStr);
if (r === null) return undefined;
// The boundary is compared as a plain string prefix by the
// callers, so only report it when the input carries it verbatim.
// An unowned path stops at the reserved root itself, so no
// node_modules lookup walks out into the real file system.
const boundary = r.vfs === null ?
getNormalizedVfsRoot() : r.vfs.mountPoint;
return StringPrototypeStartsWith(pathStr, boundary) ?
boundary : undefined;
},
legacyMainResolve(pkgPath, main, base) {
if (findVFSOrRoot(pkgPath) === null) return undefined;
for (let i = 0; i < legacyMainResolveExtensions.length; i++) {
const byMain = i <= kResolvedByMainIndexNode;
if (byMain && !main) continue;
const prefix = byMain ? main : '';
const candidate = join(pkgPath, prefix + legacyMainResolveExtensions[i]);
if (findVFSForStat(candidate)?.result === 0) return i;
}
// Third arg `exactUrl` must be undefined (not a string) so the
// message uses the "package" word and err.url is not overwritten.
// ERR_MODULE_NOT_FOUND enforces strict arity in getMessage(), so
// the undefined has to be passed explicitly.
const initial = main ? join(pkgPath, main) : join(pkgPath, 'index.js');
throw new ERR_MODULE_NOT_FOUND(initial, base, undefined);
},
getFormatOfExtensionlessFile(filePath) {
const r = findVFSOrRoot(filePath);
if (r === null) return undefined;
let content;
try {
content = r.vfs === null ? null : readVFS(r.vfs, filePath, null);
} catch {
return internalConstants.EXTENSIONLESS_FORMAT_JAVASCRIPT;
}
// Wasm magic bytes: 0x00 0x61 0x73 0x6d
if (content && content.length >= 4 &&
content[0] === 0x00 && content[1] === 0x61 &&
content[2] === 0x73 && content[3] === 0x6d) {
return internalConstants.EXTENSIONLESS_FORMAT_WASM;
}
return internalConstants.EXTENSIONLESS_FORMAT_JAVASCRIPT;
},
readPackageJSON(jsonPath, isESM, base, specifier) {
const r = findVFSOrRoot(jsonPath);
if (r === null) return undefined;
if (r.vfs === null) return kLoaderOverrideNoResult;
const { vfs } = r;
if (vfsStat(vfs, jsonPath) !== 0) return kLoaderOverrideNoResult;
let content;
try {
content = vfs.readFileSync(jsonPath);
} catch {
return kLoaderOverrideNoResult;
}
// Both CJS and ESM raise ERR_INVALID_PACKAGE_CONFIG since
// nodejs/node#48606.
return nativeModulesBinding.parsePackageJSON(
content, jsonPath, isESM, base, specifier);
},
getNearestParentPackageJSON(checkPath) {
const r = findVFSOrRoot(checkPath);
if (r === null) return undefined;
if (r.vfs === null) return kLoaderOverrideNoResult;
const found = findVFSPackageJSON(r.vfs, checkPath, r.normalized);
return found.tuple ?? kLoaderOverrideNoResult;
},
getNearestParentPackageJSONType(checkPath) {
const r = findVFSOrRoot(checkPath);
if (r === null) return undefined;
if (r.vfs === null) return kLoaderOverrideNoResult;
const found = findVFSPackageJSON(r.vfs, checkPath, r.normalized);
// Tuple shape: [name, main, type, imports, exports, filePath]. No
// package.json above the path is "no result", which the caller reads
// the same way it reads a scope without a `type`.
return found.tuple?.[2] ?? kLoaderOverrideNoResult;
},
getPackageScopeConfig(resolved) {
let filePath;
if (StringPrototypeStartsWith(resolved, 'file:')) {
try {
filePath = fileURLToPath(resolved);
} catch {
return undefined;
}
} else {
filePath = resolved;
}
const r = findVFSOrRoot(filePath);
if (r === null) return undefined;
// The "not found" marker is the package.json beside the queried
// path, which is what the native binding reports for it.
if (r.vfs === null) return join(dirname(filePath), 'package.json');
const found = findVFSPackageJSON(r.vfs, filePath, r.normalized);
if (found.tuple !== undefined) return found.tuple;
return found.sentinel;
},
getPackageType(url) {
let filePath;
if (StringPrototypeStartsWith(url, 'file:')) {
try {
filePath = fileURLToPath(url);
} catch {
return undefined;
}
} else {
filePath = url;
}
const r = findVFSOrRoot(filePath);
if (r === null) return undefined;
if (r.vfs === null) return kLoaderOverrideNoResult;
const found = findVFSPackageJSON(r.vfs, filePath, r.normalized);
if (found.tuple !== undefined) {
// Tuple shape: [name, main, type, imports, exports, filePath].
const type = found.tuple[2];
if (type === 'module' || type === 'commonjs') return type;
}
return kLoaderOverrideNoResult;
},
});
}
let originalDlopen;
function installAddonLoader() {
originalDlopen = process.dlopen;
process.dlopen = function(module, filename, flags) {
// dlopen(2) cannot open a native addon that lives in a VFS by path (it has
// no real inode). Read its bytes and hand them to the internal
// dlopenBinary(), which writes them to a private, self-cleaning temporary
// image - an in-memory memfd on Linux - and loads that. Only VFS paths take
// this route; everything else loads straight from disk through the
// unchanged process.dlopen().
if (StringPrototypeStartsWith(filename, normalizedVfsRootPrefix)) {
const { readFileSync } = require('fs');
const { dlopenBinary } = internalBinding('process_methods');
return dlopenBinary(module, filename, flags, readFileSync(filename));
}
// Do not forward a missing flags argument as `undefined`:
// process.dlopen() coerces it to 0, which is not a valid dlopen(2)
// mode, instead of applying the default flags.
if (flags === undefined) return originalDlopen(module, filename);
return originalDlopen(module, filename, flags);
};
}
/**
* Reads the bytes of a file that lives in a mounted VFS. Returns undefined
* for a path outside the reserved VFS root - the caller should open the
* path itself - and throws ENOENT for a path under the root that no
* mounted VFS serves, since no real file can exist there. Installed into
* node:ffi while hooks are installed, so DynamicLibrary can load a
* VFS-resident library from a private image, the same way the module
* loader handles a native addon in a VFS.
* @param {string} pathStr The path of the library
* @returns {Buffer|undefined} The library's bytes, or undefined
*/
function readVirtualBinary(pathStr) {
const normalized = normalizeMountedPath(pathStr);
if (!StringPrototypeStartsWith(normalized, normalizedVfsRootPrefix)) {
return undefined;
}
const layerId = getLayerIdFromPath(normalized);
const vfs = layerId === -1 ? undefined : activeVFSLayers.get(layerId);
if (vfs === undefined || !vfs.shouldHandleNormalized(normalized)) {
throw createENOENT('open', pathStr);
}
return vfs.readFileSync(normalized);
}
/**
* Install all VFS hooks: module loader overrides and fs handlers.
*/
function installHooks() {
if (hooksInstalled) return;
debug('install hooks');
normalizedVfsRoot = getNormalizedVfsRoot();
normalizedVfsRootPrefix = normalizedVfsRoot + sep;
rootVFS ??= new VirtualFileSystem(
new ReservedRootProvider(activeVFSLayers),
{ __proto__: null, emitExperimentalWarning: false, [kReservedRoot]: true });
installModuleLoaderOverrides();
installAddonLoader();
const { setVfsLibraryReader } = require('internal/ffi/vfs');
setVfsLibraryReader(readVirtualBinary);
vfsHandlerObj = createVfsHandlers();
setVfsHandlers(vfsHandlerObj);
hooksInstalled = true;
}
/**
* Tear down all VFS hooks when the last instance is deregistered. The
* fast path in the loader wrappers is restored so subsequent require/
* import calls pay zero overhead until another VFS is mounted.
*/
function uninstallHooks() {
if (!hooksInstalled) return;
debug('uninstall hooks');
const { setLoaderOverrides } = require('internal/modules/helpers');
setLoaderOverrides();
setVfsHandlers(null);
vfsHandlerObj = undefined;
const { setVfsLibraryReader } = require('internal/ffi/vfs');
setVfsLibraryReader(null);
process.dlopen = originalDlopen;
hooksInstalled = false;
}
module.exports = {
registerVFS,
deregisterVFS,
};