PathInternal.cs 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  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.
  4. using System.Diagnostics;
  5. using System.Text;
  6. namespace System.IO
  7. {
  8. /// <summary>Contains internal path helpers that are shared between many projects.</summary>
  9. internal static partial class PathInternal
  10. {
  11. /// <summary>
  12. /// Returns true if the path starts in a directory separator.
  13. /// </summary>
  14. internal static bool StartsWithDirectorySeparator(ReadOnlySpan<char> path) => path.Length > 0 && IsDirectorySeparator(path[0]);
  15. #if MS_IO_REDIST
  16. internal static string EnsureTrailingSeparator(string path)
  17. => EndsInDirectorySeparator(path) ? path : path + DirectorySeparatorCharAsString;
  18. internal static bool EndsInDirectorySeparator(string path)
  19. => !string.IsNullOrEmpty(path) && IsDirectorySeparator(path[path.Length - 1]);
  20. #else
  21. internal static string EnsureTrailingSeparator(string path)
  22. => Path.EndsInDirectorySeparator(path.AsSpan()) ? path : path + DirectorySeparatorCharAsString;
  23. #endif
  24. internal static bool IsRoot(ReadOnlySpan<char> path)
  25. => path.Length == GetRootLength(path);
  26. /// <summary>
  27. /// Get the common path length from the start of the string.
  28. /// </summary>
  29. internal static int GetCommonPathLength(string first, string second, bool ignoreCase)
  30. {
  31. int commonChars = EqualStartingCharacterCount(first, second, ignoreCase: ignoreCase);
  32. // If nothing matches
  33. if (commonChars == 0)
  34. return commonChars;
  35. // Or we're a full string and equal length or match to a separator
  36. if (commonChars == first.Length
  37. && (commonChars == second.Length || IsDirectorySeparator(second[commonChars])))
  38. return commonChars;
  39. if (commonChars == second.Length && IsDirectorySeparator(first[commonChars]))
  40. return commonChars;
  41. // It's possible we matched somewhere in the middle of a segment e.g. C:\Foodie and C:\Foobar.
  42. while (commonChars > 0 && !IsDirectorySeparator(first[commonChars - 1]))
  43. commonChars--;
  44. return commonChars;
  45. }
  46. /// <summary>
  47. /// Gets the count of common characters from the left optionally ignoring case
  48. /// </summary>
  49. internal static unsafe int EqualStartingCharacterCount(string first, string second, bool ignoreCase)
  50. {
  51. if (string.IsNullOrEmpty(first) || string.IsNullOrEmpty(second)) return 0;
  52. int commonChars = 0;
  53. fixed (char* f = first)
  54. fixed (char* s = second)
  55. {
  56. char* l = f;
  57. char* r = s;
  58. char* leftEnd = l + first.Length;
  59. char* rightEnd = r + second.Length;
  60. while (l != leftEnd && r != rightEnd
  61. && (*l == *r || (ignoreCase && char.ToUpperInvariant((*l)) == char.ToUpperInvariant((*r)))))
  62. {
  63. commonChars++;
  64. l++;
  65. r++;
  66. }
  67. }
  68. return commonChars;
  69. }
  70. /// <summary>
  71. /// Returns true if the two paths have the same root
  72. /// </summary>
  73. internal static bool AreRootsEqual(string first, string second, StringComparison comparisonType)
  74. {
  75. int firstRootLength = GetRootLength(first.AsSpan());
  76. int secondRootLength = GetRootLength(second.AsSpan());
  77. return firstRootLength == secondRootLength
  78. && string.Compare(
  79. strA: first,
  80. indexA: 0,
  81. strB: second,
  82. indexB: 0,
  83. length: firstRootLength,
  84. comparisonType: comparisonType) == 0;
  85. }
  86. /// <summary>
  87. /// Try to remove relative segments from the given path (without combining with a root).
  88. /// </summary>
  89. /// <param name="path">Input path</param>
  90. /// <param name="rootLength">The length of the root of the given path</param>
  91. internal static string RemoveRelativeSegments(string path, int rootLength)
  92. {
  93. Span<char> initialBuffer = stackalloc char[260 /* PathInternal.MaxShortPath */];
  94. ValueStringBuilder sb = new ValueStringBuilder(initialBuffer);
  95. if (RemoveRelativeSegments(path.AsSpan(), rootLength, ref sb))
  96. {
  97. path = sb.ToString();
  98. }
  99. sb.Dispose();
  100. return path;
  101. }
  102. /// <summary>
  103. /// Try to remove relative segments from the given path (without combining with a root).
  104. /// </summary>
  105. /// <param name="path">Input path</param>
  106. /// <param name="rootLength">The length of the root of the given path</param>
  107. /// <param name="sb">String builder that will store the result</param>
  108. /// <returns>"true" if the path was modified</returns>
  109. internal static bool RemoveRelativeSegments(ReadOnlySpan<char> path, int rootLength, ref ValueStringBuilder sb)
  110. {
  111. Debug.Assert(rootLength > 0);
  112. bool flippedSeparator = false;
  113. int skip = rootLength;
  114. // We treat "\.." , "\." and "\\" as a relative segment. We want to collapse the first separator past the root presuming
  115. // the root actually ends in a separator. Otherwise the first segment for RemoveRelativeSegments
  116. // in cases like "\\?\C:\.\" and "\\?\C:\..\", the first segment after the root will be ".\" and "..\" which is not considered as a relative segment and hence not be removed.
  117. if (PathInternal.IsDirectorySeparator(path[skip - 1]))
  118. skip--;
  119. // Remove "//", "/./", and "/../" from the path by copying each character to the output,
  120. // except the ones we're removing, such that the builder contains the normalized path
  121. // at the end.
  122. if (skip > 0)
  123. {
  124. sb.Append(path.Slice(0, skip));
  125. }
  126. for (int i = skip; i < path.Length; i++)
  127. {
  128. char c = path[i];
  129. if (PathInternal.IsDirectorySeparator(c) && i + 1 < path.Length)
  130. {
  131. // Skip this character if it's a directory separator and if the next character is, too,
  132. // e.g. "parent//child" => "parent/child"
  133. if (PathInternal.IsDirectorySeparator(path[i + 1]))
  134. {
  135. continue;
  136. }
  137. // Skip this character and the next if it's referring to the current directory,
  138. // e.g. "parent/./child" => "parent/child"
  139. if ((i + 2 == path.Length || PathInternal.IsDirectorySeparator(path[i + 2])) &&
  140. path[i + 1] == '.')
  141. {
  142. i++;
  143. continue;
  144. }
  145. // Skip this character and the next two if it's referring to the parent directory,
  146. // e.g. "parent/child/../grandchild" => "parent/grandchild"
  147. if (i + 2 < path.Length &&
  148. (i + 3 == path.Length || PathInternal.IsDirectorySeparator(path[i + 3])) &&
  149. path[i + 1] == '.' && path[i + 2] == '.')
  150. {
  151. // Unwind back to the last slash (and if there isn't one, clear out everything).
  152. int s;
  153. for (s = sb.Length - 1; s >= skip; s--)
  154. {
  155. if (PathInternal.IsDirectorySeparator(sb[s]))
  156. {
  157. sb.Length = (i + 3 >= path.Length && s == skip) ? s + 1 : s; // to avoid removing the complete "\tmp\" segment in cases like \\?\C:\tmp\..\, C:\tmp\..
  158. break;
  159. }
  160. }
  161. if (s < skip)
  162. {
  163. sb.Length = skip;
  164. }
  165. i += 2;
  166. continue;
  167. }
  168. }
  169. // Normalize the directory separator if needed
  170. if (c != PathInternal.DirectorySeparatorChar && c == PathInternal.AltDirectorySeparatorChar)
  171. {
  172. c = PathInternal.DirectorySeparatorChar;
  173. flippedSeparator = true;
  174. }
  175. sb.Append(c);
  176. }
  177. // If we haven't changed the source path, return the original
  178. if (!flippedSeparator && sb.Length == path.Length)
  179. {
  180. return false;
  181. }
  182. // We may have eaten the trailing separator from the root when we started and not replaced it
  183. if (skip != rootLength && sb.Length < rootLength)
  184. {
  185. sb.Append(path[rootLength - 1]);
  186. }
  187. return true;
  188. }
  189. }
  190. }