diff options
author | Jeremy Kuhne <jeremy.kuhne@microsoft.com> | 2018-02-28 05:53:54 +0300 |
---|---|---|
committer | Anirudh Agnihotry <anirudhagnihotry098@gmail.com> | 2018-03-01 01:00:31 +0300 |
commit | 117da3f807d0213002c32e422b68cc059b55b1fa (patch) | |
tree | d719579aa1504ef54c9d21bf57e073ea729617bd | |
parent | 1039ab115369640766c900c35c02f97900864010 (diff) |
Add Path.Join() methods. (dotnet/coreclr#16561)
* Add Path.Join() methods.
See #25536.
* Address feedback and fix a couple issues now that I've got tests running correctly.
* Update per final API approval
* Fix Unix, remove redundant helper.
* Merge and tweak join methods in GetFullPath(string, string)
* Tweak again
Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com>
4 files changed, 136 insertions, 84 deletions
diff --git a/src/System.Private.CoreLib/shared/System/IO/Path.Unix.cs b/src/System.Private.CoreLib/shared/System/IO/Path.Unix.cs index 12949308c..fd24cc810 100644 --- a/src/System.Private.CoreLib/shared/System/IO/Path.Unix.cs +++ b/src/System.Private.CoreLib/shared/System/IO/Path.Unix.cs @@ -61,7 +61,7 @@ namespace System.IO if (IsPathFullyQualified(path)) return GetFullPath(path); - return GetFullPath(CombineNoChecks(basePath, path)); + return GetFullPath(CombineInternal(basePath, path)); } private static string RemoveLongPathPrefix(string path) diff --git a/src/System.Private.CoreLib/shared/System/IO/Path.Windows.cs b/src/System.Private.CoreLib/shared/System/IO/Path.Windows.cs index c92211f73..b921db9e6 100644 --- a/src/System.Private.CoreLib/shared/System/IO/Path.Windows.cs +++ b/src/System.Private.CoreLib/shared/System/IO/Path.Windows.cs @@ -80,26 +80,30 @@ namespace System.IO // Path is current drive rooted i.e. starts with \: // "\Foo" and "C:\Bar" => "C:\Foo" // "\Foo" and "\\?\C:\Bar" => "\\?\C:\Foo" - combinedPath = CombineNoChecks(GetPathRoot(basePath), path.AsSpan().Slice(1)); + combinedPath = Join(GetPathRoot(basePath.AsSpan()), path); } else if (length >= 2 && PathInternal.IsValidDriveChar(path[0]) && path[1] == PathInternal.VolumeSeparatorChar) { // Drive relative paths Debug.Assert(length == 2 || !PathInternal.IsDirectorySeparator(path[2])); - if (StringSpanHelpers.Equals(GetVolumeName(path.AsSpan()), GetVolumeName(basePath.AsSpan()))) + if (StringSpanHelpers.Equals(GetVolumeName(path), GetVolumeName(basePath))) { // Matching root // "C:Foo" and "C:\Bar" => "C:\Bar\Foo" // "C:Foo" and "\\?\C:\Bar" => "\\?\C:\Bar\Foo" - combinedPath = CombineNoChecks(basePath, path.AsSpan().Slice(2)); + combinedPath = Join(basePath, path.AsSpan().Slice(2)); } else { // No matching root, root to specified drive // "D:Foo" and "C:\Bar" => "D:Foo" // "D:Foo" and "\\?\C:\Bar" => "\\?\D:\Foo" - combinedPath = PathInternal.IsDevice(basePath) ? CombineNoChecksInternal(basePath.AsSpan().Slice(0, 4), path.AsSpan().Slice(0, 2), @"\", path.AsSpan().Slice(2)) : path.Insert(2, "\\"); + combinedPath = !PathInternal.IsDevice(basePath) + ? path.Insert(2, @"\") + : length == 2 + ? JoinInternal(basePath.AsSpan().Slice(0, 4), path, @"\") + : JoinInternal(basePath.AsSpan().Slice(0, 4), path.AsSpan().Slice(0, 2), @"\", path.AsSpan().Slice(2)); } } else @@ -107,7 +111,7 @@ namespace System.IO // "Simple" relative path // "Foo" and "C:\Bar" => "C:\Bar\Foo" // "Foo" and "\\?\C:\Bar" => "\\?\C:\Bar\Foo" - combinedPath = CombineNoChecks(basePath, path); + combinedPath = JoinInternal(basePath, path); } // Device paths are normalized by definition, so passing something of this format @@ -216,19 +220,11 @@ namespace System.IO } /// <summary> - /// Returns true if the path ends in a directory separator. - /// </summary> - internal static bool EndsInDirectorySeparator(ReadOnlySpan<char> path) - { - return path.Length > 0 && PathInternal.IsDirectorySeparator(path[path.Length - 1]); - } - - /// <summary> /// Trims the ending directory separator if present. /// </summary> /// <param name="path"></param> internal static ReadOnlySpan<char> TrimEndingDirectorySeparator(ReadOnlySpan<char> path) => - EndsInDirectorySeparator(path) ? + PathInternal.EndsInDirectorySeparator(path) ? path.Slice(0, path.Length - 1) : path; diff --git a/src/System.Private.CoreLib/shared/System/IO/Path.cs b/src/System.Private.CoreLib/shared/System/IO/Path.cs index 586ddf3a6..41ae1cd0b 100644 --- a/src/System.Private.CoreLib/shared/System/IO/Path.cs +++ b/src/System.Private.CoreLib/shared/System/IO/Path.cs @@ -262,7 +262,6 @@ namespace System.IO return !PathInternal.IsPartiallyQualified(path); } - /// <summary> /// Tests if a path's file name includes a file extension. A trailing period /// is not considered an extension. @@ -296,7 +295,7 @@ namespace System.IO if (path1 == null || path2 == null) throw new ArgumentNullException((path1 == null) ? nameof(path1) : nameof(path2)); - return CombineNoChecks(path1, path2); + return CombineInternal(path1, path2); } public static string Combine(string path1, string path2, string path3) @@ -304,7 +303,7 @@ namespace System.IO if (path1 == null || path2 == null || path3 == null) throw new ArgumentNullException((path1 == null) ? nameof(path1) : (path2 == null) ? nameof(path2) : nameof(path3)); - return CombineNoChecks(path1, path2, path3); + return CombineInternal(path1, path2, path3); } public static string Combine(string path1, string path2, string path3, string path4) @@ -312,7 +311,7 @@ namespace System.IO if (path1 == null || path2 == null || path3 == null || path4 == null) throw new ArgumentNullException((path1 == null) ? nameof(path1) : (path2 == null) ? nameof(path2) : (path3 == null) ? nameof(path3) : nameof(path4)); - return CombineNoChecks(path1, path2, path3, path4); + return CombineInternal(path1, path2, path3, path4); } public static string Combine(params string[] paths) @@ -383,11 +382,102 @@ namespace System.IO return StringBuilderCache.GetStringAndRelease(finalPath); } - /// <summary> - /// Combines two paths. Does no validation of paths, only concatenates the paths - /// and places a directory separator between them if needed. - /// </summary> - private static string CombineNoChecks(ReadOnlySpan<char> first, ReadOnlySpan<char> second) + // Unlike Combine(), Join() methods do not consider rooting. They simply combine paths, ensuring that there + // is a directory separator between them. + + public static string Join(ReadOnlySpan<char> path1, ReadOnlySpan<char> path2) + { + if (path1.Length == 0) + return new string(path2); + if (path2.Length == 0) + return new string(path1); + + return JoinInternal(path1, path2); + } + + public static string Join(ReadOnlySpan<char> path1, ReadOnlySpan<char> path2, ReadOnlySpan<char> path3) + { + if (path1.Length == 0) + return Join(path2, path3); + + if (path2.Length == 0) + return Join(path1, path3); + + if (path3.Length == 0) + return Join(path1, path2); + + return JoinInternal(path1, path2, path3); + } + + public static bool TryJoin(ReadOnlySpan<char> path1, ReadOnlySpan<char> path2, Span<char> destination, out int charsWritten) + { + charsWritten = 0; + if (path1.Length == 0 && path2.Length == 0) + return true; + + if (path1.Length == 0 || path2.Length == 0) + { + ref ReadOnlySpan<char> pathToUse = ref path1.Length == 0 ? ref path2 : ref path1; + if (destination.Length < pathToUse.Length) + { + return false; + } + + pathToUse.CopyTo(destination); + charsWritten = pathToUse.Length; + return true; + } + + bool needsSeparator = !(PathInternal.EndsInDirectorySeparator(path1) || PathInternal.StartsWithDirectorySeparator(path2)); + int charsNeeded = path1.Length + path2.Length + (needsSeparator ? 1 : 0); + if (destination.Length < charsNeeded) + return false; + + path1.CopyTo(destination); + if (needsSeparator) + destination[path1.Length] = DirectorySeparatorChar; + + path2.CopyTo(destination.Slice(path1.Length + (needsSeparator ? 1 : 0))); + + charsWritten = charsNeeded; + return true; + } + + public static bool TryJoin(ReadOnlySpan<char> path1, ReadOnlySpan<char> path2, ReadOnlySpan<char> path3, Span<char> destination, out int charsWritten) + { + charsWritten = 0; + if (path1.Length == 0 && path2.Length == 0 && path3.Length == 0) + return true; + + if (path1.Length == 0) + return TryJoin(path2, path3, destination, out charsWritten); + if (path2.Length == 0) + return TryJoin(path1, path3, destination, out charsWritten); + if (path3.Length == 0) + return TryJoin(path1, path2, destination, out charsWritten); + + int neededSeparators = PathInternal.EndsInDirectorySeparator(path1) || PathInternal.StartsWithDirectorySeparator(path2) ? 0 : 1; + bool needsSecondSeparator = !(PathInternal.EndsInDirectorySeparator(path2) || PathInternal.StartsWithDirectorySeparator(path3)); + if (needsSecondSeparator) + neededSeparators++; + + int charsNeeded = path1.Length + path2.Length + path3.Length + neededSeparators; + if (destination.Length < charsNeeded) + return false; + + bool result = TryJoin(path1, path2, destination, out charsWritten); + Debug.Assert(result, "should never fail joining first two paths"); + + if (needsSecondSeparator) + destination[charsWritten++] = DirectorySeparatorChar; + + path3.CopyTo(destination.Slice(charsWritten)); + charsWritten += path3.Length; + + return true; + } + + private static string CombineInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second) { if (first.Length == 0) return second.Length == 0 @@ -400,10 +490,10 @@ namespace System.IO if (IsPathRooted(second)) return new string(second); - return CombineNoChecksInternal(first, second); + return JoinInternal(first, second); } - private static string CombineNoChecks(string first, string second) + private static string CombineInternal(string first, string second) { if (string.IsNullOrEmpty(first)) return second; @@ -414,86 +504,48 @@ namespace System.IO if (IsPathRooted(second.AsSpan())) return second; - return CombineNoChecksInternal(first, second); - } - - private static string CombineNoChecks(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third) - { - if (first.Length == 0) - return CombineNoChecks(second, third); - if (second.Length == 0) - return CombineNoChecks(first, third); - if (third.Length == 0) - return CombineNoChecks(first, second); - - if (IsPathRooted(third)) - return new string(third); - if (IsPathRooted(second)) - return CombineNoChecks(second, third); - - return CombineNoChecksInternal(first, second, third); + return JoinInternal(first, second); } - private static string CombineNoChecks(string first, string second, string third) + private static string CombineInternal(string first, string second, string third) { if (string.IsNullOrEmpty(first)) - return CombineNoChecks(second, third); + return CombineInternal(second, third); if (string.IsNullOrEmpty(second)) - return CombineNoChecks(first, third); + return CombineInternal(first, third); if (string.IsNullOrEmpty(third)) - return CombineNoChecks(first, second); + return CombineInternal(first, second); if (IsPathRooted(third.AsSpan())) return third; if (IsPathRooted(second.AsSpan())) - return CombineNoChecks(second, third); - - return CombineNoChecksInternal(first, second, third); - } - - private static string CombineNoChecks(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third, ReadOnlySpan<char> fourth) - { - if (first.Length == 0) - return CombineNoChecks(second, third, fourth); - if (second.Length == 0) - return CombineNoChecks(first, third, fourth); - if (third.Length == 0) - return CombineNoChecks(first, second, fourth); - if (fourth.Length == 0) - return CombineNoChecks(first, second, third); - - if (IsPathRooted(fourth)) - return new string(fourth); - if (IsPathRooted(third)) - return CombineNoChecks(third, fourth); - if (IsPathRooted(second)) - return CombineNoChecks(second, third, fourth); + return CombineInternal(second, third); - return CombineNoChecksInternal(first, second, third, fourth); + return JoinInternal(first, second, third); } - private static string CombineNoChecks(string first, string second, string third, string fourth) + private static string CombineInternal(string first, string second, string third, string fourth) { if (string.IsNullOrEmpty(first)) - return CombineNoChecks(second, third, fourth); + return CombineInternal(second, third, fourth); if (string.IsNullOrEmpty(second)) - return CombineNoChecks(first, third, fourth); + return CombineInternal(first, third, fourth); if (string.IsNullOrEmpty(third)) - return CombineNoChecks(first, second, fourth); + return CombineInternal(first, second, fourth); if (string.IsNullOrEmpty(fourth)) - return CombineNoChecks(first, second, third); + return CombineInternal(first, second, third); if (IsPathRooted(fourth.AsSpan())) return fourth; if (IsPathRooted(third.AsSpan())) - return CombineNoChecks(third, fourth); + return CombineInternal(third, fourth); if (IsPathRooted(second.AsSpan())) - return CombineNoChecks(second, third, fourth); + return CombineInternal(second, third, fourth); - return CombineNoChecksInternal(first, second, third, fourth); + return JoinInternal(first, second, third, fourth); } - private unsafe static string CombineNoChecksInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second) + private unsafe static string JoinInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second) { Debug.Assert(first.Length > 0 && second.Length > 0, "should have dealt with empty paths"); @@ -515,7 +567,7 @@ namespace System.IO } } - private unsafe static string CombineNoChecksInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third) + private unsafe static string JoinInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third) { Debug.Assert(first.Length > 0 && second.Length > 0 && third.Length > 0, "should have dealt with empty paths"); @@ -543,7 +595,7 @@ namespace System.IO } } - private unsafe static string CombineNoChecksInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third, ReadOnlySpan<char> fourth) + private unsafe static string JoinInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third, ReadOnlySpan<char> fourth) { Debug.Assert(first.Length > 0 && second.Length > 0 && third.Length > 0 && fourth.Length > 0, "should have dealt with empty paths"); diff --git a/src/System.Private.CoreLib/shared/System/IO/PathInternal.cs b/src/System.Private.CoreLib/shared/System/IO/PathInternal.cs index 3eac1e74e..f2f350ddd 100644 --- a/src/System.Private.CoreLib/shared/System/IO/PathInternal.cs +++ b/src/System.Private.CoreLib/shared/System/IO/PathInternal.cs @@ -10,8 +10,12 @@ namespace System.IO /// <summary> /// Returns true if the path ends in a directory separator. /// </summary> - internal static bool EndsInDirectorySeparator(string path) => - !string.IsNullOrEmpty(path) && IsDirectorySeparator(path[path.Length - 1]); + internal static bool EndsInDirectorySeparator(ReadOnlySpan<char> path) => path.Length > 0 && IsDirectorySeparator(path[path.Length - 1]); + + /// <summary> + /// Returns true if the path starts in a directory separator. + /// </summary> + internal static bool StartsWithDirectorySeparator(ReadOnlySpan<char> path) => path.Length > 0 && IsDirectorySeparator(path[0]); /// <summary> /// Get the common path length from the start of the string. |