diff options
author | Anirudh Agnihotry <anirudhagnihotry098@gmail.com> | 2018-02-05 17:39:37 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-02-05 17:39:37 -0800 |
commit | 2ede04461bb94a10a95467c5855ab2162073d6d2 (patch) | |
tree | efd0b88473e92f21a82f243abec18d94045b61e0 /src | |
parent | 4578505a521bb4cbcf16d2361d87d24a9cacd25f (diff) | |
download | coreclr-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.resx | 3 | ||||
-rw-r--r-- | src/mscorlib/shared/System/IO/Path.Unix.cs | 95 | ||||
-rw-r--r-- | src/mscorlib/shared/System/IO/Path.Windows.cs | 127 | ||||
-rw-r--r-- | src/mscorlib/shared/System/IO/Path.cs | 308 |
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> |