diff --git a/lib/internal/vfs/providers/real.js b/lib/internal/vfs/providers/real.js index fbcff25e39ccfe..085485ca8a1a97 100644 --- a/lib/internal/vfs/providers/real.js +++ b/lib/internal/vfs/providers/real.js @@ -1,10 +1,12 @@ 'use strict'; const { + ArrayPrototypePush, Promise, StringPrototypeStartsWith, } = primordials; +const { Buffer } = require('buffer'); const fs = require('fs'); const path = require('path'); const { VirtualProvider } = require('internal/vfs/provider'); @@ -17,6 +19,8 @@ const { createENOENT, } = require('internal/vfs/errors'); +const kReadFileUnknownBufferLength = 8192; + /** * A file handle that wraps a real file descriptor. */ @@ -34,6 +38,20 @@ class RealFileHandle extends VirtualFileHandle { } } + #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; + } + + #readFileUnknownSizeResult(buffers, totalRead, options) { + return this.#readFileResult( + Buffer.concat(buffers, totalRead), totalRead, options); + } + /** * @param {string} path The VFS path * @param {string} flags The open flags @@ -79,12 +97,77 @@ class RealFileHandle extends VirtualFileHandle { readFileSync(options) { this.#checkClosed('read'); - return fs.readFileSync(this.#realPath, options); + const size = fs.fstatSync(this.#fd).size; + if (size === 0) { + const buffers = []; + let totalRead = 0; + + while (true) { + const buffer = Buffer.allocUnsafe(kReadFileUnknownBufferLength); + const read = fs.readSync( + this.#fd, buffer, 0, buffer.byteLength, totalRead); + if (read === 0) break; + ArrayPrototypePush(buffers, buffer.subarray(0, read)); + totalRead += read; + } + + return this.#readFileUnknownSizeResult(buffers, totalRead, options); + } + + const buffer = Buffer.allocUnsafe(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; + if (size === 0) { + const buffers = []; + let totalRead = 0; + + while (true) { + const buffer = Buffer.allocUnsafe(kReadFileUnknownBufferLength); + const { bytesRead: read } = await this.read( + buffer, + 0, + buffer.byteLength, + totalRead, + ); + if (read === 0) break; + ArrayPrototypePush(buffers, buffer.subarray(0, read)); + totalRead += read; + } + + return this.#readFileUnknownSizeResult(buffers, totalRead, options); + } + + const buffer = Buffer.allocUnsafe(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..50d31470b8644f 100644 --- a/test/parallel/test-vfs-real-provider-handle.js +++ b/test/parallel/test-vfs-real-provider-handle.js @@ -75,6 +75,55 @@ 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')); + } + } + + // ===== readFile reads past the fallback chunk when fstat reports size 0 ===== + { + const content = 'a'.repeat(8192) + 'trailing data'; + fs.writeFileSync(path.join(root, 'zero-stat.txt'), content); + const syncHandle = await myVfs.provider.open('/zero-stat.txt', 'r'); + const asyncHandle = await myVfs.provider.open('/zero-stat.txt', 'r'); + const originalFstatSync = fs.fstatSync; + const originalFstat = fs.fstat; + + fs.fstatSync = common.mustCall(function fstatSync(...args) { + const stats = originalFstatSync.apply(this, args); + stats.size = 0; + return stats; + }); + fs.fstat = common.mustCall(function fstat(fd, options, callback) { + return originalFstat.call(this, fd, options, (err, stats) => { + if (stats) stats.size = 0; + callback(err, stats); + }); + }); + + try { + assert.strictEqual(syncHandle.readFileSync('utf8'), content); + assert.strictEqual(await asyncHandle.readFile('utf8'), content); + } finally { + fs.fstatSync = originalFstatSync; + fs.fstat = originalFstat; + await syncHandle.close(); + await asyncHandle.close(); + } + } + // ===== EBADF after close ===== { await myVfs.promises.writeFile('/h.txt', 'hello');