diff --git a/src/libraries/Common/tests/Tests/System/IO/PathInternal.Tests.cs b/src/libraries/Common/tests/Tests/System/IO/PathInternal.Tests.cs index 7bfa141a5c58c6..d31d6906fc9c65 100644 --- a/src/libraries/Common/tests/Tests/System/IO/PathInternal.Tests.cs +++ b/src/libraries/Common/tests/Tests/System/IO/PathInternal.Tests.cs @@ -64,345 +64,5 @@ public void GetCommonPathLength(string first, string second, bool ignoreCase, in { Assert.Equal(expected, PathInternal.GetCommonPathLength(first, second, ignoreCase)); } - - public static TheoryData RemoveRelativeSegmentsData => new TheoryData - { - { @"C:\git\runtime", 2, @"C:\git\runtime"}, - { @"C:\\git\runtime", 2, @"C:\git\runtime"}, - { @"C:\git\\runtime", 2, @"C:\git\runtime"}, - { @"C:\git\.\runtime\.\\", 2, @"C:\git\runtime\"}, - { @"C:\git\runtime", 2, @"C:\git\runtime"}, - { @"C:\git\..\runtime", 2, @"C:\runtime"}, - { @"C:\git\runtime\..\", 2, @"C:\git\"}, - { @"C:\git\runtime\..\..\..\", 2, @"C:\"}, - { @"C:\git\runtime\..\..\.\", 2, @"C:\"}, - { @"C:\git\..\.\runtime\temp\..", 2, @"C:\runtime"}, - { @"C:\git\..\\\.\..\runtime", 2, @"C:\runtime"}, - { @"C:\git\runtime\", 2, @"C:\git\runtime\"}, - { @"C:\git\temp\..\runtime\", 2, @"C:\git\runtime\"}, - - { @"C:\.", 3, @"C:\"}, - { @"C:\..", 3, @"C:\"}, - { @"C:\..\..", 3, @"C:\"}, - { @"C:\.", 2, @"C:"}, - { @"C:\..", 2, @"C:"}, - { @"C:\..\..", 2, @"C:"}, - { @"C:A\.", 2, @"C:A"}, - { @"C:A\..", 2, @"C:"}, - { @"C:A\..\..", 2, @"C:"}, - { @"C:A\..\..\..", 2, @"C:"}, - - { @"C:\tmp\home", 3, @"C:\tmp\home" }, - { @"C:\tmp\..", 3, @"C:\" }, - { @"C:\tmp\home\..\.\.\", 3, @"C:\tmp\" }, - { @"C:\tmp\..\..\..\", 3, @"C:\" }, - { @"C:\tmp\\home", 3, @"C:\tmp\home" }, - { @"C:\.\tmp\\home", 3, @"C:\tmp\home" }, - { @"C:\..\tmp\home", 3, @"C:\tmp\home" }, - { @"C:\..\..\..\tmp\.\home", 3, @"C:\tmp\home" }, - { @"C:\\tmp\\\home", 3, @"C:\tmp\home" }, - { @"C:\tmp\home\git\.\..\.\git\runtime\..\", 3, @"C:\tmp\home\git\" }, - { @"C:\.\tmp\home", 3, @"C:\tmp\home" }, - - { @"C:\tmp\home", 6, @"C:\tmp\home" }, - { @"C:\tmp\..", 6, @"C:\tmp" }, - { @"C:\tmp\home\..\.\.\", 5, @"C:\tmp\" }, - { @"C:\tmp\..\..\..\", 6, @"C:\tmp\" }, - { @"C:\tmp\\home", 5, @"C:\tmp\home" }, - { @"C:\.\tmp\\home", 4, @"C:\.\tmp\home" }, - { @"C:\..\tmp\home", 5, @"C:\..\tmp\home" }, - { @"C:\..\..\..\tmp\.\home", 6, @"C:\..\tmp\home" }, - { @"C:\\tmp\\\home", 7, @"C:\\tmp\home" }, - { @"C:\tmp\home\git\.\..\.\git\runtime\..\", 7, @"C:\tmp\home\git\" }, - { @"C:\.\tmp\home", 5, @"C:\.\tmp\home" }, - - { @"C:\tmp\..", 2, @"C:\" }, - { @"C:\tmp\home\..\..\.\", 2, @"C:\" }, - { @"C:\tmp\..\..\..\", 2, @"C:\" }, - { @"C:\tmp\\home", 2, @"C:\tmp\home" }, - { @"C:\.\tmp\\home", 2, @"C:\tmp\home" }, - { @"C:\..\tmp\home", 2, @"C:\tmp\home" }, - { @"C:\..\..\..\tmp\.\home", 2, @"C:\tmp\home" }, - { @"C:\\tmp\\\home", 2, @"C:\tmp\home" }, - { @"C:\tmp\home\git\.\..\.\git\runtime\..\", 2, @"C:\tmp\home\git\" }, - { @"C:\.\tmp\home", 2, @"C:\tmp\home" }, - - { @"C:\tmp\..\..\", 10, @"C:\tmp\..\" }, - { @"C:\tmp\home\..\.\.\", 12, @"C:\tmp\home\" }, - { @"C:\tmp\..\..\..\", 10, @"C:\tmp\..\" }, - { @"C:\tmp\\home\..\.\\", 13, @"C:\tmp\\home\" }, - { @"C:\.\tmp\\home\git\git", 9, @"C:\.\tmp\home\git\git" }, - { @"C:\..\tmp\.\home", 10, @"C:\..\tmp\home" }, - { @"C:\..\..\..\tmp\.\home", 10, @"C:\..\..\..\tmp\home" }, - { @"C:\\tmp\\\home\..", 7, @"C:\\tmp\" }, - { @"C:\tmp\home\git\.\..\.\git\runtime\..\", 18, @"C:\tmp\home\git\.\git\" }, - { @"C:\.\tmp\home\.\.\", 9, @"C:\.\tmp\home\" }, - }; - - public static TheoryData RemoveRelativeSegmentsFirstRelativeSegment => new TheoryData - { - { @"C:\\git\runtime", 2, @"C:\git\runtime"}, - { @"C:\.\git\runtime", 2, @"C:\git\runtime"}, - { @"C:\\.\git\.\runtime", 2, @"C:\git\runtime"}, - { @"C:\..\git\runtime", 2, @"C:\git\runtime"}, - { @"C:\.\git\..\runtime", 2, @"C:\runtime"}, - { @"C:\.\git\runtime\..\", 2, @"C:\git\"}, - { @"C:\.\git\runtime\..\..\..\", 2, @"C:\"}, - { @"C:\.\git\runtime\..\..\.\", 2, @"C:\"}, - { @"C:\.\git\..\.\runtime\temp\..", 2, @"C:\runtime"}, - { @"C:\.\git\..\\\.\..\runtime", 2, @"C:\runtime"}, - { @"C:\.\git\runtime\", 2, @"C:\git\runtime\"}, - { @"C:\.\git\temp\..\runtime\", 2, @"C:\git\runtime\"}, - { @"C:\\..\..", 3, @"C:\"} - }; - - public static TheoryData RemoveRelativeSegmentsSkipAboveRoot => new TheoryData - { - { @"C:\temp\..\" , 7, @"C:\temp\" }, - { @"C:\temp\..\git" , 7, @"C:\temp\git" }, - { @"C:\temp\..\git" , 8, @"C:\temp\git" }, - { @"C:\temp\..\.\" , 8, @"C:\temp\" }, - { @"C:\temp\..\" , 9, @"C:\temp\..\" }, - { @"C:\temp\..\git" , 9, @"C:\temp\..\git" }, - { @"C:\git\..\temp\..\" , 15, @"C:\git\..\temp\" }, - { @"C:\\\.\..\..\temp\..\" , 17, @"C:\\\.\..\..\temp\" }, - }; - - public static TheoryData RemoveRelativeSegmentsFirstRelativeSegmentRoot => new TheoryData - { - { @"C:\\git\runtime", 3, @"C:\git\runtime"}, - { @"C:\.\git\runtime", 3, @"C:\git\runtime"}, - { @"C:\\.\git\.\runtime", 3, @"C:\git\runtime"}, - { @"C:\..\git\runtime", 3, @"C:\git\runtime"}, - { @"C:\.\git\..\runtime", 3, @"C:\runtime"}, - { @"C:\.\git\runtime\..\", 3, @"C:\git\"}, - { @"C:\.\git\runtime\..\..\..\", 3, @"C:\"}, - { @"C:\.\git\runtime\..\..\.\", 3, @"C:\"}, - { @"C:\.\git\..\.\runtime\temp\..", 3, @"C:\runtime"}, - { @"C:\.\git\..\\\.\..\runtime", 3, @"C:\runtime"}, - { @"C:\.\git\runtime\", 3, @"C:\git\runtime\"}, - { @"C:\.\git\temp\..\runtime\", 3, @"C:\git\runtime\"}, - }; - - [Theory, - MemberData(nameof(RemoveRelativeSegmentsData)), - MemberData(nameof(RemoveRelativeSegmentsFirstRelativeSegment)), - MemberData(nameof(RemoveRelativeSegmentsFirstRelativeSegmentRoot)), - MemberData(nameof(RemoveRelativeSegmentsSkipAboveRoot))] - [PlatformSpecific(TestPlatforms.Windows)] - public void RemoveRelativeSegmentsTest(string path, int skip, string expected) - { - Assert.Equal(expected, PathInternal.RemoveRelativeSegments(path, skip)); - Assert.Equal(@"\\.\" + expected, PathInternal.RemoveRelativeSegments(@"\\.\" + path, skip + 4)); - Assert.Equal(@"\\?\" + expected, PathInternal.RemoveRelativeSegments(@"\\?\" + path, skip + 4)); - } - - public static TheoryData RemoveRelativeSegmentsUncData => new TheoryData - { - { @"Server\Share\git\runtime", 12, @"Server\Share\git\runtime"}, - { @"Server\Share\\git\runtime", 12, @"Server\Share\git\runtime"}, - { @"Server\Share\git\\runtime", 12, @"Server\Share\git\runtime"}, - { @"Server\Share\git\.\runtime\.\\", 12, @"Server\Share\git\runtime\"}, - { @"Server\Share\git\runtime", 12, @"Server\Share\git\runtime"}, - { @"Server\Share\git\..\runtime", 12, @"Server\Share\runtime"}, - { @"Server\Share\git\runtime\..\", 12, @"Server\Share\git\"}, - { @"Server\Share\git\runtime\..\..\..\", 12, @"Server\Share\"}, - { @"Server\Share\git\runtime\..\..\.\", 12, @"Server\Share\"}, - { @"Server\Share\git\..\.\runtime\temp\..", 12, @"Server\Share\runtime"}, - { @"Server\Share\git\..\\\.\..\runtime", 12, @"Server\Share\runtime"}, - { @"Server\Share\git\runtime\", 12, @"Server\Share\git\runtime\"}, - { @"Server\Share\git\temp\..\runtime\", 12, @"Server\Share\git\runtime\"}, - }; - - [Theory, - MemberData(nameof(RemoveRelativeSegmentsUncData))] - [PlatformSpecific(TestPlatforms.Windows)] - public void RemoveRelativeSegmentsUncTest(string path, int skip, string expected) - { - Assert.Equal(@"\\" + expected, PathInternal.RemoveRelativeSegments(@"\\" + path, skip + 2)); - Assert.Equal(@"\\.\UNC\" + expected, PathInternal.RemoveRelativeSegments(@"\\.\UNC\" + path, skip + 8)); - Assert.Equal(@"\\?\UNC\" + expected, PathInternal.RemoveRelativeSegments(@"\\?\UNC\" + path, skip + 8)); - } - - public static TheoryData RemoveRelativeSegmentsDeviceData => new TheoryData - { - { @"\\.\git\runtime", 7, @"\\.\git\runtime"}, - { @"\\.\git\runtime", 7, @"\\.\git\runtime"}, - { @"\\.\git\\runtime", 7, @"\\.\git\runtime"}, - { @"\\.\git\.\runtime\.\\", 7, @"\\.\git\runtime\"}, - { @"\\.\git\runtime", 7, @"\\.\git\runtime"}, - { @"\\.\git\..\runtime", 7, @"\\.\git\runtime"}, - { @"\\.\git\runtime\..\", 7, @"\\.\git\"}, - { @"\\.\git\runtime\..\..\..\", 7, @"\\.\git\"}, - { @"\\.\git\runtime\..\..\.\", 7, @"\\.\git\"}, - { @"\\.\git\..\.\runtime\temp\..", 7, @"\\.\git\runtime"}, - { @"\\.\git\..\\\.\..\runtime", 7, @"\\.\git\runtime"}, - { @"\\.\git\runtime\", 7, @"\\.\git\runtime\"}, - { @"\\.\git\temp\..\runtime\", 7, @"\\.\git\runtime\"}, - - { @"\\.\.\runtime", 5, @"\\.\.\runtime"}, - { @"\\.\.\runtime", 5, @"\\.\.\runtime"}, - { @"\\.\.\\runtime", 5, @"\\.\.\runtime"}, - { @"\\.\.\.\runtime\.\\", 5, @"\\.\.\runtime\"}, - { @"\\.\.\runtime", 5, @"\\.\.\runtime"}, - { @"\\.\.\..\runtime", 5, @"\\.\.\runtime"}, - { @"\\.\.\runtime\..\", 5, @"\\.\.\"}, - { @"\\.\.\runtime\..\..\..\", 5, @"\\.\.\"}, - { @"\\.\.\runtime\..\..\.\", 5, @"\\.\.\"}, - { @"\\.\.\..\.\runtime\temp\..", 5, @"\\.\.\runtime"}, - { @"\\.\.\..\\\.\..\runtime", 5, @"\\.\.\runtime"}, - { @"\\.\.\runtime\", 5, @"\\.\.\runtime\"}, - { @"\\.\.\temp\..\runtime\", 5, @"\\.\.\runtime\"}, - - { @"\\.\..\runtime", 6, @"\\.\..\runtime"}, - { @"\\.\..\runtime", 6, @"\\.\..\runtime"}, - { @"\\.\..\\runtime", 6, @"\\.\..\runtime"}, - { @"\\.\..\.\runtime\.\\", 6, @"\\.\..\runtime\"}, - { @"\\.\..\runtime", 6, @"\\.\..\runtime"}, - { @"\\.\..\..\runtime", 6, @"\\.\..\runtime"}, - { @"\\.\..\runtime\..\", 6, @"\\.\..\"}, - { @"\\.\..\runtime\..\..\..\", 6, @"\\.\..\"}, - { @"\\.\..\runtime\..\..\.\", 6, @"\\.\..\"}, - { @"\\.\..\..\.\runtime\temp\..", 6, @"\\.\..\runtime"}, - { @"\\.\..\..\\\.\..\runtime", 6, @"\\.\..\runtime"}, - { @"\\.\..\runtime\", 6, @"\\.\..\runtime\"}, - { @"\\.\..\temp\..\runtime\", 6, @"\\.\..\runtime\"}, - - { @"\\.\\runtime", 4, @"\\.\runtime"}, - { @"\\.\\runtime", 4, @"\\.\runtime"}, - { @"\\.\\\runtime", 4, @"\\.\runtime"}, - { @"\\.\\.\runtime\.\\", 4, @"\\.\runtime\"}, - { @"\\.\\runtime", 4, @"\\.\runtime"}, - { @"\\.\\..\runtime", 4, @"\\.\runtime"}, - { @"\\.\\runtime\..\", 4, @"\\.\"}, - { @"\\.\\runtime\..\..\..\", 4, @"\\.\"}, - { @"\\.\\runtime\..\..\.\", 4, @"\\.\"}, - { @"\\.\\..\.\runtime\temp\..", 4, @"\\.\runtime"}, - { @"\\.\\..\\\.\..\runtime", 4, @"\\.\runtime"}, - { @"\\.\\runtime\", 4, @"\\.\runtime\"}, - { @"\\.\\temp\..\runtime\", 4, @"\\.\runtime\"}, - }; - - public static TheoryData RemoveRelativeSegmentsDeviceRootData => new TheoryData - { - { @"\\.\git\runtime", 8, @"\\.\git\runtime"}, - { @"\\.\git\runtime", 8, @"\\.\git\runtime"}, - { @"\\.\git\\runtime", 8, @"\\.\git\runtime"}, - { @"\\.\git\.\runtime\.\\", 8, @"\\.\git\runtime\"}, - { @"\\.\git\runtime", 8, @"\\.\git\runtime"}, - { @"\\.\git\..\runtime", 8, @"\\.\git\runtime"}, - { @"\\.\git\runtime\..\", 8, @"\\.\git\"}, - { @"\\.\git\runtime\..\..\..\", 8, @"\\.\git\"}, - { @"\\.\git\runtime\..\..\.\", 8, @"\\.\git\"}, - { @"\\.\git\..\.\runtime\temp\..", 8, @"\\.\git\runtime"}, - { @"\\.\git\..\\\.\..\runtime", 8, @"\\.\git\runtime"}, - { @"\\.\git\runtime\", 8, @"\\.\git\runtime\"}, - { @"\\.\git\temp\..\runtime\", 8, @"\\.\git\runtime\"}, - - { @"\\.\.\runtime", 6, @"\\.\.\runtime"}, - { @"\\.\.\runtime", 6, @"\\.\.\runtime"}, - { @"\\.\.\\runtime", 6, @"\\.\.\runtime"}, - { @"\\.\.\.\runtime\.\\", 6, @"\\.\.\runtime\"}, - { @"\\.\.\runtime", 6, @"\\.\.\runtime"}, - { @"\\.\.\..\runtime", 6, @"\\.\.\runtime"}, - { @"\\.\.\runtime\..\", 6, @"\\.\.\"}, - { @"\\.\.\runtime\..\..\..\", 6, @"\\.\.\"}, - { @"\\.\.\runtime\..\..\.\", 6, @"\\.\.\"}, - { @"\\.\.\..\.\runtime\temp\..", 6, @"\\.\.\runtime"}, - { @"\\.\.\..\\\.\..\runtime", 6, @"\\.\.\runtime"}, - { @"\\.\.\runtime\", 6, @"\\.\.\runtime\"}, - { @"\\.\.\temp\..\runtime\", 6, @"\\.\.\runtime\"}, - - { @"\\.\..\runtime", 7, @"\\.\..\runtime"}, - { @"\\.\..\runtime", 7, @"\\.\..\runtime"}, - { @"\\.\..\\runtime", 7, @"\\.\..\runtime"}, - { @"\\.\..\.\runtime\.\\", 7, @"\\.\..\runtime\"}, - { @"\\.\..\runtime", 7, @"\\.\..\runtime"}, - { @"\\.\..\..\runtime", 7, @"\\.\..\runtime"}, - { @"\\.\..\runtime\..\", 7, @"\\.\..\"}, - { @"\\.\..\runtime\..\..\..\", 7, @"\\.\..\"}, - { @"\\.\..\runtime\..\..\.\", 7, @"\\.\..\"}, - { @"\\.\..\..\.\runtime\temp\..", 7, @"\\.\..\runtime"}, - { @"\\.\..\..\\\.\..\runtime", 7, @"\\.\..\runtime"}, - { @"\\.\..\runtime\", 7, @"\\.\..\runtime\"}, - { @"\\.\..\temp\..\runtime\", 7, @"\\.\..\runtime\"}, - - { @"\\.\\runtime", 5, @"\\.\\runtime"}, - { @"\\.\\runtime", 5, @"\\.\\runtime"}, - { @"\\.\\\runtime", 5, @"\\.\\runtime"}, - { @"\\.\\.\runtime\.\\", 5, @"\\.\\runtime\"}, - { @"\\.\\runtime", 5, @"\\.\\runtime"}, - { @"\\.\\..\runtime", 5, @"\\.\\runtime"}, - { @"\\.\\runtime\..\", 5, @"\\.\\"}, - { @"\\.\\runtime\..\..\..\", 5, @"\\.\\"}, - { @"\\.\\runtime\..\..\.\", 5, @"\\.\\"}, - { @"\\.\\..\.\runtime\temp\..", 5, @"\\.\\runtime"}, - { @"\\.\\..\\\.\..\runtime", 5, @"\\.\\runtime"}, - { @"\\.\\runtime\", 5, @"\\.\\runtime\"}, - { @"\\.\\temp\..\runtime\", 5, @"\\.\\runtime\"}, - }; - - [Theory, - MemberData(nameof(RemoveRelativeSegmentsDeviceData)), - MemberData(nameof(RemoveRelativeSegmentsDeviceRootData))] - [PlatformSpecific(TestPlatforms.Windows)] - public void RemoveRelativeSegmentsDeviceTest(string path, int skip, string expected) - { - Assert.Equal(expected, PathInternal.RemoveRelativeSegments(path, skip)); - StringBuilder sb = new StringBuilder(expected); - sb.Replace('.', '?', 0, 4); - expected = sb.ToString(); - - sb = new StringBuilder(path); - sb.Replace('.', '?', 0, 4); - path = sb.ToString(); - Assert.Equal(expected, PathInternal.RemoveRelativeSegments(path, skip)); - } - - public static TheoryData RemoveRelativeSegmentUnixData => new TheoryData - { - { "/tmp/home", 1, "/tmp/home" }, - { "/tmp/..", 1, "/" }, - { "/tmp/home/../././", 1, "/tmp/" }, - { "/tmp/../../../", 1, "/" }, - { "/tmp//home", 1, "/tmp/home" }, - { "/./tmp//home", 1, "/tmp/home" }, - { "/../tmp/home", 1, "/tmp/home" }, - { "/../../../tmp/./home", 1, "/tmp/home" }, - { "//tmp///home", 1, "/tmp/home" }, - { "/tmp/home/git/./.././git/runtime/../", 1, "/tmp/home/git/" }, - { "/./tmp/home", 1, "/tmp/home" }, - - { "/tmp/home", 4, "/tmp/home" }, - { "/tmp/..", 4, "/tmp" }, - { "/tmp/home/../././", 4, "/tmp/" }, - { "/tmp/../../../", 4, "/tmp/" }, - { "/tmp//home", 4, "/tmp/home" }, - { "/./tmp//home", 2, "/./tmp/home" }, - { "/../tmp/home", 3, "/../tmp/home" }, - { "/../../../tmp/./home", 4, "/../tmp/home" }, - { "//tmp///home", 5, "//tmp/home" }, - { "/tmp/home/git/./.././git/runtime/../", 5, "/tmp/home/git/" }, - { "/./tmp/home", 3, "/./tmp/home" }, - - { "/tmp/../../", 8, "/tmp/../" }, - { "/tmp/home/../././", 10, "/tmp/home/" }, - { "/tmp/../../../", 8, "/tmp/../" }, - { "/tmp//home/.././/", 11, "/tmp//home/" }, - { "/./tmp//home/git/git", 7, "/./tmp/home/git/git" }, - { "/../tmp/./home", 8, "/../tmp/home" }, - { "/../../../tmp/./home", 8, "/../../../tmp/home" }, - { "//tmp///home/..", 5, "//tmp/" }, - { "/tmp/home/git/./.././git/runtime/../", 16, "/tmp/home/git/./git/" }, - { "/./tmp/home/././", 7, "/./tmp/home/" }, - }; - - [Theory, - MemberData(nameof(RemoveRelativeSegmentUnixData))] - [PlatformSpecific(TestPlatforms.AnyUnix)] - public void RemoveRelativeSegmentsUnix(string path, int skip, string expected) - { - Assert.Equal(expected, PathInternal.RemoveRelativeSegments(path, skip)); - } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Path.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Path.Unix.cs index 25cec9463f3f20..ba358de086a754 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Path.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Path.Unix.cs @@ -34,7 +34,7 @@ public static string GetFullPath(string path) // We would ideally use realpath to do this, but it resolves symlinks, requires that the file actually exist, // and turns it into a full path, which we only want if fullCheck is true. - string collapsedString = PathInternal.RemoveRelativeSegments(path, PathInternal.GetRootLength(path)); + string collapsedString = Path.RemoveRedundantSegments(path); Debug.Assert(collapsedString.Length < path.Length || collapsedString.ToString() == path, "Either we've removed characters, or the string should be unmodified from the input path."); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Path.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Path.Windows.cs index ce27a28f0e783c..6d36c443dc2e73 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Path.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Path.Windows.cs @@ -130,7 +130,7 @@ public static string GetFullPath(string path, string basePath) // them properly. As such we need to manually remove segments and not use GetFullPath(). return PathInternal.IsDevice(combinedPath.AsSpan()) - ? PathInternal.RemoveRelativeSegments(combinedPath, PathInternal.GetRootLength(combinedPath.AsSpan())) + ? RemoveRedundantSegments(combinedPath.AsSpan()) : GetFullPath(combinedPath); } @@ -213,11 +213,18 @@ public static bool IsPathRooted(ReadOnlySpan path) if (PathInternal.IsEffectivelyEmpty(path.AsSpan())) return null; - ReadOnlySpan result = GetPathRoot(path.AsSpan()); - if (path!.Length == result.Length) - return PathInternal.NormalizeDirectorySeparators(path); + int rootLength = PathInternal.GetRootLength(path.AsSpan()); + if (rootLength <= 0) + return null; - return PathInternal.NormalizeDirectorySeparators(result.ToString()); + Span destination = stackalloc char[rootLength]; + // Fails when path already normalized (or empty, but won't happen here) + if (PathInternal.TryNormalizeDirectorySeparators(path.AsSpan(0, rootLength), destination, out int charsWritten)) + { + return destination.Slice(0, charsWritten).ToString(); + } + // Reaching here means the path was already normalized + return path!.Substring(0, rootLength); } /// @@ -228,8 +235,8 @@ public static ReadOnlySpan GetPathRoot(ReadOnlySpan path) if (PathInternal.IsEffectivelyEmpty(path)) return ReadOnlySpan.Empty; - int pathRoot = PathInternal.GetRootLength(path); - return pathRoot <= 0 ? ReadOnlySpan.Empty : path.Slice(0, pathRoot); + int rootLength = PathInternal.GetRootLength(path); + return rootLength <= 0 ? ReadOnlySpan.Empty : path.Slice(0, rootLength); } /// Gets whether the system is case-sensitive. diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs index b0cdad93bc7a8d..a1e82512980dce 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs @@ -107,7 +107,18 @@ public static partial class Path return null; int end = GetDirectoryNameOffset(path.AsSpan()); - return end >= 0 ? PathInternal.NormalizeDirectorySeparators(path.Substring(0, end)) : null; + + if (end >= 0) + { + Span destination = stackalloc char[end]; + // Fails when empty or already normalized + if (PathInternal.TryNormalizeDirectorySeparators(path.AsSpan(0, end), destination, out int charsWritten)) + { + return destination.Slice(0, charsWritten).ToString(); + } + return path.Substring(0, end); + } + return null; } /// @@ -926,5 +937,99 @@ private static string GetRelativePath(string relativeTo, string path, StringComp /// Returns true if the path ends in a directory separator. /// public static bool EndsInDirectorySeparator(string path) => PathInternal.EndsInDirectorySeparator(path); + + /// + /// Removes any redundant segments found in the specified path. + /// + /// A string containing an absolute or relative path that may or may not contain redundant segments. + /// without any redundant segments. + [return: NotNullIfNotNull("path")] + public static string? RemoveRedundantSegments(string? path) + { + if (path == null) + return null; + + if (PathInternal.IsEffectivelyEmpty(path.AsSpan())) + return string.Empty; + + ValueStringBuilder sb = new ValueStringBuilder(path.Length); + + ReadOnlySpan root = GetPathRoot(path.AsSpan()); + bool isFullyQualified = IsPathFullyQualified(path); + + if (!PathInternal.TryRemoveRedundantSegments(path.AsSpan(), root.Length, isFullyQualified, ref sb)) + { + sb.Dispose(); + return path; + } + + return sb.ToString(); // Disposes + } + + /// + /// Removes any redundant segments found in the specified path. + /// + /// A read-only span containing an absolute or relative path that may or may not contain redundant segments. + /// without any redundant segments. + public static string RemoveRedundantSegments(ReadOnlySpan path) + { + if (PathInternal.IsEffectivelyEmpty(path)) + return string.Empty; + + ValueStringBuilder sb = new ValueStringBuilder(path.Length); + + string result; + + ReadOnlySpan root = GetPathRoot(path); + bool isFullyQualified = IsPathFullyQualified(path); + + if (!PathInternal.TryRemoveRedundantSegments(path, root.Length, isFullyQualified, ref sb)) + { + result = path.ToString(); + sb.Dispose(); + } + else + { + result = sb.ToString(); // Disposes + } + + return result; + } + + /// + /// Tries to remove any redundant segments found in the specified path. + /// + /// A read-only span containing an absolute or relative path that may or may not contain redundant segments. + /// When the method returns , a span containing without any redundant segments. When the method returns , the operation failed. + /// When the method returns , contains the total number of characters written in ; when the method returns , the value is zero. + /// if the operation succeeds; if the operation fails. + /// The method returns `false` when the path is empty or when the destination span's length is less than the resulting path's length. + public static bool TryRemoveRedundantSegments(ReadOnlySpan path, Span destination, out int charsWritten) + { + charsWritten = 0; + + if (PathInternal.IsEffectivelyEmpty(path)) + return false; + + ValueStringBuilder sb = new ValueStringBuilder(path.Length); + + bool result = false; + + ReadOnlySpan root = GetPathRoot(path); + bool isFullyQualified = IsPathFullyQualified(path); + + if (PathInternal.TryRemoveRedundantSegments(path, root.Length, isFullyQualified, ref sb)) + { + result = sb.TryCopyTo(destination, out charsWritten); // Disposes + } + else if (path.TryCopyTo(destination)) + { + charsWritten = path.Length; + result = true; + sb.Dispose(); + } + + return result; + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.Windows.cs index b30f690e0ea58a..97805f944a6b16 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.Windows.cs @@ -314,7 +314,7 @@ internal static bool IsDirectorySeparator(char c) /// Like the current NormalizePath this will not try and analyze periods/spaces within directory segments. /// /// - /// The only callers that used to use Path.Normalize(fullCheck=false) were Path.GetDirectoryName() and Path.GetPathRoot(). Both usages do + /// The only callers that used to use Path.Normalize(fullCheck=false) were Path.GetDirectoryName() and Path.GetPathRoot(string). Both usages do /// not need trimming of trailing whitespace here. /// /// GetPathRoot() could technically skip normalizing separators after the second segment- consider as a future optimization. @@ -341,6 +341,22 @@ internal static bool IsDirectorySeparator(char c) if (string.IsNullOrEmpty(path)) return path; + Span destination = stackalloc char[path.Length]; + if (!TryNormalizeDirectorySeparators(path.AsSpan(), destination, out int charsWritten)) + { + return path; + } + + return destination.Slice(0, charsWritten).ToString(); + } + + internal static bool TryNormalizeDirectorySeparators(ReadOnlySpan path, Span destination, out int charsWritten) + { + charsWritten = 0; + + if (IsEffectivelyEmpty(path)) + return false; + char current; // Make a pass to see if we need to normalize so we can potentially skip allocating @@ -360,7 +376,7 @@ internal static bool IsDirectorySeparator(char c) } if (normalized) - return path; + return false; var builder = new ValueStringBuilder(stackalloc char[MaxShortPath]); @@ -391,7 +407,7 @@ internal static bool IsDirectorySeparator(char c) builder.Append(current); } - return builder.ToString(); + return builder.TryCopyTo(destination, out charsWritten); } /// @@ -411,5 +427,6 @@ internal static bool IsEffectivelyEmpty(ReadOnlySpan path) } return true; } + } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.cs b/src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.cs index fd3529067f71c4..08762a09568385 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.cs @@ -102,106 +102,175 @@ internal static bool AreRootsEqual(string? first, string? second, StringComparis } /// - /// Try to remove relative segments from the given path (without combining with a root). + /// Trims one trailing directory separator beyond the root of the path. /// - /// Input path - /// The length of the root of the given path - internal static string RemoveRelativeSegments(string path, int rootLength) - { - var sb = new ValueStringBuilder(stackalloc char[260 /* PathInternal.MaxShortPath */]); + [return: NotNullIfNotNull("path")] + internal static string? TrimEndingDirectorySeparator(string? path) => + EndsInDirectorySeparator(path) && !IsRoot(path.AsSpan()) ? + path!.Substring(0, path.Length - 1) : + path; - if (RemoveRelativeSegments(path.AsSpan(), rootLength, ref sb)) - { - path = sb.ToString(); - } + /// + /// Returns true if the path ends in a directory separator. + /// + internal static bool EndsInDirectorySeparator(string? path) => + !string.IsNullOrEmpty(path) && IsDirectorySeparator(path[path.Length - 1]); - sb.Dispose(); - return path; - } + /// + /// Trims one trailing directory separator beyond the root of the path. + /// + internal static ReadOnlySpan TrimEndingDirectorySeparator(ReadOnlySpan path) => + EndsInDirectorySeparator(path) && !IsRoot(path) ? + path.Slice(0, path.Length - 1) : + path; /// - /// Try to remove relative segments from the given path (without combining with a root). + /// Returns true if the path ends in a directory separator. /// - /// Input path - /// The length of the root of the given path - /// String builder that will store the result - /// "true" if the path was modified - internal static bool RemoveRelativeSegments(ReadOnlySpan path, int rootLength, ref ValueStringBuilder sb) + internal static bool EndsInDirectorySeparator(ReadOnlySpan path) => + path.Length > 0 && IsDirectorySeparator(path[path.Length - 1]); + + /// + /// Tries to remove relative segments from the given path, starting the analysis at the specified location. + /// + /// The input path. + /// The length of the root prefix, if any. + /// Whether the path is fully qualified or not. + /// A reference to a value string builder that will store the result. + /// if the path was modified; otherwise. + internal static bool TryRemoveRedundantSegments(ReadOnlySpan path, int rootLength, bool isFullyQualified, ref ValueStringBuilder sb) { - Debug.Assert(rootLength > 0); + Debug.Assert(path.Length > 0); + + int charsToSkip = rootLength; bool flippedSeparator = false; + char c; - int skip = rootLength; - // We treat "\.." , "\." and "\\" as a relative segment. We want to collapse the first separator past the root presuming - // the root actually ends in a separator. Otherwise the first segment for RemoveRelativeSegments - // in cases like "\\?\C:\.\" and "\\?\C:\..\", the first segment after the root will be ".\" and "..\" which is not considered as a relative segment and hence not be removed. - if (PathInternal.IsDirectorySeparator(path[skip - 1])) - skip--; - - // Remove "//", "/./", and "/../" from the path by copying each character to the output, - // except the ones we're removing, such that the builder contains the normalized path - // at the end. - if (skip > 0) + // Remove "//", "/./", and "/../" from the path by copying each character to the output, except the ones we're removing, + // such that the builder contains the normalized path at the end. + if (charsToSkip > 0) { - sb.Append(path.Slice(0, skip)); + // We treat "\.." , "\." and "\\" as a redundant segment. + // We want to collapse the first separator past the root presuming the root actually ends in a separator. + // In cases like "\\?\C:\.\" and "\\?\C:\..\", the first segment after the root will be ".\" and "..\" which is not + // considered as a redundant segment and hence not be removed. + if (IsDirectorySeparator(path[charsToSkip - 1])) + { + charsToSkip--; + } + + // Append the root, if any. + // Normalize its directory separators if needed + for (int i = 0; i < charsToSkip; i++) + { + c = path[i]; + flippedSeparator |= TryNormalizeSeparatorCharacter(ref c); + sb.Append(c); + } } - for (int i = skip; i < path.Length; i++) + // Iterate the characters after the root, if any. + for (int currPos = charsToSkip; currPos < path.Length; currPos++) { - char c = path[i]; + c = path[currPos]; - if (PathInternal.IsDirectorySeparator(c) && i + 1 < path.Length) + bool isDirectorySeparator = IsDirectorySeparator(c); + + // Normal case: Start analysis of current segment on the separator + if (isDirectorySeparator && currPos + 1 < path.Length) { - // Skip this character if it's a directory separator and if the next character is, too, - // e.g. "parent//child" => "parent/child" - if (PathInternal.IsDirectorySeparator(path[i + 1])) + // Skip repeated separators, take only the last one. + // e.g. "parent//child" => "parent/child", or "parent/////child" => "parent/child" + if (IsDirectorySeparator(path[currPos + 1])) { continue; } - // Skip this character and the next if it's referring to the current directory, - // e.g. "parent/./child" => "parent/child" - if ((i + 2 == path.Length || PathInternal.IsDirectorySeparator(path[i + 2])) && - path[i + 1] == '.') + // Skip the next segment if it's a single dot (current directory). + // Even if we are at the beginning of a path that is not fully qualified, we always remove these. + // e.g. "parent/./child" => "parent/child", or "parent/." => "parent/" or "./other" => "other" + if (IsNextSegmentSingleDot(path, currPos)) { - i++; + currPos++; continue; } - // Skip this character and the next two if it's referring to the parent directory, + // Skip the next segment if it's a double dot (backtrack to parent directory). // e.g. "parent/child/../grandchild" => "parent/grandchild" - if (i + 2 < path.Length && - (i + 3 == path.Length || PathInternal.IsDirectorySeparator(path[i + 3])) && - path[i + 1] == '.' && path[i + 2] == '.') + if (IsNextSegmentDoubleDot(path, currPos)) { - // Unwind back to the last slash (and if there isn't one, clear out everything). - int s; - for (s = sb.Length - 1; s >= skip; s--) + // Double dots can only get removed if we know who the parent is, which is always possible with fully qualified paths. + if (isFullyQualified) { - if (PathInternal.IsDirectorySeparator(sb[s])) - { - sb.Length = (i + 3 >= path.Length && s == skip) ? s + 1 : s; // to avoid removing the complete "\tmp\" segment in cases like \\?\C:\tmp\..\, C:\tmp\.. - break; - } + TryUnwindBackToPreviousSeparator(ref sb, path, charsToSkip, currPos); + currPos += 2; + continue; } - if (s < skip) + // Non fully qualified paths need to check if there is a folder segment before reaching position 0. + else { - sb.Length = skip; + // If the previous segment is double dot, it means it wasn't processed in a previous loop + // on purpose due to the path being unqualified up to the current position. + // Otherwise, it has to be a folder or a single dot (single dots are always backtracked). + // e.g. "../known" or "../../known" or "known" + if (!IsPreviousSegmentDoubleDot(path, currPos)) + { + // No folder was found behind the current position, only single or double dots. Add the double dots. + if (!TryUnwindBackToPreviousSeparator(ref sb, path, charsToSkip, currPos)) + { + // If the buffer contains data, add a directory separator only if the buffer does not have one already. + // e.g. "..\.\.." => "..\.." + if (sb.Length > 0 && !IsDirectorySeparator(sb[sb.Length - 1])) + { + sb.Append(path[currPos]); + flippedSeparator |= TryNormalizeSeparatorCharacter(ref sb[sb.Length - 1]); + } + // Add the double dots + sb.Append(".."); + } + + currPos += 2; + continue; + } } - - i += 2; - continue; } } + // Special case: single dot segments at the beginning of the path must be skipped + else if (charsToSkip == 0 && sb.Length == 0 && IsNextSegmentSingleDot(path, currPos - 1)) + { + currPos++; + continue; + } // Normalize the directory separator if needed - if (c != PathInternal.DirectorySeparatorChar && c == PathInternal.AltDirectorySeparatorChar) + if (isDirectorySeparator) { - c = PathInternal.DirectorySeparatorChar; - flippedSeparator = true; + flippedSeparator |= TryNormalizeSeparatorCharacter(ref c); } - sb.Append(c); + // Always add the character to the buffer if it's not a directory separator. + + // If it's a directory separator, only append it when: + // - The path is fully qualified: + // e.g. In Unix, a rooted path: "/home/.." => "/" + // - Is not fully qualified, but the buffer already has content: + // e.g. "folder/" => "folder/" + // - Is not fully qualified, buffer is empty but the very first segment is a double dot: + // e.g. "/../folder" => "/../folder" + + // If it's a directory separator, do not append when it's the first character of a sequence with these conditions: + // - Is not fully qualified, started with actual folders which got removed by double dot segments (buffer is empty), and + // has more double dot segments than folders, which would make the double dots reach the beginning of the buffer: + // e.g. "folder/../.." => ".." or "folder/folder/../../../" => "../" + // - Is not fully qualified but is rooted, starts with double dots, or started with actual folders which got removed by + // double dot segments (buffer is empty), and has more double dot segments than folders, which would make the double dots + // reach the beginning of the buffer: + // e.g. "C:..\System32" => "C:\System32" or "C:System32\..\..\" => "C:..\" + if (!isDirectorySeparator || isFullyQualified || sb.Length > rootLength || + (IsNextSegmentDoubleDot(path, currPos) && (currPos == 0 || sb.Length > rootLength))) + { + sb.Append(c); + } } // If we haven't changed the source path, return the original @@ -210,42 +279,101 @@ internal static bool RemoveRelativeSegments(ReadOnlySpan path, int rootLen return false; } - // We may have eaten the trailing separator from the root when we started and not replaced it - if (skip != rootLength && sb.Length < rootLength) + // Final adjustments: + // We may have eaten the trailing separator from the root when we started and not replaced it. + // Only append the trailing separator if the buffer contained information. + if (charsToSkip != rootLength && sb.Length > 0) { - sb.Append(path[rootLength - 1]); + if (sb.Length < charsToSkip) + { + sb.Append(path[charsToSkip - 1]); + } + // e.g. "C:\." => "C:\" or "\\?\C:\.." => "\\?\C:\" + else if (sb.Length == charsToSkip && path.Length > charsToSkip && IsDirectorySeparator(path[charsToSkip])) + { + sb.Append(path[charsToSkip]); + } + } + // If the buffer contained information, but the path was not fully qualified and finished with a separator, + // the separator may have been added, but we should never return a single separator for non-fully qualified paths. + // e.g. "folder/../" => "" + else if (!isFullyQualified && sb.Length == 1 && IsDirectorySeparator(sb[0])) + { + sb.Length = 0; } return true; } - /// - /// Trims one trailing directory separator beyond the root of the path. - /// - [return: NotNullIfNotNull("path")] - internal static string? TrimEndingDirectorySeparator(string? path) => - EndsInDirectorySeparator(path) && !IsRoot(path.AsSpan()) ? - path!.Substring(0, path.Length - 1) : - path; + // Adjusts the length of the buffer to the position of the previous directory separator due to a double dot. + private static bool TryUnwindBackToPreviousSeparator(ref ValueStringBuilder sb, ReadOnlySpan path, int charsToSkip, int currPos) + { + bool onlyDotsAndSeparators = true; + for (int pos = currPos; pos >= charsToSkip; pos--) + { + if (path[pos] != '.' && !IsDirectorySeparator(path[pos])) + { + onlyDotsAndSeparators = false; + } + } + if (onlyDotsAndSeparators) + { + return false; + } - /// - /// Returns true if the path ends in a directory separator. - /// - internal static bool EndsInDirectorySeparator(string? path) => - !string.IsNullOrEmpty(path) && IsDirectorySeparator(path[path.Length - 1]); + int unwindPosition; + for (unwindPosition = sb.Length - 1; unwindPosition >= charsToSkip; unwindPosition--) + { + if (IsDirectorySeparator(sb[unwindPosition])) + { + // Avoid removing the root separator. + // e.g. "\\?\C:\tmp\..\" => "\\?\C:\" or "C:\tmp\.." => "C:\" or "C:\.." => "C:\" + sb.Length = (currPos + 3 >= path.Length && unwindPosition == charsToSkip) ? unwindPosition + 1 : unwindPosition; + break; + } + } + // Never go beyond the root. + // Or in the case of an unqualified path, if the initial segment was a folder + // without a separator at the beginning, the resulting string is empty. + if (unwindPosition < charsToSkip) + { + sb.Length = charsToSkip; + } - /// - /// Trims one trailing directory separator beyond the root of the path. - /// - internal static ReadOnlySpan TrimEndingDirectorySeparator(ReadOnlySpan path) => - EndsInDirectorySeparator(path) && !IsRoot(path) ? - path.Slice(0, path.Length - 1) : - path; + return true; + } - /// - /// Returns true if the path ends in a directory separator. - /// - internal static bool EndsInDirectorySeparator(ReadOnlySpan path) => - path.Length > 0 && IsDirectorySeparator(path[path.Length - 1]); + // If the character is a directory separator, ensure it is set to the current operating system's character. + private static bool TryNormalizeSeparatorCharacter(ref char c) + { + if (c != DirectorySeparatorChar && c == AltDirectorySeparatorChar) + { + c = DirectorySeparatorChar; + return true; + } + return false; + } + + // Checks if the segment before the specified position in the path is a "/../" segment. + private static bool IsPreviousSegmentDoubleDot(ReadOnlySpan path, int currPos) + { + return currPos == 0 || + (currPos - 2 >= 0 && path[currPos - 2] == '.' && path[currPos - 1] == '.'); + } + + // CHecks if the segment after the specified position in the path is a "/../" segment. + private static bool IsNextSegmentDoubleDot(ReadOnlySpan path, int currPos) + { + return currPos + 2 < path.Length && + (currPos + 3 == path.Length || IsDirectorySeparator(path[currPos + 3])) && + path[currPos + 1] == '.' && path[currPos + 2] == '.'; + } + + // CHecks if the segment after the specified position in the path is a "/./" segment. + private static bool IsNextSegmentSingleDot(ReadOnlySpan path, int currPos) + { + return (currPos + 2 == path.Length || IsDirectorySeparator(path[currPos + 2])) && + path[currPos + 1] == '.'; + } } } diff --git a/src/libraries/System.Runtime.Extensions/tests/System.Runtime.Extensions.Tests.csproj b/src/libraries/System.Runtime.Extensions/tests/System.Runtime.Extensions.Tests.csproj index fb959a4a8fadc8..69ceb606d6f962 100644 --- a/src/libraries/System.Runtime.Extensions/tests/System.Runtime.Extensions.Tests.csproj +++ b/src/libraries/System.Runtime.Extensions/tests/System.Runtime.Extensions.Tests.csproj @@ -10,6 +10,7 @@ + diff --git a/src/libraries/System.Runtime.Extensions/tests/System/IO/Path.RemoveRedundantSegments.cs b/src/libraries/System.Runtime.Extensions/tests/System/IO/Path.RemoveRedundantSegments.cs new file mode 100644 index 00000000000000..6cc87b541eff3b --- /dev/null +++ b/src/libraries/System.Runtime.Extensions/tests/System/IO/Path.RemoveRedundantSegments.cs @@ -0,0 +1,588 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.Runtime.Serialization; +using System.Tests; +using System.Text; +using Xunit; + +namespace System.IO.Tests +{ + public static class RemoveRedundantSegmentsTests + { + private static readonly string[] s_Prefixes = new string[] { "", @"\\?\", @"\\.\" }; + private static readonly string[] s_UncPrefixes = new string[] { @"\\", @"\\?\UNC\", @"\\.\UNC\" }; + + // Null or empty tests + public static TheoryData NullOrEmptyData => new TheoryData + { + { null, null }, + { "", "" }, + { " ", "" }, + { " ", "" } + }; + + // Normal paths + public static TheoryData WindowsNormalData => new TheoryData + { + // A '\' inside a string that is prefixed with @ is actually passed as an escaped backward slash "\\" + { @"C:", @"C:" }, + { @"C:\", @"C:\" }, + { @"C:\Users", @"C:\Users" }, + { @"C:\Users\", @"C:\Users\" }, + { @"C:\Users\myuser", @"C:\Users\myuser" }, + { @"C:\Users\myuser\", @"C:\Users\myuser\" }, + }; + public static TheoryData UnixNormalData => new TheoryData + { + // AltDirectorySeparatorChar gets normalized to DirectorySeparatorChar, if they are not the same in the current platform + { @"/", @"/" }, + { @"/home", @"/home" }, + { @"/home/", @"/home/" }, + { @"/home/myuser", @"/home/myuser" }, + { @"/home/myuser/", @"/home/myuser/" }, + }; + + // Paths with '..' to indicate parent backtracking + public static TheoryData WindowsValidParentBacktrackingData => new TheoryData + { + { @"C:\..", @"C:\" }, + { @"C:\..\", @"C:\" }, + { @"C:\..\Users", @"C:\Users" }, + { @"C:\..\Users\", @"C:\Users\" }, + { @"C:\Users\..", @"C:\" }, + { @"C:\Users\..\", @"C:\" }, + { @"C:\Users\..\..", @"C:\" }, + { @"C:\Users\..\..\", @"C:\" }, + { @"C:\Users\..\myuser", @"C:\myuser" }, + { @"C:\Users\..\myuser\", @"C:\myuser\" }, + { @"C:\Users\..\..\myuser", @"C:\myuser" }, + { @"C:\Users\..\..\myuser\", @"C:\myuser\" }, + { @"C:\Users\myuser\..", @"C:\Users" }, + { @"C:\Users\myuser\..\", @"C:\Users\" }, + { @"C:\Users\myuser\..\..", @"C:\" }, + { @"C:\Users\myuser\..\..\", @"C:\" }, + { @"C:\Users\..\myuser\..", @"C:\" }, + { @"C:\Users\..\myuser\..\", @"C:\" }, + { @"C:\Users\..\myuser\..\..", @"C:\" }, + { @"C:\Users\..\myuser\..\..\", @"C:\" }, + { @"C:\Users\..\myuser\..\..\..", @"C:\" }, + { @"C:\Users\..\myuser\..\..\..\", @"C:\" }, + { @"C:\Users\..\..\myuser\..\..", @"C:\" }, + { @"C:\Users\..\..\myuser\..\..\", @"C:\" }, + }; + public static TheoryData UnixValidParentBacktrackingData => new TheoryData + { + { @"/home/..", @"/" }, + { @"/home/../", @"/" }, + { @"/home/../myuser", @"/myuser" }, + { @"/home/../myuser/", @"/myuser/" }, + { @"/home/myuser/..", @"/home" }, + { @"/home/myuser/../", @"/home/" }, + { @"/home/myuser/../..", @"/" }, + { @"/home/myuser/../../", @"/" }, + { @"/home/../myuser/..", @"/" }, + { @"/home/../myuser/../", @"/" }, + }; + + // Paths with '.' to indicate current directory + public static TheoryData WindowsValidCurrentDirectoryData => new TheoryData + { + { @"C:\.", @"C:\" }, + { @"C:\.\", @"C:\" }, + { @"C:\.\.", @"C:\" }, + { @"C:\.\.\", @"C:\" }, + { @"C:\.\Users", @"C:\Users" }, + { @"C:\.\Users\", @"C:\Users\" }, + { @"C:\.\.\Users", @"C:\Users" }, + { @"C:\.\.\Users\", @"C:\Users\" }, + { @"C:\Users\.", @"C:\Users" }, + { @"C:\Users\.\", @"C:\Users\" }, + { @"C:\Users\.\.", @"C:\Users" }, + { @"C:\Users\.\.\", @"C:\Users\" }, + { @"C:\.\Users\myuser", @"C:\Users\myuser" }, + { @"C:\.\Users\myuser\", @"C:\Users\myuser\" }, + { @"C:\.\Users\.\myuser", @"C:\Users\myuser" }, + { @"C:\.\Users\.\myuser\", @"C:\Users\myuser\" }, + { @"C:\.\Users\.\myuser\.", @"C:\Users\myuser" }, + { @"C:\.\Users\.\myuser\.\", @"C:\Users\myuser\" }, + { @"C:\Users\.\.\myuser\.\.", @"C:\Users\myuser" }, + { @"C:\Users\.\.\myuser\.\.\", @"C:\Users\myuser\" }, + }; + + public static TheoryData UnixValidCurrentDirectoryData => new TheoryData + { + { @"/.", @"/" }, + { @"/./", @"/" }, + { @"/./.", @"/" }, + { @"/././", @"/" }, + { @"/./home", @"/home" }, + { @"/./home/", @"/home/" }, + { @"/././home", @"/home" }, + { @"/././home/", @"/home/" }, + { @"/home/.", @"/home" }, + { @"/home/./", @"/home/" }, + { @"/home/./.", @"/home" }, + { @"/home/././", @"/home/" }, + { @"/./home/myuser", @"/home/myuser" }, + { @"/./home/myuser/", @"/home/myuser/" }, + { @"/./home/./myuser", @"/home/myuser" }, + { @"/./home/./myuser/", @"/home/myuser/" }, + { @"/./home/./myuser/.", @"/home/myuser" }, + { @"/./home/./myuser/./", @"/home/myuser/" }, + { @"/home/././myuser/./.", @"/home/myuser" }, + { @"/home/././myuser/././", @"/home/myuser/" }, + }; + + // Combined '.' and '..' + public static TheoryData WindowsCombinedRedundantData => new TheoryData + { + { @"C:\.\..", @"C:\" }, + { @"C:\.\..\", @"C:\" }, + { @"C:\..\.", @"C:\" }, + { @"C:\..\.\", @"C:\" }, + { @"C:\.\..\.", @"C:\" }, + { @"C:\.\..\.\", @"C:\" }, + { @"C:\..\.\.", @"C:\" }, + { @"C:\..\.\.\", @"C:\" }, + { @"C:\.\..\..", @"C:\" }, + { @"C:\.\..\..\", @"C:\" }, + { @"C:\..\.\..", @"C:\" }, + { @"C:\..\.\..\", @"C:\" }, + { @"C:\Users\.\..", @"C:\" }, + { @"C:\Users\.\..\", @"C:\" }, + { @"C:\Users\..\.", @"C:\" }, + { @"C:\Users\..\.\", @"C:\" }, + { @"C:\.\Users\..", @"C:\" }, + { @"C:\.\Users\..\", @"C:\" }, + { @"C:\..\Users\.", @"C:\Users" }, + { @"C:\..\Users\.\", @"C:\Users\" }, + { @"C:\.\..\Users", @"C:\Users" }, + { @"C:\.\..\Users\", @"C:\Users\" }, + { @"C:\..\.\Users", @"C:\Users" }, + { @"C:\..\.\Users\", @"C:\Users\" }, + { @"C:\.\Users\myuser\..", @"C:\Users" }, + { @"C:\.\Users\myuser\..\", @"C:\Users\" }, + { @"C:\..\Users\myuser\.", @"C:\Users\myuser" }, + { @"C:\..\Users\myuser\.\", @"C:\Users\myuser\" }, + { @"C:\.\Users\..\myuser", @"C:\myuser" }, + { @"C:\.\Users\..\myuser\", @"C:\myuser\" }, + { @"C:\..\Users\.\myuser", @"C:\Users\myuser" }, + { @"C:\..\Users\.\myuser\", @"C:\Users\myuser\" }, + }; + + public static TheoryData UnixCombinedRedundantData => new TheoryData + { + { @"/home/./..", @"/" }, + { @"/home/./../", @"/" }, + { @"/home/../.", @"/" }, + { @"/home/.././", @"/" }, + { @"/./home/..", @"/" }, + { @"/./home/../", @"/" }, + { @"/./home/myuser/..", @"/home" }, + { @"/./home/myuser/../", @"/home/" }, + { @"/./home/../myuser", @"/myuser" }, + { @"/./home/../myuser/", @"/myuser/" }, + }; + + public static TheoryData DifferentBehaviorCurrentDirectoryData => new TheoryData + { + // Path Unix Windows + { @"/./home", @"/home", @"home" }, + { @"/././home/", @"/home/", @"home\" }, + { @"/home/.", @"/home", @"home" }, + { @"/home/./", @"/home/", @"home\" }, + { @"/home/./.", @"/home", @"home" }, + { @"/home/././", @"/home/", @"home\" }, + { @"/./home/./", @"/home/", @"home\" }, + { @"/./home/./.", @"/home", @"home" }, + { @"/./home/././", @"/home/", @"home\" }, + { @"/./home/././folder", @"/home/folder", @"home\folder" }, + { @"/./home/././folder/", @"/home/folder/", @"home\folder\" }, + }; + + public static TheoryData DifferentBehaviorBacktrackingData => new TheoryData + { + // Path Unix Windows + { @"/home/../myuser/../..", @"/", @".." }, + { @"/home/../myuser/../../", @"/", @"..\" }, + { @"/home/../myuser/../../..", @"/", @"..\.." }, + { @"/home/../myuser/../../../", @"/", @"..\..\" }, + { @"/home/../..", @"/", @".." }, + { @"/home/../../", @"/", @"..\" }, + { @"/home/../../myuser", @"/myuser", @"..\myuser" }, + { @"/home/../../myuser/", @"/myuser/", @"..\myuser\" }, + { @"/home/../../myuser/../..", @"/", @"..\.." }, + { @"/home/../../myuser/../../", @"/", @"..\..\" }, + { @"/..", @"/", @"\.." }, + { @"/../", @"/", @"\..\" }, + { @"/../home", @"/home", @"\..\home" }, + { @"/../home/", @"/home/", @"\..\home\" }, + { @"/../home/./myuser", @"/home/myuser", @"\..\home\myuser" }, + { @"/../home/./myuser/", @"/home/myuser/", @"\..\home\myuser\" }, + }; + + // Combined '.' and '..' + public static TheoryData DifferentBehaviorCombinedRedundantData => new TheoryData + { + // Path Unix Windows + { @"/./..", @"/", @".." }, + { @"/./../", @"/", @"..\" }, + { @"/../.", @"/", @"\.." }, + { @"/.././", @"/", @"\..\" }, + { @"/./../.", @"/", @".." }, + { @"/./.././", @"/", @"..\" }, + { @"/.././.", @"/", @"\.." }, + { @"/../././", @"/", @"\..\" }, + { @"/./../..", @"/", @"..\.." }, + { @"/./../../", @"/", @"..\..\" }, + { @"/.././..", @"/", @"\..\.." }, + { @"/.././../", @"/", @"\..\..\" }, + { @"/../home/.", @"/home", @"\..\home" }, + { @"/../home/./", @"/home/", @"\..\home\" }, + { @"/./../home", @"/home", @"..\home" }, + { @"/./../home/", @"/home/", @"..\home\" }, + { @"/.././home", @"/home", @"\..\home" }, + { @"/.././home/", @"/home/", @"\..\home\" }, + { @"/../home/myuser/.", @"/home/myuser", @"\..\home\myuser" }, + { @"/../home/myuser/./", @"/home/myuser/", @"\..\home\myuser\" }, + { @"/../home/myuser/./../", @"/home/", @"\..\home\" }, + { @"/../home/myuser/./../folder", @"/home/folder", @"\..\home\folder" }, + }; + + + // Duplicate separators + public static TheoryData WindowsDuplicateSeparatorsData => new TheoryData + { + { @"C:\\", @"C:\" }, + { @"C:\\\", @"C:\" }, + { @"C://", @"C:\" }, + { @"C:///", @"C:\" }, + { @"C:\/", @"C:\" }, + { @"C:/\", @"C:\" }, + { @"C:\/\", @"C:\" }, + { @"C:/\\", @"C:\" }, + { @"C:\//", @"C:\" }, + { @"C:/\/", @"C:\" }, + { @"C:\\Users", @"C:\Users" }, + { @"C:\\\Users", @"C:\Users" }, + { @"C:\\Users\\\", @"C:\Users\" }, + { @"C:\\\Users\\", @"C:\Users\" }, + { @"C:\\Users\\/\", @"C:\Users\" }, + { @"C:\\\Users\/\", @"C:\Users\" }, + { @"C:\/Users", @"C:\Users" }, + { @"C:\/Users/", @"C:\Users\" }, + { @"C:\/Users/", @"C:\Users\" }, + { @"C:\\Users\\.\", @"C:\Users\" }, + { @"C:\\\Users\..\\", @"C:\" }, + { @"C:\\Users\\./.\", @"C:\Users\" }, + { @"C:\.\\Users\/./\\", @"C:\Users\" }, + { @"C:\\.\Users\/../\\./", @"C:\" }, + }; + + public static TheoryData UnixDuplicateSeparatorsData => new TheoryData + { + { @"/home/", @"/home/" }, + { @"/home\\\", @"/home/" }, + { @"/home//", @"/home/" }, + { @"/home///", @"/home/" }, + { @"/home\/", @"/home/" }, + { @"/home/\", @"/home/" }, + { @"/home\/\", @"/home/" }, + { @"/home/\\", @"/home/" }, + { @"/home\//", @"/home/" }, + { @"/home/\/", @"/home/" }, + { @"/home\\myuser", @"/home/myuser" }, + { @"/home\\\myuser", @"/home/myuser" }, + { @"/home\\myuser\\\", @"/home/myuser/" }, + { @"/home\\\myuser\\", @"/home/myuser/" }, + { @"/home\\myuser\\/\", @"/home/myuser/" }, + { @"/home\\\myuser\/\", @"/home/myuser/" }, + { @"/home\/myuser", @"/home/myuser" }, + { @"/home\/myuser/", @"/home/myuser/" }, + { @"/home\/myuser/", @"/home/myuser/" }, + { @"/home\\myuser\\.\", @"/home/myuser/" }, + { @"/home\\\myuser\..\\", @"/home/" }, + { @"/home\\myuser\\./.\", @"/home/myuser/" }, + { @"/home\.\\myuser\/./\\", @"/home/myuser/" }, + { @"/home\\.\myuser\/../\\./", @"/home/" }, + }; + + // Network locations - "Server\Share" always stays. UNC prefixes get prepended in the tests. + public static TheoryData UncData => new TheoryData + { + { @"Server\Share\git\runtime", @"Server\Share\git\runtime"}, + { @"Server\Share\\git\runtime", @"Server\Share\git\runtime"}, + { @"Server\Share\git\\runtime", @"Server\Share\git\runtime"}, + { @"Server\Share\git\.\runtime\.\\", @"Server\Share\git\runtime\"}, + { @"Server\Share\git\runtime", @"Server\Share\git\runtime"}, + { @"Server\Share\git\..\runtime", @"Server\Share\runtime"}, + { @"Server\Share\git\runtime\..\", @"Server\Share\git\"}, + { @"Server\Share\git\runtime\..\..\..\", @"Server\Share\"}, + { @"Server\Share\git\runtime\..\..\.\", @"Server\Share\"}, + { @"Server\Share\git\..\.\runtime\temp\..", @"Server\Share\runtime"}, + { @"Server\Share\git\..\\\.\..\runtime", @"Server\Share\runtime"}, + { @"Server\Share\git\runtime\", @"Server\Share\git\runtime\"}, + { @"Server\Share\git\temp\..\runtime\", @"Server\Share\git\runtime\"}, + }; + + // Paths that are not rooted + public static TheoryData UnqualifiedPathsData => new TheoryData + { + { @"Users\myuser\..\", @"Users\" }, + { @"Users\myuser\..", @"Users" }, + { @"Users\..\..", @".." }, + { @"Users\..\..\", @"..\" }, + { @"myuser\..\", @"" }, + { @"myuser", @"myuser" }, + { @".\myuser", @"myuser" }, + { @".\myuser\", @"myuser\" }, + { @".\.\myuser", @"myuser" }, + { @".\.\myuser\", @"myuser\" }, + { @"..\myuser", @"..\myuser" }, + { @"..\myuser\", @"..\myuser\" }, + { @"..\\myuser\", @"..\myuser\" }, + { @"..\myuser\..", @".." }, + { @"..\first\..\second", @"..\second" }, + { @"..\first\..\..\second\..", @"..\.." }, + { @"..\first\..\..\second\..\", @"..\..\" }, + { @"..\first\..\..\second\..", @"..\.." }, + { @"..\first\..\..\second\..\..", @"..\..\.." }, + { @"..\first\..\..\second\..\..\", @"..\..\..\" }, + }; + + public static TheoryData ValidEdgecasesData => new TheoryData + { + { @"C:\Users\myuser\folder.with\one\dot", @"C:\Users\myuser\folder.with\one\dot" }, + { @"C:\Users\myuser\folder..with\two\dots", @"C:\Users\myuser\folder..with\two\dots" }, + { @"C:\Users\.folder\startswithdot", @"C:\Users\.folder\startswithdot" }, + { @"C:\Users\folder.\endswithdot", @"C:\Users\folder.\endswithdot" }, + { @"C:\Users\..folder\startswithtwodots", @"C:\Users\..folder\startswithtwodots" }, + { @"C:\Users\folder..\endswithtwodots", @"C:\Users\folder..\endswithtwodots" }, + { @"C:\Users\myuser\this\is\a\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\long\path\but\it\should\not\matter\extraword\..\", @"C:\Users\myuser\this\is\a\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\really\long\path\but\it\should\not\matter\" }, + { @"C:\Users\myuser\this_is_a_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_long_foldername\but_it_should_not_matter\extraword\..\", @"C:\Users\myuser\this_is_a_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_long_foldername\but_it_should_not_matter\" }, + }; + + [Theory] + [MemberData(nameof(NullOrEmptyData))] + [MemberData(nameof(UnqualifiedPathsData))] + [MemberData(nameof(ValidEdgecasesData))] + public static void SpecialCases_String(string path, string expected) => TestRedundantSegments(path, expected); + + [Theory] + [MemberData(nameof(NullOrEmptyData))] + [MemberData(nameof(UnqualifiedPathsData))] + [MemberData(nameof(ValidEdgecasesData))] + public static void SpecialCases_Span(string path, string expected) => TestRedundantSegments(path.AsSpan(), expected); + + [Theory] + [MemberData(nameof(UnqualifiedPathsData))] + [MemberData(nameof(ValidEdgecasesData))] + public static void SpecialCases_True_Try(string path, string expected) => TestTryRedundantSegments(path, expected, true, expected.Length); + + [Theory] + [MemberData(nameof(NullOrEmptyData))] + public static void SpecialCases_False_Try(string path, string expected) => TestTryRedundantSegments(path, expected, false, 0); + + [Theory] + [MemberData(nameof(DifferentBehaviorCurrentDirectoryData))] + [MemberData(nameof(DifferentBehaviorBacktrackingData))] + [MemberData(nameof(DifferentBehaviorCombinedRedundantData))] + public static void DifferentBehavior_String(string path, string expectedUnix, string expectedWindows) + { + string expected = (PlatformDetection.IsWindows) ? expectedWindows : expectedUnix; + TestRedundantSegments(path, expected); + } + + [Theory] + [MemberData(nameof(DifferentBehaviorCurrentDirectoryData))] + [MemberData(nameof(DifferentBehaviorBacktrackingData))] + [MemberData(nameof(DifferentBehaviorCombinedRedundantData))] + public static void DifferentBehavior_Span(string path, string expectedUnix, string expectedWindows) + { + string expected = (PlatformDetection.IsWindows) ? expectedWindows : expectedUnix; + TestRedundantSegments(path.AsSpan(), expected); + } + + [Theory] + [MemberData(nameof(DifferentBehaviorCurrentDirectoryData))] + [MemberData(nameof(DifferentBehaviorBacktrackingData))] + [MemberData(nameof(DifferentBehaviorCombinedRedundantData))] + public static void DifferentBehavior_Try(string path, string expectedUnix, string expectedWindows) + { + string expected = (PlatformDetection.IsWindows) ? expectedWindows : expectedUnix; + TestTryRedundantSegments(path, expected, true, expected.Length); + } + + [Theory] + [MemberData(nameof(WindowsNormalData))] + [MemberData(nameof(WindowsValidParentBacktrackingData))] + [MemberData(nameof(WindowsValidCurrentDirectoryData))] + [MemberData(nameof(WindowsCombinedRedundantData))] + [MemberData(nameof(WindowsDuplicateSeparatorsData))] + [PlatformSpecific(TestPlatforms.Windows)] + public static void WindowsValid_String(string path, string expected) + { + foreach (string prefix in s_Prefixes) + { + TestRedundantSegments(prefix + path, prefix + expected); + } + } + + [Theory] + [MemberData(nameof(WindowsNormalData))] + [MemberData(nameof(WindowsValidParentBacktrackingData))] + [MemberData(nameof(WindowsValidCurrentDirectoryData))] + [MemberData(nameof(WindowsCombinedRedundantData))] + [MemberData(nameof(WindowsDuplicateSeparatorsData))] + [PlatformSpecific(TestPlatforms.Windows)] + public static void WindowsValid_Span(string path, string expected) + { + foreach (string prefix in s_Prefixes) + { + TestRedundantSegments((prefix + path).AsSpan(), prefix + expected); + } + } + + [Theory] + [MemberData(nameof(WindowsNormalData))] + [MemberData(nameof(WindowsValidParentBacktrackingData))] + [MemberData(nameof(WindowsValidCurrentDirectoryData))] + [MemberData(nameof(WindowsCombinedRedundantData))] + [MemberData(nameof(WindowsDuplicateSeparatorsData))] + [PlatformSpecific(TestPlatforms.Windows)] + public static void WindowsValid_Try(string path, string expected) + { + foreach (string prefix in s_Prefixes) + { + TestTryRedundantSegments(prefix + path, prefix + expected, true, (prefix + expected).Length); + } + } + + + [Theory] + [MemberData(nameof(UncData))] + [PlatformSpecific(TestPlatforms.Windows)] + public static void WindowsUnc_String(string path, string expected) + { + foreach (string prefix in s_UncPrefixes) + { + TestRedundantSegments(prefix + path, prefix + expected); + } + } + + [Theory] + [MemberData(nameof(UncData))] + [PlatformSpecific(TestPlatforms.Windows)] + public static void WindowsUnc_Span(string path, string expected) + { + foreach (string prefix in s_UncPrefixes) + { + TestRedundantSegments((prefix + path).AsSpan(), prefix + expected); + } + } + + [Theory] + [MemberData(nameof(UncData))] + [PlatformSpecific(TestPlatforms.Windows)] + public static void WindowsUnc_Try(string path, string expected) + { + foreach (string prefix in s_UncPrefixes) + { + TestTryRedundantSegments(prefix + path, prefix + expected, true, (prefix + expected).Length); + } + } + + + // The expected string is returned in Unix with '/' as separator. + // The trailing '/' is considered the root, hence it's a fully qualified path. + + [Theory] + [MemberData(nameof(UnixNormalData))] + [MemberData(nameof(UnixValidParentBacktrackingData))] + [MemberData(nameof(UnixValidCurrentDirectoryData))] + [MemberData(nameof(UnixCombinedRedundantData))] + [MemberData(nameof(UnixDuplicateSeparatorsData))] + [PlatformSpecific(TestPlatforms.AnyUnix)] + public static void UnixValid_String(string path, string expected) + { + if (!PlatformDetection.IsWindows) + { + expected = expected.Replace('\\', '/'); + } + TestRedundantSegments(path, expected); + } + + [Theory] + [MemberData(nameof(UnixNormalData))] + [MemberData(nameof(UnixValidParentBacktrackingData))] + [MemberData(nameof(UnixValidCurrentDirectoryData))] + [MemberData(nameof(UnixCombinedRedundantData))] + [MemberData(nameof(UnixDuplicateSeparatorsData))] + [PlatformSpecific(TestPlatforms.AnyUnix)] + public static void UnixValid_Span(string path, string expected) + { + if (!PlatformDetection.IsWindows) + { + expected = expected.Replace('\\', '/'); + } + TestRedundantSegments(path.AsSpan(), expected); + } + + [Theory] + [MemberData(nameof(UnixNormalData))] + [MemberData(nameof(UnixValidParentBacktrackingData))] + [MemberData(nameof(UnixValidCurrentDirectoryData))] + [MemberData(nameof(UnixCombinedRedundantData))] + [MemberData(nameof(UnixDuplicateSeparatorsData))] + [PlatformSpecific(TestPlatforms.AnyUnix)] + public static void UnixValid_Try(string path, string expected) + { + if (!PlatformDetection.IsWindows) + { + expected = expected.Replace('\\', '/'); + } + TestTryRedundantSegments(path, expected, true, expected.Length); + } + + + + [Fact] + public static void DestinationTooSmall_Try() + { + Span actualDestination = stackalloc char[1]; + bool actualReturn = Path.TryRemoveRedundantSegments(@"C:\Users\myuser", actualDestination, out int actualCharsWritten); + string stringDestination = actualDestination.Slice(0, actualCharsWritten).ToString(); + Assert.False(actualReturn); + Assert.Equal(0, actualCharsWritten); + Assert.Equal(0, stringDestination.Length); + } + + + // Helper methods + + private static void TestTryRedundantSegments(string path, string expected, bool expectedReturn, int expectedCharsWritten) + { + Span actualDestination = stackalloc char[(path != null) ? path.Length : 1]; + bool actualReturn = Path.TryRemoveRedundantSegments(path.AsSpan(), actualDestination, out int actualCharsWritten); + Assert.Equal(expectedReturn, actualReturn); + Assert.Equal(expected ?? string.Empty, actualDestination.Slice(0, actualCharsWritten).ToString()); + Assert.Equal(expectedCharsWritten, actualCharsWritten); + } + + private static void TestRedundantSegments(ReadOnlySpan path, string expected) + { + string actual = Path.RemoveRedundantSegments(path); + Assert.Equal(expected ?? string.Empty, actual); + } + + private static void TestRedundantSegments(string path, string expected) + { + string actual = Path.RemoveRedundantSegments(path); + Assert.Equal(expected, actual); + } + + } +} diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 3cea060702adb4..80cf8014bbe3ac 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -6920,10 +6920,13 @@ public static partial class Path public static string Join(string? path1, string? path2, string? path3) { throw null; } public static string Join(string? path1, string? path2, string? path3, string? path4) { throw null; } public static string Join(params string?[] paths) { throw null; } + public static string RemoveRedundantSegments(string path) { throw null; } + public static string RemoveRedundantSegments(System.ReadOnlySpan path) { throw null; } public static System.ReadOnlySpan TrimEndingDirectorySeparator(System.ReadOnlySpan path) { throw null; } public static string TrimEndingDirectorySeparator(string path) { throw null; } public static bool TryJoin(System.ReadOnlySpan path1, System.ReadOnlySpan path2, System.ReadOnlySpan path3, System.Span destination, out int charsWritten) { throw null; } public static bool TryJoin(System.ReadOnlySpan path1, System.ReadOnlySpan path2, System.Span destination, out int charsWritten) { throw null; } + public static bool TryRemoveRedundantSegments(System.ReadOnlySpan path, System.Span destination, out int charsWritten) { throw null; } } public partial class PathTooLongException : System.IO.IOException {