Skip to content

Commit 1eb522c

Browse files
jazellyclaude
andcommitted
fs: keep lexical realpath semantics on Windows
Windows applies `..` lexically, both in the path being opened and in symlink targets, so `fs.realpathSync.native()` resolves `link\..` to the directory holding the link. Resolving the link first made the JS implementation disagree with the operating system there. Only keep `.` and `..` during the walk on POSIX, and normalize the input and link targets up front on Windows as before. Skip the new test on Windows, where `d -> c\..\d` points at itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jason Zhang <xzha4350@gmail.com>
1 parent d1c5735 commit 1eb522c

2 files changed

Lines changed: 30 additions & 49 deletions

File tree

‎lib/fs.js‎

Lines changed: 26 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3339,18 +3339,23 @@ if (isWindows) {
33393339
};
33403340
}
33413341

3342-
function toAbsolutePathPreservingDots(p) {
3343-
if (!pathModule.isAbsolute(p)) {
3344-
const root = splitRoot(p);
3345-
if (isWindows && root.length === 2) {
3346-
return `${pathModule.resolve(root)}${pathModule.sep}${StringPrototypeSlice(p, root.length)}`;
3347-
}
3348-
return `${pathModule.resolve()}${pathModule.sep}${p}`;
3349-
}
3350-
if (isWindows && splitRoot(p).length === 1) {
3351-
return `${pathModule.parse(pathModule.resolve()).root}${StringPrototypeSlice(p, 1)}`;
3352-
}
3353-
return p;
3342+
// realpath(3) applies `..` to the resolved parent, so on POSIX `.` and `..`
3343+
// are kept in the path while walking it. Windows applies `..` lexically, both
3344+
// in the path being opened and in symlink targets, so the path and the link
3345+
// targets are normalized up front there, matching `fs.realpathSync.native()`.
3346+
function toRealpathInput(p) {
3347+
if (isWindows) return pathModule.resolve(p);
3348+
return pathModule.isAbsolute(p) ? p : `${pathModule.resolve()}/${p}`;
3349+
}
3350+
3351+
function joinRealpathLink(previous, linkTarget) {
3352+
if (isWindows) return pathModule.resolve(previous, linkTarget);
3353+
return pathModule.isAbsolute(linkTarget) ? linkTarget : `${previous}${linkTarget}`;
3354+
}
3355+
3356+
function joinRealpathRemainder(resolvedLink, remainder) {
3357+
if (isWindows) return pathModule.resolve(resolvedLink, remainder);
3358+
return remainder.length === 0 ? resolvedLink : `${resolvedLink}/${remainder}`;
33543359
}
33553360

33563361
/**
@@ -3371,7 +3376,7 @@ function realpathSync(p, options) {
33713376
p += '';
33723377
}
33733378
validatePath(p);
3374-
p = toAbsolutePathPreservingDots(p);
3379+
p = toRealpathInput(p);
33753380

33763381
// Check the resolved input path up front so that the lstat walk below cannot
33773382
// be used to probe the existence of paths outside the granted set.
@@ -3501,30 +3506,18 @@ function realpathSync(p, options) {
35013506
isFileType(targetStats, S_IFSOCK);
35023507
linkTarget = binding.readlink(base, undefined);
35033508
}
3504-
const linkRoot = splitRoot(linkTarget);
3505-
if (pathModule.isAbsolute(linkTarget)) {
3506-
resolvedLink = isWindows && linkRoot.length === 1 ?
3507-
pathModule.resolve(previous, linkRoot) +
3508-
StringPrototypeSlice(linkTarget, linkRoot.length) :
3509-
linkTarget;
3510-
} else if (isWindows && linkRoot.length === 2) {
3511-
resolvedLink = pathModule.resolve(linkRoot) +
3512-
pathModule.sep + StringPrototypeSlice(linkTarget, linkRoot.length);
3513-
} else {
3514-
resolvedLink = `${previous}${linkTarget}`;
3515-
}
3509+
resolvedLink = joinRealpathLink(previous, linkTarget);
35163510

3517-
if (StringPrototypeIndexOf(linkTarget, '..') === -1) {
3511+
// On POSIX, a target containing `..` resolves relative to the path
3512+
// walked before it, so it cannot be cached by the link alone.
3513+
if (isWindows || StringPrototypeIndexOf(linkTarget, '..') === -1) {
35183514
cache?.set(base, pathModule.resolve(resolvedLink));
35193515
}
35203516
if (!isWindows) seenLinks.set(id, linkTarget);
35213517
}
35223518

35233519
// Resolve the link, then start over
3524-
const remainder = StringPrototypeSlice(p, pos);
3525-
p = remainder.length === 0 ?
3526-
resolvedLink :
3527-
`${resolvedLink}${pathModule.sep}${remainder}`;
3520+
p = joinRealpathRemainder(resolvedLink, StringPrototypeSlice(p, pos));
35283521

35293522
// Skip over roots
35303523
current = base = splitRoot(p);
@@ -3594,7 +3587,7 @@ function realpath(p, options, callback) {
35943587
p += '';
35953588
}
35963589
validatePath(p);
3597-
p = toAbsolutePathPreservingDots(p);
3590+
p = toRealpathInput(p);
35983591

35993592
const seenLinks = new SafeMap();
36003593
const knownHard = new SafeSet();
@@ -3712,28 +3705,12 @@ function realpath(p, options, callback) {
37123705
function gotTarget(err, target) {
37133706
if (err) return callback(err);
37143707

3715-
const linkRoot = splitRoot(target);
3716-
let resolvedLink;
3717-
if (pathModule.isAbsolute(target)) {
3718-
resolvedLink = isWindows && linkRoot.length === 1 ?
3719-
pathModule.resolve(previous, linkRoot) +
3720-
StringPrototypeSlice(target, linkRoot.length) :
3721-
target;
3722-
} else if (isWindows && linkRoot.length === 2) {
3723-
resolvedLink = pathModule.resolve(linkRoot) +
3724-
pathModule.sep + StringPrototypeSlice(target, linkRoot.length);
3725-
} else {
3726-
resolvedLink = `${previous}${target}`;
3727-
}
3728-
gotResolvedLink(resolvedLink);
3708+
gotResolvedLink(joinRealpathLink(previous, target));
37293709
}
37303710

37313711
function gotResolvedLink(resolvedLink) {
37323712
// Resolve the link, then start over
3733-
const remainder = StringPrototypeSlice(p, pos);
3734-
p = remainder.length === 0 ?
3735-
resolvedLink :
3736-
`${resolvedLink}${pathModule.sep}${remainder}`;
3713+
p = joinRealpathRemainder(resolvedLink, StringPrototypeSlice(p, pos));
37373714
current = base = splitRoot(p);
37383715
pos = current.length;
37393716

‎test/parallel/test-fs-realpath-symlink-dot-dot.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ if (process.argv[2] === 'child') {
1919
process.stdout.write(fs.realpathSync(process.argv[3]));
2020
}
2121
} else {
22+
// Windows applies `..` in symlink targets lexically, so `d -> c\..\d` points
23+
// at itself there and the walk fails with ELOOP, like the operating system.
24+
if (common.isWindows)
25+
common.skip('symlink targets are normalized lexically on Windows');
2226
if (!common.canCreateSymLink())
2327
common.skip('insufficient privileges');
2428

0 commit comments

Comments
 (0)