diff --git a/lib/internal/vfs/providers/memory.js b/lib/internal/vfs/providers/memory.js index acec5b4166b2..ce59a0611616 100644 --- a/lib/internal/vfs/providers/memory.js +++ b/lib/internal/vfs/providers/memory.js @@ -840,6 +840,10 @@ class MemoryProvider extends VirtualProvider { // Check if destination exists const existingDest = newParent.children.get(newName); + if (existingDest === entry) { + // Both names resolve to the same entry: rename does nothing + return; + } if (existingDest) { // Cannot overwrite a directory with a non-directory if (existingDest.isDirectory() && !entry.isDirectory()) { @@ -849,6 +853,15 @@ class MemoryProvider extends VirtualProvider { if (!existingDest.isDirectory() && entry.isDirectory()) { throw createENOTDIR('rename', newPath); } + if (existingDest.isDirectory()) { + this.#ensurePopulated(existingDest, normalizedNew); + // Cannot overwrite a non-empty directory + if (existingDest.children.size > 0) { + throw createENOTEMPTY('rename', newPath); + } + } else { + existingDest.nlink--; + } } // Remove from old location (after destination validation) diff --git a/test/parallel/test-vfs-memory-provider-dynamic.js b/test/parallel/test-vfs-memory-provider-dynamic.js index 9ddd105a5a55..13ed609820e7 100644 --- a/test/parallel/test-vfs-memory-provider-dynamic.js +++ b/test/parallel/test-vfs-memory-provider-dynamic.js @@ -46,35 +46,43 @@ function makeFileEntry(prototypeFrom, contentProvider) { return fileEntry; } -// ===== Lazy-populated directory ===== -{ - const provider = new MemoryProvider(); - const root = getRoot(provider); - - const dir = { - __proto__: Object.getPrototypeOf(root), +function makeDirEntry(prototypeFrom, populate) { + const t = Date.now(); + const dirEntry = { __proto__: Object.getPrototypeOf(prototypeFrom) }; + Object.assign(dirEntry, { 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'); - }, + populate, 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); + atime: t, + mtime: t, + ctime: t, + birthtime: t, + }); + dirEntry.isFile = prototypeFrom.isFile.bind(dirEntry); + dirEntry.isDirectory = prototypeFrom.isDirectory.bind(dirEntry); + dirEntry.isSymbolicLink = prototypeFrom.isSymbolicLink.bind(dirEntry); + dirEntry.isDynamic = prototypeFrom.isDynamic.bind(dirEntry); + dirEntry.getContentSync = prototypeFrom.getContentSync.bind(dirEntry); + dirEntry.getContentAsync = prototypeFrom.getContentAsync.bind(dirEntry); + return dirEntry; +} + +// ===== Lazy-populated directory ===== +{ + const provider = new MemoryProvider(); + const root = getRoot(provider); + + const dir = makeDirEntry(root, (scoped) => { + scoped.addFile('hello.txt', 'lazy hello'); + scoped.addFile('dyn.txt', () => 'dynamic-string'); + scoped.addDirectory('subdir', null); + scoped.addSymlink('link.txt', '/lazy/hello.txt'); + }); root.children.set('lazy', dir); const myVfs = vfs.create(provider); @@ -125,3 +133,19 @@ function makeFileEntry(prototypeFrom, contentProvider) { assert.strictEqual(s, 'async-only'); })); } + +// ===== Renaming over a lazy directory does not discard its entries ===== +{ + const provider = new MemoryProvider(); + const root = getRoot(provider); + root.children.set('lazy', makeDirEntry(root, (scoped) => { + scoped.addFile('keep.txt', 'keep'); + })); + + const myVfs = vfs.create(provider); + myVfs.mkdirSync('/src'); + + assert.throws(() => myVfs.renameSync('/src', '/lazy'), { code: 'ENOTEMPTY' }); + assert.strictEqual(myVfs.existsSync('/src'), true); + assert.strictEqual(myVfs.readFileSync('/lazy/keep.txt', 'utf8'), 'keep'); +} diff --git a/test/parallel/test-vfs-rename.js b/test/parallel/test-vfs-rename.js index 69daba43c11d..b1ca98dc684e 100644 --- a/test/parallel/test-vfs-rename.js +++ b/test/parallel/test-vfs-rename.js @@ -58,3 +58,67 @@ const vfs = require('node:vfs'); assert.strictEqual(myVfs.existsSync('/a/b/c'), false); assert.strictEqual(myVfs.readFileSync('/a/file.txt', 'utf8'), 'data'); } + +// Renaming a directory onto a non-empty directory throws ENOTEMPTY +{ + const myVfs = vfs.create(); + myVfs.mkdirSync('/src'); + myVfs.mkdirSync('/dst'); + myVfs.writeFileSync('/dst/keep.txt', 'keep'); + + assert.throws(() => myVfs.renameSync('/src', '/dst'), { code: 'ENOTEMPTY' }); + assert.strictEqual(myVfs.readFileSync('/dst/keep.txt', 'utf8'), 'keep'); + assert.strictEqual(myVfs.existsSync('/src'), true); +} + +// Renaming a directory onto an empty directory succeeds +{ + const myVfs = vfs.create(); + myVfs.mkdirSync('/src'); + myVfs.writeFileSync('/src/a.txt', 'a'); + myVfs.mkdirSync('/dst'); + + myVfs.renameSync('/src', '/dst'); + assert.strictEqual(myVfs.existsSync('/src'), false); + assert.strictEqual(myVfs.readFileSync('/dst/a.txt', 'utf8'), 'a'); +} + +// Overwriting a file drops one of its links +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/a.txt', 'a'); + myVfs.writeFileSync('/b.txt', 'b'); + myVfs.linkSync('/b.txt', '/b-link.txt'); + assert.strictEqual(myVfs.statSync('/b-link.txt').nlink, 2); + + myVfs.renameSync('/a.txt', '/b.txt'); + assert.strictEqual(myVfs.statSync('/b-link.txt').nlink, 1); + assert.strictEqual(myVfs.readFileSync('/b-link.txt', 'utf8'), 'b'); +} + +// Renaming a path onto itself is a no-op +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/a.txt', 'a'); + myVfs.mkdirSync('/d'); + myVfs.writeFileSync('/d/keep.txt', 'keep'); + + myVfs.renameSync('/a.txt', '/a.txt'); + assert.strictEqual(myVfs.readFileSync('/a.txt', 'utf8'), 'a'); + assert.strictEqual(myVfs.statSync('/a.txt').nlink, 1); + + myVfs.renameSync('/d', '/d'); + assert.strictEqual(myVfs.readFileSync('/d/keep.txt', 'utf8'), 'keep'); +} + +// Renaming a hard link onto another link to the same file is a no-op +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/a.txt', 'a'); + myVfs.linkSync('/a.txt', '/b.txt'); + + myVfs.renameSync('/a.txt', '/b.txt'); + assert.strictEqual(myVfs.existsSync('/a.txt'), true); + assert.strictEqual(myVfs.existsSync('/b.txt'), true); + assert.strictEqual(myVfs.statSync('/a.txt').nlink, 2); +}