diff --git a/Semantics.Paths/Implementations/RelativeDirectoryPath.cs b/Semantics.Paths/Implementations/RelativeDirectoryPath.cs index 664b2d2..2ab0e32 100644 --- a/Semantics.Paths/Implementations/RelativeDirectoryPath.cs +++ b/Semantics.Paths/Implementations/RelativeDirectoryPath.cs @@ -2,11 +2,9 @@ namespace ktsu.Semantics.Paths; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; -#if !NET5_0_OR_GREATER -using System.Runtime.InteropServices; -#endif /// /// Represents a relative directory path @@ -209,18 +207,26 @@ public RelativeDirectoryPath AsRelative(AbsoluteDirectoryPath baseDirectory) return this; } - // Use Path.GetFullPath with a dummy base to normalize relative paths -#if NET5_0_OR_GREATER - string dummyBase = OperatingSystem.IsWindows() ? "C:\\" : "/"; -#else - string dummyBase = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "C:\\" : "/"; -#endif - string fullPath = Path.GetFullPath(Path.Combine(dummyBase, path)); -#if NETSTANDARD2_0 - string normalized = PathPolyfill.GetRelativePath(dummyBase, fullPath); -#else - string normalized = Path.GetRelativePath(dummyBase, fullPath); -#endif + // Resolve segments lexically. Resolving against a dummy root would discard every ".." that + // climbs above the starting point, changing which directory the path names. + List segments = []; + foreach (string segment in path.Split(['/', '\\'], StringSplitOptions.RemoveEmptyEntries)) + { + if (segment == ".") + { + continue; + } + + if (segment == ".." && segments.Count > 0 && segments[^1] != "..") + { + segments.RemoveAt(segments.Count - 1); + continue; + } + + segments.Add(segment); + } + + string normalized = segments.Count == 0 ? "." : string.Join(Path.DirectorySeparatorChar.ToString(), segments); return Create(normalized); } diff --git a/Semantics.Test/Paths/PathUtilityTests.cs b/Semantics.Test/Paths/PathUtilityTests.cs index 6829637..2382aaf 100644 --- a/Semantics.Test/Paths/PathUtilityTests.cs +++ b/Semantics.Test/Paths/PathUtilityTests.cs @@ -178,8 +178,22 @@ public void Normalize_WithOnlyDots_ResolvesCorrectly() RelativeDirectoryPath normalized = dotPath.Normalize(); - Assert.IsNotNull(normalized); - Assert.Contains("folder", normalized.WeakString); + Assert.AreEqual(TestPaths.Relative("..", "folder"), normalized.WeakString); + } + + [TestMethod] + [DataRow(new[] { "..", "sibling" }, new[] { "..", "sibling" })] + [DataRow(new[] { "a", "..", "..", "b" }, new[] { "..", "b" })] + [DataRow(new[] { "..", "..", "x", "y" }, new[] { "..", "..", "x", "y" })] + [DataRow(new[] { "a", ".", "b", "..", "c" }, new[] { "a", "c" })] + [DataRow(new[] { "a", ".." }, new[] { "." })] + public void Normalize_KeepsParentSegmentsThatClimbAboveTheStart(string[] input, string[] expected) + { + RelativeDirectoryPath path = RelativeDirectoryPath.Create(TestPaths.Relative(input)); + + RelativeDirectoryPath normalized = path.Normalize(); + + Assert.AreEqual(TestPaths.Relative(expected), normalized.WeakString); } [TestMethod]