diff --git a/lib/internal/vfs/providers/memory.js b/lib/internal/vfs/providers/memory.js index 995b8f236200d5..b306c774cdfab7 100644 --- a/lib/internal/vfs/providers/memory.js +++ b/lib/internal/vfs/providers/memory.js @@ -7,6 +7,7 @@ const { SafeMap, SafeSet, StringPrototypeReplaceAll, + StringPrototypeStartsWith, Symbol, } = primordials; @@ -794,6 +795,11 @@ class MemoryProvider extends VirtualProvider { // Get the entry (without following symlinks for the entry itself) const entry = this.#getEntry(normalizedOld, 'rename', false); + if (entry.isDirectory() && + StringPrototypeStartsWith(normalizedNew, `${normalizedOld}/`)) { + throw createEINVAL('rename', oldPath); + } + // Validate destination parent exists (do not auto-create) const newParent = this.#ensureParent(normalizedNew, false, 'rename'); const newName = pathPosix.basename(normalizedNew); diff --git a/test/parallel/test-vfs-rename.js b/test/parallel/test-vfs-rename.js index 26506f01dd7338..69daba43c11ddf 100644 --- a/test/parallel/test-vfs-rename.js +++ b/test/parallel/test-vfs-rename.js @@ -44,3 +44,17 @@ const vfs = require('node:vfs'); assert.strictEqual(myVfs.existsSync('/d/a.txt'), false); assert.strictEqual(myVfs.existsSync('/d/b.txt'), true); } + +// Renaming a directory into its own descendant throws EINVAL +{ + const myVfs = vfs.create(); + myVfs.mkdirSync('/a/b', { recursive: true }); + myVfs.writeFileSync('/a/file.txt', 'data'); + + assert.throws(() => myVfs.renameSync('/a', '/a/b/c'), { code: 'EINVAL' }); + assert.deepStrictEqual(myVfs.readdirSync('/'), ['a']); + assert.strictEqual(myVfs.existsSync('/a'), true); + assert.strictEqual(myVfs.existsSync('/a/b'), true); + assert.strictEqual(myVfs.existsSync('/a/b/c'), false); + assert.strictEqual(myVfs.readFileSync('/a/file.txt', 'utf8'), 'data'); +}