diff --git a/TruePath.Tests/LocalPathTests.cs b/TruePath.Tests/LocalPathTests.cs index 458ea5f..3a6ace4 100644 --- a/TruePath.Tests/LocalPathTests.cs +++ b/TruePath.Tests/LocalPathTests.cs @@ -6,6 +6,46 @@ namespace TruePath.Tests; public class LocalPathTests { + [Fact] + public void AnyExclusivelyRelativePath() + { + // Arrange + var dots = new string(Enumerable.Repeat('.', Random.Shared.Next(1, 21)).ToArray()); + var backslashes = new string(Enumerable.Repeat(Path.DirectorySeparatorChar, Random.Shared.Next(0, 20)).ToArray()); + var result = string.Concat(dots.AsSpan(), backslashes.AsSpan()).ToArray(); + Random.Shared.Shuffle(result.ToArray()); + var path = new string(result); + + var a = new LocalPath(path); + + // Act + var parent = a.Parent; + + // Assert + Assert.Null(parent); + } + + [Theory] + [InlineData(".")] + [InlineData("..")] + [InlineData("../..")] + [InlineData("../../")] + [InlineData("../...")] + [InlineData(".../..")] + [InlineData("./.")] + [InlineData("../../.")] + public void ExclusivelyRelativePath(string path) + { + // Arrange + var a = new LocalPath(path); + + // Act + var parent = a.Parent; + + // Assert + Assert.Null(parent); + } + [Theory] [InlineData("user", "user/documents")] [InlineData("usEr", "User/documents")] @@ -14,6 +54,8 @@ public class LocalPathTests public void IsPrefixOfShouldBeEquivalentToStartsWith(string pathA, string pathB) { // Arrange + var y = PathStrings.Normalize("../.."); + var a = new LocalPath(pathA); var b = new LocalPath(pathB); diff --git a/TruePath/LocalPath.cs b/TruePath/LocalPath.cs index 9cb3522..adf53c4 100644 --- a/TruePath/LocalPath.cs +++ b/TruePath/LocalPath.cs @@ -27,7 +27,18 @@ public readonly struct LocalPath(string value) : IEquatable, IPath, I public bool IsAbsolute => Path.IsPathRooted(Value); /// - public LocalPath? Parent => Path.GetDirectoryName(Value) is { } parent ? new(parent) : null; + public LocalPath? Parent { + get + { + var value = PathStrings.ResolveRelativePaths(Value); + if (string.IsNullOrWhiteSpace(value)) + { + return null; + } + + return Path.GetDirectoryName(Value) is { } parent ? new(parent) : null; + } + } /// IPath? IPath.Parent => Parent; diff --git a/TruePath/PathStrings.cs b/TruePath/PathStrings.cs index 2724942..2b0303b 100644 --- a/TruePath/PathStrings.cs +++ b/TruePath/PathStrings.cs @@ -182,4 +182,34 @@ private static bool SourceContainsDriveLetter(ReadOnlySpan source) return DriveLetters.Contains(letter) && colon == ':'; } + + /// + /// Resolves and normalizes relative paths in the given path string by removing redundant path components. + /// + /// The path string to process. + /// + /// The normalized path string. If the source path contains only directory separators or dots, an empty string is returned. Otherwise, the original path is returned. + /// + /// + /// This method iterates over the characters in the input path string and removes those that are directory separators or dots. If the resulting length of the path is zero, indicating that the path only contained separators or dots, it returns an empty string. Otherwise, it returns the original path unchanged. + /// + internal static string ResolveRelativePaths(string source) + { + if (string.IsNullOrWhiteSpace(source)) + { + return string.Empty; + } + + var c = source.Length; + + foreach (var @char in source) + { + if (@char == Path.DirectorySeparatorChar || @char == '.') + { + c--; + } + } + + return c == 0 ? string.Empty : source; + } }