diff --git a/path/relative_test.ts b/path/relative_test.ts index 243ef08ce5f8..3211dacb40dd 100644 --- a/path/relative_test.ts +++ b/path/relative_test.ts @@ -34,6 +34,22 @@ const relativeTests = { ["\\\\foo\\baz", "\\\\foo\\baz-quux", "..\\baz-quux"], ["C:\\baz", "\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz"], ["\\\\foo\\bar\\baz", "C:\\baz", "C:\\baz"], + // Lowercasing can change a path's length ("İ" becomes "i̇"), see + // https://github.com/denoland/std/issues/7336 + [ + "C:\\İPTV\\player", + "C:\\İPTV\\player\\node_modules\\zod\\index.ts", + "node_modules\\zod\\index.ts", + ], + ["C:\\İPTV\\player", "C:\\İPTV\\player\\src\\main.ts", "src\\main.ts"], + ["C:\\İ\\İ\\a", "C:\\İ\\İ\\a\\bcd.ts", "bcd.ts"], + ["c:\\a\\İ", "c:\\a\\İ\\test.txt", "test.txt"], + ["c:\\İ\\a\\İ", "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"], + ["C:\\", "C:\\İPTV", "İPTV"], + ["C:\\İPTV\\player", "C:\\", "..\\.."], + ["C:\\İPTV\\player", "D:\\other\\target", "D:\\other\\target"], ] as const, // arguments result posix: [ diff --git a/path/windows/relative.ts b/path/windows/relative.ts index 1f494ece736b..e6ee3dffdb91 100644 --- a/path/windows/relative.ts +++ b/path/windows/relative.ts @@ -39,6 +39,43 @@ export function relative(from: string, to: string): string { if (from === to) return ""; + // Lowercasing can change the length of a path ("İ" becomes "i̇", which is + // two code units), so indexes found in the lowercased strings don't line up + // with the original paths. Fall back to comparing path segments, as Node.js + // does in https://github.com/nodejs/node/pull/53991 + if (fromOrig.length !== from.length || toOrig.length !== to.length) { + const fromSegments = fromOrig.split("\\"); + const toSegments = toOrig.split("\\"); + if (fromSegments[fromSegments.length - 1] === "") fromSegments.pop(); + if (toSegments[toSegments.length - 1] === "") toSegments.pop(); + + const fromSegmentCount = fromSegments.length; + const toSegmentCount = toSegments.length; + const maxSharedSegments = fromSegmentCount < toSegmentCount + ? fromSegmentCount + : toSegmentCount; + + let sharedSegments = 0; + for (; sharedSegments < maxSharedSegments; sharedSegments++) { + const fromSegment = fromSegments[sharedSegments]!; + const toSegment = toSegments[sharedSegments]!; + if (fromSegment.toLowerCase() !== toSegment.toLowerCase()) break; + } + + if (sharedSegments === 0) { + return toOrig; + } else if (sharedSegments === maxSharedSegments) { + if (toSegmentCount > maxSharedSegments) { + return toSegments.slice(sharedSegments).join("\\"); + } + // Equal-length paths with all segments shared would have returned above. + return "..\\".repeat(fromSegmentCount - 1 - sharedSegments) + ".."; + } + + return "..\\".repeat(fromSegmentCount - sharedSegments) + + toSegments.slice(sharedSegments).join("\\"); + } + // Trim any leading backslashes let fromStart = 0; let fromEnd = from.length;