From 918bfbab3e1534798f62197a2386cad2b1bdcbc9 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Mon, 29 Jun 2020 15:42:43 -0700 Subject: [PATCH 01/25] Remove old internal API unit tests. --- .../Tests/System/IO/PathInternal.Tests.cs | 340 ------------------ 1 file changed, 340 deletions(-) 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 edd9c3228e803c..c1aa7a2494b9c7 100644 --- a/src/libraries/Common/tests/Tests/System/IO/PathInternal.Tests.cs +++ b/src/libraries/Common/tests/Tests/System/IO/PathInternal.Tests.cs @@ -63,345 +63,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)); - } } } From 1319ff0fff04f117a5ccadd36f7119398eb1e57d Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Mon, 29 Jun 2020 15:43:27 -0700 Subject: [PATCH 02/25] Add refs to System.Runtime.Extensions --- src/libraries/System.Runtime/ref/System.Runtime.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 6c2e4029a28875..5e93ae13116d88 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -7268,6 +7268,9 @@ 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 bool TryRemoveRedundantSegments(System.ReadOnlySpan path, System.Span destination, out int charsWritten) { 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; } From 523784a5909d699adde641f763c68ff43ffff592 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Mon, 29 Jun 2020 15:46:38 -0700 Subject: [PATCH 03/25] csproj and projitems changes --- .../Microsoft.IO.Redist/src/Microsoft.IO.Redist.csproj | 6 +++++- .../src/System.Private.CoreLib.Shared.projitems | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/libraries/Microsoft.IO.Redist/src/Microsoft.IO.Redist.csproj b/src/libraries/Microsoft.IO.Redist/src/Microsoft.IO.Redist.csproj index d116870a954500..2fb6fc1a5e7a25 100644 --- a/src/libraries/Microsoft.IO.Redist/src/Microsoft.IO.Redist.csproj +++ b/src/libraries/Microsoft.IO.Redist/src/Microsoft.IO.Redist.csproj @@ -1,4 +1,4 @@ - + net472 $(DefineConstants);MS_IO_REDIST @@ -120,6 +120,10 @@ Link="System\IO\PathInternal.cs" /> + + + @@ -1610,6 +1611,7 @@ + @@ -1617,7 +1619,7 @@ - + @@ -1822,6 +1824,7 @@ + From 470b4d3cc8c966a200c6a40b331c2883a958ff25 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Mon, 29 Jun 2020 15:48:27 -0700 Subject: [PATCH 04/25] Add Path.cs new API code and const int for initial buffer length. --- .../src/System/IO/Path.cs | 91 ++++++++++++++++++- 1 file changed, 88 insertions(+), 3 deletions(-) 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 235c8d96e22752..fcff88296eb7ba 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs @@ -32,6 +32,11 @@ public static partial class Path // 8 random bytes provides 12 chars in our encoding for the 8.3 name. private const int KeyLength = 8; + // Initial cross-platform length for a buffer that is to be passed to the ValueStringBuilder constructor. + // The ValueStringBuilder will increase the internal buffer length when necessary. + // The value is equivalent to PathInternal.MaxShortPath, a limitation that only exists in older Windows versions. + private const int InitialValueStringBuilderBufferLength = 260; + [Obsolete("Please use GetInvalidPathChars or GetInvalidFileNameChars instead.")] public static readonly char[] InvalidPathChars = GetInvalidPathChars(); @@ -383,7 +388,7 @@ public static string Combine(params string[] paths) maxSize++; } - var builder = new ValueStringBuilder(stackalloc char[260]); // MaxShortPath on Windows + var builder = new ValueStringBuilder(stackalloc char[InitialValueStringBuilderBufferLength]); builder.EnsureCapacity(maxSize); for (int i = firstComponent; i < paths.Length; i++) @@ -490,7 +495,7 @@ public static string Join(params string?[] paths) } maxSize += paths.Length - 1; - var builder = new ValueStringBuilder(stackalloc char[260]); // MaxShortPath on Windows + var builder = new ValueStringBuilder(stackalloc char[InitialValueStringBuilderBufferLength]); builder.EnsureCapacity(maxSize); for (int i = 0; i < paths.Length; i++) @@ -916,7 +921,7 @@ private static string GetRelativePath(string relativeTo, string path, StringComp // C:\Foo\Bar C:\Bar\Bar L3, S2 -> ..\..\Bar\Bar // C:\Foo\Foo C:\Foo\Bar L7, S1 -> ..\Bar - var sb = new ValueStringBuilder(stackalloc char[260]); + var sb = new ValueStringBuilder(stackalloc char[InitialValueStringBuilderBufferLength]); sb.EnsureCapacity(Math.Max(relativeTo.Length, path.Length)); // Add parent segments for segments past the common on the "from" path @@ -983,5 +988,85 @@ 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 redundant segments from the specified path string. + /// + /// The path to analyze. + /// A string without 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(stackalloc char[InitialValueStringBuilderBufferLength]); + + if (!RedundantSegmentHelper.TryRemoveRedundantSegments(path.AsSpan(), ref sb)) + { + sb.Dispose(); + return path; + } + + return sb.ToString(); // Disposes + } + + /// + /// Removes redundant segments from the specified path read-only span. + /// + /// The path to analyze. + /// A string without redundant segments. + public static string RemoveRedundantSegments(ReadOnlySpan path) + { + if (PathInternal.IsEffectivelyEmpty(path)) + return string.Empty; + + ValueStringBuilder sb = new ValueStringBuilder(stackalloc char[InitialValueStringBuilderBufferLength]); + + if (!RedundantSegmentHelper.TryRemoveRedundantSegments(path, ref sb)) + { + sb.Dispose(); + return path.ToString(); + } + else + { + return sb.ToString(); // Disposes + } + } + + /// + /// Tries to remove redundant segments from the specified path read-only span. + /// + /// The path to analyze. + /// A span where the output is saved. + /// The total number of characters written to , which is less or equal than the length of . + /// if the original path was modified and writing into was successful; otherwise. + public static bool TryRemoveRedundantSegments(ReadOnlySpan path, Span destination, out int charsWritten) + { + charsWritten = 0; + + if (PathInternal.IsEffectivelyEmpty(path)) + return false; + + ValueStringBuilder sb = new ValueStringBuilder(stackalloc char[InitialValueStringBuilderBufferLength]); + + bool result = false; + + if (RedundantSegmentHelper.TryRemoveRedundantSegments(path, ref sb)) + { + result = sb.TryCopyTo(destination, out charsWritten); // Disposes + } + else if (path.TryCopyTo(destination)) + { + charsWritten = path.Length; + result = true; + sb.Dispose(); + } + + return result; + } } } From 2e2a925e54ac435824af237863baa1e11cae464c Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Mon, 29 Jun 2020 15:49:37 -0700 Subject: [PATCH 05/25] Update platform-specific Path calls to old internal method. --- .../System.Private.CoreLib/src/System/IO/Path.Unix.cs | 2 +- .../System.Private.CoreLib/src/System/IO/Path.Windows.cs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) 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 9303667afa6397..cba2cff81bb798 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 @@ -33,7 +33,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.AsSpan()); 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 314f08f355e044..7a48439be1998d 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 @@ -128,9 +128,7 @@ public static string GetFullPath(string path, string basePath) // to Windows APIs won't do anything by design. Additionally, GetFullPathName() in Windows doesn't root // 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())) - : GetFullPath(combinedPath); + return PathInternal.IsDevice(combinedPath.AsSpan()) ? RemoveRedundantSegments(combinedPath.AsSpan()) : GetFullPath(combinedPath); } public static string GetTempPath() From 1e17018fc872342757fb6366baa2928d0ae66755 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Mon, 29 Jun 2020 15:49:55 -0700 Subject: [PATCH 06/25] Remove old internal method from PathInternal --- .../src/System/IO/PathInternal.cs | 118 ------------------ 1 file changed, 118 deletions(-) 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 fbd47fea72d90b..dbc395436397de 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.cs @@ -100,124 +100,6 @@ internal static bool AreRootsEqual(string? first, string? second, StringComparis comparisonType: comparisonType) == 0; } - /// - /// Try to remove relative segments from the given path (without combining with a root). - /// - /// 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 */]); - - if (RemoveRelativeSegments(path.AsSpan(), rootLength, ref sb)) - { - path = sb.ToString(); - } - - sb.Dispose(); - return path; - } - - /// - /// Try to remove relative segments from the given path (without combining with a root). - /// - /// 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) - { - Debug.Assert(rootLength > 0); - bool flippedSeparator = false; - - 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) - { - sb.Append(path.Slice(0, skip)); - } - - for (int i = skip; i < path.Length; i++) - { - char c = path[i]; - - if (PathInternal.IsDirectorySeparator(c) && i + 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])) - { - 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] == '.') - { - i++; - continue; - } - - // Skip this character and the next two if it's referring to the 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] == '.') - { - // 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--) - { - 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; - } - } - if (s < skip) - { - sb.Length = skip; - } - - i += 2; - continue; - } - } - - // Normalize the directory separator if needed - if (c != PathInternal.DirectorySeparatorChar && c == PathInternal.AltDirectorySeparatorChar) - { - c = PathInternal.DirectorySeparatorChar; - flippedSeparator = true; - } - - sb.Append(c); - } - - // If we haven't changed the source path, return the original - if (!flippedSeparator && sb.Length == path.Length) - { - 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) - { - sb.Append(path[rootLength - 1]); - } - - return true; - } - /// /// Trims one trailing directory separator beyond the root of the path. /// From 170140d77268ed21ae5c07c72e541a0bd6f56d68 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Mon, 29 Jun 2020 15:50:33 -0700 Subject: [PATCH 07/25] Add RedundantSegmentHelper class, with platform-specific methods. --- .../System/IO/RedundantSegmentHelper.Unix.cs | 127 ++++++++ .../IO/RedundantSegmentHelper.Windows.cs | 298 ++++++++++++++++++ .../src/System/IO/RedundantSegmentHelper.cs | 123 ++++++++ 3 files changed, 548 insertions(+) create mode 100644 src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Unix.cs create mode 100644 src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs create mode 100644 src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.cs diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Unix.cs new file mode 100644 index 00000000000000..97f82c28939c03 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Unix.cs @@ -0,0 +1,127 @@ +// 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. + +#nullable enable +using System.Diagnostics; +using System.Text; + +namespace System.IO +{ + internal static partial class RedundantSegmentHelper + { + // Attempts to remove redundant segments from a path. + // Redundant segments are: ".", ".." or duplicate directory separators. + // Returns true if the original path was modified. + internal static bool TryRemoveRedundantSegments(ReadOnlySpan originalPath, ref ValueStringBuilder sb) + { + Debug.Assert(originalPath.Length > 0); + + bool flippedSeparator = false; + int rootLength = PathInternal.GetRootLength(originalPath); + + // Append characters that should not be touched + if (rootLength == 1) + { + flippedSeparator |= AppendWithFlippedSeparators(originalPath[0], ref sb); + } + + char c; + // Start analysis at zero to ensure duplicate separators get skipped + for (int currPos = 0; currPos < originalPath.Length; currPos++) + { + c = originalPath[currPos]; + + bool isSeparator = PathInternal.IsDirectorySeparator(c); + + if (isSeparator && currPos + 1 < originalPath.Length) + { + // Skip repeated separators, take only the last one. + // e.g. "parent//child" => "parent/child", or "parent/////child" => "parent/child" + if (PathInternal.IsDirectorySeparator(originalPath[currPos + 1])) + { + continue; + } + + // Handle redundant segments + if (IsNextSegmentOnlyDots(originalPath, currPos, out int totalDots)) + { + Debug.Assert(totalDots > 0); + + // Skip the next segment if it's a single dot (current directory). + // Only keep it if it's at the beginning of the unqualified path. + // e.g. "parent/./child" => "parent/child", or "parent/." => "parent/" or "./other" => "other" + // or "./folder" => "./folder/, or "././folder" => "./folder" + if (totalDots == 1) + { + // The only case where we add it is if we are at the beginning and the root is not "" + if (rootLength == 0 && sb.Length == 0) + { + sb.Append('.'); + } + currPos++; + continue; + } + // Skip the next segment if it's a double dot (backtrack to parent directory). + // e.g. "parent/child/../grandchild" => "parent/grandchild" + else if (totalDots == 2) + { + // Unqualified paths need to check if there is a valid folder segment before reaching the beginning and root is not "\" + // Fully qualified paths should succeed to backtrack, even when reaching the root + if (!TryBacktrackToPreviousSeparator(currPos, rootLength, rootLength > 0, ref sb)) + { + // Before appending ".." we need to check if a separator needs to be added first: + // e.g. "..\.\.." => "..\.." or "\..\.\.." => "\..\.." or "path\..\.." => ".." + if ((sb.Length > rootLength) && !PathInternal.IsDirectorySeparator(sb[^1])) + { + flippedSeparator |= AppendWithFlippedSeparators(originalPath[currPos], ref sb); + } + // Add the double dots + sb.Append(".."); + } + currPos += 2; + continue; + } + } + } + + // special case that can only happen at the beginning: do not add the dot if path is rooted + // - "./" => "./" + // - "/." -> "/" + // - "." => "." + // - "/folder/." => "/folder/" + if (currPos == 0 && rootLength == 0 && c == '.' && sb.Length > rootLength && + (currPos + 1 >= originalPath.Length || PathInternal.IsDirectorySeparator(originalPath[currPos + 1]))) + { + continue; + } + + // Normalize the directory separator if needed + if (isSeparator) + { + if (sb.Length > rootLength && !PathInternal.IsDirectorySeparator(sb[^1])) + { + flippedSeparator |= AppendWithFlippedSeparators(originalPath[currPos], ref sb); + } + } + // Append all other path characters + else + { + sb.Append(c); + } + } + + // Final adjustments: + + // If the buffer contained information, but the path finished with a separator, the + // separator may have been added, but we should never return a single separator for unqualified paths. + // e.g. "folder/../" => "" + if (rootLength == 0 && originalPath.Length > 0 && sb.Length == 1 && PathInternal.IsDirectorySeparator(sb[0])) + { + sb.Length = 0; + } + + return flippedSeparator || sb.Length < originalPath.Length; + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs new file mode 100644 index 00000000000000..896f038bc48d77 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs @@ -0,0 +1,298 @@ +// 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. + +#nullable enable +using System.Diagnostics; +using System.Text; + +namespace System.IO +{ + internal static partial class RedundantSegmentHelper + { + // Attempts to remove redundant segments from a path. + // Redundant segments are: ".", ".." or duplicate directory separators. + // Returns true if the original path was modified. + internal static bool TryRemoveRedundantSegments(ReadOnlySpan originalPath, ref ValueStringBuilder sb) + { + Debug.Assert(originalPath.Length > 0); + + // Paths that begin with "\??\" or "\\?\" are by definition normalized and should not get segments removed + // If the user wants segments removed, they can pass a sliced span of the string that excludes the prefix + if (PathInternal.IsExtended(originalPath)) + { + return false; + } + + // GetRootLength can calculate the total root that includes a device/extended prefix and + // also unqualified roots like a rootless drive + any folder after that + int charsToSkip = PathInternal.GetRootLength(originalPath); + bool flippedSeparator = false; + + bool isDeviceUNCPath = PathInternal.IsDeviceUNC(originalPath); + bool isDevicePath = PathInternal.IsDevice(originalPath); + + int prefixLength = isDevicePath ? PathInternal.DevicePrefixLength : 0; + ReadOnlySpan pathWithoutPrefix = originalPath.Slice(prefixLength); + int rootLength = PathInternal.GetRootLength(pathWithoutPrefix); + int prefixAndRootLength = prefixLength + rootLength; + + // Append characters that should not be touched: + // - "C:" - known drive, unknown root + // - "\" - known root, unknown drive + // - "C:\" - known root, known drive + // - "\\.\UNC\Server\Share\" (or any device prefix) + // - "\\Server\Share\" + // The chars to skip may also include additional segments that GetRootLength considers part of the root. + if (charsToSkip > 0) + { + flippedSeparator |= AppendWithFlippedSeparators(originalPath.Slice(0, charsToSkip), ref sb); + + // Special case: UNC paths exclude the trailing separator, make sure to consider it as part of the root, + // that way we ensure initial trailing separators are removed in the main loop + if (originalPath.Length > charsToSkip && PathInternal.IsDirectorySeparator(originalPath[charsToSkip])) + { + if (sb.Length > 0 && !PathInternal.IsDirectorySeparator(sb[sb.Length - 1])) + { + flippedSeparator |= AppendWithFlippedSeparators(originalPath[charsToSkip], ref sb); + } + charsToSkip++; + prefixAndRootLength++; + } + } + + + // For the string after the device prefix, if any, save if the path is rooted, + // so we determine if double dots should be accumulated at the beginning or not. + // For example: + // - "\\Server\Share\" should be considered rooted + // - "\\.\UNC\Anything" should be considered rooted + // - "\\.\C:Users" is not rooted (separator missing after drive) + // - "\Users" is rooted (even if drive is unknown) + // - "\\.\C:..\ is not rooted" + int totalDots; + bool isRootKnown = isDeviceUNCPath || + (sb.Length > 0 && + sb.Length >= prefixAndRootLength && + PathInternal.IsDirectorySeparator(sb[prefixAndRootLength - 1])); + + // Make sure we analyze a string that begins in a separator so we ensure we can remove trailing separators after it + ReadOnlySpan path = originalPath.Slice(isRootKnown ? charsToSkip - 1 : charsToSkip); + + char c; + for (int currPos = 0; currPos < path.Length; currPos++) + { + c = path[currPos]; + + bool isSeparator = PathInternal.IsDirectorySeparator(c); + + if (isSeparator && currPos + 1 < path.Length) + { + // Skip repeated separators, take only the last one. + // e.g. "parent//child" => "parent/child", or "parent/////child" => "parent/child" + if (PathInternal.IsDirectorySeparator(path[currPos + 1])) + { + continue; + } + + // Handle redundant segments + if (IsNextSegmentOnlyDots(path, currPos, out totalDots)) + { + Debug.Assert(totalDots > 0); + + // Skip the next segment if it's a single dot (current directory). + // Only keep it if it's at the beginning of the unqualified path. + // e.g. "parent/./child" => "parent/child", or "parent/." => "parent/" or "./other" => "other" + // or "./folder" => "./folder/, or "././folder" => "./folder" + if (totalDots == 1) + { + // The only case where we add it is if we are at the beginning and the root is not "" + if (!isRootKnown && sb.Length == 0) + { + sb.Append('.'); + } + currPos++; + continue; + } + // Skip the next segment if it's a double dot (backtrack to parent directory). + // e.g. "parent/child/../grandchild" => "parent/grandchild" + else if (totalDots == 2) + { + // Unqualified paths need to check if there is a valid folder segment before reaching the beginning and root is not "\" + // Fully qualified paths should succeed to backtrack, even when reaching the root + if (!TryBacktrackToPreviousSeparator(currPos, charsToSkip, isRootKnown, ref sb)) + { + // Before appending ".." we need to check if a separator needs to be added first: + // e.g. "..\.\.." => "..\.." or "\..\.\.." => "\..\.." or "C:path\..\.." => "C:.." + if ((sb.Length > charsToSkip) && !PathInternal.IsDirectorySeparator(sb[sb.Length - 1])) + { + flippedSeparator |= AppendWithFlippedSeparators(path[currPos], ref sb); + } + // Add the double dots + sb.Append(".."); + } + currPos += 2; + continue; + } + } + } + + // special case that can only happen at the beginning: do not add the dot if path is rooted + // - "./" => "./" + // - "/." -> "/" + // - "C:." => "C:." + // - "C:\." => "C:\" + // - "//./C:folder/." => "//./C:/folder/" + if (currPos == 0 && !isRootKnown && c == '.' && sb.Length > prefixAndRootLength && (currPos + 1 >= path.Length || PathInternal.IsDirectorySeparator(path[currPos + 1]))) + { + continue; + } + + // Normalize the directory separator if needed + if (isSeparator) + { + if (sb.Length > charsToSkip && !PathInternal.IsDirectorySeparator(sb[sb.Length - 1])) + { + // Only remove trailing dots from paths without a device prefix + if (!isDevicePath) + { + TryRemoveTrailingDotsFromPreviousSegment(charsToSkip, ref sb); + } + flippedSeparator |= AppendWithFlippedSeparators(path[currPos], ref sb); + } + } + // Append all other path characters + else + { + sb.Append(c); + } + } + + // Final adjustments: + + // Paths without a device prefix cannot have trailing dots + if (!isDevicePath && sb.Length > charsToSkip) + { + // Final segments with 3 or more dots, which do not end in a separator, need to get removed + if (!PathInternal.IsDirectorySeparator(sb[sb.Length - 1]) && + IsPreviousSegmentOnlyDots(sb.AsSpan(), charsToSkip, sb.Length - 1, out totalDots) && + totalDots > 2) + { + TryBacktrackToPreviousSeparator(sb.Length - 1, charsToSkip, isRootKnown, ref sb); + // to keep this case consistent with Windows, make sure to re-add the separator + if (sb.Length > charsToSkip && !PathInternal.IsDirectorySeparator(sb[sb.Length - 1])) + { + sb.Append(PathInternal.DirectorySeparatorChar); + } + } + // Final segments that have trailing dots need the dots removed + TryRemoveTrailingDotsFromPreviousSegment(charsToSkip, ref sb); + } + + // If the buffer contained information, but the path finished with a separator, the + // separator may have been added, but we should never return a single separator for unqualified paths. + // e.g. "folder/../" => "" + if (!isRootKnown && path.Length > 0 && sb.Length == 1 && PathInternal.IsDirectorySeparator(sb[0])) + { + sb.Length = 0; + } + + return flippedSeparator || sb.Length < originalPath.Length; + } + + // Checks if there is a previous segment and it ends with dots, then removes them. + private static void TryRemoveTrailingDotsFromPreviousSegment(int charsToSkip, ref ValueStringBuilder sb) + { + if (sb.Length == 0) + { + return; + } + + bool separator = PathInternal.IsDirectorySeparator(sb[sb.Length - 1]); + if (separator) + { + // "x/" or "/x/" exit early + if (sb.Length - 2 >= 0 && !PathInternal.IsDirectorySeparator(sb[sb.Length - 2])) + { + // Temporarily remove it + sb.Length -= 1; + } + else + { + return; + } + } + + int i = sb.Length - 1; + + int trailingDots = 0; + while (i > charsToSkip && i >= 0 && !PathInternal.IsDirectorySeparator(sb[i])) + { + if (sb[i] == '.') + { + trailingDots++; + } + // If anything other than a dot is found, we finish + // The loop already knows to exit if a dir separator is found, to prevent removing dot-only segments + else + { + if (trailingDots > 0) + { + sb.Length -= trailingDots; + } + break; + } + i--; + } + + // Place back the trailing separator + if (separator) + { + sb.Append(PathInternal.DirectorySeparatorChar); + } + } + + // Checks if there is a previous segment and if it consists of only dots + private static bool IsPreviousSegmentOnlyDots(ReadOnlySpan fullPath, int charsToSkip, int currPos, out int totalDots) + { + totalDots = 0; + if (currPos < charsToSkip) + { + return false; + } + + int pos = currPos; + if (PathInternal.IsDirectorySeparator(fullPath[pos])) + { + pos -= 1; + } + + while (pos >= charsToSkip && !PathInternal.IsDirectorySeparator(fullPath[pos])) + { + if (fullPath[pos] != '.') + { + totalDots = 0; + return false; + } + pos--; + totalDots++; + } + return totalDots > 0; + } + + // 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 || PathInternal.IsDirectorySeparator(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 || PathInternal.IsDirectorySeparator(path[currPos + 3])) && + path[currPos + 1] == '.' && path[currPos + 2] == '.'; + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.cs new file mode 100644 index 00000000000000..23f08048b0d06b --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.cs @@ -0,0 +1,123 @@ +// 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. + +#nullable enable +using System.Diagnostics; +using System.Text; + +namespace System.IO +{ + internal static partial class RedundantSegmentHelper + { + // Checks if the previous segment consists of only one dot and it's the first segment after the root + // currPos is expected to be at a separator + private static bool IsPreviousSegmentFirstSingleDot(ReadOnlySpan fullPath, int charsToSkip, int currPos) + { + return currPos - 1 >= charsToSkip && + fullPath[currPos - 1] == '.' && + currPos - 2 < charsToSkip; + } + + // Checks if the segment before the specified position in the path is a ".." segment. + // currPos is expected to be located at a separator. + private static bool IsPreviousSegmentDoubleDot(ReadOnlySpan path, int rootLength, int currPos) + { + return currPos - 1 > rootLength && path[currPos - 1] == '.' && + currPos - 2 >= rootLength && path[currPos - 2] == '.' && + (currPos - 3 < rootLength || PathInternal.IsDirectorySeparator(path[currPos - 3])); + } + + // Checks if the next segment consists of only dots + private static bool IsNextSegmentOnlyDots(ReadOnlySpan path, int currPos, out int totalDots) + { + totalDots = 0; + if (currPos >= path.Length - 1) + { + return false; + } + currPos += 1; // Because path[currPos] is a separator + while (currPos < path.Length && !PathInternal.IsDirectorySeparator(path[currPos])) + { + if (path[currPos] != '.') + { + totalDots = 0; + return false; + } + currPos++; + totalDots++; + } + return totalDots > 0; + } + + // 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 != PathInternal.DirectorySeparatorChar && c == PathInternal.AltDirectorySeparatorChar) + { + c = PathInternal.DirectorySeparatorChar; + return true; + } + return false; + } + + // Inserts the specified character into the string builder + private static bool AppendWithFlippedSeparators(char c, ref ValueStringBuilder sb) + { + bool flippedSeparator = TryNormalizeSeparatorCharacter(ref c); + sb.Append(c); + return flippedSeparator; + } + + // Inserts the characters of the specified span into the string builder + private static bool AppendWithFlippedSeparators(ReadOnlySpan value, ref ValueStringBuilder sb) + { + bool flippedSeparator = false; + for (int i = 0; i < value.Length; i++) + { + flippedSeparator |= AppendWithFlippedSeparators(value[i], ref sb); + } + return flippedSeparator; + } + + // Adjusts the length of the buffer to the position of the previous valid directory separator due to a "..". + // So if the previous segment is "..", it means it wasn't processed in a previous loop on purpose + // due to the path being unqualified up to the current position, in which case we do nothing. + // e.g. "../.." => "../.." or "../../folder/../../" => "../../../" + // The previous segment is a backtrackable segment if it's a folder name. + // e.g. "folder/.." => "" or "folder/folder/./../" => "folder/" + private static bool TryBacktrackToPreviousSeparator(int currPos, int charsToSkip, bool isRootedWithSeparator, ref ValueStringBuilder sb) + { + // Special case: when nothing has been added and the root consists of "/", assume the ".." was successfully backtracked because it's a valid root + if (sb.Length == charsToSkip && isRootedWithSeparator) + { + return true; + } + else if (sb.Length > charsToSkip && + !IsPreviousSegmentDoubleDot(sb.AsSpan(), charsToSkip, sb.Length) && + !IsPreviousSegmentFirstSingleDot(sb.AsSpan(), charsToSkip, sb.Length)) + { + // Remove the separator + if (PathInternal.IsDirectorySeparator(sb[sb.Length - 1])) + { + sb.Length--; + } + // Backtrack until reaching any limit + while (sb.Length > charsToSkip && !PathInternal.IsDirectorySeparator(sb[sb.Length - 1])) + { + sb.Length--; + } + + // Remove the previous separator, it may not be re-added outside + if (sb.Length > charsToSkip && PathInternal.IsDirectorySeparator(sb[sb.Length - 1])) + { + sb.Length--; + } + + return true; + } + + return false; + } + } +} From ce0e1a70b26e326104be76b64539a625a7dce24c Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Mon, 29 Jun 2020 15:52:49 -0700 Subject: [PATCH 08/25] Existing GetFullPath methods: align test cases, add missing tests for extended prefix. --- .../tests/System/IO/PathTests_Windows.cs | 396 +++++++++++++----- 1 file changed, 280 insertions(+), 116 deletions(-) diff --git a/src/libraries/System.Runtime.Extensions/tests/System/IO/PathTests_Windows.cs b/src/libraries/System.Runtime.Extensions/tests/System/IO/PathTests_Windows.cs index 949e44c6a17235..6f3c37b39d57b6 100644 --- a/src/libraries/System.Runtime.Extensions/tests/System/IO/PathTests_Windows.cs +++ b/src/libraries/System.Runtime.Extensions/tests/System/IO/PathTests_Windows.cs @@ -390,18 +390,18 @@ public void GetFullPath_TrailingPeriodsCut(string component) public static TheoryData GetFullPath_Windows_FullyQualified => new TheoryData { - { @"C:\git\runtime", @"C:\git\runtime", @"C:\git\runtime" }, - { @"C:\git\runtime.\.\.\.\.\.", @"C:\git\runtime", @"C:\git\runtime" }, - { @"C:\git\runtime\\\.", @"C:\git\runtime", @"C:\git\runtime" }, + { @"C:\git\runtime", @"C:\git\runtime", @"C:\git\runtime" }, + { @"C:\git\runtime.\.\.\.\.\.", @"C:\git\runtime", @"C:\git\runtime" }, + { @"C:\git\runtime\\\.", @"C:\git\runtime", @"C:\git\runtime" }, { @"C:\git\runtime\..\runtime\.\..\runtime", @"C:\git\runtime", @"C:\git\runtime" }, - { @"C:\somedir\..", @"C:\git\runtime", @"C:\" }, - { @"C:\", @"C:\git\runtime", @"C:\" }, - { @"..\..\..\..", @"C:\git\runtime", @"C:\" }, - { @"C:\\\", @"C:\git\runtime", @"C:\" }, - { @"C:\..\..\", @"C:\git\runtime", @"C:\" }, - { @"C:\..\git\..\.\", @"C:\git\runtime", @"C:\" }, + { @"C:\somedir\..", @"C:\git\runtime", @"C:\" }, + { @"C:\", @"C:\git\runtime", @"C:\" }, + { @"..\..\..\..", @"C:\git\runtime", @"C:\" }, + { @"C:\\\", @"C:\git\runtime", @"C:\" }, + { @"C:\..\..\", @"C:\git\runtime", @"C:\" }, + { @"C:\..\git\..\.\", @"C:\git\runtime", @"C:\" }, { @"C:\git\runtime\..\..\..\", @"C:\git\runtime", @"C:\" }, - { @"C:\.\runtime\", @"C:\git\runtime", @"C:\runtime\" }, + { @"C:\.\runtime\", @"C:\git\runtime", @"C:\runtime\" }, }; [Theory, @@ -414,25 +414,25 @@ public void GetFullPath_BasicExpansions_Windows(string path, string basePath, st public static TheoryData GetFullPath_Windows_PathIsDevicePath => new TheoryData { // Device Paths with \\?\ wont get normalized i.e. relative segments wont get removed. - { @"\\?\C:\git\runtime.\.\.\.\.\.", @"C:\git\runtime", @"\\?\C:\git\runtime.\.\.\.\.\." }, - { @"\\?\C:\git\runtime\\\.", @"C:\git\runtime", @"\\?\C:\git\runtime\\\." }, + { @"\\?\C:\git\runtime.\.\.\.\.\.", @"C:\git\runtime", @"\\?\C:\git\runtime.\.\.\.\.\." }, + { @"\\?\C:\git\runtime\\\.", @"C:\git\runtime", @"\\?\C:\git\runtime\\\." }, { @"\\?\C:\git\runtime\..\runtime\.\..\runtime", @"C:\git\runtime", @"\\?\C:\git\runtime\..\runtime\.\..\runtime" }, { @"\\?\\somedir\..", @"C:\git\runtime", @"\\?\\somedir\.." }, - { @"\\?\", @"C:\git\runtime", @"\\?\" }, + { @"\\?\", @"C:\git\runtime", @"\\?\" }, { @"\\?\..\..\..\..", @"C:\git\runtime", @"\\?\..\..\..\.." }, - { @"\\?\\\\" , @"C:\git\runtime", @"\\?\\\\" }, - { @"\\?\C:\Foo." , @"C:\git\runtime", @"\\?\C:\Foo." }, - { @"\\?\C:\Foo " , @"C:\git\runtime", @"\\?\C:\Foo " }, + { @"\\?\\\\" , @"C:\git\runtime", @"\\?\\\\" }, + { @"\\?\C:\Foo." , @"C:\git\runtime", @"\\?\C:\Foo." }, + { @"\\?\C:\Foo " , @"C:\git\runtime", @"\\?\C:\Foo " }, - { @"\\.\C:\git\runtime.\.\.\.\.\.", @"C:\git\runtime", @"\\.\C:\git\runtime" }, - { @"\\.\C:\git\runtime\\\.", @"C:\git\runtime", @"\\.\C:\git\runtime" }, + { @"\\.\C:\git\runtime.\.\.\.\.\.", @"C:\git\runtime", @"\\.\C:\git\runtime" }, + { @"\\.\C:\git\runtime\\\.", @"C:\git\runtime", @"\\.\C:\git\runtime" }, { @"\\.\C:\git\runtime\..\runtime\.\..\runtime", @"C:\git\runtime", @"\\.\C:\git\runtime" }, { @"\\.\\somedir\..", @"C:\git\runtime", @"\\.\" }, - { @"\\.\", @"C:\git\runtime", @"\\.\" }, + { @"\\.\", @"C:\git\runtime", @"\\.\" }, { @"\\.\..\..\..\..", @"C:\git\runtime", @"\\.\" }, - { @"\\.\", @"C:\git\runtime", @"\\.\" }, - { @"\\.\C:\Foo." , @"C:\git\runtime", @"\\.\C:\Foo" }, - { @"\\.\C:\Foo " , @"C:\git\runtime", @"\\.\C:\Foo" }, + { @"\\.\", @"C:\git\runtime", @"\\.\" }, + { @"\\.\C:\Foo." , @"C:\git\runtime", @"\\.\C:\Foo" }, + { @"\\.\C:\Foo " , @"C:\git\runtime", @"\\.\C:\Foo" }, }; [Theory, @@ -441,43 +441,45 @@ public void GetFullPath_BasicExpansions_Windows_PathIsDevicePath(string path, st { Assert.Equal(expected, Path.GetFullPath(path, basePath)); Assert.Equal(expected, Path.GetFullPath(path, @"\\.\" + basePath)); + + // Paths starting with \\?\ are considered normalized and should not get modified Assert.Equal(expected, Path.GetFullPath(path, @"\\?\" + basePath)); } public static TheoryData GetFullPath_Windows_UNC => new TheoryData { - { @"foo", @"", @"foo" }, - { @"foo", @"server1", @"server1\foo" }, - { @"\foo", @"server2", @"server2\foo" }, - { @"foo", @"server3\", @"server3\foo" }, - { @"..\foo", @"server4", @"server4\..\foo" }, - { @".\foo", @"server5\share", @"server5\share\foo" }, + { @"foo", @"", @"foo" }, + { @"foo", @"server1", @"server1\foo" }, + { @"\foo", @"server2", @"server2\foo" }, + { @"foo", @"server3\", @"server3\foo" }, + { @"..\foo", @"server4", @"server4\..\foo" }, + { @".\foo", @"server5\share", @"server5\share\foo" }, { @"..\foo", @"server6\share", @"server6\share\foo" }, - { @"\foo", @"a\b\\", @"a\b\foo" }, - { @"foo", @"LOCALHOST\share8\test.txt.~SS", @"LOCALHOST\share8\test.txt.~SS\foo" }, - { @"foo", @"LOCALHOST\share9", @"LOCALHOST\share9\foo" }, - { @"foo", @"LOCALHOST\shareA\dir", @"LOCALHOST\shareA\dir\foo" }, - { @". \foo", @"LOCALHOST\shareB\", @"LOCALHOST\shareB\. \foo" }, - { @".. \foo", @"LOCALHOST\shareC\", @"LOCALHOST\shareC\.. \foo" }, - { @" \foo", @"LOCALHOST\shareD\", @"LOCALHOST\shareD\ \foo" }, - - { "foo", @"LOCALHOST\ shareE\", @"LOCALHOST\ shareE\foo" }, + { @"\foo", @"a\b\\", @"a\b\foo" }, + { @"foo", @"LOCALHOST\share8\test.txt.~SS", @"LOCALHOST\share8\test.txt.~SS\foo" }, + { @"foo", @"LOCALHOST\share9", @"LOCALHOST\share9\foo" }, + { @"foo", @"LOCALHOST\shareA\dir", @"LOCALHOST\shareA\dir\foo" }, + { @". \foo", @"LOCALHOST\shareB\", @"LOCALHOST\shareB\. \foo" }, + { @".. \foo", @"LOCALHOST\shareC\", @"LOCALHOST\shareC\.. \foo" }, + { @" \foo", @"LOCALHOST\shareD\", @"LOCALHOST\shareD\ \foo" }, + + { "foo", @"LOCALHOST\ shareE\", @"LOCALHOST\ shareE\foo" }, { "foo", @"LOCALHOST\shareF\test.txt.~SS", @"LOCALHOST\shareF\test.txt.~SS\foo" }, - { "foo", @"LOCALHOST\shareG", @"LOCALHOST\shareG\foo" }, - { "foo", @"LOCALHOST\shareH\dir", @"LOCALHOST\shareH\dir\foo" }, - { "foo", @"LOCALHOST\shareK\", @"LOCALHOST\shareK\foo" }, - { "foo", @"LOCALHOST\ shareL\", @"LOCALHOST\ shareL\foo" }, + { "foo", @"LOCALHOST\shareG", @"LOCALHOST\shareG\foo" }, + { "foo", @"LOCALHOST\shareH\dir", @"LOCALHOST\shareH\dir\foo" }, + { "foo", @"LOCALHOST\shareK\", @"LOCALHOST\shareK\foo" }, + { "foo", @"LOCALHOST\ shareL\", @"LOCALHOST\ shareL\foo" }, // Relative segments eating into the root - { @".\..\foo\..\", @"server\share", @"server\share\" }, - { @"..\foo\tmp\..\..\", @"server\share", @"server\share\" }, - { @"..\..\..\foo", @"server\share", @"server\share\foo" }, - { @"..\foo\..\..\tmp", @"server\share", @"server\share\tmp" }, - { @"..\foo", @"server\share", @"server\share\foo" }, - { @"...\\foo", @"server\share", @"server\share\...\foo" }, - { @"...\..\.\foo", @"server\share", @"server\share\foo" }, + { @".\..\foo\..\", @"server\share", @"server\share\" }, + { @"..\foo\tmp\..\..\", @"server\share", @"server\share\" }, + { @"..\..\..\foo", @"server\share", @"server\share\foo" }, + { @"..\foo\..\..\tmp", @"server\share", @"server\share\tmp" }, + { @"..\foo", @"server\share", @"server\share\foo" }, + { @"...\\foo", @"server\share", @"server\share\...\foo" }, + { @"...\..\.\foo", @"server\share", @"server\share\foo" }, { @"..\foo\tmp\..\..\..\..\..\", @"server\share", @"server\share\" }, - { @"..\..\..\..\foo", @"server\share", @"server\share\foo" }, + { @"..\..\..\..\foo", @"server\share", @"server\share\foo" }, }; [Theory, @@ -486,46 +488,89 @@ public void GetFullPath_CommonUnc_Windows(string path, string basePath, string e { Assert.Equal(@"\\" + expected, Path.GetFullPath(path, @"\\" + basePath)); Assert.Equal(@"\\.\UNC\" + expected, Path.GetFullPath(path, @"\\.\UNC\" + basePath)); + } + + public static TheoryData GetFullPath_Windows_UNC_ExtendedPrefix => new TheoryData + { + { @"foo", @"", @"foo" }, + { @"foo", @"server1", @"server1\foo" }, + { @"\foo", @"server2", @"server2\foo" }, + { @"foo", @"server3\", @"server3\foo" }, + { @"..\foo", @"server4", @"server4\..\foo" }, + { @".\foo", @"server5\share", @"server5\share\.\foo" }, + { @"..\foo", @"server6\share", @"server6\share\..\foo" }, + { @"\foo", @"a\b\\", @"a\b\foo" }, + { @"foo", @"LOCALHOST\share8\test.txt.~SS", @"LOCALHOST\share8\test.txt.~SS\foo" }, + { @"foo", @"LOCALHOST\share9", @"LOCALHOST\share9\foo" }, + { @"foo", @"LOCALHOST\shareA\dir", @"LOCALHOST\shareA\dir\foo" }, + { @". \foo", @"LOCALHOST\shareB\", @"LOCALHOST\shareB\. \foo" }, + { @".. \foo", @"LOCALHOST\shareC\", @"LOCALHOST\shareC\.. \foo" }, + { @" \foo", @"LOCALHOST\shareD\", @"LOCALHOST\shareD\ \foo" }, + + { "foo", @"LOCALHOST\ shareE\", @"LOCALHOST\ shareE\foo" }, + { "foo", @"LOCALHOST\shareF\test.txt.~SS", @"LOCALHOST\shareF\test.txt.~SS\foo" }, + { "foo", @"LOCALHOST\shareG", @"LOCALHOST\shareG\foo" }, + { "foo", @"LOCALHOST\shareH\dir", @"LOCALHOST\shareH\dir\foo" }, + { "foo", @"LOCALHOST\shareK\", @"LOCALHOST\shareK\foo" }, + { "foo", @"LOCALHOST\ shareL\", @"LOCALHOST\ shareL\foo" }, + + // Relative segments eating into the root + { @".\..\foo\..\", @"server\share", @"server\share\.\..\foo\..\" }, + { @"..\foo\tmp\..\..\", @"server\share", @"server\share\..\foo\tmp\..\..\" }, + { @"..\..\..\foo", @"server\share", @"server\share\..\..\..\foo" }, + { @"..\foo\..\..\tmp", @"server\share", @"server\share\..\foo\..\..\tmp" }, + { @"..\foo", @"server\share", @"server\share\..\foo" }, + { @"...\\foo", @"server\share", @"server\share\...\\foo" }, + { @"...\..\.\foo", @"server\share", @"server\share\...\..\.\foo" }, + { @"..\foo\tmp\..\..\..\..\..\", @"server\share", @"server\share\..\foo\tmp\..\..\..\..\..\" }, + { @"..\..\..\..\foo", @"server\share", @"server\share\..\..\..\..\foo" }, + }; + + [Theory, + MemberData(nameof(GetFullPath_Windows_UNC_ExtendedPrefix))] + public void GetFullPath_CommonUnc_Windows_ExtendedPrefix(string path, string basePath, string expected) + { + // Paths starting with \\?\ are considered normalized and should not get modified Assert.Equal(@"\\?\UNC\" + expected, Path.GetFullPath(path, @"\\?\UNC\" + basePath)); } public static TheoryData GetFullPath_Windows_CommonDevicePaths => new TheoryData { // Device paths - { "foo", @"C:\ ", @"C:\ \foo" }, - { @" \ \foo", @"C:\", @"C:\ \ \foo" }, - { @" .\foo", @"C:\", @"C:\ .\foo" }, - { @" ..\foo", @"C:\", @"C:\ ..\foo" }, - { @"...\foo", @"C:\", @"C:\...\foo" }, - - { @"foo", @"C:\\", @"C:\foo" }, - { @"foo.", @"C:\\", @"C:\foo." }, - { @"foo \git", @"C:\\", @"C:\foo \git" }, - { @"foo. \git", @"C:\\", @"C:\foo. \git" }, - { @" foo \git", @"C:\\", @"C:\ foo \git" }, - { @"foo ", @"C:\\", @"C:\foo " }, - { @"|\foo", @"C:\", @"C:\|\foo" }, - { @".\foo", @"C:\", @"C:\foo" }, - { @"..\foo", @"C:\", @"C:\foo" }, - - { @"\Foo1\.\foo", @"C:\", @"C:\Foo1\foo" }, - { @"\Foo2\..\foo", @"C:\", @"C:\foo" }, - - { @"foo", @"GLOBALROOT\", @"GLOBALROOT\foo" }, - { @"foo", @"", @"foo" }, - { @".\foo", @"", @".\foo" }, - { @"..\foo", @"", @"..\foo" }, - { @"C:", @"", @"C:\"}, + { @"foo", @"C:\ ", @"C:\ \foo" }, + { @" \ \foo", @"C:\", @"C:\ \ \foo" }, + { @" .\foo", @"C:\", @"C:\ .\foo" }, + { @" ..\foo", @"C:\", @"C:\ ..\foo" }, + { @"...\foo", @"C:\", @"C:\...\foo" }, + + { @"foo", @"C:\\", @"C:\foo" }, + { @"foo.", @"C:\\", @"C:\foo." }, + { @"foo \git", @"C:\\", @"C:\foo \git" }, + { @"foo. \git", @"C:\\", @"C:\foo. \git" }, + { @" foo \git", @"C:\\", @"C:\ foo \git" }, + { @"foo ", @"C:\\", @"C:\foo " }, + { @"|\foo", @"C:\", @"C:\|\foo" }, + { @".\foo", @"C:\", @"C:\foo" }, + { @"..\foo", @"C:\", @"C:\foo" }, + + { @"\Foo1\.\foo", @"C:\", @"C:\Foo1\foo" }, + { @"\Foo2\..\foo", @"C:\", @"C:\foo" }, + + { @"foo", @"GLOBALROOT\", @"GLOBALROOT\foo" }, + { @"foo", @"", @"foo" }, + { @".\foo", @"", @".\foo" }, + { @"..\foo", @"", @"..\foo" }, + { @"C:", @"", @"C:\"}, // Relative segments eating into the root - { @"foo", @"GLOBALROOT\", @"GLOBALROOT\foo" }, - { @"..\..\foo\..\..\", @"", @"..\" }, - { @".\..\..\..\..\foo", @"", @".\foo" }, - { @"..\foo\..\..\..\", @"", @"..\" }, - { @"\.\.\..\", @"C:\", @"C:\"}, - { @"..\..\..\foo", @"GLOBALROOT\", @"GLOBALROOT\foo" }, - { @"foo\..\..\", @"", @"foo\" }, - { @".\.\foo\..\", @"", @".\" }, + { @"foo", @"GLOBALROOT\", @"GLOBALROOT\foo" }, + { @"..\..\foo\..\..\", @"", @"..\" }, + { @".\..\..\..\..\foo", @"", @".\foo" }, + { @"..\foo\..\..\..\", @"", @"..\" }, + { @"\.\.\..\", @"C:\", @"C:\"}, + { @"..\..\..\foo", @"GLOBALROOT\", @"GLOBALROOT\foo" }, + { @"foo\..\..\", @"", @"foo\" }, + { @".\.\foo\..\", @"", @".\" }, }; [Theory, @@ -533,75 +578,120 @@ public void GetFullPath_CommonUnc_Windows(string path, string basePath, string e public void GetFullPath_CommonDevice_Windows(string path, string basePath, string expected) { Assert.Equal(@"\\.\" + expected, Path.GetFullPath(path, @"\\.\" + basePath)); + } + + public static TheoryData GetFullPath_Windows_CommonExtendedPaths => new TheoryData + { + // Device paths + { @"foo", @"C:\ ", @"C:\ \foo" }, + { @" \ \foo", @"C:\", @"C:\ \ \foo" }, + { @" .\foo", @"C:\", @"C:\ .\foo" }, + { @" ..\foo", @"C:\", @"C:\ ..\foo" }, + { @"...\foo", @"C:\", @"C:\...\foo" }, + + { @"foo", @"C:\\", @"C:\\foo" }, + { @"foo.", @"C:\\", @"C:\\foo." }, + { @"foo \git", @"C:\\", @"C:\\foo \git" }, + { @"foo. \git", @"C:\\", @"C:\\foo. \git" }, + { @" foo \git", @"C:\\", @"C:\\ foo \git" }, + { @"foo ", @"C:\\", @"C:\\foo " }, + { @"|\foo", @"C:\", @"C:\|\foo" }, + { @".\foo", @"C:\", @"C:\.\foo" }, + { @"..\foo", @"C:\", @"C:\..\foo" }, + + { @"\Foo1\.\foo", @"C:\", @"C:\Foo1\.\foo" }, + { @"\Foo2\..\foo", @"C:\", @"C:\Foo2\..\foo" }, + + { @"foo", @"GLOBALROOT\", @"GLOBALROOT\foo" }, + { @"foo", @"", @"foo" }, + { @".\foo", @"", @".\foo" }, + { @"..\foo", @"", @"..\foo" }, + { @"C:", @"", @"C:\"}, + + // Relative segments eating into the root + { @"foo", @"GLOBALROOT\", @"GLOBALROOT\foo" }, + { @"..\..\foo\..\..\", @"", @"..\..\foo\..\..\" }, + { @".\..\..\..\..\foo", @"", @".\..\..\..\..\foo" }, + { @"..\foo\..\..\..\", @"", @"..\foo\..\..\..\" }, + { @"\.\.\..\", @"C:\", @"C:\.\.\..\"}, + { @"..\..\..\foo", @"GLOBALROOT\", @"GLOBALROOT\..\..\..\foo" }, + { @"foo\..\..\", @"", @"foo\..\..\" }, + { @".\.\foo\..\", @"", @".\.\foo\..\" }, + }; + + [Theory, + MemberData(nameof(GetFullPath_Windows_CommonExtendedPaths))] + public void GetFullPath_CommonExtended_Windows(string path, string basePath, string expected) + { Assert.Equal(@"\\?\" + expected, Path.GetFullPath(path, @"\\?\" + basePath)); } public static TheoryData GetFullPath_CommonRootedWindowsData => new TheoryData { - { "", @"C:\git\runtime", @"C:\git\runtime" }, + { "", @"C:\git\runtime", @"C:\git\runtime" }, { "..", @"C:\git\runtime", @"C:\git" }, // Current drive rooted - { @"\tmp\bar", @"C:\git\runtime", @"C:\tmp\bar" }, - { @"\.\bar", @"C:\git\runtime", @"C:\bar" }, - { @"\tmp\..", @"C:\git\runtime", @"C:\" }, + { @"\tmp\bar", @"C:\git\runtime", @"C:\tmp\bar" }, + { @"\.\bar", @"C:\git\runtime", @"C:\bar" }, + { @"\tmp\..", @"C:\git\runtime", @"C:\" }, { @"\tmp\bar\..", @"C:\git\runtime", @"C:\tmp" }, { @"\tmp\bar\..", @"C:\git\runtime", @"C:\tmp" }, - { @"\", @"C:\git\runtime", @"C:\" }, + { @"\", @"C:\git\runtime", @"C:\" }, - { @"..\..\tmp\bar", @"C:\git\runtime", @"C:\tmp\bar" }, - { @"..\..\.\bar", @"C:\git\runtime", @"C:\bar" }, - { @"..\..\..\..\tmp\..", @"C:\git\runtime", @"C:\" }, + { @"..\..\tmp\bar", @"C:\git\runtime", @"C:\tmp\bar" }, + { @"..\..\.\bar", @"C:\git\runtime", @"C:\bar" }, + { @"..\..\..\..\tmp\..", @"C:\git\runtime", @"C:\" }, { @"\tmp\..\bar..\..\..", @"C:\git\runtime", @"C:\" }, - { @"\tmp\..\bar\..", @"C:\git\runtime", @"C:\" }, - { @"\.\.\..\..\", @"C:\git\runtime", @"C:\" }, + { @"\tmp\..\bar\..", @"C:\git\runtime", @"C:\" }, + { @"\.\.\..\..\", @"C:\git\runtime", @"C:\" }, // Specific drive rooted { @"C:tmp\foo\..", @"C:\git\runtime", @"C:\git\runtime\tmp" }, - { @"C:tmp\foo\.", @"C:\git\runtime", @"C:\git\runtime\tmp\foo" }, + { @"C:tmp\foo\.", @"C:\git\runtime", @"C:\git\runtime\tmp\foo" }, { @"C:tmp\foo\..", @"C:\git\runtime", @"C:\git\runtime\tmp" }, { @"C:tmp", @"C:\git\runtime", @"C:\git\runtime\tmp" }, - { @"C:", @"C:\git\runtime", @"C:\git\runtime" }, - { @"C", @"C:\git\runtime", @"C:\git\runtime\C" }, + { @"C:", @"C:\git\runtime", @"C:\git\runtime" }, + { @"C", @"C:\git\runtime", @"C:\git\runtime\C" }, { @"Z:tmp\foo\..", @"C:\git\runtime", @"Z:\tmp" }, - { @"Z:tmp\foo\.", @"C:\git\runtime", @"Z:\tmp\foo" }, + { @"Z:tmp\foo\.", @"C:\git\runtime", @"Z:\tmp\foo" }, { @"Z:tmp\foo\..", @"C:\git\runtime", @"Z:\tmp" }, { @"Z:tmp", @"C:\git\runtime", @"Z:\tmp" }, - { @"Z:", @"C:\git\runtime", @"Z:\" }, - { @"Z", @"C:\git\runtime", @"C:\git\runtime\Z" }, + { @"Z:", @"C:\git\runtime", @"Z:\" }, + { @"Z", @"C:\git\runtime", @"C:\git\runtime\Z" }, // Relative segments eating into the root { @"C:..\..\..\tmp\foo\..", @"C:\git\runtime", @"C:\tmp" }, - { @"C:tmp\..\..\foo\.", @"C:\git\runtime", @"C:\git\foo" }, - { @"C:..\..\tmp\foo\..", @"C:\git\runtime", @"C:\tmp" }, + { @"C:tmp\..\..\foo\.", @"C:\git\runtime", @"C:\git\foo" }, + { @"C:..\..\tmp\foo\..", @"C:\git\runtime", @"C:\tmp" }, { @"C:tmp\..\", @"C:\git\runtime", @"C:\git\runtime\" }, - { @"C:", @"C:\git\runtime", @"C:\git\runtime" }, - { @"C", @"C:\git\runtime", @"C:\git\runtime\C" }, + { @"C:", @"C:\git\runtime", @"C:\git\runtime" }, + { @"C", @"C:\git\runtime", @"C:\git\runtime\C" }, { @"C:tmp\..\..\..\..\foo\..", @"C:\git\runtime", @"C:\" }, - { @"C:tmp\..\..\foo\.", @"C:\", @"C:\foo" }, - { @"C:..\..\tmp\..\foo\..", @"C:\", @"C:\" }, - { @"C:tmp\..\", @"C:\", @"C:\" }, + { @"C:tmp\..\..\foo\.", @"C:\", @"C:\foo" }, + { @"C:..\..\tmp\..\foo\..", @"C:\", @"C:\" }, + { @"C:tmp\..\", @"C:\", @"C:\" }, { @"Z:tmp\foo\..", @"C:\git\runtime", @"Z:\tmp" }, - { @"Z:tmp\foo\.", @"C:\git\runtime", @"Z:\tmp\foo" }, + { @"Z:tmp\foo\.", @"C:\git\runtime", @"Z:\tmp\foo" }, { @"Z:tmp\foo\..", @"C:\git\runtime", @"Z:\tmp" }, - { @"Z:tmp", @"C:\git\runtime", @"Z:\tmp" }, - { @"Z:", @"C:\git\runtime", @"Z:\" }, - { @"Z", @"C:\git\runtime", @"C:\git\runtime\Z" }, + { @"Z:tmp", @"C:\git\runtime", @"Z:\tmp" }, + { @"Z:", @"C:\git\runtime", @"Z:\" }, + { @"Z", @"C:\git\runtime", @"C:\git\runtime\Z" }, { @"Z:..\..\..\tmp\foo\..", @"C:\git\runtime", @"Z:\tmp" }, - { @"Z:tmp\..\..\foo\.", @"C:\git\runtime", @"Z:\foo" }, - { @"Z:..\..\tmp\foo\..", @"C:\git\runtime", @"Z:\tmp" }, - { @"Z:tmp\..\", @"C:\git\runtime", @"Z:\" }, - { @"Z:", @"C:\git\runtime", @"Z:\" }, - { @"Z", @"C:\git\runtime", @"C:\git\runtime\Z" }, + { @"Z:tmp\..\..\foo\.", @"C:\git\runtime", @"Z:\foo" }, + { @"Z:..\..\tmp\foo\..", @"C:\git\runtime", @"Z:\tmp" }, + { @"Z:tmp\..\", @"C:\git\runtime", @"Z:\" }, + { @"Z:", @"C:\git\runtime", @"Z:\" }, + { @"Z", @"C:\git\runtime", @"C:\git\runtime\Z" }, { @"Z:tmp\..\..\..\..\foo\..", @"C:\git\runtime", @"Z:\" }, - { @"Z:tmp\..\..\foo\.", @"C:\", @"Z:\foo" }, - { @"Z:..\..\tmp\..\foo\..", @"C:\", @"Z:\" }, - { @"Z:tmp\..\", @"C:\", @"Z:\" }, + { @"Z:tmp\..\..\foo\.", @"C:\", @"Z:\foo" }, + { @"Z:..\..\tmp\..\foo\..", @"C:\", @"Z:\" }, + { @"Z:tmp\..\", @"C:\", @"Z:\" }, }; [Theory, @@ -610,6 +700,80 @@ public void GetFullPath_CommonUnRooted_Windows(string path, string basePath, str { Assert.Equal(expected, Path.GetFullPath(path, basePath)); Assert.Equal(@"\\.\" + expected, Path.GetFullPath(path, @"\\.\" + basePath)); + } + public static TheoryData GetFullPath_CommonRootedWindowsData_ExtendedPrefix => new TheoryData + { + { "", @"C:\git\runtime", @"C:\git\runtime" }, + { "..", @"C:\git\runtime", @"C:\git\runtime\.." }, + + // Current drive rooted + { @"\tmp\bar", @"C:\git\runtime", @"C:\tmp\bar" }, + { @"\.\bar", @"C:\git\runtime", @"C:\.\bar" }, + { @"\tmp\..", @"C:\git\runtime", @"C:\tmp\.." }, + { @"\tmp\bar\..", @"C:\git\runtime", @"C:\tmp\bar\.." }, + { @"\tmp\bar\..", @"C:\git\runtime", @"C:\tmp\bar\.." }, + { @"\", @"C:\git\runtime", @"C:\" }, + + { @"..\..\tmp\bar", @"C:\git\runtime", @"C:\git\runtime\..\..\tmp\bar" }, + { @"..\..\.\bar", @"C:\git\runtime", @"C:\git\runtime\..\..\.\bar" }, + { @"..\..\..\..\tmp\..", @"C:\git\runtime", @"C:\git\runtime\..\..\..\..\tmp\.." }, + { @"\tmp\..\bar..\..\..", @"C:\git\runtime", @"C:\tmp\..\bar..\..\.." }, + { @"\tmp\..\bar\..", @"C:\git\runtime", @"C:\tmp\..\bar\.." }, + { @"\.\.\..\..\", @"C:\git\runtime", @"C:\.\.\..\..\" }, + + // Specific drive rooted + { @"C:tmp\foo\..", @"C:\git\runtime", @"C:\git\runtime\tmp\foo\.." }, + { @"C:tmp\foo\.", @"C:\git\runtime", @"C:\git\runtime\tmp\foo\." }, + { @"C:tmp\foo\..", @"C:\git\runtime", @"C:\git\runtime\tmp\foo\.." }, + { @"C:tmp", @"C:\git\runtime", @"C:\git\runtime\tmp" }, + { @"C:", @"C:\git\runtime", @"C:\git\runtime" }, + { @"C", @"C:\git\runtime", @"C:\git\runtime\C" }, + + { @"Z:tmp\foo\..", @"C:\git\runtime", @"Z:\tmp\foo\.." }, + { @"Z:tmp\foo\.", @"C:\git\runtime", @"Z:\tmp\foo\." }, + { @"Z:tmp\foo\..", @"C:\git\runtime", @"Z:\tmp\foo\.." }, + { @"Z:tmp", @"C:\git\runtime", @"Z:\tmp" }, + { @"Z:", @"C:\git\runtime", @"Z:\" }, + { @"Z", @"C:\git\runtime", @"C:\git\runtime\Z" }, + + // Relative segments eating into the root + { @"C:..\..\..\tmp\foo\..", @"C:\git\runtime", @"C:\git\runtime\..\..\..\tmp\foo\.." }, + { @"C:tmp\..\..\foo\.", @"C:\git\runtime", @"C:\git\runtime\tmp\..\..\foo\." }, + { @"C:..\..\tmp\foo\..", @"C:\git\runtime", @"C:\git\runtime\..\..\tmp\foo\.." }, + { @"C:tmp\..\", @"C:\git\runtime", @"C:\git\runtime\tmp\..\" }, + { @"C:", @"C:\git\runtime", @"C:\git\runtime" }, + { @"C", @"C:\git\runtime", @"C:\git\runtime\C" }, + + { @"C:tmp\..\..\..\..\foo\..", @"C:\git\runtime", @"C:\git\runtime\tmp\..\..\..\..\foo\.." }, + { @"C:tmp\..\..\foo\.", @"C:\", @"C:\tmp\..\..\foo\." }, + { @"C:..\..\tmp\..\foo\..", @"C:\", @"C:\..\..\tmp\..\foo\.." }, + { @"C:tmp\..\", @"C:\", @"C:\tmp\..\" }, + + { @"Z:tmp\foo\..", @"C:\git\runtime", @"Z:\tmp\foo\.." }, + { @"Z:tmp\foo\.", @"C:\git\runtime", @"Z:\tmp\foo\." }, + { @"Z:tmp\foo\..", @"C:\git\runtime", @"Z:\tmp\foo\.." }, + { @"Z:tmp", @"C:\git\runtime", @"Z:\tmp" }, + { @"Z:", @"C:\git\runtime", @"Z:\" }, + { @"Z", @"C:\git\runtime", @"C:\git\runtime\Z" }, + + { @"Z:..\..\..\tmp\foo\..", @"C:\git\runtime", @"Z:\..\..\..\tmp\foo\.." }, + { @"Z:tmp\..\..\foo\.", @"C:\git\runtime", @"Z:\tmp\..\..\foo\." }, + { @"Z:..\..\tmp\foo\..", @"C:\git\runtime", @"Z:\..\..\tmp\foo\.." }, + { @"Z:tmp\..\", @"C:\git\runtime", @"Z:\tmp\..\" }, + { @"Z:", @"C:\git\runtime", @"Z:\" }, + { @"Z", @"C:\git\runtime", @"C:\git\runtime\Z" }, + + { @"Z:tmp\..\..\..\..\foo\..", @"C:\git\runtime", @"Z:\tmp\..\..\..\..\foo\.." }, + { @"Z:tmp\..\..\foo\.", @"C:\", @"Z:\tmp\..\..\foo\." }, + { @"Z:..\..\tmp\..\foo\..", @"C:\", @"Z:\..\..\tmp\..\foo\.." }, + { @"Z:tmp\..\", @"C:\", @"Z:\tmp\..\" }, + }; + + [Theory, + MemberData(nameof(GetFullPath_CommonRootedWindowsData_ExtendedPrefix))] + public void GetFullPath_CommonUnRooted_Windows_ExtendedPrefix(string path, string basePath, string expected) + { + // Paths starting with \\?\ are considered normalized and should not get modified Assert.Equal(@"\\?\" + expected, Path.GetFullPath(path, @"\\?\" + basePath)); } From b0ca7d62a118cc1f8402561cac399a5618031f40 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Mon, 29 Jun 2020 15:52:59 -0700 Subject: [PATCH 09/25] RedundantSegment unit tests. --- .../System.Runtime.Extensions.Tests.csproj | 5 +- .../System/IO/RedundantSegmentsTestsBase.cs | 52 ++ .../System/IO/RedundantSegmentsTests_Unix.cs | 172 ++++ .../IO/RedundantSegmentsTests_Windows.cs | 870 ++++++++++++++++++ 4 files changed, 1098 insertions(+), 1 deletion(-) create mode 100644 src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTestsBase.cs create mode 100644 src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Unix.cs create mode 100644 src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Windows.cs 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 36a8692d96ba56..a9b12bcae328b3 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 @@ -14,6 +14,9 @@ + + + @@ -77,7 +80,7 @@ - diff --git a/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTestsBase.cs b/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTestsBase.cs new file mode 100644 index 00000000000000..51805e75aa4dc2 --- /dev/null +++ b/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTestsBase.cs @@ -0,0 +1,52 @@ +// 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.Collections.Generic; +using Xunit; + +namespace System.IO.Tests +{ + public class RedundantSegmentsTestsBase + { + #region Helpers + + protected void TestString(string original, string expected) + { + string actual = Path.RemoveRedundantSegments(original); + Assert.Equal(expected, actual); + } + + protected void TestSpan(string original, string expected) + { + string actual = Path.RemoveRedundantSegments(original.AsSpan()); + Assert.Equal(expected, actual); + } + + protected void TestTry(string original, string expected) + { + Span actual = stackalloc char[expected.Length]; + Assert.True(Path.TryRemoveRedundantSegments(original.AsSpan(), actual, out int charsWritten)); + Assert.Equal(expected, actual.Slice(0, charsWritten).ToString()); + } + + protected void TestAll(string original, string expected) + { + TestString(original, expected); + TestSpan(original, expected); + TestTry(original, expected); + } + + #endregion + } + + internal static class RedundantSegmentTestsExtensions + { + internal static void Add(this List> list, string original, string qualified, string unqualified) => + list.Add(new Tuple(original, qualified, unqualified)); + internal static void Add(this List> list, string original, string qualified, string unqualified, string devicePrefix) => + list.Add(new Tuple(original, qualified, unqualified, devicePrefix)); + internal static void Add(this List> list, string original, string qualified, string unqualified, string devicePrefixUnrooted, string devicePrefixRooted) => + list.Add(new Tuple(original, qualified, unqualified, devicePrefixUnrooted, devicePrefixRooted)); + } +} diff --git a/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Unix.cs b/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Unix.cs new file mode 100644 index 00000000000000..3216d91561ebe8 --- /dev/null +++ b/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Unix.cs @@ -0,0 +1,172 @@ +// 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.Collections.Generic; +using System.Linq; +using Xunit; + +namespace System.IO.Tests +{ + [PlatformSpecific(TestPlatforms.AnyUnix)] + public class RedundantSegmentsTests_Unix : RedundantSegmentsTestsBase + { + #region Tests + + [Theory] + [MemberData(nameof(MemberData_Unix))] + public void Unix_Tests(string original, string expected) + { + TestAll(original, expected); + } + + #endregion + + #region Test data + + private static readonly Dictionary TestPaths_Unix = new Dictionary + { + // Qualified Unmodified + { @"/", @"/" }, + { @"/home", @"/home" }, + { @"/home/", @"/home/" }, + + // Qualified Modified + + // Single + { @"/.", @"/" }, + { @"/./", @"/" }, + { @"/./././", @"/" }, + + { @"/home/.", @"/home" }, + { @"/home/./", @"/home/" }, + { @"/home/./user", @"/home/user" }, + { @"/home/././user", @"/home/user" }, + + // Double + { @"/..", @"/" }, + { @"/../", @"/" }, + { @"/../../../", @"/" }, + + { @"/home/..", @"/" }, + { @"/home/../", @"/" }, + { @"/home/../user", @"/user" }, + { @"/home/../../user", @"/user" }, + { @"/home/../user/..", @"/" }, + { @"/home/../../user/../..", @"/" }, + { @"/home/.././user/../.", @"/" }, + + { @"/../folder", @"/folder" }, + { @"/../folder/", @"/folder/" }, + { @"/../folder/..", @"/" }, + { @"/../folder/../", @"/" }, + + { @"/../../folder", @"/folder" }, + { @"/../../folder/", @"/folder/" }, + { @"/../../folder/../..", @"/" }, + { @"/../../folder/../../", @"/" }, + + // Combined + { @"/.././", @"/" }, + { @"/./../", @"/" }, + + // Duplicate separators + { @"///", @"/" }, + { @"//home//", @"/home/" }, + { @"//.//", @"/" }, + { @"//..//", @"/" }, + + // Unqualified unmodified + { @"home", @"home" }, + { @"home/", @"home/" }, + { @"./home", @"./home" }, + + // Unqualified Modified + + //Single + { @".", @"." }, + { @"./", @"./" }, + { @"./.", @"." }, + { @"././", @"./" }, + + { @"folder/.", @"folder" }, + { @"folder/./", @"folder/" }, + { @"folder/./.", @"folder" }, + { @"folder/././", @"folder/" }, + + { @"./folder", @"./folder" }, + { @"./folder/", @"./folder/" }, + { @"././folder", @"./folder" }, + { @"././folder/", @"./folder/" }, + + { @"././folder/./.", @"./folder" }, + { @"././folder/././", @"./folder/" }, + + // Double + { @"..", @".." }, + { @"../", @"../" }, + { @"../..", @"../.." }, + { @"../../", @"../../" }, + + { @"folder/..", @"" }, + { @"folder/../", @"" }, + { @"foder/../..", @".." }, + { @"folder/../../", @"../" }, + + { @"../folder", @"../folder" }, + { @"../folder/", @"../folder/" }, + { @"../../folder", @"../../folder" }, + { @"../../folder/", @"../../folder/" }, + + // Combined + { @"folder/./..", @"" }, + { @"folder/../.", @"." }, + + { @"./folder/..", @"." }, + { @"./folder/../", @"./" }, + + { @"folder/subfolder/./", @"folder/subfolder/" }, + { @"folder/./subfolder", @"folder/subfolder" }, + { @"folder/../subfolder", @"subfolder" }, + { @"folder/../../subfolder", @"../subfolder" }, + { @"folder/../subfolder/../.", @"." }, + { @"folder/./subfolder/../..", @"" }, + { @"folder/./subfolder/../../", @"" }, + + // Special cases from Windows do not apply here: + // "...", "....", "dot." or "name\more" are valid segment names + + { @"/home/....", @"/home/...." }, + { @"/home/..../", @"/home/..../" }, + { @"/..../folder", @"/..../folder" }, + { @"/home/dot.", @"/home/dot." }, + { @"/home/dot./", @"/home/dot./" }, + { @"/home/.dot", @"/home/.dot" }, + { @"/home/.dot/", @"/home/.dot/" }, + + { @"/home/folder\same/subfolder", @"/home/folder\same/subfolder" }, + { @"/home/folder\same/subfolder/..", @"/home/folder\same" }, + { @"/home/folder\same/subfolder/../", @"/home/folder\same/" }, + { @"/home/folder\same/subfolder/../..", @"/home" }, + { @"/home/folder\same/subfolder/../../", @"/home/" }, + + { @"....", @"...." }, + { @"..../", @"..../" }, + { @".dot", @".dot" }, + { @".dot/", @".dot/" }, + { @"dot.", @"dot." }, + { @"dot./", @"dot./" }, + + { @"/home/..../.", @"/home/...." }, + { @"/home/...././", @"/home/..../" }, + { @"/home/..../..", @"/home" }, + { @"/home/..../../", @"/home/" }, + }; + + public static IEnumerable MemberData_Unix => + from p in TestPaths_Unix + select new object[] { p.Key, p.Value }; + + #endregion + } +} diff --git a/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Windows.cs b/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Windows.cs new file mode 100644 index 00000000000000..117b34966b016a --- /dev/null +++ b/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Windows.cs @@ -0,0 +1,870 @@ +// 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.Collections.Generic; +using System.Linq; +using Xunit; + +namespace System.IO.Tests +{ + [PlatformSpecific(TestPlatforms.Windows)] + public class RedundantSegmentsTests_Windows : RedundantSegmentsTestsBase + { + #region Tests + + #region Qualified NoRedundancy + + [Theory] + [MemberData(nameof(MemberData_DevicePrefix))] + public void Unmodified(string original) => TestAll(original, original); + + [Theory] + [MemberData(nameof(MemberData_Qualified_NoRedundancy_DriveAndRoot))] + public void Qualified_NoRedundancy(string original) => TestAll(original, original); + + [Theory] + [MemberData(nameof(MemberData_Qualified_NoRedundancy_DriveAndRoot_EdgeCases))] + public void Qualified_NoRedundancy_EdgeCases(string original, string expected) => TestAll(original, expected); + + [Theory] + [MemberData(nameof(MemberData_Qualified_NoRedundancy_Prefix_DriveAndRoot))] + public void Qualified_NoRedundancy_Prefix(string original) => TestAll(original, original); + + [Theory] + [MemberData(nameof(MemberData_Qualified_NoRedundancy_Prefix_DriveAndRoot_EdgeCases))] + public void Qualified_NoRedundancy_Prefix_EdgeCases(string original, string expected) => TestAll(original, expected); + + [Theory] + [MemberData(nameof(MemberData_ServerShare_NoRedundancy))] + public void Qualified_NoRedundancy_ServerShare(string original) => TestAll(original, original); + + [Theory] + [MemberData(nameof(MemberData_ServerShare_NoRedundancy_EdgeCases))] + public void Qualified_NoRedundancy_ServerShare_EdgeCases(string original, string expected) => TestAll(original, expected); + + [Theory] + [MemberData(nameof(MemberData_UNC_NoRedundancy))] + public void Qualified_NoRedundancy_UNC(string original) => TestAll(original, original); + + [Theory] + [MemberData(nameof(MemberData_UNC_NoRedundancy_EdgeCases))] + public void Qualified_NoRedundancy_UNC_EdgeCases(string original, string expected) => TestAll(original, expected); + + #endregion + + #region Qualified redundant + + [Theory] + [MemberData(nameof(MemberData_Qualified_Redundant_DriveAndRoot_SingleDot))] + [MemberData(nameof(MemberData_Qualified_Redundant_DriveAndRoot_DoubleDot))] + [MemberData(nameof(MemberData_Qualified_Redundant_DriveAndRoot_Combined))] + public void Qualified_Redundant(string original, string expected) => TestAll(original, expected); + + [Theory] + [MemberData(nameof(MemberData_Qualified_Redundant_DriveAndRoot_SingleDot_EdgeCases))] + [MemberData(nameof(MemberData_Qualified_Redundant_DriveAndRoot_DoubleDot_EdgeCases))] + public void Qualified_Redundant_EdgeCases(string original, string expected) => TestAll(original, expected); + + [Theory] + [MemberData(nameof(MemberData_Qualified_Redundant_Prefix_DriveAndRoot_SingleDot))] + [MemberData(nameof(MemberData_Qualified_Redundant_Prefix_DriveAndRoot_DoubleDot))] + [MemberData(nameof(MemberData_Qualified_Redundant_Prefix_DriveAndRoot_Combined))] + public void Qualified_Redundant_Prefix(string original, string expected) => TestAll(original, expected); + + [Theory] + [MemberData(nameof(MemberData_Qualified_Redundant_Prefix_DriveAndRoot_SingleDot_EdgeCases))] + [MemberData(nameof(MemberData_Qualified_Redundant_Prefix_DriveAndRoot_DoubleDot_EdgeCases))] + public void Qualified_Redundant_Prefix_EdgeCases(string original, string expected) => TestAll(original, expected); + + [Theory] + [MemberData(nameof(MemberData_ServerShare_Redundant_SingleDot))] + [MemberData(nameof(MemberData_ServerShare_Redundant_DoubleDot))] + [MemberData(nameof(MemberData_ServerShare_Redundant_Combined))] + public void Qualified_Redundant_ServerShare(string original, string expected) => TestAll(original, expected); + + [Theory] + [MemberData(nameof(MemberData_ServerShare_Redundant_SingleDot_EdgeCases))] + [MemberData(nameof(MemberData_ServerShare_Redundant_DoubleDot_EdgeCases))] + public void Qualified_Redundant_ServerShare_EdgeCases(string original, string expected) => TestAll(original, expected); + + [Theory] + [MemberData(nameof(MemberData_UNC_Redundant_SingleDot))] + [MemberData(nameof(MemberData_UNC_Redundant_DoubleDot))] + [MemberData(nameof(MemberData_UNC_Redundant_Combined))] + public void Qualified_Redundant_UNC(string original, string expected) => TestAll(original, expected); + + [Theory] + [MemberData(nameof(MemberData_UNC_Redundant_SingleDot_EdgeCases))] + [MemberData(nameof(MemberData_UNC_Redundant_DoubleDot_EdgeCases))] + public void Qualified_Redundant_UNC_EdgeCases(string original, string expected) => TestAll(original, expected); + + #endregion + + #region Unqualified NoRedundancy + + [Theory] + [MemberData(nameof(MemberData_Unqualified_NoRedundancy))] + public void Unqualified_NoRedundancy(string original) => TestAll(original, original); + + [Theory] + [MemberData(nameof(MemberData_Unqualified_NoRedundancy_EdgeCases))] + public void Unqualified_NoRedundancy_EdgeCases(string original, string expected) => TestAll(original, expected); + + [Theory] + [MemberData(nameof(MemberData_Unqualified_NoRedundancy_DrivelessRoot))] + public void Unqualified_NoRedundancy_DrivelessRoot(string original) => TestAll(original, original); + + [Theory] + [MemberData(nameof(MemberData_Unqualified_NoRedundancy_DrivelessRoot_EdgeCases))] + public void Unqualified_NoRedundancy_DrivelessRoot_EdgeCases(string original, string expected) => TestAll(original, expected); + + [Theory] + [MemberData(nameof(MemberData_Unqualified_NoRedundancy_DriveRootless))] + public void Unqualified_NoRedundancy_DriveRootless(string original) => TestAll(original, original); + + [Theory] + [MemberData(nameof(MemberData_Unqualified_NoRedundancy_DriveRootless_EdgeCases))] + public void Unqualified_NoRedundancy_DriveRootless_EdgeCases(string original, string expected) => TestAll(original, expected); + + [Theory] + [MemberData(nameof(MemberData_Unqualified_NoRedundancy_Prefix_DriveRootless))] + public void Unqualified_NoRedundancy_Prefix_DriveRootless(string original) => TestAll(original, original); + + [Theory] + [MemberData(nameof(MemberData_Unqualified_NoRedundancy_Prefix_DriveRootless_EdgeCases))] + public void Unqualified_NoRedundancy_Prefix_DriveRootless_EdgeData(string original, string expected) => TestAll(original, expected); + + #endregion + + #region Unqualified redundant + + [Theory] + [MemberData(nameof(MemberData_Unqualified_Redundant_SingleDot))] + [MemberData(nameof(MemberData_Unqualified_Redundant_DoubleDot))] + [MemberData(nameof(MemberData_Unqualified_Redundant_Combined))] + public void Unqualified_Redundant(string original, string expected) => TestAll(original, expected); + + [Theory] + [MemberData(nameof(MemberData_Unqualified_Redundant_SingleDot_EdgeCases))] + [MemberData(nameof(MemberData_Unqualified_Redundant_DoubleDot_EdgeCases))] + public void Unqualified_Redundant_EdgeCases(string original, string expected) => TestAll(original, expected); + + [Theory] + [MemberData(nameof(MemberData_Unqualified_Redundant_DrivelessRoot_SingleDot))] + [MemberData(nameof(MemberData_Unqualified_Redundant_DrivelessRoot_DoubleDot))] + public void Unqualified_Redundant_DrivelessRoot(string original, string expected) => TestAll(original, expected); + + [Theory] + [MemberData(nameof(MemberData_Unqualified_Redundant_DrivelessRoot_SingleDot_EdgeCases))] + [MemberData(nameof(MemberData_Unqualified_Redundant_DrivelessRoot_DoubleDot_EdgeCases))] + public void Unqualified_Redundant_DrivelessRoot_EdgeCases(string original, string expected) => TestAll(original, expected); + + [Theory] + [MemberData(nameof(MemberData_Unqualified_Redundant_DriveRootless_SingleDot))] + [MemberData(nameof(MemberData_Unqualified_Redundant_DriveRootless_DoubleDot))] + [MemberData(nameof(MemberData_Unqualified_Redundant_DriveRootless_Combined))] + public void Unqualified_Redundant_DriveRootless(string original, string expected) => TestAll(original, expected); + + [Theory] + [MemberData(nameof(MemberData_Unqualified_Redundant_DriveRootless_SingleDot_EdgeCases))] + [MemberData(nameof(MemberData_Unqualified_Redundant_DriveRootless_DoubleDot_EdgeCases))] + public void Unqualified_Redundant_DriveRootless_EdgeCases(string original, string expected) => TestAll(original, expected); + + [Theory] + [MemberData(nameof(MemberData_Unqualified_Redundant_Prefix_DriveRootless_SingleDot))] + [MemberData(nameof(MemberData_Unqualified_Redundant_Prefix_DriveRootless_DoubleDot))] + [MemberData(nameof(MemberData_Unqualified_Redundant_Prefix_DriveRootless_Combined))] + public void Unqualified_Redundant_Prefix_DriveRootless(string original, string expected) => TestAll(original, expected); + + [Theory] + [MemberData(nameof(MemberData_Unqualified_Redundant_Prefix_DriveRootless_SingleDot_EdgeCases))] + [MemberData(nameof(MemberData_Unqualified_Redundant_Prefix_DriveRootless_DoubleDot_EdgeCases))] + public void Unqualified_Redundant_Prefix_DriveRootless_EdgeCases(string original, string expected) => TestAll(original, expected); + + #endregion + + #endregion + + #region Test data + + private const string ServerShare = @"\\Server\Share\"; + private const string UNCServerShare = @"UNC\Server\Share\"; + private const string Prefix_Windows_Drive_Rootless = "C:"; + private const string Prefix_Windows_Driveless_Root = @"\"; + private const string Prefix_Windows_Drive_Root = Prefix_Windows_Drive_Rootless + Prefix_Windows_Driveless_Root; + private static readonly string DevicePrefix = @"\\.\"; + + + #region Device prefix + + // No matter what the string is, if it's preceded by a device prefix, we don't do anything + private static readonly string[] Suffixes = new string[] +{ + @"", + @"\", @"\\", + @"/", @"//", @"\/", @"/\", + @".", @".\", @".\\", + @".", @"./", @".//", + @"\.", @"\\.", @"\.\", @"\\.\\", + @"/.", @"//.", @"/./", @"//.//", + @"\.\.", @"\\.\\.", @"\.\.\", @"\\.\\.\\", + @"\..", @"\\..", @"\..\", @"\\..\\", + @"\..\..", @"\\..\\..", @"\..\..\", @"\\..\\..\\", + @"\.\..", @"\\.\\..", @"\.\..\", @"\\.\\..\\", + @"\..\.", @"\\..\\.", @"\..\.\", @"\\..\\.\\" + }; + private static readonly string[] ExtendedPrefixes = new string[] + { + @"\\?\", + @"\??\" + }; + private static readonly string[] TestPaths_DevicePrefix = new string[] + { + @"C", + @"C:", + @"C:\", + @"C:/", + @"C:\folder", + @"C:/folder", + @"C:A", + @"C:A", + @"C:A\folder", + @"C:A/folder", + }; + public static IEnumerable MemberData_DevicePrefix => + from prefix in ExtendedPrefixes + from s in TestPaths_DevicePrefix + from suffix in Suffixes + select new object[] { prefix + s + suffix }; + + private static readonly string[] TestPaths_DevicePrefix_UNC = new string[] + { + @"UNC", + @"UNC\Server", + @"UNC/Server", + @"UNC\Server\Share", + @"UNC/Server/Share", + @"UNC\Server\Share\folder", + @"UNC/Server/Share/folder", + }; + public static IEnumerable MemberData_DevicePrefix_UNC => + from prefix in ExtendedPrefixes + from s in TestPaths_DevicePrefix_UNC + from suffix in Suffixes + select new object[] { prefix + s + suffix }; + + #endregion + + #region No redundancy + + private static readonly string[] TestPaths_NoRedundancy = new string[] + { + @"folder", + @"folder\", + @"folder\file.txt", + @"folder\subfolder", + @"folder\subfolder\", + @"folder\subfolder\file.txt" + }; + public static IEnumerable MemberData_Qualified_NoRedundancy_DriveAndRoot => + from s in TestPaths_NoRedundancy + select new object[] { Prefix_Windows_Drive_Root + s }; + public static IEnumerable MemberData_Qualified_NoRedundancy_Prefix_DriveAndRoot => + from s in TestPaths_NoRedundancy + select new object[] { DevicePrefix + Prefix_Windows_Drive_Root + s }; + public static IEnumerable MemberData_Qualified_NoRedundancy_Prefix_DriveRootless => + from s in TestPaths_NoRedundancy + select new object[] { DevicePrefix + Prefix_Windows_Drive_Rootless + s }; + public static IEnumerable MemberData_Unqualified_NoRedundancy_DriveRootless => + from s in TestPaths_NoRedundancy + select new object[] { Prefix_Windows_Drive_Rootless + s }; + public static IEnumerable MemberData_Unqualified_NoRedundancy_Prefix_DriveRootless => + from s in TestPaths_NoRedundancy + select new object[] { DevicePrefix + Prefix_Windows_Drive_Rootless + s }; + public static IEnumerable MemberData_Unqualified_NoRedundancy => + from s in TestPaths_NoRedundancy + select new object[] { s }; + public static IEnumerable MemberData_Unqualified_NoRedundancy_DrivelessRoot => + from s in TestPaths_NoRedundancy + select new object[] { Prefix_Windows_Driveless_Root + s }; + public static IEnumerable MemberData_ServerShare_NoRedundancy => + from s in TestPaths_NoRedundancy + select new object[] { ServerShare + s }; + public static IEnumerable MemberData_UNC_NoRedundancy => + from s in TestPaths_NoRedundancy + select new object[] { DevicePrefix + UNCServerShare + s }; + + #endregion + + #region Single dot + + private static readonly List> TestPaths_Redundant_SingleDot = new List> + { + // The original and qualified strings must get the root string prefixed + // Original | Qualified | Unqualified | Device prefix + { @".", @"", @".", @"." }, + { @".\", @"", @".\", @".\" }, + { @".\.", @"", @".", @".\" }, + { @".\.\", @"", @".\", @".\" }, + + { @".\folder", @"folder", @".\folder", @".\folder" }, + { @".\folder\", @"folder\", @".\folder\", @".\folder\" }, + { @".\folder\.", @"folder", @".\folder", @".\folder" }, + { @".\folder\.\", @"folder\", @".\folder\", @".\folder\" }, + { @".\folder\.\.", @"folder", @".\folder", @".\folder" }, + { @".\folder\.\.\", @"folder\", @".\folder\", @".\folder\" }, + + { @".\.\folder", @"folder", @".\folder", @".\folder" }, + { @".\.\folder\", @"folder\", @".\folder\", @".\folder\" }, + { @".\.\folder\.", @"folder", @".\folder", @".\folder" }, + { @".\.\folder\.\", @"folder\", @".\folder\", @".\folder\" }, + { @".\.\folder\.\.", @"folder", @".\folder", @".\folder" }, + { @".\.\folder\.\.\", @"folder\", @".\folder\", @".\folder\" }, + + { @"folder\.", @"folder", @"folder", @"folder\" }, + { @"folder\.\", @"folder\", @"folder\", @"folder\" }, + { @"folder\.\.", @"folder", @"folder", @"folder\" }, + { @"folder\.\.\", @"folder\", @"folder\", @"folder\" }, + + { @"folder\subfolder\.", @"folder\subfolder", @"folder\subfolder", @"folder\subfolder" }, + { @"folder\subfolder\.\", @"folder\subfolder\", @"folder\subfolder\", @"folder\subfolder\" }, + { @"folder\subfolder\.\.", @"folder\subfolder", @"folder\subfolder", @"folder\subfolder" }, + { @"folder\subfolder\.\.\", @"folder\subfolder\", @"folder\subfolder\", @"folder\subfolder\" }, + + { @".\folder\subfolder\.", @"folder\subfolder", @".\folder\subfolder", @".\folder\subfolder" }, + { @".\folder\subfolder\.\", @"folder\subfolder\", @".\folder\subfolder\", @".\folder\subfolder\" }, + { @".\folder\subfolder\.\.", @"folder\subfolder", @".\folder\subfolder", @".\folder\subfolder" }, + { @".\folder\subfolder\.\.\", @"folder\subfolder\", @".\folder\subfolder\", @".\folder\subfolder\" }, + + { @".\.\folder\subfolder\.", @"folder\subfolder", @".\folder\subfolder", @".\folder\subfolder" }, + { @".\.\folder\subfolder\.\", @"folder\subfolder\", @".\folder\subfolder\", @".\folder\subfolder\" }, + { @".\.\folder\subfolder\.\.", @"folder\subfolder", @".\folder\subfolder", @".\folder\subfolder" }, + { @".\.\folder\subfolder\.\.\", @"folder\subfolder\", @".\folder\subfolder\", @".\folder\subfolder\" }, + + { @".\folder\.\subfolder\.", @"folder\subfolder", @".\folder\subfolder", @".\folder\subfolder" }, + { @".\folder\.\subfolder\.\", @"folder\subfolder\", @".\folder\subfolder\", @".\folder\subfolder\" }, + { @".\folder\.\subfolder\.\.", @"folder\subfolder", @".\folder\subfolder", @".\folder\subfolder" }, + { @".\folder\.\subfolder\.\.\", @"folder\subfolder\", @".\folder\subfolder\", @".\folder\subfolder\" }, + + { @".\folder\.\.\subfolder\.", @"folder\subfolder", @".\folder\subfolder", @".\folder\subfolder" }, + { @".\folder\.\.\subfolder\.\", @"folder\subfolder\", @".\folder\subfolder\", @".\folder\subfolder\" }, + { @".\folder\.\.\subfolder\.\.", @"folder\subfolder", @".\folder\subfolder", @".\folder\subfolder" }, + { @".\folder\.\.\subfolder\.\.\", @"folder\subfolder\", @".\folder\subfolder\", @".\folder\subfolder\" }, + + { @".\file.txt", @"file.txt", @".\file.txt", @".\file.txt" }, + { @".\.\file.txt", @"file.txt", @".\file.txt", @".\file.txt" }, + + { @".\folder\file.txt", @"folder\file.txt", @".\folder\file.txt", @".\folder\file.txt" }, + { @".\folder\.\file.txt", @"folder\file.txt", @".\folder\file.txt", @".\folder\file.txt" }, + { @".\folder\.\.\file.txt", @"folder\file.txt", @".\folder\file.txt", @".\folder\file.txt" }, + + { @".\.\folder\file.txt", @"folder\file.txt", @".\folder\file.txt", @".\folder\file.txt" }, + { @".\.\folder\.\file.txt", @"folder\file.txt", @".\folder\file.txt", @".\folder\file.txt" }, + { @".\.\folder\.\.\file.txt", @"folder\file.txt", @".\folder\file.txt", @".\folder\file.txt" }, + + { @"folder\.\file.txt", @"folder\file.txt", @"folder\file.txt", @"folder\file.txt" }, + { @"folder\.\.\file.txt", @"folder\file.txt", @"folder\file.txt", @"folder\file.txt" }, + + { @"folder\subfolder\.\file.txt", @"folder\subfolder\file.txt", @"folder\subfolder\file.txt", @"folder\subfolder\file.txt" }, + { @"folder\subfolder\.\.\file.txt", @"folder\subfolder\file.txt", @"folder\subfolder\file.txt", @"folder\subfolder\file.txt" }, + + { @".\folder\subfolder\.\file.txt", @"folder\subfolder\file.txt", @".\folder\subfolder\file.txt", @".\folder\subfolder\file.txt" }, + { @".\folder\subfolder\.\.\file.txt", @"folder\subfolder\file.txt", @".\folder\subfolder\file.txt", @".\folder\subfolder\file.txt" }, + + { @".\.\folder\subfolder\.\file.txt", @"folder\subfolder\file.txt", @".\folder\subfolder\file.txt", @".\folder\subfolder\file.txt" }, + { @".\.\folder\subfolder\.\.\file.txt", @"folder\subfolder\file.txt", @".\folder\subfolder\file.txt", @".\folder\subfolder\file.txt" }, + + { @".\folder\.\subfolder\.\file.txt", @"folder\subfolder\file.txt", @".\folder\subfolder\file.txt", @".\folder\subfolder\file.txt" }, + { @".\folder\.\subfolder\.\.\file.txt", @"folder\subfolder\file.txt", @".\folder\subfolder\file.txt", @".\folder\subfolder\file.txt" }, + + { @".\.\folder\.\.\subfolder\.\file.txt", @"folder\subfolder\file.txt", @".\folder\subfolder\file.txt", @".\folder\subfolder\file.txt" }, + { @".\.\folder\.\.\subfolder\.\.\file.txt", @"folder\subfolder\file.txt", @".\folder\subfolder\file.txt", @".\folder\subfolder\file.txt" }, + }; + public static IEnumerable MemberData_Qualified_Redundant_DriveAndRoot_SingleDot => + from t in TestPaths_Redundant_SingleDot + select new object[] { Prefix_Windows_Drive_Root + t.Item1, Prefix_Windows_Drive_Root + t.Item2 }; + public static IEnumerable MemberData_Qualified_Redundant_Prefix_DriveAndRoot_SingleDot => + from t in TestPaths_Redundant_SingleDot + select new object[] { DevicePrefix + Prefix_Windows_Drive_Root + t.Item1, DevicePrefix + Prefix_Windows_Drive_Root + t.Item2 }; + public static IEnumerable MemberData_Unqualified_Redundant_DriveRootless_SingleDot => + from t in TestPaths_Redundant_SingleDot + select new object[] { Prefix_Windows_Drive_Rootless + t.Item1, Prefix_Windows_Drive_Rootless + t.Item3 }; + public static IEnumerable MemberData_Unqualified_Redundant_Prefix_DriveRootless_SingleDot => + from t in TestPaths_Redundant_SingleDot + select new object[] { DevicePrefix + Prefix_Windows_Drive_Rootless + t.Item1, DevicePrefix + Prefix_Windows_Drive_Rootless + t.Item4 }; + public static IEnumerable MemberData_Unqualified_Redundant_SingleDot => + from t in TestPaths_Redundant_SingleDot + select new object[] { t.Item1, t.Item3 }; + public static IEnumerable MemberData_Unqualified_Redundant_DrivelessRoot_SingleDot => + from t in TestPaths_Redundant_SingleDot + select new object[] { Prefix_Windows_Driveless_Root + t.Item1, Prefix_Windows_Driveless_Root + t.Item2 }; + public static IEnumerable MemberData_ServerShare_Redundant_SingleDot => + from t in TestPaths_Redundant_SingleDot + select new object[] { ServerShare + t.Item1, ServerShare + t.Item2 }; + public static IEnumerable MemberData_UNC_Redundant_SingleDot => + from t in TestPaths_Redundant_SingleDot + select new object[] { DevicePrefix + UNCServerShare + t.Item1, DevicePrefix + UNCServerShare + t.Item2 }; + + #endregion + + #region Double dot + + private static readonly List> TestPaths_Redundant_DoubleDot = new List> + { + // The original and qualified strings must get the root string prefixed + // Original | Qualified | Unqualified | Device prefix + { @"..", @"", @"..", @".." }, + { @"..\", @"", @"..\", @"..\" }, + { @"..\..", @"", @"..\..", @"..\.." }, + { @"..\..\", @"", @"..\..\", @"..\..\" }, + + { @"..\folder", @"folder", @"..\folder", @"..\folder" }, + { @"..\folder\", @"folder\", @"..\folder\", @"..\folder\" }, + { @"..\folder\..", @"", @"..", @"..\" }, + { @"..\folder\..\", @"", @"..\", @"..\" }, + { @"..\folder\..\..", @"", @"..\..", @"..\.." }, + { @"..\folder\..\..\", @"", @"..\..\", @"..\..\" }, + + { @"..\..\folder", @"folder", @"..\..\folder", @"..\..\folder" }, + { @"..\..\folder\", @"folder\", @"..\..\folder\", @"..\..\folder\" }, + { @"..\..\folder\..", @"", @"..\..", @"..\.." }, + { @"..\..\folder\..\", @"", @"..\..\", @"..\..\" }, + { @"..\..\folder\..\..", @"", @"..\..\..", @"..\..\.." }, + { @"..\..\folder\..\..\", @"", @"..\..\..\", @"..\..\..\" }, + + { @"folder\..", @"", @"", @"folder\.." }, + { @"folder\..\", @"", @"", @"folder\..\" }, + { @"folder\..\..", @"", @"..", @"folder\..\.." }, + { @"folder\..\..\", @"", @"..\", @"folder\..\..\" }, + { @"folder\..\..\..", @"", @"..\..", @"folder\..\..\.." }, + { @"folder\..\..\..\", @"", @"..\..\", @"folder\..\..\..\" }, + + { @"folder\subfolder\..", @"folder", @"folder", @"folder\" }, + { @"folder\subfolder\..\", @"folder\", @"folder\", @"folder\" }, + { @"folder\subfolder\..\..", @"", @"", @"folder\.." }, + { @"folder\subfolder\..\..\", @"", @"", @"folder\..\" }, + { @"folder\subfolder\..\..\..", @"", @"..", @"folder\..\.." }, + { @"folder\subfolder\..\..\..\", @"", @"..\", @"folder\..\..\" }, + + { @"..\folder\subfolder\..", @"folder", @"..\folder", @"..\folder" }, + { @"..\folder\subfolder\..\", @"folder\", @"..\folder\", @"..\folder\" }, + { @"..\folder\subfolder\..\..", @"", @"..", @"..\" }, + { @"..\folder\subfolder\..\..\", @"", @"..\", @"..\" }, + { @"..\folder\subfolder\..\..\..", @"", @"..\..", @"..\.." }, + { @"..\folder\subfolder\..\..\..\", @"", @"..\..\", @"..\..\" }, + + { @"..\folder\..\subfolder\..", @"", @"..", @"..\" }, + { @"..\folder\..\subfolder\..\", @"", @"..\", @"..\" }, + { @"..\folder\..\subfolder\..\..", @"", @"..\..", @"..\.." }, + { @"..\folder\..\subfolder\..\..\", @"", @"..\..\", @"..\..\" }, + { @"..\folder\..\subfolder\..\..\..", @"", @"..\..\..", @"..\..\.." }, + { @"..\folder\..\subfolder\..\..\..\", @"", @"..\..\..\", @"..\..\..\" }, + + { @"..\folder\..\..\subfolder\..", @"", @"..\..", @"..\.." }, + { @"..\folder\..\..\subfolder\..\", @"", @"..\..\", @"..\..\" }, + { @"..\folder\..\..\subfolder\..\..", @"", @"..\..\..", @"..\..\.." }, + { @"..\folder\..\..\subfolder\..\..\", @"", @"..\..\..\", @"..\..\..\" }, + { @"..\folder\..\..\subfolder\..\..\..", @"", @"..\..\..\..", @"..\..\..\.." }, + { @"..\folder\..\..\subfolder\..\..\..\", @"", @"..\..\..\..\", @"..\..\..\..\" }, + + { @"..\file.txt", @"file.txt", @"..\file.txt", @"..\file.txt" }, + { @"..\..\file.txt", @"file.txt", @"..\..\file.txt", @"..\..\file.txt" }, + + { @"..\folder\file.txt", @"folder\file.txt", @"..\folder\file.txt", @"..\folder\file.txt" }, + { @"..\folder\..\file.txt", @"file.txt", @"..\file.txt", @"..\file.txt" }, + { @"..\folder\..\..\file.txt", @"file.txt", @"..\..\file.txt", @"..\..\file.txt" }, + + { @"..\..\folder\file.txt", @"folder\file.txt", @"..\..\folder\file.txt", @"..\..\folder\file.txt" }, + { @"..\..\folder\..\file.txt", @"file.txt", @"..\..\file.txt", @"..\..\file.txt" }, + { @"..\..\folder\..\..\file.txt", @"file.txt", @"..\..\..\file.txt", @"..\..\..\file.txt" }, + + { @"folder\..\file.txt", @"file.txt", @"file.txt", @"folder\..\file.txt" }, + { @"folder\..\..\file.txt", @"file.txt", @"..\file.txt", @"folder\..\..\file.txt" }, + { @"folder\..\..\..\file.txt", @"file.txt", @"..\..\file.txt", @"folder\..\..\..\file.txt" }, + + { @"folder\subfolder\..\file.txt", @"folder\file.txt", @"folder\file.txt", @"folder\file.txt" }, + { @"folder\subfolder\..\..\file.txt", @"file.txt", @"file.txt", @"folder\..\file.txt" }, + { @"folder\subfolder\..\..\..\file.txt", @"file.txt", @"..\file.txt", @"folder\..\..\file.txt" }, + + { @"..\folder\subfolder\..\file.txt", @"folder\file.txt", @"..\folder\file.txt", @"..\folder\file.txt" }, + { @"..\folder\subfolder\..\..\file.txt", @"file.txt", @"..\file.txt", @"..\file.txt" }, + { @"..\folder\subfolder\..\..\..\file.txt", @"file.txt", @"..\..\file.txt", @"..\..\file.txt" }, + + { @"..\folder\..\subfolder\..\file.txt", @"file.txt", @"..\file.txt", @"..\file.txt" }, + { @"..\folder\..\subfolder\..\..\file.txt", @"file.txt", @"..\..\file.txt", @"..\..\file.txt" }, + { @"..\folder\..\subfolder\..\..\..\file.txt", @"file.txt", @"..\..\..\file.txt", @"..\..\..\file.txt" }, + + { @"..\folder\..\..\subfolder\..\file.txt", @"file.txt", @"..\..\file.txt", @"..\..\file.txt" }, + { @"..\folder\..\..\subfolder\..\..\file.txt", @"file.txt", @"..\..\..\file.txt", @"..\..\..\file.txt" }, + { @"..\folder\..\..\subfolder\..\..\..\file.txt", @"file.txt", @"..\..\..\..\file.txt", @"..\..\..\..\file.txt" }, + }; + public static IEnumerable MemberData_Qualified_Redundant_DriveAndRoot_DoubleDot => + from t in TestPaths_Redundant_DoubleDot + select new object[] { Prefix_Windows_Drive_Root + t.Item1, Prefix_Windows_Drive_Root + t.Item2 }; + public static IEnumerable MemberData_Qualified_Redundant_Prefix_DriveAndRoot_DoubleDot => + from t in TestPaths_Redundant_DoubleDot + select new object[] { DevicePrefix + Prefix_Windows_Drive_Root + t.Item1, DevicePrefix + Prefix_Windows_Drive_Root + t.Item2 }; + public static IEnumerable MemberData_Unqualified_Redundant_DriveRootless_DoubleDot => + from t in TestPaths_Redundant_DoubleDot + select new object[] { Prefix_Windows_Drive_Rootless + t.Item1, Prefix_Windows_Drive_Rootless + t.Item3 }; + public static IEnumerable MemberData_Unqualified_Redundant_Prefix_DriveRootless_DoubleDot => + from t in TestPaths_Redundant_DoubleDot + select new object[] { DevicePrefix + Prefix_Windows_Drive_Rootless + t.Item1, DevicePrefix + Prefix_Windows_Drive_Rootless + t.Item4 }; + public static IEnumerable MemberData_Unqualified_Redundant_DoubleDot => + from t in TestPaths_Redundant_DoubleDot + select new object[] { t.Item1, t.Item3 }; + public static IEnumerable MemberData_Unqualified_Redundant_DrivelessRoot_DoubleDot => + from t in TestPaths_Redundant_DoubleDot + select new object[] { Prefix_Windows_Driveless_Root + t.Item1, Prefix_Windows_Driveless_Root + t.Item2 }; + public static IEnumerable MemberData_ServerShare_Redundant_DoubleDot => + from t in TestPaths_Redundant_DoubleDot + select new object[] { ServerShare + t.Item1, ServerShare + t.Item2 }; + public static IEnumerable MemberData_UNC_Redundant_DoubleDot => + from t in TestPaths_Redundant_DoubleDot + select new object[] { DevicePrefix + UNCServerShare + t.Item1, DevicePrefix + UNCServerShare + t.Item2 }; + + #endregion + + #region Combined: single + double dot + + private static readonly List> TestPaths_Redundant_Combined = new List> + { + // The original and qualified strings must get the root string prefixed + // Original | Qualified | Unqualified | Device prefix + { @"..\.", @"", @"..", @"..\" }, + { @"..\.\", @"", @"..\", @"..\" }, + { @"..\..\.", @"", @"..\..", @"..\.." }, + { @"..\..\.\", @"", @"..\..\", @"..\..\" }, + + { @".\..\.", @"", @".\..", @".\.." }, + { @".\..\.\", @"", @".\..\", @".\..\" }, + { @".\..\..\.", @"", @".\..\..", @".\..\.." }, + { @".\..\..\.\", @"", @".\..\..\", @".\..\..\" }, + + { @"..\.\file.txt", @"file.txt", @"..\file.txt", @"..\file.txt" }, + { @"..\.\..\file.txt", @"file.txt", @"..\..\file.txt", @"..\..\file.txt" }, + { @"..\..\.\file.txt", @"file.txt", @"..\..\file.txt", @"..\..\file.txt" }, + { @"..\.\..\.\file.txt", @"file.txt", @"..\..\file.txt", @"..\..\file.txt" }, + + { @".\..\.\file.txt", @"file.txt", @".\..\file.txt", @".\..\file.txt" }, + { @".\..\.\..\file.txt", @"file.txt", @".\..\..\file.txt", @".\..\..\file.txt" }, + { @".\..\..\.\file.txt", @"file.txt", @".\..\..\file.txt", @".\..\..\file.txt" }, + { @".\..\.\..\.\file.txt", @"file.txt", @".\..\..\file.txt", @".\..\..\file.txt" }, + + { @"..\.\folder", @"folder", @"..\folder", @"..\folder" }, + { @"..\.\folder\", @"folder\", @"..\folder\", @"..\folder\" }, + { @"..\.\folder\..", @"", @"..", @"..\" }, + { @"..\.\folder\..\", @"", @"..\", @"..\" }, + { @"..\.\folder\..\..", @"", @"..\..", @"..\.." }, + { @"..\.\folder\..\..\", @"", @"..\..\", @"..\..\" }, + + { @"..\folder\.", @"folder", @"..\folder", @"..\folder" }, + { @"..\folder\.\", @"folder\", @"..\folder\", @"..\folder\" }, + { @"..\folder\.\..", @"", @"..", @"..\" }, + { @"..\folder\.\..\", @"", @"..\", @"..\" }, + { @"..\folder\.\..\..", @"", @"..\..", @"..\.." }, + { @"..\folder\.\..\..\", @"", @"..\..\", @"..\..\" }, + + { @"folder\.\subfolder\..", @"folder", @"folder", @"folder\" }, + { @"folder\.\subfolder\..\", @"folder\", @"folder\", @"folder\" }, + { @"folder\.\subfolder\..\..", @"", @"", @"folder\.." }, + { @"folder\.\subfolder\..\..\", @"", @"", @"folder\..\" }, + { @"folder\.\subfolder\..\..\..", @"", @"..", @"folder\..\.." }, + { @"folder\.\subfolder\..\..\..\", @"", @"..\", @"folder\..\..\" }, + + { @".\folder\.\subfolder\..", @"folder", @".\folder", @".\folder" }, + { @".\folder\.\subfolder\..\", @"folder\", @".\folder\", @".\folder\" }, + { @".\folder\.\subfolder\..\..", @"", @".", @".\" }, + { @".\folder\.\subfolder\..\..\", @"", @".\", @".\" }, + { @".\folder\.\subfolder\..\..\..", @"", @".\..", @".\.." }, + { @".\folder\.\subfolder\..\..\..\", @"", @".\..\", @".\..\" }, + }; + public static IEnumerable MemberData_Qualified_Redundant_DriveAndRoot_Combined => + from t in TestPaths_Redundant_Combined + select new object[] { Prefix_Windows_Drive_Root + t.Item1, Prefix_Windows_Drive_Root + t.Item2 }; + public static IEnumerable MemberData_Qualified_Redundant_Prefix_DriveAndRoot_Combined => + from t in TestPaths_Redundant_Combined + select new object[] { DevicePrefix + Prefix_Windows_Drive_Root + t.Item1, DevicePrefix + Prefix_Windows_Drive_Root + t.Item2 }; + public static IEnumerable MemberData_Unqualified_Redundant_DriveRootless_Combined => + from t in TestPaths_Redundant_Combined + select new object[] { Prefix_Windows_Drive_Rootless + t.Item1, Prefix_Windows_Drive_Rootless + t.Item3 }; + public static IEnumerable MemberData_Unqualified_Redundant_Prefix_DriveRootless_Combined => + from t in TestPaths_Redundant_Combined + select new object[] { DevicePrefix + Prefix_Windows_Drive_Rootless + t.Item1, DevicePrefix + Prefix_Windows_Drive_Rootless + t.Item4 }; + public static IEnumerable MemberData_Unqualified_Redundant_Combined => + from t in TestPaths_Redundant_Combined + select new object[] { t.Item1, t.Item3 }; + public static IEnumerable MemberData_Unqualified_Redundant_DrivelessRoot_Combined => + from t in TestPaths_Redundant_Combined + select new object[] { Prefix_Windows_Driveless_Root + t.Item1, Prefix_Windows_Driveless_Root + t.Item3 }; + public static IEnumerable MemberData_ServerShare_Redundant_Combined => + from t in TestPaths_Redundant_Combined + select new object[] { ServerShare + t.Item1, ServerShare + t.Item2 }; + public static IEnumerable MemberData_UNC_Redundant_Combined => + from t in TestPaths_Redundant_Combined + select new object[] { DevicePrefix + UNCServerShare + t.Item1, DevicePrefix + UNCServerShare + t.Item2 }; + + #endregion + + #region Edge cases: more than two dots, paths with trailing dot + + private static readonly List> TestPaths_NoRedundancy_EdgeCases = new List> + { + // Original | Qualified | Unqualified | Device prefix + + // Trailing more than 2 dots + { @"...", @"", @"", @"..." }, + { @"...\", @"...\", @"...\", @"...\" }, + { @"folder\...", @"folder\", @"folder\", @"folder\..." }, + { @"folder\...\", @"folder\...\", @"folder\...\", @"folder\...\" }, + + { @"....", @"", @"", @"...." }, + { @"....\", @"....\", @"....\", @"....\" }, + { @"folder\....", @"folder\", @"folder\", @"folder\...." }, + { @"folder\....\", @"folder\....\", @"folder\....\", @"folder\....\" }, + + // Starting with more than 2 dots + { @"...\subfolder", @"...\subfolder", @"...\subfolder", @"...\subfolder" }, + { @"...\subfolder\", @"...\subfolder\", @"...\subfolder\", @"...\subfolder\" }, + { @"...\file.txt", @"...\file.txt", @"...\file.txt", @"...\file.txt" }, + { @"...\subfolder\file.txt", @"...\subfolder\file.txt", @"...\subfolder\file.txt", @"...\subfolder\file.txt" }, + + { @"....\subfolder", @"....\subfolder", @"....\subfolder", @"....\subfolder" }, + { @"....\subfolder\", @"....\subfolder\", @"....\subfolder\", @"....\subfolder\" }, + { @"....\file.txt", @"....\file.txt", @"....\file.txt", @"....\file.txt" }, + { @"....\subfolder\file.txt", @"....\subfolder\file.txt", @"....\subfolder\file.txt", @"....\subfolder\file.txt" }, + + // file/folder ending in dot + { @"dot.", @"dot", @"dot", @"dot." }, + { @"dot.\", @"dot\", @"dot\", @"dot.\" }, + { @"folder\dot.", @"folder\dot", @"folder\dot", @"folder\dot." }, + { @"folder\dot.\", @"folder\dot\", @"folder\dot\", @"folder\dot.\" }, + { @"dot.\subfolder", @"dot\subfolder", @"dot\subfolder", @"dot.\subfolder" }, + { @"dot.\subfolder\", @"dot\subfolder\", @"dot\subfolder\", @"dot.\subfolder\" }, + + { @"dot.\file.txt", @"dot\file.txt", @"dot\file.txt", @"dot.\file.txt" }, + { @"dot.\subfolder\file.txt", @"dot\subfolder\file.txt", @"dot\subfolder\file.txt", @"dot.\subfolder\file.txt" }, + }; + public static IEnumerable MemberData_Qualified_NoRedundancy_DriveAndRoot_EdgeCases => + from t in TestPaths_NoRedundancy_EdgeCases + select new object[] { Prefix_Windows_Drive_Root + t.Item1, Prefix_Windows_Drive_Root + t.Item2 }; + public static IEnumerable MemberData_Qualified_NoRedundancy_Prefix_DriveAndRoot_EdgeCases => + from t in TestPaths_NoRedundancy_EdgeCases + select new object[] { DevicePrefix + Prefix_Windows_Drive_Root + t.Item1, DevicePrefix + Prefix_Windows_Drive_Root + t.Item4 }; + public static IEnumerable MemberData_Unqualified_NoRedundancy_DriveRootless_EdgeCases => + from t in TestPaths_NoRedundancy_EdgeCases + select new object[] { Prefix_Windows_Drive_Rootless + t.Item1, Prefix_Windows_Drive_Rootless + t.Item3 }; + public static IEnumerable MemberData_Unqualified_NoRedundancy_Prefix_DriveRootless_EdgeCases => + from t in TestPaths_NoRedundancy_EdgeCases + select new object[] { DevicePrefix + Prefix_Windows_Drive_Rootless + t.Item1, DevicePrefix + Prefix_Windows_Drive_Rootless + t.Item4 }; + public static IEnumerable MemberData_Unqualified_NoRedundancy_EdgeCases => + from t in TestPaths_NoRedundancy_EdgeCases + select new object[] { t.Item1, t.Item3 }; + public static IEnumerable MemberData_Unqualified_NoRedundancy_DrivelessRoot_EdgeCases => + from t in TestPaths_NoRedundancy_EdgeCases + select new object[] { Prefix_Windows_Driveless_Root + t.Item1, Prefix_Windows_Driveless_Root + t.Item2 }; + public static IEnumerable MemberData_ServerShare_NoRedundancy_EdgeCases => + from t in TestPaths_NoRedundancy_EdgeCases + select new object[] { ServerShare + t.Item1, ServerShare + t.Item2 }; // Qualified but not a device path + public static IEnumerable MemberData_UNC_NoRedundancy_EdgeCases => + from t in TestPaths_NoRedundancy_EdgeCases + select new object[] { DevicePrefix + UNCServerShare + t.Item1, DevicePrefix + UNCServerShare + t.Item4 }; + + #endregion + + #region Edge cases + single dot + + private static readonly List> TestPaths_Redundant_SingleDot_EdgeCases = new List> + { + // The original and qualified strings must get the root string prefixed + // Original | Qualified | Unqualified | Device unrooted | Device rooted + + // Folder with 3 dots + { @"...\.", @"", @"", @"...\", @"..." }, + { @"...\.\", @"...\", @"...\", @"...\", @"...\" }, + { @"...\.\.", @"", @"", @"...\", @"..." }, + { @"...\.\.\", @"...\", @"...\", @"...\", @"...\" }, + + { @"...\subfolder\.", @"...\subfolder", @"...\subfolder", @"...\subfolder", @"...\subfolder" }, + { @"...\subfolder\.\", @"...\subfolder\", @"...\subfolder\", @"...\subfolder\", @"...\subfolder\" }, + { @"...\subfolder\.\.", @"...\subfolder", @"...\subfolder", @"...\subfolder", @"...\subfolder" }, + { @"...\subfolder\.\.\", @"...\subfolder\", @"...\subfolder\", @"...\subfolder\", @"...\subfolder\" }, + { @"...\.\subfolder\.\", @"...\subfolder\", @"...\subfolder\", @"...\subfolder\", @"...\subfolder\" }, + { @"...\.\subfolder\.\.", @"...\subfolder", @"...\subfolder", @"...\subfolder", @"...\subfolder" }, + { @"...\.\subfolder\.\.\", @"...\subfolder\", @"...\subfolder\", @"...\subfolder\", @"...\subfolder\" }, + + { @"...\.\file.txt", @"...\file.txt", @"...\file.txt", @"...\file.txt", @"...\file.txt" }, + { @"...\.\.\file.txt", @"...\file.txt", @"...\file.txt", @"...\file.txt", @"...\file.txt" }, + { @"...\subfolder\.\file.txt", @"...\subfolder\file.txt", @"...\subfolder\file.txt", @"...\subfolder\file.txt", @"...\subfolder\file.txt" }, + { @"...\subfolder\.\.\file.txt", @"...\subfolder\file.txt", @"...\subfolder\file.txt", @"...\subfolder\file.txt", @"...\subfolder\file.txt" }, + { @"...\.\subfolder\.\file.txt", @"...\subfolder\file.txt", @"...\subfolder\file.txt", @"...\subfolder\file.txt", @"...\subfolder\file.txt" }, + { @"...\.\subfolder\.\.\file.txt", @"...\subfolder\file.txt", @"...\subfolder\file.txt", @"...\subfolder\file.txt", @"...\subfolder\file.txt" }, + + // Folder with 4 dots + { @"....\.", @"", @"", @"....\", @"...." }, + { @"....\.\", @"....\", @"....\", @"....\", @"....\" }, + { @"....\.\.", @"", @"", @"....\", @"...." }, + { @"....\.\.\", @"....\", @"....\", @"....\", @"....\" }, + + { @"....\subfolder\.", @"....\subfolder", @"....\subfolder", @"....\subfolder", @"....\subfolder" }, + { @"....\subfolder\.\", @"....\subfolder\", @"....\subfolder\", @"....\subfolder\", @"....\subfolder\" }, + { @"....\subfolder\.\.", @"....\subfolder", @"....\subfolder", @"....\subfolder", @"....\subfolder" }, + { @"....\subfolder\.\.\", @"....\subfolder\", @"....\subfolder\", @"....\subfolder\", @"....\subfolder\" }, + { @"....\.\subfolder\.\", @"....\subfolder\", @"....\subfolder\", @"....\subfolder\", @"....\subfolder\" }, + { @"....\.\subfolder\.\.", @"....\subfolder", @"....\subfolder", @"....\subfolder", @"....\subfolder" }, + { @"....\.\subfolder\.\.\", @"....\subfolder\", @"....\subfolder\", @"....\subfolder\", @"....\subfolder\" }, + + { @"....\.\file.txt", @"....\file.txt", @"....\file.txt", @"....\file.txt", @"....\file.txt" }, + { @"....\.\.\file.txt", @"....\file.txt", @"....\file.txt", @"....\file.txt", @"....\file.txt" }, + { @"....\subfolder\.\file.txt", @"....\subfolder\file.txt", @"....\subfolder\file.txt", @"....\subfolder\file.txt", @"....\subfolder\file.txt" }, + { @"....\subfolder\.\.\file.txt", @"....\subfolder\file.txt", @"....\subfolder\file.txt", @"....\subfolder\file.txt", @"....\subfolder\file.txt" }, + { @"....\.\subfolder\.\file.txt", @"....\subfolder\file.txt", @"....\subfolder\file.txt", @"....\subfolder\file.txt", @"....\subfolder\file.txt" }, + { @"....\.\subfolder\.\.\file.txt", @"....\subfolder\file.txt", @"....\subfolder\file.txt", @"....\subfolder\file.txt", @"....\subfolder\file.txt" }, + + // file/folder ending in dot + { @"dot.\.", @"dot", @"dot", @"dot.\", @"dot." }, + { @"dot.\.\", @"dot\", @"dot\", @"dot.\", @"dot.\" }, + { @"dot.\.\.", @"dot", @"dot", @"dot.\", @"dot." }, + { @"dot.\.\.\", @"dot\", @"dot\", @"dot.\", @"dot.\" }, + + { @"dot.\subfolder\.", @"dot\subfolder", @"dot\subfolder", @"dot.\subfolder", @"dot.\subfolder" }, + { @"dot.\subfolder\.\", @"dot\subfolder\", @"dot\subfolder\", @"dot.\subfolder\", @"dot.\subfolder\" }, + { @"dot.\subfolder\.\.", @"dot\subfolder", @"dot\subfolder", @"dot.\subfolder", @"dot.\subfolder" }, + { @"dot.\subfolder\.\.\", @"dot\subfolder\", @"dot\subfolder\", @"dot.\subfolder\", @"dot.\subfolder\" }, + { @"dot.\.\subfolder\.\", @"dot\subfolder\", @"dot\subfolder\", @"dot.\subfolder\", @"dot.\subfolder\" }, + { @"dot.\.\subfolder\.\.", @"dot\subfolder", @"dot\subfolder", @"dot.\subfolder", @"dot.\subfolder" }, + { @"dot.\.\subfolder\.\.\", @"dot\subfolder\", @"dot\subfolder\", @"dot.\subfolder\", @"dot.\subfolder\" }, + + { @"dot.\.\file.txt", @"dot\file.txt", @"dot\file.txt", @"dot.\file.txt", @"dot.\file.txt" }, + { @"dot.\.\.\file.txt", @"dot\file.txt", @"dot\file.txt", @"dot.\file.txt", @"dot.\file.txt" }, + { @"dot.\subfolder\.\file.txt", @"dot\subfolder\file.txt", @"dot\subfolder\file.txt", @"dot.\subfolder\file.txt", @"dot.\subfolder\file.txt" }, + { @"dot.\subfolder\.\.\file.txt", @"dot\subfolder\file.txt", @"dot\subfolder\file.txt", @"dot.\subfolder\file.txt", @"dot.\subfolder\file.txt" }, + { @"dot.\.\subfolder\.\file.txt", @"dot\subfolder\file.txt", @"dot\subfolder\file.txt", @"dot.\subfolder\file.txt", @"dot.\subfolder\file.txt" }, + { @"dot.\.\subfolder\.\.\file.txt", @"dot\subfolder\file.txt", @"dot\subfolder\file.txt", @"dot.\subfolder\file.txt", @"dot.\subfolder\file.txt" }, + }; + public static IEnumerable MemberData_Qualified_Redundant_DriveAndRoot_SingleDot_EdgeCases => + from t in TestPaths_Redundant_SingleDot_EdgeCases + select new object[] { Prefix_Windows_Drive_Root + t.Item1, Prefix_Windows_Drive_Root + t.Item2 }; + public static IEnumerable MemberData_Qualified_Redundant_Prefix_DriveAndRoot_SingleDot_EdgeCases => + from t in TestPaths_Redundant_SingleDot_EdgeCases + select new object[] { DevicePrefix + Prefix_Windows_Drive_Root + t.Item1, DevicePrefix + Prefix_Windows_Drive_Root + t.Item5 }; + public static IEnumerable MemberData_Unqualified_Redundant_DriveRootless_SingleDot_EdgeCases => + from t in TestPaths_Redundant_SingleDot_EdgeCases + select new object[] { Prefix_Windows_Drive_Rootless + t.Item1, Prefix_Windows_Drive_Rootless + t.Item3 }; + public static IEnumerable MemberData_Unqualified_Redundant_Prefix_DriveRootless_SingleDot_EdgeCases => + from t in TestPaths_Redundant_SingleDot_EdgeCases + select new object[] { DevicePrefix + Prefix_Windows_Drive_Rootless + t.Item1, DevicePrefix + Prefix_Windows_Drive_Rootless + t.Item4 }; + public static IEnumerable MemberData_Unqualified_Redundant_SingleDot_EdgeCases => + from t in TestPaths_Redundant_SingleDot_EdgeCases + select new object[] { t.Item1, t.Item3 }; + public static IEnumerable MemberData_Unqualified_Redundant_DrivelessRoot_SingleDot_EdgeCases => + from t in TestPaths_Redundant_SingleDot_EdgeCases + select new object[] { Prefix_Windows_Driveless_Root + t.Item1, Prefix_Windows_Driveless_Root + t.Item2 }; + public static IEnumerable MemberData_ServerShare_Redundant_SingleDot_EdgeCases => + from t in TestPaths_Redundant_SingleDot_EdgeCases + select new object[] { ServerShare + t.Item1, ServerShare + t.Item2 }; // Qualified but not a device path + public static IEnumerable MemberData_UNC_Redundant_SingleDot_EdgeCases => + from t in TestPaths_Redundant_SingleDot_EdgeCases + select new object[] { DevicePrefix + UNCServerShare + t.Item1, DevicePrefix + UNCServerShare + t.Item5 }; + + #endregion + + #region Edge cases + double dot + + private static readonly List> TestPaths_Redundant_DoubleDot_EdgeCases = new List> + { + // Original | Qualified | Unqualified | Device prefix unrooted | Device prefix rooted + // Folder with 3 dots + { @"...\..", @"", @"", @"...\..", @"" }, + { @"...\..\", @"", @"", @"...\..\", @"" }, + { @"...\..\..", @"", @"..", @"...\..\..", @"" }, + { @"...\..\..\", @"", @"..\", @"...\..\..\", @"" }, + + { @"...\subfolder\..", @"", @"", @"...\", @"..." }, + { @"...\subfolder\..\", @"...\", @"...\", @"...\", @"...\" }, + { @"...\subfolder\..\..", @"", @"", @"...\..", @"" }, + { @"...\subfolder\..\..\", @"", @"", @"...\..\", @"" }, + { @"...\..\subfolder\..\", @"", @"", @"...\..\", @"" }, + { @"...\..\subfolder\..\..", @"", @"..", @"...\..\..", @"" }, + { @"...\..\subfolder\..\..\", @"", @"..\", @"...\..\..\", @"" }, + + { @"...\..\file.txt", @"file.txt", @"file.txt", @"...\..\file.txt", @"file.txt" }, + { @"...\..\..\file.txt", @"file.txt", @"..\file.txt", @"...\..\..\file.txt", @"file.txt" }, + { @"...\subfolder\..\file.txt", @"...\file.txt", @"...\file.txt", @"...\file.txt", @"...\file.txt" }, + { @"...\subfolder\..\..\file.txt", @"file.txt", @"file.txt", @"...\..\file.txt", @"file.txt" }, + { @"...\..\subfolder\..\file.txt", @"file.txt", @"file.txt", @"...\..\file.txt", @"file.txt" }, + { @"...\..\subfolder\..\..\file.txt", @"file.txt", @"..\file.txt", @"...\..\..\file.txt", @"file.txt" }, + + // Folder with 4 dots + { @"....\..", @"", @"", @"....\..", @"" }, + { @"....\..\", @"", @"", @"....\..\", @"" }, + { @"....\..\..", @"", @"..", @"....\..\..", @"" }, + { @"....\..\..\", @"", @"..\", @"....\..\..\", @"" }, + + { @"....\subfolder\..", @"", @"", @"....\", @"...." }, + { @"....\subfolder\..\", @"....\", @"....\", @"....\", @"....\" }, + { @"....\subfolder\..\..", @"", @"", @"....\..", @"" }, + { @"....\subfolder\..\..\", @"", @"", @"....\..\", @"" }, + { @"....\..\subfolder\..\", @"", @"", @"....\..\", @"" }, + { @"....\..\subfolder\..\..", @"", @"..", @"....\..\..", @"" }, + { @"....\..\subfolder\..\..\", @"", @"..\", @"....\..\..\", @"" }, + + { @"....\..\file.txt", @"file.txt", @"file.txt", @"....\..\file.txt", @"file.txt" }, + { @"....\..\..\file.txt", @"file.txt", @"..\file.txt", @"....\..\..\file.txt", @"file.txt" }, + { @"....\subfolder\..\file.txt", @"....\file.txt", @"....\file.txt", @"....\file.txt", @"....\file.txt" }, + { @"....\subfolder\..\..\file.txt", @"file.txt", @"file.txt", @"....\..\file.txt", @"file.txt" }, + { @"....\..\subfolder\..\file.txt", @"file.txt", @"file.txt", @"....\..\file.txt", @"file.txt" }, + { @"....\..\subfolder\..\..\file.txt", @"file.txt", @"..\file.txt", @"....\..\..\file.txt", @"file.txt" }, + + // file/folder ending in dot + { @"dot.\..", @"", @"", @"dot.\..", @"" }, + { @"dot.\..\", @"", @"", @"dot.\..\", @"" }, + { @"dot.\..\..", @"", @"..", @"dot.\..\..", @"" }, + { @"dot.\..\..\", @"", @"..\", @"dot.\..\..\", @"" }, + + { @"dot.\subfolder\..", @"dot", @"dot", @"dot.\", @"dot." }, + { @"dot.\subfolder\..\", @"dot\", @"dot\", @"dot.\", @"dot.\" }, + { @"dot.\subfolder\..\..", @"", @"", @"dot.\..", @"" }, + { @"dot.\subfolder\..\..\", @"", @"", @"dot.\..\", @"" }, + { @"dot.\..\subfolder\..\", @"", @"", @"dot.\..\", @"" }, + { @"dot.\..\subfolder\..\..", @"", @"..", @"dot.\..\..", @"" }, + { @"dot.\..\subfolder\..\..\", @"", @"..\", @"dot.\..\..\", @"" }, + + { @"dot.\..\file.txt", @"file.txt", @"file.txt", @"dot.\..\file.txt", @"file.txt" }, + { @"dot.\..\..\file.txt", @"file.txt", @"..\file.txt", @"dot.\..\..\file.txt", @"file.txt" }, + { @"dot.\subfolder\..\file.txt", @"dot\file.txt", @"dot\file.txt", @"dot.\file.txt", @"dot.\file.txt" }, + { @"dot.\subfolder\..\..\file.txt", @"file.txt", @"file.txt", @"dot.\..\file.txt", @"file.txt" }, + { @"dot.\..\subfolder\..\file.txt", @"file.txt", @"file.txt", @"dot.\..\file.txt", @"file.txt" }, + { @"dot.\..\subfolder\..\..\file.txt", @"file.txt", @"..\file.txt", @"dot.\..\..\file.txt", @"file.txt" }, + }; + public static IEnumerable MemberData_Qualified_Redundant_DriveAndRoot_DoubleDot_EdgeCases => + from t in TestPaths_Redundant_DoubleDot_EdgeCases + select new object[] { Prefix_Windows_Drive_Root + t.Item1, Prefix_Windows_Drive_Root + t.Item2 }; + public static IEnumerable MemberData_Qualified_Redundant_Prefix_DriveAndRoot_DoubleDot_EdgeCases => + from t in TestPaths_Redundant_DoubleDot_EdgeCases + select new object[] { DevicePrefix + Prefix_Windows_Drive_Root + t.Item1, DevicePrefix + Prefix_Windows_Drive_Root + t.Item5 }; + public static IEnumerable MemberData_Unqualified_Redundant_DriveRootless_DoubleDot_EdgeCases => + from t in TestPaths_Redundant_DoubleDot_EdgeCases + select new object[] { Prefix_Windows_Drive_Rootless + t.Item1, Prefix_Windows_Drive_Rootless + t.Item3 }; + public static IEnumerable MemberData_Unqualified_Redundant_Prefix_DriveRootless_DoubleDot_EdgeCases => + from t in TestPaths_Redundant_DoubleDot_EdgeCases + select new object[] { DevicePrefix + Prefix_Windows_Drive_Rootless + t.Item1, DevicePrefix + Prefix_Windows_Drive_Rootless + t.Item4 }; + public static IEnumerable MemberData_Unqualified_Redundant_DoubleDot_EdgeCases => + from t in TestPaths_Redundant_DoubleDot_EdgeCases + select new object[] { t.Item1, t.Item3 }; + public static IEnumerable MemberData_Unqualified_Redundant_DrivelessRoot_DoubleDot_EdgeCases => + from t in TestPaths_Redundant_DoubleDot_EdgeCases + select new object[] { Prefix_Windows_Driveless_Root + t.Item1, Prefix_Windows_Driveless_Root + t.Item2 }; + public static IEnumerable MemberData_ServerShare_Redundant_DoubleDot_EdgeCases => + from t in TestPaths_Redundant_DoubleDot_EdgeCases + select new object[] { ServerShare + t.Item1, ServerShare + t.Item2 }; // Qualified but not a device path + public static IEnumerable MemberData_UNC_Redundant_DoubleDot_EdgeCases => + from t in TestPaths_Redundant_DoubleDot_EdgeCases + select new object[] { DevicePrefix + UNCServerShare + t.Item1, DevicePrefix + UNCServerShare + t.Item5 }; + + #endregion + + #endregion + } +} From a75b43ab60c113c20ca968e88d3f7f80953e41aa Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Mon, 29 Jun 2020 16:31:58 -0700 Subject: [PATCH 10/25] Address suggestions --- .../src/System/IO/Path.cs | 28 +++++++++++++------ .../IO/RedundantSegmentHelper.Windows.cs | 15 ---------- 2 files changed, 20 insertions(+), 23 deletions(-) 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 fcff88296eb7ba..ca2bc868d03a9c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs @@ -998,14 +998,19 @@ private static string GetRelativePath(string relativeTo, string path, StringComp public static string? RemoveRedundantSegments(string? path) { if (path == null) + { return null; + } - if (PathInternal.IsEffectivelyEmpty(path.AsSpan())) + var spanPath = path.AsSpan(); + if (PathInternal.IsEffectivelyEmpty(spanPath)) + { return string.Empty; + } ValueStringBuilder sb = new ValueStringBuilder(stackalloc char[InitialValueStringBuilderBufferLength]); - if (!RedundantSegmentHelper.TryRemoveRedundantSegments(path.AsSpan(), ref sb)) + if (!RedundantSegmentHelper.TryRemoveRedundantSegments(spanPath, ref sb)) { sb.Dispose(); return path; @@ -1022,7 +1027,9 @@ private static string GetRelativePath(string relativeTo, string path, StringComp public static string RemoveRedundantSegments(ReadOnlySpan path) { if (PathInternal.IsEffectivelyEmpty(path)) + { return string.Empty; + } ValueStringBuilder sb = new ValueStringBuilder(stackalloc char[InitialValueStringBuilderBufferLength]); @@ -1049,21 +1056,26 @@ public static bool TryRemoveRedundantSegments(ReadOnlySpan path, Span fullPath, int c } return totalDots > 0; } - - // 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 || PathInternal.IsDirectorySeparator(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 || PathInternal.IsDirectorySeparator(path[currPos + 3])) && - path[currPos + 1] == '.' && path[currPos + 2] == '.'; - } } } From a60fe1b1013dc9c5115662a48b2e787f74162837 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Mon, 29 Jun 2020 17:43:31 -0700 Subject: [PATCH 11/25] Add comment to explain charsToSkip vs prefixAndRootLength. --- .../src/System/IO/RedundantSegmentHelper.Windows.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs index 6cb18e7e8379b2..719132d81e8644 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs @@ -34,8 +34,11 @@ internal static bool TryRemoveRedundantSegments(ReadOnlySpan originalPath, int prefixLength = isDevicePath ? PathInternal.DevicePrefixLength : 0; ReadOnlySpan pathWithoutPrefix = originalPath.Slice(prefixLength); - int rootLength = PathInternal.GetRootLength(pathWithoutPrefix); - int prefixAndRootLength = prefixLength + rootLength; + + // In a path like "\\.\C:..\folder\subfolder\file.txt": + // - PathInternal.GetRootLength(originalPath) will return "\\.\C:..\", which includes the extra redundant segment. + // - PathInternal.GetRootLength(pathWithoutPrefix) will return "C:", which will help to determine later if a path was rooted or not. + int prefixAndRootLength = prefixLength + PathInternal.GetRootLength(pathWithoutPrefix); // Append characters that should not be touched: // - "C:" - known drive, unknown root From 51601560ad9b83d28d1b5f76856bf51dc3fc9d62 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Mon, 28 Sep 2020 16:43:45 -0700 Subject: [PATCH 12/25] Update license header. --- .../src/System/IO/RedundantSegmentHelper.Unix.cs | 1 - .../src/System/IO/RedundantSegmentHelper.Windows.cs | 1 - .../src/System/IO/RedundantSegmentHelper.cs | 1 - .../tests/System/IO/RedundantSegmentsTestsBase.cs | 1 - .../tests/System/IO/RedundantSegmentsTests_Unix.cs | 1 - .../tests/System/IO/RedundantSegmentsTests_Windows.cs | 1 - 6 files changed, 6 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Unix.cs index 97f82c28939c03..15184ee3b3b667 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Unix.cs @@ -1,6 +1,5 @@ // 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. #nullable enable using System.Diagnostics; diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs index 719132d81e8644..81294902f11b8f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs @@ -1,6 +1,5 @@ // 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. #nullable enable using System.Diagnostics; diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.cs index 23f08048b0d06b..92ca38b14c6c11 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.cs @@ -1,6 +1,5 @@ // 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. #nullable enable using System.Diagnostics; diff --git a/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTestsBase.cs b/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTestsBase.cs index 51805e75aa4dc2..990e1302e41285 100644 --- a/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTestsBase.cs +++ b/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTestsBase.cs @@ -1,6 +1,5 @@ // 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.Collections.Generic; using Xunit; diff --git a/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Unix.cs b/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Unix.cs index 3216d91561ebe8..c1d21a2d24be25 100644 --- a/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Unix.cs +++ b/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Unix.cs @@ -1,6 +1,5 @@ // 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.Collections.Generic; using System.Linq; diff --git a/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Windows.cs b/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Windows.cs index 117b34966b016a..8f0d2d6ec6755b 100644 --- a/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Windows.cs +++ b/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Windows.cs @@ -1,6 +1,5 @@ // 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.Collections.Generic; using System.Linq; From 57165337818438fe1044b57f0587d638b8c29933 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Mon, 28 Sep 2020 16:44:51 -0700 Subject: [PATCH 13/25] Improve public documentation. --- .../System.Private.CoreLib/src/System/IO/Path.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 ca2bc868d03a9c..4a2b382912b97d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs @@ -990,10 +990,10 @@ private static string GetRelativePath(string relativeTo, string path, StringComp public static bool EndsInDirectorySeparator(string path) => PathInternal.EndsInDirectorySeparator(path); /// - /// Removes redundant segments from the specified path string. + /// Removes redundant segments from the specified string containing a path. /// - /// The path to analyze. - /// A string without redundant segments. + /// The path to remove redundant segments from. + /// The without redundant segments, or if is , or if is effectively empty (an empty string or whitespace characters). [return: NotNullIfNotNull("path")] public static string? RemoveRedundantSegments(string? path) { @@ -1020,10 +1020,10 @@ private static string GetRelativePath(string relativeTo, string path, StringComp } /// - /// Removes redundant segments from the specified path read-only span. + /// Removes redundant segments from the specified read-only span of characters containing a path. /// - /// The path to analyze. - /// A string without redundant segments. + /// The path to remove redundant segments from. + /// The without redundant segments, or if is effectively empty (an empty string or whitespace characters). public static string RemoveRedundantSegments(ReadOnlySpan path) { if (PathInternal.IsEffectivelyEmpty(path)) @@ -1048,9 +1048,9 @@ public static string RemoveRedundantSegments(ReadOnlySpan path) /// Tries to remove redundant segments from the specified path read-only span. /// /// The path to analyze. - /// A span where the output is saved. + /// When this method returns , if contained redundant segments, this parameter contains the without redundant segments, or if did not contain redundant segments, or was an invalid path, this parameter contains the value of , unmodified; when this method returns , this parameter does not contain a valid value. /// The total number of characters written to , which is less or equal than the length of . - /// if the original path was modified and writing into was successful; otherwise. + /// if the original path was modified and writing into was successful; if is effectively empty (an empty string or whitespace characters) or there was a problem attempting to write into . public static bool TryRemoveRedundantSegments(ReadOnlySpan path, Span destination, out int charsWritten) { charsWritten = 0; From d791614d31ccbbdd189cc0e7a951635801ef61bf Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Wed, 25 Nov 2020 13:15:40 -0800 Subject: [PATCH 14/25] Remove unused directive and add nullability directive to Unix Path file. --- src/libraries/System.Private.CoreLib/src/System/IO/Path.Unix.cs | 1 + .../src/System/IO/RedundantSegmentHelper.cs | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) 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 cba2cff81bb798..6c960007db2e86 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 @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#nullable enable using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text; diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.cs index 92ca38b14c6c11..8f7cf912769263 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable enable -using System.Diagnostics; using System.Text; namespace System.IO From 8236cdcb74a53252a0b7864c0d96fa01e0debc35 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Tue, 19 Jan 2021 09:06:19 -0800 Subject: [PATCH 15/25] Remove duplicate test cases causing test warnings. --- .../tests/System/IO/PathTests_Windows.cs | 33 ------------------- .../IO/RedundantSegmentsTests_Windows.cs | 9 ++--- 2 files changed, 3 insertions(+), 39 deletions(-) diff --git a/src/libraries/System.Runtime.Extensions/tests/System/IO/PathTests_Windows.cs b/src/libraries/System.Runtime.Extensions/tests/System/IO/PathTests_Windows.cs index 6f3c37b39d57b6..7dd2fd16e97120 100644 --- a/src/libraries/System.Runtime.Extensions/tests/System/IO/PathTests_Windows.cs +++ b/src/libraries/System.Runtime.Extensions/tests/System/IO/PathTests_Windows.cs @@ -430,9 +430,7 @@ public void GetFullPath_BasicExpansions_Windows(string path, string basePath, st { @"\\.\\somedir\..", @"C:\git\runtime", @"\\.\" }, { @"\\.\", @"C:\git\runtime", @"\\.\" }, { @"\\.\..\..\..\..", @"C:\git\runtime", @"\\.\" }, - { @"\\.\", @"C:\git\runtime", @"\\.\" }, { @"\\.\C:\Foo." , @"C:\git\runtime", @"\\.\C:\Foo" }, - { @"\\.\C:\Foo " , @"C:\git\runtime", @"\\.\C:\Foo" }, }; [Theory, @@ -563,12 +561,10 @@ public void GetFullPath_CommonUnc_Windows_ExtendedPrefix(string path, string bas { @"C:", @"", @"C:\"}, // Relative segments eating into the root - { @"foo", @"GLOBALROOT\", @"GLOBALROOT\foo" }, { @"..\..\foo\..\..\", @"", @"..\" }, { @".\..\..\..\..\foo", @"", @".\foo" }, { @"..\foo\..\..\..\", @"", @"..\" }, { @"\.\.\..\", @"C:\", @"C:\"}, - { @"..\..\..\foo", @"GLOBALROOT\", @"GLOBALROOT\foo" }, { @"foo\..\..\", @"", @"foo\" }, { @".\.\foo\..\", @"", @".\" }, }; @@ -609,7 +605,6 @@ public void GetFullPath_CommonDevice_Windows(string path, string basePath, strin { @"C:", @"", @"C:\"}, // Relative segments eating into the root - { @"foo", @"GLOBALROOT\", @"GLOBALROOT\foo" }, { @"..\..\foo\..\..\", @"", @"..\..\foo\..\..\" }, { @".\..\..\..\..\foo", @"", @".\..\..\..\..\foo" }, { @"..\foo\..\..\..\", @"", @"..\foo\..\..\..\" }, @@ -636,7 +631,6 @@ public void GetFullPath_CommonExtended_Windows(string path, string basePath, str { @"\.\bar", @"C:\git\runtime", @"C:\bar" }, { @"\tmp\..", @"C:\git\runtime", @"C:\" }, { @"\tmp\bar\..", @"C:\git\runtime", @"C:\tmp" }, - { @"\tmp\bar\..", @"C:\git\runtime", @"C:\tmp" }, { @"\", @"C:\git\runtime", @"C:\" }, { @"..\..\tmp\bar", @"C:\git\runtime", @"C:\tmp\bar" }, @@ -649,14 +643,12 @@ public void GetFullPath_CommonExtended_Windows(string path, string basePath, str // Specific drive rooted { @"C:tmp\foo\..", @"C:\git\runtime", @"C:\git\runtime\tmp" }, { @"C:tmp\foo\.", @"C:\git\runtime", @"C:\git\runtime\tmp\foo" }, - { @"C:tmp\foo\..", @"C:\git\runtime", @"C:\git\runtime\tmp" }, { @"C:tmp", @"C:\git\runtime", @"C:\git\runtime\tmp" }, { @"C:", @"C:\git\runtime", @"C:\git\runtime" }, { @"C", @"C:\git\runtime", @"C:\git\runtime\C" }, { @"Z:tmp\foo\..", @"C:\git\runtime", @"Z:\tmp" }, { @"Z:tmp\foo\.", @"C:\git\runtime", @"Z:\tmp\foo" }, - { @"Z:tmp\foo\..", @"C:\git\runtime", @"Z:\tmp" }, { @"Z:tmp", @"C:\git\runtime", @"Z:\tmp" }, { @"Z:", @"C:\git\runtime", @"Z:\" }, { @"Z", @"C:\git\runtime", @"C:\git\runtime\Z" }, @@ -666,27 +658,16 @@ public void GetFullPath_CommonExtended_Windows(string path, string basePath, str { @"C:tmp\..\..\foo\.", @"C:\git\runtime", @"C:\git\foo" }, { @"C:..\..\tmp\foo\..", @"C:\git\runtime", @"C:\tmp" }, { @"C:tmp\..\", @"C:\git\runtime", @"C:\git\runtime\" }, - { @"C:", @"C:\git\runtime", @"C:\git\runtime" }, - { @"C", @"C:\git\runtime", @"C:\git\runtime\C" }, { @"C:tmp\..\..\..\..\foo\..", @"C:\git\runtime", @"C:\" }, { @"C:tmp\..\..\foo\.", @"C:\", @"C:\foo" }, { @"C:..\..\tmp\..\foo\..", @"C:\", @"C:\" }, { @"C:tmp\..\", @"C:\", @"C:\" }, - { @"Z:tmp\foo\..", @"C:\git\runtime", @"Z:\tmp" }, - { @"Z:tmp\foo\.", @"C:\git\runtime", @"Z:\tmp\foo" }, - { @"Z:tmp\foo\..", @"C:\git\runtime", @"Z:\tmp" }, - { @"Z:tmp", @"C:\git\runtime", @"Z:\tmp" }, - { @"Z:", @"C:\git\runtime", @"Z:\" }, - { @"Z", @"C:\git\runtime", @"C:\git\runtime\Z" }, - { @"Z:..\..\..\tmp\foo\..", @"C:\git\runtime", @"Z:\tmp" }, { @"Z:tmp\..\..\foo\.", @"C:\git\runtime", @"Z:\foo" }, { @"Z:..\..\tmp\foo\..", @"C:\git\runtime", @"Z:\tmp" }, { @"Z:tmp\..\", @"C:\git\runtime", @"Z:\" }, - { @"Z:", @"C:\git\runtime", @"Z:\" }, - { @"Z", @"C:\git\runtime", @"C:\git\runtime\Z" }, { @"Z:tmp\..\..\..\..\foo\..", @"C:\git\runtime", @"Z:\" }, { @"Z:tmp\..\..\foo\.", @"C:\", @"Z:\foo" }, @@ -711,7 +692,6 @@ public void GetFullPath_CommonUnRooted_Windows(string path, string basePath, str { @"\.\bar", @"C:\git\runtime", @"C:\.\bar" }, { @"\tmp\..", @"C:\git\runtime", @"C:\tmp\.." }, { @"\tmp\bar\..", @"C:\git\runtime", @"C:\tmp\bar\.." }, - { @"\tmp\bar\..", @"C:\git\runtime", @"C:\tmp\bar\.." }, { @"\", @"C:\git\runtime", @"C:\" }, { @"..\..\tmp\bar", @"C:\git\runtime", @"C:\git\runtime\..\..\tmp\bar" }, @@ -724,14 +704,12 @@ public void GetFullPath_CommonUnRooted_Windows(string path, string basePath, str // Specific drive rooted { @"C:tmp\foo\..", @"C:\git\runtime", @"C:\git\runtime\tmp\foo\.." }, { @"C:tmp\foo\.", @"C:\git\runtime", @"C:\git\runtime\tmp\foo\." }, - { @"C:tmp\foo\..", @"C:\git\runtime", @"C:\git\runtime\tmp\foo\.." }, { @"C:tmp", @"C:\git\runtime", @"C:\git\runtime\tmp" }, { @"C:", @"C:\git\runtime", @"C:\git\runtime" }, { @"C", @"C:\git\runtime", @"C:\git\runtime\C" }, { @"Z:tmp\foo\..", @"C:\git\runtime", @"Z:\tmp\foo\.." }, { @"Z:tmp\foo\.", @"C:\git\runtime", @"Z:\tmp\foo\." }, - { @"Z:tmp\foo\..", @"C:\git\runtime", @"Z:\tmp\foo\.." }, { @"Z:tmp", @"C:\git\runtime", @"Z:\tmp" }, { @"Z:", @"C:\git\runtime", @"Z:\" }, { @"Z", @"C:\git\runtime", @"C:\git\runtime\Z" }, @@ -741,27 +719,16 @@ public void GetFullPath_CommonUnRooted_Windows(string path, string basePath, str { @"C:tmp\..\..\foo\.", @"C:\git\runtime", @"C:\git\runtime\tmp\..\..\foo\." }, { @"C:..\..\tmp\foo\..", @"C:\git\runtime", @"C:\git\runtime\..\..\tmp\foo\.." }, { @"C:tmp\..\", @"C:\git\runtime", @"C:\git\runtime\tmp\..\" }, - { @"C:", @"C:\git\runtime", @"C:\git\runtime" }, - { @"C", @"C:\git\runtime", @"C:\git\runtime\C" }, { @"C:tmp\..\..\..\..\foo\..", @"C:\git\runtime", @"C:\git\runtime\tmp\..\..\..\..\foo\.." }, { @"C:tmp\..\..\foo\.", @"C:\", @"C:\tmp\..\..\foo\." }, { @"C:..\..\tmp\..\foo\..", @"C:\", @"C:\..\..\tmp\..\foo\.." }, { @"C:tmp\..\", @"C:\", @"C:\tmp\..\" }, - { @"Z:tmp\foo\..", @"C:\git\runtime", @"Z:\tmp\foo\.." }, - { @"Z:tmp\foo\.", @"C:\git\runtime", @"Z:\tmp\foo\." }, - { @"Z:tmp\foo\..", @"C:\git\runtime", @"Z:\tmp\foo\.." }, - { @"Z:tmp", @"C:\git\runtime", @"Z:\tmp" }, - { @"Z:", @"C:\git\runtime", @"Z:\" }, - { @"Z", @"C:\git\runtime", @"C:\git\runtime\Z" }, - { @"Z:..\..\..\tmp\foo\..", @"C:\git\runtime", @"Z:\..\..\..\tmp\foo\.." }, { @"Z:tmp\..\..\foo\.", @"C:\git\runtime", @"Z:\tmp\..\..\foo\." }, { @"Z:..\..\tmp\foo\..", @"C:\git\runtime", @"Z:\..\..\tmp\foo\.." }, { @"Z:tmp\..\", @"C:\git\runtime", @"Z:\tmp\..\" }, - { @"Z:", @"C:\git\runtime", @"Z:\" }, - { @"Z", @"C:\git\runtime", @"C:\git\runtime\Z" }, { @"Z:tmp\..\..\..\..\foo\..", @"C:\git\runtime", @"Z:\tmp\..\..\..\..\foo\.." }, { @"Z:tmp\..\..\foo\.", @"C:\", @"Z:\tmp\..\..\foo\." }, diff --git a/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Windows.cs b/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Windows.cs index 8f0d2d6ec6755b..e38e8d3aba30ed 100644 --- a/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Windows.cs +++ b/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Windows.cs @@ -203,8 +203,9 @@ public class RedundantSegmentsTests_Windows : RedundantSegmentsTestsBase @"", @"\", @"\\", @"/", @"//", @"\/", @"/\", - @".", @".\", @".\\", - @".", @"./", @".//", + @".", + @".\", @".\\", + @"./", @".//", @"\.", @"\\.", @"\.\", @"\\.\\", @"/.", @"//.", @"/./", @"//.//", @"\.\.", @"\\.\\.", @"\.\.\", @"\\.\\.\\", @@ -220,14 +221,10 @@ public class RedundantSegmentsTests_Windows : RedundantSegmentsTestsBase }; private static readonly string[] TestPaths_DevicePrefix = new string[] { - @"C", @"C:", - @"C:\", - @"C:/", @"C:\folder", @"C:/folder", @"C:A", - @"C:A", @"C:A\folder", @"C:A/folder", }; From 45c3de1bb3c198328ab68396d288214363786839 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Mon, 29 Jun 2020 15:50:33 -0700 Subject: [PATCH 16/25] Add RedundantSegmentHelper class, with platform-specific methods. --- .../System/IO/RedundantSegmentHelper.Unix.cs | 1 + .../IO/RedundantSegmentHelper.Windows.cs | 18 ++++++++++++++++++ .../src/System/IO/RedundantSegmentHelper.cs | 2 ++ 3 files changed, 21 insertions(+) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Unix.cs index 15184ee3b3b667..97f82c28939c03 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Unix.cs @@ -1,5 +1,6 @@ // 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. #nullable enable using System.Diagnostics; diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs index 81294902f11b8f..513c8b9bc2cf45 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs @@ -1,5 +1,6 @@ // 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. #nullable enable using System.Diagnostics; @@ -33,6 +34,8 @@ internal static bool TryRemoveRedundantSegments(ReadOnlySpan originalPath, int prefixLength = isDevicePath ? PathInternal.DevicePrefixLength : 0; ReadOnlySpan pathWithoutPrefix = originalPath.Slice(prefixLength); + int rootLength = PathInternal.GetRootLength(pathWithoutPrefix); + int prefixAndRootLength = prefixLength + rootLength; // In a path like "\\.\C:..\folder\subfolder\file.txt": // - PathInternal.GetRootLength(originalPath) will return "\\.\C:..\", which includes the extra redundant segment. @@ -281,5 +284,20 @@ private static bool IsPreviousSegmentOnlyDots(ReadOnlySpan fullPath, int c } return totalDots > 0; } + + // 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 || PathInternal.IsDirectorySeparator(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 || PathInternal.IsDirectorySeparator(path[currPos + 3])) && + path[currPos + 1] == '.' && path[currPos + 2] == '.'; + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.cs index 8f7cf912769263..23f08048b0d06b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.cs @@ -1,7 +1,9 @@ // 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. #nullable enable +using System.Diagnostics; using System.Text; namespace System.IO From 9d3e004628c727ebbde9bc6a5cd3373cc65279ac Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Mon, 29 Jun 2020 15:52:59 -0700 Subject: [PATCH 17/25] RedundantSegment unit tests. --- .../tests/System/IO/RedundantSegmentsTestsBase.cs | 1 + .../tests/System/IO/RedundantSegmentsTests_Unix.cs | 1 + .../tests/System/IO/RedundantSegmentsTests_Windows.cs | 5 +++++ 3 files changed, 7 insertions(+) diff --git a/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTestsBase.cs b/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTestsBase.cs index 990e1302e41285..51805e75aa4dc2 100644 --- a/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTestsBase.cs +++ b/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTestsBase.cs @@ -1,5 +1,6 @@ // 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.Collections.Generic; using Xunit; diff --git a/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Unix.cs b/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Unix.cs index c1d21a2d24be25..3216d91561ebe8 100644 --- a/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Unix.cs +++ b/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Unix.cs @@ -1,5 +1,6 @@ // 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.Collections.Generic; using System.Linq; diff --git a/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Windows.cs b/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Windows.cs index e38e8d3aba30ed..9b589a954e7eb4 100644 --- a/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Windows.cs +++ b/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Windows.cs @@ -1,5 +1,6 @@ // 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.Collections.Generic; using System.Linq; @@ -221,10 +222,14 @@ public class RedundantSegmentsTests_Windows : RedundantSegmentsTestsBase }; private static readonly string[] TestPaths_DevicePrefix = new string[] { + @"C", @"C:", + @"C:\", + @"C:/", @"C:\folder", @"C:/folder", @"C:A", + @"C:A", @"C:A\folder", @"C:A/folder", }; From c92959dd76b71a792e3bd6e180c51806f746c975 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Mon, 29 Jun 2020 16:31:58 -0700 Subject: [PATCH 18/25] Address suggestions --- .../System/IO/RedundantSegmentHelper.Windows.cs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs index 513c8b9bc2cf45..d1a056ca357efc 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs @@ -284,20 +284,5 @@ private static bool IsPreviousSegmentOnlyDots(ReadOnlySpan fullPath, int c } return totalDots > 0; } - - // 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 || PathInternal.IsDirectorySeparator(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 || PathInternal.IsDirectorySeparator(path[currPos + 3])) && - path[currPos + 1] == '.' && path[currPos + 2] == '.'; - } } } From f1fa5d100b949560f6ba8662f91480f17d9dacf2 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Mon, 29 Jun 2020 17:43:31 -0700 Subject: [PATCH 19/25] Add comment to explain charsToSkip vs prefixAndRootLength. --- .../src/System/IO/RedundantSegmentHelper.Windows.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs index d1a056ca357efc..8213e64f04e53f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs @@ -34,8 +34,11 @@ internal static bool TryRemoveRedundantSegments(ReadOnlySpan originalPath, int prefixLength = isDevicePath ? PathInternal.DevicePrefixLength : 0; ReadOnlySpan pathWithoutPrefix = originalPath.Slice(prefixLength); - int rootLength = PathInternal.GetRootLength(pathWithoutPrefix); - int prefixAndRootLength = prefixLength + rootLength; + + // In a path like "\\.\C:..\folder\subfolder\file.txt": + // - PathInternal.GetRootLength(originalPath) will return "\\.\C:..\", which includes the extra redundant segment. + // - PathInternal.GetRootLength(pathWithoutPrefix) will return "C:", which will help to determine later if a path was rooted or not. + int prefixAndRootLength = prefixLength + PathInternal.GetRootLength(pathWithoutPrefix); // In a path like "\\.\C:..\folder\subfolder\file.txt": // - PathInternal.GetRootLength(originalPath) will return "\\.\C:..\", which includes the extra redundant segment. From ec7bca3e3b2135b94e59234f32daa9696476ba21 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Mon, 28 Sep 2020 16:43:45 -0700 Subject: [PATCH 20/25] Update license header. --- .../src/System/IO/RedundantSegmentHelper.Unix.cs | 1 - .../src/System/IO/RedundantSegmentHelper.Windows.cs | 1 - .../src/System/IO/RedundantSegmentHelper.cs | 1 - .../tests/System/IO/RedundantSegmentsTestsBase.cs | 1 - .../tests/System/IO/RedundantSegmentsTests_Unix.cs | 1 - .../tests/System/IO/RedundantSegmentsTests_Windows.cs | 1 - 6 files changed, 6 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Unix.cs index 97f82c28939c03..15184ee3b3b667 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Unix.cs @@ -1,6 +1,5 @@ // 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. #nullable enable using System.Diagnostics; diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs index 8213e64f04e53f..19448e6f29e3bf 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs @@ -1,6 +1,5 @@ // 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. #nullable enable using System.Diagnostics; diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.cs index 23f08048b0d06b..92ca38b14c6c11 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.cs @@ -1,6 +1,5 @@ // 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. #nullable enable using System.Diagnostics; diff --git a/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTestsBase.cs b/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTestsBase.cs index 51805e75aa4dc2..990e1302e41285 100644 --- a/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTestsBase.cs +++ b/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTestsBase.cs @@ -1,6 +1,5 @@ // 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.Collections.Generic; using Xunit; diff --git a/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Unix.cs b/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Unix.cs index 3216d91561ebe8..c1d21a2d24be25 100644 --- a/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Unix.cs +++ b/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Unix.cs @@ -1,6 +1,5 @@ // 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.Collections.Generic; using System.Linq; diff --git a/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Windows.cs b/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Windows.cs index 9b589a954e7eb4..e9e1444756ba19 100644 --- a/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Windows.cs +++ b/src/libraries/System.Runtime.Extensions/tests/System/IO/RedundantSegmentsTests_Windows.cs @@ -1,6 +1,5 @@ // 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.Collections.Generic; using System.Linq; From 19405cd84cfd630be78839e4e308effc0528a6bf Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Wed, 25 Nov 2020 13:15:40 -0800 Subject: [PATCH 21/25] Remove unused directive and add nullability directive to Unix Path file. --- .../src/System/IO/RedundantSegmentHelper.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.cs index 92ca38b14c6c11..8f7cf912769263 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable enable -using System.Diagnostics; using System.Text; namespace System.IO From 90dc158824dcc6659b5981171f24f08611a6712d Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Wed, 25 Nov 2020 15:57:47 -0800 Subject: [PATCH 22/25] Add condition to Interop.Libraries.cs in S.R.Ext.Tests.csproj required for Interop.GetNodeName.cs to compile due to the dependency. --- .../tests/System.Runtime.Extensions.Tests.csproj | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 a9b12bcae328b3..d1d2ade1d8b032 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 @@ -77,10 +77,9 @@ - - From 39d9387c78f7bc60d8a06699f59ff46473899e93 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Wed, 25 Nov 2020 20:49:14 -0800 Subject: [PATCH 23/25] Small fixes --- .../System.Private.CoreLib/src/System/IO/Path.Unix.cs | 5 ++++- src/libraries/System.Private.CoreLib/src/System/IO/Path.cs | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) 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 6c960007db2e86..818f3734123931 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 @@ -58,9 +58,12 @@ public static string GetFullPath(string path, string basePath) if (basePath.Contains('\0') || path.Contains('\0')) throw new ArgumentException(SR.Argument_InvalidPathChars); - if (IsPathFullyQualified(path)) + if (Path.IsPathRooted(path)) return GetFullPath(path); + if (PathInternal.IsEffectivelyEmpty(path.AsSpan())) + return basePath; + return GetFullPath(CombineInternal(basePath, path)); } 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 4a2b382912b97d..0e6831eb11dfaa 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs @@ -1008,7 +1008,7 @@ private static string GetRelativePath(string relativeTo, string path, StringComp return string.Empty; } - ValueStringBuilder sb = new ValueStringBuilder(stackalloc char[InitialValueStringBuilderBufferLength]); + var sb = new ValueStringBuilder(spanPath.Length > InitialValueStringBuilderBufferLength? InitialValueStringBuilderBufferLength: spanPath.Length); if (!RedundantSegmentHelper.TryRemoveRedundantSegments(spanPath, ref sb)) { @@ -1031,7 +1031,7 @@ public static string RemoveRedundantSegments(ReadOnlySpan path) return string.Empty; } - ValueStringBuilder sb = new ValueStringBuilder(stackalloc char[InitialValueStringBuilderBufferLength]); + var sb = new ValueStringBuilder(path.Length > InitialValueStringBuilderBufferLength ? InitialValueStringBuilderBufferLength : path.Length); if (!RedundantSegmentHelper.TryRemoveRedundantSegments(path, ref sb)) { @@ -1060,7 +1060,7 @@ public static bool TryRemoveRedundantSegments(ReadOnlySpan path, Span InitialValueStringBuilderBufferLength ? InitialValueStringBuilderBufferLength : path.Length); bool result = false; From c65e25fdeb821ea7958dda4354d438bc89fa7743 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Tue, 19 Jan 2021 09:47:53 -0800 Subject: [PATCH 24/25] Fix merge conflict --- .../src/System/IO/RedundantSegmentHelper.Windows.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs index 19448e6f29e3bf..81294902f11b8f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RedundantSegmentHelper.Windows.cs @@ -39,11 +39,6 @@ internal static bool TryRemoveRedundantSegments(ReadOnlySpan originalPath, // - PathInternal.GetRootLength(pathWithoutPrefix) will return "C:", which will help to determine later if a path was rooted or not. int prefixAndRootLength = prefixLength + PathInternal.GetRootLength(pathWithoutPrefix); - // In a path like "\\.\C:..\folder\subfolder\file.txt": - // - PathInternal.GetRootLength(originalPath) will return "\\.\C:..\", which includes the extra redundant segment. - // - PathInternal.GetRootLength(pathWithoutPrefix) will return "C:", which will help to determine later if a path was rooted or not. - int prefixAndRootLength = prefixLength + PathInternal.GetRootLength(pathWithoutPrefix); - // Append characters that should not be touched: // - "C:" - known drive, unknown root // - "\" - known root, unknown drive From 3b72390e433cc0f5cb062f84e6b13eec0b7e127b Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Tue, 19 Jan 2021 14:27:03 -0800 Subject: [PATCH 25/25] Use string overload in Path.GetFullPath to ensure the same string is returned if no redundancy is found, and avoid one allocation --- src/libraries/System.Private.CoreLib/src/System/IO/Path.Unix.cs | 2 +- .../System.Private.CoreLib/src/System/IO/Path.Windows.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 818f3734123931..edde1954b8c945 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 = Path.RemoveRedundantSegments(path.AsSpan()); + 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 7a48439be1998d..8b79bd4f5bcffe 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 @@ -128,7 +128,7 @@ public static string GetFullPath(string path, string basePath) // to Windows APIs won't do anything by design. Additionally, GetFullPathName() in Windows doesn't root // them properly. As such we need to manually remove segments and not use GetFullPath(). - return PathInternal.IsDevice(combinedPath.AsSpan()) ? RemoveRedundantSegments(combinedPath.AsSpan()) : GetFullPath(combinedPath); + return PathInternal.IsDevice(combinedPath.AsSpan()) ? RemoveRedundantSegments(combinedPath) : GetFullPath(combinedPath); } public static string GetTempPath()