Skip to content

Commit

Permalink
path: fix unicode path problems in path.relative
Browse files Browse the repository at this point in the history
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: nodejs#27534
  • Loading branch information
monoblaine committed Jul 20, 2024
1 parent a407d1f commit 1308f84
Show file tree
Hide file tree
Showing 2 changed files with 45 additions and 64 deletions.
107 changes: 43 additions & 64 deletions lib/path.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const {
StringPrototypeLastIndexOf,
StringPrototypeReplace,
StringPrototypeSlice,
StringPrototypeSplit,
StringPrototypeToLowerCase,
} = primordials;

Expand Down Expand Up @@ -530,89 +531,67 @@ const win32 = {
) {
fromEnd--;
}
const fromLen = fromEnd - fromStart;
if (fromStart > 0 || fromEnd < from.length)
from = StringPrototypeSlice(from, fromStart, fromEnd);

// Trim any leading backslashes
const toLenBeforeTrim = to.length;
let toStart = 0;
while (toStart < to.length &&
while (toStart < toLenBeforeTrim &&
StringPrototypeCharCodeAt(to, toStart) === CHAR_BACKWARD_SLASH) {
toStart++;
}
// Trim trailing backslashes (applicable to UNC paths only)
let toEnd = to.length;
let toEnd = toLenBeforeTrim;
while (toEnd - 1 > toStart &&
StringPrototypeCharCodeAt(to, toEnd - 1) === CHAR_BACKWARD_SLASH) {
toEnd--;
}
const toLen = toEnd - toStart;
if (toStart > 0 || toEnd < toLenBeforeTrim)
to = StringPrototypeSlice(to, toStart, toEnd);

// Even the device roots are different. Return the original `to`.
if (StringPrototypeCharCodeAt(from, 0) !== StringPrototypeCharCodeAt(to, 0))
return toOrig;

// Split paths
const fromParts = StringPrototypeSplit(from, '\\');
const toParts = StringPrototypeSplit(to, '\\');

// Compare paths to find the longest common path from root
const length = fromLen < toLen ? fromLen : toLen;
let lastCommonSep = -1;
let i = 0;
for (; i < length; i++) {
const fromCode = StringPrototypeCharCodeAt(from, fromStart + i);
if (fromCode !== StringPrototypeCharCodeAt(to, toStart + i))
break;
else if (fromCode === CHAR_BACKWARD_SLASH)
lastCommonSep = i;
}
const partLen = fromParts.length < toParts.length ?
fromParts.length :
toParts.length;

// 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;
} else {
if (toLen > length) {
if (StringPrototypeCharCodeAt(to, toStart + i) ===
CHAR_BACKWARD_SLASH) {
// We get here if `from` is the exact base path for `to`.
// For example: from='C:\\foo\\bar'; to='C:\\foo\\bar\\baz'
return StringPrototypeSlice(toOrig, toStart + i + 1);
}
if (i === 2) {
// We get here if `from` is the device root.
// For example: from='C:\\'; to='C:\\foo'
return StringPrototypeSlice(toOrig, toStart + i);
}
}
if (fromLen > length) {
if (StringPrototypeCharCodeAt(from, fromStart + i) ===
CHAR_BACKWARD_SLASH) {
// 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) {
// We get here if `to` is the device root.
// For example: from='C:\\foo\\bar'; to='C:\\'
lastCommonSep = 3;
}
}
if (lastCommonSep === -1)
lastCommonSep = 0;
}
// Find the number of common ancestors
while (i < partLen && fromParts[i] === toParts[i])
i++;

const numOfCommonAncestors = i;
let numOfStepsToWalkUp = fromParts.length - numOfCommonAncestors;
let out = '';
// Generate the relative path based on the path difference between `to` and
// `from`
for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) {
if (i === fromEnd ||
StringPrototypeCharCodeAt(from, i) === CHAR_BACKWARD_SLASH) {
out += out.length === 0 ? '..' : '\\..';
}
}

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

// Lastly, append the rest of the destination (`to`) path that comes after
// the common path parts
if (out.length > 0)
return `${out}${StringPrototypeSlice(toOrig, toStart, toEnd)}`;
let partIx = 0;

// Find the starting position of the remaining part
const toOrigEnd = toOrig.length - toLenBeforeTrim + toEnd;
for (i = toStart; partIx < numOfCommonAncestors && i <= toOrigEnd; i++)
if (StringPrototypeCharCodeAt(toOrig, i) === CHAR_BACKWARD_SLASH)
partIx++;

if (partIx > 0 && i <= toOrigEnd) {
if (out.length > 0)
out += '\\';

out += StringPrototypeSlice(toOrig, i, toOrigEnd);
}

if (StringPrototypeCharCodeAt(toOrig, toStart) === CHAR_BACKWARD_SLASH)
++toStart;
return StringPrototypeSlice(toOrig, toStart, toEnd);
return out;
},

/**
Expand Down
2 changes: 2 additions & 0 deletions test/parallel/test-path-relative.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ const relativeTests = [
['\\\\foo\\baz', '\\\\foo\\baz-quux', '..\\baz-quux'],
['C:\\baz', '\\\\foo\\bar\\baz', '\\\\foo\\bar\\baz'],
['\\\\foo\\bar\\baz', 'C:\\baz', 'C:\\baz'],
['c:\\a\\İ', 'c:\\a\\İ\\test.txt', 'test.txt'],
['c:\\İ\\a\\İ', 'c:\\İ\\b\\İ\\test.txt', '..\\..\\b\\İ\\test.txt'],
],
],
[ path.posix.relative,
Expand Down

0 comments on commit 1308f84

Please sign in to comment.