diff --git a/lib/internal/vfs/providers/real.js b/lib/internal/vfs/providers/real.js index 2f25f582244c..de18cda14941 100644 --- a/lib/internal/vfs/providers/real.js +++ b/lib/internal/vfs/providers/real.js @@ -458,12 +458,16 @@ class RealFSProvider extends VirtualProvider { mkdirSync(vfsPath, options) { const realPath = this.#resolvePath(vfsPath); - return fs.mkdirSync(realPath, options); + const created = fs.mkdirSync(realPath, options); + if (created === undefined) return undefined; + return this.#resolvedToVfsPath(created, vfsPath, 'mkdir'); } async mkdir(vfsPath, options) { const realPath = this.#resolvePath(vfsPath); - return fs.promises.mkdir(realPath, options); + const created = await fs.promises.mkdir(realPath, options); + if (created === undefined) return undefined; + return this.#resolvedToVfsPath(created, vfsPath, 'mkdir'); } rmdirSync(vfsPath) { @@ -566,7 +570,13 @@ class RealFSProvider extends VirtualProvider { // 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); + // Both sides are put in the Windows long-path form first, because the + // native bindings answer in it (\\?\C:\dir) and path.relative() reads + // that as a different root than a plain rootPath, which would make every + // comparison look like an escape. A no-op elsewhere, and idempotent, so + // a rootPath already in that form is covered too. + const rel = path.relative(path.toNamespacedPath(this.#rootPath), + path.toNamespacedPath(resolved)); if (rel === '') return '/'; if (rel === '..' || StringPrototypeStartsWith(rel, '..' + path.sep) || diff --git a/test/parallel/test-vfs-real-provider-promises.js b/test/parallel/test-vfs-real-provider-promises.js index 932a30fd086d..a7e8d3f7d6d1 100644 --- a/test/parallel/test-vfs-real-provider-promises.js +++ b/test/parallel/test-vfs-real-provider-promises.js @@ -35,6 +35,19 @@ const myVfs = vfs.create(new vfs.RealFSProvider(root)); assert.deepStrictEqual(entries.sort(), ['sub']); await myVfs.promises.rmdir('/d/sub'); + // Recursive mkdir returns the first directory created, as a VFS-relative + // path (never the backing-directory path), matching MemoryProvider. + const createdSub = await myVfs.promises.mkdir('/d/sub', { recursive: true }); + assert.strictEqual(createdSub, '/d/sub'); + assert.strictEqual(await myVfs.promises.mkdir('/d/sub', { recursive: true }), + undefined); + const created = await myVfs.promises.mkdir('/d/deep/nested', { recursive: true }); + assert.strictEqual(created, '/d/deep'); + assert.strictEqual(myVfs.existsSync(created), true); + await myVfs.promises.rmdir('/d/sub'); + await myVfs.promises.rmdir('/d/deep/nested'); + await myVfs.promises.rmdir('/d/deep'); + // rename await myVfs.promises.writeFile('/old.txt', 'x'); await myVfs.promises.rename('/old.txt', '/new.txt'); diff --git a/test/parallel/test-vfs-real-provider.js b/test/parallel/test-vfs-real-provider.js index a54181d8f6dc..9a6077cd9d6f 100644 --- a/test/parallel/test-vfs-real-provider.js +++ b/test/parallel/test-vfs-real-provider.js @@ -7,7 +7,7 @@ // test-vfs-real-provider-handle.js, and symlinks/path-escape live in // test-vfs-real-provider-symlinks.js. -require('../common'); +const common = require('../common'); const tmpdir = require('../common/tmpdir'); const assert = require('assert'); const fs = require('fs'); @@ -106,6 +106,49 @@ fs.mkdirSync(testDir, { recursive: true }); fs.rmdirSync(path.join(testDir, 'deep/nested/dir')); fs.rmdirSync(path.join(testDir, 'deep/nested')); fs.rmdirSync(path.join(testDir, 'deep')); + + // Recursive mkdir returns the first directory created, as a VFS-relative + // path (never the backing-directory path), matching MemoryProvider. + const created = realVfs.mkdirSync('/returned/nested/dir', { recursive: true }); + assert.strictEqual(created, '/returned'); + assert.strictEqual(realVfs.existsSync(created), true); + assert.strictEqual( + realVfs.mkdirSync('/returned/nested/dir', { recursive: true }), + undefined, + ); + assert.strictEqual(realVfs.mkdirSync('/returned/new-dir'), + undefined); + fs.rmdirSync(path.join(testDir, 'returned/nested/dir')); + fs.rmdirSync(path.join(testDir, 'returned/nested')); + fs.rmdirSync(path.join(testDir, 'returned/new-dir')); + fs.rmdirSync(path.join(testDir, 'returned')); +} + +// Mounted recursive mkdir returns a path that exists on the real filesystem. +{ + const realVfs = vfs.create(new vfs.RealFSProvider(testDir)); + const mountPoint = realVfs.mount(); + try { + const created = realVfs.mkdirSync(path.join(mountPoint, 'm/nested/dir'), + { recursive: true }); + assert.strictEqual(created, path.join(mountPoint, 'm')); + assert.strictEqual(fs.existsSync(created), true); + } finally { + realVfs.unmount(); + fs.rmSync(path.join(testDir, 'm'), { recursive: true, force: true }); + } +} + +// Recursive mkdir works when the root is given in the Windows long-path +// form, which is what the native bindings hand back: the returned path must +// still be translated relative to the root, not rejected as an escape. +if (common.isWindows) { + const longVfs = vfs.create(new vfs.RealFSProvider( + path.toNamespacedPath(testDir))); + const created = longVfs.mkdirSync('/long/deep/dir', { recursive: true }); + assert.strictEqual(created, '/long'); + assert.strictEqual(longVfs.existsSync('/long/deep/dir'), true); + fs.rmSync(path.join(testDir, 'long'), { recursive: true, force: true }); } // unlink