Skip to content

Commit 7789cf3

Browse files
vfs: fix rename over a lazy destination directory
MemoryProvider#renameSync() read children.size on the destination directory without populating it first. A lazy directory keeps its entries in a populate callback until something reads them, so a lazy destination looked empty, passed the ENOTEMPTY check and was replaced along with everything it would have contained. #lookupEntry() populates only the directories it walks through, never the final entry, so the destination has to be populated explicitly. The regression test lives in test-vfs-memory-provider-dynamic.js because lazy directories have no public construction API and that file already builds the entries by hand; the directory scaffolding there is now shared through a makeDirEntry() helper.
1 parent 5eeb333 commit 7789cf3

2 files changed

Lines changed: 47 additions & 22 deletions

File tree

lib/internal/vfs/providers/memory.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,7 @@ class MemoryProvider extends VirtualProvider {
854854
throw createENOTDIR('rename', newPath);
855855
}
856856
if (existingDest.isDirectory()) {
857+
this.#ensurePopulated(existingDest, normalizedNew);
857858
// Cannot overwrite a non-empty directory
858859
if (existingDest.children.size > 0) {
859860
throw createENOTEMPTY('rename', newPath);

test/parallel/test-vfs-memory-provider-dynamic.js

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -46,35 +46,43 @@ function makeFileEntry(prototypeFrom, contentProvider) {
4646
return fileEntry;
4747
}
4848

49-
// ===== Lazy-populated directory =====
50-
{
51-
const provider = new MemoryProvider();
52-
const root = getRoot(provider);
53-
54-
const dir = {
55-
__proto__: Object.getPrototypeOf(root),
49+
function makeDirEntry(prototypeFrom, populate) {
50+
const t = Date.now();
51+
const dirEntry = { __proto__: Object.getPrototypeOf(prototypeFrom) };
52+
Object.assign(dirEntry, {
5653
type: 1, // TYPE_DIR
5754
mode: 0o755,
5855
children: new Map(),
59-
populate: (scoped) => {
60-
scoped.addFile('hello.txt', 'lazy hello');
61-
scoped.addFile('dyn.txt', () => 'dynamic-string');
62-
scoped.addDirectory('subdir', null);
63-
scoped.addSymlink('link.txt', '/lazy/hello.txt');
64-
},
56+
populate,
6557
populated: false,
6658
nlink: 1,
6759
uid: 0,
6860
gid: 0,
69-
};
70-
const t = Date.now();
71-
dir.atime = t; dir.mtime = t; dir.ctime = t; dir.birthtime = t;
72-
dir.isFile = root.isFile.bind(dir);
73-
dir.isDirectory = root.isDirectory.bind(dir);
74-
dir.isSymbolicLink = root.isSymbolicLink.bind(dir);
75-
dir.isDynamic = root.isDynamic.bind(dir);
76-
dir.getContentSync = root.getContentSync.bind(dir);
77-
dir.getContentAsync = root.getContentAsync.bind(dir);
61+
atime: t,
62+
mtime: t,
63+
ctime: t,
64+
birthtime: t,
65+
});
66+
dirEntry.isFile = prototypeFrom.isFile.bind(dirEntry);
67+
dirEntry.isDirectory = prototypeFrom.isDirectory.bind(dirEntry);
68+
dirEntry.isSymbolicLink = prototypeFrom.isSymbolicLink.bind(dirEntry);
69+
dirEntry.isDynamic = prototypeFrom.isDynamic.bind(dirEntry);
70+
dirEntry.getContentSync = prototypeFrom.getContentSync.bind(dirEntry);
71+
dirEntry.getContentAsync = prototypeFrom.getContentAsync.bind(dirEntry);
72+
return dirEntry;
73+
}
74+
75+
// ===== Lazy-populated directory =====
76+
{
77+
const provider = new MemoryProvider();
78+
const root = getRoot(provider);
79+
80+
const dir = makeDirEntry(root, (scoped) => {
81+
scoped.addFile('hello.txt', 'lazy hello');
82+
scoped.addFile('dyn.txt', () => 'dynamic-string');
83+
scoped.addDirectory('subdir', null);
84+
scoped.addSymlink('link.txt', '/lazy/hello.txt');
85+
});
7886
root.children.set('lazy', dir);
7987

8088
const myVfs = vfs.create(provider);
@@ -125,3 +133,19 @@ function makeFileEntry(prototypeFrom, contentProvider) {
125133
assert.strictEqual(s, 'async-only');
126134
}));
127135
}
136+
137+
// ===== Renaming over a lazy directory does not discard its entries =====
138+
{
139+
const provider = new MemoryProvider();
140+
const root = getRoot(provider);
141+
root.children.set('lazy', makeDirEntry(root, (scoped) => {
142+
scoped.addFile('keep.txt', 'keep');
143+
}));
144+
145+
const myVfs = vfs.create(provider);
146+
myVfs.mkdirSync('/src');
147+
148+
assert.throws(() => myVfs.renameSync('/src', '/lazy'), { code: 'ENOTEMPTY' });
149+
assert.strictEqual(myVfs.existsSync('/src'), true);
150+
assert.strictEqual(myVfs.readFileSync('/lazy/keep.txt', 'utf8'), 'keep');
151+
}

0 commit comments

Comments
 (0)