PathInternal.cs 10 KB

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