Add Path.Join() methods. (#16561)
authorJeremy Kuhne <jeremy.kuhne@microsoft.com>
Wed, 28 Feb 2018 02:53:54 +0000 (18:53 -0800)
committerGitHub <noreply@github.com>
Wed, 28 Feb 2018 02:53:54 +0000 (18:53 -0800)
* 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

src/mscorlib/shared/System/IO/Path.Unix.cs
src/mscorlib/shared/System/IO/Path.Windows.cs
src/mscorlib/shared/System/IO/Path.cs
src/mscorlib/shared/System/IO/PathInternal.cs

index 1294930..fd24cc8 100644 (file)
@@ -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)
index c92211f..b921db9 100644 (file)
@@ -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;
 
index 586ddf3..41ae1cd 100644 (file)
@@ -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");
 
index 3eac1e7..f2f350d 100644 (file)
@@ -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.