Implementation of GetFullPath(string path, string basePath) (#15579)
authorAnirudh Agnihotry <anirudhagnihotry098@gmail.com>
Tue, 6 Feb 2018 01:39:37 +0000 (17:39 -0800)
committerGitHub <noreply@github.com>
Tue, 6 Feb 2018 01:39:37 +0000 (17:39 -0800)
GetFullPathAPI Overload

src/mscorlib/Resources/Strings.resx
src/mscorlib/shared/System/IO/Path.Unix.cs
src/mscorlib/shared/System/IO/Path.Windows.cs
src/mscorlib/shared/System/IO/Path.cs

index 2b66ce5..b98553c 100644 (file)
   <data name="IO_InvalidReadLength" xml:space="preserve">
     <value>The read operation returned an invalid length.</value>
   </data>
+  <data name="Arg_BasePathNotFullyQualified" xml:space="preserve">
+    <value>Basepath argument is not fully qualified.</value>
+  </data>
   <data name="Arg_ElementsInSourceIsGreaterThanDestination" xml:space="preserve">
     <value>Number of elements in source vector is greater than the destination array</value>
   </data>
index 81a796b..7e18a53 100644 (file)
@@ -44,91 +44,24 @@ namespace System.IO
             return result;
         }
 
-        /// <summary>
-        /// Try to remove relative segments from the given path (without combining with a root).
-        /// </summary>
-        /// <param name="skip">Skip the specified number of characters before evaluating.</param>
-        private static string RemoveRelativeSegments(string path, int skip = 0)
+        public static string GetFullPath(string path, string basePath)
         {
-            bool flippedSeparator = false;
+            if (path == null)
+                throw new ArgumentException(nameof(path));
 
-            // 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.
-            var sb = StringBuilderCache.Acquire(path.Length);
-            if (skip > 0)
-            {
-                sb.Append(path, 0, skip);
-            }
+            if (basePath == null)
+                throw new ArgumentNullException(nameof(basePath));
 
-            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 >= 0; s--)
-                        {
-                            if (PathInternal.IsDirectorySeparator(sb[s]))
-                            {
-                                sb.Length = s;
-                                break;
-                            }
-                        }
-                        if (s < 0)
-                        {
-                            sb.Length = 0;
-                        }
-
-                        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 (!IsPathFullyQualified(basePath))
+                throw new ArgumentException(SR.Arg_BasePathNotFullyQualified);
 
-            if (flippedSeparator || sb.Length != path.Length)
-            {
-                return StringBuilderCache.GetStringAndRelease(sb);
-            }
-            else
-            {
-                // We haven't changed the source path, return the original
-                StringBuilderCache.Release(sb);
-                return path;
-            }
+            if (basePath.Contains('\0') || path.Contains('\0'))
+                throw new ArgumentException(SR.Argument_InvalidPathChars);
+
+            if (IsPathFullyQualified(path))
+                return GetFullPath(path);
+
+            return GetFullPath(CombineNoChecks(basePath, path));
         }
 
         private static string RemoveLongPathPrefix(string path)
index 862617d..0d969db 100644 (file)
@@ -87,6 +87,69 @@ namespace System.IO
             return fullPath;
         }
 
+        public static string GetFullPath(string path, string basePath)
+        {
+            if (path == null)
+                throw new ArgumentException(nameof(path));
+
+            if (basePath == null)
+                throw new ArgumentNullException(nameof(basePath));
+
+            if (!IsPathFullyQualified(basePath))
+                throw new ArgumentException(SR.Arg_BasePathNotFullyQualified);
+
+            if (basePath.Contains('\0') || path.Contains('\0'))
+                throw new ArgumentException(SR.Argument_InvalidPathChars);
+
+            if (IsPathFullyQualified(path))
+                return GetFullPath(path);
+
+            int length = path.Length;
+            string combinedPath = null;
+
+            if ((length >= 1 && PathInternal.IsDirectorySeparator(path[0])))
+            {
+                // 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.AsReadOnlySpan().Slice(1));
+            }
+            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.AsReadOnlySpan()), GetVolumeName(basePath.AsReadOnlySpan())))
+                {
+                    // Matching root
+                    // "C:Foo" and "C:\Bar" => "C:\Bar\Foo"
+                    // "C:Foo" and "\\?\C:\Bar" => "\\?\C:\Bar\Foo"
+                    combinedPath = CombineNoChecks(basePath, path.AsReadOnlySpan().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 = path.Insert(2, "\\");
+                }
+            }
+            else
+            {
+                // "Simple" relative path
+                // "Foo" and "C:\Bar" => "C:\Bar\Foo"
+                // "Foo" and "\\?\C:\Bar" => "\\?\C:\Bar\Foo"
+                combinedPath = CombineNoChecks(basePath, path);
+            }
+
+            // Device paths are normalized by definition, so passing something of this format
+            // to GetFullPath() won't do anything by design. Additionally, GetFullPathName() in
+            // Windows doesn't root them properly. As such we need to manually remove segments.
+            return PathInternal.IsDevice(combinedPath)
+                ? RemoveRelativeSegments(combinedPath, PathInternal.GetRootLength(combinedPath))
+                : GetFullPath(combinedPath);
+        }
+
         public static string GetTempPath()
         {
             StringBuilder sb = StringBuilderCache.Acquire(Interop.Kernel32.MAX_PATH);
@@ -142,7 +205,7 @@ namespace System.IO
             ReadOnlySpan<char> result = GetPathRoot(path.AsReadOnlySpan());
             if (path.Length == result.Length)
                 return PathInternal.NormalizeDirectorySeparators(path);
-           
+
             return PathInternal.NormalizeDirectorySeparators(new string(result));
         }
 
@@ -160,5 +223,67 @@ namespace System.IO
 
         /// <summary>Gets whether the system is case-sensitive.</summary>
         internal static bool IsCaseSensitive { get { return false; } }
+
+
+        /// <summary>
+        /// Returns the volume name for dos, UNC and device paths.
+        /// </summary>
+        internal static ReadOnlySpan<char> GetVolumeName(ReadOnlySpan<char> path)
+        {
+            // 3 cases: UNC ("\\server\share"), Device ("\\?\C:\"), or Dos ("C:\")
+            ReadOnlySpan<char> root = GetPathRoot(path);
+            if (root.Length == 0)
+                return root;
+
+            int offset = GetUncRootLength(path);
+            if (offset >= 0)
+            {
+                // Cut from "\\?\UNC\Server\Share" to "Server\Share"
+                // Cut from  "\\Server\Share" to "Server\Share"
+                return TrimEndingDirectorySeparator(root.Slice(offset));
+            }
+            else if (PathInternal.IsDevice(path))
+            {
+                return TrimEndingDirectorySeparator(root.Slice(4)); // Cut from "\\?\C:\" to "C:"
+            }
+
+            return TrimEndingDirectorySeparator(root); // e.g. "C:"
+        }
+
+        /// <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) ?
+                path.Slice(0, path.Length - 1) :
+                path;
+
+        /// <summary>
+        /// Returns offset as -1 if the path is not in Unc format, otherwise returns the root length.
+        /// </summary>
+        /// <param name="path"></param>
+        /// <returns></returns>
+        internal static int GetUncRootLength(ReadOnlySpan<char> path)
+        {
+            bool isDevice = PathInternal.IsDevice(path);
+
+            if (!isDevice && StringSpanHelpers.Equals(path.Slice(0, 2), @"\\") )
+                return 2;
+            else if (isDevice && path.Length >= 8
+                && (StringSpanHelpers.Equals(path.Slice(0, 8), PathInternal.UncExtendedPathPrefix)
+                || StringSpanHelpers.Equals(path.Slice(5, 4), @"UNC\")))
+                return 8;
+
+            return -1;
+        }
     }
 }
index 3814a92..892cdaf 100644 (file)
@@ -3,6 +3,8 @@
 // See the LICENSE file in the project root for more information.
 
 using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
 using System.Text;
 
 namespace System.IO
@@ -352,120 +354,145 @@ namespace System.IO
             return StringBuilderCache.GetStringAndRelease(finalPath);
         }
 
-        private static string CombineNoChecks(string path1, string path2)
+        /// <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)
         {
-            if (path2.Length == 0)
-                return path1;
+            if (first.Length == 0)
+                return second.Length == 0
+                    ? string.Empty
+                    : new string(second);
 
-            if (path1.Length == 0)
-                return path2;
+            if (second.Length == 0)
+                return new string(first);
 
-            if (IsPathRooted(path2))
-                return path2;
+            if (IsPathRooted(second)) // will change to span version after the span pr is merged
+                return new string(second);
 
-            char ch = path1[path1.Length - 1];
-            return PathInternal.IsDirectoryOrVolumeSeparator(ch) ?
-                path1 + path2 :
-                path1 + PathInternal.DirectorySeparatorCharAsString + path2;
+            return CombineNoChecksInternal(first, second);
         }
 
-        private static string CombineNoChecks(string path1, string path2, string path3)
+        private static string CombineNoChecks(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third)
         {
-            if (path1.Length == 0)
-                return CombineNoChecks(path2, path3);
-            if (path2.Length == 0)
-                return CombineNoChecks(path1, path3);
-            if (path3.Length == 0)
-                return CombineNoChecks(path1, path2);
-
-            if (IsPathRooted(path3))
-                return path3;
-            if (IsPathRooted(path2))
-                return CombineNoChecks(path2, path3);
-
-            bool hasSep1 = PathInternal.IsDirectoryOrVolumeSeparator(path1[path1.Length - 1]);
-            bool hasSep2 = PathInternal.IsDirectoryOrVolumeSeparator(path2[path2.Length - 1]);
-
-            if (hasSep1 && hasSep2)
-            {
-                return path1 + path2 + path3;
-            }
-            else if (hasSep1)
-            {
-                return path1 + path2 + PathInternal.DirectorySeparatorCharAsString + path3;
-            }
-            else if (hasSep2)
-            {
-                return path1 + PathInternal.DirectorySeparatorCharAsString + path2 + path3;
-            }
-            else
-            {
-                // string.Concat only has string-based overloads up to four arguments; after that requires allocating
-                // a params string[].  Instead, try to use a cached StringBuilder.
-                StringBuilder sb = StringBuilderCache.Acquire(path1.Length + path2.Length + path3.Length + 2);
-                sb.Append(path1)
-                  .Append(PathInternal.DirectorySeparatorChar)
-                  .Append(path2)
-                  .Append(PathInternal.DirectorySeparatorChar)
-                  .Append(path3);
-                return StringBuilderCache.GetStringAndRelease(sb);
-            }
+            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);            
         }
 
-        private static string CombineNoChecks(string path1, string path2, string path3, string path4)
+        private static string CombineNoChecks(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third, ReadOnlySpan<char> fourth)
         {
-            if (path1.Length == 0)
-                return CombineNoChecks(path2, path3, path4);
-            if (path2.Length == 0)
-                return CombineNoChecks(path1, path3, path4);
-            if (path3.Length == 0)
-                return CombineNoChecks(path1, path2, path4);
-            if (path4.Length == 0)
-                return CombineNoChecks(path1, path2, path3);
-
-            if (IsPathRooted(path4))
-                return path4;
-            if (IsPathRooted(path3))
-                return CombineNoChecks(path3, path4);
-            if (IsPathRooted(path2))
-                return CombineNoChecks(path2, path3, path4);
-
-            bool hasSep1 = PathInternal.IsDirectoryOrVolumeSeparator(path1[path1.Length - 1]);
-            bool hasSep2 = PathInternal.IsDirectoryOrVolumeSeparator(path2[path2.Length - 1]);
-            bool hasSep3 = PathInternal.IsDirectoryOrVolumeSeparator(path3[path3.Length - 1]);
-
-            if (hasSep1 && hasSep2 && hasSep3)
+            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 CombineNoChecksInternal(first, second, third, fourth);
+        }
+
+        private unsafe static string CombineNoChecksInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second)
+        {
+            Debug.Assert(first.Length > 0 && second.Length > 0, "should have dealt with empty paths");
+
+            bool hasSeparator = PathInternal.IsDirectorySeparator(first[first.Length - 1])
+                || PathInternal.IsDirectorySeparator(second[0]);
+
+            fixed (char* f = &MemoryMarshal.GetReference(first), s = &MemoryMarshal.GetReference(second))
             {
-                // Use string.Concat overload that takes four strings
-                return path1 + path2 + path3 + path4;
+                return string.Create(
+                    first.Length + second.Length + (hasSeparator ? 0 : 1),
+                    (First: (IntPtr)f, FirstLength: first.Length, Second: (IntPtr)s, SecondLength: second.Length, HasSeparator: hasSeparator),
+                    (destination, state) =>
+                    {
+                        new Span<char>((char*)state.First, state.FirstLength).CopyTo(destination);
+                        if (!state.HasSeparator)
+                            destination[state.FirstLength] = PathInternal.DirectorySeparatorChar;
+                        new Span<char>((char*)state.Second, state.SecondLength).CopyTo(destination.Slice(state.FirstLength + (state.HasSeparator ? 0 : 1)));
+                    });
             }
-            else
-            {
-                // string.Concat only has string-based overloads up to four arguments; after that requires allocating
-                // a params string[].  Instead, try to use a cached StringBuilder.
-                StringBuilder sb = StringBuilderCache.Acquire(path1.Length + path2.Length + path3.Length + path4.Length + 3);
+        }
 
-                sb.Append(path1);
-                if (!hasSep1)
-                {
-                    sb.Append(PathInternal.DirectorySeparatorChar);
-                }
+        private unsafe static string CombineNoChecksInternal(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");
 
-                sb.Append(path2);
-                if (!hasSep2)
-                {
-                    sb.Append(PathInternal.DirectorySeparatorChar);
-                }
+            bool firstHasSeparator = PathInternal.IsDirectorySeparator(first[first.Length - 1])
+                || PathInternal.IsDirectorySeparator(second[0]);
+            bool thirdHasSeparator = PathInternal.IsDirectorySeparator(second[second.Length - 1])
+                || PathInternal.IsDirectorySeparator(third[0]);
 
-                sb.Append(path3);
-                if (!hasSep3)
-                {
-                    sb.Append(PathInternal.DirectorySeparatorChar);
-                }
+            fixed (char* f = &MemoryMarshal.GetReference(first), s = &MemoryMarshal.GetReference(second), t = &MemoryMarshal.GetReference(third))
+            {
+                return string.Create(
+                    first.Length + second.Length + third.Length + (firstHasSeparator ? 0 : 1) + (thirdHasSeparator ? 0 : 1),
+                    (First: (IntPtr)f, FirstLength: first.Length, Second: (IntPtr)s, SecondLength: second.Length,
+                        Third: (IntPtr)t, ThirdLength: third.Length, FirstHasSeparator: firstHasSeparator, ThirdHasSeparator: thirdHasSeparator),
+                    (destination, state) =>
+                    {
+                        new Span<char>((char*)state.First, state.FirstLength).CopyTo(destination);
+                        if (!state.FirstHasSeparator)
+                            destination[state.FirstLength] = PathInternal.DirectorySeparatorChar;
+                        new Span<char>((char*)state.Second, state.SecondLength).CopyTo(destination.Slice(state.FirstLength + (state.FirstHasSeparator ? 0 : 1)));
+                        if (!state.ThirdHasSeparator)
+                            destination[destination.Length - state.ThirdLength - 1] = PathInternal.DirectorySeparatorChar;
+                        new Span<char>((char*)state.Third, state.ThirdLength).CopyTo(destination.Slice(destination.Length - state.ThirdLength));
+                    });
+            }
+        }
 
-                sb.Append(path4);
+        private unsafe static string CombineNoChecksInternal(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");
 
-                return StringBuilderCache.GetStringAndRelease(sb);
+            bool firstHasSeparator = PathInternal.IsDirectorySeparator(first[first.Length - 1])
+                || PathInternal.IsDirectorySeparator(second[0]);
+            bool thirdHasSeparator = PathInternal.IsDirectorySeparator(second[second.Length - 1])
+                || PathInternal.IsDirectorySeparator(third[0]);
+            bool fourthHasSeparator = PathInternal.IsDirectorySeparator(third[third.Length - 1])
+                || PathInternal.IsDirectorySeparator(fourth[0]);
+
+            fixed (char* f = &MemoryMarshal.GetReference(first), s = &MemoryMarshal.GetReference(second), t = &MemoryMarshal.GetReference(third), u = &MemoryMarshal.GetReference(fourth))
+            {
+                return string.Create(
+                    first.Length + second.Length + third.Length + fourth.Length + (firstHasSeparator ? 0 : 1) + (thirdHasSeparator ? 0 : 1) + (fourthHasSeparator ? 0 : 1),
+                    (First: (IntPtr)f, FirstLength: first.Length, Second: (IntPtr)s, SecondLength: second.Length,
+                        Third: (IntPtr)t, ThirdLength: third.Length, Fourth: (IntPtr)u, FourthLength:fourth.Length,
+                        FirstHasSeparator: firstHasSeparator, ThirdHasSeparator: thirdHasSeparator, FourthHasSeparator: fourthHasSeparator),
+                    (destination, state) =>
+                    {
+                        new Span<char>((char*)state.First, state.FirstLength).CopyTo(destination);
+                        if (!state.FirstHasSeparator)
+                            destination[state.FirstLength] = PathInternal.DirectorySeparatorChar;
+                        new Span<char>((char*)state.Second, state.SecondLength).CopyTo(destination.Slice(state.FirstLength + (state.FirstHasSeparator ? 0 : 1)));
+                        if (!state.ThirdHasSeparator)
+                            destination[state.FirstLength + state.SecondLength + (state.FirstHasSeparator ? 0 : 1)] = PathInternal.DirectorySeparatorChar;
+                        new Span<char>((char*)state.Third, state.ThirdLength).CopyTo(destination.Slice(state.FirstLength + state.SecondLength + (state.FirstHasSeparator ? 0 : 1) + (state.ThirdHasSeparator ? 0 : 1)));
+                        if (!state.FourthHasSeparator)
+                            destination[destination.Length - state.FourthLength - 1] = PathInternal.DirectorySeparatorChar;
+                        new Span<char>((char*)state.Fourth, state.FourthLength).CopyTo(destination.Slice(destination.Length - state.FourthLength));
+                    });
             }
         }
 
@@ -528,6 +555,93 @@ namespace System.IO
         }
 
         /// <summary>
+        /// Try to remove relative segments from the given path (without combining with a root).
+        /// </summary>
+        /// <param name="skip">Skip the specified number of characters before evaluating.</param>
+        private static string RemoveRelativeSegments(string path, int skip = 0)
+        {
+            bool flippedSeparator = false;
+
+            // 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.
+            var sb = StringBuilderCache.Acquire(path.Length);
+            if (skip > 0)
+            {
+                sb.Append(path, 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 >= 0; s--)
+                        {
+                            if (PathInternal.IsDirectorySeparator(sb[s]))
+                            {
+                                sb.Length = s;
+                                break;
+                            }
+                        }
+                        if (s < 0)
+                        {
+                            sb.Length = 0;
+                        }
+
+                        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 (flippedSeparator || sb.Length != path.Length)
+            {
+                return StringBuilderCache.GetStringAndRelease(sb);
+            }
+            else
+            {
+                // We haven't changed the source path, return the original
+                StringBuilderCache.Release(sb);
+                return path;
+            }
+        }
+
+        /// <summary>
         /// Create a relative path from one path to another. Paths will be resolved before calculating the difference.
         /// Default path comparison for the active platform will be used (OrdinalIgnoreCase for Windows or Mac, Ordinal for Unix).
         /// </summary>