diff --git a/lib/path.js b/lib/path.js index 63b037cddfb986..6ed09da6957ca2 100644 --- a/lib/path.js +++ b/lib/path.js @@ -707,8 +707,17 @@ const win32 = { // We found a mismatch before the first common path separator was seen, so // return the original `to`. if (i !== length) { - if (lastCommonSep === -1) - return toOrig; + if (lastCommonSep === -1) { + // Two driveless rooted paths (e.g. `\\foo` and `\\bar`) share the + // current-drive root, so a relative path can still be produced even + // when their first segments differ. For any other roots (different + // drives, UNC vs. drive, etc.) no relative path exists. + if (fromStart === 1 && toStart === 1) { + lastCommonSep = 0; + } else { + return toOrig; + } + } } else { if (toLen > length) { if (StringPrototypeCharCodeAt(to, toStart + i) === @@ -717,9 +726,12 @@ const win32 = { // For example: from='C:\\foo\\bar'; to='C:\\foo\\bar\\baz' return StringPrototypeSlice(toOrig, toStart + i + 1); } - if (i === 2) { + if (i === 2 && fromStart === 0) { // We get here if `from` is the device root. // For example: from='C:\\'; to='C:\\foo' + // `fromStart === 0` ensures this only applies to device-rooted paths + // (e.g. `C:\\`) and not to driveless rooted paths (e.g. `\\foo`), + // where index 2 is regular path content rather than a device root. return StringPrototypeSlice(toOrig, toStart + i); } } @@ -729,9 +741,11 @@ const win32 = { // We get here if `to` is the exact base path for `from`. // For example: from='C:\\foo\\bar'; to='C:\\foo' lastCommonSep = i; - } else if (i === 2) { + } else if (i === 2 && toStart === 0) { // We get here if `to` is the device root. // For example: from='C:\\foo\\bar'; to='C:\\' + // `toStart === 0` ensures this only applies to device-rooted paths + // (e.g. `C:\\`) and not to driveless rooted paths (e.g. `\\foo`). lastCommonSep = 3; } } @@ -753,8 +767,17 @@ const win32 = { // Lastly, append the rest of the destination (`to`) path that comes after // the common path parts - if (out.length > 0) + if (out.length > 0) { + // The remaining `to` tail normally begins with the common path separator, + // which joins it to the `..` segments. For driveless rooted paths (e.g. + // `\\foo`) the leading separator was trimmed and `lastCommonSep` is the + // root, so insert the missing separator to avoid gluing `..` to the tail. + if (toStart < toEnd && + StringPrototypeCharCodeAt(toOrig, toStart) !== CHAR_BACKWARD_SLASH) { + return `${out}\\${StringPrototypeSlice(toOrig, toStart, toEnd)}`; + } return `${out}${StringPrototypeSlice(toOrig, toStart, toEnd)}`; + } if (StringPrototypeCharCodeAt(toOrig, toStart) === CHAR_BACKWARD_SLASH) ++toStart; diff --git a/test/parallel/test-path-relative.js b/test/parallel/test-path-relative.js index 999ef93784b523..7385711184f6ea 100644 --- a/test/parallel/test-path-relative.js +++ b/test/parallel/test-path-relative.js @@ -37,6 +37,23 @@ const relativeTests = [ ['c:\\İ\\a\\i̇', 'c:\\İ\\b\\İ\\test.txt', '..\\..\\b\\İ\\test.txt'], ['c:\\i̇\\a\\İ', 'c:\\İ\\b\\İ\\test.txt', '..\\..\\b\\İ\\test.txt'], ['c:\\ß\\a\\ß', 'c:\\ß\\b\\ß\\test.txt', '..\\..\\b\\ß\\test.txt'], + // Driveless rooted paths (absolute relative to the current drive). These + // are structurally equivalent to POSIX paths and must not glue the `..` + // segments to the destination tail or trip the device-root shortcuts. + // Refs: https://github.com/nodejs/node/issues/63600 + ['\\aaaa\\bbbb', '\\aaaa', '..'], + ['\\aaaa\\bbbb', '\\cccc', '..\\..\\cccc'], + ['\\aaaa\\bbbb', '\\aaaa\\bbbb', ''], + ['\\aaaa\\bbbb', '\\aaaa\\cccc', '..\\cccc'], + ['\\aaaa\\', '\\aaaa\\cccc', 'cccc'], + ['\\', '\\aaaa\\bbbb', 'aaaa\\bbbb'], + ['\\aa\\bb', '\\a', '..\\..\\a'], + ['\\aa', '\\a', '..\\a'], + ['\\abc\\d', '\\ab', '..\\..\\ab'], + ['\\ab', '\\abcd', '..\\abcd'], + ['\\foo\\bar', '\\f', '..\\..\\f'], + ['\\a', '\\a\\b\\c', 'b\\c'], + ['\\page1\\page2\\foo', '\\', '..\\..\\..'], ], ], [ path.posix.relative,