Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion lib/internal/vfs/providers/memory.js
Original file line number Diff line number Diff line change
Expand Up @@ -695,10 +695,13 @@ class MemoryProvider extends VirtualProvider {
const segments = this.#splitPath(normalized);
let current = this[kRoot];
let currentPath = '/';
let resolvedCurrentPath = '/';
let firstCreated;

for (const segment of segments) {
currentPath = pathPosix.join(currentPath, segment);
const resolvedPath = pathPosix.join(resolvedCurrentPath, segment);
this.#ensurePopulated(current, resolvedCurrentPath);
let entry = current.children.get(segment);
if (!entry) {
entry = new MemoryEntry(TYPE_DIR, { mode: options?.mode });
Expand All @@ -707,7 +710,23 @@ class MemoryProvider extends VirtualProvider {
if (firstCreated === undefined) {
firstCreated = currentPath;
}
} else if (!entry.isDirectory()) {
resolvedCurrentPath = resolvedPath;
} else if (entry.isSymbolicLink()) {
const targetPath = this.#resolveSymlinkTarget(resolvedPath, entry.target);
const result = this.#lookupEntry(targetPath, true, 0);
if (result.eloop) {
throw createELOOP('mkdir', path);
}
if (!result.entry) {
throw createENOENT('mkdir', path);
}
entry = result.entry;
resolvedCurrentPath = result.resolvedPath;
} else {
resolvedCurrentPath = resolvedPath;
}

if (!entry.isDirectory()) {
throw createENOTDIR('mkdir', path);
}
current = entry;
Expand Down
60 changes: 59 additions & 1 deletion test/parallel/test-vfs-mkdir.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// mkdirSync / rmdirSync behaviour: return value, recursive option, mode
// option, error cases.

require('../common');
const common = require('../common');
const assert = require('assert');
const vfs = require('node:vfs');

Expand All @@ -17,6 +17,51 @@ const vfs = require('node:vfs');
assert.strictEqual(result, '/a/b');
}

// Recursive mkdir follows symlinked intermediate directories, but returns the
// path of the first created directory as requested by the caller.
{
const myVfs = vfs.create();
myVfs.mkdirSync('/target');
myVfs.symlinkSync('/target', '/link');

const result = myVfs.mkdirSync('/link/subdir/deep', { recursive: true });

assert.strictEqual(result, '/link/subdir');
assert.strictEqual(myVfs.existsSync('/target/subdir/deep'), true);
assert.strictEqual(myVfs.existsSync('/link/subdir/deep'), true);
}

// Recursive mkdir also resolves relative symlink targets from the symlink's
// resolved parent directory.
{
const myVfs = vfs.create();
myVfs.mkdirSync('/parent/target', { recursive: true });
myVfs.symlinkSync('target', '/parent/link');

myVfs.mkdirSync('/parent/link/subdir', { recursive: true });

assert.strictEqual(myVfs.existsSync('/parent/target/subdir'), true);
}

// Recursive mkdir through symlinks keeps native error behavior for bad
// intermediate targets.
{
const myVfs = vfs.create();
myVfs.symlinkSync('/missing', '/dangling');
assert.throws(
() => myVfs.mkdirSync('/dangling/subdir', { recursive: true }),
{ code: 'ENOENT' });
}

{
const myVfs = vfs.create();
myVfs.writeFileSync('/file', 'x');
myVfs.symlinkSync('/file', '/link');
assert.throws(
() => myVfs.mkdirSync('/link/subdir', { recursive: true }),
{ code: 'ENOTDIR' });
}

// mkdirSync with explicit mode (non-recursive)
{
const myVfs = vfs.create();
Expand Down Expand Up @@ -47,3 +92,16 @@ const vfs = require('node:vfs');
myVfs.writeFileSync('/d/x', '');
assert.throws(() => myVfs.rmdirSync('/d'), { code: 'ENOTEMPTY' });
}

// promises.mkdir uses the same recursive symlink handling.
(async () => {
const myVfs = vfs.create();
myVfs.mkdirSync('/target');
myVfs.symlinkSync('/target', '/link');

const result = await myVfs.promises.mkdir('/link/subdir/deep',
{ recursive: true });

assert.strictEqual(result, '/link/subdir');
assert.strictEqual(myVfs.existsSync('/target/subdir/deep'), true);
})().then(common.mustCall());
Loading