Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 74 additions & 36 deletions lib/internal/vfs/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const { assertEncoding, 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;
});
Expand Down Expand Up @@ -115,29 +116,50 @@ function deregisterVFS(vfs) {
}

/**
* Resolves a path string to the active VFS that owns it, or null.
* 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
* Resolves a path string to the reserved VFS root, or null for a path
* outside 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.
*
* A path under the root that no active layer owns comes 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, normalized: string }|null}
* @returns {{ vfs: object|null, normalized: string }|null}
*/
function findVFS(inputPath) {
function findVFSOrRoot(inputPath) {
const normalized = normalizeMountedPath(inputPath);
if (!StringPrototypeStartsWith(normalized, normalizedVfsRootPrefix)) {
return null;
}
const layerId = getLayerIdFromPath(normalized);
if (layerId === -1) return null;
const vfs = activeVFSLayers.get(layerId);
const vfs = layerId === -1 ? undefined : activeVFSLayers.get(layerId);
if (vfs === undefined || !vfs.shouldHandleNormalized(normalized)) {
return null;
return { vfs: null, normalized };
}
return { vfs, normalized };
}

/**
* Resolves a path string to the active VFS that owns it, or null.
* @param {string} inputPath
* @returns {{ vfs: object, normalized: string }|null}
*/
function findVFS(inputPath) {
const r = findVFSOrRoot(inputPath);
return r === null || r.vfs === null ? null : r;
}

/**
* Drop the cache entries under `vfs`'s mount point from the
* JS-reachable loader caches. Real-fs entries and other-VFS entries
Expand Down Expand Up @@ -210,16 +232,16 @@ function findVFSForStat(filename) {
}

/**
* Finds the VFS owning `filename` and reads it.
* 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 {{ vfs: object, content: Buffer|string }|null}
* @returns {Buffer|string}
*/
function findVFSForRead(filename, options) {
const r = findVFS(filename);
if (r === null) return null;
function readVFS(vfs, filename, options) {
try {
return { vfs: r.vfs, content: r.vfs.readFileSync(filename, options) };
return vfs.readFileSync(filename, options);
} catch (e) {
const code = e?.code;
if (code === 'ENOENT' || code === 'EISDIR') {
Expand Down Expand Up @@ -794,30 +816,40 @@ function installModuleLoaderOverrides() {
// wrapLoaderMethod then falls through to the native binding.
setLoaderOverrides({
internalModuleStat(filename) {
const result = findVFSForStat(filename);
return result !== null ? result.result : undefined;
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 result = findVFSForRead(pathStr, options);
return result !== null ? result.content : undefined;
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) {
return findVFSWith(filename, 'realpath', (vfs, n) => vfs.realpathSync(n));
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 = findVFS(pathStr);
const r = findVFSOrRoot(pathStr);
if (r === null) return undefined;
const mountPoint = r.vfs.mountPoint;
// The boundary is compared as a plain string prefix by the
// callers, so only report it when the input carries the mount
// point verbatim.
return StringPrototypeStartsWith(pathStr, mountPoint) ?
mountPoint : undefined;
// 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 (findVFS(pkgPath) === null) return undefined;
if (findVFSOrRoot(pkgPath) === null) return undefined;

for (let i = 0; i < legacyMainResolveExtensions.length; i++) {
const byMain = i <= kResolvedByMainIndexNode;
Expand All @@ -835,14 +867,14 @@ function installModuleLoaderOverrides() {
throw new ERR_MODULE_NOT_FOUND(initial, base, undefined);
},
getFormatOfExtensionlessFile(filePath) {
let result;
const r = findVFSOrRoot(filePath);
if (r === null) return undefined;
let content;
try {
result = findVFSForRead(filePath, null);
content = r.vfs === null ? null : readVFS(r.vfs, filePath, null);
} catch {
return internalConstants.EXTENSIONLESS_FORMAT_JAVASCRIPT;
}
if (result === null) return undefined;
const content = result.content;
// Wasm magic bytes: 0x00 0x61 0x73 0x6d
if (content && content.length >= 4 &&
content[0] === 0x00 && content[1] === 0x61 &&
Expand All @@ -852,8 +884,9 @@ function installModuleLoaderOverrides() {
return internalConstants.EXTENSIONLESS_FORMAT_JAVASCRIPT;
},
readPackageJSON(jsonPath, isESM, base, specifier) {
const r = findVFS(jsonPath);
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;
Expand All @@ -868,8 +901,9 @@ function installModuleLoaderOverrides() {
content, jsonPath, isESM, base, specifier);
},
getNearestParentPackageJSON(checkPath) {
const r = findVFS(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;
},
Expand All @@ -884,8 +918,11 @@ function installModuleLoaderOverrides() {
} else {
filePath = resolved;
}
const r = findVFS(filePath);
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;
Expand All @@ -901,8 +938,9 @@ function installModuleLoaderOverrides() {
} else {
filePath = url;
}
const r = findVFS(filePath);
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].
Expand Down
59 changes: 59 additions & 0 deletions test/parallel/test-vfs-reserved-root-unowned.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Flags: --experimental-vfs --expose-internals
'use strict';

// The module loader manufactures paths under the reserved VFS root that no
// layer owns: 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. Such paths cannot name anything real, but
// they must still be answered by the VFS instead of being handed to the native
// loader: on Windows the reserved root sits under `\\.\nul`, and
// `\\.\nul\<anything>` opens the NUL device, which stats as a character device
// and reads as empty. The native loader would then "find" a file at
// `<mount>.js` and reject the empty package.json above it as invalid JSON.

require('../common');
const assert = require('assert');
const path = require('path');
const { pathToFileURL } = require('url');
const vfs = require('node:vfs');
const { loaderMethods } = require('internal/modules/helpers');
const { getNormalizedVfsRoot } = require('internal/vfs/router');

const layer = vfs.create();
layer.writeFileSync('/index.js', 'module.exports = "ran";');
const mountPoint = layer.mount();

const root = getNormalizedVfsRoot();
const unowned = [
`${mountPoint}.js`,
`${mountPoint}.json`,
`${mountPoint}.node`,
path.join(root, 'package.json'),
path.join(root, 'nope', 'index.js'),
];

for (const p of unowned) {
assert.ok(loaderMethods.internalModuleStat(p) < 0, p);
assert.throws(() => loaderMethods.readFileSync(p), { code: 'ENOENT' }, p);
assert.throws(() => loaderMethods.realpathSync(p), { code: 'ENOENT' }, p);
assert.strictEqual(loaderMethods.getNearestParentPackageJSON(p), undefined, p);
assert.strictEqual(loaderMethods.readPackageJSON(p, false), undefined, p);
assert.strictEqual(loaderMethods.getPackageType(pathToFileURL(p).href), undefined, p);
// The "not found" marker is the last candidate examined, like the native
// binding returns.
assert.strictEqual(
loaderMethods.getPackageScopeConfig(pathToFileURL(p).href),
path.join(path.dirname(p), 'package.json'), p);
// Upward walks (node_modules lookups) stop at the reserved root rather than
// continuing into the real file system.
assert.strictEqual(loaderMethods.getResolutionRoot(p), root, p);
}

// Paths outside the reserved root are still left to the native loader.
assert.strictEqual(loaderMethods.getResolutionRoot(__filename), undefined);

// The mount point itself resolves as a directory to its index through the
// layer, which is the sequence that produced the sibling probes above.
assert.strictEqual(require(mountPoint), 'ran');

layer.unmount();
Loading