1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
5 using System.Diagnostics;
6 using System.Runtime.InteropServices;
11 // Provides methods for processing file system strings in a cross-platform manner.
12 // Most of the methods don't do a complete parsing (such as examining a UNC hostname),
13 // but they will handle most string operations.
14 public static partial class Path
16 // Public static readonly variant of the separators. The Path implementation itself is using
17 // internal const variant of the separators for better performance.
18 public static readonly char DirectorySeparatorChar = PathInternal.DirectorySeparatorChar;
19 public static readonly char AltDirectorySeparatorChar = PathInternal.AltDirectorySeparatorChar;
20 public static readonly char VolumeSeparatorChar = PathInternal.VolumeSeparatorChar;
21 public static readonly char PathSeparator = PathInternal.PathSeparator;
23 // For generating random file names
24 // 8 random bytes provides 12 chars in our encoding for the 8.3 name.
25 private const int KeyLength = 8;
27 [Obsolete("Please use GetInvalidPathChars or GetInvalidFileNameChars instead.")]
28 public static readonly char[] InvalidPathChars = GetInvalidPathChars();
30 // Changes the extension of a file path. The path parameter
31 // specifies a file path, and the extension parameter
32 // specifies a file extension (with a leading period, such as
35 // The function returns a file path with the same root, directory, and base
36 // name parts as path, but with the file extension changed to
37 // the specified extension. If path is null, the function
38 // returns null. If path does not contain a file extension,
39 // the new file extension is appended to the path. If extension
40 // is null, any existing extension is removed from path.
41 public static string ChangeExtension(string path, string extension)
46 for (int i = path.Length - 1; i >= 0; i--)
51 s = path.Substring(0, i);
54 if (PathInternal.IsDirectorySeparator(ch)) break;
57 if (extension != null && path.Length != 0)
59 s = (extension.Length == 0 || extension[0] != '.') ?
70 /// Returns the directory portion of a file path. This method effectively
71 /// removes the last segment of the given file path, i.e. it returns a
72 /// string consisting of all characters up to but not including the last
73 /// backslash ("\") in the file path. The returned value is null if the
74 /// specified path is null, empty, or a root (such as "\", "C:", or
75 /// "\\server\share").
78 /// Directory separators are normalized in the returned string.
80 public static string GetDirectoryName(string path)
82 if (PathInternal.IsEffectivelyEmpty(path))
85 int end = GetDirectoryNameOffset(path);
86 return end >= 0 ? PathInternal.NormalizeDirectorySeparators(path.Substring(0, end)) : null;
90 /// Returns the directory portion of a file path. The returned value is empty
91 /// if the specified path is null, empty, or a root (such as "\", "C:", or
92 /// "\\server\share").
95 /// Unlike the string overload, this method will not normalize directory separators.
97 public static ReadOnlySpan<char> GetDirectoryName(ReadOnlySpan<char> path)
99 if (PathInternal.IsEffectivelyEmpty(path))
100 return ReadOnlySpan<char>.Empty;
102 int end = GetDirectoryNameOffset(path);
103 return end >= 0 ? path.Slice(0, end) : ReadOnlySpan<char>.Empty;
106 private static int GetDirectoryNameOffset(ReadOnlySpan<char> path)
108 int rootLength = PathInternal.GetRootLength(path);
109 int end = path.Length;
110 if (end <= rootLength)
113 while (end > rootLength && !PathInternal.IsDirectorySeparator(path[--end]));
115 // Trim off any remaining separators (to deal with C:\foo\\bar)
116 while (end > rootLength && PathInternal.IsDirectorySeparator(path[end - 1]))
123 /// Returns the extension of the given path. The returned value includes the period (".") character of the
124 /// extension except when you have a terminal period when you get string.Empty, such as ".exe" or ".cpp".
125 /// The returned value is null if the given path is null or empty if the given path does not include an
128 public static string GetExtension(string path)
133 return new string(GetExtension(path.AsSpan()));
137 /// Returns the extension of the given path.
140 /// The returned value is an empty ReadOnlySpan if the given path does not include an extension.
142 public static ReadOnlySpan<char> GetExtension(ReadOnlySpan<char> path)
144 int length = path.Length;
146 for (int i = length - 1; i >= 0; i--)
152 return path.Slice(i, length - i);
154 return ReadOnlySpan<char>.Empty;
156 if (PathInternal.IsDirectorySeparator(ch))
159 return ReadOnlySpan<char>.Empty;
163 /// Returns the name and extension parts of the given path. The resulting string contains
164 /// the characters of path that follow the last separator in path. The resulting string is
165 /// null if path is null.
167 public static string GetFileName(string path)
172 ReadOnlySpan<char> result = GetFileName(path.AsSpan());
173 if (path.Length == result.Length)
176 return new string(result);
180 /// The returned ReadOnlySpan contains the characters of the path that follows the last separator in path.
182 public static ReadOnlySpan<char> GetFileName(ReadOnlySpan<char> path)
184 int root = GetPathRoot(path).Length;
186 // We don't want to cut off "C:\file.txt:stream" (i.e. should be "file.txt:stream")
187 // but we *do* want "C:Foo" => "Foo". This necessitates checking for the root.
189 for (int i = path.Length; --i >= 0;)
191 if (i < root || PathInternal.IsDirectorySeparator(path[i]))
192 return path.Slice(i + 1, path.Length - i - 1);
198 public static string GetFileNameWithoutExtension(string path)
203 ReadOnlySpan<char> result = GetFileNameWithoutExtension(path.AsSpan());
204 if (path.Length == result.Length)
207 return new string(result);
211 /// Returns the characters between the last separator and last (.) in the path.
213 public static ReadOnlySpan<char> GetFileNameWithoutExtension(ReadOnlySpan<char> path)
215 ReadOnlySpan<char> fileName = GetFileName(path);
216 int lastPeriod = fileName.LastIndexOf('.');
217 return lastPeriod == -1 ?
218 fileName : // No extension was found
219 fileName.Slice(0, lastPeriod);
223 /// Returns a cryptographically strong random 8.3 string that can be
224 /// used as either a folder name or a file name.
226 public static unsafe string GetRandomFileName()
228 byte* pKey = stackalloc byte[KeyLength];
229 Interop.GetRandomBytes(pKey, KeyLength);
231 const int RandomFileNameLength = 12;
232 char* pRandomFileName = stackalloc char[RandomFileNameLength];
233 Populate83FileNameFromRandomBytes(pKey, KeyLength, pRandomFileName, RandomFileNameLength);
234 return new string(pRandomFileName, 0, RandomFileNameLength);
238 /// Returns true if the path is fixed to a specific drive or UNC path. This method does no
239 /// validation of the path (URIs will be returned as relative as a result).
240 /// Returns false if the path specified is relative to the current drive or working directory.
243 /// Handles paths that use the alternate directory separator. It is a frequent mistake to
244 /// assume that rooted paths <see cref="Path.IsPathRooted(string)"/> are not relative. This isn't the case.
245 /// "C:a" is drive relative- meaning that it will be resolved against the current directory
246 /// for C: (rooted, but relative). "C:\a" is rooted and not relative (the current directory
247 /// will not be used to modify the path).
249 /// <exception cref="ArgumentNullException">
250 /// Thrown if <paramref name="path"/> is null.
252 public static bool IsPathFullyQualified(string path)
255 throw new ArgumentNullException(nameof(path));
257 return IsPathFullyQualified(path.AsSpan());
260 public static bool IsPathFullyQualified(ReadOnlySpan<char> path)
262 return !PathInternal.IsPartiallyQualified(path);
267 /// Tests if a path's file name includes a file extension. A trailing period
268 /// is not considered an extension.
270 public static bool HasExtension(string path)
274 return HasExtension(path.AsSpan());
279 public static bool HasExtension(ReadOnlySpan<char> path)
281 for (int i = path.Length - 1; i >= 0; i--)
286 return i != path.Length - 1;
288 if (PathInternal.IsDirectorySeparator(ch))
294 public static string Combine(string path1, string path2)
296 if (path1 == null || path2 == null)
297 throw new ArgumentNullException((path1 == null) ? nameof(path1) : nameof(path2));
299 return CombineNoChecks(path1, path2);
302 public static string Combine(string path1, string path2, string path3)
304 if (path1 == null || path2 == null || path3 == null)
305 throw new ArgumentNullException((path1 == null) ? nameof(path1) : (path2 == null) ? nameof(path2) : nameof(path3));
307 return CombineNoChecks(path1, path2, path3);
310 public static string Combine(string path1, string path2, string path3, string path4)
312 if (path1 == null || path2 == null || path3 == null || path4 == null)
313 throw new ArgumentNullException((path1 == null) ? nameof(path1) : (path2 == null) ? nameof(path2) : (path3 == null) ? nameof(path3) : nameof(path4));
315 return CombineNoChecks(path1, path2, path3, path4);
318 public static string Combine(params string[] paths)
322 throw new ArgumentNullException(nameof(paths));
326 int firstComponent = 0;
328 // We have two passes, the first calculates how large a buffer to allocate and does some precondition
329 // checks on the paths passed in. The second actually does the combination.
331 for (int i = 0; i < paths.Length; i++)
333 if (paths[i] == null)
335 throw new ArgumentNullException(nameof(paths));
338 if (paths[i].Length == 0)
343 if (IsPathRooted(paths[i]))
346 finalSize = paths[i].Length;
350 finalSize += paths[i].Length;
353 char ch = paths[i][paths[i].Length - 1];
354 if (!PathInternal.IsDirectorySeparator(ch))
358 StringBuilder finalPath = StringBuilderCache.Acquire(finalSize);
360 for (int i = firstComponent; i < paths.Length; i++)
362 if (paths[i].Length == 0)
367 if (finalPath.Length == 0)
369 finalPath.Append(paths[i]);
373 char ch = finalPath[finalPath.Length - 1];
374 if (!PathInternal.IsDirectorySeparator(ch))
376 finalPath.Append(PathInternal.DirectorySeparatorChar);
379 finalPath.Append(paths[i]);
383 return StringBuilderCache.GetStringAndRelease(finalPath);
387 /// Combines two paths. Does no validation of paths, only concatenates the paths
388 /// and places a directory separator between them if needed.
390 private static string CombineNoChecks(ReadOnlySpan<char> first, ReadOnlySpan<char> second)
392 if (first.Length == 0)
393 return second.Length == 0
395 : new string(second);
397 if (second.Length == 0)
398 return new string(first);
400 if (IsPathRooted(second))
401 return new string(second);
403 return CombineNoChecksInternal(first, second);
406 private static string CombineNoChecks(string first, string second)
408 if (string.IsNullOrEmpty(first))
411 if (string.IsNullOrEmpty(second))
414 if (IsPathRooted(second.AsSpan()))
417 return CombineNoChecksInternal(first, second);
420 private static string CombineNoChecks(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third)
422 if (first.Length == 0)
423 return CombineNoChecks(second, third);
424 if (second.Length == 0)
425 return CombineNoChecks(first, third);
426 if (third.Length == 0)
427 return CombineNoChecks(first, second);
429 if (IsPathRooted(third))
430 return new string(third);
431 if (IsPathRooted(second))
432 return CombineNoChecks(second, third);
434 return CombineNoChecksInternal(first, second, third);
437 private static string CombineNoChecks(string first, string second, string third)
439 if (string.IsNullOrEmpty(first))
440 return CombineNoChecks(second, third);
441 if (string.IsNullOrEmpty(second))
442 return CombineNoChecks(first, third);
443 if (string.IsNullOrEmpty(third))
444 return CombineNoChecks(first, second);
446 if (IsPathRooted(third.AsSpan()))
448 if (IsPathRooted(second.AsSpan()))
449 return CombineNoChecks(second, third);
451 return CombineNoChecksInternal(first, second, third);
454 private static string CombineNoChecks(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third, ReadOnlySpan<char> fourth)
456 if (first.Length == 0)
457 return CombineNoChecks(second, third, fourth);
458 if (second.Length == 0)
459 return CombineNoChecks(first, third, fourth);
460 if (third.Length == 0)
461 return CombineNoChecks(first, second, fourth);
462 if (fourth.Length == 0)
463 return CombineNoChecks(first, second, third);
465 if (IsPathRooted(fourth))
466 return new string(fourth);
467 if (IsPathRooted(third))
468 return CombineNoChecks(third, fourth);
469 if (IsPathRooted(second))
470 return CombineNoChecks(second, third, fourth);
472 return CombineNoChecksInternal(first, second, third, fourth);
475 private static string CombineNoChecks(string first, string second, string third, string fourth)
477 if (string.IsNullOrEmpty(first))
478 return CombineNoChecks(second, third, fourth);
479 if (string.IsNullOrEmpty(second))
480 return CombineNoChecks(first, third, fourth);
481 if (string.IsNullOrEmpty(third))
482 return CombineNoChecks(first, second, fourth);
483 if (string.IsNullOrEmpty(fourth))
484 return CombineNoChecks(first, second, third);
486 if (IsPathRooted(fourth.AsSpan()))
488 if (IsPathRooted(third.AsSpan()))
489 return CombineNoChecks(third, fourth);
490 if (IsPathRooted(second.AsSpan()))
491 return CombineNoChecks(second, third, fourth);
493 return CombineNoChecksInternal(first, second, third, fourth);
496 private unsafe static string CombineNoChecksInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second)
498 Debug.Assert(first.Length > 0 && second.Length > 0, "should have dealt with empty paths");
500 bool hasSeparator = PathInternal.IsDirectorySeparator(first[first.Length - 1])
501 || PathInternal.IsDirectorySeparator(second[0]);
503 fixed (char* f = &MemoryMarshal.GetReference(first), s = &MemoryMarshal.GetReference(second))
505 return string.Create(
506 first.Length + second.Length + (hasSeparator ? 0 : 1),
507 (First: (IntPtr)f, FirstLength: first.Length, Second: (IntPtr)s, SecondLength: second.Length, HasSeparator: hasSeparator),
508 (destination, state) =>
510 new Span<char>((char*)state.First, state.FirstLength).CopyTo(destination);
511 if (!state.HasSeparator)
512 destination[state.FirstLength] = PathInternal.DirectorySeparatorChar;
513 new Span<char>((char*)state.Second, state.SecondLength).CopyTo(destination.Slice(state.FirstLength + (state.HasSeparator ? 0 : 1)));
518 private unsafe static string CombineNoChecksInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third)
520 Debug.Assert(first.Length > 0 && second.Length > 0 && third.Length > 0, "should have dealt with empty paths");
522 bool firstHasSeparator = PathInternal.IsDirectorySeparator(first[first.Length - 1])
523 || PathInternal.IsDirectorySeparator(second[0]);
524 bool thirdHasSeparator = PathInternal.IsDirectorySeparator(second[second.Length - 1])
525 || PathInternal.IsDirectorySeparator(third[0]);
527 fixed (char* f = &MemoryMarshal.GetReference(first), s = &MemoryMarshal.GetReference(second), t = &MemoryMarshal.GetReference(third))
529 return string.Create(
530 first.Length + second.Length + third.Length + (firstHasSeparator ? 0 : 1) + (thirdHasSeparator ? 0 : 1),
531 (First: (IntPtr)f, FirstLength: first.Length, Second: (IntPtr)s, SecondLength: second.Length,
532 Third: (IntPtr)t, ThirdLength: third.Length, FirstHasSeparator: firstHasSeparator, ThirdHasSeparator: thirdHasSeparator),
533 (destination, state) =>
535 new Span<char>((char*)state.First, state.FirstLength).CopyTo(destination);
536 if (!state.FirstHasSeparator)
537 destination[state.FirstLength] = PathInternal.DirectorySeparatorChar;
538 new Span<char>((char*)state.Second, state.SecondLength).CopyTo(destination.Slice(state.FirstLength + (state.FirstHasSeparator ? 0 : 1)));
539 if (!state.ThirdHasSeparator)
540 destination[destination.Length - state.ThirdLength - 1] = PathInternal.DirectorySeparatorChar;
541 new Span<char>((char*)state.Third, state.ThirdLength).CopyTo(destination.Slice(destination.Length - state.ThirdLength));
546 private unsafe static string CombineNoChecksInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third, ReadOnlySpan<char> fourth)
548 Debug.Assert(first.Length > 0 && second.Length > 0 && third.Length > 0 && fourth.Length > 0, "should have dealt with empty paths");
550 bool firstHasSeparator = PathInternal.IsDirectorySeparator(first[first.Length - 1])
551 || PathInternal.IsDirectorySeparator(second[0]);
552 bool thirdHasSeparator = PathInternal.IsDirectorySeparator(second[second.Length - 1])
553 || PathInternal.IsDirectorySeparator(third[0]);
554 bool fourthHasSeparator = PathInternal.IsDirectorySeparator(third[third.Length - 1])
555 || PathInternal.IsDirectorySeparator(fourth[0]);
557 fixed (char* f = &MemoryMarshal.GetReference(first), s = &MemoryMarshal.GetReference(second), t = &MemoryMarshal.GetReference(third), u = &MemoryMarshal.GetReference(fourth))
559 return string.Create(
560 first.Length + second.Length + third.Length + fourth.Length + (firstHasSeparator ? 0 : 1) + (thirdHasSeparator ? 0 : 1) + (fourthHasSeparator ? 0 : 1),
561 (First: (IntPtr)f, FirstLength: first.Length, Second: (IntPtr)s, SecondLength: second.Length,
562 Third: (IntPtr)t, ThirdLength: third.Length, Fourth: (IntPtr)u, FourthLength:fourth.Length,
563 FirstHasSeparator: firstHasSeparator, ThirdHasSeparator: thirdHasSeparator, FourthHasSeparator: fourthHasSeparator),
564 (destination, state) =>
566 new Span<char>((char*)state.First, state.FirstLength).CopyTo(destination);
567 if (!state.FirstHasSeparator)
568 destination[state.FirstLength] = PathInternal.DirectorySeparatorChar;
569 new Span<char>((char*)state.Second, state.SecondLength).CopyTo(destination.Slice(state.FirstLength + (state.FirstHasSeparator ? 0 : 1)));
570 if (!state.ThirdHasSeparator)
571 destination[state.FirstLength + state.SecondLength + (state.FirstHasSeparator ? 0 : 1)] = PathInternal.DirectorySeparatorChar;
572 new Span<char>((char*)state.Third, state.ThirdLength).CopyTo(destination.Slice(state.FirstLength + state.SecondLength + (state.FirstHasSeparator ? 0 : 1) + (state.ThirdHasSeparator ? 0 : 1)));
573 if (!state.FourthHasSeparator)
574 destination[destination.Length - state.FourthLength - 1] = PathInternal.DirectorySeparatorChar;
575 new Span<char>((char*)state.Fourth, state.FourthLength).CopyTo(destination.Slice(destination.Length - state.FourthLength));
580 private static readonly char[] s_base32Char = {
581 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
582 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
583 'q', 'r', 's', 't', 'u', 'v', 'w', 'x',
584 'y', 'z', '0', '1', '2', '3', '4', '5'};
586 private static unsafe void Populate83FileNameFromRandomBytes(byte* bytes, int byteCount, char* chars, int charCount)
588 Debug.Assert(bytes != null);
589 Debug.Assert(chars != null);
591 // This method requires bytes of length 8 and chars of length 12.
592 Debug.Assert(byteCount == 8, $"Unexpected {nameof(byteCount)}");
593 Debug.Assert(charCount == 12, $"Unexpected {nameof(charCount)}");
601 // Consume the 5 Least significant bits of the first 5 bytes
602 chars[0] = s_base32Char[b0 & 0x1F];
603 chars[1] = s_base32Char[b1 & 0x1F];
604 chars[2] = s_base32Char[b2 & 0x1F];
605 chars[3] = s_base32Char[b3 & 0x1F];
606 chars[4] = s_base32Char[b4 & 0x1F];
608 // Consume 3 MSB of b0, b1, MSB bits 6, 7 of b3, b4
609 chars[5] = s_base32Char[(
611 ((b3 & 0x60) >> 2))];
613 chars[6] = s_base32Char[(
615 ((b4 & 0x60) >> 2))];
617 // Consume 3 MSB bits of b2, 1 MSB bit of b3, b4
620 Debug.Assert(((b2 & 0xF8) == 0), "Unexpected set bits");
622 if ((b3 & 0x80) != 0)
624 if ((b4 & 0x80) != 0)
627 chars[7] = s_base32Char[b2];
629 // Set the file extension separator
632 // Consume the 5 Least significant bits of the remaining 3 bytes
633 chars[9] = s_base32Char[(bytes[5] & 0x1F)];
634 chars[10] = s_base32Char[(bytes[6] & 0x1F)];
635 chars[11] = s_base32Char[(bytes[7] & 0x1F)];
639 /// Try to remove relative segments from the given path (without combining with a root).
641 /// <param name="skip">Skip the specified number of characters before evaluating.</param>
642 private static string RemoveRelativeSegments(string path, int skip = 0)
644 bool flippedSeparator = false;
646 // Remove "//", "/./", and "/../" from the path by copying each character to the output,
647 // except the ones we're removing, such that the builder contains the normalized path
649 StringBuilder sb = StringBuilderCache.Acquire(path.Length);
652 sb.Append(path, 0, skip);
655 for (int i = skip; i < path.Length; i++)
659 if (PathInternal.IsDirectorySeparator(c) && i + 1 < path.Length)
661 // Skip this character if it's a directory separator and if the next character is, too,
662 // e.g. "parent//child" => "parent/child"
663 if (PathInternal.IsDirectorySeparator(path[i + 1]))
668 // Skip this character and the next if it's referring to the current directory,
669 // e.g. "parent/./child" => "parent/child"
670 if ((i + 2 == path.Length || PathInternal.IsDirectorySeparator(path[i + 2])) &&
677 // Skip this character and the next two if it's referring to the parent directory,
678 // e.g. "parent/child/../grandchild" => "parent/grandchild"
679 if (i + 2 < path.Length &&
680 (i + 3 == path.Length || PathInternal.IsDirectorySeparator(path[i + 3])) &&
681 path[i + 1] == '.' && path[i + 2] == '.')
683 // Unwind back to the last slash (and if there isn't one, clear out everything).
685 for (s = sb.Length - 1; s >= 0; s--)
687 if (PathInternal.IsDirectorySeparator(sb[s]))
703 // Normalize the directory separator if needed
704 if (c != PathInternal.DirectorySeparatorChar && c == PathInternal.AltDirectorySeparatorChar)
706 c = PathInternal.DirectorySeparatorChar;
707 flippedSeparator = true;
713 if (flippedSeparator || sb.Length != path.Length)
715 return StringBuilderCache.GetStringAndRelease(sb);
719 // We haven't changed the source path, return the original
720 StringBuilderCache.Release(sb);
726 /// Create a relative path from one path to another. Paths will be resolved before calculating the difference.
727 /// Default path comparison for the active platform will be used (OrdinalIgnoreCase for Windows or Mac, Ordinal for Unix).
729 /// <param name="relativeTo">The source path the output should be relative to. This path is always considered to be a directory.</param>
730 /// <param name="path">The destination path.</param>
731 /// <returns>The relative path or <paramref name="path"/> if the paths don't share the same root.</returns>
732 /// <exception cref="ArgumentNullException">Thrown if <paramref name="relativeTo"/> or <paramref name="path"/> is <c>null</c> or an empty string.</exception>
733 public static string GetRelativePath(string relativeTo, string path)
735 return GetRelativePath(relativeTo, path, StringComparison);
738 private static string GetRelativePath(string relativeTo, string path, StringComparison comparisonType)
740 if (string.IsNullOrEmpty(relativeTo)) throw new ArgumentNullException(nameof(relativeTo));
741 if (PathInternal.IsEffectivelyEmpty(path)) throw new ArgumentNullException(nameof(path));
742 Debug.Assert(comparisonType == StringComparison.Ordinal || comparisonType == StringComparison.OrdinalIgnoreCase);
744 relativeTo = GetFullPath(relativeTo);
745 path = GetFullPath(path);
747 // Need to check if the roots are different- if they are we need to return the "to" path.
748 if (!PathInternal.AreRootsEqual(relativeTo, path, comparisonType))
751 int commonLength = PathInternal.GetCommonPathLength(relativeTo, path, ignoreCase: comparisonType == StringComparison.OrdinalIgnoreCase);
753 // If there is nothing in common they can't share the same root, return the "to" path as is.
754 if (commonLength == 0)
757 // Trailing separators aren't significant for comparison
758 int relativeToLength = relativeTo.Length;
759 if (PathInternal.EndsInDirectorySeparator(relativeTo))
762 bool pathEndsInSeparator = PathInternal.EndsInDirectorySeparator(path);
763 int pathLength = path.Length;
764 if (pathEndsInSeparator)
767 // If we have effectively the same path, return "."
768 if (relativeToLength == pathLength && commonLength >= relativeToLength) return ".";
770 // We have the same root, we need to calculate the difference now using the
771 // common Length and Segment count past the length.
775 // C:\Foo C:\Bar L3, S1 -> ..\Bar
776 // C:\Foo C:\Foo\Bar L6, S0 -> Bar
777 // C:\Foo\Bar C:\Bar\Bar L3, S2 -> ..\..\Bar\Bar
778 // C:\Foo\Foo C:\Foo\Bar L7, S1 -> ..\Bar
780 StringBuilder sb = StringBuilderCache.Acquire(Math.Max(relativeTo.Length, path.Length));
782 // Add parent segments for segments past the common on the "from" path
783 if (commonLength < relativeToLength)
785 sb.Append(PathInternal.ParentDirectoryPrefix);
787 for (int i = commonLength; i < relativeToLength; i++)
789 if (PathInternal.IsDirectorySeparator(relativeTo[i]))
791 sb.Append(PathInternal.ParentDirectoryPrefix);
795 else if (PathInternal.IsDirectorySeparator(path[commonLength]))
797 // No parent segments and we need to eat the initial separator
798 // (C:\Foo C:\Foo\Bar case)
802 // Now add the rest of the "to" path, adding back the trailing separator
803 int count = pathLength - commonLength;
804 if (pathEndsInSeparator)
807 sb.Append(path, commonLength, count);
808 return StringBuilderCache.GetStringAndRelease(sb);
811 /// <summary>Returns a comparison that can be used to compare file and directory names for equality.</summary>
812 internal static StringComparison StringComparison
816 return IsCaseSensitive ?
817 StringComparison.Ordinal :
818 StringComparison.OrdinalIgnoreCase;