PathHelper.Windows.cs 11 KB

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