mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
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:
1 parent
c9dbb86683
commit
c9562ddb82
79 files changed
+10859
-1
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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().
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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:')) {
|
||||
|
||||
@@ -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);
|
||||
@@ -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');
|
||||
@@ -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);
|
||||
@@ -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');
|
||||
}));
|
||||
@@ -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());
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}));
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
@@ -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: '' });
|
||||
}
|
||||
@@ -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());
|
||||
@@ -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));
|
||||
@@ -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());
|
||||
@@ -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' });
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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());
|
||||
@@ -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()));
|
||||
}
|
||||
@@ -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'));
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
@@ -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());
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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' },
|
||||
);
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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, '');
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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());
|
||||
@@ -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());
|
||||
@@ -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());
|
||||
@@ -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());
|
||||
@@ -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());
|
||||
@@ -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());
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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');
|
||||
}));
|
||||
}
|
||||
Reference in new issue
Block a user