Skip to content

Commit 9acdfcd

Browse files
committed
path: fix unicode path problems in path.relative
This commit changes the way two paths are compared in path.relative: Instead of comparing each char code in path strings one by one, which causes problems when the number of char codes in lowercased path string does not match the original one (e.g. path contains certain Unicode characters like 'İ'), it now splits the path string by backslash and compares the parts instead. Fixes: #27534
1 parent 6bc871f commit 9acdfcd

File tree

2 files changed

+45
-62
lines changed

2 files changed

+45
-62
lines changed

lib/path.js

Lines changed: 42 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -464,86 +464,67 @@ const win32 = {
464464
from.charCodeAt(fromEnd - 1) === CHAR_BACKWARD_SLASH) {
465465
fromEnd--;
466466
}
467-
const fromLen = fromEnd - fromStart;
467+
if (fromStart > 0 || fromEnd < from.length)
468+
from = from.slice(fromStart, fromEnd);
468469

469470
// Trim any leading backslashes
471+
const toLenBeforeTrim = to.length;
470472
let toStart = 0;
471-
while (toStart < to.length &&
473+
while (toStart < toLenBeforeTrim &&
472474
to.charCodeAt(toStart) === CHAR_BACKWARD_SLASH) {
473475
toStart++;
474476
}
475477
// Trim trailing backslashes (applicable to UNC paths only)
476-
let toEnd = to.length;
478+
let toEnd = toLenBeforeTrim;
477479
while (toEnd - 1 > toStart &&
478480
to.charCodeAt(toEnd - 1) === CHAR_BACKWARD_SLASH) {
479481
toEnd--;
480482
}
481-
const toLen = toEnd - toStart;
483+
if (toStart > 0 || toEnd < toLenBeforeTrim)
484+
to = to.slice(toStart, toEnd);
485+
486+
// Even the device roots are different. Return the original `to`.
487+
if (from.charCodeAt(0) !== to.charCodeAt(0))
488+
return toOrig;
489+
490+
// Split paths
491+
const fromParts = from.split('\\');
492+
const toParts = to.split('\\');
482493

483-
// Compare paths to find the longest common path from root
484-
const length = fromLen < toLen ? fromLen : toLen;
485-
let lastCommonSep = -1;
486494
let i = 0;
487-
for (; i < length; i++) {
488-
const fromCode = from.charCodeAt(fromStart + i);
489-
if (fromCode !== to.charCodeAt(toStart + i))
490-
break;
491-
else if (fromCode === CHAR_BACKWARD_SLASH)
492-
lastCommonSep = i;
493-
}
495+
const partLen = fromParts.length < toParts.length ?
496+
fromParts.length :
497+
toParts.length;
494498

495-
// We found a mismatch before the first common path separator was seen, so
496-
// return the original `to`.
497-
if (i !== length) {
498-
if (lastCommonSep === -1)
499-
return toOrig;
500-
} else {
501-
if (toLen > length) {
502-
if (to.charCodeAt(toStart + i) === CHAR_BACKWARD_SLASH) {
503-
// We get here if `from` is the exact base path for `to`.
504-
// For example: from='C:\\foo\\bar'; to='C:\\foo\\bar\\baz'
505-
return toOrig.slice(toStart + i + 1);
506-
}
507-
if (i === 2) {
508-
// We get here if `from` is the device root.
509-
// For example: from='C:\\'; to='C:\\foo'
510-
return toOrig.slice(toStart + i);
511-
}
512-
}
513-
if (fromLen > length) {
514-
if (from.charCodeAt(fromStart + i) === CHAR_BACKWARD_SLASH) {
515-
// We get here if `to` is the exact base path for `from`.
516-
// For example: from='C:\\foo\\bar'; to='C:\\foo'
517-
lastCommonSep = i;
518-
} else if (i === 2) {
519-
// We get here if `to` is the device root.
520-
// For example: from='C:\\foo\\bar'; to='C:\\'
521-
lastCommonSep = 3;
522-
}
523-
}
524-
if (lastCommonSep === -1)
525-
lastCommonSep = 0;
526-
}
499+
// Find the number of common ancestors
500+
while (i < partLen && fromParts[i] === toParts[i])
501+
i++;
527502

503+
const numOfCommonAncestors = i;
504+
let numOfStepsToWalkUp = fromParts.length - numOfCommonAncestors;
528505
let out = '';
529-
// Generate the relative path based on the path difference between `to` and
530-
// `from`
531-
for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) {
532-
if (i === fromEnd || from.charCodeAt(i) === CHAR_BACKWARD_SLASH) {
533-
out += out.length === 0 ? '..' : '\\..';
534-
}
535-
}
536506

537-
toStart += lastCommonSep;
507+
// Walk up the directory tree to get the nearest common ancestor (if any)
508+
// and then append the "different" parts (if any)
509+
while (numOfStepsToWalkUp-- > 0)
510+
out += out.length === 0 ? '..' : '\\..';
538511

539-
// Lastly, append the rest of the destination (`to`) path that comes after
540-
// the common path parts
541-
if (out.length > 0)
542-
return `${out}${toOrig.slice(toStart, toEnd)}`;
512+
let partIx = 0;
513+
514+
// Find the starting position of the remaining part
515+
const toOrigEnd = toOrig.length - toLenBeforeTrim + toEnd;
516+
for (i = toStart; partIx < numOfCommonAncestors && i <= toOrigEnd; i++)
517+
if (toOrig.charCodeAt(i) === CHAR_BACKWARD_SLASH)
518+
partIx++;
519+
520+
if (partIx > 0 && i <= toOrigEnd) {
521+
if (out.length > 0)
522+
out += '\\';
523+
524+
out += toOrig.slice(i, toOrigEnd);
525+
}
543526

544-
if (toOrig.charCodeAt(toStart) === CHAR_BACKWARD_SLASH)
545-
++toStart;
546-
return toOrig.slice(toStart, toEnd);
527+
return out;
547528
},
548529

549530
toNamespacedPath(path) {

test/parallel/test-path-relative.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ const relativeTests = [
3131
['\\\\foo\\baz-quux', '\\\\foo\\baz', '..\\baz'],
3232
['\\\\foo\\baz', '\\\\foo\\baz-quux', '..\\baz-quux'],
3333
['C:\\baz', '\\\\foo\\bar\\baz', '\\\\foo\\bar\\baz'],
34-
['\\\\foo\\bar\\baz', 'C:\\baz', 'C:\\baz']
34+
['\\\\foo\\bar\\baz', 'C:\\baz', 'C:\\baz'],
35+
['c:\\a\\İ', 'c:\\a\\İ\\test.txt', 'test.txt'],
36+
['c:\\İ\\a\\İ', 'c:\\İ\\b\\İ\\test.txt', '..\\..\\b\\İ\\test.txt']
3537
]
3638
],
3739
[ path.posix.relative,

0 commit comments

Comments
 (0)