Skip to content

Commit a17ee29

Browse files
committed
vfs: drop mount() prefix argument and layer- path segment
`mount()` no longer accepts an argument. The user-supplied prefix served no purpose beyond a cosmetic label - actual layer identity comes from the per-instance `layerId`, so distinct instances never shared a mount point even when they passed the same prefix. Removing the argument eliminates the input validation, the escape check for `..` segments, and the API-shape doubt about what a "logical prefix" means. The `layer-` prefix on the layer segment of the mount path is likewise gone: mount points are now `${os.devNull}/vfs/<id>` (for example `/dev/null/vfs/0`) instead of `${os.devNull}/vfs/layer-0/<prefix>`. The parser in `getLayerIdFromPath` simplifies to reading the digit segment immediately after the VFS root. Docs, tests and benchmark adapted. The dispatch benchmark still reports flat per-call latency across 1..10 mounted layers. Signed-off-by: Matteo Collina <hello@matteocollina.com>
1 parent de17e81 commit a17ee29

56 files changed

Lines changed: 215 additions & 262 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

benchmark/vfs/bench-fs-dispatch.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ function mountLayers(count) {
3535
const handles = [];
3636
for (let i = 0; i < count; i++) {
3737
const v = vfs.create();
38-
v.mount('/bench');
38+
v.mount();
3939
handles.push(v);
4040
}
4141
return handles;

doc/api/vfs.md

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ shape of the [`node:fs`][] API. All paths are POSIX-style and absolute
6363

6464
By default, the file tree is private to the VFS instance. To expose
6565
it through the global `node:fs` module, `require()`, and `import`,
66-
call [`vfs.mount(prefix)`][]; call [`vfs.unmount()`][] (or rely on a
66+
call [`vfs.mount()`][]; call [`vfs.unmount()`][] (or rely on a
6767
`using` declaration) to detach again.
6868

6969
## `vfs.create([provider][, options])`
@@ -112,15 +112,12 @@ added: v26.4.0
112112
* `emitExperimentalWarning` {boolean} Whether to emit the experimental
113113
warning. **Default:** `true`.
114114

115-
### `vfs.mount([prefix])`
115+
### `vfs.mount()`
116116

117117
<!-- YAML
118118
added: REPLACEME
119119
-->
120120

121-
* `prefix` {string} A logical name for the mount inside the reserved
122-
VFS namespace. Interpreted as a relative path; leading separators
123-
are ignored. **Default:** `'/'`.
124121
* Returns: {string} The absolute mount point.
125122

126123
Mounts the virtual file system and returns the resulting mount point.
@@ -129,33 +126,30 @@ After mounting, files in the VFS can be accessed through the
129126
using paths under the returned mount point.
130127

131128
Mount points always live inside a reserved namespace,
132-
`${os.devNull}/vfs/layer-<layerId>/`. Because [`os.devNull`][] is a
129+
`${os.devNull}/vfs/<layerId>`. Because [`os.devNull`][] is a
133130
character device (POSIX) or a device-namespace path (Windows) that
134131
cannot have child file system entries, no real file-system path can
135132
exist under this namespace: virtual paths never conflate with (or
136133
shadow) real paths, and the layer that owns a path is visible in the
137-
path itself. The `prefix` argument is a purely logical name inside
138-
the namespace. It is never resolved against the working directory,
139-
and a prefix that attempts to escape the namespace (for example with
140-
`..` segments) throws `ERR_INVALID_ARG_VALUE`.
134+
path itself.
141135

142136
```cjs
143137
const vfs = require('node:vfs');
144138
const fs = require('node:fs');
145139

146140
const myVfs = vfs.create();
147141
myVfs.writeFileSync('/data.txt', 'Hello');
148-
const mountPoint = myVfs.mount('/virtual');
149-
// e.g. '/dev/null/vfs/layer-0/virtual'
142+
const mountPoint = myVfs.mount();
143+
// e.g. '/dev/null/vfs/0'
150144

151145
fs.readFileSync(`${mountPoint}/data.txt`, 'utf8'); // 'Hello'
152146
```
153147

154148
Each `VirtualFileSystem` instance may be mounted at most once at a
155149
time. Attempting to mount an already-mounted instance throws
156150
`ERR_INVALID_STATE`. Because each instance mounts inside its own
157-
`layer-<layerId>` namespace, mounts from different instances can
158-
never overlap, even when they use the same `prefix`.
151+
per-layer namespace, mounts from different instances can never
152+
overlap.
159153

160154
The VFS supports the [Explicit Resource Management][] proposal. Use
161155
a `using` declaration to unmount automatically when leaving scope:
@@ -168,7 +162,7 @@ let mountPoint;
168162
{
169163
using myVfs = vfs.create();
170164
myVfs.writeFileSync('/data.txt', 'Hello');
171-
mountPoint = myVfs.mount('/virtual');
165+
mountPoint = myVfs.mount();
172166

173167
fs.readFileSync(`${mountPoint}/data.txt`, 'utf8'); // 'Hello'
174168
} // VFS is automatically unmounted here
@@ -184,8 +178,7 @@ added: REPLACEME
184178

185179
Unmounts the virtual file system. After unmounting, virtual files
186180
are no longer reachable through `node:fs`, `require()`, or `import`.
187-
The same instance may be mounted again, at the same or a different
188-
prefix, by calling `mount()`.
181+
The same instance may be mounted again by calling `mount()`.
189182

190183
This method is idempotent: calling `unmount()` on a VFS that is not
191184
currently mounted has no effect.
@@ -209,7 +202,7 @@ added: REPLACEME
209202
* {string | null}
210203

211204
The current mount point as an absolute string (the value returned by
212-
the last [`vfs.mount(prefix)`][] call), or `null` when the VFS is not
205+
the last [`vfs.mount()`][] call), or `null` when the VFS is not
213206
mounted.
214207

215208
### `vfs.layerId`
@@ -225,7 +218,7 @@ construction. The id is stable across `mount()` / `unmount()` cycles
225218
for the lifetime of the instance, and is independent of the order in
226219
which VFS layers are mounted.
227220

228-
The layer id forms the `layer-<id>` segment of the reserved mount
221+
The layer id forms the `<id>` segment of the reserved mount
229222
namespace, so every path served by this instance carries the id, and
230223
it appears in the `NODE_DEBUG=vfs` output for `register` and
231224
`deregister` events.
@@ -346,7 +339,7 @@ myVfs.mkdirSync('/lib');
346339
myVfs.writeFileSync('/lib/greet.js', 'module.exports = () => "hi";');
347340
myVfs.writeFileSync(
348341
'/lib/package.json', '{"main": "./greet.js"}');
349-
const mountPoint = myVfs.mount('/virtual');
342+
const mountPoint = myVfs.mount();
350343

351344
const greet = require(`${mountPoint}/lib`);
352345
console.log(greet()); // 'hi'
@@ -364,7 +357,7 @@ import vfs from 'node:vfs';
364357

365358
const myVfs = vfs.create();
366359
myVfs.writeFileSync('/mod.mjs', 'export const value = 42;');
367-
const mountPoint = myVfs.mount('/virtual');
360+
const mountPoint = myVfs.mount();
368361

369362
const { value } = await import(
370363
pathToFileURL(`${mountPoint}/mod.mjs`).href,
@@ -524,5 +517,5 @@ fields use synthetic but stable values:
524517
[`fs.Stats`]: fs.md#class-fsstats
525518
[`node:fs`]: fs.md
526519
[`os.devNull`]: os.md#osdevnull
527-
[`vfs.mount(prefix)`]: #vfsmountprefix
520+
[`vfs.mount()`]: #vfsmount
528521
[`vfs.unmount()`]: #vfsunmount

lib/internal/vfs/file_system.js

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,13 @@ const {
99

1010
const {
1111
codes: {
12-
ERR_INVALID_ARG_VALUE,
1312
ERR_INVALID_STATE,
1413
},
1514
} = require('internal/errors');
16-
const { validateBoolean, validateString } = require('internal/validators');
15+
const { validateBoolean } = require('internal/validators');
1716
const { MemoryProvider } = require('internal/vfs/providers/memory');
1817
const path = require('path');
19-
const { posix: pathPosix, join: platformJoin, resolve: resolvePath, toNamespacedPath } = path;
18+
const { posix: pathPosix, resolve: resolvePath, toNamespacedPath } = path;
2019
const { join: joinPath } = pathPosix;
2120
const {
2221
getLayerRoot,
@@ -169,37 +168,22 @@ class VirtualFileSystem {
169168
// ==================== Mount ====================
170169

171170
/**
172-
* Mounts the VFS inside this instance's reserved namespace and
173-
* returns the resulting mount point. The mount point always lives
174-
* under `${os.devNull}/vfs/layer-<layerId>/` - a location that
175-
* cannot exist on the real file system (`os.devNull` is a
176-
* character device on POSIX and a device-namespace path on Windows;
177-
* neither can have child filesystem entries) - so virtual paths
178-
* never conflate with real paths and the owning layer is decidable
179-
* from the path alone.
180-
* @param {string} [prefix] Logical prefix appended to the reserved
181-
* namespace. Interpreted as a relative path inside the namespace;
182-
* leading separators are ignored.
171+
* Mounts the VFS at its reserved mount point and returns that
172+
* mount point. The mount point is `${os.devNull}/vfs/<layerId>` -
173+
* a location that cannot exist on the real file system (`os.devNull`
174+
* is a character device on POSIX and a device-namespace path on
175+
* Windows; neither can have child filesystem entries) - so virtual
176+
* paths never conflate with real paths, and the owning layer of any
177+
* path is decidable from the path alone.
183178
* @returns {string} The absolute mount point
184179
*/
185-
mount(prefix = '/') {
180+
mount() {
186181
if (this[kMounted]) {
187182
throw new ERR_INVALID_STATE('VFS is already mounted');
188183
}
189-
validateString(prefix, 'prefix');
190-
const layerRoot = getLayerRoot(this[kLayerId]);
191-
// platformJoin treats an absolute prefix as relative to layerRoot;
192-
// resolvePath drops any trailing separator (e.g. for prefix '/').
193-
// The containment check rejects '..' segments escaping the
194-
// reserved namespace.
195-
const mountPoint = resolvePath(platformJoin(layerRoot, prefix));
196-
const normalized = normalizeMountedPath(mountPoint);
197-
if (!isUnderMountPoint(normalized, normalizeMountedPath(layerRoot))) {
198-
throw new ERR_INVALID_ARG_VALUE(
199-
'prefix', prefix, 'must not escape the VFS namespace');
200-
}
184+
const mountPoint = getLayerRoot(this[kLayerId]);
201185
this[kMountPoint] = mountPoint;
202-
this[kNormalizedMountPoint] = normalized;
186+
this[kNormalizedMountPoint] = normalizeMountedPath(mountPoint);
203187
this[kMounted] = true;
204188
debug('mount %s', mountPoint);
205189
loadVfsSetup();

lib/internal/vfs/router.js

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const {
99

1010
const { isAbsolute, relative, resolve, sep, toNamespacedPath } = require('path');
1111

12-
// All VFS mount points live under `${os.devNull}/vfs/layer-<id>/`.
12+
// All VFS mount points live under `${os.devNull}/vfs/<id>/`.
1313
// `os.devNull` is a character device on POSIX (`/dev/null`) and a
1414
// device-namespace path on Windows (`\\.\NUL`); neither can have child
1515
// filesystem entries, so no real file-system path can exist under this
@@ -32,30 +32,26 @@ function getNormalizedVfsRoot() {
3232
}
3333

3434
/**
35-
* Returns the reserved namespace root for a VFS layer:
36-
* `${os.devNull}/vfs/layer-<id>`.
35+
* Returns the reserved mount point for a VFS layer:
36+
* `${os.devNull}/vfs/<id>`.
3737
* @param {number} layerId
3838
* @returns {string}
3939
*/
4040
function getLayerRoot(layerId) {
41-
return getVfsRoot() + sep + 'layer-' + layerId;
41+
return getVfsRoot() + sep + layerId;
4242
}
4343

4444
/**
4545
* Extracts the layer id from a normalized path under the VFS root, or
46-
* returns -1 when the path does not carry a well-formed
47-
* `.../vfs/layer-<id>` segment. The caller must have verified that
48-
* `normalizedPath` starts with the normalized VFS root.
46+
* returns -1 when the path does not carry a well-formed `.../vfs/<id>`
47+
* segment. The caller must have verified that `normalizedPath` starts
48+
* with the normalized VFS root plus a separator.
4949
* @param {string} normalizedPath
5050
* @returns {number}
5151
*/
5252
function getLayerIdFromPath(normalizedPath) {
53-
// Skip '<root><sep>layer-'.
54-
const start = getNormalizedVfsRoot().length + 7;
55-
if (!StringPrototypeStartsWith(normalizedPath, sep + 'layer-',
56-
getNormalizedVfsRoot().length)) {
57-
return -1;
58-
}
53+
// Skip '<root><sep>'.
54+
const start = getNormalizedVfsRoot().length + 1;
5955
let id = 0;
6056
let i = start;
6157
const len = normalizedPath.length;
@@ -66,7 +62,7 @@ function getLayerIdFromPath(normalizedPath) {
6662
if (c < 48 || c > 57) return -1; // not a digit
6763
id = id * 10 + (c - 48);
6864
}
69-
if (i === start) return -1; // 'layer-' with no digits
65+
if (i === start) return -1; // no digits at all
7066
return id;
7167
}
7268

lib/internal/vfs/setup.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ function writeFileSyncFd(fd, data, options) {
8787
}
8888

8989
// Registry of active VFS instances, keyed by layer id. Because every
90-
// mount point embeds its layer id (`${os.devNull}/vfs/layer-<id>/...`),
90+
// mount point embeds its layer id (`${os.devNull}/vfs/<id>`),
9191
// dispatch is a map lookup rather than a scan of mounted layers.
9292
const activeVFSLayers = new SafeMap();
9393

@@ -128,7 +128,7 @@ function deregisterVFS(vfs) {
128128
/**
129129
* Resolves a path string to the active VFS that owns it, or null.
130130
* Ownership is decidable from the path alone: all mount points live
131-
* under the reserved `${os.devNull}/vfs/layer-<id>/` namespace,
131+
* under the reserved `${os.devNull}/vfs/<id>` namespace,
132132
* so a single prefix comparison rejects every real-file-system path
133133
* and a map lookup finds the owning layer.
134134
* @param {string} inputPath

test/parallel/test-vfs-destructuring.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ myVfs.writeFileSync('/file.txt', 'hello from vfs');
2424
myVfs.writeFileSync('/sub/nested.txt', 'nested content');
2525

2626
// mount() returns the actual mount point (under os.devNull/vfs/...);
27-
// '/vfs_destr' is just a logical label. The returned path is already in the
28-
// platform's native form, so assertion targets are built from it directly.
29-
const MOUNT = myVfs.mount('/vfs_destr');
27+
// the returned path is already in the platform's native form, so assertion
28+
// targets are built from it directly.
29+
const MOUNT = myVfs.mount();
3030
const FILE = path.join(MOUNT, 'file.txt');
3131

3232
{

test/parallel/test-vfs-fs-accessSync.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const vfs = require('node:vfs');
1212
const myVfs = vfs.create();
1313
myVfs.mkdirSync('/src', { recursive: true });
1414
myVfs.writeFileSync('/src/hello.txt', 'hello');
15-
const mountPoint = myVfs.mount('/accessSync');
15+
const mountPoint = myVfs.mount();
1616

1717
// Existing path succeeds
1818
fs.accessSync(path.join(mountPoint, 'src/hello.txt'));

test/parallel/test-vfs-fs-callback-error-paths.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ function mounted() {
1313
const myVfs = vfs.create();
1414
myVfs.mkdirSync('/src', { recursive: true });
1515
myVfs.writeFileSync('/src/hello.txt', 'hello');
16-
const mountPoint = myVfs.mount('/m');
16+
const mountPoint = myVfs.mount();
1717
return { myVfs, mountPoint };
1818
}
1919

test/parallel/test-vfs-fs-chmod-callback.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const vfs = require('node:vfs');
1212
const myVfs = vfs.create();
1313
myVfs.mkdirSync('/src', { recursive: true });
1414
myVfs.writeFileSync('/src/hello.txt', 'hello');
15-
const mountPoint = myVfs.mount('/chmod-cb');
15+
const mountPoint = myVfs.mount();
1616

1717
const target = path.join(mountPoint, 'src/hello.txt');
1818
const uid = process.getuid?.() ?? 0;

test/parallel/test-vfs-fs-chmodSync.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const vfs = require('node:vfs');
1313
const myVfs = vfs.create();
1414
myVfs.mkdirSync('/src', { recursive: true });
1515
myVfs.writeFileSync('/src/hello.txt', 'hello');
16-
const mountPoint = myVfs.mount('/chmodSync');
16+
const mountPoint = myVfs.mount();
1717

1818
const target = path.join(mountPoint, 'src/hello.txt');
1919
const uid = process.getuid?.() ?? 0;

0 commit comments

Comments
 (0)