PathHelper.Windows.cs 11 KB

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