From c66bceffa2893e971a0ab3d8518fcf31494d141d Mon Sep 17 00:00:00 2001 From: Punisheroot <44579963+Punisheroot@users.noreply.github.com> Date: Sat, 26 Sep 2026 19:51:55 +0200 Subject: [PATCH 1/3] fix(path): handle lowercasing that changes path length in windows relative() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `relative()` lowercases both paths to compare them case-insensitively, then slices the original `to` path using indexes found in the lowercased strings. Some characters change length when lowercased ("İ" becomes "i̇", which is two code units), shifting those indexes and dropping leading characters from the result. Fall back to comparing path segments when lowercasing changes the length of either path, mirroring the fix merged for nodejs/node#53991. Regression cases are added to path/relative_test.ts. Fixes #7336 --- path/relative_test.ts | 13 +++++++++++++ path/windows/relative.ts | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/path/relative_test.ts b/path/relative_test.ts index 243ef08ce5f8..ca6a1f57f49b 100644 --- a/path/relative_test.ts +++ b/path/relative_test.ts @@ -34,6 +34,19 @@ 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"], ] as const, // arguments result posix: [ diff --git a/path/windows/relative.ts b/path/windows/relative.ts index 1f494ece736b..273772714cea 100644 --- a/path/windows/relative.ts +++ b/path/windows/relative.ts @@ -39,6 +39,45 @@ 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("\\"); + } + if (fromSegmentCount > maxSharedSegments) { + return "..\\".repeat(fromSegmentCount - 1 - sharedSegments) + ".."; + } + return ""; + } + + return "..\\".repeat(fromSegmentCount - sharedSegments) + + toSegments.slice(sharedSegments).join("\\"); + } + // Trim any leading backslashes let fromStart = 0; let fromEnd = from.length; From 5cd5a30327c6d4521a9680e20c1b3a7320c15008 Mon Sep 17 00:00:00 2001 From: Punisheroot <44579963+Punisheroot@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:41:23 +0200 Subject: [PATCH 2/3] test(path): cover Windows relative fallback branches --- path/relative_test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/path/relative_test.ts b/path/relative_test.ts index ca6a1f57f49b..3211dacb40dd 100644 --- a/path/relative_test.ts +++ b/path/relative_test.ts @@ -47,6 +47,9 @@ const relativeTests = { ["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: [ From c286abbb786fc9d8a441f8a2427a174941133ff0 Mon Sep 17 00:00:00 2001 From: Punisheroot <44579963+Punisheroot@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:59:54 +0200 Subject: [PATCH 3/3] refactor(path): simplify Windows relative fallback --- path/windows/relative.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/path/windows/relative.ts b/path/windows/relative.ts index 273772714cea..e6ee3dffdb91 100644 --- a/path/windows/relative.ts +++ b/path/windows/relative.ts @@ -68,10 +68,8 @@ export function relative(from: string, to: string): string { if (toSegmentCount > maxSharedSegments) { return toSegments.slice(sharedSegments).join("\\"); } - if (fromSegmentCount > maxSharedSegments) { - return "..\\".repeat(fromSegmentCount - 1 - sharedSegments) + ".."; - } - return ""; + // Equal-length paths with all segments shared would have returned above. + return "..\\".repeat(fromSegmentCount - 1 - sharedSegments) + ".."; } return "..\\".repeat(fromSegmentCount - sharedSegments) +