diff --git a/lib/internal/vfs/providers/real.js b/lib/internal/vfs/providers/real.js index fbcff25e39ccfe..7637f1ef7a97b6 100644 --- a/lib/internal/vfs/providers/real.js +++ b/lib/internal/vfs/providers/real.js @@ -5,6 +5,7 @@ const { StringPrototypeStartsWith, } = primordials; +const { Buffer } = require('buffer'); const fs = require('fs'); const path = require('path'); const { VirtualProvider } = require('internal/vfs/provider'); @@ -34,6 +35,19 @@ class RealFileHandle extends VirtualFileHandle { } } + #readFileBuffer(size) { + return Buffer.allocUnsafe(size || 8192); + } + + #readFileResult(buffer, bytesRead, options) { + buffer = buffer.subarray(0, bytesRead); + const encoding = typeof options === 'string' ? options : options?.encoding; + if (encoding && encoding !== 'buffer') { + buffer = buffer.toString(encoding); + } + return buffer; + } + /** * @param {string} path The VFS path * @param {string} flags The open flags @@ -79,12 +93,41 @@ class RealFileHandle extends VirtualFileHandle { readFileSync(options) { this.#checkClosed('read'); - return fs.readFileSync(this.#realPath, options); + const size = fs.fstatSync(this.#fd).size; + const buffer = this.#readFileBuffer(size); + let bytesRead = 0; + while (bytesRead < buffer.byteLength) { + const read = fs.readSync( + this.#fd, + buffer, + bytesRead, + buffer.byteLength - bytesRead, + bytesRead, + ); + if (read === 0) break; + bytesRead += read; + } + + return this.#readFileResult(buffer, bytesRead, options); } async readFile(options) { this.#checkClosed('read'); - return fs.promises.readFile(this.#realPath, options); + const size = (await this.stat()).size; + const buffer = this.#readFileBuffer(size); + let bytesRead = 0; + while (bytesRead < buffer.byteLength) { + const { bytesRead: read } = await this.read( + buffer, + bytesRead, + buffer.byteLength - bytesRead, + bytesRead, + ); + if (read === 0) break; + bytesRead += read; + } + + return this.#readFileResult(buffer, bytesRead, options); } writeFileSync(data, options) { diff --git a/test/parallel/test-vfs-fs-readFileSync.js b/test/parallel/test-vfs-fs-readFileSync.js index 96e39892e66a9f..1ca906b2a0577c 100644 --- a/test/parallel/test-vfs-fs-readFileSync.js +++ b/test/parallel/test-vfs-fs-readFileSync.js @@ -43,3 +43,32 @@ assert.strictEqual( } myVfs.unmount(); + +// readFileSync via a RealFSProvider fd remains usable after the backing path +// is renamed. +{ + const root = path.join('/tmp', 'vfs-real-readFileSync-' + process.pid); + const realMountPoint = path.join('/tmp', 'vfs-real-readFileSync-mount-' + process.pid); + fs.rmSync(root, { recursive: true, force: true }); + fs.rmSync(realMountPoint, { recursive: true, force: true }); + fs.mkdirSync(root, { recursive: true }); + fs.mkdirSync(realMountPoint, { recursive: true }); + + const realVfs = vfs + .create(new vfs.RealFSProvider(root), { emitExperimentalWarning: false }) + .mount(realMountPoint); + try { + fs.writeFileSync(path.join(root, 'a.txt'), 'still readable'); + const fd = fs.openSync(path.join(realMountPoint, 'a.txt'), 'r'); + try { + fs.renameSync(path.join(root, 'a.txt'), path.join(root, 'b.txt')); + assert.strictEqual(fs.readFileSync(fd, 'utf8'), 'still readable'); + } finally { + fs.closeSync(fd); + } + } finally { + realVfs.unmount(); + fs.rmSync(root, { recursive: true, force: true }); + fs.rmSync(realMountPoint, { recursive: true, force: true }); + } +} diff --git a/test/parallel/test-vfs-real-provider-handle.js b/test/parallel/test-vfs-real-provider-handle.js index 5246e28e3206c5..7739548002da7a 100644 --- a/test/parallel/test-vfs-real-provider-handle.js +++ b/test/parallel/test-vfs-real-provider-handle.js @@ -75,6 +75,23 @@ const myVfs = vfs.create(new vfs.RealFSProvider(root)); await handle.close(); } + // ===== readFile through an open real fd survives backing path rename ===== + { + fs.writeFileSync(path.join(root, 'rename-read.txt'), 'still readable'); + const syncHandle = await myVfs.provider.open('/rename-read.txt', 'r'); + const asyncHandle = await myVfs.provider.open('/rename-read.txt', 'r'); + fs.renameSync(path.join(root, 'rename-read.txt'), + path.join(root, 'rename-read-renamed.txt')); + try { + assert.strictEqual(syncHandle.readFileSync('utf8'), 'still readable'); + assert.strictEqual(await asyncHandle.readFile('utf8'), 'still readable'); + } finally { + await syncHandle.close(); + await asyncHandle.close(); + fs.unlinkSync(path.join(root, 'rename-read-renamed.txt')); + } + } + // ===== EBADF after close ===== { await myVfs.promises.writeFile('/h.txt', 'hello');