summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAnirudh Agnihotry <anirudhagnihotry098@gmail.com>2018-02-05 17:39:37 -0800
committerGitHub <noreply@github.com>2018-02-05 17:39:37 -0800
commit2ede04461bb94a10a95467c5855ab2162073d6d2 (patch)
treeefd0b88473e92f21a82f243abec18d94045b61e0 /src
parent4578505a521bb4cbcf16d2361d87d24a9cacd25f (diff)
downloadcoreclr-2ede04461bb94a10a95467c5855ab2162073d6d2.tar.gz
coreclr-2ede04461bb94a10a95467c5855ab2162073d6d2.tar.bz2
coreclr-2ede04461bb94a10a95467c5855ab2162073d6d2.zip
Implementation of GetFullPath(string path, string basePath) (#15579)
GetFullPathAPI Overload
Diffstat (limited to 'src')
-rw-r--r--src/mscorlib/Resources/Strings.resx3
-rw-r--r--src/mscorlib/shared/System/IO/Path.Unix.cs95
-rw-r--r--src/mscorlib/shared/System/IO/Path.Windows.cs127
-rw-r--r--src/mscorlib/shared/System/IO/Path.cs308
4 files changed, 354 insertions, 179 deletions
diff --git a/src/mscorlib/Resources/Strings.resx b/src/mscorlib/Resources/Strings.resx
index 2b66ce5a03..b98553c2a0 100644
--- a/src/mscorlib/Resources/Strings.resx
+++ b/src/mscorlib/Resources/Strings.resx
@@ -3700,6 +3700,9 @@
<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>
diff --git a/src/mscorlib/shared/System/IO/Path.Unix.cs b/src/mscorlib/shared/System/IO/Path.Unix.cs
index 81a796bc4f..7e18a53785 100644
--- a/src/mscorlib/shared/System/IO/Path.Unix.cs
+++ b/src/mscorlib/shared/System/IO/Path.Unix.cs
@@ -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)
diff --git a/src/mscorlib/shared/System/IO/Path.Windows.cs b/src/mscorlib/shared/System/IO/Path.Windows.cs
index 862617d6d8..0d969db55c 100644
--- a/src/mscorlib/shared/System/IO/Path.Windows.cs
+++ b/src/mscorlib/shared/System/IO/Path.Windows.cs
@@ -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;
+ }
}
}
diff --git a/src/mscorlib/shared/System/IO/Path.cs b/src/mscorlib/shared/System/IO/Path.cs
index 3814a92182..892cdafa59 100644
--- a/src/mscorlib/shared/System/IO/Path.cs
+++ b/src/mscorlib/shared/System/IO/Path.cs
@@ -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>