PathHelper.Windows.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  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. #nullable enable
  5. using System.Diagnostics;
  6. using System.Runtime.InteropServices;
  7. using System.Text;
  8. namespace System.IO
  9. {
  10. /// <summary>
  11. /// Wrapper to help with path normalization.
  12. /// </summary>
  13. internal class PathHelper
  14. {
  15. /// <summary>
  16. /// Normalize the given path.
  17. /// </summary>
  18. /// <remarks>
  19. /// Normalizes via Win32 GetFullPathName().
  20. /// </remarks>
  21. /// <param name="path">Path to normalize</param>
  22. /// <exception cref="PathTooLongException">Thrown if we have a string that is too large to fit into a UNICODE_STRING.</exception>
  23. /// <exception cref="IOException">Thrown if the path is empty.</exception>
  24. /// <returns>Normalized path</returns>
  25. internal static string Normalize(string path)
  26. {
  27. Span<char> initialBuffer = stackalloc char[PathInternal.MaxShortPath];
  28. var builder = new ValueStringBuilder(initialBuffer);
  29. // Get the full path
  30. GetFullPathName(path.AsSpan(), ref builder);
  31. // If we have the exact same string we were passed in, don't allocate another string.
  32. // TryExpandShortName does this input identity check.
  33. string result = builder.AsSpan().IndexOf('~') >= 0
  34. ? TryExpandShortFileName(ref builder, originalPath: path)
  35. : builder.AsSpan().Equals(path.AsSpan(), StringComparison.Ordinal) ? path : builder.ToString();
  36. // Clear the buffer
  37. builder.Dispose();
  38. return result;
  39. }
  40. /// <summary>
  41. /// Normalize the given path.
  42. /// </summary>
  43. /// <remarks>
  44. /// Exceptions are the same as the string overload.
  45. /// </remarks>
  46. internal static string Normalize(ref ValueStringBuilder path)
  47. {
  48. Span<char> initialBuffer = stackalloc char[PathInternal.MaxShortPath];
  49. var builder = new ValueStringBuilder(initialBuffer);
  50. // Get the full path
  51. GetFullPathName(path.AsSpan(terminate: true), ref builder);
  52. string result = builder.AsSpan().IndexOf('~') >= 0
  53. ? TryExpandShortFileName(ref builder, originalPath: null)
  54. : builder.ToString();
  55. // Clear the buffer
  56. builder.Dispose();
  57. return result;
  58. }
  59. /// <summary>
  60. /// Calls GetFullPathName on the given path.
  61. /// </summary>
  62. /// <param name="path">The path name. MUST be null terminated after the span.</param>
  63. /// <param name="builder">Builder that will store the result.</param>
  64. private static void GetFullPathName(ReadOnlySpan<char> path, ref ValueStringBuilder builder)
  65. {
  66. // If the string starts with an extended prefix we would need to remove it from the path before we call GetFullPathName as
  67. // it doesn't root extended paths correctly. We don't currently resolve extended paths, so we'll just assert here.
  68. Debug.Assert(PathInternal.IsPartiallyQualified(path) || !PathInternal.IsExtended(path));
  69. uint result = 0;
  70. while ((result = Interop.Kernel32.GetFullPathNameW(ref MemoryMarshal.GetReference(path), (uint)builder.Capacity, ref builder.GetPinnableReference(), IntPtr.Zero)) > builder.Capacity)
  71. {
  72. // Reported size is greater than the buffer size. Increase the capacity.
  73. builder.EnsureCapacity(checked((int)result));
  74. }
  75. if (result == 0)
  76. {
  77. // Failure, get the error and throw
  78. int errorCode = Marshal.GetLastWin32Error();
  79. if (errorCode == 0)
  80. errorCode = Interop.Errors.ERROR_BAD_PATHNAME;
  81. throw Win32Marshal.GetExceptionForWin32Error(errorCode, path.ToString());
  82. }
  83. builder.Length = (int)result;
  84. }
  85. internal static int PrependDevicePathChars(ref ValueStringBuilder content, bool isDosUnc, ref ValueStringBuilder buffer)
  86. {
  87. int length = content.Length;
  88. length += isDosUnc
  89. ? PathInternal.UncExtendedPrefixLength - PathInternal.UncPrefixLength
  90. : PathInternal.DevicePrefixLength;
  91. buffer.EnsureCapacity(length + 1);
  92. buffer.Length = 0;
  93. if (isDosUnc)
  94. {
  95. // Is a \\Server\Share, put \\?\UNC\ in the front
  96. buffer.Append(PathInternal.UncExtendedPathPrefix);
  97. // Copy Server\Share\... over to the buffer
  98. buffer.Append(content.AsSpan(PathInternal.UncPrefixLength));
  99. // Return the prefix difference
  100. return PathInternal.UncExtendedPrefixLength - PathInternal.UncPrefixLength;
  101. }
  102. else
  103. {
  104. // Not an UNC, put the \\?\ prefix in front, then the original string
  105. buffer.Append(PathInternal.ExtendedPathPrefix);
  106. buffer.Append(content.AsSpan());
  107. return PathInternal.DevicePrefixLength;
  108. }
  109. }
  110. internal static string TryExpandShortFileName(ref ValueStringBuilder outputBuilder, string? originalPath)
  111. {
  112. // We guarantee we'll expand short names for paths that only partially exist. As such, we need to find the part of the path that actually does exist. To
  113. // avoid allocating a lot we'll create only one input array and modify the contents with embedded nulls.
  114. Debug.Assert(!PathInternal.IsPartiallyQualified(outputBuilder.AsSpan()), "should have resolved by now");
  115. // We'll have one of a few cases by now (the normalized path will have already:
  116. //
  117. // 1. Dos path (C:\)
  118. // 2. Dos UNC (\\Server\Share)
  119. // 3. Dos device path (\\.\C:\, \\?\C:\)
  120. //
  121. // We want to put the extended syntax on the front if it doesn't already have it (for long path support and speed), which may mean switching from \\.\.
  122. //
  123. // Note that we will never get \??\ here as GetFullPathName() does not recognize \??\ and will return it as C:\??\ (or whatever the current drive is).
  124. int rootLength = PathInternal.GetRootLength(outputBuilder.AsSpan());
  125. bool isDevice = PathInternal.IsDevice(outputBuilder.AsSpan());
  126. // As this is a corner case we're not going to add a stackalloc here to keep the stack pressure down.
  127. var inputBuilder = new ValueStringBuilder();
  128. bool isDosUnc = false;
  129. int rootDifference = 0;
  130. bool wasDotDevice = false;
  131. // Add the extended prefix before expanding to allow growth over MAX_PATH
  132. if (isDevice)
  133. {
  134. // We have one of the following (\\?\ or \\.\)
  135. inputBuilder.Append(outputBuilder.AsSpan());
  136. if (outputBuilder[2] == '.')
  137. {
  138. wasDotDevice = true;
  139. inputBuilder[2] = '?';
  140. }
  141. }
  142. else
  143. {
  144. isDosUnc = !PathInternal.IsDevice(outputBuilder.AsSpan()) && outputBuilder.Length > 1 && outputBuilder[0] == '\\' && outputBuilder[1] == '\\';
  145. rootDifference = PrependDevicePathChars(ref outputBuilder, isDosUnc, ref inputBuilder);
  146. }
  147. rootLength += rootDifference;
  148. int inputLength = inputBuilder.Length;
  149. bool success = false;
  150. int foundIndex = inputBuilder.Length - 1;
  151. while (!success)
  152. {
  153. uint result = Interop.Kernel32.GetLongPathNameW(
  154. ref inputBuilder.GetPinnableReference(terminate: true), ref outputBuilder.GetPinnableReference(), (uint)outputBuilder.Capacity);
  155. // Replace any temporary null we added
  156. if (inputBuilder[foundIndex] == '\0') inputBuilder[foundIndex] = '\\';
  157. if (result == 0)
  158. {
  159. // Look to see if we couldn't find the file
  160. int error = Marshal.GetLastWin32Error();
  161. if (error != Interop.Errors.ERROR_FILE_NOT_FOUND && error != Interop.Errors.ERROR_PATH_NOT_FOUND)
  162. {
  163. // Some other failure, give up
  164. break;
  165. }
  166. // We couldn't find the path at the given index, start looking further back in the string.
  167. foundIndex--;
  168. for (; foundIndex > rootLength && inputBuilder[foundIndex] != '\\'; foundIndex--) ;
  169. if (foundIndex == rootLength)
  170. {
  171. // Can't trim the path back any further
  172. break;
  173. }
  174. else
  175. {
  176. // Temporarily set a null in the string to get Windows to look further up the path
  177. inputBuilder[foundIndex] = '\0';
  178. }
  179. }
  180. else if (result > outputBuilder.Capacity)
  181. {
  182. // Not enough space. The result count for this API does not include the null terminator.
  183. outputBuilder.EnsureCapacity(checked((int)result));
  184. result = Interop.Kernel32.GetLongPathNameW(ref inputBuilder.GetPinnableReference(), ref outputBuilder.GetPinnableReference(), (uint)outputBuilder.Capacity);
  185. }
  186. else
  187. {
  188. // Found the path
  189. success = true;
  190. outputBuilder.Length = checked((int)result);
  191. if (foundIndex < inputLength - 1)
  192. {
  193. // It was a partial find, put the non-existent part of the path back
  194. outputBuilder.Append(inputBuilder.AsSpan(foundIndex, inputBuilder.Length - foundIndex));
  195. }
  196. }
  197. }
  198. // If we were able to expand the path, use it, otherwise use the original full path result
  199. ref ValueStringBuilder builderToUse = ref (success ? ref outputBuilder : ref inputBuilder);
  200. // Switch back from \\?\ to \\.\ if necessary
  201. if (wasDotDevice)
  202. builderToUse[2] = '.';
  203. // Change from \\?\UNC\ to \\?\UN\\ if needed
  204. if (isDosUnc)
  205. builderToUse[PathInternal.UncExtendedPrefixLength - PathInternal.UncPrefixLength] = '\\';
  206. // Strip out any added characters at the front of the string
  207. ReadOnlySpan<char> output = builderToUse.AsSpan(rootDifference);
  208. string returnValue = ((originalPath != null) && output.Equals(originalPath.AsSpan(), StringComparison.Ordinal))
  209. ? originalPath : output.ToString();
  210. inputBuilder.Dispose();
  211. return returnValue;
  212. }
  213. }
  214. }