vfs: add minimal node:vfs subsystem

Adds the node:vfs builtin module with VirtualFileSystem and
provider classes. No integration with fs, modules, or SEA.

Assisted-by: Claude-Opus4.7
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: https://github.com/nodejs/node/pull/63115
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
Reviewed-By: Robert Nagy <ronagy@icloud.com>
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
This commit is contained in:
Matteo Collina authored and GitHub committed 2026-05-23 18:43:31 +00:00
1 parent c9dbb86683
commit c9562ddb82
79 files changed
+10859 -1

No files matched your search

+12
View File
@@ -1444,6 +1444,16 @@ The flag may be specified more than once; tests must contain **every**
filter value to run. See [Test tags][] for details on declaring and
inheriting tags.
### `--experimental-vfs`
<!-- YAML
added: REPLACEME
-->
> Stability: 1 - Experimental
Enable the experimental [`node:vfs`][] module.
### `--experimental-vm-modules`
<!-- YAML
@@ -3786,6 +3796,7 @@ one is included in the list below.
* `--experimental-stream-iter`
* `--experimental-test-isolation`
* `--experimental-top-level-await`
* `--experimental-vfs`
* `--experimental-vm-modules`
* `--experimental-wasi-unstable-preview1`
* `--force-context-aware`
@@ -4428,6 +4439,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[`node:ffi`]: ffi.md
[`node:sqlite`]: sqlite.md
[`node:stream/iter`]: stream_iter.md
[`node:vfs`]: vfs.md
[`process.setUncaughtExceptionCaptureCallback()`]: process.md#processsetuncaughtexceptioncapturecallbackfn
[`tls.DEFAULT_MAX_VERSION`]: tls.md#tlsdefault_max_version
[`tls.DEFAULT_MIN_VERSION`]: tls.md#tlsdefault_min_version
+1
View File
@@ -65,6 +65,7 @@
* [URL](url.md)
* [Utilities](util.md)
* [V8](v8.md)
* [Virtual File System](vfs.md)
* [VM](vm.md)
* [WASI](wasi.md)
* [Web Crypto API](webcrypto.md)
+310
View File
@@ -0,0 +1,310 @@
# Virtual File System
<!--introduced_in=REPLACEME-->
<!-- YAML
added: REPLACEME
-->
> Stability: 1 - Experimental
<!-- source_link=lib/vfs.js -->
The `node:vfs` module provides an in-memory virtual file system with a
`node:fs`-like API. It is useful for tests, fixtures, embedded assets, and other
scenarios where you need a self-contained file system without touching the
actual file-system.
To access it:
```mjs
import vfs from 'node:vfs';
```
```cjs
const vfs = require('node:vfs');
```
This module is only available under the `node:` scheme, and only when Node.js
is started with the `--experimental-vfs` flag.
## Basic usage
```cjs
const vfs = require('node:vfs');
const myVfs = vfs.create();
myVfs.mkdirSync('/dir', { recursive: true });
myVfs.writeFileSync('/dir/hello.txt', 'Hello, VFS!');
console.log(myVfs.readFileSync('/dir/hello.txt', 'utf8')); // 'Hello, VFS!'
```
`vfs.create()` returns a [`VirtualFileSystem`][] instance backed by a
[`MemoryProvider`][] by default. The instance exposes synchronous,
callback-based, and promise-based file system methods that mirror the
shape of the [`node:fs`][] API. All paths are POSIX-style and absolute
(starting with `/`).
## `vfs.create([provider][, options])`
<!-- YAML
added: REPLACEME
-->
* `provider` {VirtualProvider} The provider to use. **Default:**
`new MemoryProvider()`.
* `options` {Object}
* `emitExperimentalWarning` {boolean} Whether to emit the experimental
warning when the instance is created. **Default:** `true`.
* Returns: {VirtualFileSystem}
Convenience factory equivalent to `new VirtualFileSystem(provider, options)`.
```cjs
const vfs = require('node:vfs');
// Default in-memory provider
const memoryVfs = vfs.create();
// Explicit provider
const realVfs = vfs.create(new vfs.RealFSProvider('/tmp/sandbox'));
```
## Class: `VirtualFileSystem`
<!-- YAML
added: REPLACEME
-->
A `VirtualFileSystem` wraps a [`VirtualProvider`][] and exposes a
`node:fs`-like API. Each instance maintains its own file tree.
### `new VirtualFileSystem([provider][, options])`
<!-- YAML
added: REPLACEME
-->
* `provider` {VirtualProvider} The provider to use. **Default:**
`new MemoryProvider()`.
* `options` {Object}
* `emitExperimentalWarning` {boolean} Whether to emit the experimental
warning. **Default:** `true`.
### `vfs.provider`
<!-- YAML
added: REPLACEME
-->
* {VirtualProvider}
The provider backing this VFS instance.
### `vfs.readonly`
<!-- YAML
added: REPLACEME
-->
* {boolean}
`true` when the underlying provider is read-only.
### APIs
`VirtualFileSystem` implements the following methods, with the same
signatures as their [`node:fs`][] counterparts:
#### Synchronous API
* `existsSync(path)`
* `statSync(path[, options])`
* `lstatSync(path[, options])`
* `readFileSync(path[, options])`
* `writeFileSync(path, data[, options])`
* `appendFileSync(path, data[, options])`
* `readdirSync(path[, options])`
* `mkdirSync(path[, options])`
* `rmdirSync(path)`
* `unlinkSync(path)`
* `renameSync(oldPath, newPath)`
* `copyFileSync(src, dest[, mode])`
* `realpathSync(path[, options])`
* `readlinkSync(path[, options])`
* `symlinkSync(target, path[, type])`
* `accessSync(path[, mode])`
* `rmSync(path[, options])`
* `truncateSync(path[, len])`
* `ftruncateSync(fd[, len])`
* `linkSync(existingPath, newPath)`
* `chmodSync(path, mode)`
* `chownSync(path, uid, gid)`
* `utimesSync(path, atime, mtime)`
* `lutimesSync(path, atime, mtime)`
* `mkdtempSync(prefix)`
* `opendirSync(path[, options])`
* `openAsBlob(path[, options])`
* File-descriptor ops: `openSync`, `closeSync`, `readSync`, `writeSync`,
`fstatSync`
* Streams: `createReadStream`, `createWriteStream`
* Watchers: `watch`, `watchFile`, `unwatchFile`
#### Callback API
`readFile`, `writeFile`, `stat`, `lstat`, `readdir`, `realpath`, `readlink`,
`access`, `open`, `close`, `read`, `write`, `rm`, `fstat`, `truncate`,
`ftruncate`, `link`, `mkdtemp`, `opendir`. Each takes a Node.js-style
callback `(err, ...result) => {}`.
#### Promise API
`vfs.promises` exposes the promise-based variants:
```cjs
const vfs = require('node:vfs');
async function example() {
const myVfs = vfs.create();
await myVfs.promises.writeFile('/file.txt', 'hello');
const data = await myVfs.promises.readFile('/file.txt', 'utf8');
return data;
}
example();
```
The promise namespace mirrors `fs.promises` and includes `readFile`,
`writeFile`, `appendFile`, `stat`, `lstat`, `readdir`, `mkdir`, `rmdir`,
`unlink`, `rename`, `copyFile`, `realpath`, `readlink`, `symlink`,
`access`, `rm`, `truncate`, `link`, `mkdtemp`, `chmod`, `chown`, `lchown`,
`utimes`, `lutimes`, `open`, `lchmod`, and `watch`.
## Class: `VirtualProvider`
<!-- YAML
added: REPLACEME
-->
The base class for all VFS providers. Subclasses implement the essential
primitives (`open`, `stat`, `readdir`, `mkdir`, `rmdir`, `unlink`,
`rename`, ...) and inherit default implementations of the derived
The base class for all VFS providers. Subclasses implement the essential
primitives (such as `open`, `stat`, `readdir`, `mkdir`, `rmdir`, `unlink`,
`rename`, etc.) and inherit default implementations of the derived
methods (such as `readFile`, `writeFile`, `exists`, `copyFile`, `access`, etc.).
### Capability flags
* `provider.readonly` {boolean} **Default:** `false`.
* `provider.supportsSymlinks` {boolean} **Default:** `false`.
* `provider.supportsWatch` {boolean} **Default:** `false`.
### Creating custom providers
```cjs
const { VirtualProvider } = require('node:vfs');
class StaticProvider extends VirtualProvider {
get readonly() { return true; }
statSync(path) { /* ... */ }
openSync(path, flags) { /* ... */ }
readdirSync(path, options) { /* ... */ }
// ...
}
```
The base class throws `ERR_METHOD_NOT_IMPLEMENTED` for any primitive
that has not been overridden, and rejects writes from a `readonly`
provider with `EROFS`.
## Class: `MemoryProvider`
<!-- YAML
added: REPLACEME
-->
The default in-memory provider. Stores files, directories, and symbolic
links in a `Map`-backed tree, supports symlinks (`supportsSymlinks ===
true`), and supports watching (`supportsWatch === true`).
### `memoryProvider.setReadOnly()`
<!-- YAML
added: REPLACEME
-->
Locks the provider into read-only mode. Subsequent writes through any
[`VirtualFileSystem`][] using this provider throw `EROFS`. There is no
way to revert the provider to writable.
```cjs
const vfs = require('node:vfs');
const provider = new vfs.MemoryProvider();
const myVfs = vfs.create(provider);
myVfs.writeFileSync('/seed.txt', 'initial');
provider.setReadOnly();
myVfs.writeFileSync('/x.txt', 'fail'); // throws EROFS
```
## Class: `RealFSProvider`
<!-- YAML
added: REPLACEME
-->
A provider that wraps a directory (i.e. one on the actual file system) and exposes its
contents through the VFS API. All VFS paths are resolved relative to
the root and verified to stay inside it; symbolic links resolving
outside the root are rejected.
### `new RealFSProvider(rootPath)`
<!-- YAML
added: REPLACEME
-->
* `rootPath` {string} The absolute file-system path to use as the root.
Must be a non-empty string.
```cjs
const vfs = require('node:vfs');
const realVfs = vfs.create(new vfs.RealFSProvider('/tmp/sandbox'));
realVfs.writeFileSync('/file.txt', 'hello'); // writes /tmp/sandbox/file.txt
```
### `realFSProvider.rootPath`
<!-- YAML
added: REPLACEME
-->
* {string}
The resolved absolute path used as the root.
## Implementation details
### `Stats` objects
VFS `Stats` objects are real instances of [`fs.Stats`][] (or
[`fs.BigIntStats`][] when `{ bigint: true }` is requested). Their
fields use synthetic but stable values:
* `dev` is `4085` (the VFS device id).
* `ino` is monotonically increasing per process.
* `blksize` is `4096`.
* `blocks` is `Math.ceil(size / 512)`.
* Times default to the moment the entry was created/last modified.
[`MemoryProvider`]: #class-memoryprovider
[`VirtualFileSystem`]: #class-virtualfilesystem
[`VirtualProvider`]: #class-virtualprovider
[`fs.BigIntStats`]: fs.md#class-fsbigintstats
[`fs.Stats`]: fs.md#class-fsstats
[`node:fs`]: fs.md
+7
View File
@@ -758,6 +758,11 @@ Enable the experimental
.Sy node:stream/iter
module.
.
.It Fl -experimental-vfs
Enable the experimental
.Sy node:vfs
module.
.
.It Fl -experimental-sea-config
Use this flag to generate a blob that can be injected into the Node.js
binary to produce a single executable application. See the documentation
@@ -1945,6 +1950,8 @@ one is included in the list below.
.It
\fB--experimental-top-level-await\fR
.It
\fB--experimental-vfs\fR
.It
\fB--experimental-vm-modules\fR
.It
\fB--experimental-wasi-unstable-preview1\fR
+10 -1
View File
@@ -131,9 +131,18 @@ const schemelessBlockList = new SafeSet([
'quic',
'test',
'test/reporters',
'vfs',
]);
// Modules that will only be enabled at run time.
const experimentalModuleList = new SafeSet(['dtls', 'ffi', 'sqlite', 'quic', 'stream/iter', 'zlib/iter']);
const experimentalModuleList = new SafeSet([
'dtls',
'ffi',
'quic',
'sqlite',
'stream/iter',
'vfs',
'zlib/iter',
]);
// Set up process.binding() and process._linkedBinding().
{
+3
View File
@@ -496,6 +496,9 @@ function initializeCJS() {
if (!getOptionValue('--experimental-ffi')) {
modules = modules.filter((i) => i !== 'node:ffi');
}
if (!getOptionValue('--experimental-vfs')) {
modules = modules.filter((i) => i !== 'node:vfs');
}
Module.builtinModules = ObjectFreeze(modules);
initializeCjsConditions();
+10
View File
@@ -118,6 +118,7 @@ function prepareExecution(options) {
setupSQLite();
setupStreamIter();
setupDTLS();
setupVfs();
setupQuic();
setupWebStorage();
setupWebsocket();
@@ -431,6 +432,15 @@ function setupQuic() {
BuiltinModule.allowRequireByUsers('quic');
}
function setupVfs() {
if (!getOptionValue('--experimental-vfs')) {
return;
}
const { BuiltinModule } = require('internal/bootstrap/realm');
BuiltinModule.allowRequireByUsers('vfs');
}
function setupWebStorage() {
if (getEmbedderOptions().noBrowserGlobals ||
!getOptionValue('--experimental-webstorage')) {
+104
View File
@@ -0,0 +1,104 @@
'use strict';
const {
SymbolAsyncDispose,
SymbolAsyncIterator,
SymbolDispose,
} = primordials;
const {
codes: {
ERR_DIR_CLOSED,
},
} = require('internal/errors');
/**
* Virtual directory handle returned by VFS opendir/opendirSync.
* Mimics the subset of the native Dir interface used by Node.js internals
* (e.g. fs.cp, fs.promises.cp).
*/
class VirtualDir {
#path;
#entries;
#index;
#closed;
constructor(dirPath, entries) {
this.#path = dirPath;
this.#entries = entries;
this.#index = 0;
this.#closed = false;
}
get path() {
return this.#path;
}
readSync() {
if (this.#closed) {
throw new ERR_DIR_CLOSED();
}
if (this.#index >= this.#entries.length) {
return null;
}
return this.#entries[this.#index++];
}
async read(callback) {
if (typeof callback === 'function') {
try {
const result = this.readSync();
process.nextTick(callback, null, result);
} catch (err) {
process.nextTick(callback, err);
}
return;
}
return this.readSync();
}
closeSync() {
if (this.#closed) {
throw new ERR_DIR_CLOSED();
}
this.#closed = true;
}
async close(callback) {
if (typeof callback === 'function') {
this.closeSync();
process.nextTick(callback, null);
return;
}
this.closeSync();
}
async *entries() {
if (this.#closed) {
throw new ERR_DIR_CLOSED();
}
try {
let entry;
while ((entry = this.readSync()) !== null) {
yield entry;
}
} finally {
if (!this.#closed) {
this.closeSync();
}
}
}
[SymbolDispose]() {
if (!this.#closed) {
this.closeSync();
}
}
}
VirtualDir.prototype[SymbolAsyncIterator] = VirtualDir.prototype.entries;
VirtualDir.prototype[SymbolAsyncDispose] = VirtualDir.prototype.close;
module.exports = {
VirtualDir,
};
+193
View File
@@ -0,0 +1,193 @@
'use strict';
const {
ErrorCaptureStackTrace,
} = primordials;
const {
UVException,
} = require('internal/errors');
const {
UV_ENOENT,
UV_ENOTDIR,
UV_ENOTEMPTY,
UV_EISDIR,
UV_EBADF,
UV_EEXIST,
UV_EROFS,
UV_EINVAL,
UV_ELOOP,
UV_EACCES,
} = internalBinding('uv');
/**
* Creates an ENOENT error for virtual file system operations.
* @param {string} syscall The system call name
* @param {string} path The path that was not found
* @returns {Error}
*/
function createENOENT(syscall, path) {
const err = new UVException({
errno: UV_ENOENT,
syscall,
path,
});
ErrorCaptureStackTrace(err, createENOENT);
return err;
}
/**
* Creates an ENOTDIR error.
* @param {string} syscall The system call name
* @param {string} path The path that is not a directory
* @returns {Error}
*/
function createENOTDIR(syscall, path) {
const err = new UVException({
errno: UV_ENOTDIR,
syscall,
path,
});
ErrorCaptureStackTrace(err, createENOTDIR);
return err;
}
/**
* Creates an ENOTEMPTY error for non-empty directory.
* @param {string} syscall The system call name
* @param {string} path The path of the non-empty directory
* @returns {Error}
*/
function createENOTEMPTY(syscall, path) {
const err = new UVException({
errno: UV_ENOTEMPTY,
syscall,
path,
});
ErrorCaptureStackTrace(err, createENOTEMPTY);
return err;
}
/**
* Creates an EISDIR error.
* @param {string} syscall The system call name
* @param {string} path The path that is a directory
* @returns {Error}
*/
function createEISDIR(syscall, path) {
const err = new UVException({
errno: UV_EISDIR,
syscall,
path,
});
ErrorCaptureStackTrace(err, createEISDIR);
return err;
}
/**
* Creates an EBADF error for invalid file descriptor operations.
* @param {string} syscall The system call name
* @returns {Error}
*/
function createEBADF(syscall) {
const err = new UVException({
errno: UV_EBADF,
syscall,
});
ErrorCaptureStackTrace(err, createEBADF);
return err;
}
/**
* Creates an EEXIST error.
* @param {string} syscall The system call name
* @param {string} path The path that already exists
* @returns {Error}
*/
function createEEXIST(syscall, path) {
const err = new UVException({
errno: UV_EEXIST,
syscall,
path,
});
ErrorCaptureStackTrace(err, createEEXIST);
return err;
}
/**
* Creates an EROFS error for read-only file system.
* @param {string} syscall The system call name
* @param {string} path The path
* @returns {Error}
*/
function createEROFS(syscall, path) {
const err = new UVException({
errno: UV_EROFS,
syscall,
path,
});
ErrorCaptureStackTrace(err, createEROFS);
return err;
}
/**
* Creates an EINVAL error for invalid argument.
* @param {string} syscall The system call name
* @param {string} path The path
* @returns {Error}
*/
function createEINVAL(syscall, path) {
const err = new UVException({
errno: UV_EINVAL,
syscall,
path,
});
ErrorCaptureStackTrace(err, createEINVAL);
return err;
}
/**
* Creates an ELOOP error for too many symbolic links.
* @param {string} syscall The system call name
* @param {string} path The path
* @returns {Error}
*/
function createELOOP(syscall, path) {
const err = new UVException({
errno: UV_ELOOP,
syscall,
path,
});
ErrorCaptureStackTrace(err, createELOOP);
return err;
}
/**
* Creates an EACCES error for permission denied.
* @param {string} syscall The system call name
* @param {string} path The path
* @returns {Error}
*/
function createEACCES(syscall, path) {
const err = new UVException({
errno: UV_EACCES,
syscall,
path,
});
ErrorCaptureStackTrace(err, createEACCES);
return err;
}
module.exports = {
createENOENT,
createENOTDIR,
createENOTEMPTY,
createEISDIR,
createEBADF,
createEEXIST,
createEROFS,
createEINVAL,
createELOOP,
createEACCES,
};
+87
View File
@@ -0,0 +1,87 @@
'use strict';
const {
SafeMap,
Symbol,
} = primordials;
// Private symbols
const kFd = Symbol('kFd');
const kEntry = Symbol('kEntry');
// VFS FDs use bit 30 set to avoid conflicts with real OS fds.
// Real fds are small non-negative integers; VFS fds start at 0x40000000.
const VFS_FD_MASK = 0x40000000;
let nextFd = 0;
// Global registry of open virtual file descriptors
const openFDs = new SafeMap();
/**
* Represents an open virtual file descriptor.
* Wraps a VirtualFileHandle from the provider.
*/
class VirtualFD {
/**
* @param {number} fd The file descriptor number
* @param {VirtualFileHandle} entry The virtual file handle
*/
constructor(fd, entry) {
this[kFd] = fd;
this[kEntry] = entry;
}
/**
* Gets the file descriptor number.
* @returns {number}
*/
get fd() {
return this[kFd];
}
/**
* Gets the file handle.
* @returns {VirtualFileHandle}
*/
get entry() {
return this[kEntry];
}
}
/**
* Opens a virtual file and returns its file descriptor.
* @param {VirtualFileHandle} entry The virtual file handle
* @returns {number} The file descriptor
*/
function openVirtualFd(entry) {
const fd = VFS_FD_MASK | nextFd++;
const vfd = new VirtualFD(fd, entry);
openFDs.set(fd, vfd);
return fd;
}
/**
* Gets a VirtualFD by its file descriptor number.
* @param {number} fd The file descriptor number
* @returns {VirtualFD|undefined}
*/
function getVirtualFd(fd) {
return openFDs.get(fd);
}
/**
* Closes a virtual file descriptor.
* @param {number} fd The file descriptor number
* @returns {boolean} True if the fd was found and closed
*/
function closeVirtualFd(fd) {
return openFDs.delete(fd);
}
module.exports = {
VFS_FD_MASK,
VirtualFD,
openVirtualFd,
getVirtualFd,
closeVirtualFd,
};
+720
View File
@@ -0,0 +1,720 @@
'use strict';
const {
DateNow,
MathMax,
MathMin,
Number,
Symbol,
SymbolAsyncDispose,
SymbolDispose,
} = primordials;
const { Buffer } = require('buffer');
const {
codes: {
ERR_INVALID_STATE,
ERR_METHOD_NOT_IMPLEMENTED,
},
} = require('internal/errors');
const {
createEBADF,
} = require('internal/vfs/errors');
// Private symbols
const kPath = Symbol('kPath');
const kFlags = Symbol('kFlags');
const kMode = Symbol('kMode');
const kPosition = Symbol('kPosition');
const kClosed = Symbol('kClosed');
/**
* Base class for virtual file handles.
* Provides the interface that file handles must implement.
*/
class VirtualFileHandle {
/**
* @param {string} path The file path
* @param {string} flags The open flags
* @param {number} [mode] The file mode
*/
constructor(path, flags, mode) {
this[kPath] = path;
this[kFlags] = flags;
this[kMode] = mode ?? 0o644;
this[kPosition] = 0;
this[kClosed] = false;
}
/**
* Gets the file path.
* @returns {string}
*/
get path() {
return this[kPath];
}
/**
* Gets the open flags.
* @returns {string}
*/
get flags() {
return this[kFlags];
}
/**
* Gets the file mode.
* @returns {number}
*/
get mode() {
return this[kMode];
}
/**
* Gets the current position.
* @returns {number}
*/
get position() {
return this[kPosition];
}
/**
* Sets the current position.
* @param {number} pos The new position
*/
set position(pos) {
this[kPosition] = pos;
}
/**
* Returns true if the handle is closed.
* @returns {boolean}
*/
get closed() {
return this[kClosed];
}
/**
* Throws if the handle is closed.
* @param {string} syscall The syscall name for the error
*/
#checkClosed(syscall) {
if (this[kClosed]) {
throw createEBADF(syscall);
}
}
/**
* Reads data from the file.
* @param {Buffer} buffer The buffer to read into
* @param {number} offset The offset in the buffer to start writing
* @param {number} length The number of bytes to read
* @param {number|null} position The position to read from (null uses current position)
* @returns {Promise<{ bytesRead: number, buffer: Buffer }>}
*/
async read(buffer, offset, length, position) {
this.#checkClosed('read');
throw new ERR_METHOD_NOT_IMPLEMENTED('read');
}
/**
* Reads data from the file synchronously.
* @param {Buffer} buffer The buffer to read into
* @param {number} offset The offset in the buffer to start writing
* @param {number} length The number of bytes to read
* @param {number|null} position The position to read from (null uses current position)
* @throws {ERR_METHOD_NOT_IMPLEMENTED} When not implemented by subclass
*/
readSync(buffer, offset, length, position) {
this.#checkClosed('read');
throw new ERR_METHOD_NOT_IMPLEMENTED('readSync');
}
/**
* Writes data to the file.
* @param {Buffer} buffer The buffer to write from
* @param {number} offset The offset in the buffer to start reading
* @param {number} length The number of bytes to write
* @param {number|null} position The position to write to (null uses current position)
* @returns {Promise<{ bytesWritten: number, buffer: Buffer }>}
*/
async write(buffer, offset, length, position) {
this.#checkClosed('write');
throw new ERR_METHOD_NOT_IMPLEMENTED('write');
}
/**
* Writes data to the file synchronously.
* @param {Buffer} buffer The buffer to write from
* @param {number} offset The offset in the buffer to start reading
* @param {number} length The number of bytes to write
* @param {number|null} position The position to write to (null uses current position)
* @throws {ERR_METHOD_NOT_IMPLEMENTED} When not implemented by subclass
*/
writeSync(buffer, offset, length, position) {
this.#checkClosed('write');
throw new ERR_METHOD_NOT_IMPLEMENTED('writeSync');
}
/**
* Reads the entire file.
* @param {object|string} [options] Options or encoding
* @returns {Promise<Buffer|string>}
*/
async readFile(options) {
this.#checkClosed('read');
throw new ERR_METHOD_NOT_IMPLEMENTED('readFile');
}
/**
* Reads the entire file synchronously.
* @param {object|string} [options] Options or encoding
* @throws {ERR_METHOD_NOT_IMPLEMENTED} When not implemented by subclass
*/
readFileSync(options) {
this.#checkClosed('read');
throw new ERR_METHOD_NOT_IMPLEMENTED('readFileSync');
}
/**
* Writes data to the file (replacing content).
* @param {Buffer|string} data The data to write
* @param {object} [options] Options
* @returns {Promise<void>}
*/
async writeFile(data, options) {
this.#checkClosed('write');
throw new ERR_METHOD_NOT_IMPLEMENTED('writeFile');
}
/**
* Writes data to the file synchronously (replacing content).
* @param {Buffer|string} data The data to write
* @param {object} [options] Options
*/
writeFileSync(data, options) {
this.#checkClosed('write');
throw new ERR_METHOD_NOT_IMPLEMENTED('writeFileSync');
}
/**
* Gets file stats.
* @param {object} [options] Options
* @returns {Promise<Stats>}
*/
async stat(options) {
this.#checkClosed('fstat');
throw new ERR_METHOD_NOT_IMPLEMENTED('stat');
}
/**
* Gets file stats synchronously.
* @param {object} [options] Options
* @throws {ERR_METHOD_NOT_IMPLEMENTED} When not implemented by subclass
*/
statSync(options) {
this.#checkClosed('fstat');
throw new ERR_METHOD_NOT_IMPLEMENTED('statSync');
}
/**
* Truncates the file.
* @param {number} [len] The new length
* @returns {Promise<void>}
*/
async truncate(len) {
this.#checkClosed('ftruncate');
throw new ERR_METHOD_NOT_IMPLEMENTED('truncate');
}
/**
* Truncates the file synchronously.
* @param {number} [len] The new length
*/
truncateSync(len) {
this.#checkClosed('ftruncate');
throw new ERR_METHOD_NOT_IMPLEMENTED('truncateSync');
}
/**
* No-op chmod - VFS files don't have real permissions.
* @returns {Promise<void>}
*/
async chmod() {}
/**
* No-op chown - VFS files don't have real ownership.
* @returns {Promise<void>}
*/
async chown() {}
/**
* No-op utimes - timestamps are handled by the provider.
* @returns {Promise<void>}
*/
async utimes() {}
/**
* No-op datasync - VFS is in-memory.
* @returns {Promise<void>}
*/
async datasync() {}
/**
* No-op sync - VFS is in-memory.
* @returns {Promise<void>}
*/
async sync() {}
/**
* Reads data from the file into multiple buffers.
* @param {Buffer[]} buffers The buffers to read into
* @param {number|null} [position] The position to read from
* @returns {Promise<{ bytesRead: number, buffers: Buffer[] }>}
*/
async readv(buffers, position) {
this.#checkClosed('readv');
let totalRead = 0;
for (let i = 0; i < buffers.length; i++) {
const buf = buffers[i];
const pos = position != null ? position + totalRead : null;
const { bytesRead } = await this.read(buf, 0, buf.byteLength, pos);
totalRead += bytesRead;
if (bytesRead < buf.byteLength) break;
}
return { __proto__: null, bytesRead: totalRead, buffers };
}
/**
* Writes data from multiple buffers to the file.
* @param {Buffer[]} buffers The buffers to write from
* @param {number|null} [position] The position to write to
* @returns {Promise<{ bytesWritten: number, buffers: Buffer[] }>}
*/
async writev(buffers, position) {
this.#checkClosed('writev');
let totalWritten = 0;
for (let i = 0; i < buffers.length; i++) {
const buf = buffers[i];
const pos = position != null ? position + totalWritten : null;
const { bytesWritten } = await this.write(buf, 0, buf.byteLength, pos);
totalWritten += bytesWritten;
if (bytesWritten < buf.byteLength) break;
}
return { __proto__: null, bytesWritten: totalWritten, buffers };
}
/**
* Appends data to the file.
* @param {Buffer|string} data The data to append
* @param {object} [options] Options
* @returns {Promise<void>}
*/
async appendFile(data, options) {
this.#checkClosed('appendFile');
const buffer = typeof data === 'string' ?
Buffer.from(data, options?.encoding) : data;
await this.write(buffer, 0, buffer.length, null);
}
readableWebStream() {
throw new ERR_METHOD_NOT_IMPLEMENTED('readableWebStream');
}
readLines() {
throw new ERR_METHOD_NOT_IMPLEMENTED('readLines');
}
createReadStream() {
throw new ERR_METHOD_NOT_IMPLEMENTED('createReadStream');
}
createWriteStream() {
throw new ERR_METHOD_NOT_IMPLEMENTED('createWriteStream');
}
/**
* Closes the file handle.
* @returns {Promise<void>}
*/
async close() {
this[kClosed] = true;
}
/**
* Closes the file handle synchronously.
*/
closeSync() {
this[kClosed] = true;
}
}
VirtualFileHandle.prototype[SymbolAsyncDispose] = VirtualFileHandle.prototype.close;
VirtualFileHandle.prototype[SymbolDispose] = VirtualFileHandle.prototype.closeSync;
/**
* A file handle for in-memory file content.
* Used by MemoryProvider and similar providers.
*/
class MemoryFileHandle extends VirtualFileHandle {
#content;
#size;
#entry;
#getStats;
#checkClosed(syscall) {
if (this.closed) {
throw createEBADF(syscall);
}
}
/**
* @param {string} path The file path
* @param {string} flags The open flags
* @param {number} [mode] The file mode
* @param {Buffer} content The initial file content
* @param {object} entry The entry object (for updating content)
* @param {Function} getStats Function to get updated stats
*/
constructor(path, flags, mode, content, entry, getStats) {
super(path, flags, mode);
this.#content = content;
this.#size = content.length;
this.#entry = entry;
this.#getStats = getStats;
// Handle different open modes
if (flags === 'w' || flags === 'w+' ||
flags === 'wx' || flags === 'wx+') {
// Write mode: truncate
this.#content = Buffer.alloc(0);
this.#size = 0;
if (entry) {
entry.content = this.#content;
}
} else if (flags === 'a' || flags === 'a+' ||
flags === 'ax' || flags === 'ax+') {
// Append mode: position at end
this.position = this.#size;
}
}
/**
* Throws EBADF if the handle was not opened for writing.
*/
#checkWritable() {
if (this.flags === 'r') {
throw createEBADF('write');
}
}
/**
* Throws EBADF if the handle was not opened for reading.
*/
#checkReadable() {
const f = this.flags;
if (f === 'w' || f === 'a' || f === 'wx' || f === 'ax') {
throw createEBADF('read');
}
}
/**
* Returns true if this handle was opened in append mode.
* @returns {boolean}
*/
#isAppend() {
const f = this.flags;
return f === 'a' || f === 'a+' || f === 'ax' || f === 'ax+';
}
/**
* Gets the current content synchronously.
* For dynamic content providers, this gets fresh content from the entry.
* @returns {Buffer}
*/
get content() {
// If entry has a dynamic content provider, get fresh content sync
if (this.#entry?.isDynamic && this.#entry.isDynamic()) {
return this.#entry.getContentSync();
}
return this.#content.subarray(0, this.#size);
}
/**
* Gets the current content asynchronously.
* For dynamic content providers, this gets fresh content from the entry.
* @returns {Promise<Buffer>}
*/
async getContentAsync() {
// If entry has a dynamic content provider, get fresh content async
if (this.#entry?.getContentAsync) {
return this.#entry.getContentAsync();
}
return this.#content;
}
/**
* Reads data from the file synchronously.
* @param {Buffer} buffer The buffer to read into
* @param {number} offset The offset in the buffer to start writing
* @param {number} length The number of bytes to read
* @param {number|null} position The position to read from (null uses current position)
* @returns {number} The number of bytes read
*/
readSync(buffer, offset, length, position) {
this.#checkClosed('read');
this.#checkReadable();
// Get content (resolves dynamic content providers)
const content = this.content;
const readPos = position !== null && position !== undefined ?
Number(position) : this.position;
const available = content.length - readPos;
if (available <= 0) {
return 0;
}
const bytesToRead = MathMin(length, available);
content.copy(buffer, offset, readPos, readPos + bytesToRead);
// Update position if not using explicit position
if (position === null || position === undefined) {
this.position = readPos + bytesToRead;
}
return bytesToRead;
}
/**
* Reads data from the file.
* @param {Buffer} buffer The buffer to read into
* @param {number} offset The offset in the buffer to start writing
* @param {number} length The number of bytes to read
* @param {number|null} position The position to read from (null uses current position)
* @returns {Promise<{ bytesRead: number, buffer: Buffer }>}
*/
async read(buffer, offset, length, position) {
const bytesRead = this.readSync(buffer, offset, length, position);
return { __proto__: null, bytesRead, buffer };
}
/**
* Writes data to the file synchronously.
* @param {Buffer} buffer The buffer to write from
* @param {number} offset The offset in the buffer to start reading
* @param {number} length The number of bytes to write
* @param {number|null} position The position to write to (null uses current position)
* @returns {number} The number of bytes written
*/
writeSync(buffer, offset, length, position) {
this.#checkClosed('write');
this.#checkWritable();
// In append mode, always write at the end
const writePos = this.#isAppend() ?
this.#size :
(position !== null && position !== undefined ?
Number(position) : this.position);
const data = buffer.subarray(offset, offset + length);
// Expand buffer if needed (geometric doubling for amortized O(1) appends)
const neededSize = writePos + length;
if (neededSize > this.#content.length) {
const newCapacity = MathMax(neededSize, this.#content.length * 2);
const newContent = Buffer.alloc(newCapacity);
this.#content.copy(newContent, 0, 0, this.#size);
this.#content = newContent;
}
// Write the data
data.copy(this.#content, writePos);
// Update actual content size
if (neededSize > this.#size) {
this.#size = neededSize;
}
// Update the entry's content, mtime, and ctime
if (this.#entry) {
const now = DateNow();
this.#entry.content = this.#content.subarray(0, this.#size);
this.#entry.mtime = now;
this.#entry.ctime = now;
}
// Update position if not using explicit position
if (position === null || position === undefined) {
this.position = writePos + length;
}
return length;
}
/**
* Writes data to the file.
* @param {Buffer} buffer The buffer to write from
* @param {number} offset The offset in the buffer to start reading
* @param {number} length The number of bytes to write
* @param {number|null} position The position to write to (null uses current position)
* @returns {Promise<{ bytesWritten: number, buffer: Buffer }>}
*/
async write(buffer, offset, length, position) {
const bytesWritten = this.writeSync(buffer, offset, length, position);
return { __proto__: null, bytesWritten, buffer };
}
/**
* Reads the entire file synchronously.
* @param {object|string} [options] Options or encoding
* @returns {Buffer|string}
*/
readFileSync(options) {
this.#checkClosed('read');
this.#checkReadable();
// Get content (resolves dynamic content providers)
const content = this.content;
const encoding = typeof options === 'string' ? options : options?.encoding;
if (encoding) {
return content.toString(encoding);
}
return Buffer.from(content);
}
/**
* Reads the entire file.
* @param {object|string} [options] Options or encoding
* @returns {Promise<Buffer|string>}
*/
async readFile(options) {
this.#checkClosed('read');
this.#checkReadable();
// Get content asynchronously (supports async content providers)
const content = await this.getContentAsync();
const encoding = typeof options === 'string' ? options : options?.encoding;
if (encoding) {
return content.toString(encoding);
}
return Buffer.from(content);
}
/**
* Writes data to the file synchronously.
* Replaces content in 'w' mode, appends in 'a' mode.
* @param {Buffer|string} data The data to write
* @param {object} [options] Options
*/
writeFileSync(data, options) {
this.#checkClosed('write');
this.#checkWritable();
const buffer = typeof data === 'string' ? Buffer.from(data, options?.encoding) : data;
// In append mode, append to existing content
if (this.#isAppend()) {
const neededSize = this.#size + buffer.length;
if (neededSize > this.#content.length) {
const newCapacity = MathMax(neededSize, this.#content.length * 2);
const newContent = Buffer.alloc(newCapacity);
this.#content.copy(newContent, 0, 0, this.#size);
this.#content = newContent;
}
buffer.copy(this.#content, this.#size);
this.#size = neededSize;
} else {
this.#content = Buffer.from(buffer);
this.#size = buffer.length;
}
// Update the entry's content, mtime, and ctime
if (this.#entry) {
const now = DateNow();
this.#entry.content = this.#content.subarray(0, this.#size);
this.#entry.mtime = now;
this.#entry.ctime = now;
}
this.position = this.#size;
}
/**
* Writes data to the file (replacing content).
* @param {Buffer|string} data The data to write
* @param {object} [options] Options
* @returns {Promise<void>}
*/
async writeFile(data, options) {
this.writeFileSync(data, options);
}
/**
* Gets file stats synchronously.
* @param {object} [options] Options
* @returns {Stats}
*/
statSync(options) {
this.#checkClosed('fstat');
if (this.#getStats) {
return this.#getStats(this.#size);
}
throw new ERR_INVALID_STATE('stats not available');
}
/**
* Gets file stats.
* @param {object} [options] Options
* @returns {Promise<Stats>}
*/
async stat(options) {
return this.statSync(options);
}
/**
* Truncates the file synchronously.
* @param {number} [len] The new length
*/
truncateSync(len = 0) {
this.#checkClosed('ftruncate');
this.#checkWritable();
if (len < this.#size) {
// Zero out truncated region to avoid stale data
this.#content.fill(0, len, this.#size);
this.#size = len;
} else if (len > this.#size) {
if (len > this.#content.length) {
const newContent = Buffer.alloc(len);
this.#content.copy(newContent, 0, 0, this.#size);
this.#content = newContent;
} else {
// Buffer has enough capacity, just zero-fill the extension
this.#content.fill(0, this.#size, len);
}
this.#size = len;
}
// Update the entry's content, mtime, and ctime
if (this.#entry) {
const now = DateNow();
this.#entry.content = this.#content.subarray(0, this.#size);
this.#entry.mtime = now;
this.#entry.ctime = now;
}
}
/**
* Truncates the file.
* @param {number} [len] The new length
* @returns {Promise<void>}
*/
async truncate(len) {
this.truncateSync(len);
}
}
module.exports = {
VirtualFileHandle,
MemoryFileHandle,
};
File diff suppressed because it is too large. Load diff
+618
View File
@@ -0,0 +1,618 @@
'use strict';
const {
ERR_METHOD_NOT_IMPLEMENTED,
} = require('internal/errors').codes;
const {
createEROFS,
createEEXIST,
createEACCES,
} = require('internal/vfs/errors');
const {
fs: {
R_OK,
W_OK,
X_OK,
COPYFILE_EXCL,
},
} = internalBinding('constants');
/**
* Base class for VFS providers.
* Providers implement the essential primitives that the VFS delegates to.
*
* Implementations must override the essential primitives (open, stat, readdir, etc.)
* Default implementations for derived methods (readFile, writeFile, etc.) are provided.
*/
class VirtualProvider {
// === CAPABILITY FLAGS ===
/**
* Returns true if this provider is read-only.
* @returns {boolean}
*/
get readonly() {
return false;
}
/**
* Returns true if this provider supports symbolic links.
* @returns {boolean}
*/
get supportsSymlinks() {
return false;
}
/**
* Returns true if this provider supports file watching.
* @returns {boolean}
*/
get supportsWatch() {
return false;
}
// === ESSENTIAL PRIMITIVES (must be implemented by subclasses) ===
/**
* Opens a file and returns a file handle.
* @param {string} path The file path (relative to provider root)
* @param {string} flags The open flags ('r', 'r+', 'w', 'w+', 'a', 'a+')
* @param {number} [mode] The file mode (for creating files)
* @returns {Promise<VirtualFileHandle>}
*/
async open(path, flags, mode) {
throw new ERR_METHOD_NOT_IMPLEMENTED('open');
}
/**
* Opens a file synchronously and returns a file handle.
* @param {string} path The file path (relative to provider root)
* @param {string} flags The open flags ('r', 'r+', 'w', 'w+', 'a', 'a+')
* @param {number} [mode] The file mode (for creating files)
* @throws {ERR_METHOD_NOT_IMPLEMENTED} When not implemented by subclass
*/
openSync(path, flags, mode) {
throw new ERR_METHOD_NOT_IMPLEMENTED('openSync');
}
/**
* Gets stats for a path.
* @param {string} path The path to stat
* @param {object} [options] Options
* @returns {Promise<Stats>}
*/
async stat(path, options) {
throw new ERR_METHOD_NOT_IMPLEMENTED('stat');
}
/**
* Gets stats for a path synchronously.
* @param {string} path The path to stat
* @param {object} [options] Options
* @throws {ERR_METHOD_NOT_IMPLEMENTED} When not implemented by subclass
*/
statSync(path, options) {
throw new ERR_METHOD_NOT_IMPLEMENTED('statSync');
}
/**
* Gets stats for a path without following symlinks.
* @param {string} path The path to stat
* @param {object} [options] Options
* @returns {Promise<Stats>}
*/
async lstat(path, options) {
// Default: same as stat (for providers that don't support symlinks)
return this.stat(path, options);
}
/**
* Gets stats for a path synchronously without following symlinks.
* @param {string} path The path to stat
* @param {object} [options] Options
* @returns {Stats}
*/
lstatSync(path, options) {
// Default: same as statSync (for providers that don't support symlinks)
return this.statSync(path, options);
}
/**
* Reads directory contents.
* @param {string} path The directory path
* @param {object} [options] Options
* @returns {Promise<string[]|Dirent[]>}
*/
async readdir(path, options) {
throw new ERR_METHOD_NOT_IMPLEMENTED('readdir');
}
/**
* Reads directory contents synchronously.
* @param {string} path The directory path
* @param {object} [options] Options
* @throws {ERR_METHOD_NOT_IMPLEMENTED} When not implemented by subclass
*/
readdirSync(path, options) {
throw new ERR_METHOD_NOT_IMPLEMENTED('readdirSync');
}
/**
* Creates a directory.
* @param {string} path The directory path
* @param {object} [options] Options
* @returns {Promise<void>}
*/
async mkdir(path, options) {
if (this.readonly) {
throw createEROFS('mkdir', path);
}
throw new ERR_METHOD_NOT_IMPLEMENTED('mkdir');
}
/**
* Creates a directory synchronously.
* @param {string} path The directory path
* @param {object} [options] Options
*/
mkdirSync(path, options) {
if (this.readonly) {
throw createEROFS('mkdir', path);
}
throw new ERR_METHOD_NOT_IMPLEMENTED('mkdirSync');
}
/**
* Removes a directory.
* @param {string} path The directory path
* @returns {Promise<void>}
*/
async rmdir(path) {
if (this.readonly) {
throw createEROFS('rmdir', path);
}
throw new ERR_METHOD_NOT_IMPLEMENTED('rmdir');
}
/**
* Removes a directory synchronously.
* @param {string} path The directory path
*/
rmdirSync(path) {
if (this.readonly) {
throw createEROFS('rmdir', path);
}
throw new ERR_METHOD_NOT_IMPLEMENTED('rmdirSync');
}
/**
* Removes a file.
* @param {string} path The file path
* @returns {Promise<void>}
*/
async unlink(path) {
if (this.readonly) {
throw createEROFS('unlink', path);
}
throw new ERR_METHOD_NOT_IMPLEMENTED('unlink');
}
/**
* Removes a file synchronously.
* @param {string} path The file path
*/
unlinkSync(path) {
if (this.readonly) {
throw createEROFS('unlink', path);
}
throw new ERR_METHOD_NOT_IMPLEMENTED('unlinkSync');
}
/**
* Renames a file or directory.
* @param {string} oldPath The old path
* @param {string} newPath The new path
* @returns {Promise<void>}
*/
async rename(oldPath, newPath) {
if (this.readonly) {
throw createEROFS('rename', oldPath);
}
throw new ERR_METHOD_NOT_IMPLEMENTED('rename');
}
/**
* Renames a file or directory synchronously.
* @param {string} oldPath The old path
* @param {string} newPath The new path
*/
renameSync(oldPath, newPath) {
if (this.readonly) {
throw createEROFS('rename', oldPath);
}
throw new ERR_METHOD_NOT_IMPLEMENTED('renameSync');
}
// === DEFAULT IMPLEMENTATIONS (built on primitives) ===
/**
* Reads a file.
* @param {string} path The file path
* @param {object|string} [options] Options or encoding
* @returns {Promise<Buffer|string>}
*/
async readFile(path, options) {
const flag = (typeof options === 'object' && options !== null) ?
(options.flag ?? 'r') : 'r';
const handle = await this.open(path, flag);
try {
return await handle.readFile(options);
} finally {
await handle.close();
}
}
/**
* Reads a file synchronously.
* @param {string} path The file path
* @param {object|string} [options] Options or encoding
* @returns {Buffer|string}
*/
readFileSync(path, options) {
const flag = (typeof options === 'object' && options !== null) ?
(options.flag ?? 'r') : 'r';
const handle = this.openSync(path, flag);
try {
return handle.readFileSync(options);
} finally {
handle.closeSync();
}
}
/**
* Writes a file.
* @param {string} path The file path
* @param {Buffer|string} data The data to write
* @param {object} [options] Options
* @returns {Promise<void>}
*/
async writeFile(path, data, options) {
if (this.readonly) {
throw createEROFS('open', path);
}
const flag = options?.flag ?? 'w';
const handle = await this.open(path, flag, options?.mode);
try {
await handle.writeFile(data, options);
} finally {
await handle.close();
}
}
/**
* Writes a file synchronously.
* @param {string} path The file path
* @param {Buffer|string} data The data to write
* @param {object} [options] Options
*/
writeFileSync(path, data, options) {
if (this.readonly) {
throw createEROFS('open', path);
}
const flag = options?.flag ?? 'w';
const handle = this.openSync(path, flag, options?.mode);
try {
handle.writeFileSync(data, options);
} finally {
handle.closeSync();
}
}
/**
* Appends to a file.
* @param {string} path The file path
* @param {Buffer|string} data The data to append
* @param {object} [options] Options
* @returns {Promise<void>}
*/
async appendFile(path, data, options) {
if (this.readonly) {
throw createEROFS('open', path);
}
const flag = options?.flag ?? 'a';
const handle = await this.open(path, flag, options?.mode);
try {
await handle.writeFile(data, options);
} finally {
await handle.close();
}
}
/**
* Appends to a file synchronously.
* @param {string} path The file path
* @param {Buffer|string} data The data to append
* @param {object} [options] Options
*/
appendFileSync(path, data, options) {
if (this.readonly) {
throw createEROFS('open', path);
}
const flag = options?.flag ?? 'a';
const handle = this.openSync(path, flag, options?.mode);
try {
handle.writeFileSync(data, options);
} finally {
handle.closeSync();
}
}
/**
* Checks if a path exists.
* @param {string} path The path to check
* @returns {Promise<boolean>}
*/
async exists(path) {
try {
await this.stat(path);
return true;
} catch {
return false;
}
}
/**
* Checks if a path exists synchronously.
* @param {string} path The path to check
* @returns {boolean}
*/
existsSync(path) {
try {
this.statSync(path);
return true;
} catch {
return false;
}
}
/**
* Copies a file.
* @param {string} src Source path
* @param {string} dest Destination path
* @param {number} [mode] Copy mode flags
* @returns {Promise<void>}
*/
async copyFile(src, dest, mode) {
if (this.readonly) {
throw createEROFS('copyfile', dest);
}
if ((mode & COPYFILE_EXCL) !== 0) {
if (await this.exists(dest)) {
throw createEEXIST('copyfile', dest);
}
}
const content = await this.readFile(src);
await this.writeFile(dest, content);
}
/**
* Copies a file synchronously.
* @param {string} src Source path
* @param {string} dest Destination path
* @param {number} [mode] Copy mode flags
*/
copyFileSync(src, dest, mode) {
if (this.readonly) {
throw createEROFS('copyfile', dest);
}
if ((mode & COPYFILE_EXCL) !== 0) {
if (this.existsSync(dest)) {
throw createEEXIST('copyfile', dest);
}
}
const content = this.readFileSync(src);
this.writeFileSync(dest, content);
}
/**
* Gets the real path by resolving symlinks.
* @param {string} path The path
* @param {object} [options] Options
* @returns {Promise<string>}
*/
async realpath(path, options) {
// Default: return the path as-is (for providers without symlinks)
// First verify the path exists
await this.stat(path);
return path;
}
/**
* Gets the real path synchronously.
* @param {string} path The path
* @param {object} [options] Options
* @returns {string}
*/
realpathSync(path, options) {
// Default: return the path as-is (for providers without symlinks)
// First verify the path exists
this.statSync(path);
return path;
}
/**
* Checks file accessibility.
* @param {string} path The path to check
* @param {number} [mode] Access mode
* @returns {Promise<void>}
*/
async access(path, mode) {
const stats = await this.stat(path);
this.#checkAccessMode(path, stats, mode);
}
/**
* Checks file accessibility synchronously.
* @param {string} path The path to check
* @param {number} [mode] Access mode
*/
accessSync(path, mode) {
const stats = this.statSync(path);
this.#checkAccessMode(path, stats, mode);
}
/**
* Checks access mode bits against file stats.
* @param {string} path The path (for error messages)
* @param {Stats} stats The file stats
* @param {number} mode The requested access mode
*/
#checkAccessMode(path, stats, mode) {
if (mode == null || mode === 0) return; // F_OK = 0, existence-only check
const fileMode = stats.mode & 0o777; // Permission bits
// Check owner permissions (simplified: treat VFS user as owner)
if ((mode & R_OK) !== 0 && (fileMode & 0o400) === 0) {
throw createEACCES('access', path);
}
if ((mode & W_OK) !== 0 && (fileMode & 0o200) === 0) {
throw createEACCES('access', path);
}
if ((mode & X_OK) !== 0 && (fileMode & 0o100) === 0) {
throw createEACCES('access', path);
}
}
// === HARD LINK OPERATIONS (optional) ===
/**
* Creates a hard link.
* @param {string} existingPath The existing file path
* @param {string} newPath The new link path
* @returns {Promise<void>}
*/
async link(existingPath, newPath) {
if (this.readonly) {
throw createEROFS('link', newPath);
}
throw new ERR_METHOD_NOT_IMPLEMENTED('link');
}
/**
* Creates a hard link synchronously.
* @param {string} existingPath The existing file path
* @param {string} newPath The new link path
*/
linkSync(existingPath, newPath) {
if (this.readonly) {
throw createEROFS('link', newPath);
}
throw new ERR_METHOD_NOT_IMPLEMENTED('linkSync');
}
// === SYMLINK OPERATIONS (optional, throw ENOENT by default) ===
/**
* Reads the target of a symbolic link.
* @param {string} path The symlink path
* @param {object} [options] Options
* @returns {Promise<string>}
*/
async readlink(path, options) {
throw new ERR_METHOD_NOT_IMPLEMENTED('readlink');
}
/**
* Reads the target of a symbolic link synchronously.
* @param {string} path The symlink path
* @param {object} [options] Options
* @throws {ERR_METHOD_NOT_IMPLEMENTED} When not implemented by subclass
*/
readlinkSync(path, options) {
throw new ERR_METHOD_NOT_IMPLEMENTED('readlinkSync');
}
/**
* Creates a symbolic link.
* @param {string} target The symlink target
* @param {string} path The symlink path
* @param {string} [type] The symlink type (file, dir, junction)
* @returns {Promise<void>}
*/
async symlink(target, path, type) {
if (this.readonly) {
throw createEROFS('symlink', path);
}
throw new ERR_METHOD_NOT_IMPLEMENTED('symlink');
}
/**
* Creates a symbolic link synchronously.
* @param {string} target The symlink target
* @param {string} path The symlink path
* @param {string} [type] The symlink type (file, dir, junction)
*/
symlinkSync(target, path, type) {
if (this.readonly) {
throw createEROFS('symlink', path);
}
throw new ERR_METHOD_NOT_IMPLEMENTED('symlinkSync');
}
// === WATCH OPERATIONS (optional, polling-based) ===
/**
* Watches a file or directory for changes.
* Returns an EventEmitter-like object that emits 'change' and 'close' events.
* @param {string} path The path to watch
* @param {object} [options] Watch options
* @param {number} [options.interval] Polling interval in ms (default: 100)
* @param {boolean} [options.recursive] Watch subdirectories (default: false)
* @throws {ERR_METHOD_NOT_IMPLEMENTED} When not overridden by subclass
*/
watch(path, options) {
throw new ERR_METHOD_NOT_IMPLEMENTED('watch');
}
/**
* Watches a file or directory for changes (async iterable version).
* Used by fs.promises.watch().
* @param {string} path The path to watch
* @param {object} [options] Watch options
* @param {number} [options.interval] Polling interval in ms (default: 100)
* @param {boolean} [options.recursive] Watch subdirectories (default: false)
* @param {AbortSignal} [options.signal] AbortSignal for cancellation
* @throws {ERR_METHOD_NOT_IMPLEMENTED} When not overridden by subclass
*/
watchAsync(path, options) {
throw new ERR_METHOD_NOT_IMPLEMENTED('watchAsync');
}
/**
* Watches a file for changes using stat polling.
* Returns a StatWatcher-like object that emits 'change' events with stats.
* @param {string} path The path to watch
* @param {object} [options] Watch options
* @param {number} [options.interval] Polling interval in ms (default: 5007)
* @param {boolean} [options.persistent] Whether the watcher should prevent exit
* @throws {ERR_METHOD_NOT_IMPLEMENTED} When not overridden by subclass
*/
watchFile(path, options) {
throw new ERR_METHOD_NOT_IMPLEMENTED('watchFile');
}
/**
* Stops watching a file for changes.
* @param {string} path The path to stop watching
* @param {Function} [listener] Optional listener to remove
*/
unwatchFile(path, listener) {
throw new ERR_METHOD_NOT_IMPLEMENTED('unwatchFile');
}
}
module.exports = {
VirtualProvider,
};
File diff suppressed because it is too large. Load diff
+477
View File
@@ -0,0 +1,477 @@
'use strict';
const {
Promise,
StringPrototypeStartsWith,
} = primordials;
const fs = require('fs');
const path = require('path');
const { VirtualProvider } = require('internal/vfs/provider');
const { VirtualFileHandle } = require('internal/vfs/file_handle');
const { getValidatedPath } = require('internal/fs/utils');
const { setOwnProperty } = require('internal/util');
const {
createEACCES,
createEBADF,
createENOENT,
} = require('internal/vfs/errors');
/**
* A file handle that wraps a real file descriptor.
*/
// TODO(mcollina): reuse FileHandle from internal/fs/promises for the async
// methods instead of manually wrapping fs.read/write/fstat/ftruncate/close in
// Promises. Blocked on a way to wrap an existing numeric fd in a FileHandle so
// sync-opened handles can still share one underlying handle for async ops.
class RealFileHandle extends VirtualFileHandle {
#fd;
#realPath;
#checkClosed(syscall) {
if (this.closed) {
throw createEBADF(syscall);
}
}
/**
* @param {string} path The VFS path
* @param {string} flags The open flags
* @param {number} mode The file mode
* @param {number} fd The real file descriptor
* @param {string} realPath The real filesystem path
*/
constructor(path, flags, mode, fd, realPath) {
super(path, flags, mode);
this.#fd = fd;
this.#realPath = realPath;
}
readSync(buffer, offset, length, position) {
this.#checkClosed('read');
return fs.readSync(this.#fd, buffer, offset, length, position);
}
async read(buffer, offset, length, position) {
this.#checkClosed('read');
return new Promise((resolve, reject) => {
fs.read(this.#fd, buffer, offset, length, position, (err, bytesRead) => {
if (err) reject(err);
else resolve({ __proto__: null, bytesRead, buffer });
});
});
}
writeSync(buffer, offset, length, position) {
this.#checkClosed('write');
return fs.writeSync(this.#fd, buffer, offset, length, position);
}
async write(buffer, offset, length, position) {
this.#checkClosed('write');
return new Promise((resolve, reject) => {
fs.write(this.#fd, buffer, offset, length, position, (err, bytesWritten) => {
if (err) reject(err);
else resolve({ __proto__: null, bytesWritten, buffer });
});
});
}
readFileSync(options) {
this.#checkClosed('read');
return fs.readFileSync(this.#realPath, options);
}
async readFile(options) {
this.#checkClosed('read');
return fs.promises.readFile(this.#realPath, options);
}
writeFileSync(data, options) {
this.#checkClosed('write');
fs.writeFileSync(this.#realPath, data, options);
}
async writeFile(data, options) {
this.#checkClosed('write');
return fs.promises.writeFile(this.#realPath, data, options);
}
statSync(options) {
this.#checkClosed('fstat');
return fs.fstatSync(this.#fd, options);
}
async stat(options) {
this.#checkClosed('fstat');
return new Promise((resolve, reject) => {
fs.fstat(this.#fd, options, (err, stats) => {
if (err) reject(err);
else resolve(stats);
});
});
}
truncateSync(len = 0) {
this.#checkClosed('ftruncate');
fs.ftruncateSync(this.#fd, len);
}
async truncate(len = 0) {
this.#checkClosed('ftruncate');
return new Promise((resolve, reject) => {
fs.ftruncate(this.#fd, len, (err) => {
if (err) reject(err);
else resolve();
});
});
}
closeSync() {
if (!this.closed) {
fs.closeSync(this.#fd);
super.closeSync();
}
}
async close() {
if (!this.closed) {
return new Promise((resolve, reject) => {
fs.close(this.#fd, (err) => {
if (err) reject(err);
else {
super.closeSync();
resolve();
}
});
});
}
}
}
/**
* A provider that wraps a real filesystem directory.
* Allows mounting a real directory at a different VFS path.
*/
class RealFSProvider extends VirtualProvider {
#rootPath;
/**
* @param {string} rootPath The real filesystem path to use as root
*/
constructor(rootPath) {
super();
// Resolve to absolute path and normalize
this.#rootPath = path.resolve(getValidatedPath(rootPath, 'rootPath'));
setOwnProperty(this, 'readonly', false);
setOwnProperty(this, 'supportsSymlinks', true);
}
/**
* Gets the root path of this provider.
* @returns {string}
*/
get rootPath() {
return this.#rootPath;
}
/**
* Resolves a VFS path to a real filesystem path.
* Ensures the path doesn't escape the root directory.
* @param {string} vfsPath The VFS path (relative to provider root)
* @returns {string} The real filesystem path
* @private
*/
#resolvePath(vfsPath, followSymlinks = true) {
// Normalize the VFS path (remove leading slash, handle . and ..)
let normalized = vfsPath;
if (normalized.startsWith('/')) {
normalized = normalized.slice(1);
}
// Join with root and resolve
const realPath = path.resolve(this.#rootPath, normalized);
// Security check: ensure the resolved path is within rootPath
const rootWithSep = this.#rootPath.endsWith(path.sep) ?
this.#rootPath :
this.#rootPath + path.sep;
if (realPath !== this.#rootPath && !StringPrototypeStartsWith(realPath, rootWithSep)) {
throw createENOENT('open', vfsPath);
}
// Resolve symlinks to prevent escape via symbolic links
if (followSymlinks) {
try {
const resolved = fs.realpathSync(realPath);
if (resolved !== this.#rootPath &&
!StringPrototypeStartsWith(resolved, rootWithSep)) {
throw createENOENT('open', vfsPath);
}
return resolved;
} catch (err) {
if (err?.code !== 'ENOENT') throw err;
// Path doesn't exist yet - verify deepest existing ancestor
this.#verifyAncestorInRoot(realPath, rootWithSep, vfsPath);
return realPath;
}
}
// For lstat/readlink (no final symlink follow), check parent only
this.#verifyAncestorInRoot(realPath, rootWithSep, vfsPath);
return realPath;
}
/**
* Verifies that the deepest existing ancestor of a path is within rootPath.
* @param {string} realPath The real filesystem path
* @param {string} rootWithSep The rootPath with trailing separator
* @param {string} vfsPath The original VFS path (for error messages)
*/
#verifyAncestorInRoot(realPath, rootWithSep, vfsPath) {
let current = path.dirname(realPath);
while (current.length >= this.#rootPath.length) {
try {
const resolved = fs.realpathSync(current);
if (resolved !== this.#rootPath &&
!StringPrototypeStartsWith(resolved, rootWithSep)) {
throw createENOENT('open', vfsPath);
}
return;
} catch (err) {
if (err?.code !== 'ENOENT') throw err;
current = path.dirname(current);
}
}
}
openSync(vfsPath, flags, mode) {
const realPath = this.#resolvePath(vfsPath);
const fd = fs.openSync(realPath, flags, mode);
return new RealFileHandle(vfsPath, flags, mode ?? 0o644, fd, realPath);
}
async open(vfsPath, flags, mode) {
const realPath = this.#resolvePath(vfsPath);
return new Promise((resolve, reject) => {
fs.open(realPath, flags, mode, (err, fd) => {
if (err) reject(err);
else resolve(new RealFileHandle(vfsPath, flags, mode ?? 0o644, fd, realPath));
});
});
}
statSync(vfsPath, options) {
const realPath = this.#resolvePath(vfsPath);
return fs.statSync(realPath, options);
}
async stat(vfsPath, options) {
const realPath = this.#resolvePath(vfsPath);
return fs.promises.stat(realPath, options);
}
lstatSync(vfsPath, options) {
const realPath = this.#resolvePath(vfsPath, false);
return fs.lstatSync(realPath, options);
}
async lstat(vfsPath, options) {
const realPath = this.#resolvePath(vfsPath, false);
return fs.promises.lstat(realPath, options);
}
readdirSync(vfsPath, options) {
const realPath = this.#resolvePath(vfsPath);
return fs.readdirSync(realPath, options);
}
async readdir(vfsPath, options) {
const realPath = this.#resolvePath(vfsPath);
return fs.promises.readdir(realPath, options);
}
mkdirSync(vfsPath, options) {
const realPath = this.#resolvePath(vfsPath);
return fs.mkdirSync(realPath, options);
}
async mkdir(vfsPath, options) {
const realPath = this.#resolvePath(vfsPath);
return fs.promises.mkdir(realPath, options);
}
rmdirSync(vfsPath) {
const realPath = this.#resolvePath(vfsPath);
fs.rmdirSync(realPath);
}
async rmdir(vfsPath) {
const realPath = this.#resolvePath(vfsPath);
return fs.promises.rmdir(realPath);
}
unlinkSync(vfsPath) {
const realPath = this.#resolvePath(vfsPath);
fs.unlinkSync(realPath);
}
async unlink(vfsPath) {
const realPath = this.#resolvePath(vfsPath);
return fs.promises.unlink(realPath);
}
renameSync(oldVfsPath, newVfsPath) {
const oldRealPath = this.#resolvePath(oldVfsPath);
const newRealPath = this.#resolvePath(newVfsPath);
fs.renameSync(oldRealPath, newRealPath);
}
async rename(oldVfsPath, newVfsPath) {
const oldRealPath = this.#resolvePath(oldVfsPath);
const newRealPath = this.#resolvePath(newVfsPath);
return fs.promises.rename(oldRealPath, newRealPath);
}
readlinkSync(vfsPath, options) {
const realPath = this.#resolvePath(vfsPath, false);
const target = fs.readlinkSync(realPath, options);
// Translate absolute targets within rootPath to VFS-relative
if (path.isAbsolute(target)) {
const rootWithSep = this.#rootPath + path.sep;
if (target === this.#rootPath) {
return '/';
}
if (StringPrototypeStartsWith(target, rootWithSep)) {
return '/' + target.slice(rootWithSep.length).replace(/\\/g, '/');
}
}
return target;
}
async readlink(vfsPath, options) {
const realPath = this.#resolvePath(vfsPath, false);
const target = await fs.promises.readlink(realPath, options);
// Translate absolute targets within rootPath to VFS-relative
if (path.isAbsolute(target)) {
const rootWithSep = this.#rootPath + path.sep;
if (target === this.#rootPath) {
return '/';
}
if (StringPrototypeStartsWith(target, rootWithSep)) {
return '/' + target.slice(rootWithSep.length).replace(/\\/g, '/');
}
}
return target;
}
symlinkSync(target, vfsPath, type) {
// Validate target resolves within rootPath
if (path.isAbsolute(target)) {
throw createEACCES('symlink', vfsPath);
}
const realPath = this.#resolvePath(vfsPath);
const resolvedTarget = path.resolve(path.dirname(realPath), target);
const rootWithSep = this.#rootPath.endsWith(path.sep) ?
this.#rootPath : this.#rootPath + path.sep;
if (resolvedTarget !== this.#rootPath &&
!StringPrototypeStartsWith(resolvedTarget, rootWithSep)) {
throw createEACCES('symlink', vfsPath);
}
fs.symlinkSync(target, realPath, type);
}
async symlink(target, vfsPath, type) {
// Validate target resolves within rootPath
if (path.isAbsolute(target)) {
throw createEACCES('symlink', vfsPath);
}
const realPath = this.#resolvePath(vfsPath);
const resolvedTarget = path.resolve(path.dirname(realPath), target);
const rootWithSep = this.#rootPath.endsWith(path.sep) ?
this.#rootPath : this.#rootPath + path.sep;
if (resolvedTarget !== this.#rootPath &&
!StringPrototypeStartsWith(resolvedTarget, rootWithSep)) {
throw createEACCES('symlink', vfsPath);
}
return fs.promises.symlink(target, realPath, type);
}
// path.relative handles case-insensitivity on Windows, which matters here
// because fs.realpathSync (a JS impl) preserves case but fs.promises.realpath
// (native) canonicalizes the drive letter and other components.
#resolvedToVfsPath(resolved, vfsPath, syscall) {
const rel = path.relative(this.#rootPath, resolved);
if (rel === '') return '/';
if (rel === '..' ||
StringPrototypeStartsWith(rel, '..' + path.sep) ||
path.isAbsolute(rel)) {
throw createEACCES(syscall, vfsPath);
}
return '/' + rel.replace(/\\/g, '/');
}
realpathSync(vfsPath, options) {
const realPath = this.#resolvePath(vfsPath);
const resolved = fs.realpathSync(realPath, options);
return this.#resolvedToVfsPath(resolved, vfsPath, 'realpath');
}
async realpath(vfsPath, options) {
const realPath = this.#resolvePath(vfsPath);
const resolved = await fs.promises.realpath(realPath, options);
return this.#resolvedToVfsPath(resolved, vfsPath, 'realpath');
}
accessSync(vfsPath, mode) {
const realPath = this.#resolvePath(vfsPath);
fs.accessSync(realPath, mode);
}
async access(vfsPath, mode) {
const realPath = this.#resolvePath(vfsPath);
return fs.promises.access(realPath, mode);
}
copyFileSync(srcVfsPath, destVfsPath, mode) {
const srcRealPath = this.#resolvePath(srcVfsPath);
const destRealPath = this.#resolvePath(destVfsPath);
fs.copyFileSync(srcRealPath, destRealPath, mode);
}
async copyFile(srcVfsPath, destVfsPath, mode) {
const srcRealPath = this.#resolvePath(srcVfsPath);
const destRealPath = this.#resolvePath(destVfsPath);
return fs.promises.copyFile(srcRealPath, destRealPath, mode);
}
get supportsWatch() {
return true;
}
watch(vfsPath, options) {
const realPath = this.#resolvePath(vfsPath);
return fs.watch(realPath, options);
}
watchAsync(vfsPath, options) {
const realPath = this.#resolvePath(vfsPath);
return fs.promises.watch(realPath, options);
}
watchFile(vfsPath, options) {
const realPath = this.#resolvePath(vfsPath);
return fs.watchFile(realPath, options, () => {});
}
unwatchFile(vfsPath, listener) {
const realPath = this.#resolvePath(vfsPath);
fs.unwatchFile(realPath, listener);
}
}
module.exports = {
RealFSProvider,
RealFileHandle,
};
+300
View File
@@ -0,0 +1,300 @@
'use strict';
const {
BigInt,
BigInt64Array,
DateNow,
Float64Array,
MathCeil,
MathFloor,
} = primordials;
const {
fs: {
S_IFDIR,
S_IFREG,
S_IFLNK,
},
} = internalBinding('constants');
const { getStatsFromBinding } = require('internal/fs/utils');
// Default block size for virtual files (4KB)
const kDefaultBlockSize = 4096;
// Distinctive device number for VFS files (0xVF5 = 4085)
const kVfsDev = 4085;
// Incrementing inode counter for unique ino values
let inoCounter = 1;
// Reusable arrays for creating Stats objects.
// IMPORTANT: Safe only because getStatsFromBinding copies synchronously.
// Do not use in async paths.
// Format: dev, mode, nlink, uid, gid, rdev, blksize, ino, size, blocks,
// atime_sec, atime_nsec, mtime_sec, mtime_nsec, ctime_sec, ctime_nsec,
// birthtime_sec, birthtime_nsec
const statsArray = new Float64Array(18);
const bigintStatsArray = new BigInt64Array(18);
/**
* Converts milliseconds to seconds and nanoseconds.
* @param {number} ms Milliseconds
* @returns {{ sec: number, nsec: number }}
*/
function msToTimeSpec(ms) {
const sec = MathFloor(ms / 1000);
const nsec = (ms % 1000) * 1_000_000;
return { sec, nsec };
}
/**
* Fills the bigint stats array with the given values.
* @returns {Stats}
*/
function fillBigIntStatsArray(
dev, mode, nlink, uid, gid, rdev, blksize, ino,
size, blocks, atime, mtime, ctime, birthtime,
) {
bigintStatsArray[0] = BigInt(dev);
bigintStatsArray[1] = BigInt(mode);
bigintStatsArray[2] = BigInt(nlink);
bigintStatsArray[3] = BigInt(uid);
bigintStatsArray[4] = BigInt(gid);
bigintStatsArray[5] = BigInt(rdev);
bigintStatsArray[6] = BigInt(blksize);
bigintStatsArray[7] = BigInt(ino);
bigintStatsArray[8] = BigInt(size);
bigintStatsArray[9] = BigInt(blocks);
bigintStatsArray[10] = BigInt(atime.sec);
bigintStatsArray[11] = BigInt(atime.nsec);
bigintStatsArray[12] = BigInt(mtime.sec);
bigintStatsArray[13] = BigInt(mtime.nsec);
bigintStatsArray[14] = BigInt(ctime.sec);
bigintStatsArray[15] = BigInt(ctime.nsec);
bigintStatsArray[16] = BigInt(birthtime.sec);
bigintStatsArray[17] = BigInt(birthtime.nsec);
return getStatsFromBinding(bigintStatsArray);
}
/**
* Creates a Stats object for a virtual file.
* @param {number} size The file size in bytes
* @param {object} [options] Optional stat properties
* @param {number} [options.mode] File mode (default: 0o644)
* @param {number} [options.uid] User ID (default: process.getuid() or 0)
* @param {number} [options.gid] Group ID (default: process.getgid() or 0)
* @param {number} [options.atimeMs] Access time in ms
* @param {number} [options.mtimeMs] Modification time in ms
* @param {number} [options.ctimeMs] Change time in ms
* @param {number} [options.birthtimeMs] Birth time in ms
* @param {boolean} [options.bigint] Return BigIntStats
* @returns {Stats}
*/
function createFileStats(size, options = {}) {
const now = DateNow();
const mode = (options.mode ?? 0o644) | S_IFREG;
const nlink = options.nlink ?? 1;
const uid = options.uid ?? (process.getuid?.() ?? 0);
const gid = options.gid ?? (process.getgid?.() ?? 0);
const atimeMs = options.atimeMs ?? now;
const mtimeMs = options.mtimeMs ?? now;
const ctimeMs = options.ctimeMs ?? now;
const birthtimeMs = options.birthtimeMs ?? now;
const blocks = MathCeil(size / 512);
const ino = inoCounter++;
const atime = msToTimeSpec(atimeMs);
const mtime = msToTimeSpec(mtimeMs);
const ctime = msToTimeSpec(ctimeMs);
const birthtime = msToTimeSpec(birthtimeMs);
if (options.bigint) {
return fillBigIntStatsArray(
kVfsDev, mode, nlink, uid, gid, 0, kDefaultBlockSize, ino,
size, blocks, atime, mtime, ctime, birthtime,
);
}
statsArray[0] = kVfsDev; // dev
statsArray[1] = mode; // mode
statsArray[2] = nlink; // nlink
statsArray[3] = uid; // uid
statsArray[4] = gid; // gid
statsArray[5] = 0; // rdev
statsArray[6] = kDefaultBlockSize; // blksize
statsArray[7] = ino; // ino
statsArray[8] = size; // size
statsArray[9] = blocks; // blocks
statsArray[10] = atime.sec; // atime_sec
statsArray[11] = atime.nsec; // atime_nsec
statsArray[12] = mtime.sec; // mtime_sec
statsArray[13] = mtime.nsec; // mtime_nsec
statsArray[14] = ctime.sec; // ctime_sec
statsArray[15] = ctime.nsec; // ctime_nsec
statsArray[16] = birthtime.sec; // birthtime_sec
statsArray[17] = birthtime.nsec; // birthtime_nsec
return getStatsFromBinding(statsArray);
}
/**
* Creates a Stats object for a virtual directory.
* @param {object} [options] Optional stat properties
* @param {number} [options.mode] Directory mode (default: 0o755)
* @param {number} [options.uid] User ID (default: process.getuid() or 0)
* @param {number} [options.gid] Group ID (default: process.getgid() or 0)
* @param {number} [options.atimeMs] Access time in ms
* @param {number} [options.mtimeMs] Modification time in ms
* @param {number} [options.ctimeMs] Change time in ms
* @param {number} [options.birthtimeMs] Birth time in ms
* @returns {Stats}
*/
function createDirectoryStats(options = {}) {
const now = DateNow();
const mode = (options.mode ?? 0o755) | S_IFDIR;
const uid = options.uid ?? (process.getuid?.() ?? 0);
const gid = options.gid ?? (process.getgid?.() ?? 0);
const atimeMs = options.atimeMs ?? now;
const mtimeMs = options.mtimeMs ?? now;
const ctimeMs = options.ctimeMs ?? now;
const birthtimeMs = options.birthtimeMs ?? now;
const ino = inoCounter++;
const atime = msToTimeSpec(atimeMs);
const mtime = msToTimeSpec(mtimeMs);
const ctime = msToTimeSpec(ctimeMs);
const birthtime = msToTimeSpec(birthtimeMs);
if (options.bigint) {
return fillBigIntStatsArray(
kVfsDev, mode, 1, uid, gid, 0, kDefaultBlockSize, ino,
kDefaultBlockSize, 8, atime, mtime, ctime, birthtime,
);
}
statsArray[0] = kVfsDev; // dev
statsArray[1] = mode; // mode
statsArray[2] = 1; // nlink
statsArray[3] = uid; // uid
statsArray[4] = gid; // gid
statsArray[5] = 0; // rdev
statsArray[6] = kDefaultBlockSize; // blksize
statsArray[7] = ino; // ino
statsArray[8] = kDefaultBlockSize; // size (directory size)
statsArray[9] = 8; // blocks
statsArray[10] = atime.sec; // atime_sec
statsArray[11] = atime.nsec; // atime_nsec
statsArray[12] = mtime.sec; // mtime_sec
statsArray[13] = mtime.nsec; // mtime_nsec
statsArray[14] = ctime.sec; // ctime_sec
statsArray[15] = ctime.nsec; // ctime_nsec
statsArray[16] = birthtime.sec; // birthtime_sec
statsArray[17] = birthtime.nsec; // birthtime_nsec
return getStatsFromBinding(statsArray);
}
/**
* Creates a Stats object for a virtual symbolic link.
* @param {number} size The symlink size (length of target path)
* @param {object} [options] Optional stat properties
* @param {number} [options.mode] Symlink mode (default: 0o777)
* @param {number} [options.uid] User ID (default: process.getuid() or 0)
* @param {number} [options.gid] Group ID (default: process.getgid() or 0)
* @param {number} [options.atimeMs] Access time in ms
* @param {number} [options.mtimeMs] Modification time in ms
* @param {number} [options.ctimeMs] Change time in ms
* @param {number} [options.birthtimeMs] Birth time in ms
* @returns {Stats}
*/
function createSymlinkStats(size, options = {}) {
const now = DateNow();
const mode = (options.mode ?? 0o777) | S_IFLNK;
const uid = options.uid ?? (process.getuid?.() ?? 0);
const gid = options.gid ?? (process.getgid?.() ?? 0);
const atimeMs = options.atimeMs ?? now;
const mtimeMs = options.mtimeMs ?? now;
const ctimeMs = options.ctimeMs ?? now;
const birthtimeMs = options.birthtimeMs ?? now;
const blocks = MathCeil(size / 512);
const ino = inoCounter++;
const atime = msToTimeSpec(atimeMs);
const mtime = msToTimeSpec(mtimeMs);
const ctime = msToTimeSpec(ctimeMs);
const birthtime = msToTimeSpec(birthtimeMs);
if (options.bigint) {
return fillBigIntStatsArray(
kVfsDev, mode, 1, uid, gid, 0, kDefaultBlockSize, ino,
size, blocks, atime, mtime, ctime, birthtime,
);
}
statsArray[0] = kVfsDev; // dev
statsArray[1] = mode; // mode
statsArray[2] = 1; // nlink
statsArray[3] = uid; // uid
statsArray[4] = gid; // gid
statsArray[5] = 0; // rdev
statsArray[6] = kDefaultBlockSize; // blksize
statsArray[7] = ino; // ino
statsArray[8] = size; // size
statsArray[9] = blocks; // blocks
statsArray[10] = atime.sec; // atime_sec
statsArray[11] = atime.nsec; // atime_nsec
statsArray[12] = mtime.sec; // mtime_sec
statsArray[13] = mtime.nsec; // mtime_nsec
statsArray[14] = ctime.sec; // ctime_sec
statsArray[15] = ctime.nsec; // ctime_nsec
statsArray[16] = birthtime.sec; // birthtime_sec
statsArray[17] = birthtime.nsec; // birthtime_nsec
return getStatsFromBinding(statsArray);
}
/**
* Creates a zeroed Stats object for non-existent files.
* All fields are zero, including mode (no S_IFREG bit set).
* This matches Node.js fs.watchFile() behavior for missing files.
* @returns {Stats}
*/
function createZeroStats(options) {
const zero = { sec: 0, nsec: 0 };
if (options?.bigint) {
return fillBigIntStatsArray(
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, zero, zero, zero, zero,
);
}
statsArray[0] = 0; // dev
statsArray[1] = 0; // mode (no file type bits)
statsArray[2] = 0; // nlink
statsArray[3] = 0; // uid
statsArray[4] = 0; // gid
statsArray[5] = 0; // rdev
statsArray[6] = 0; // blksize
statsArray[7] = 0; // ino
statsArray[8] = 0; // size
statsArray[9] = 0; // blocks
statsArray[10] = 0; // atime_sec
statsArray[11] = 0; // atime_nsec
statsArray[12] = 0; // mtime_sec
statsArray[13] = 0; // mtime_nsec
statsArray[14] = 0; // ctime_sec
statsArray[15] = 0; // ctime_nsec
statsArray[16] = 0; // birthtime_sec
statsArray[17] = 0; // birthtime_nsec
return getStatsFromBinding(statsArray);
}
module.exports = {
createFileStats,
createDirectoryStats,
createSymlinkStats,
createZeroStats,
};
+353
View File
@@ -0,0 +1,353 @@
'use strict';
const {
MathMin,
} = primordials;
const { Buffer } = require('buffer');
const { Readable, Writable } = require('stream');
const { createEBADF } = require('internal/vfs/errors');
const { getVirtualFd } = require('internal/vfs/fd');
const { kEmptyObject } = require('internal/util');
const { validateInteger } = require('internal/validators');
const {
codes: {
ERR_OUT_OF_RANGE,
},
} = require('internal/errors');
/**
* A readable stream for virtual files.
*/
class VirtualReadStream extends Readable {
#vfs;
#path;
#fd = null;
#end;
#pos;
#content = null;
#autoClose;
/**
* Number of bytes read so far.
* @type {number}
*/
bytesRead = 0;
/**
* True until the first read completes.
* @type {boolean}
*/
pending = true;
/**
* @param {VirtualFileSystem} vfs The VFS instance
* @param {string} filePath The path to the file
* @param {object} [options] Stream options
*/
constructor(vfs, filePath, options = kEmptyObject) {
const {
start,
end,
highWaterMark = 64 * 1024,
encoding,
fd,
...streamOptions
} = options;
// Validate start/end matching real ReadStream behavior
if (start !== undefined) {
validateInteger(start, 'start', 0);
}
if (end !== undefined && end !== Infinity) {
validateInteger(end, 'end', 0);
}
if (start !== undefined && end !== undefined && end !== Infinity &&
start > end) {
throw new ERR_OUT_OF_RANGE(
'start',
`<= "end" (here: ${end})`,
start,
);
}
super({ ...streamOptions, highWaterMark, encoding });
this.#vfs = vfs;
this.#path = filePath;
this.#end = end === undefined ? Infinity : end;
this.#pos = start === undefined ? 0 : start;
this.#autoClose = options.autoClose !== false;
if (fd !== null && fd !== undefined) {
// Use the already-open file descriptor
this.#fd = fd;
process.nextTick(() => {
this.emit('open', this.#fd);
this.emit('ready');
});
} else {
// Open the file on next tick so listeners can be attached.
// Note: #openFile will not throw - if it fails, the stream is destroyed.
process.nextTick(() => this.#openFile());
}
}
/**
* Gets the file path.
* @returns {string}
*/
get path() {
return this.#path;
}
/**
* Opens the virtual file.
* Events are emitted synchronously within this method, which runs
* asynchronously via process.nextTick - matching real fs behavior.
*/
#openFile() {
try {
this.#fd = this.#vfs.openSync(this.#path);
this.emit('open', this.#fd);
this.emit('ready');
} catch (err) {
this.destroy(err);
}
}
/**
* Implements the readable _read method.
* @param {number} size Number of bytes to read
*/
_read(size) {
if (this.destroyed || this.#fd === null) {
this.destroy(createEBADF('read'));
return;
}
// Load content on first read (lazy loading)
if (this.#content === null) {
try {
const vfd = getVirtualFd(this.#fd);
if (!vfd) {
this.destroy(createEBADF('read'));
return;
}
// Use the file handle's readFileSync to get content
this.#content = vfd.entry.readFileSync();
this.pending = false;
} catch (err) {
this.destroy(err);
return;
}
}
// Calculate how much to read
// Note: end is inclusive, so we use end + 1 for the upper bound
const endPos = this.#end === Infinity ? this.#content.length : this.#end + 1;
const remaining = MathMin(endPos, this.#content.length) - this.#pos;
if (remaining <= 0) {
this.push(null);
return;
}
const bytesToRead = MathMin(size, remaining);
const chunk = this.#content.subarray(this.#pos, this.#pos + bytesToRead);
this.#pos += bytesToRead;
this.bytesRead += bytesToRead;
this.push(chunk);
// Check if we've reached the end
if (this.#pos >= endPos || this.#pos >= this.#content.length) {
this.push(null);
}
}
/**
* Closes the file descriptor.
* Note: Does not emit 'close' - the base Readable class handles that.
*/
#close() {
if (this.#fd !== null) {
try {
this.#vfs.closeSync(this.#fd);
} catch {
// Ignore close errors
}
this.#fd = null;
}
}
/**
* Implements the readable _destroy method.
* @param {Error|null} err The error
* @param {Function} callback Callback
*/
_destroy(err, callback) {
if (this.#autoClose) {
this.#close();
}
callback(err);
}
}
/**
* A writable stream for virtual files.
*/
class VirtualWriteStream extends Writable {
#vfs;
#path;
#fd = null;
#autoClose;
#start;
/**
* Number of bytes written so far.
* @type {number}
*/
bytesWritten = 0;
/**
* True until the first write completes.
* @type {boolean}
*/
pending = true;
/**
* @param {VirtualFileSystem} vfs The VFS instance
* @param {string} filePath The path to the file
* @param {object} [options] Stream options
*/
constructor(vfs, filePath, options = kEmptyObject) {
const {
highWaterMark = 64 * 1024,
...streamOptions
} = options;
// Validate start matching real WriteStream behavior
if (options.start !== undefined) {
validateInteger(options.start, 'start', 0);
}
super({ ...streamOptions, highWaterMark });
this.#vfs = vfs;
this.#path = filePath;
this.#autoClose = options.autoClose !== false;
this.#start = options.start;
const fd = options.fd;
if (fd !== null && fd !== undefined) {
// Use the already-open file descriptor
this.#fd = fd;
if (this.#start !== undefined) {
this.#setPosition(this.#start);
}
process.nextTick(() => {
this.emit('open', this.#fd);
this.emit('ready');
});
} else {
// Open file synchronously (VFS is in-memory) so writes can proceed
// immediately. Emit events on next tick for listener attachment.
const flags = options.flags || 'w';
try {
this.#fd = this.#vfs.openSync(this.#path, flags);
if (this.#start !== undefined) {
this.#setPosition(this.#start);
}
} catch (err) {
process.nextTick(() => this.destroy(err));
return;
}
process.nextTick(() => {
this.emit('open', this.#fd);
this.emit('ready');
});
}
}
/**
* Sets the file handle position for the given fd.
* @param {number} pos The position to set
*/
#setPosition(pos) {
const vfd = getVirtualFd(this.#fd);
if (vfd) {
vfd.entry.position = pos;
}
}
/**
* Gets the file path.
* @returns {string}
*/
get path() {
return this.#path;
}
/**
* Implements the writable _write method.
* @param {Buffer|string} chunk Data to write
* @param {string} encoding Encoding
* @param {Function} callback Callback
*/
_write(chunk, encoding, callback) {
if (this.destroyed || this.#fd === null) {
callback(createEBADF('write'));
return;
}
try {
const buffer = typeof chunk === 'string' ?
Buffer.from(chunk, encoding) : chunk;
this.#vfs.writeSync(this.#fd, buffer, 0, buffer.length, null);
this.bytesWritten += buffer.length;
this.pending = false;
callback();
} catch (err) {
callback(err);
}
}
/**
* Implements the writable _final method (flush before close).
* @param {Function} callback Callback
*/
_final(callback) {
callback();
}
/**
* Closes the file descriptor.
*/
#close() {
if (this.#fd !== null) {
try {
this.#vfs.closeSync(this.#fd);
} catch {
// Ignore close errors
}
this.#fd = null;
}
}
/**
* Implements the writable _destroy method.
* @param {Error|null} err The error
* @param {Function} callback Callback
*/
_destroy(err, callback) {
if (this.#autoClose) {
this.#close();
}
callback(err);
}
}
module.exports = {
VirtualReadStream,
VirtualWriteStream,
};
+688
View File
@@ -0,0 +1,688 @@
'use strict';
const {
ArrayPrototypePush,
ObjectAssign,
Promise,
PromiseResolve,
SafeMap,
SafeSet,
SymbolAsyncIterator,
} = primordials;
const { AbortError } = require('internal/errors');
const { Buffer } = require('buffer');
const { EventEmitter } = require('events');
const { basename, join } = require('path');
const {
setInterval,
clearInterval,
} = require('timers');
/**
* VFSWatcher - Polling-based file/directory watcher for VFS.
* Emits 'change' events when the file content or stats change.
* Compatible with fs.watch() return value interface.
*/
class VFSWatcher extends EventEmitter {
#vfs;
#path;
#interval;
#timer = null;
#lastStats;
#closed = false;
#persistent;
#recursive;
#encoding;
#trackedFiles;
#signal;
#abortHandler = null;
/**
* @param {VirtualProvider} provider The VFS provider
* @param {string} path The path to watch (provider-relative)
* @param {object} [options] Options
* @param {number} [options.interval] Polling interval in ms (default: 100)
* @param {boolean} [options.persistent] Keep process alive (default: true)
* @param {boolean} [options.recursive] Watch subdirectories (default: false)
* @param {AbortSignal} [options.signal] AbortSignal for cancellation
*/
constructor(provider, path, options = {}) {
super();
this.#vfs = provider;
this.#path = path;
this.#interval = options.interval ?? 100;
this.#persistent = options.persistent !== false;
this.#recursive = options.recursive === true;
this.#encoding = options.encoding;
this.#trackedFiles = new SafeMap(); // path -> { stats, relativePath }
this.#signal = options.signal;
// Handle AbortSignal
if (this.#signal) {
if (this.#signal.aborted) {
this.close();
return;
}
this.#abortHandler = () => this.close();
this.#signal.addEventListener('abort', this.#abortHandler, { once: true });
}
// Get initial stats
this.#lastStats = this.#getStats();
// If watching a directory, build file list
if (this.#lastStats?.isDirectory()) {
if (this.#recursive) {
this.#buildFileList(this.#path, '');
} else {
this.#buildChildList(this.#path);
}
}
// Start polling
this.#startPolling();
}
/**
* Encodes a filename according to the watcher's encoding option.
* @param {string} filename The filename to encode
* @returns {string|Buffer} The encoded filename
*/
#encodeFilename(filename) {
if (this.#encoding === 'buffer') {
return Buffer.from(filename);
}
return filename;
}
/**
* Gets stats for the watched path.
* @returns {Stats|null} The stats or null if file doesn't exist
*/
#getStats() {
try {
return this.#vfs.statSync(this.#path);
} catch {
return null;
}
}
/**
* Starts the polling timer.
*/
#startPolling() {
if (this.#closed) return;
this.#timer = setInterval(() => this.#poll(), this.#interval);
// If not persistent, unref the timer to allow process to exit
if (!this.#persistent && this.#timer.unref) {
this.#timer.unref();
}
}
/**
* Polls for changes.
*/
#poll() {
if (this.#closed) return;
// For directory watching, poll tracked children
if (this.#lastStats?.isDirectory()) {
this.#pollDirectory();
return;
}
// For single file watching
const newStats = this.#getStats();
if (this.#statsChanged(this.#lastStats, newStats)) {
const eventType = this.#determineEventType(this.#lastStats, newStats);
const filename = this.#encodeFilename(basename(this.#path));
this.emit('change', eventType, filename);
}
this.#lastStats = newStats;
}
/**
* Polls directory children for changes, detecting new and deleted files.
*/
#pollDirectory() {
// Rescan for new files
if (this.#recursive) {
this.#rescanRecursive(this.#path, '');
} else {
this.#rescanChildren(this.#path);
}
// Check tracked files for changes/deletions
for (const { 0: filePath, 1: info } of this.#trackedFiles) {
const newStats = this.#getStatsFor(filePath);
if (newStats === null && info.stats !== null) {
// File was deleted
this.emit('change', 'rename', this.#encodeFilename(info.relativePath));
this.#trackedFiles.delete(filePath);
} else if (this.#statsChanged(info.stats, newStats)) {
const eventType = this.#determineEventType(info.stats, newStats);
this.emit('change', eventType, this.#encodeFilename(info.relativePath));
info.stats = newStats;
}
}
}
/**
* Rescans direct children for new entries.
* @param {string} dirPath The directory path
*/
#rescanChildren(dirPath) {
try {
const entries = this.#vfs.readdirSync(dirPath);
for (const name of entries) {
const fullPath = join(dirPath, name);
if (!this.#trackedFiles.has(fullPath)) {
const stats = this.#getStatsFor(fullPath);
this.#trackedFiles.set(fullPath, {
stats,
relativePath: name,
});
this.emit('change', 'rename', this.#encodeFilename(name));
}
}
} catch {
// Directory might not exist or be readable
}
}
/**
* Recursively rescans for new entries.
* @param {string} dirPath The directory path
* @param {string} relativePath The relative path from watched root
*/
#rescanRecursive(dirPath, relativePath) {
try {
const entries = this.#vfs.readdirSync(dirPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dirPath, entry.name);
const relPath = relativePath ?
join(relativePath, entry.name) : entry.name;
if (entry.isDirectory()) {
this.#rescanRecursive(fullPath, relPath);
} else if (!this.#trackedFiles.has(fullPath)) {
const stats = this.#getStatsFor(fullPath);
this.#trackedFiles.set(fullPath, {
stats,
relativePath: relPath,
});
this.emit('change', 'rename', this.#encodeFilename(relPath));
}
}
} catch {
// Directory might not exist or be readable
}
}
/**
* Gets stats for a specific path.
* @param {string} filePath The file path
* @returns {Stats|null}
*/
#getStatsFor(filePath) {
try {
return this.#vfs.statSync(filePath);
} catch {
return null;
}
}
/**
* Builds the list of files to track for recursive watching.
* @param {string} dirPath The directory path
* @param {string} relativePath The relative path from the watched root
*/
#buildFileList(dirPath, relativePath) {
try {
const entries = this.#vfs.readdirSync(dirPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dirPath, entry.name);
const relPath = relativePath ? join(relativePath, entry.name) : entry.name;
if (entry.isDirectory()) {
// Recurse into subdirectory
this.#buildFileList(fullPath, relPath);
} else {
// Track the file
const stats = this.#getStatsFor(fullPath);
this.#trackedFiles.set(fullPath, {
stats,
relativePath: relPath,
});
}
}
} catch {
// Directory might not exist or be readable
}
}
/**
* Builds a list of direct children to track for non-recursive watching.
* @param {string} dirPath The directory path
*/
#buildChildList(dirPath) {
try {
const entries = this.#vfs.readdirSync(dirPath);
for (const name of entries) {
const fullPath = join(dirPath, name);
const stats = this.#getStatsFor(fullPath);
this.#trackedFiles.set(fullPath, {
stats,
relativePath: name,
});
}
} catch {
// Directory might not exist or be readable
}
}
/**
* Checks if stats have changed.
* @param {Stats|null} oldStats Previous stats
* @param {Stats|null} newStats Current stats
* @returns {boolean} True if stats changed
*/
#statsChanged(oldStats, newStats) {
// File created or deleted
if ((oldStats === null) !== (newStats === null)) {
return true;
}
// Both null - no change
if (oldStats === null && newStats === null) {
return false;
}
// Compare mtime and size
if (oldStats.mtimeMs !== newStats.mtimeMs) {
return true;
}
if (oldStats.size !== newStats.size) {
return true;
}
return false;
}
/**
* Determines the event type based on stats change.
* @param {Stats|null} oldStats Previous stats
* @param {Stats|null} newStats Current stats
* @returns {string} 'rename' or 'change'
*/
#determineEventType(oldStats, newStats) {
// File was created or deleted
if ((oldStats === null) !== (newStats === null)) {
return 'rename';
}
// Content changed
return 'change';
}
/**
* Closes the watcher and stops polling.
*/
close() {
if (this.#closed) return;
this.#closed = true;
if (this.#timer) {
clearInterval(this.#timer);
this.#timer = null;
}
// Clear tracked files
this.#trackedFiles.clear();
// Remove abort handler
if (this.#signal && this.#abortHandler) {
this.#signal.removeEventListener('abort', this.#abortHandler);
}
this.emit('close');
}
/**
* Alias for close() - compatibility with FSWatcher.
* @returns {this}
*/
unref() {
this.#timer?.unref?.();
return this;
}
/**
* Makes the timer keep the process alive - compatibility with FSWatcher.
* @returns {this}
*/
ref() {
this.#timer?.ref?.();
return this;
}
}
/**
* VFSStatWatcher - Polling-based stat watcher for VFS.
* Emits 'change' events with current and previous stats.
* Compatible with fs.watchFile() return value interface.
*/
class VFSStatWatcher extends EventEmitter {
#vfs;
#path;
#interval;
#persistent;
#bigint;
#closed = false;
#timer = null;
#lastStats;
#listeners;
/**
* @param {VirtualProvider} provider The VFS provider
* @param {string} path The path to watch (provider-relative)
* @param {object} [options] Options
* @param {number} [options.interval] Polling interval in ms (default: 5007)
* @param {boolean} [options.persistent] Keep process alive (default: true)
*/
constructor(provider, path, options = {}) {
super();
this.#vfs = provider;
this.#path = path;
this.#interval = options.interval ?? 5007;
this.#persistent = options.persistent !== false;
this.#bigint = options.bigint === true;
this.#listeners = new SafeSet();
// Get initial stats
this.#lastStats = this.#getStats();
// Start polling
this.#startPolling();
}
/**
* Gets stats for the watched path.
* @returns {Stats} The stats (with zeroed values if file doesn't exist)
*/
#getStats() {
try {
return this.#vfs.statSync(this.#path, { bigint: this.#bigint });
} catch {
// Return a zeroed stats object for non-existent files
// This matches Node.js behavior
return this.#createZeroStats();
}
}
/**
* Creates a zeroed stats object for non-existent files.
* @returns {object} Zeroed stats
*/
#createZeroStats() {
const { createZeroStats } = require('internal/vfs/stats');
return createZeroStats({ bigint: this.#bigint });
}
/**
* Starts the polling timer.
*/
#startPolling() {
if (this.#closed) return;
this.#timer = setInterval(() => this.#poll(), this.#interval);
// If not persistent, unref the timer to allow process to exit
if (!this.#persistent && this.#timer.unref) {
this.#timer.unref();
}
}
/**
* Polls for changes.
*/
#poll() {
if (this.#closed) return;
const newStats = this.#getStats();
if (this.#statsChanged(this.#lastStats, newStats)) {
const prevStats = this.#lastStats;
this.#lastStats = newStats;
this.emit('change', newStats, prevStats);
}
}
/**
* Checks if stats have changed.
* @param {Stats} oldStats Previous stats
* @param {Stats} newStats Current stats
* @returns {boolean} True if stats changed
*/
#statsChanged(oldStats, newStats) {
// Compare mtime and ctime
if (oldStats.mtimeMs !== newStats.mtimeMs) {
return true;
}
if (oldStats.ctimeMs !== newStats.ctimeMs) {
return true;
}
if (oldStats.size !== newStats.size) {
return true;
}
return false;
}
/**
* Adds a listener for the given event.
* Tracks 'change' listeners for internal bookkeeping.
* @param {string} event The event name
* @param {Function} listener The listener function
* @returns {this}
*/
addListener(event, listener) {
if (event === 'change') {
this.#listeners.add(listener);
}
super.addListener(event, listener);
return this;
}
/**
* Removes a listener for the given event.
* @param {string} event The event name
* @param {Function} listener The listener function
* @returns {this}
*/
removeListener(event, listener) {
if (event === 'change') {
this.#listeners.delete(listener);
}
super.removeListener(event, listener);
return this;
}
/**
* Removes all listeners for an event.
* Overrides EventEmitter to also clear internal #listeners tracking.
* @param {string} eventName The event name
* @returns {this}
*/
removeAllListeners(eventName) {
if (eventName === 'change') {
this.#listeners.clear();
}
super.removeAllListeners(eventName);
return this;
}
/**
* Returns true if there are no listeners.
* @returns {boolean}
*/
hasNoListeners() {
return this.#listeners.size === 0;
}
/**
* Stops the watcher.
*/
stop() {
if (this.#closed) return;
this.#closed = true;
if (this.#timer) {
clearInterval(this.#timer);
this.#timer = null;
}
this.emit('stop');
}
/**
* Makes the timer not keep the process alive.
* @returns {this}
*/
unref() {
this.#timer?.unref?.();
return this;
}
/**
* Makes the timer keep the process alive.
* @returns {this}
*/
ref() {
this.#timer?.ref?.();
return this;
}
}
/**
* VFSWatchAsyncIterable - Async iterable wrapper for VFSWatcher.
* Compatible with fs.promises.watch() return value interface.
*/
const kMaxPendingEvents = 1024;
class VFSWatchAsyncIterable {
#watcher;
#closed = false;
#pendingEvents = [];
#pendingResolvers = [];
/**
* @param {VirtualProvider} provider The VFS provider
* @param {string} path The path to watch (provider-relative)
* @param {object} [options] Options
*/
constructor(provider, path, options = {}) {
// Strip signal from options passed to VFSWatcher - we handle abort
// at the iterable level to reject pending next() with AbortError
// instead of resolving with done:true via the 'close' event.
const signal = options.signal;
const watcherOptions = ObjectAssign({ __proto__: null }, options);
delete watcherOptions.signal;
this.#watcher = new VFSWatcher(provider, path, watcherOptions);
this.#watcher.on('change', (eventType, filename) => {
const event = { eventType, filename };
if (this.#pendingResolvers.length > 0) {
const { resolve } = this.#pendingResolvers.shift();
resolve({ value: event, done: false });
} else if (this.#pendingEvents.length < kMaxPendingEvents) {
ArrayPrototypePush(this.#pendingEvents, event);
}
// Drop events when queue is full to prevent unbounded memory growth
});
this.#watcher.on('close', () => {
this.#closed = true;
// Resolve any pending iterators
while (this.#pendingResolvers.length > 0) {
const { resolve } = this.#pendingResolvers.shift();
resolve({ value: undefined, done: true });
}
});
// Handle abort signal - reject pending next() with AbortError
if (signal) {
const onAbort = () => {
this.#closed = true;
const err = new AbortError(undefined, { cause: signal.reason });
while (this.#pendingResolvers.length > 0) {
const { reject } = this.#pendingResolvers.shift();
reject(err);
}
this.#watcher.close();
};
if (signal.aborted) {
onAbort();
} else {
signal.addEventListener('abort', onAbort, { once: true });
}
}
}
/**
* Returns the async iterator.
* @returns {AsyncIterator}
*/
[SymbolAsyncIterator]() {
return this;
}
/**
* Gets the next event.
* @returns {Promise<IteratorResult>}
*/
next() {
if (this.#closed) {
return PromiseResolve({ value: undefined, done: true });
}
if (this.#pendingEvents.length > 0) {
const event = this.#pendingEvents.shift();
return PromiseResolve({ value: event, done: false });
}
return new Promise((resolve, reject) => {
ArrayPrototypePush(this.#pendingResolvers, { resolve, reject });
});
}
/**
* Closes the iterator and underlying watcher.
* @returns {Promise<IteratorResult>}
*/
return() {
this.#watcher.close();
return PromiseResolve({ value: undefined, done: true });
}
/**
* Handles iterator throw.
* @param {Error} error The error to throw
* @returns {Promise<IteratorResult>}
*/
throw(error) {
this.#watcher.close();
return PromiseResolve({ value: undefined, done: true });
}
}
module.exports = {
VFSWatcher,
VFSStatWatcher,
VFSWatchAsyncIterable,
};
+37
View File
@@ -0,0 +1,37 @@
'use strict';
const {
FunctionPrototypeSymbolHasInstance,
} = primordials;
const { VirtualFileSystem } = require('internal/vfs/file_system');
const { VirtualProvider } = require('internal/vfs/provider');
const { MemoryProvider } = require('internal/vfs/providers/memory');
const { RealFSProvider } = require('internal/vfs/providers/real');
/**
* Creates a new VirtualFileSystem instance.
* @param {VirtualProvider} [provider] The provider to use (defaults to MemoryProvider)
* @param {object} [options] Configuration options
* @param {boolean} [options.moduleHooks] Whether to enable require/import hooks (default: true)
* @param {boolean} [options.virtualCwd] Whether to enable virtual working directory
* @returns {VirtualFileSystem}
*/
function create(provider, options) {
// Handle case where first arg is options (no provider)
if (provider != null &&
!FunctionPrototypeSymbolHasInstance(VirtualProvider, provider) &&
typeof provider === 'object') {
options = provider;
provider = undefined;
}
return new VirtualFileSystem(provider, options);
}
module.exports = {
create,
VirtualFileSystem,
VirtualProvider,
MemoryProvider,
RealFSProvider,
};
+1
View File
@@ -153,6 +153,7 @@ BuiltinLoader::BuiltinCategories BuiltinLoader::GetBuiltinCategories() const {
"sqlite", // Experimental.
"stream/iter", // Experimental.
"sys", // Deprecated.
"vfs", // Experimental.
"wasi", // Experimental.
"zlib/iter", // Experimental.
#if !HAVE_SQLITE
+4
View File
@@ -619,6 +619,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
NoOp{},
#endif
kAllowedInEnvvar);
AddOption("--experimental-vfs",
"experimental node:vfs module",
&EnvironmentOptions::experimental_vfs,
kAllowedInEnvvar);
AddOption("--experimental-quic",
#ifndef OPENSSL_NO_QUIC
"experimental QUIC support",
+1
View File
@@ -132,6 +132,7 @@ class EnvironmentOptions : public Options {
bool experimental_websocket = true;
bool experimental_sqlite = HAVE_SQLITE;
bool experimental_stream_iter = EXPERIMENTALS_DEFAULT_VALUE;
bool experimental_vfs = EXPERIMENTALS_DEFAULT_VALUE;
bool webstorage = HAVE_SQLITE;
bool experimental_dtls = EXPERIMENTALS_DEFAULT_VALUE;
bool experimental_quic = EXPERIMENTALS_DEFAULT_VALUE;
@@ -40,6 +40,8 @@ if (!hasIntl) {
publicBuiltins.delete('node:dtls');
// TODO(@jasnell): Remove this once node:quic graduates from unflagged.
publicBuiltins.delete('node:quic');
// Remove this once node:vfs graduates from unflagged.
publicBuiltins.delete('node:vfs');
if (!hasInspector) {
publicBuiltins.delete('inspector');
+2
View File
@@ -64,6 +64,8 @@ require(fixtures.path('resolve-paths', 'default', 'verify-paths.js'));
if (mod === 'node:quic') return;
// TODO: Remove once node:ffi is no longer flagged
if (mod === 'node:ffi') return;
// Remove once node:vfs is no longer flagged
if (mod === 'node:vfs') return;
if (mod === 'node:sqlite' && !common.hasSQLite) return;
assert.strictEqual(require.resolve.paths(mod), null);
if (!mod.startsWith('node:')) {
+41
View File
@@ -0,0 +1,41 @@
// Flags: --experimental-vfs
'use strict';
// access / accessSync honour the R_OK / W_OK / X_OK / F_OK mode bits and
// throw EACCES when the file's permission bits don't allow the request.
const common = require('../common');
const assert = require('assert');
const fs = require('fs');
const vfs = require('node:vfs');
const { R_OK, W_OK, X_OK } = fs.constants;
const myVfs = vfs.create();
// No read permission (mode 0o222 → owner has W only)
myVfs.writeFileSync('/no-r.txt', 'x');
myVfs.chmodSync('/no-r.txt', 0o222);
assert.throws(() => myVfs.accessSync('/no-r.txt', R_OK), { code: 'EACCES' });
assert.rejects(myVfs.promises.access('/no-r.txt', R_OK),
{ code: 'EACCES' }).then(common.mustCall());
// No write permission (mode 0o444 → owner has R only)
myVfs.writeFileSync('/no-w.txt', 'x');
myVfs.chmodSync('/no-w.txt', 0o444);
assert.throws(() => myVfs.accessSync('/no-w.txt', W_OK), { code: 'EACCES' });
assert.rejects(myVfs.promises.access('/no-w.txt', W_OK),
{ code: 'EACCES' }).then(common.mustCall());
// No execute permission (mode 0o644)
myVfs.writeFileSync('/no-x.txt', 'x');
myVfs.chmodSync('/no-x.txt', 0o644);
assert.throws(() => myVfs.accessSync('/no-x.txt', X_OK), { code: 'EACCES' });
assert.rejects(myVfs.promises.access('/no-x.txt', X_OK),
{ code: 'EACCES' }).then(common.mustCall());
// F_OK (mode 0) is an existence-only check and does not require permission
myVfs.accessSync('/no-r.txt', 0);
// Mode passed as null also exits early (existence-only)
myVfs.accessSync('/no-r.txt', null);
+19
View File
@@ -0,0 +1,19 @@
// Flags: --experimental-vfs
'use strict';
// writeSync in append mode must append, not overwrite.
require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
const myVfs = vfs.create();
myVfs.writeFileSync('/append.txt', 'init');
const fd = myVfs.openSync('/append.txt', 'a');
const buf = Buffer.from(' more');
myVfs.writeSync(fd, buf, 0, buf.length);
myVfs.closeSync(fd);
const content = myVfs.readFileSync('/append.txt', 'utf8');
assert.strictEqual(content, 'init more');
+18
View File
@@ -0,0 +1,18 @@
// Flags: --experimental-vfs
'use strict';
// VFS readSync should accept a BigInt position parameter.
require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'abcde');
const fd = myVfs.openSync('/file.txt', 'r');
const buf = Buffer.alloc(1);
const bytesRead = myVfs.readSync(fd, buf, 0, 1, 1n);
assert.strictEqual(bytesRead, 1);
assert.strictEqual(buf.toString(), 'b');
myVfs.closeSync(fd);
+154
View File
@@ -0,0 +1,154 @@
// Flags: --experimental-vfs
'use strict';
// Exercise the VFS callback-style async API on every method.
const common = require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
const myVfs = vfs.create();
myVfs.mkdirSync('/dir/sub', { recursive: true });
myVfs.writeFileSync('/dir/file.txt', 'hello');
myVfs.writeFileSync('/dir/other.txt', 'other');
// readFile (no options)
myVfs.readFile('/dir/file.txt', common.mustSucceed((data) => {
assert.ok(Buffer.isBuffer(data));
}));
// writeFile + appendFile (no options) -> readFile
myVfs.writeFile('/cb-write.txt', 'a', common.mustSucceed(() => {
myVfs.readFile('/cb-write.txt', 'utf8', common.mustSucceed((data) => {
assert.strictEqual(data, 'a');
}));
}));
// stat / lstat (with and without options)
myVfs.stat('/dir/file.txt', common.mustSucceed((st) => {
assert.strictEqual(st.size, 5);
}));
myVfs.stat('/dir/file.txt', { bigint: true }, common.mustSucceed((st) => {
assert.strictEqual(typeof st.size, 'bigint');
}));
myVfs.lstat('/dir/file.txt', common.mustSucceed((st) => {
assert.ok(st.isFile());
}));
// readdir
myVfs.readdir('/dir', common.mustSucceed((names) => {
assert.ok(names.includes('file.txt'));
}));
// realpath
myVfs.realpath('/dir/file.txt', common.mustSucceed((p) => {
assert.strictEqual(p, '/dir/file.txt');
}));
// access (with and without mode)
myVfs.access('/dir/file.txt', common.mustSucceed());
myVfs.access('/dir/file.txt', 0, common.mustSucceed());
myVfs.access('/missing.txt', common.mustCall((err) => {
assert.strictEqual(err.code, 'ENOENT');
}));
// open / read / write / close cb chain
myVfs.open('/dir/file.txt', common.mustSucceed((fd) => {
const buf = Buffer.alloc(5);
myVfs.read(fd, buf, 0, 5, 0, common.mustSucceed((bytesRead) => {
assert.strictEqual(bytesRead, 5);
assert.strictEqual(buf.toString(), 'hello');
myVfs.close(fd, common.mustSucceed());
}));
}));
// Open with explicit flags / mode
myVfs.open('/dir/new1.txt', 'w', common.mustSucceed((fd) => {
const buf = Buffer.from('xyz');
myVfs.write(fd, buf, 0, 3, 0, common.mustSucceed((bytesWritten) => {
assert.strictEqual(bytesWritten, 3);
myVfs.fstat(fd, common.mustSucceed((st) => {
assert.strictEqual(st.size, 3);
myVfs.ftruncate(fd, 1, common.mustSucceed(() => {
myVfs.close(fd, common.mustCall());
}));
}));
}));
}));
// Open with explicit flags, no mode arg form
myVfs.open('/dir/new2.txt', 'w', 0o644, common.mustSucceed((fd) => {
myVfs.close(fd, common.mustCall());
}));
// rm callback (file)
myVfs.writeFileSync('/cb-rm.txt', 'x');
myVfs.rm('/cb-rm.txt', common.mustSucceed(() => {
assert.strictEqual(myVfs.existsSync('/cb-rm.txt'), false);
}));
// Rm callback with options (recursive)
myVfs.mkdirSync('/cb-rm-dir/sub', { recursive: true });
myVfs.writeFileSync('/cb-rm-dir/sub/f.txt', 'x');
myVfs.rm('/cb-rm-dir', { recursive: true }, common.mustSucceed());
// Rm callback failure path
myVfs.rm('/missing-rm', common.mustCall((err) => {
assert.strictEqual(err.code, 'ENOENT');
}));
// truncate / ftruncate cb
myVfs.writeFileSync('/cb-tr.txt', 'abcdef');
myVfs.truncate('/cb-tr.txt', 3, common.mustSucceed(() => {
assert.strictEqual(myVfs.readFileSync('/cb-tr.txt', 'utf8'), 'abc');
}));
myVfs.truncate('/missing-tr.txt', common.mustCall((err) => {
assert.ok(err);
}));
myVfs.ftruncate(0xFFFFFFF /* invalid fd */, common.mustCall((err) => {
assert.strictEqual(err.code, 'EBADF');
}));
// link cb
myVfs.writeFileSync('/cb-link-src.txt', 'x');
myVfs.link('/cb-link-src.txt', '/cb-link-dst.txt', common.mustSucceed());
myVfs.link('/missing-src.txt', '/cb-bad-link.txt', common.mustCall((err) => {
assert.ok(err);
}));
// mkdtemp cb
myVfs.mkdtemp('/tmp-', common.mustSucceed((p) => {
assert.ok(p.startsWith('/tmp-'));
}));
myVfs.mkdtemp('/tmp-', {}, common.mustSucceed((p) => {
assert.ok(p.startsWith('/tmp-'));
}));
// opendir cb
myVfs.opendir('/dir', common.mustSucceed((dir) => {
assert.strictEqual(dir.path, '/dir');
dir.closeSync();
}));
myVfs.opendir('/missing-dir', common.mustCall((err) => {
assert.ok(err);
}));
// EBADF callback paths
myVfs.read(0xFFFFFFF, Buffer.alloc(1), 0, 1, 0, common.mustCall((err) => {
assert.strictEqual(err.code, 'EBADF');
}));
myVfs.write(0xFFFFFFF, Buffer.alloc(1), 0, 1, 0, common.mustCall((err) => {
assert.strictEqual(err.code, 'EBADF');
}));
myVfs.close(0xFFFFFFF, common.mustCall((err) => {
assert.strictEqual(err.code, 'EBADF');
}));
myVfs.fstat(0xFFFFFFF, common.mustCall((err) => {
assert.strictEqual(err.code, 'EBADF');
}));
// readlink cb
myVfs.symlinkSync('/dir/file.txt', '/cb-link');
myVfs.readlink('/cb-link', common.mustSucceed((target) => {
assert.strictEqual(target, '/dir/file.txt');
}));
+52
View File
@@ -0,0 +1,52 @@
// Flags: --experimental-vfs
'use strict';
// Tests for VFS copyFile mode support:
// - COPYFILE_EXCL throws when destination exists
// - Without COPYFILE_EXCL, copy overwrites destination
const common = require('../common');
const assert = require('assert');
const fs = require('fs');
const vfs = require('node:vfs');
const { COPYFILE_EXCL } = fs.constants;
// copyFileSync with COPYFILE_EXCL throws when destination exists.
{
const myVfs = vfs.create();
myVfs.writeFileSync('/src.txt', 'src');
myVfs.writeFileSync('/dst.txt', 'dst');
assert.throws(
() => myVfs.copyFileSync('/src.txt', '/dst.txt', COPYFILE_EXCL),
{ code: 'EEXIST' },
);
assert.strictEqual(myVfs.readFileSync('/dst.txt', 'utf8'), 'dst');
}
// copyFileSync without COPYFILE_EXCL succeeds and overwrites.
{
const myVfs = vfs.create();
myVfs.writeFileSync('/src.txt', 'new-content');
myVfs.writeFileSync('/dst.txt', 'old');
myVfs.copyFileSync('/src.txt', '/dst.txt');
assert.strictEqual(myVfs.readFileSync('/dst.txt', 'utf8'), 'new-content');
}
// promises.copyFile with COPYFILE_EXCL
(async () => {
const myVfs = vfs.create();
myVfs.writeFileSync('/src.txt', 's');
myVfs.writeFileSync('/dst.txt', 'd');
await assert.rejects(
myVfs.promises.copyFile('/src.txt', '/dst.txt', COPYFILE_EXCL),
{ code: 'EEXIST' },
);
await myVfs.promises.copyFile('/src.txt', '/dst.txt');
assert.strictEqual(myVfs.readFileSync('/dst.txt', 'utf8'), 's');
})().then(common.mustCall());
+65
View File
@@ -0,0 +1,65 @@
// Flags: --experimental-vfs
'use strict';
// Constructor variants and option validation for vfs.create() and
// `new VirtualFileSystem(...)`.
require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
// vfs.create() with no arguments uses the default MemoryProvider
{
const myVfs = vfs.create();
assert.ok(myVfs.provider instanceof vfs.MemoryProvider);
}
// vfs.create with first arg as options object (no provider)
{
const myVfs = vfs.create({ emitExperimentalWarning: false });
assert.ok(myVfs);
assert.ok(myVfs.provider instanceof vfs.MemoryProvider);
}
// vfs.create with explicit provider
{
const provider = new vfs.MemoryProvider();
const myVfs = vfs.create(provider);
assert.strictEqual(myVfs.provider, provider);
}
// new VirtualFileSystem(options) directly
{
const myVfs = new vfs.VirtualFileSystem({ emitExperimentalWarning: false });
assert.ok(myVfs);
}
// emitExperimentalWarning option must be a boolean
{
assert.throws(
() => new vfs.VirtualFileSystem({ emitExperimentalWarning: 'not-bool' }),
{ code: 'ERR_INVALID_ARG_TYPE' });
}
// existsSync swallows ALL errors from the provider, not just ENOENT
{
class ThrowingProvider extends vfs.VirtualProvider {
existsSync() { throw new Error('boom'); }
}
const myVfs = vfs.create(new ThrowingProvider());
assert.strictEqual(myVfs.existsSync('/anything'), false);
}
// Walking a path through a regular-file parent throws ENOTDIR
{
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'x');
assert.throws(() => myVfs.writeFileSync('/file.txt/oops', 'y'),
{ code: 'ENOTDIR' });
}
// statSync('/') returns the root directory
{
const myVfs = vfs.create();
assert.ok(myVfs.statSync('/').isDirectory());
}
+49
View File
@@ -0,0 +1,49 @@
// Flags: --experimental-vfs
'use strict';
require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
// Test that writeFileSync updates both mtime and ctime
{
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'initial');
const stat1 = myVfs.statSync('/file.txt');
const oldCtime = stat1.ctimeMs;
myVfs.writeFileSync('/file.txt', 'updated');
const stat2 = myVfs.statSync('/file.txt');
assert.ok(stat2.mtimeMs >= oldCtime);
assert.ok(stat2.ctimeMs >= oldCtime);
// Ctime and mtime should be the same value (both set from same write)
assert.strictEqual(stat2.ctimeMs, stat2.mtimeMs);
}
// Test that writeSync via file handle updates ctime
{
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'initial');
const fd = myVfs.openSync('/file.txt', 'r+');
const buf = Buffer.from('X');
myVfs.writeSync(fd, buf, 0, 1, 0);
myVfs.closeSync(fd);
const stat = myVfs.statSync('/file.txt');
assert.strictEqual(stat.ctimeMs, stat.mtimeMs);
}
// Test that truncateSync updates ctime
{
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'some content');
const fd = myVfs.openSync('/file.txt', 'r+');
myVfs.ftruncateSync(fd, 0);
myVfs.closeSync(fd);
const stat = myVfs.statSync('/file.txt');
assert.strictEqual(stat.ctimeMs, stat.mtimeMs);
}
+114
View File
@@ -0,0 +1,114 @@
// Flags: --experimental-vfs
'use strict';
// Exercise the VirtualDir handle returned by opendirSync.
const common = require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
const myVfs = vfs.create();
myVfs.mkdirSync('/d');
myVfs.writeFileSync('/d/a.txt', 'a');
myVfs.writeFileSync('/d/b.txt', 'b');
myVfs.mkdirSync('/d/sub');
// readSync iteration
{
const dir = myVfs.opendirSync('/d');
assert.strictEqual(dir.path, '/d');
const names = [];
let entry;
while ((entry = dir.readSync()) !== null) {
names.push(entry.name);
}
assert.deepStrictEqual(names.sort(), ['a.txt', 'b.txt', 'sub']);
dir.closeSync();
// Closing again must throw
assert.throws(() => dir.closeSync(), { code: 'ERR_DIR_CLOSED' });
// Reading after close throws
assert.throws(() => dir.readSync(), { code: 'ERR_DIR_CLOSED' });
}
// for-await iteration
(async () => {
const dir = myVfs.opendirSync('/d');
const names = [];
for await (const entry of dir) {
names.push(entry.name);
}
assert.deepStrictEqual(names.sort(), ['a.txt', 'b.txt', 'sub']);
})().then(common.mustCall());
// Async read with callback
(async () => {
const dir = myVfs.opendirSync('/d');
await new Promise((resolve, reject) => {
dir.read((err, entry) => {
if (err) reject(err);
else resolve(entry);
});
});
await new Promise((resolve, reject) => {
dir.close((err) => {
if (err) reject(err);
else resolve();
});
});
})().then(common.mustCall());
// Async read without callback returns a promise
(async () => {
const dir = myVfs.opendirSync('/d');
const entry = await dir.read();
assert.ok(entry);
await dir.close();
})().then(common.mustCall());
// using/explicit resource management
{
const dir = myVfs.opendirSync('/d');
dir[Symbol.dispose]();
assert.throws(() => dir.readSync(), { code: 'ERR_DIR_CLOSED' });
}
// opendir (callback)
myVfs.opendir('/d', common.mustSucceed((dir) => {
assert.strictEqual(dir.path, '/d');
dir.closeSync();
}));
// read() callback on a closed dir delivers ERR_DIR_CLOSED
{
const dir = myVfs.opendirSync('/d');
dir.closeSync();
dir.read(common.mustCall((err) => {
assert.strictEqual(err.code, 'ERR_DIR_CLOSED');
}));
// entries() iteration on a closed dir rejects with ERR_DIR_CLOSED
(async () => {
await assert.rejects(
// eslint-disable-next-line no-unused-vars
(async () => { for await (const _ of dir.entries()); })(),
{ code: 'ERR_DIR_CLOSED' });
})().then(common.mustCall());
// [Symbol.dispose] is a no-op on an already-closed dir (must not throw)
dir[Symbol.dispose]();
}
// Async dir.close() returns a promise when invoked without a callback
(async () => {
const dir = myVfs.opendirSync('/d');
await dir.close();
})().then(common.mustCall());
// opendirSync without options object
{
const dir = myVfs.opendirSync('/d');
dir.closeSync();
}
// Opendir error path (missing directory)
myVfs.opendir('/missing-dir', common.mustCall((err) => {
assert.ok(err);
}));
+319
View File
@@ -0,0 +1,319 @@
// Flags: --experimental-vfs
'use strict';
const common = require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
// Test openSync and closeSync
{
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'hello world');
const fd = myVfs.openSync('/file.txt');
assert.ok((fd & 0x40000000) !== 0, 'VFS fd should have bit 30 set');
myVfs.closeSync(fd);
}
// Test openSync with non-existent file
{
const myVfs = vfs.create();
assert.throws(() => {
myVfs.openSync('/nonexistent.txt');
}, { code: 'ENOENT' });
}
// Test openSync with directory
{
const myVfs = vfs.create();
myVfs.mkdirSync('/mydir', { recursive: true });
assert.throws(() => {
myVfs.openSync('/mydir');
}, { code: 'EISDIR' });
}
// Test closeSync with invalid fd
{
const myVfs = vfs.create();
assert.throws(() => {
myVfs.closeSync(12345);
}, { code: 'EBADF' });
}
// Test readSync
{
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'hello world');
const fd = myVfs.openSync('/file.txt');
const buffer = Buffer.alloc(5);
const bytesRead = myVfs.readSync(fd, buffer, 0, 5, 0);
assert.strictEqual(bytesRead, 5);
assert.strictEqual(buffer.toString(), 'hello');
myVfs.closeSync(fd);
}
// Test readSync with position tracking
{
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'hello world');
const fd = myVfs.openSync('/file.txt');
const buffer1 = Buffer.alloc(5);
const buffer2 = Buffer.alloc(6);
// Read first 5 bytes
let bytesRead = myVfs.readSync(fd, buffer1, 0, 5, null);
assert.strictEqual(bytesRead, 5);
assert.strictEqual(buffer1.toString(), 'hello');
// Continue reading (position should advance)
bytesRead = myVfs.readSync(fd, buffer2, 0, 6, null);
assert.strictEqual(bytesRead, 6);
assert.strictEqual(buffer2.toString(), ' world');
myVfs.closeSync(fd);
}
// Test readSync with explicit position
{
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'hello world');
const fd = myVfs.openSync('/file.txt');
const buffer = Buffer.alloc(5);
// Read from position 6 (start of "world")
const bytesRead = myVfs.readSync(fd, buffer, 0, 5, 6);
assert.strictEqual(bytesRead, 5);
assert.strictEqual(buffer.toString(), 'world');
myVfs.closeSync(fd);
}
// Test readSync at end of file
{
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'short');
const fd = myVfs.openSync('/file.txt');
const buffer = Buffer.alloc(10);
// Read from position beyond file
const bytesRead = myVfs.readSync(fd, buffer, 0, 10, 100);
assert.strictEqual(bytesRead, 0);
myVfs.closeSync(fd);
}
// Test readSync with invalid fd
{
const myVfs = vfs.create();
const buffer = Buffer.alloc(10);
assert.throws(() => {
myVfs.readSync(99999, buffer, 0, 10, 0);
}, { code: 'EBADF' });
}
// Test fstatSync
{
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'hello world');
const fd = myVfs.openSync('/file.txt');
const stats = myVfs.fstatSync(fd);
assert.strictEqual(stats.isFile(), true);
assert.strictEqual(stats.isDirectory(), false);
assert.strictEqual(stats.size, 11);
myVfs.closeSync(fd);
}
// Test fstatSync with invalid fd
{
const myVfs = vfs.create();
assert.throws(() => {
myVfs.fstatSync(99999);
}, { code: 'EBADF' });
}
// Test async open and close
{
const myVfs = vfs.create();
myVfs.writeFileSync('/async-file.txt', 'async content');
myVfs.open('/async-file.txt', common.mustCall((err, fd) => {
assert.strictEqual(err, null);
assert.ok((fd & 0x40000000) !== 0);
myVfs.close(fd, common.mustCall((err) => {
assert.strictEqual(err, null);
}));
}));
}
// Test async open with error
{
const myVfs = vfs.create();
myVfs.open('/nonexistent.txt', common.mustCall((err, fd) => {
assert.strictEqual(err.code, 'ENOENT');
assert.strictEqual(fd, undefined);
}));
}
// Test async close with invalid fd
{
const myVfs = vfs.create();
myVfs.close(99999, common.mustCall((err) => {
assert.strictEqual(err.code, 'EBADF');
}));
}
// Test async read
{
const myVfs = vfs.create();
myVfs.writeFileSync('/read-test.txt', 'read content');
myVfs.open('/read-test.txt', common.mustCall((err, fd) => {
assert.strictEqual(err, null);
const buffer = Buffer.alloc(4);
myVfs.read(fd, buffer, 0, 4, 0, common.mustCall((err, bytesRead, buf) => {
assert.strictEqual(err, null);
assert.strictEqual(bytesRead, 4);
assert.strictEqual(buf, buffer);
assert.strictEqual(buffer.toString(), 'read');
myVfs.close(fd, common.mustCall());
}));
}));
}
// Test async read with position tracking
{
const myVfs = vfs.create();
myVfs.writeFileSync('/track-test.txt', 'ABCDEFGHIJ');
myVfs.open('/track-test.txt', common.mustCall((err, fd) => {
assert.strictEqual(err, null);
const buffer1 = Buffer.alloc(3);
const buffer2 = Buffer.alloc(3);
myVfs.read(fd, buffer1, 0, 3, null, common.mustCall((err, bytesRead, buf) => {
assert.strictEqual(err, null);
assert.strictEqual(bytesRead, 3);
assert.strictEqual(buffer1.toString(), 'ABC');
// Continue reading without explicit position
myVfs.read(fd, buffer2, 0, 3, null, common.mustCall((err, bytesRead, buf) => {
assert.strictEqual(err, null);
assert.strictEqual(bytesRead, 3);
assert.strictEqual(buffer2.toString(), 'DEF');
myVfs.close(fd, common.mustCall());
}));
}));
}));
}
// Test async read with invalid fd
{
const myVfs = vfs.create();
const buffer = Buffer.alloc(10);
myVfs.read(99999, buffer, 0, 10, 0, common.mustCall((err, bytesRead, buf) => {
assert.strictEqual(err.code, 'EBADF');
}));
}
// Test async fstat
{
const myVfs = vfs.create();
myVfs.writeFileSync('/fstat-test.txt', '12345');
myVfs.open('/fstat-test.txt', common.mustCall((err, fd) => {
assert.strictEqual(err, null);
myVfs.fstat(fd, common.mustCall((err, stats) => {
assert.strictEqual(err, null);
assert.strictEqual(stats.isFile(), true);
assert.strictEqual(stats.size, 5);
myVfs.close(fd, common.mustCall());
}));
}));
}
// Test async fstat with invalid fd
{
const myVfs = vfs.create();
myVfs.fstat(99999, common.mustCall((err, stats) => {
assert.strictEqual(err.code, 'EBADF');
}));
}
// Test that separate VFS instances have separate fd spaces
{
const vfs1 = vfs.create();
const vfs2 = vfs.create();
vfs1.writeFileSync('/file1.txt', 'content1');
vfs2.writeFileSync('/file2.txt', 'content2');
const fd1 = vfs1.openSync('/file1.txt');
const fd2 = vfs2.openSync('/file2.txt');
// Both should get valid fds
assert.ok((fd1 & 0x40000000) !== 0);
assert.ok((fd2 & 0x40000000) !== 0);
// Read from fd1 using vfs1
const buf1 = Buffer.alloc(8);
const read1 = vfs1.readSync(fd1, buf1, 0, 8, 0);
assert.strictEqual(read1, 8);
assert.strictEqual(buf1.toString(), 'content1');
// Read from fd2 using vfs2
const buf2 = Buffer.alloc(8);
const read2 = vfs2.readSync(fd2, buf2, 0, 8, 0);
assert.strictEqual(read2, 8);
assert.strictEqual(buf2.toString(), 'content2');
vfs1.closeSync(fd1);
vfs2.closeSync(fd2);
}
// Test multiple opens of same file
{
const myVfs = vfs.create();
myVfs.writeFileSync('/multi.txt', 'multi content');
const fd1 = myVfs.openSync('/multi.txt');
const fd2 = myVfs.openSync('/multi.txt');
assert.notStrictEqual(fd1, fd2);
const buf1 = Buffer.alloc(5);
const buf2 = Buffer.alloc(5);
myVfs.readSync(fd1, buf1, 0, 5, 0);
myVfs.readSync(fd2, buf2, 0, 5, 0);
assert.strictEqual(buf1.toString(), 'multi');
assert.strictEqual(buf2.toString(), 'multi');
myVfs.closeSync(fd1);
myVfs.closeSync(fd2);
}
+205
View File
@@ -0,0 +1,205 @@
// Flags: --experimental-vfs
'use strict';
// Exercise VirtualFileHandle / MemoryFileHandle methods directly via
// the promises.open() handle returned by VFS.
const common = require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'hello world');
(async () => {
// Open file via provider directly (returns a real FileHandle)
const handle = await myVfs.provider.open('/file.txt', 'r');
assert.ok(handle);
assert.strictEqual(handle.path, '/file.txt');
assert.strictEqual(handle.flags, 'r');
assert.strictEqual(typeof handle.mode, 'number');
assert.strictEqual(handle.closed, false);
// readFile
const content = await handle.readFile('utf8');
assert.strictEqual(content, 'hello world');
// stat
const stats = await handle.stat();
assert.strictEqual(stats.size, 11);
// read into buffer
const buf = Buffer.alloc(5);
const { bytesRead } = await handle.read(buf, 0, 5, 0);
assert.strictEqual(bytesRead, 5);
assert.strictEqual(buf.toString(), 'hello');
// readv
const b1 = Buffer.alloc(5);
const b2 = Buffer.alloc(6);
const readvResult = await handle.readv([b1, b2], 0);
assert.strictEqual(readvResult.bytesRead, 11);
assert.strictEqual(b1.toString(), 'hello');
assert.strictEqual(b2.toString(), ' world');
// no-op metadata methods
await handle.chmod();
await handle.chown();
await handle.utimes();
await handle.datasync();
await handle.sync();
await handle.close();
assert.strictEqual(handle.closed, true);
})().then(common.mustCall());
// Write mode: truncate, write, writev, appendFile, truncate
(async () => {
const handle = await myVfs.provider.open('/out.txt', 'w+');
// No explicit position so the handle position advances naturally
await handle.write(Buffer.from('hello'), 0, 5);
await handle.writev([Buffer.from(' '), Buffer.from('world')]);
const stats = await handle.stat();
assert.strictEqual(stats.size, 11);
await handle.appendFile('!');
const content = await handle.readFile('utf8');
assert.strictEqual(content, 'hello world!');
await handle.truncate(5);
const truncated = await handle.readFile('utf8');
assert.strictEqual(truncated, 'hello');
await handle.close();
})().then(common.mustCall());
// readSync / writeSync / readFileSync / writeFileSync / statSync / truncateSync / closeSync
{
const fd = myVfs.openSync('/sync.txt', 'w');
const buf = Buffer.from('abc');
myVfs.writeSync(fd, buf, 0, 3, 0);
myVfs.closeSync(fd);
const fd2 = myVfs.openSync('/sync.txt', 'r');
const out = Buffer.alloc(3);
myVfs.readSync(fd2, out, 0, 3, 0);
assert.strictEqual(out.toString(), 'abc');
const stats = myVfs.fstatSync(fd2);
assert.strictEqual(stats.size, 3);
myVfs.closeSync(fd2);
}
// using-style explicit resource management for handles
(async () => {
const handle = await myVfs.provider.open('/file.txt', 'r');
await handle[Symbol.asyncDispose]();
assert.strictEqual(handle.closed, true);
})().then(common.mustCall());
// readableWebStream / readLines / createReadStream / createWriteStream throw
(async () => {
const handle = await myVfs.provider.open('/file.txt', 'r');
assert.throws(() => handle.readableWebStream(),
{ code: 'ERR_METHOD_NOT_IMPLEMENTED' });
assert.throws(() => handle.readLines(),
{ code: 'ERR_METHOD_NOT_IMPLEMENTED' });
assert.throws(() => handle.createReadStream(),
{ code: 'ERR_METHOD_NOT_IMPLEMENTED' });
assert.throws(() => handle.createWriteStream(),
{ code: 'ERR_METHOD_NOT_IMPLEMENTED' });
await handle.close();
})().then(common.mustCall());
// Operations after close throw EBADF
(async () => {
const handle = await myVfs.provider.open('/file.txt', 'r');
await handle.close();
await assert.rejects(handle.read(Buffer.alloc(1), 0, 1, 0),
{ code: 'EBADF' });
assert.throws(() => handle.readSync(Buffer.alloc(1), 0, 1, 0),
{ code: 'EBADF' });
await assert.rejects(handle.stat(), { code: 'EBADF' });
})().then(common.mustCall());
// Readv with a partial read at EOF (second buffer larger than remaining)
(async () => {
const handle = await myVfs.provider.open('/file.txt', 'r');
const b1 = Buffer.alloc(5);
const b2 = Buffer.alloc(20);
const r = await handle.readv([b1, b2], 0);
assert.strictEqual(b1.toString(), 'hello');
assert.strictEqual(r.bytesRead, 11);
await handle.close();
})().then(common.mustCall());
// Writev with explicit position 0
(async () => {
const wh = await myVfs.provider.open('/wv.txt', 'w+');
await wh.writev([Buffer.from('AB'), Buffer.from('CD')], 0);
await wh.close();
assert.strictEqual(myVfs.readFileSync('/wv.txt', 'utf8'), 'ABCD');
})().then(common.mustCall());
// appendFile with string + encoding option
(async () => {
const ah = await myVfs.provider.open('/ap.txt', 'a+');
await ah.appendFile('hello', { encoding: 'utf8' });
await ah.close();
assert.strictEqual(myVfs.readFileSync('/ap.txt', 'utf8'), 'hello');
})().then(common.mustCall());
// 'w'-mode handle rejects all read ops with EBADF
(async () => {
const handle = await myVfs.provider.open('/wonly.txt', 'w');
assert.throws(() => handle.readSync(Buffer.alloc(1), 0, 1, 0),
{ code: 'EBADF' });
await assert.rejects(handle.read(Buffer.alloc(1), 0, 1, 0),
{ code: 'EBADF' });
assert.throws(() => handle.readFileSync(), { code: 'EBADF' });
await assert.rejects(handle.readFile(), { code: 'EBADF' });
await handle.close();
})().then(common.mustCall());
// 'r'-mode handle rejects all write ops with EBADF
(async () => {
myVfs.writeFileSync('/ronly.txt', 'x');
const handle = await myVfs.provider.open('/ronly.txt', 'r');
assert.throws(() => handle.writeSync(Buffer.from('y'), 0, 1, 0),
{ code: 'EBADF' });
await assert.rejects(handle.write(Buffer.from('y'), 0, 1, 0),
{ code: 'EBADF' });
assert.throws(() => handle.writeFileSync('y'), { code: 'EBADF' });
await assert.rejects(handle.writeFile('y'), { code: 'EBADF' });
assert.throws(() => handle.truncateSync(0), { code: 'EBADF' });
await assert.rejects(handle.truncate(0), { code: 'EBADF' });
await handle.close();
})().then(common.mustCall());
// writeFile with string + encoding
(async () => {
const handle = await myVfs.provider.open('/se.txt', 'w+');
await handle.writeFile('héllo', { encoding: 'utf8' });
assert.strictEqual(await handle.readFile('utf8'), 'héllo');
await handle.close();
})().then(common.mustCall());
// Truncate extending past current size zero-fills
(async () => {
const handle = await myVfs.provider.open('/grow.txt', 'w+');
await handle.writeFile('abc');
await handle.truncate(10);
assert.strictEqual((await handle.stat()).size, 10);
assert.strictEqual((await handle.readFile()).length, 10);
await handle.close();
})().then(common.mustCall());
// readv / writev / appendFile on a closed handle reject with EBADF
(async () => {
const handle = await myVfs.provider.open('/file.txt', 'r');
await handle.close();
await assert.rejects(handle.readv([Buffer.alloc(1)], 0), { code: 'EBADF' });
await assert.rejects(handle.writev([Buffer.alloc(1)], 0), { code: 'EBADF' });
await assert.rejects(handle.appendFile('x'), { code: 'EBADF' });
})().then(common.mustCall());
+59
View File
@@ -0,0 +1,59 @@
'use strict';
// node:vfs is gated behind --experimental-vfs. Without the flag the
// module is not exposed; bare `vfs` (without the node: scheme) is also
// blocked.
require('../common');
const { spawnSyncAndAssert } = require('../common/child_process');
// Without the flag, requiring node:vfs throws ERR_UNKNOWN_BUILTIN_MODULE.
{
spawnSyncAndAssert(process.execPath, [
'-e', 'require("node:vfs")',
], { status: 1, stderr: /ERR_UNKNOWN_BUILTIN_MODULE/ });
}
// Without the flag, importing node:vfs throws ERR_UNKNOWN_BUILTIN_MODULE.
{
spawnSyncAndAssert(process.execPath, [
'--input-type=module',
'-e', 'import("node:vfs").catch((e) => { console.error(e.code); process.exit(1); });',
], {
status: 1,
stderr: /ERR_UNKNOWN_BUILTIN_MODULE/,
});
}
// With the flag, node:vfs loads and works.
{
const script =
'const v = require("node:vfs");' +
'const x = v.create();' +
'x.writeFileSync("/x", "hi");' +
'console.log(x.readFileSync("/x", "utf8"));';
spawnSyncAndAssert(process.execPath, ['--experimental-vfs', '-e', script], {
stdout: 'hi',
trim: true,
});
}
// Bare `vfs` (no node: scheme) is always blocked.
{
spawnSyncAndAssert(process.execPath, [
'--experimental-vfs',
'-e', "require('vfs')",
], { status: 1, stderr: /Cannot find module 'vfs'/ });
}
// Module.builtinModules reflects whether --experimental-vfs is active.
for (const [flag, expected] of [
['--experimental-vfs', 'true\n'],
['--no-experimental-vfs', 'false\n'],
]) {
spawnSyncAndAssert(process.execPath, [
flag,
'-p',
'require("node:module").builtinModules.includes("node:vfs")',
], { stdout: expected, stderr: '' });
}
+32
View File
@@ -0,0 +1,32 @@
// Flags: --experimental-vfs
'use strict';
// Test that nlink count is updated correctly when creating/removing hard links.
const common = require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
const myVfs = vfs.create();
myVfs.writeFileSync('/src.txt', 'content');
// Initially nlink should be 1
assert.strictEqual(myVfs.statSync('/src.txt').nlink, 1);
// After hard link, nlink should be 2 on both
myVfs.linkSync('/src.txt', '/link.txt');
assert.strictEqual(myVfs.statSync('/src.txt').nlink, 2);
assert.strictEqual(myVfs.statSync('/link.txt').nlink, 2);
// Removing one decrements nlink on the other
myVfs.unlinkSync('/link.txt');
assert.strictEqual(myVfs.statSync('/src.txt').nlink, 1);
// promises.link equivalent
(async () => {
const v = vfs.create();
v.writeFileSync('/a', 'x');
await v.promises.link('/a', '/b');
assert.strictEqual(v.statSync('/a').nlink, 2);
assert.strictEqual(v.statSync('/b').nlink, 2);
})().then(common.mustCall());
+24
View File
@@ -0,0 +1,24 @@
// Flags: --experimental-vfs
'use strict';
// Hard-link error cases: creating a link to a directory or to an
// already-existing path.
require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
// Linking to a directory throws EINVAL
{
const myVfs = vfs.create();
myVfs.mkdirSync('/d');
assert.throws(() => myVfs.linkSync('/d', '/d-link'), { code: 'EINVAL' });
}
// Linking to an existing target throws EEXIST
{
const myVfs = vfs.create();
myVfs.writeFileSync('/a.txt', 'x');
myVfs.writeFileSync('/b.txt', 'y');
assert.throws(() => myVfs.linkSync('/a.txt', '/b.txt'), { code: 'EEXIST' });
}
@@ -0,0 +1,15 @@
// Flags: --experimental-vfs --expose-internals
'use strict';
// MemoryFileHandle internals: the "stats not available" path when there
// is no entry/getStats callback wired up.
require('../common');
const assert = require('assert');
const { MemoryFileHandle } = require('internal/vfs/file_handle');
// MemoryFileHandle without a #getStats callback throws ERR_INVALID_STATE
{
const h = new MemoryFileHandle('/x', 'r', 0o644, Buffer.alloc(0), null, undefined);
assert.throws(() => h.statSync(), { code: 'ERR_INVALID_STATE' });
}
@@ -0,0 +1,127 @@
// Flags: --experimental-vfs --expose-internals
'use strict';
// MemoryProvider supports dynamic content providers and lazily-populated
// directories internally. These features have no public construction API,
// so we drive them directly through MemoryEntry / MemoryProvider.
const common = require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
const { MemoryProvider } = require('internal/vfs/providers/memory');
function getRoot(provider) {
const symbols = Object.getOwnPropertySymbols(provider);
const kRoot = symbols.find((s) => s.description === 'kRoot');
assert.ok(kRoot, 'kRoot symbol expected on MemoryProvider');
return provider[kRoot];
}
function makeFileEntry(prototypeFrom, contentProvider) {
const t = Date.now();
const fileEntry = { __proto__: Object.getPrototypeOf(prototypeFrom) };
Object.assign(fileEntry, {
type: 0, // TYPE_FILE
mode: 0o644,
content: Buffer.alloc(0),
contentProvider,
children: null,
target: null,
populate: null,
populated: true,
nlink: 1,
uid: 0,
gid: 0,
atime: t,
mtime: t,
ctime: t,
birthtime: t,
});
fileEntry.isFile = prototypeFrom.isFile.bind(fileEntry);
fileEntry.isDirectory = prototypeFrom.isDirectory.bind(fileEntry);
fileEntry.isSymbolicLink = prototypeFrom.isSymbolicLink.bind(fileEntry);
fileEntry.isDynamic = prototypeFrom.isDynamic.bind(fileEntry);
fileEntry.getContentSync = prototypeFrom.getContentSync.bind(fileEntry);
fileEntry.getContentAsync = prototypeFrom.getContentAsync.bind(fileEntry);
return fileEntry;
}
// ===== Lazy-populated directory =====
{
const provider = new MemoryProvider();
const root = getRoot(provider);
const dir = {
__proto__: Object.getPrototypeOf(root),
type: 1, // TYPE_DIR
mode: 0o755,
children: new Map(),
populate: (scoped) => {
scoped.addFile('hello.txt', 'lazy hello');
scoped.addFile('dyn.txt', () => 'dynamic-string');
scoped.addDirectory('subdir', null);
scoped.addSymlink('link.txt', '/lazy/hello.txt');
},
populated: false,
nlink: 1,
uid: 0,
gid: 0,
};
const t = Date.now();
dir.atime = t; dir.mtime = t; dir.ctime = t; dir.birthtime = t;
dir.isFile = root.isFile.bind(dir);
dir.isDirectory = root.isDirectory.bind(dir);
dir.isSymbolicLink = root.isSymbolicLink.bind(dir);
dir.isDynamic = root.isDynamic.bind(dir);
dir.getContentSync = root.getContentSync.bind(dir);
dir.getContentAsync = root.getContentAsync.bind(dir);
root.children.set('lazy', dir);
const myVfs = vfs.create(provider);
// Reading the lazy directory triggers populate
const entries = myVfs.readdirSync('/lazy');
assert.deepStrictEqual(entries.sort(),
['dyn.txt', 'hello.txt', 'link.txt', 'subdir']);
// Static file in the lazy directory
assert.strictEqual(myVfs.readFileSync('/lazy/hello.txt', 'utf8'),
'lazy hello');
// Dynamic content provider returning a string (sync read)
assert.strictEqual(myVfs.readFileSync('/lazy/dyn.txt', 'utf8'),
'dynamic-string');
// Dynamic content provider via promises.readFile
myVfs.promises.readFile('/lazy/dyn.txt', 'utf8').then(common.mustCall((s) => {
assert.strictEqual(s, 'dynamic-string');
}));
}
// ===== Dynamic content provider returning a Buffer =====
{
const provider = new MemoryProvider();
const root = getRoot(provider);
root.children.set('buf-dyn.txt',
makeFileEntry(root, () => Buffer.from('buffer-content')));
const myVfs = vfs.create(provider);
assert.strictEqual(myVfs.readFileSync('/buf-dyn.txt', 'utf8'),
'buffer-content');
}
// ===== Async-only content provider: sync API throws ERR_INVALID_STATE =====
{
const provider = new MemoryProvider();
const root = getRoot(provider);
root.children.set('async-only.txt',
makeFileEntry(root, async () => 'async-only'));
const myVfs = vfs.create(provider);
assert.throws(() => myVfs.readFileSync('/async-only.txt'),
{ code: 'ERR_INVALID_STATE' });
myVfs.promises.readFile('/async-only.txt', 'utf8').then(common.mustCall((s) => {
assert.strictEqual(s, 'async-only');
}));
}
@@ -0,0 +1,42 @@
// Flags: --experimental-vfs
'use strict';
// MemoryProvider: numeric open flags (mirroring fs.constants.O_*) must be
// normalised to their string equivalents.
require('../common');
const assert = require('assert');
const fs = require('fs');
const vfs = require('node:vfs');
const { O_RDONLY, O_RDWR, O_WRONLY, O_CREAT, O_TRUNC, O_EXCL, O_APPEND } = fs.constants;
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'orig');
// O_RDONLY (0)
myVfs.closeSync(myVfs.openSync('/file.txt', O_RDONLY));
// O_RDWR ('r+')
myVfs.closeSync(myVfs.openSync('/file.txt', O_RDWR));
// 'w' = O_WRONLY | O_CREAT | O_TRUNC
myVfs.closeSync(myVfs.openSync('/created.txt', O_WRONLY | O_CREAT | O_TRUNC));
// 'wx' = O_WRONLY | O_CREAT | O_EXCL
myVfs.closeSync(myVfs.openSync('/excl.txt', O_WRONLY | O_CREAT | O_EXCL));
// 'wx' on an existing file throws EEXIST
assert.throws(
() => myVfs.openSync('/file.txt', O_WRONLY | O_CREAT | O_EXCL),
{ code: 'EEXIST' });
// 'a' = O_APPEND | O_RDWR | O_CREAT (mapped to 'a+')
myVfs.closeSync(myVfs.openSync('/app.txt', O_APPEND | O_RDWR | O_CREAT));
// 'ax+' = O_APPEND | O_EXCL | O_RDWR | O_CREAT
myVfs.closeSync(myVfs.openSync('/axplus.txt',
O_APPEND | O_EXCL | O_RDWR | O_CREAT));
// Bogus non-string non-number defaults to 'r'
myVfs.closeSync(myVfs.openSync('/file.txt', null));
+664
View File
@@ -0,0 +1,664 @@
// Flags: --experimental-vfs --expose-internals
'use strict';
const common = require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
// Test MemoryProvider can be instantiated directly
{
const provider = new vfs.MemoryProvider();
assert.strictEqual(provider.readonly, false);
assert.strictEqual(provider.supportsSymlinks, true);
}
// Test creating VFS with MemoryProvider (default)
{
const myVfs = vfs.create();
assert.ok(myVfs);
assert.strictEqual(myVfs.readonly, false);
assert.ok(myVfs.provider instanceof vfs.MemoryProvider);
}
// Test creating VFS with explicit MemoryProvider
{
const myVfs = vfs.create(new vfs.MemoryProvider());
assert.ok(myVfs);
assert.strictEqual(myVfs.readonly, false);
}
// Test reading and writing files
{
const myVfs = vfs.create();
// Write a file
myVfs.writeFileSync('/hello.txt', 'Hello from VFS!');
// Read it back
assert.strictEqual(myVfs.readFileSync('/hello.txt', 'utf8'), 'Hello from VFS!');
// Read as Buffer
const buf = myVfs.readFileSync('/hello.txt');
assert.ok(Buffer.isBuffer(buf));
assert.strictEqual(buf.toString(), 'Hello from VFS!');
// Overwrite
myVfs.writeFileSync('/hello.txt', 'Overwritten');
assert.strictEqual(myVfs.readFileSync('/hello.txt', 'utf8'), 'Overwritten');
}
// Test appendFile
{
const myVfs = vfs.create();
myVfs.writeFileSync('/append.txt', 'start');
myVfs.appendFileSync('/append.txt', '-end');
assert.strictEqual(myVfs.readFileSync('/append.txt', 'utf8'), 'start-end');
// Append to non-existent file creates it
myVfs.appendFileSync('/new-append.txt', 'new content');
assert.strictEqual(myVfs.readFileSync('/new-append.txt', 'utf8'), 'new content');
}
// Test stat operations
{
const myVfs = vfs.create();
myVfs.writeFileSync('/stat-test.txt', 'content');
myVfs.mkdirSync('/stat-dir', { recursive: true });
const fileStat = myVfs.statSync('/stat-test.txt');
assert.strictEqual(fileStat.isFile(), true);
assert.strictEqual(fileStat.isDirectory(), false);
assert.strictEqual(fileStat.size, 7);
const dirStat = myVfs.statSync('/stat-dir');
assert.strictEqual(dirStat.isFile(), false);
assert.strictEqual(dirStat.isDirectory(), true);
// Test ENOENT
assert.throws(() => {
myVfs.statSync('/nonexistent');
}, { code: 'ENOENT' });
}
// Test lstatSync (same as statSync for memory provider since no real symlink following)
{
const myVfs = vfs.create();
myVfs.writeFileSync('/lstat.txt', 'lstat test');
const stat = myVfs.lstatSync('/lstat.txt');
assert.strictEqual(stat.isFile(), true);
}
// Test readdirSync
{
const myVfs = vfs.create();
myVfs.mkdirSync('/readdir-test/subdir', { recursive: true });
myVfs.writeFileSync('/readdir-test/a.txt', 'a');
myVfs.writeFileSync('/readdir-test/b.txt', 'b');
const entries = myVfs.readdirSync('/readdir-test');
assert.deepStrictEqual(entries.sort(), ['a.txt', 'b.txt', 'subdir']);
// With file types
const dirents = myVfs.readdirSync('/readdir-test', { withFileTypes: true });
assert.strictEqual(dirents.length, 3);
const fileEntry = dirents.find((d) => d.name === 'a.txt');
assert.ok(fileEntry);
assert.strictEqual(fileEntry.isFile(), true);
const dirEntry = dirents.find((d) => d.name === 'subdir');
assert.ok(dirEntry);
assert.strictEqual(dirEntry.isDirectory(), true);
// ENOENT for non-existent directory
assert.throws(() => {
myVfs.readdirSync('/nonexistent');
}, { code: 'ENOENT' });
// ENOTDIR for file
assert.throws(() => {
myVfs.readdirSync('/readdir-test/a.txt');
}, { code: 'ENOTDIR' });
}
// Test mkdir and rmdir
{
const myVfs = vfs.create();
myVfs.mkdirSync('/new-dir');
assert.strictEqual(myVfs.existsSync('/new-dir'), true);
assert.strictEqual(myVfs.statSync('/new-dir').isDirectory(), true);
myVfs.rmdirSync('/new-dir');
assert.strictEqual(myVfs.existsSync('/new-dir'), false);
// EEXIST for existing directory
myVfs.mkdirSync('/exists');
assert.throws(() => {
myVfs.mkdirSync('/exists');
}, { code: 'EEXIST' });
// ENOTEMPTY for non-empty directory
myVfs.mkdirSync('/nonempty');
myVfs.writeFileSync('/nonempty/file.txt', 'content');
assert.throws(() => {
myVfs.rmdirSync('/nonempty');
}, { code: 'ENOTEMPTY' });
}
// Test recursive mkdir
{
const myVfs = vfs.create();
myVfs.mkdirSync('/deep/nested/dir', { recursive: true });
assert.strictEqual(myVfs.existsSync('/deep/nested/dir'), true);
assert.strictEqual(myVfs.statSync('/deep/nested/dir').isDirectory(), true);
// Recursive on existing is OK
myVfs.mkdirSync('/deep/nested/dir', { recursive: true });
assert.strictEqual(myVfs.existsSync('/deep/nested/dir'), true);
}
// Test unlink
{
const myVfs = vfs.create();
myVfs.writeFileSync('/to-delete.txt', 'delete me');
assert.strictEqual(myVfs.existsSync('/to-delete.txt'), true);
myVfs.unlinkSync('/to-delete.txt');
assert.strictEqual(myVfs.existsSync('/to-delete.txt'), false);
// ENOENT for non-existent file
assert.throws(() => {
myVfs.unlinkSync('/nonexistent.txt');
}, { code: 'ENOENT' });
// EISDIR for directory
myVfs.mkdirSync('/dir-to-unlink');
assert.throws(() => {
myVfs.unlinkSync('/dir-to-unlink');
}, { code: 'EISDIR' });
}
// Test rename
{
const myVfs = vfs.create();
myVfs.writeFileSync('/old-name.txt', 'rename me');
myVfs.renameSync('/old-name.txt', '/new-name.txt');
assert.strictEqual(myVfs.existsSync('/old-name.txt'), false);
assert.strictEqual(myVfs.existsSync('/new-name.txt'), true);
assert.strictEqual(myVfs.readFileSync('/new-name.txt', 'utf8'), 'rename me');
// ENOENT for non-existent source
assert.throws(() => {
myVfs.renameSync('/nonexistent.txt', '/dest.txt');
}, { code: 'ENOENT' });
}
// Test copyFile
{
const myVfs = vfs.create();
myVfs.writeFileSync('/source.txt', 'copy me');
myVfs.copyFileSync('/source.txt', '/dest.txt');
assert.strictEqual(myVfs.existsSync('/source.txt'), true);
assert.strictEqual(myVfs.existsSync('/dest.txt'), true);
assert.strictEqual(myVfs.readFileSync('/dest.txt', 'utf8'), 'copy me');
// ENOENT for non-existent source
assert.throws(() => {
myVfs.copyFileSync('/nonexistent.txt', '/fail.txt');
}, { code: 'ENOENT' });
}
// Test realpathSync
{
const myVfs = vfs.create();
myVfs.mkdirSync('/path/to', { recursive: true });
myVfs.writeFileSync('/path/to/real.txt', 'content');
const resolved = myVfs.realpathSync('/path/to/real.txt');
assert.strictEqual(resolved, '/path/to/real.txt');
// With .. components
const normalized = myVfs.realpathSync('/path/to/../to/real.txt');
assert.strictEqual(normalized, '/path/to/real.txt');
// ENOENT for non-existent
assert.throws(() => {
myVfs.realpathSync('/nonexistent');
}, { code: 'ENOENT' });
}
// Test existsSync
{
const myVfs = vfs.create();
myVfs.writeFileSync('/exists.txt', 'content');
assert.strictEqual(myVfs.existsSync('/exists.txt'), true);
assert.strictEqual(myVfs.existsSync('/not-exists.txt'), false);
}
// Test accessSync
{
const myVfs = vfs.create();
myVfs.writeFileSync('/accessible.txt', 'content');
// Should not throw for existing file
myVfs.accessSync('/accessible.txt');
// Should throw ENOENT for non-existent
assert.throws(() => {
myVfs.accessSync('/nonexistent.txt');
}, { code: 'ENOENT' });
}
// Test file handle operations via openSync
{
const myVfs = vfs.create();
myVfs.writeFileSync('/handle-test.txt', 'hello world');
const fd = myVfs.openSync('/handle-test.txt', 'r');
assert.ok((fd & 0x40000000) !== 0);
const handle = require('internal/vfs/fd').getVirtualFd(fd);
// Read via file handle
const buffer = Buffer.alloc(5);
const bytesRead = handle.entry.readSync(buffer, 0, 5, 0);
assert.strictEqual(bytesRead, 5);
assert.strictEqual(buffer.toString(), 'hello');
myVfs.closeSync(fd);
}
// Test file handle write operations
{
const myVfs = vfs.create();
const fd = myVfs.openSync('/write-handle.txt', 'w');
const handle = require('internal/vfs/fd').getVirtualFd(fd);
const buffer = Buffer.from('written via handle');
const bytesWritten = handle.entry.writeSync(buffer, 0, buffer.length, 0);
assert.strictEqual(bytesWritten, buffer.length);
myVfs.closeSync(fd);
// Verify content
assert.strictEqual(myVfs.readFileSync('/write-handle.txt', 'utf8'), 'written via handle');
}
// Test file handle readFile and writeFile
{
const myVfs = vfs.create();
myVfs.writeFileSync('/handle-rw.txt', 'original');
const fd = myVfs.openSync('/handle-rw.txt', 'r+');
const handle = require('internal/vfs/fd').getVirtualFd(fd);
// Read via readFile
const content = handle.entry.readFileSync('utf8');
assert.strictEqual(content, 'original');
// Write via writeFile
handle.entry.writeFileSync('replaced');
myVfs.closeSync(fd);
assert.strictEqual(myVfs.readFileSync('/handle-rw.txt', 'utf8'), 'replaced');
}
// Test symlink operations
{
const myVfs = vfs.create();
myVfs.writeFileSync('/target.txt', 'target content');
myVfs.symlinkSync('/target.txt', '/link.txt');
// Reading through symlink should work
assert.strictEqual(myVfs.readFileSync('/link.txt', 'utf8'), 'target content');
// ReadlinkSync should return target
assert.strictEqual(myVfs.readlinkSync('/link.txt'), '/target.txt');
// Lstat on symlink should show it's a symlink
const lstat = myVfs.lstatSync('/link.txt');
assert.strictEqual(lstat.isSymbolicLink(), true);
}
// Test reading directory as file should fail
{
const myVfs = vfs.create();
myVfs.mkdirSync('/mydir', { recursive: true });
assert.throws(() => {
myVfs.readFileSync('/mydir');
}, { code: 'EISDIR' });
}
// Test that readFileSync returns independent buffer copies
{
const myVfs = vfs.create();
myVfs.writeFileSync('/independent.txt', 'original content');
const buf1 = myVfs.readFileSync('/independent.txt');
const buf2 = myVfs.readFileSync('/independent.txt');
// Both should have the same content
assert.deepStrictEqual(buf1, buf2);
// Mutating one should not affect the other
buf1[0] = 0xFF;
assert.notDeepStrictEqual(buf1, buf2);
assert.strictEqual(buf2.toString(), 'original content');
// A third read should still return the original content
const buf3 = myVfs.readFileSync('/independent.txt');
assert.strictEqual(buf3.toString(), 'original content');
}
// ==================== Async Operations ====================
// Test async read and write operations
(async () => {
const myVfs = vfs.create();
await myVfs.promises.writeFile('/async-test.txt', 'async content');
const content = await myVfs.promises.readFile('/async-test.txt', 'utf8');
assert.strictEqual(content, 'async content');
const stat = await myVfs.promises.stat('/async-test.txt');
assert.strictEqual(stat.isFile(), true);
await myVfs.promises.unlink('/async-test.txt');
assert.strictEqual(myVfs.existsSync('/async-test.txt'), false);
})().then(common.mustCall());
// Test async lstat
(async () => {
const myVfs = vfs.create();
myVfs.writeFileSync('/async-lstat.txt', 'async lstat');
const stat = await myVfs.promises.lstat('/async-lstat.txt');
assert.strictEqual(stat.isFile(), true);
})().then(common.mustCall());
// Test async copyFile
(async () => {
const myVfs = vfs.create();
myVfs.writeFileSync('/async-src.txt', 'async copy');
await myVfs.promises.copyFile('/async-src.txt', '/async-dest.txt');
assert.strictEqual(myVfs.existsSync('/async-dest.txt'), true);
assert.strictEqual(myVfs.readFileSync('/async-dest.txt', 'utf8'), 'async copy');
})().then(common.mustCall());
// Test async mkdir and rmdir
(async () => {
const myVfs = vfs.create();
await myVfs.promises.mkdir('/async-dir');
assert.strictEqual(myVfs.existsSync('/async-dir'), true);
await myVfs.promises.rmdir('/async-dir');
assert.strictEqual(myVfs.existsSync('/async-dir'), false);
})().then(common.mustCall());
// Test async rename
(async () => {
const myVfs = vfs.create();
myVfs.writeFileSync('/async-old.txt', 'async rename');
await myVfs.promises.rename('/async-old.txt', '/async-new.txt');
assert.strictEqual(myVfs.existsSync('/async-old.txt'), false);
assert.strictEqual(myVfs.existsSync('/async-new.txt'), true);
})().then(common.mustCall());
// Test async readdir
(async () => {
const myVfs = vfs.create();
myVfs.mkdirSync('/async-readdir', { recursive: true });
myVfs.writeFileSync('/async-readdir/file.txt', 'content');
const entries = await myVfs.promises.readdir('/async-readdir');
assert.deepStrictEqual(entries, ['file.txt']);
})().then(common.mustCall());
// Test async appendFile
(async () => {
const myVfs = vfs.create();
myVfs.writeFileSync('/async-append.txt', 'start');
await myVfs.promises.appendFile('/async-append.txt', '-end');
assert.strictEqual(myVfs.readFileSync('/async-append.txt', 'utf8'), 'start-end');
})().then(common.mustCall());
// Test async access
(async () => {
const myVfs = vfs.create();
myVfs.writeFileSync('/async-access.txt', 'content');
await myVfs.promises.access('/async-access.txt');
await assert.rejects(
myVfs.promises.access('/nonexistent.txt'),
{ code: 'ENOENT' }
);
})().then(common.mustCall());
// Test async realpath
(async () => {
const myVfs = vfs.create();
myVfs.mkdirSync('/async-real/path', { recursive: true });
myVfs.writeFileSync('/async-real/path/file.txt', 'content');
const resolved = await myVfs.promises.realpath('/async-real/path/file.txt');
assert.strictEqual(resolved, '/async-real/path/file.txt');
})().then(common.mustCall());
// Test async file handle read
(async () => {
const myVfs = vfs.create();
myVfs.writeFileSync('/async-handle.txt', 'async read test');
const fd = myVfs.openSync('/async-handle.txt', 'r');
const handle = require('internal/vfs/fd').getVirtualFd(fd);
const buffer = Buffer.alloc(10);
const result = await handle.entry.read(buffer, 0, 10, 0);
assert.strictEqual(result.bytesRead, 10);
assert.strictEqual(buffer.toString(), 'async read');
myVfs.closeSync(fd);
})().then(common.mustCall());
// Test async file handle write
(async () => {
const myVfs = vfs.create();
const fd = myVfs.openSync('/async-write.txt', 'w');
const handle = require('internal/vfs/fd').getVirtualFd(fd);
const buffer = Buffer.from('async write');
const result = await handle.entry.write(buffer, 0, buffer.length, 0);
assert.strictEqual(result.bytesWritten, buffer.length);
myVfs.closeSync(fd);
// Verify content
assert.strictEqual(myVfs.readFileSync('/async-write.txt', 'utf8'), 'async write');
})().then(common.mustCall());
// Test async file handle stat
(async () => {
const myVfs = vfs.create();
myVfs.writeFileSync('/stat-handle.txt', 'stat test');
const fd = myVfs.openSync('/stat-handle.txt', 'r');
const handle = require('internal/vfs/fd').getVirtualFd(fd);
const stat = await handle.entry.stat();
assert.strictEqual(stat.isFile(), true);
myVfs.closeSync(fd);
})().then(common.mustCall());
// Test async file handle truncate
(async () => {
const myVfs = vfs.create();
myVfs.writeFileSync('/truncate-handle.txt', 'truncate this');
const fd = myVfs.openSync('/truncate-handle.txt', 'r+');
const handle = require('internal/vfs/fd').getVirtualFd(fd);
await handle.entry.truncate(8);
myVfs.closeSync(fd);
// Verify content was truncated
assert.strictEqual(myVfs.readFileSync('/truncate-handle.txt', 'utf8'), 'truncate');
})().then(common.mustCall());
// Test async file handle close
(async () => {
const myVfs = vfs.create();
myVfs.writeFileSync('/close-handle.txt', 'close test');
const fd = myVfs.openSync('/close-handle.txt', 'r');
const handle = require('internal/vfs/fd').getVirtualFd(fd);
await handle.entry.close();
assert.strictEqual(handle.entry.closed, true);
})().then(common.mustCall());
// Test async readFile and writeFile on handle
(async () => {
const myVfs = vfs.create();
myVfs.writeFileSync('/async-rw.txt', 'async original');
const fd = myVfs.openSync('/async-rw.txt', 'r+');
const handle = require('internal/vfs/fd').getVirtualFd(fd);
// Async read
const content = await handle.entry.readFile('utf8');
assert.strictEqual(content, 'async original');
// Async write
await handle.entry.writeFile('async replaced');
myVfs.closeSync(fd);
assert.strictEqual(myVfs.readFileSync('/async-rw.txt', 'utf8'), 'async replaced');
})().then(common.mustCall());
// ==================== Readonly Mode ====================
// Test MemoryProvider readonly mode
{
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'content');
myVfs.mkdirSync('/dir', { recursive: true });
// Set to readonly
myVfs.provider.setReadOnly();
assert.strictEqual(myVfs.provider.readonly, true);
// Read operations should still work
assert.strictEqual(myVfs.readFileSync('/file.txt', 'utf8'), 'content');
assert.strictEqual(myVfs.existsSync('/file.txt'), true);
assert.ok(myVfs.statSync('/file.txt'));
// Write operations should throw EROFS
assert.throws(() => {
myVfs.writeFileSync('/file.txt', 'new content');
}, { code: 'EROFS' });
assert.throws(() => {
myVfs.writeFileSync('/new.txt', 'content');
}, { code: 'EROFS' });
assert.throws(() => {
myVfs.appendFileSync('/file.txt', 'more');
}, { code: 'EROFS' });
assert.throws(() => {
myVfs.mkdirSync('/newdir');
}, { code: 'EROFS' });
assert.throws(() => {
myVfs.unlinkSync('/file.txt');
}, { code: 'EROFS' });
assert.throws(() => {
myVfs.rmdirSync('/dir');
}, { code: 'EROFS' });
assert.throws(() => {
myVfs.renameSync('/file.txt', '/renamed.txt');
}, { code: 'EROFS' });
assert.throws(() => {
myVfs.copyFileSync('/file.txt', '/copy.txt');
}, { code: 'EROFS' });
assert.throws(() => {
myVfs.symlinkSync('/file.txt', '/link');
}, { code: 'EROFS' });
}
// Test async operations on readonly MemoryProvider
(async () => {
const myVfs = vfs.create();
myVfs.writeFileSync('/readonly.txt', 'content');
myVfs.provider.setReadOnly();
await assert.rejects(
myVfs.promises.writeFile('/readonly.txt', 'new'),
{ code: 'EROFS' }
);
await assert.rejects(
myVfs.promises.appendFile('/readonly.txt', 'more'),
{ code: 'EROFS' }
);
await assert.rejects(
myVfs.promises.mkdir('/newdir'),
{ code: 'EROFS' }
);
await assert.rejects(
myVfs.promises.unlink('/readonly.txt'),
{ code: 'EROFS' }
);
await assert.rejects(
myVfs.promises.copyFile('/readonly.txt', '/copy.txt'),
{ code: 'EROFS' }
);
})().then(common.mustCall());
+49
View File
@@ -0,0 +1,49 @@
// Flags: --experimental-vfs
'use strict';
// mkdirSync / rmdirSync behaviour: return value, recursive option, mode
// option, error cases.
require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
// mkdirSync({ recursive: true }) returns the path of the first newly-
// created directory when some parents already exist.
{
const myVfs = vfs.create();
myVfs.mkdirSync('/a');
const result = myVfs.mkdirSync('/a/b/c', { recursive: true });
assert.strictEqual(result, '/a/b');
}
// mkdirSync with explicit mode (non-recursive)
{
const myVfs = vfs.create();
myVfs.mkdirSync('/d-mode', { mode: 0o700 });
assert.strictEqual(myVfs.statSync('/d-mode').mode & 0o777, 0o700);
}
// mkdirSync with explicit mode + recursive
{
const myVfs = vfs.create();
myVfs.mkdirSync('/r-mode/sub/deep', { recursive: true, mode: 0o700 });
assert.strictEqual(myVfs.statSync('/r-mode/sub/deep').mode & 0o777, 0o700);
}
// Recursive mkdir through a regular-file blocker throws ENOTDIR
{
const myVfs = vfs.create();
myVfs.writeFileSync('/blocker', 'x');
assert.throws(
() => myVfs.mkdirSync('/blocker/sub', { recursive: true }),
{ code: 'ENOTDIR' });
}
// Rmdir on a non-empty directory throws ENOTEMPTY
{
const myVfs = vfs.create();
myVfs.mkdirSync('/d');
myVfs.writeFileSync('/d/x', '');
assert.throws(() => myVfs.rmdirSync('/d'), { code: 'ENOTEMPTY' });
}
+38
View File
@@ -0,0 +1,38 @@
// Flags: --experimental-vfs
'use strict';
// mkdtemp / mkdtempSync behaviour.
const common = require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
// mkdtempSync returns the created directory path (with random suffix)
{
const myVfs = vfs.create();
const dir = myVfs.mkdtempSync('/tmp-');
assert.ok(dir.startsWith('/tmp-'));
assert.ok(myVfs.statSync(dir).isDirectory());
}
// Mkdtemp callback variant - success
{
const myVfs = vfs.create();
myVfs.mkdtemp('/tmp-', common.mustSucceed((dir) => {
assert.ok(dir.startsWith('/tmp-'));
}));
}
// Mkdtemp callback variant - with options object
{
const myVfs = vfs.create();
myVfs.mkdtemp('/tmp-', {}, common.mustSucceed((dir) => {
assert.ok(dir.startsWith('/tmp-'));
}));
}
// Mkdtemp callback variant — error path (parent doesn't exist)
{
const myVfs = vfs.create();
myVfs.mkdtemp('/missing/prefix-', common.expectsError());
}
@@ -0,0 +1,25 @@
// Flags: --experimental-vfs
'use strict';
// Operations that modify a directory should bump its mtime/ctime.
const common = require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
const myVfs = vfs.create();
myVfs.mkdirSync('/dir');
function getTimestamps(p) {
const st = myVfs.statSync(p);
return { mtimeMs: st.mtimeMs, ctimeMs: st.ctimeMs };
}
const before = getTimestamps('/dir');
// Wait long enough for ms-resolution mtime to differ
setTimeout(common.mustCall(() => {
myVfs.writeFileSync('/dir/file.txt', 'hello');
const after = getTimestamps('/dir');
assert.ok(after.mtimeMs >= before.mtimeMs);
assert.ok(after.ctimeMs >= before.ctimeMs);
}), 5);
+17
View File
@@ -0,0 +1,17 @@
// Flags: --experimental-vfs
'use strict';
// VFS promises.open returns a usable handle.
const common = require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
const myVfs = vfs.create();
myVfs.writeFileSync('/hello.txt', 'hello from vfs');
(async () => {
const fh = await myVfs.promises.open('/hello.txt', 'r');
assert.ok(fh);
assert.ok(typeof fh === 'number');
})().then(common.mustCall());
+483
View File
@@ -0,0 +1,483 @@
// Flags: --experimental-vfs --expose-internals
'use strict';
const common = require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
// Test callback-based readFile
{
const myVfs = vfs.create();
myVfs.writeFileSync('/test.txt', 'hello world');
myVfs.readFile('/test.txt', common.mustSucceed((data) => {
assert.ok(Buffer.isBuffer(data));
assert.strictEqual(data.toString(), 'hello world');
}));
myVfs.readFile('/test.txt', 'utf8', common.mustSucceed((data) => {
assert.strictEqual(data, 'hello world');
}));
myVfs.readFile('/test.txt', { encoding: 'utf8' }, common.mustSucceed((data) => {
assert.strictEqual(data, 'hello world');
}));
}
// Test callback-based readFile with non-existent file
{
const myVfs = vfs.create();
myVfs.readFile('/nonexistent.txt', common.expectsError({
code: 'ENOENT',
}));
}
// Test callback-based readFile with directory
{
const myVfs = vfs.create();
myVfs.mkdirSync('/mydir', { recursive: true });
myVfs.readFile('/mydir', common.expectsError({
code: 'EISDIR',
}));
}
// Test callback-based stat
{
const myVfs = vfs.create();
myVfs.mkdirSync('/dir', { recursive: true });
myVfs.writeFileSync('/file.txt', 'content');
myVfs.stat('/file.txt', common.mustSucceed((stats) => {
assert.strictEqual(stats.isFile(), true);
assert.strictEqual(stats.isDirectory(), false);
assert.strictEqual(stats.size, 7);
}));
myVfs.stat('/dir', common.mustSucceed((stats) => {
assert.strictEqual(stats.isFile(), false);
assert.strictEqual(stats.isDirectory(), true);
}));
myVfs.stat('/nonexistent', common.expectsError({
code: 'ENOENT',
}));
}
// Test callback-based lstat (same as stat for VFS)
{
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'content');
myVfs.lstat('/file.txt', common.mustCall((err, stats) => {
assert.strictEqual(err, null);
assert.strictEqual(stats.isFile(), true);
}));
}
// Test callback-based readdir
{
const myVfs = vfs.create();
myVfs.mkdirSync('/dir/subdir', { recursive: true });
myVfs.writeFileSync('/dir/file1.txt', 'a');
myVfs.writeFileSync('/dir/file2.txt', 'b');
myVfs.readdir('/dir', common.mustCall((err, entries) => {
assert.strictEqual(err, null);
assert.deepStrictEqual(entries.sort(), ['file1.txt', 'file2.txt', 'subdir']);
}));
myVfs.readdir('/dir', { withFileTypes: true }, common.mustCall((err, entries) => {
assert.strictEqual(err, null);
assert.strictEqual(entries.length, 3);
const file1 = entries.find((e) => e.name === 'file1.txt');
assert.strictEqual(file1.isFile(), true);
assert.strictEqual(file1.isDirectory(), false);
const subdir = entries.find((e) => e.name === 'subdir');
assert.strictEqual(subdir.isFile(), false);
assert.strictEqual(subdir.isDirectory(), true);
}));
myVfs.readdir('/nonexistent', common.mustCall((err, entries) => {
assert.strictEqual(err.code, 'ENOENT');
assert.strictEqual(entries, undefined);
}));
myVfs.readdir('/dir/file1.txt', common.mustCall((err, entries) => {
assert.strictEqual(err.code, 'ENOTDIR');
assert.strictEqual(entries, undefined);
}));
}
// Test callback-based realpath
{
const myVfs = vfs.create();
myVfs.mkdirSync('/path/to', { recursive: true });
myVfs.writeFileSync('/path/to/file.txt', 'content');
myVfs.realpath('/path/to/file.txt', common.mustCall((err, resolved) => {
assert.strictEqual(err, null);
assert.strictEqual(resolved, '/path/to/file.txt');
}));
myVfs.realpath('/path/to/../to/file.txt', common.mustCall((err, resolved) => {
assert.strictEqual(err, null);
assert.strictEqual(resolved, '/path/to/file.txt');
}));
myVfs.realpath('/nonexistent', common.mustCall((err, resolved) => {
assert.strictEqual(err.code, 'ENOENT');
assert.strictEqual(resolved, undefined);
}));
}
// Test callback-based access
{
const myVfs = vfs.create();
myVfs.writeFileSync('/accessible.txt', 'content');
myVfs.access('/accessible.txt', common.mustCall((err) => {
assert.strictEqual(err, null);
}));
myVfs.access('/nonexistent.txt', common.mustCall((err) => {
assert.strictEqual(err.code, 'ENOENT');
}));
}
// Test callback-based writeFile
{
const myVfs = vfs.create();
myVfs.writeFile('/cb-write.txt', 'callback written', common.mustCall((err) => {
assert.strictEqual(err, null);
assert.strictEqual(myVfs.readFileSync('/cb-write.txt', 'utf8'), 'callback written');
}));
// Overwrite existing
myVfs.writeFileSync('/cb-overwrite.txt', 'old');
myVfs.writeFile('/cb-overwrite.txt', 'new', common.mustCall((err) => {
assert.strictEqual(err, null);
assert.strictEqual(myVfs.readFileSync('/cb-overwrite.txt', 'utf8'), 'new');
}));
// Write with Buffer
myVfs.writeFile('/cb-buf.txt', Buffer.from('buf data'), common.mustCall((err) => {
assert.strictEqual(err, null);
assert.strictEqual(myVfs.readFileSync('/cb-buf.txt', 'utf8'), 'buf data');
}));
}
// Test callback-based readlink
{
const myVfs = vfs.create();
myVfs.writeFileSync('/link-target.txt', 'content');
myVfs.symlinkSync('/link-target.txt', '/my-link.txt');
myVfs.readlink('/my-link.txt', common.mustCall((err, target) => {
assert.strictEqual(err, null);
assert.strictEqual(target, '/link-target.txt');
}));
myVfs.readlink('/link-target.txt', common.mustCall((err) => {
assert.strictEqual(err.code, 'EINVAL');
}));
}
// Test callback-based open, read, fstat, close
{
const myVfs = vfs.create();
myVfs.writeFileSync('/fd-test.txt', 'fd content');
myVfs.open('/fd-test.txt', 'r', common.mustCall((err, fd) => {
assert.strictEqual(err, null);
assert.strictEqual(typeof fd, 'number');
// fstat
myVfs.fstat(fd, common.mustCall((err, stats) => {
assert.strictEqual(err, null);
assert.strictEqual(stats.isFile(), true);
assert.strictEqual(stats.size, 10);
}));
// read
const buf = Buffer.alloc(10);
myVfs.read(fd, buf, 0, 10, 0, common.mustCall((err, bytesRead, buffer) => {
assert.strictEqual(err, null);
assert.strictEqual(bytesRead, 10);
assert.strictEqual(buffer.toString(), 'fd content');
}));
// close
myVfs.close(fd, common.mustCall((err) => {
assert.strictEqual(err, null);
}));
}));
// open non-existent
myVfs.open('/nonexistent.txt', 'r', common.mustCall((err) => {
assert.strictEqual(err.code, 'ENOENT');
}));
}
// ==================== Promise API Tests ====================
// Test promises.readFile
(async () => {
const myVfs = vfs.create();
myVfs.writeFileSync('/promise-test.txt', 'promise content');
const bufferData = await myVfs.promises.readFile('/promise-test.txt');
assert.ok(Buffer.isBuffer(bufferData));
assert.strictEqual(bufferData.toString(), 'promise content');
const stringData = await myVfs.promises.readFile('/promise-test.txt', 'utf8');
assert.strictEqual(stringData, 'promise content');
const stringData2 = await myVfs.promises.readFile('/promise-test.txt', { encoding: 'utf8' });
assert.strictEqual(stringData2, 'promise content');
await assert.rejects(
myVfs.promises.readFile('/nonexistent.txt'),
{ code: 'ENOENT' }
);
myVfs.mkdirSync('/promisedir', { recursive: true });
await assert.rejects(
myVfs.promises.readFile('/promisedir'),
{ code: 'EISDIR' }
);
})().then(common.mustCall());
// Test promises.stat
(async () => {
const myVfs = vfs.create();
myVfs.mkdirSync('/stat-dir', { recursive: true });
myVfs.writeFileSync('/stat-file.txt', 'hello');
const fileStats = await myVfs.promises.stat('/stat-file.txt');
assert.strictEqual(fileStats.isFile(), true);
assert.strictEqual(fileStats.size, 5);
const dirStats = await myVfs.promises.stat('/stat-dir');
assert.strictEqual(dirStats.isDirectory(), true);
await assert.rejects(
myVfs.promises.stat('/nonexistent'),
{ code: 'ENOENT' }
);
})().then(common.mustCall());
// Test promises.lstat
(async () => {
const myVfs = vfs.create();
myVfs.writeFileSync('/lstat-file.txt', 'content');
const stats = await myVfs.promises.lstat('/lstat-file.txt');
assert.strictEqual(stats.isFile(), true);
})().then(common.mustCall());
// Test promises.readdir
(async () => {
const myVfs = vfs.create();
myVfs.mkdirSync('/pdir/sub', { recursive: true });
myVfs.writeFileSync('/pdir/a.txt', 'a');
myVfs.writeFileSync('/pdir/b.txt', 'b');
const names = await myVfs.promises.readdir('/pdir');
assert.deepStrictEqual(names.sort(), ['a.txt', 'b.txt', 'sub']);
const dirents = await myVfs.promises.readdir('/pdir', { withFileTypes: true });
assert.strictEqual(dirents.length, 3);
const aFile = dirents.find((e) => e.name === 'a.txt');
assert.strictEqual(aFile.isFile(), true);
await assert.rejects(
myVfs.promises.readdir('/nonexistent'),
{ code: 'ENOENT' }
);
await assert.rejects(
myVfs.promises.readdir('/pdir/a.txt'),
{ code: 'ENOTDIR' }
);
})().then(common.mustCall());
// Test promises.realpath
(async () => {
const myVfs = vfs.create();
myVfs.mkdirSync('/real/path', { recursive: true });
myVfs.writeFileSync('/real/path/file.txt', 'content');
const resolved = await myVfs.promises.realpath('/real/path/file.txt');
assert.strictEqual(resolved, '/real/path/file.txt');
const normalized = await myVfs.promises.realpath('/real/path/../path/file.txt');
assert.strictEqual(normalized, '/real/path/file.txt');
await assert.rejects(
myVfs.promises.realpath('/nonexistent'),
{ code: 'ENOENT' }
);
})().then(common.mustCall());
// Test promises.access
(async () => {
const myVfs = vfs.create();
myVfs.writeFileSync('/access-test.txt', 'content');
await myVfs.promises.access('/access-test.txt');
await assert.rejects(
myVfs.promises.access('/nonexistent'),
{ code: 'ENOENT' }
);
})().then(common.mustCall());
// Test promises.writeFile
(async () => {
const myVfs = vfs.create();
await myVfs.promises.writeFile('/write-test.txt', 'async written');
assert.strictEqual(myVfs.readFileSync('/write-test.txt', 'utf8'), 'async written');
// Overwrite existing file
await myVfs.promises.writeFile('/write-test.txt', 'overwritten');
assert.strictEqual(myVfs.readFileSync('/write-test.txt', 'utf8'), 'overwritten');
// Write with Buffer
await myVfs.promises.writeFile('/buffer-write.txt', Buffer.from('buffer data'));
assert.strictEqual(myVfs.readFileSync('/buffer-write.txt', 'utf8'), 'buffer data');
})().then(common.mustCall());
// Test promises.appendFile
(async () => {
const myVfs = vfs.create();
myVfs.writeFileSync('/append-test.txt', 'start');
await myVfs.promises.appendFile('/append-test.txt', '-end');
assert.strictEqual(myVfs.readFileSync('/append-test.txt', 'utf8'), 'start-end');
// Append to non-existent file creates it
await myVfs.promises.appendFile('/new-append.txt', 'new content');
assert.strictEqual(myVfs.readFileSync('/new-append.txt', 'utf8'), 'new content');
})().then(common.mustCall());
// Test promises.mkdir
(async () => {
const myVfs = vfs.create();
await myVfs.promises.mkdir('/async-dir');
const stat = myVfs.statSync('/async-dir');
assert.strictEqual(stat.isDirectory(), true);
// Recursive mkdir
await myVfs.promises.mkdir('/async-dir/nested/deep', { recursive: true });
assert.strictEqual(myVfs.statSync('/async-dir/nested/deep').isDirectory(), true);
// Mkdir on existing directory throws without recursive
await assert.rejects(
myVfs.promises.mkdir('/async-dir'),
{ code: 'EEXIST' }
);
})().then(common.mustCall());
// Test promises.unlink
(async () => {
const myVfs = vfs.create();
myVfs.writeFileSync('/unlink-test.txt', 'to delete');
await myVfs.promises.unlink('/unlink-test.txt');
assert.strictEqual(myVfs.existsSync('/unlink-test.txt'), false);
await assert.rejects(
myVfs.promises.unlink('/nonexistent.txt'),
{ code: 'ENOENT' }
);
})().then(common.mustCall());
// Test promises.rmdir
(async () => {
const myVfs = vfs.create();
myVfs.mkdirSync('/rmdir-test');
await myVfs.promises.rmdir('/rmdir-test');
assert.strictEqual(myVfs.existsSync('/rmdir-test'), false);
// Rmdir on non-empty directory throws
myVfs.mkdirSync('/nonempty');
myVfs.writeFileSync('/nonempty/file.txt', 'content');
await assert.rejects(
myVfs.promises.rmdir('/nonempty'),
{ code: 'ENOTEMPTY' }
);
})().then(common.mustCall());
// Test promises.rename
(async () => {
const myVfs = vfs.create();
myVfs.writeFileSync('/rename-src.txt', 'rename me');
await myVfs.promises.rename('/rename-src.txt', '/rename-dest.txt');
assert.strictEqual(myVfs.existsSync('/rename-src.txt'), false);
assert.strictEqual(myVfs.readFileSync('/rename-dest.txt', 'utf8'), 'rename me');
})().then(common.mustCall());
// Test promises.copyFile
(async () => {
const myVfs = vfs.create();
myVfs.writeFileSync('/copy-src.txt', 'copy me');
await myVfs.promises.copyFile('/copy-src.txt', '/copy-dest.txt');
assert.strictEqual(myVfs.readFileSync('/copy-dest.txt', 'utf8'), 'copy me');
// Source still exists
assert.strictEqual(myVfs.existsSync('/copy-src.txt'), true);
await assert.rejects(
myVfs.promises.copyFile('/nonexistent.txt', '/fail.txt'),
{ code: 'ENOENT' }
);
})().then(common.mustCall());
// Test promises.symlink and promises.readlink
(async () => {
const myVfs = vfs.create();
myVfs.writeFileSync('/symlink-target.txt', 'symlink content');
await myVfs.promises.symlink('/symlink-target.txt', '/symlink-link.txt');
// Verify symlink was created
const lstat = myVfs.lstatSync('/symlink-link.txt');
assert.strictEqual(lstat.isSymbolicLink(), true);
// Read through symlink
const content = await myVfs.promises.readFile('/symlink-link.txt', 'utf8');
assert.strictEqual(content, 'symlink content');
// Readlink should return target
const target = await myVfs.promises.readlink('/symlink-link.txt');
assert.strictEqual(target, '/symlink-target.txt');
// Readlink on non-symlink should error
await assert.rejects(
myVfs.promises.readlink('/symlink-target.txt'),
{ code: 'EINVAL' }
);
})().then(common.mustCall());
// Test async truncate (via file handle)
(async () => {
const myVfs = vfs.create();
myVfs.writeFileSync('/truncate-test.txt', 'async content');
const fd = myVfs.openSync('/truncate-test.txt', 'r+');
const { getVirtualFd } = require('internal/vfs/fd');
const handle = getVirtualFd(fd);
await handle.entry.truncate(5);
myVfs.closeSync(fd);
assert.strictEqual(myVfs.readFileSync('/truncate-test.txt', 'utf8'), 'async');
})().then(common.mustCall());
@@ -0,0 +1,54 @@
// Flags: --experimental-vfs
'use strict';
// Recursive readdir must follow symlinks to directories.
require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
const myVfs = vfs.create();
myVfs.mkdirSync('/real-dir');
myVfs.writeFileSync('/real-dir/nested.txt', 'nested');
myVfs.mkdirSync('/root');
myVfs.symlinkSync('/real-dir', '/root/symdir');
const entries = myVfs.readdirSync('/root', { recursive: true });
assert.ok(entries.includes('symdir'));
assert.ok(
entries.includes('symdir/nested.txt'),
`Expected 'symdir/nested.txt' in entries: ${entries}`,
);
// Recursive readdir with withFileTypes:true returns Dirent objects whose
// parentPath reflects the actual location of the entry (not the entry's
// stringified relative path).
{
const v = vfs.create();
v.mkdirSync('/r/a/b', { recursive: true });
v.writeFileSync('/r/top.txt', 'x');
v.writeFileSync('/r/a/b/leaf.txt', 'y');
const dirents = v.readdirSync('/r', { withFileTypes: true, recursive: true });
const leaf = dirents.find((d) => d.name === 'leaf.txt');
assert.ok(leaf);
assert.strictEqual(leaf.parentPath, '/r/a/b');
const top = dirents.find((d) => d.name === 'top.txt');
assert.ok(top);
assert.strictEqual(top.parentPath, '/r');
}
// Non-recursive readdir with withFileTypes returns mixed entry types
{
const v = vfs.create();
v.mkdirSync('/d');
v.writeFileSync('/d/a.txt', 'x');
v.mkdirSync('/d/sub');
v.symlinkSync('a.txt', '/d/lnk');
const dirents = v.readdirSync('/d', { withFileTypes: true });
assert.ok(dirents.some((d) => d.name === 'a.txt' && d.isFile()));
assert.ok(dirents.some((d) => d.name === 'sub' && d.isDirectory()));
assert.ok(dirents.some((d) => d.name === 'lnk' && d.isSymbolicLink()));
}
+24
View File
@@ -0,0 +1,24 @@
// Flags: --experimental-vfs
'use strict';
// VFS readFile callback API.
const common = require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
const myVfs = vfs.create();
myVfs.writeFileSync('/async-read.txt', 'async content');
myVfs.readFile('/async-read.txt', 'utf8', common.mustSucceed((data) => {
assert.strictEqual(data, 'async content');
}));
myVfs.readFile('/async-read.txt', common.mustSucceed((data) => {
assert.ok(Buffer.isBuffer(data));
assert.strictEqual(data.toString(), 'async content');
}));
myVfs.readFile('/missing.txt', common.expectsError({
code: 'ENOENT',
}));
@@ -0,0 +1,21 @@
// Flags: --experimental-vfs
'use strict';
// readFileSync with invalid encoding must throw.
require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'x');
assert.throws(
() => myVfs.readFileSync('/file.txt', { encoding: 'bogus' }),
/encoding/i,
);
// Valid encodings should work
assert.strictEqual(myVfs.readFileSync('/file.txt', 'utf8'), 'x');
assert.strictEqual(myVfs.readFileSync('/file.txt', { encoding: 'utf8' }), 'x');
assert.deepStrictEqual(myVfs.readFileSync('/file.txt'), Buffer.from('x'));
+39
View File
@@ -0,0 +1,39 @@
// Flags: --experimental-vfs
'use strict';
const common = require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
// Test readFileSync with flag: 'w+' truncates existing file
{
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'original content');
// Reading with 'w+' flag should truncate then read (empty result)
const result = myVfs.readFileSync('/file.txt', { flag: 'w+' });
assert.strictEqual(result.length, 0);
}
// Test readFileSync with flag: 'a+' on new file
{
const myVfs = vfs.create();
myVfs.mkdirSync('/dir', { recursive: true });
// Reading with 'a+' flag should create file if missing and return empty
const result = myVfs.readFileSync('/dir/new.txt', { flag: 'a+' });
assert.strictEqual(result.length, 0);
// File should now exist
assert.strictEqual(myVfs.existsSync('/dir/new.txt'), true);
}
// Test async readFile with flag: 'w+' truncates existing file
{
const myVfs = vfs.create();
myVfs.writeFileSync('/file2.txt', 'some data');
myVfs.promises.readFile('/file2.txt', { flag: 'w+' }).then(common.mustCall((result) => {
assert.strictEqual(result.length, 0);
}));
}
@@ -0,0 +1,104 @@
// Flags: --experimental-vfs --expose-internals
'use strict';
// RealFileHandle: sync and async file-handle operations, plus EBADF
// behaviour after close.
const common = require('../common');
const tmpdir = require('../common/tmpdir');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const vfs = require('node:vfs');
const { getVirtualFd } = require('internal/vfs/fd');
tmpdir.refresh();
const root = path.join(tmpdir.path, 'real-handle');
fs.mkdirSync(root, { recursive: true });
const myVfs = vfs.create(new vfs.RealFSProvider(root));
(async () => {
// ===== Sync read/write/stat/truncate via openSync + getVirtualFd =====
{
fs.writeFileSync(path.join(root, 'sync-rw.txt'), 'hello world');
const fd = myVfs.openSync('/sync-rw.txt', 'r+');
const handle = getVirtualFd(fd).entry;
const buf = Buffer.alloc(5);
assert.strictEqual(handle.readSync(buf, 0, 5, 0), 5);
assert.strictEqual(buf.toString(), 'hello');
const wbuf = Buffer.from('zz');
assert.strictEqual(handle.writeSync(wbuf, 0, 2, 0), 2);
assert.strictEqual(handle.statSync().isFile(), true);
assert.strictEqual(handle.readFileSync('utf8'), 'zzllo world');
handle.writeFileSync('replaced');
assert.strictEqual(handle.readFileSync('utf8'), 'replaced');
myVfs.closeSync(fd);
}
// ===== Async read/write/stat/truncate via provider.open =====
{
await myVfs.promises.writeFile('/h2.txt', 'abcdef');
const handle = await myVfs.provider.open('/h2.txt', 'r+');
const buf = Buffer.alloc(3);
assert.strictEqual(handle.readSync(buf, 0, 3, 0), 3);
assert.strictEqual(buf.toString(), 'abc');
const r = await handle.read(Buffer.alloc(3), 0, 3, 3);
assert.strictEqual(r.bytesRead, 3);
assert.strictEqual(r.buffer.toString(), 'def');
handle.writeSync(Buffer.from('ZZ'), 0, 2, 0);
const w = await handle.write(Buffer.from('YY'), 0, 2, 4);
assert.strictEqual(w.bytesWritten, 2);
const s1 = handle.statSync();
const s2 = await handle.stat();
assert.strictEqual(s1.size, s2.size);
assert.ok(handle.readFileSync().length > 0);
assert.ok((await handle.readFile()).length > 0);
handle.writeFileSync('OVERWRITTEN');
assert.strictEqual(handle.readFileSync('utf8'), 'OVERWRITTEN');
await handle.writeFile('async-overwrite');
assert.strictEqual(await handle.readFile('utf8'), 'async-overwrite');
handle.truncateSync(3);
await handle.truncate(2);
await handle.close();
}
// ===== EBADF after close =====
{
await myVfs.promises.writeFile('/h.txt', 'hello');
const handle = await myVfs.provider.open('/h.txt', 'r');
await handle.close();
assert.strictEqual(handle.closed, true);
assert.throws(() => handle.readSync(Buffer.alloc(1), 0, 1, 0),
{ code: 'EBADF' });
await assert.rejects(handle.read(Buffer.alloc(1), 0, 1, 0),
{ code: 'EBADF' });
assert.throws(() => handle.writeSync(Buffer.from('x'), 0, 1, 0),
{ code: 'EBADF' });
await assert.rejects(handle.write(Buffer.from('x'), 0, 1, 0),
{ code: 'EBADF' });
assert.throws(() => handle.readFileSync(), { code: 'EBADF' });
await assert.rejects(handle.readFile(), { code: 'EBADF' });
assert.throws(() => handle.writeFileSync('x'), { code: 'EBADF' });
await assert.rejects(handle.writeFile('x'), { code: 'EBADF' });
assert.throws(() => handle.statSync(), { code: 'EBADF' });
await assert.rejects(handle.stat(), { code: 'EBADF' });
assert.throws(() => handle.truncateSync(), { code: 'EBADF' });
await assert.rejects(handle.truncate(), { code: 'EBADF' });
// Subsequent close is a no-op
handle.closeSync();
await handle.close();
}
})().then(common.mustCall());
@@ -0,0 +1,55 @@
// Flags: --experimental-vfs
'use strict';
// Promises API for RealFSProvider: every async/promises method,
// plus access() existing/missing.
const common = require('../common');
const tmpdir = require('../common/tmpdir');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const vfs = require('node:vfs');
tmpdir.refresh();
const root = path.join(tmpdir.path, 'real-provider-promises');
fs.mkdirSync(root, { recursive: true });
const myVfs = vfs.create(new vfs.RealFSProvider(root));
(async () => {
// writeFile + readFile
await myVfs.promises.writeFile('/a.txt', 'hello');
assert.strictEqual(await myVfs.promises.readFile('/a.txt', 'utf8'), 'hello');
// stat / lstat / access
const st = await myVfs.promises.stat('/a.txt');
assert.strictEqual(st.size, 5);
assert.strictEqual((await myVfs.promises.lstat('/a.txt')).isFile(), true);
await myVfs.promises.access('/a.txt');
await assert.rejects(myVfs.promises.access('/missing.txt'),
{ code: 'ENOENT' });
// mkdir / readdir / rmdir
await myVfs.promises.mkdir('/d/sub', { recursive: true });
const entries = await myVfs.promises.readdir('/d');
assert.deepStrictEqual(entries.sort(), ['sub']);
await myVfs.promises.rmdir('/d/sub');
// rename
await myVfs.promises.writeFile('/old.txt', 'x');
await myVfs.promises.rename('/old.txt', '/new.txt');
assert.strictEqual(myVfs.existsSync('/old.txt'), false);
assert.strictEqual(myVfs.existsSync('/new.txt'), true);
// unlink
await myVfs.promises.unlink('/new.txt');
assert.strictEqual(myVfs.existsSync('/new.txt'), false);
// copyFile
await myVfs.promises.copyFile('/a.txt', '/copy.txt');
assert.strictEqual(await myVfs.promises.readFile('/copy.txt', 'utf8'), 'hello');
// open async error
await assert.rejects(myVfs.provider.open('/missing.txt', 'r'),
{ code: 'ENOENT' });
})().then(common.mustCall());
@@ -0,0 +1,111 @@
// Flags: --experimental-vfs
'use strict';
// Symlink and path-escape behaviour for RealFSProvider.
const common = require('../common');
const tmpdir = require('../common/tmpdir');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const vfs = require('node:vfs');
tmpdir.refresh();
const root = path.join(tmpdir.path, 'real-symlinks');
fs.mkdirSync(root, { recursive: true });
const myVfs = vfs.create(new vfs.RealFSProvider(root));
(async () => {
// .. traversal in VFS paths can't escape root
{
const subDir = path.join(root, 'sandbox');
fs.mkdirSync(subDir, { recursive: true });
const subVfs = vfs.create(new vfs.RealFSProvider(subDir));
assert.throws(() => subVfs.statSync('/../hello.txt'), { code: 'ENOENT' });
assert.throws(() => subVfs.readFileSync('/../../../etc/passwd'),
{ code: 'ENOENT' });
fs.rmdirSync(subDir);
}
// Path traversal via a non-leading-slash relative path
{
const escapeProvider = new vfs.RealFSProvider(root);
assert.throws(() => escapeProvider.statSync('../etc/passwd'),
{ code: 'ENOENT' });
}
// Symlinks: absolute target rejected with EACCES
{
assert.throws(() => myVfs.symlinkSync('/etc/passwd', '/escape'),
{ code: 'EACCES' });
await assert.rejects(myVfs.promises.symlink('/etc/passwd', '/escape2'),
{ code: 'EACCES' });
}
// Symlinks: relative target outside root rejected with EACCES
{
assert.throws(() => myVfs.symlinkSync('../../escape', '/bad-link'),
{ code: 'EACCES' });
await assert.rejects(
myVfs.promises.symlink('../../escape', '/bad-link2'),
{ code: 'EACCES' });
}
// Symlink with relative target inside root — readlink returns target as-is
{
fs.writeFileSync(path.join(root, 'rel-target.txt'), 'ok');
fs.symlinkSync('rel-target.txt', path.join(root, 'rel-link'));
assert.strictEqual(myVfs.readlinkSync('/rel-link'), 'rel-target.txt');
assert.strictEqual(await myVfs.promises.readlink('/rel-link'),
'rel-target.txt');
}
// Symlink whose absolute target is inside root → translated to VFS path
{
fs.writeFileSync(path.join(root, 'target.txt'), 'x');
fs.symlinkSync(path.join(root, 'target.txt'),
path.join(root, 'abs-link'));
assert.strictEqual(myVfs.readlinkSync('/abs-link'), '/target.txt');
assert.strictEqual(await myVfs.promises.readlink('/abs-link'),
'/target.txt');
}
// Symlink whose absolute target equals root → '/'
{
fs.symlinkSync(root, path.join(root, 'root-link'));
assert.strictEqual(myVfs.readlinkSync('/root-link'), '/');
assert.strictEqual(await myVfs.promises.readlink('/root-link'), '/');
}
// Symlink that points outside root: realpath rejects with EACCES
{
fs.writeFileSync(path.join(tmpdir.path, 'outside.txt'), 'forbidden');
fs.symlinkSync(path.join(tmpdir.path, 'outside.txt'),
path.join(root, 'esc-link'));
// Readlink returns the absolute target as-is (no translation since it's
// outside the root)
const target = myVfs.readlinkSync('/esc-link');
assert.strictEqual(target, path.join(tmpdir.path, 'outside.txt'));
// Realpath rejects: the resolved target escapes root
assert.throws(() => myVfs.realpathSync('/esc-link'), { code: 'EACCES' });
await assert.rejects(myVfs.promises.realpath('/esc-link'),
{ code: 'EACCES' });
}
// Realpath on root and on a subdir
{
fs.mkdirSync(path.join(root, 'sub2'), { recursive: true });
assert.strictEqual(myVfs.realpathSync('/'), '/');
assert.strictEqual(myVfs.realpathSync('/sub2'), '/sub2');
assert.strictEqual(await myVfs.promises.realpath('/sub2'), '/sub2');
}
// RealFSProvider with a rootPath that already ends in path.sep
{
fs.writeFileSync(path.join(root, 'tr.txt'), 'tr');
const trailingProvider = new vfs.RealFSProvider(root + path.sep);
assert.strictEqual(trailingProvider.readFileSync('/tr.txt', 'utf8'), 'tr');
}
})().then(common.mustCall());
@@ -0,0 +1,40 @@
// Flags: --experimental-vfs
'use strict';
// watch / promises.watch / watchFile through RealFSProvider.
const common = require('../common');
const tmpdir = require('../common/tmpdir');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const vfs = require('node:vfs');
tmpdir.refresh();
const root = path.join(tmpdir.path, 'real-provider-watch');
fs.mkdirSync(root, { recursive: true });
const myVfs = vfs.create(new vfs.RealFSProvider(root));
assert.strictEqual(myVfs.provider.supportsWatch, true);
// fs.watch wrapper
{
fs.writeFileSync(path.join(root, 'watch-me.txt'), 'a');
const watcher = myVfs.watch('/watch-me.txt', { persistent: false });
watcher.close();
}
// promises.watch wrapper
(async () => {
fs.writeFileSync(path.join(root, 'pwatch.txt'), 'a');
const iter = myVfs.promises.watch('/pwatch.txt', { persistent: false });
await iter.return();
})().then(common.mustCall());
// watchFile / unwatchFile
{
fs.writeFileSync(path.join(root, 'wf.txt'), 'a');
const listener = () => {};
myVfs.watchFile('/wf.txt', { persistent: false }, listener);
myVfs.unwatchFile('/wf.txt', listener);
}
+148
View File
@@ -0,0 +1,148 @@
// Flags: --experimental-vfs
'use strict';
// Synchronous API for RealFSProvider: construction, basic file ops,
// stats, directories, copy, realpath. Async/promises live in
// test-vfs-real-provider-promises.js, file-handle ops live in
// test-vfs-real-provider-handle.js, and symlinks/path-escape live in
// test-vfs-real-provider-symlinks.js.
require('../common');
const tmpdir = require('../common/tmpdir');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const vfs = require('node:vfs');
tmpdir.refresh();
const testDir = path.join(tmpdir.path, 'vfs-real-provider');
fs.mkdirSync(testDir, { recursive: true });
// Capability flags + construction
{
const provider = new vfs.RealFSProvider(testDir);
assert.ok(provider);
assert.strictEqual(provider.rootPath, testDir);
assert.strictEqual(provider.readonly, false);
assert.strictEqual(provider.supportsSymlinks, true);
assert.strictEqual(provider.supportsWatch, true);
}
// Invalid rootPath
{
assert.throws(() => new vfs.RealFSProvider(123),
{ code: 'ERR_INVALID_ARG_TYPE' });
assert.throws(() => new vfs.RealFSProvider({}),
{ code: 'ERR_INVALID_ARG_TYPE' });
}
// vfs.create(provider) wires it up
{
const realVfs = vfs.create(new vfs.RealFSProvider(testDir));
assert.ok(realVfs);
assert.strictEqual(realVfs.readonly, false);
}
// readFile / writeFile sync
{
const realVfs = vfs.create(new vfs.RealFSProvider(testDir));
realVfs.writeFileSync('/hello.txt', 'Hello from VFS!');
const realPath = path.join(testDir, 'hello.txt');
assert.strictEqual(fs.existsSync(realPath), true);
assert.strictEqual(fs.readFileSync(realPath, 'utf8'), 'Hello from VFS!');
assert.strictEqual(realVfs.readFileSync('/hello.txt', 'utf8'), 'Hello from VFS!');
fs.unlinkSync(realPath);
}
// statSync / lstatSync
{
const realVfs = vfs.create(new vfs.RealFSProvider(testDir));
fs.writeFileSync(path.join(testDir, 'stat-test.txt'), 'content');
fs.mkdirSync(path.join(testDir, 'stat-dir'), { recursive: true });
assert.strictEqual(realVfs.statSync('/stat-test.txt').isFile(), true);
assert.strictEqual(realVfs.statSync('/stat-dir').isDirectory(), true);
assert.strictEqual(realVfs.lstatSync('/stat-test.txt').isFile(), true);
assert.throws(() => realVfs.statSync('/nonexistent'),
{ code: 'ENOENT' });
fs.unlinkSync(path.join(testDir, 'stat-test.txt'));
fs.rmdirSync(path.join(testDir, 'stat-dir'));
}
// readdirSync
{
const realVfs = vfs.create(new vfs.RealFSProvider(testDir));
fs.mkdirSync(path.join(testDir, 'readdir-test', 'subdir'), { recursive: true });
fs.writeFileSync(path.join(testDir, 'readdir-test', 'a.txt'), 'a');
fs.writeFileSync(path.join(testDir, 'readdir-test', 'b.txt'), 'b');
const entries = realVfs.readdirSync('/readdir-test');
assert.deepStrictEqual(entries.sort(), ['a.txt', 'b.txt', 'subdir']);
const dirents = realVfs.readdirSync('/readdir-test', { withFileTypes: true });
assert.strictEqual(dirents.length, 3);
assert.ok(dirents.some((d) => d.name === 'a.txt' && d.isFile()));
assert.ok(dirents.some((d) => d.name === 'subdir' && d.isDirectory()));
fs.unlinkSync(path.join(testDir, 'readdir-test', 'a.txt'));
fs.unlinkSync(path.join(testDir, 'readdir-test', 'b.txt'));
fs.rmdirSync(path.join(testDir, 'readdir-test', 'subdir'));
fs.rmdirSync(path.join(testDir, 'readdir-test'));
}
// mkdir / rmdir / recursive mkdir
{
const realVfs = vfs.create(new vfs.RealFSProvider(testDir));
realVfs.mkdirSync('/new-dir');
assert.strictEqual(fs.existsSync(path.join(testDir, 'new-dir')), true);
realVfs.rmdirSync('/new-dir');
assert.strictEqual(fs.existsSync(path.join(testDir, 'new-dir')), false);
realVfs.mkdirSync('/deep/nested/dir', { recursive: true });
assert.strictEqual(fs.existsSync(path.join(testDir, 'deep/nested/dir')), true);
fs.rmdirSync(path.join(testDir, 'deep/nested/dir'));
fs.rmdirSync(path.join(testDir, 'deep/nested'));
fs.rmdirSync(path.join(testDir, 'deep'));
}
// unlink
{
const realVfs = vfs.create(new vfs.RealFSProvider(testDir));
fs.writeFileSync(path.join(testDir, 'to-delete.txt'), 'delete me');
assert.strictEqual(realVfs.existsSync('/to-delete.txt'), true);
realVfs.unlinkSync('/to-delete.txt');
assert.strictEqual(fs.existsSync(path.join(testDir, 'to-delete.txt')), false);
}
// rename
{
const realVfs = vfs.create(new vfs.RealFSProvider(testDir));
fs.writeFileSync(path.join(testDir, 'old-name.txt'), 'rename me');
realVfs.renameSync('/old-name.txt', '/new-name.txt');
assert.strictEqual(fs.existsSync(path.join(testDir, 'old-name.txt')), false);
assert.strictEqual(fs.readFileSync(path.join(testDir, 'new-name.txt'), 'utf8'),
'rename me');
fs.unlinkSync(path.join(testDir, 'new-name.txt'));
}
// copyFile
{
const realVfs = vfs.create(new vfs.RealFSProvider(testDir));
fs.writeFileSync(path.join(testDir, 'source.txt'), 'copy me');
realVfs.copyFileSync('/source.txt', '/dest.txt');
assert.strictEqual(fs.readFileSync(path.join(testDir, 'dest.txt'), 'utf8'),
'copy me');
fs.unlinkSync(path.join(testDir, 'source.txt'));
fs.unlinkSync(path.join(testDir, 'dest.txt'));
}
// realpathSync (non-symlink)
{
const realVfs = vfs.create(new vfs.RealFSProvider(testDir));
fs.writeFileSync(path.join(testDir, 'real.txt'), 'content');
assert.strictEqual(realVfs.realpathSync('/real.txt'), '/real.txt');
fs.unlinkSync(path.join(testDir, 'real.txt'));
}
+46
View File
@@ -0,0 +1,46 @@
// Flags: --experimental-vfs
'use strict';
// Rename behaviour: overwrite, type mismatches, same-parent rename.
require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
// Renaming a file onto a directory throws EISDIR
{
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'x');
myVfs.mkdirSync('/dir');
assert.throws(() => myVfs.renameSync('/file.txt', '/dir'),
{ code: 'EISDIR' });
}
// Renaming a directory onto a file throws ENOTDIR
{
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'x');
myVfs.mkdirSync('/dir');
assert.throws(() => myVfs.renameSync('/dir', '/file.txt'),
{ code: 'ENOTDIR' });
}
// Renaming a file onto another file overwrites
{
const myVfs = vfs.create();
myVfs.writeFileSync('/a.txt', 'a');
myVfs.writeFileSync('/b.txt', 'b');
myVfs.renameSync('/a.txt', '/b.txt');
assert.strictEqual(myVfs.readFileSync('/b.txt', 'utf8'), 'a');
assert.strictEqual(myVfs.existsSync('/a.txt'), false);
}
// Renaming within the same parent directory
{
const myVfs = vfs.create();
myVfs.mkdirSync('/d');
myVfs.writeFileSync('/d/a.txt', 'x');
myVfs.renameSync('/d/a.txt', '/d/b.txt');
assert.strictEqual(myVfs.existsSync('/d/a.txt'), false);
assert.strictEqual(myVfs.existsSync('/d/b.txt'), true);
}
+70
View File
@@ -0,0 +1,70 @@
// Flags: --experimental-vfs
'use strict';
// Tests for VFS rmSync edge cases:
// - rmSync on a directory without recursive must throw EISDIR
// - rmSync on a symlink must not recurse into the target directory
const common = require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
// rmSync on a directory without { recursive: true } must throw EISDIR.
{
const myVfs = vfs.create();
myVfs.mkdirSync('/dir');
assert.throws(() => myVfs.rmSync('/dir'), { code: 'EISDIR' });
// Directory should still exist after the failed rmSync
assert.strictEqual(myVfs.existsSync('/dir'), true);
}
// rmSync(link, { recursive: true }) removes symlink without recursing
// into the target directory.
{
const myVfs = vfs.create();
myVfs.mkdirSync('/dir/sub', { recursive: true });
myVfs.writeFileSync('/dir/sub/file.txt', 'x');
myVfs.symlinkSync('/dir', '/link');
myVfs.rmSync('/link', { recursive: true });
// Symlink should be removed
assert.strictEqual(myVfs.existsSync('/link'), false);
// Target directory and its contents should still exist
assert.strictEqual(myVfs.existsSync('/dir/sub/file.txt'), true);
}
// rmSync with force: true ignores ENOENT
{
const myVfs = vfs.create();
myVfs.rmSync('/missing.txt', { force: true });
}
// rmSync without force on missing path throws ENOENT
{
const myVfs = vfs.create();
assert.throws(() => myVfs.rmSync('/missing.txt'), { code: 'ENOENT' });
}
// promises.rm equivalents
(async () => {
const myVfs = vfs.create();
myVfs.mkdirSync('/d/sub', { recursive: true });
myVfs.writeFileSync('/d/sub/f.txt', 'x');
await assert.rejects(myVfs.promises.rm('/d'), { code: 'EISDIR' });
await myVfs.promises.rm('/d', { recursive: true });
assert.strictEqual(myVfs.existsSync('/d'), false);
await myVfs.promises.rm('/missing', { force: true });
await assert.rejects(myVfs.promises.rm('/missing'), { code: 'ENOENT' });
// promises.rm on symlink unlinks without recursion
myVfs.mkdirSync('/d2/sub', { recursive: true });
myVfs.writeFileSync('/d2/sub/file.txt', 'x');
myVfs.symlinkSync('/d2', '/link2');
await myVfs.promises.rm('/link2', { recursive: true });
assert.strictEqual(myVfs.existsSync('/link2'), false);
assert.strictEqual(myVfs.existsSync('/d2/sub/file.txt'), true);
})().then(common.mustCall());
+31
View File
@@ -0,0 +1,31 @@
// Flags: --experimental-vfs
'use strict';
// rmdirSync on a symlink to a directory should throw ENOTDIR
const common = require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
{
const myVfs = vfs.create();
myVfs.mkdirSync('/dir');
myVfs.symlinkSync('/dir', '/link');
assert.throws(() => myVfs.rmdirSync('/link'),
{ code: 'ENOTDIR' });
// Both the symlink and directory should still exist
assert.ok(myVfs.existsSync('/link'));
assert.ok(myVfs.existsSync('/dir'));
}
// promises.rmdir equivalent
(async () => {
const myVfs = vfs.create();
myVfs.mkdirSync('/dir');
myVfs.symlinkSync('/dir', '/link');
await assert.rejects(myVfs.promises.rmdir('/link'),
{ code: 'ENOTDIR' });
})().then(common.mustCall());
+36
View File
@@ -0,0 +1,36 @@
// Flags: --experimental-vfs
'use strict';
// Verify { bigint: true } returns BigInt values for VFS stats.
require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'x');
const st = myVfs.statSync('/file.txt', { bigint: true });
assert.strictEqual(typeof st.size, 'bigint');
assert.strictEqual(st.size, 1n);
assert.strictEqual(typeof st.ino, 'bigint');
assert.strictEqual(typeof st.mode, 'bigint');
// Bigint stats for directories
{
const v = vfs.create();
v.mkdirSync('/dir');
const st = v.statSync('/dir', { bigint: true });
assert.strictEqual(typeof st.size, 'bigint');
assert.strictEqual(st.isDirectory(), true);
}
// Bigint stats for symlinks via lstat
{
const v = vfs.create();
v.mkdirSync('/dir');
v.symlinkSync('/dir', '/link');
const st = v.lstatSync('/link', { bigint: true });
assert.strictEqual(typeof st.size, 'bigint');
assert.strictEqual(st.isSymbolicLink(), true);
}
+80
View File
@@ -0,0 +1,80 @@
// Flags: --experimental-vfs --expose-internals
'use strict';
// Exercise the default-option paths in createFileStats / createDirectoryStats
// / createSymlinkStats / createZeroStats. These defaults aren't taken when
// MemoryProvider populates every option from the entry, so we drive the
// helpers directly.
require('../common');
const assert = require('assert');
const {
createFileStats,
createDirectoryStats,
createSymlinkStats,
createZeroStats,
} = require('internal/vfs/stats');
// All defaults — no options object at all
{
const st = createFileStats(42);
assert.strictEqual(st.size, 42);
assert.strictEqual((st.mode & 0o777), 0o644);
assert.strictEqual(st.nlink, 1);
assert.ok(st.isFile());
const dirSt = createDirectoryStats();
assert.ok(dirSt.isDirectory());
assert.strictEqual((dirSt.mode & 0o777), 0o755);
const linkSt = createSymlinkStats(7);
assert.ok(linkSt.isSymbolicLink());
assert.strictEqual((linkSt.mode & 0o777), 0o777);
assert.strictEqual(linkSt.size, 7);
}
// Empty options object exercises the `?? default` right-hand side.
{
const st = createFileStats(1, {});
assert.ok(st.isFile());
const dirSt = createDirectoryStats({});
assert.ok(dirSt.isDirectory());
const linkSt = createSymlinkStats(0, {});
assert.ok(linkSt.isSymbolicLink());
}
// Bigint variant of zero-stats
{
const z = createZeroStats({ bigint: true });
assert.strictEqual(typeof z.size, 'bigint');
assert.strictEqual(z.size, 0n);
assert.strictEqual(z.mode, 0n);
}
// Non-bigint zero-stats with no options
{
const z = createZeroStats();
assert.strictEqual(z.size, 0);
assert.strictEqual(z.mode, 0);
}
// Cover the `process.getuid?.() ?? 0` fallback (Windows-like environment).
// We stub the optional methods to simulate their absence.
{
const realUid = process.getuid;
const realGid = process.getgid;
process.getuid = undefined;
process.getgid = undefined;
try {
const fs = createFileStats(0);
assert.strictEqual(fs.uid, 0);
assert.strictEqual(fs.gid, 0);
const ds = createDirectoryStats();
assert.strictEqual(ds.uid, 0);
const ls = createSymlinkStats(0);
assert.strictEqual(ls.uid, 0);
} finally {
process.getuid = realUid;
process.getgid = realGid;
}
}
+24
View File
@@ -0,0 +1,24 @@
// Flags: --experimental-vfs
'use strict';
// VFS stats must have non-zero, unique ino and a distinctive dev.
require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
const myVfs = vfs.create();
myVfs.writeFileSync('/f1.txt', 'a');
myVfs.writeFileSync('/f2.txt', 'b');
const s1 = myVfs.statSync('/f1.txt');
const s2 = myVfs.statSync('/f2.txt');
// Dev should be distinctive VFS value (4085 = 0xVF5)
assert.strictEqual(s1.dev, 4085);
assert.strictEqual(s2.dev, 4085);
// Ino should be unique per call
assert.notStrictEqual(s1.ino, 0);
assert.notStrictEqual(s2.ino, 0);
assert.notStrictEqual(s1.ino, s2.ino);
+65
View File
@@ -0,0 +1,65 @@
// Flags: --experimental-vfs
'use strict';
// Error paths in VFS streams: missing files, EBADF on closed fds,
// destroyed streams, and write to a path under a missing parent.
const common = require('../common');
const vfs = require('node:vfs');
const myVfs = vfs.create();
// Read of a nonexistent file emits 'error'
{
const stream = myVfs.createReadStream('/missing.txt');
stream.on('error', common.expectsError({
code: 'ENOENT',
}));
}
// Read on a stream whose fd has been pre-closed → EBADF on first _read
{
myVfs.writeFileSync('/x.txt', 'hi');
const fd = myVfs.openSync('/x.txt');
const rs = myVfs.createReadStream('/x.txt', { fd, autoClose: false });
myVfs.closeSync(fd);
rs.on('error', common.expectsError({
code: 'EBADF',
}));
rs.resume();
}
// Read stream with autoClose:true after the fd is invalidated — covers the
// close-error swallow path inside the stream's #close.
{
myVfs.writeFileSync('/cl.txt', 'data');
const fd = myVfs.openSync('/cl.txt');
const rs = myVfs.createReadStream('/cl.txt', { fd, autoClose: true });
myVfs.closeSync(fd);
rs.on('error', common.expectsError());
rs.resume();
}
// WriteStream synchronously failing to open → destroys on next tick
{
const ws = myVfs.createWriteStream('/missing-dir/foo.txt', { flags: 'wx' });
ws.on('error', common.expectsError());
}
// WriteStream destroyed before write() — covers the destroyed-true branch
// in _write.
{
const ws = myVfs.createWriteStream('/wd.txt');
ws.on('error', () => {});
ws.destroy(new Error('boom'));
}
// _write errors when writeSync throws (closed fd)
{
myVfs.writeFileSync('/wfd.txt', '');
const fd = myVfs.openSync('/wfd.txt', 'w');
const ws = myVfs.createWriteStream('/wfd.txt', { fd, autoClose: false });
myVfs.closeSync(fd);
ws.on('error', common.expectsError());
ws.write('x');
}
@@ -0,0 +1,57 @@
// Flags: --experimental-vfs
'use strict';
// Test createReadStream / createWriteStream with an explicit `fd` option.
const common = require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'hello world');
function readStream(stream) {
return new Promise((resolve, reject) => {
const chunks = [];
stream.on('data', (c) => chunks.push(c));
stream.on('end', () => resolve(Buffer.concat(chunks).toString()));
stream.on('error', reject);
});
}
// Read using an existing fd; autoClose:false leaves fd open
{
const fd = myVfs.openSync('/file.txt', 'r');
const stream = myVfs.createReadStream('/file.txt', { fd, autoClose: false });
let opened = false;
stream.on('open', () => { opened = true; });
readStream(stream).then(common.mustCall((s) => {
assert.strictEqual(s, 'hello world');
assert.strictEqual(opened, true);
myVfs.closeSync(fd);
}));
}
// WriteStream with explicit fd; autoClose:false leaves the fd open
(async () => {
const fd = myVfs.openSync('/fd-write.txt', 'w');
const stream = myVfs.createWriteStream('/fd-write.txt', { fd, autoClose: false });
await new Promise((resolve) => stream.on('ready', resolve));
await new Promise((resolve, reject) =>
stream.end('via-fd', (err) => (err ? reject(err) : resolve())));
myVfs.closeSync(fd);
assert.strictEqual(myVfs.readFileSync('/fd-write.txt', 'utf8'), 'via-fd');
})().then(common.mustCall());
// WriteStream with explicit fd + start position
(async () => {
myVfs.writeFileSync('/pad.txt', 'AAAAAAAAAA');
const fd = myVfs.openSync('/pad.txt', 'r+');
const ws = myVfs.createWriteStream('/pad.txt', { fd, start: 5, autoClose: false });
await new Promise((resolve) => ws.on('ready', resolve));
await new Promise((resolve, reject) =>
ws.end('XX', (err) => (err ? reject(err) : resolve())));
myVfs.closeSync(fd);
// Position 5 → "AAAAAXXAAA"
assert.strictEqual(myVfs.readFileSync('/pad.txt', 'utf8'), 'AAAAAXXAAA');
})().then(common.mustCall());
@@ -0,0 +1,40 @@
// Flags: --experimental-vfs
'use strict';
// VFS streams must expose bytesRead, bytesWritten, and pending.
const common = require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
// ReadStream: bytesRead and pending
{
const myVfs = vfs.create();
myVfs.writeFileSync('/stream.txt', 'stream data');
const rs = myVfs.createReadStream('/stream.txt');
assert.strictEqual(rs.pending, true);
rs.on('data', common.mustCall(() => {
assert.strictEqual(rs.pending, false);
assert.ok(rs.bytesRead > 0);
}));
rs.on('end', common.mustCall());
}
// WriteStream: bytesWritten and pending
{
const myVfs = vfs.create();
myVfs.writeFileSync('/out.txt', '');
const ws = myVfs.createWriteStream('/out.txt');
assert.strictEqual(ws.pending, true);
assert.strictEqual(ws.bytesWritten, 0);
ws.write('hello', common.mustCall(() => {
assert.strictEqual(ws.pending, false);
assert.strictEqual(ws.bytesWritten, 5);
ws.end();
}));
}
@@ -0,0 +1,29 @@
// Flags: --experimental-vfs
'use strict';
// VFS stream constructors must validate start/end synchronously.
require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'abc');
// ReadStream: start > end must throw ERR_OUT_OF_RANGE synchronously
assert.throws(
() => myVfs.createReadStream('/file.txt', { start: 2, end: 1 }),
{ code: 'ERR_OUT_OF_RANGE' },
);
// ReadStream: negative start
assert.throws(
() => myVfs.createReadStream('/file.txt', { start: -1 }),
{ code: 'ERR_OUT_OF_RANGE' },
);
// ReadStream: negative end
assert.throws(
() => myVfs.createReadStream('/file.txt', { end: -1 }),
{ code: 'ERR_OUT_OF_RANGE' },
);
+302
View File
@@ -0,0 +1,302 @@
// Flags: --experimental-vfs
'use strict';
const common = require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
// Test basic createReadStream
{
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'hello world');
const stream = myVfs.createReadStream('/file.txt');
let data = '';
stream.on('open', common.mustCall((fd) => {
assert.notStrictEqual(fd & 0x40000000, 0);
}));
stream.on('ready', common.mustCall());
stream.on('data', (chunk) => {
data += chunk;
});
stream.on('end', common.mustCall(() => {
assert.strictEqual(data, 'hello world');
}));
stream.on('close', common.mustCall());
}
// Test createReadStream with encoding
{
const myVfs = vfs.create();
myVfs.writeFileSync('/encoded.txt', 'encoded content');
const stream = myVfs.createReadStream('/encoded.txt', { encoding: 'utf8' });
let data = '';
let receivedString = false;
stream.on('data', (chunk) => {
if (typeof chunk === 'string') {
receivedString = true;
}
data += chunk;
});
stream.on('end', common.mustCall(() => {
assert.strictEqual(receivedString, true);
assert.strictEqual(data, 'encoded content');
}));
}
// Test createReadStream with start and end
{
const myVfs = vfs.create();
myVfs.writeFileSync('/range.txt', '0123456789');
const stream = myVfs.createReadStream('/range.txt', {
start: 2,
end: 5,
});
let data = '';
stream.on('data', (chunk) => {
data += chunk;
});
stream.on('end', common.mustCall(() => {
// End is inclusive, so positions 2, 3, 4, 5 = "2345" (4 chars)
assert.strictEqual(data, '2345');
}));
}
// Test createReadStream with start only
{
const myVfs = vfs.create();
myVfs.writeFileSync('/start.txt', 'abcdefghij');
const stream = myVfs.createReadStream('/start.txt', { start: 5 });
let data = '';
stream.on('data', (chunk) => {
data += chunk;
});
stream.on('end', common.mustCall(() => {
assert.strictEqual(data, 'fghij');
}));
}
// Test createReadStream with non-existent file
{
const myVfs = vfs.create();
const stream = myVfs.createReadStream('/nonexistent.txt');
stream.on('error', common.mustCall((err) => {
assert.strictEqual(err.code, 'ENOENT');
}));
}
// Test createReadStream path property
{
const myVfs = vfs.create();
myVfs.writeFileSync('/path-test.txt', 'test');
const stream = myVfs.createReadStream('/path-test.txt');
assert.strictEqual(stream.path, '/path-test.txt');
stream.on('data', () => {}); // Consume data
stream.on('end', common.mustCall());
}
// Test createReadStream with small highWaterMark
{
const myVfs = vfs.create();
myVfs.writeFileSync('/small-hwm.txt', 'AAAABBBBCCCCDDDD');
const stream = myVfs.createReadStream('/small-hwm.txt', {
highWaterMark: 4,
});
const chunks = [];
stream.on('data', (chunk) => {
chunks.push(chunk.toString());
});
stream.on('end', common.mustCall(() => {
// With highWaterMark of 4, we should get multiple chunks
assert.ok(chunks.length >= 1);
assert.strictEqual(chunks.join(''), 'AAAABBBBCCCCDDDD');
}));
}
// Test createReadStream destroy
{
const myVfs = vfs.create();
myVfs.writeFileSync('/destroy.txt', 'content to destroy');
const stream = myVfs.createReadStream('/destroy.txt');
stream.on('open', common.mustCall(() => {
stream.destroy();
}));
stream.on('close', common.mustCall());
}
// Test createReadStream with large file
{
const myVfs = vfs.create();
const largeContent = 'X'.repeat(100000);
myVfs.writeFileSync('/large.txt', largeContent);
const stream = myVfs.createReadStream('/large.txt');
let receivedLength = 0;
stream.on('data', (chunk) => {
receivedLength += chunk.length;
});
stream.on('end', common.mustCall(() => {
assert.strictEqual(receivedLength, 100000);
}));
}
// Test createReadStream pipe to another stream
{
const myVfs = vfs.create();
const { Writable } = require('stream');
myVfs.writeFileSync('/pipe-source.txt', 'pipe this content');
const stream = myVfs.createReadStream('/pipe-source.txt');
let collected = '';
const writable = new Writable({
write(chunk, encoding, callback) {
collected += chunk;
callback();
},
});
stream.pipe(writable);
writable.on('finish', common.mustCall(() => {
assert.strictEqual(collected, 'pipe this content');
}));
}
// Test autoClose: false
{
const myVfs = vfs.create();
myVfs.writeFileSync('/no-auto-close.txt', 'content');
const stream = myVfs.createReadStream('/no-auto-close.txt', {
autoClose: false,
});
stream.on('close', common.mustCall());
// Start flowing the stream
stream.resume();
stream.on('end', common.mustCall(() => {
// With autoClose: false, close should not be emitted automatically
// We need to manually destroy the stream
setImmediate(() => {
stream.destroy();
});
}));
}
// ==================== Additional coverage ====================
const { Readable } = require('stream');
const { pipeline } = require('stream/promises');
// Slicing read stream with start/end
{
const myVfs = vfs.create();
myVfs.writeFileSync('/slice.txt', 'hello world');
const rs = myVfs.createReadStream('/slice.txt', { start: 6, end: 10 });
const chunks = [];
rs.on('data', (c) => chunks.push(c));
rs.on('end', common.mustCall(() => {
assert.strictEqual(Buffer.concat(chunks).toString(), 'world');
}));
}
// start: beyond file size → empty stream
{
const myVfs = vfs.create();
myVfs.writeFileSync('/sm.txt', 'abc');
const rs = myVfs.createReadStream('/sm.txt', { start: 10 });
rs.on('data', common.mustNotCall('no data expected'));
rs.on('end', common.mustCall());
}
// Empty file → end immediately
{
const myVfs = vfs.create();
myVfs.writeFileSync('/empty.txt', '');
const rs = myVfs.createReadStream('/empty.txt');
rs.on('data', common.mustNotCall('no data expected'));
rs.on('end', common.mustCall());
}
// Pipeline write
(async () => {
const myVfs = vfs.create();
await pipeline(
Readable.from([Buffer.from('hello'), Buffer.from(' world')]),
myVfs.createWriteStream('/out.txt'),
);
assert.strictEqual(myVfs.readFileSync('/out.txt', 'utf8'), 'hello world');
})().then(common.mustCall());
// Pipeline write with start position
(async () => {
const myVfs = vfs.create();
myVfs.writeFileSync('/pad.txt', 'AAAAAAAAAA');
await pipeline(
Readable.from([Buffer.from('XX')]),
myVfs.createWriteStream('/pad.txt', { start: 3, flags: 'r+' }),
);
assert.strictEqual(myVfs.readFileSync('/pad.txt', 'utf8'), 'AAAXXAAAAA');
})().then(common.mustCall());
// Write string chunk + encoding callback
(async () => {
const myVfs = vfs.create();
const stream = myVfs.createWriteStream('/str.txt');
await new Promise((resolve, reject) => {
stream.write('hello', 'utf8', (err) => (err ? reject(err) : resolve()));
});
await new Promise((resolve) => stream.end(resolve));
assert.strictEqual(myVfs.readFileSync('/str.txt', 'utf8'), 'hello');
})().then(common.mustCall());
// path getter
{
const myVfs = vfs.create();
myVfs.writeFileSync('/p.txt', 'x');
const rs = myVfs.createReadStream('/p.txt');
assert.strictEqual(rs.path, '/p.txt');
rs.destroy();
const ws = myVfs.createWriteStream('/p2.txt');
assert.strictEqual(ws.path, '/p2.txt');
ws.destroy();
}
// destroy() before any data triggers _destroy + close cleanup
{
const myVfs = vfs.create();
myVfs.writeFileSync('/d.txt', 'data');
const rs = myVfs.createReadStream('/d.txt');
rs.on('error', () => {});
rs.destroy();
}
+56
View File
@@ -0,0 +1,56 @@
// Flags: --experimental-vfs
'use strict';
// Symlink behaviour in the default (memory) VFS:
// - Loop detection (ELOOP)
// - Absolute and relative targets
// - Symlinked parent directories transparently follow
require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
// Direct symlink loop: /a -> /b -> /a
{
const myVfs = vfs.create();
myVfs.symlinkSync('/b', '/a');
myVfs.symlinkSync('/a', '/b');
assert.throws(() => myVfs.statSync('/a'), { code: 'ELOOP' });
assert.throws(() => myVfs.realpathSync('/a'), { code: 'ELOOP' });
}
// Symlink loop in an intermediate path component
{
const myVfs = vfs.create();
myVfs.symlinkSync('/loop2', '/loop1');
myVfs.symlinkSync('/loop1', '/loop2');
assert.throws(() => myVfs.statSync('/loop1/sub'), { code: 'ELOOP' });
}
// Absolute symlink target inside the VFS
{
const myVfs = vfs.create();
myVfs.mkdirSync('/dir');
myVfs.writeFileSync('/dir/file.txt', 'hi');
myVfs.symlinkSync('/dir', '/abs-link');
assert.strictEqual(myVfs.readFileSync('/abs-link/file.txt', 'utf8'), 'hi');
}
// Symlinked parent directory transparently follows on read+write
{
const myVfs = vfs.create();
myVfs.mkdirSync('/real-dir');
myVfs.writeFileSync('/real-dir/file.txt', 'hello');
myVfs.symlinkSync('/real-dir', '/link-dir');
assert.strictEqual(myVfs.readFileSync('/link-dir/file.txt', 'utf8'), 'hello');
myVfs.writeFileSync('/link-dir/new.txt', 'new');
assert.strictEqual(myVfs.readFileSync('/real-dir/new.txt', 'utf8'), 'new');
}
// Symlink onto an existing path throws EEXIST
{
const myVfs = vfs.create();
myVfs.writeFileSync('/a.txt', 'x');
assert.throws(() => myVfs.symlinkSync('/a.txt', '/a.txt'), { code: 'EEXIST' });
}
@@ -0,0 +1,15 @@
// Flags: --experimental-vfs
'use strict';
// truncateSync with negative length must clamp to 0, not throw.
require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'abc');
myVfs.truncateSync('/file.txt', -1);
const content = myVfs.readFileSync('/file.txt', 'utf8');
assert.strictEqual(content, '');
+27
View File
@@ -0,0 +1,27 @@
// Flags: --experimental-vfs
'use strict';
// utimes / lutimes accept Date instances, numeric seconds, strings,
// and other values (which fall through to a no-op time value).
require('../common');
const vfs = require('node:vfs');
const myVfs = vfs.create();
myVfs.writeFileSync('/u.txt', 'x');
myVfs.symlinkSync('/u.txt', '/lk');
// Numeric seconds branch
myVfs.utimesSync('/u.txt', 1000, 2000);
// Date object branch
myVfs.utimesSync('/u.txt', new Date(3000000), new Date(4000000));
// String time → DateNow() fallback
myVfs.utimesSync('/u.txt', 'now', 'now');
// null/undefined → fallthrough (returns the value as-is)
myVfs.utimesSync('/u.txt', null, undefined);
// lutimes for symlinks
myVfs.lutimesSync('/lk', new Date(0), new Date(0));
@@ -0,0 +1,88 @@
// Flags: --experimental-vfs --expose-internals
'use strict';
// Cover the VirtualFileHandle base class — all primitives must throw
// ERR_METHOD_NOT_IMPLEMENTED until a subclass overrides them.
const common = require('../common');
const assert = require('assert');
const { VirtualFileHandle } = require('internal/vfs/file_handle');
const handle = new VirtualFileHandle('/x.txt', 'r', 0o600);
assert.strictEqual(handle.path, '/x.txt');
assert.strictEqual(handle.flags, 'r');
assert.strictEqual(handle.mode, 0o600);
assert.strictEqual(handle.position, 0);
assert.strictEqual(handle.closed, false);
// Sync stubs throw ERR_METHOD_NOT_IMPLEMENTED
for (const m of ['readSync', 'writeSync', 'readFileSync', 'writeFileSync',
'statSync', 'truncateSync']) {
assert.throws(() => handle[m](), { code: 'ERR_METHOD_NOT_IMPLEMENTED' },
`${m} should throw`);
}
// Async stubs reject
for (const m of ['read', 'write', 'readFile', 'writeFile', 'stat', 'truncate']) {
assert.rejects(handle[m](), { code: 'ERR_METHOD_NOT_IMPLEMENTED' },
`${m} should reject`).then(common.mustCall());
}
// readv/writev base stubs throw
assert.rejects(handle.readv([Buffer.alloc(1)], 0),
{ code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall());
assert.rejects(handle.writev([Buffer.alloc(1)], 0),
{ code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall());
// appendFile uses write under the hood — same not-implemented
assert.rejects(handle.appendFile('x'),
{ code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall());
// readableWebStream / readLines / createReadStream / createWriteStream throw
assert.throws(() => handle.readableWebStream(),
{ code: 'ERR_METHOD_NOT_IMPLEMENTED' });
assert.throws(() => handle.readLines(),
{ code: 'ERR_METHOD_NOT_IMPLEMENTED' });
assert.throws(() => handle.createReadStream(),
{ code: 'ERR_METHOD_NOT_IMPLEMENTED' });
assert.throws(() => handle.createWriteStream(),
{ code: 'ERR_METHOD_NOT_IMPLEMENTED' });
// No-op metadata
(async () => {
await handle.chmod();
await handle.chown();
await handle.utimes();
await handle.datasync();
await handle.sync();
})().then(common.mustCall());
// close() / closeSync() / dispose
{
const h = new VirtualFileHandle('/y', 'r');
h.closeSync();
assert.strictEqual(h.closed, true);
// Operations after close throw EBADF (via #checkClosed) before NOT_IMPL
assert.throws(() => h.readSync(), { code: 'EBADF' });
assert.rejects(h.read(), { code: 'EBADF' }).then(common.mustCall());
}
// Close via async + Symbol.asyncDispose
(async () => {
const h = new VirtualFileHandle('/z', 'r');
await h.close();
assert.strictEqual(h.closed, true);
const h2 = new VirtualFileHandle('/z2', 'r');
await h2[Symbol.asyncDispose]();
assert.strictEqual(h2.closed, true);
})().then(common.mustCall());
// truncateSync default len = 0 path requires close-check too
{
const h = new VirtualFileHandle('/a', 'r');
h.closeSync();
assert.throws(() => h.truncateSync(), { code: 'EBADF' });
assert.rejects(h.truncate(), { code: 'EBADF' }).then(common.mustCall());
}
+109
View File
@@ -0,0 +1,109 @@
// Flags: --experimental-vfs
'use strict';
// Exercise the VirtualProvider base class — its capability flags,
// readonly stubs, and the default implementations built on primitives.
const common = require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
// Bare base provider: every primitive throws ERR_METHOD_NOT_IMPLEMENTED.
{
const p = new vfs.VirtualProvider();
assert.strictEqual(p.readonly, false);
assert.strictEqual(p.supportsSymlinks, false);
assert.strictEqual(p.supportsWatch, false);
for (const method of ['openSync', 'statSync', 'readdirSync', 'mkdirSync',
'rmdirSync', 'unlinkSync', 'renameSync',
'linkSync', 'readlinkSync', 'symlinkSync',
'watch', 'watchAsync', 'watchFile', 'unwatchFile']) {
assert.throws(() => p[method]('/x', '/y'),
{ code: 'ERR_METHOD_NOT_IMPLEMENTED' },
`${method} must throw`);
}
// Async primitives reject with the same error
for (const method of ['open', 'stat', 'readdir', 'mkdir', 'rmdir', 'unlink',
'rename', 'link', 'readlink', 'symlink']) {
assert.rejects(p[method]('/x', '/y'),
{ code: 'ERR_METHOD_NOT_IMPLEMENTED' },
`${method} must reject`).then(common.mustCall());
}
// lstat/lstatSync default to stat — should propagate the not-impl error
assert.throws(() => p.lstatSync('/x'),
{ code: 'ERR_METHOD_NOT_IMPLEMENTED' });
assert.rejects(p.lstat('/x'),
{ code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall());
// existsSync / exists default impl: stat throws → false
assert.strictEqual(p.existsSync('/x'), false);
p.exists('/x').then(common.mustCall((r) => {
assert.strictEqual(r, false);
}));
}
// Read-only provider: write primitives throw EROFS instead of NOT_IMPL.
{
const p = new class extends vfs.VirtualProvider {
get readonly() { return true; }
}();
for (const method of ['mkdirSync', 'rmdirSync', 'unlinkSync',
'renameSync', 'linkSync', 'symlinkSync']) {
assert.throws(() => p[method]('/x', '/y'),
{ code: 'EROFS' },
`${method} must throw EROFS`);
}
for (const method of ['mkdir', 'rmdir', 'unlink', 'rename', 'link', 'symlink']) {
assert.rejects(p[method]('/x', '/y'),
{ code: 'EROFS' }).then(common.mustCall());
}
// copyFile / writeFile / appendFile reject with EROFS
assert.rejects(p.copyFile('/a', '/b'),
{ code: 'EROFS' }).then(common.mustCall());
assert.throws(() => p.copyFileSync('/a', '/b'),
{ code: 'EROFS' });
assert.rejects(p.writeFile('/a', 'x'),
{ code: 'EROFS' }).then(common.mustCall());
assert.throws(() => p.writeFileSync('/a', 'x'),
{ code: 'EROFS' });
assert.rejects(p.appendFile('/a', 'x'),
{ code: 'EROFS' }).then(common.mustCall());
assert.throws(() => p.appendFileSync('/a', 'x'),
{ code: 'EROFS' });
}
// Default access / realpath / copyFile delegate to stat + read/write
{
// Use MemoryProvider with the public API to verify delegation paths,
// since the base class needs working primitives.
const myVfs = vfs.create();
myVfs.writeFileSync('/src.txt', 'hello');
// Realpath default: returns path as-is after stat — covered by myVfs.realpathSync
assert.strictEqual(myVfs.realpathSync('/src.txt'), '/src.txt');
// exists default impl
assert.strictEqual(myVfs.provider.existsSync('src.txt'), true);
assert.strictEqual(myVfs.provider.existsSync('missing.txt'), false);
// copyFile via base class default path (MemoryProvider doesn't override)
myVfs.provider.copyFileSync('src.txt', 'dst.txt');
assert.strictEqual(myVfs.readFileSync('/dst.txt', 'utf8'), 'hello');
// copyFile with COPYFILE_EXCL and existing dest must throw
const COPYFILE_EXCL = 1;
assert.throws(() =>
myVfs.provider.copyFileSync('src.txt', 'dst.txt', COPYFILE_EXCL),
{ code: 'EEXIST' });
// accessSync with mode=0 (existence-only)
myVfs.provider.accessSync('src.txt', 0);
// accessSync R_OK on a readable file should pass
const R_OK = 4;
myVfs.provider.accessSync('src.txt', R_OK);
}
@@ -0,0 +1,53 @@
// Flags: --experimental-vfs
'use strict';
// AbortSignal handling for watch() and promises.watch().
const common = require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
(async () => {
// Pre-aborted signal closes the watcher at construction
{
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'a');
const watcher = myVfs.watch('/file.txt', { signal: AbortSignal.abort() });
watcher.on('change', common.mustNotCall());
setImmediate(() => myVfs.writeFileSync('/file.txt', 'b'));
}
// Aborting after construction triggers close
{
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'a');
const ac = new AbortController();
const watcher = myVfs.watch('/file.txt', { signal: ac.signal });
watcher.on('change', common.mustNotCall());
ac.abort();
setImmediate(() => myVfs.writeFileSync('/file.txt', 'b'));
}
// promises.watch with pre-aborted signal resolves done immediately
{
const myVfs = vfs.create();
myVfs.writeFileSync('/p.txt', 'a');
const iter = myVfs.promises.watch('/p.txt', { signal: AbortSignal.abort() });
const r = await iter.next();
assert.strictEqual(r.done, true);
}
// promises.watch with mid-flight abort rejects pending next() with AbortError
{
const myVfs = vfs.create();
myVfs.writeFileSync('/p2.txt', 'a');
const ac = new AbortController();
const iter = myVfs.promises.watch('/p2.txt', {
signal: ac.signal,
interval: 1000,
});
const pending = iter.next();
queueMicrotask(() => ac.abort());
await assert.rejects(pending, { name: 'AbortError' });
}
})().then(common.mustCall());
+62
View File
@@ -0,0 +1,62 @@
// Flags: --experimental-vfs
'use strict';
// Tests for VFS directory watching:
// - watch() on directories reports child changes
// - File creation / deletion events
// - Listing failures during a poll are swallowed
const common = require('../common');
const assert = require('assert');
const { once } = require('events');
const vfs = require('node:vfs');
(async () => {
// Modifying a child file emits a change event.
{
const myVfs = vfs.create();
myVfs.mkdirSync('/parent', { recursive: true });
myVfs.writeFileSync('/parent/file.txt', 'x');
const watcher = myVfs.watch('/parent', { interval: 25 });
const changed = once(watcher, 'change');
myVfs.writeFileSync('/parent/file.txt', 'longer-content-changed');
assert.deepStrictEqual(await changed, ['change', 'file.txt']);
watcher.close();
}
// Non-recursive directory watch: file creation
{
const myVfs = vfs.create();
myVfs.mkdirSync('/d');
myVfs.writeFileSync('/d/a.txt', 'x');
const watcher = myVfs.watch('/d', { interval: 25 });
const changed = once(watcher, 'change');
myVfs.writeFileSync('/d/new.txt', 'x');
assert.deepStrictEqual(await changed, ['rename', 'new.txt']);
watcher.close();
}
// Non-recursive directory watch: file deletion of a tracked child
{
const myVfs = vfs.create();
myVfs.mkdirSync('/dd');
myVfs.writeFileSync('/dd/keep.txt', 'a');
myVfs.writeFileSync('/dd/goes.txt', 'b');
const watcher = myVfs.watch('/dd', { interval: 25 });
const evt = once(watcher, 'change');
myVfs.unlinkSync('/dd/goes.txt');
assert.deepStrictEqual(await evt, ['rename', 'goes.txt']);
watcher.close();
}
// The watched directory is removed mid-poll: readdirSync inside the
// poll throws and the watcher swallows the error.
{
const myVfs = vfs.create();
myVfs.mkdirSync('/gone');
myVfs.writeFileSync('/gone/f.txt', 'x');
const watcher = myVfs.watch('/gone', { interval: 25 });
myVfs.rmSync('/gone', { recursive: true });
watcher.close();
}
})().then(common.mustCall());
+21
View File
@@ -0,0 +1,21 @@
// Flags: --experimental-vfs
'use strict';
// Buffer encoding for watch(): filename arrives as a Buffer.
const common = require('../common');
const assert = require('assert');
const { once } = require('events');
const vfs = require('node:vfs');
(async () => {
const myVfs = vfs.create();
myVfs.writeFileSync('/bf.txt', 'a');
const watcher = myVfs.watch('/bf.txt', { interval: 25, encoding: 'buffer' });
const changed = once(watcher, 'change');
myVfs.writeFileSync('/bf.txt', 'longer-content-changed');
const [eventType, filename] = await changed;
assert.strictEqual(eventType, 'change');
assert.deepStrictEqual(filename, Buffer.from('bf.txt'));
watcher.close();
})().then(common.mustCall());
+84
View File
@@ -0,0 +1,84 @@
// Flags: --experimental-vfs
'use strict';
// promises.watch() returns an async iterable. Cover its event queue,
// next/return/throw, and close-while-pending behaviour.
const common = require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
(async () => {
// Basic for-await iteration receives a change event
{
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'a');
const iter = myVfs.promises.watch('/file.txt', { interval: 25 });
queueMicrotask(() => myVfs.writeFileSync('/file.txt', 'longer-changed'));
for await (const evt of iter) {
assert.strictEqual(evt.eventType, 'change');
break;
}
}
// Events queued before next() drain via the next call
{
const myVfs = vfs.create();
myVfs.writeFileSync('/q.txt', 'a');
const iter = myVfs.promises.watch('/q.txt', { interval: 25 });
myVfs.writeFileSync('/q.txt', 'longer-changed');
assert.partialDeepStrictEqual(await iter.next(), {
done: false,
value: {
eventType: 'change',
}
});
assert.deepStrictEqual(await iter.return(), {
done: true,
value: undefined,
});
}
// A change while a next() is pending shifts the resolver
{
const myVfs = vfs.create();
myVfs.writeFileSync('/q2.txt', 'a');
const iter = myVfs.promises.watch('/q2.txt', { interval: 25 });
const pending = iter.next();
myVfs.writeFileSync('/q2.txt', 'longer-changed');
const r = await pending;
if (!r.done) assert.strictEqual(r.value.eventType, 'change');
assert.partialDeepStrictEqual(await pending, {
done: false,
value: {
eventType: 'change',
}
});
assert.deepStrictEqual(await iter.return(), {
done: true,
value: undefined,
});
}
// throw() closes the watcher and resolves with done:true
{
const myVfs = vfs.create();
myVfs.writeFileSync('/q3.txt', 'a');
const iter = myVfs.promises.watch('/q3.txt', { interval: 25 });
const r = await iter.throw(new Error('go away'));
assert.deepStrictEqual(r, { done: true, value: undefined });
myVfs.writeFileSync('/q3.txt', 'b');
assert.deepStrictEqual(await iter.next(), { done: true, value: undefined });
}
// Close while a resolver is pending — drains via the 'close' handler
{
const myVfs = vfs.create();
myVfs.writeFileSync('/q4.txt', 'a');
const iter = myVfs.promises.watch('/q4.txt', { interval: 1000 });
const pending = iter.next();
queueMicrotask(() => iter.return());
const r = await pending;
assert.strictEqual(r.done, true);
}
})().then(common.mustCall());
+36
View File
@@ -0,0 +1,36 @@
// Flags: --experimental-vfs
'use strict';
// Recursive directory watching: descendant changes trigger events.
const common = require('../common');
const { once } = require('events');
const vfs = require('node:vfs');
(async () => {
// Recursive watch detects creation in a subdirectory
{
const myVfs = vfs.create();
myVfs.mkdirSync('/d/sub', { recursive: true });
myVfs.writeFileSync('/d/sub/a.txt', 'x');
const watcher = myVfs.watch('/d', { interval: 25, recursive: true });
watcher.on('change', common.mustCall(1)); // Making sure the event listener is called only once
const changedPromise = once(watcher, 'change');
myVfs.writeFileSync('/d/sub/b.txt', 'new');
await changedPromise;
watcher.close();
}
// Recursive watch detects modification of a pre-existing descendant
{
const myVfs = vfs.create();
myVfs.mkdirSync('/r/sub', { recursive: true });
myVfs.writeFileSync('/r/sub/file.txt', 'x');
const watcher = myVfs.watch('/r', { interval: 25, recursive: true });
watcher.on('change', common.mustCall(1)); // Making sure the event listener is called only once
const changedPromise = once(watcher, 'change');
myVfs.writeFileSync('/r/sub/file.txt', 'longer-content-changed');
await changedPromise;
watcher.close();
}
})().then(common.mustCall());
+75
View File
@@ -0,0 +1,75 @@
// Flags: --experimental-vfs
'use strict';
// Tests for VFS watch() on a single file: change detection, listener
// registration, ref/unref, and the watch-then-create flow.
const common = require('../common');
const assert = require('assert');
const { once } = require('events');
const vfs = require('node:vfs');
(async () => {
// Listener as 2nd argument
{
const myVfs = vfs.create();
myVfs.writeFileSync('/lf.txt', 'a');
const w = myVfs.watch('/lf.txt', common.mustNotCall());
w.close();
}
// Listener add/remove + ref/unref smoke
{
const myVfs = vfs.create();
myVfs.writeFileSync('/r.txt', 'a');
const w = myVfs.watch('/r.txt');
const fn = common.mustNotCall();
w.on('change', fn);
w.removeListener('change', fn);
w.on('change', fn);
w.removeAllListeners('change');
w.ref();
w.unref();
w.close();
}
// Double close is a no-op
{
const myVfs = vfs.create();
myVfs.writeFileSync('/x.txt', 'a');
const watcher = myVfs.watch('/x.txt');
watcher.close();
watcher.close();
}
// persistent: false reaches the unref branch
{
const myVfs = vfs.create();
myVfs.writeFileSync('/p.txt', 'a');
const watcher = myVfs.watch('/p.txt', { persistent: false });
watcher.close();
}
// Watching a missing path then creating it
{
const myVfs = vfs.create();
const watcher = myVfs.watch('/late.txt', { interval: 25 });
const changed = once(watcher, 'change');
myVfs.writeFileSync('/late.txt', 'now');
await changed;
watcher.close();
}
// Modifying the watched file emits change with the basename as filename
{
const myVfs = vfs.create();
myVfs.writeFileSync('/single.txt', 'a');
const watcher = myVfs.watch('/single.txt', { interval: 25 });
const evt = once(watcher, 'change');
myVfs.writeFileSync('/single.txt', 'longer-content-changed');
const [eventType, filename] = await evt;
assert.strictEqual(eventType, 'change');
assert.strictEqual(filename, 'single.txt');
watcher.close();
}
})().then(common.mustCall());
+103
View File
@@ -0,0 +1,103 @@
// Flags: --experimental-vfs
'use strict';
// Tests for VFS watchFile/unwatchFile.
const common = require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
// unwatchFile(path) without a specific listener cleans up the timer.
// If the timer leaks, the process would hang.
{
const myVfs = vfs.create();
myVfs.writeFileSync('/a.txt', 'x');
myVfs.watchFile('/a.txt', { interval: 50, persistent: false }, common.mustNotCall());
myVfs.unwatchFile('/a.txt');
}
// Default options: no interval option provided
{
const myVfs = vfs.create();
myVfs.writeFileSync('/dw.txt', 'a');
const listener = common.mustNotCall();
myVfs.watchFile('/dw.txt', listener);
myVfs.unwatchFile('/dw.txt', listener);
myVfs.writeFileSync('/dw.txt', 'b');
}
// Double unwatch is a no-op
{
const myVfs = vfs.create();
myVfs.writeFileSync('/sw.txt', 'a');
const listener = common.mustNotCall();
myVfs.watchFile('/sw.txt', { interval: 25 }, listener);
myVfs.unwatchFile('/sw.txt', listener);
myVfs.unwatchFile('/sw.txt', listener);
}
// Zero stats for a missing file: prev.isFile() is false and prev.mode is 0
{
const myVfs = vfs.create();
function listener(curr, prev) {
assert.strictEqual(prev.isFile(), false);
assert.strictEqual(prev.mode, 0);
myVfs.unwatchFile('/missing.txt', listener);
}
myVfs.watchFile('/missing.txt', { interval: 50, persistent: false }, common.mustCall(listener));
setImmediate(() => myVfs.writeFileSync('/missing.txt', 'x'));
}
// Content change fires the listener with curr/prev stats
(async () => {
const myVfs = vfs.create();
myVfs.writeFileSync('/sw.txt', 'a');
const listener = common.mustCallAtLeast((curr, prev) => {
assert.strictEqual(typeof curr.size, 'number');
assert.strictEqual(typeof prev.size, 'number');
}, 1);
const fired = new Promise((resolve) => {
myVfs.watchFile('/sw.txt', { interval: 25 }, (curr, prev) => {
listener(curr, prev);
resolve();
});
});
myVfs.writeFileSync('/sw.txt', 'longer-content-changed');
await fired;
myVfs.unwatchFile('/sw.txt');
})().then(common.mustCall());
// bigint: true returns BigInt fields in both curr and prev stats, plus
// the bigint createZeroStats path when watching an initially-missing file.
(async () => {
const myVfs = vfs.create();
myVfs.writeFileSync('/bi.txt', 'a');
const listener = common.mustCallAtLeast((curr, prev) => {
assert.strictEqual(typeof curr.size, 'bigint');
assert.strictEqual(typeof prev.size, 'bigint');
}, 1);
const fired = new Promise((resolve) => {
myVfs.watchFile('/bi.txt', { interval: 25, bigint: true }, (curr, prev) => {
listener(curr, prev);
resolve();
});
});
myVfs.writeFileSync('/bi.txt', 'longer-content-changed');
await fired;
myVfs.unwatchFile('/bi.txt');
})().then(common.mustCall());
// bigint: true on a missing file emits BigInt prev.size = 0n
{
const myVfs = vfs.create();
const watcher = myVfs.watchFile('/missing-b.txt',
{ interval: 50, persistent: false, bigint: true },
common.mustCallAtLeast((curr, prev) => {
assert.strictEqual(typeof curr.size, 'bigint');
assert.strictEqual(typeof prev.size, 'bigint');
myVfs.unwatchFile('/missing-b.txt');
}, 1));
setTimeout(() => myVfs.writeFileSync('/missing-b.txt', 'now-here'), 80);
setTimeout(() => myVfs.unwatchFile('/missing-b.txt'), 500);
if (watcher?.unref) watcher.unref();
}
+33
View File
@@ -0,0 +1,33 @@
// Flags: --experimental-vfs
'use strict';
// writeFile / appendFile accept explicit { flag, mode } options.
const common = require('../common');
const assert = require('assert');
const vfs = require('node:vfs');
// writeFileSync / promises.writeFile with explicit flag and mode
{
const myVfs = vfs.create();
myVfs.writeFileSync('/a.txt', 'hello', { flag: 'w', mode: 0o600 });
assert.strictEqual(myVfs.readFileSync('/a.txt', 'utf8'), 'hello');
myVfs.promises.writeFile('/b.txt', 'world', { flag: 'w', mode: 0o600 })
.then(common.mustCall(() => {
assert.strictEqual(myVfs.readFileSync('/b.txt', 'utf8'), 'world');
}));
}
// appendFileSync / promises.appendFile with explicit flag and mode
{
const myVfs = vfs.create();
myVfs.appendFileSync('/a.txt', 'first', { flag: 'a', mode: 0o600 });
myVfs.appendFileSync('/a.txt', '-second', { flag: 'a' });
assert.strictEqual(myVfs.readFileSync('/a.txt', 'utf8'), 'first-second');
myVfs.promises.appendFile('/b.txt', 'go', { flag: 'a', mode: 0o600 })
.then(common.mustCall(() => {
assert.strictEqual(myVfs.readFileSync('/b.txt', 'utf8'), 'go');
}));
}