PathInternal.cs 9.5 KB

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