PageRenderTime 31ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/src/System.Runtime.Extensions/src/System/IO/PathHelper.Windows.cs

https://gitlab.com/0072016/0072016-corefx-
C# | 390 lines | 250 code | 53 blank | 87 comment | 68 complexity | deb88685d06c71aaebf62df1971c208b MD5 | raw file
  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.CompilerServices;
  6. using System.Runtime.InteropServices;
  7. namespace System.IO
  8. {
  9. /// <summary>
  10. /// Wrapper to help with path normalization.
  11. /// </summary>
  12. unsafe internal class PathHelper
  13. {
  14. // Can't be over 8.3 and be a short name
  15. private const int MaxShortName = 12;
  16. private const char LastAnsi = (char)255;
  17. private const char Delete = (char)127;
  18. // Trim trailing white spaces, tabs etc but don't be aggressive in removing everything that has UnicodeCategory of trailing space.
  19. // string.WhitespaceChars will trim more aggressively than what the underlying FS does (for ex, NTFS, FAT).
  20. private static readonly char[] s_trimEndChars =
  21. {
  22. (char)0x9, // Horizontal tab
  23. (char)0xA, // Line feed
  24. (char)0xB, // Vertical tab
  25. (char)0xC, // Form feed
  26. (char)0xD, // Carriage return
  27. (char)0x20, // Space
  28. (char)0x85, // Next line
  29. (char)0xA0 // Non breaking space
  30. };
  31. [ThreadStatic]
  32. private static StringBuffer t_fullPathBuffer;
  33. /// <summary>
  34. /// Normalize the given path.
  35. /// </summary>
  36. /// <remarks>
  37. /// Normalizes via Win32 GetFullPathName(). It will also trim all "typical" whitespace at the end of the path (see s_trimEndChars). Will also trim initial
  38. /// spaces if the path is determined to be rooted.
  39. ///
  40. /// Note that invalid characters will be checked after the path is normalized, which could remove bad characters. (C:\|\..\a.txt -- C:\a.txt)
  41. /// </remarks>
  42. /// <param name="path">Path to normalize</param>
  43. /// <param name="checkInvalidCharacters">True to check for invalid characters</param>
  44. /// <param name="expandShortPaths">Attempt to expand short paths if true</param>
  45. /// <exception cref="ArgumentException">Thrown if the path is an illegal UNC (does not contain a full server/share) or contains illegal characters.</exception>
  46. /// <exception cref="PathTooLongException">Thrown if the path or a path segment exceeds the filesystem limits.</exception>
  47. /// <exception cref="FileNotFoundException">Thrown if Windows returns ERROR_FILE_NOT_FOUND. (See Win32Marshal.GetExceptionForWin32Error)</exception>
  48. /// <exception cref="DirectoryNotFoundException">Thrown if Windows returns ERROR_PATH_NOT_FOUND. (See Win32Marshal.GetExceptionForWin32Error)</exception>
  49. /// <exception cref="UnauthorizedAccessException">Thrown if Windows returns ERROR_ACCESS_DENIED. (See Win32Marshal.GetExceptionForWin32Error)</exception>
  50. /// <exception cref="IOException">Thrown if Windows returns an error that doesn't map to the above. (See Win32Marshal.GetExceptionForWin32Error)</exception>
  51. /// <returns>Normalized path</returns>
  52. internal static string Normalize(string path, bool checkInvalidCharacters, bool expandShortPaths)
  53. {
  54. // Get the full path
  55. StringBuffer fullPath = t_fullPathBuffer ?? (t_fullPathBuffer = new StringBuffer(PathInternal.MaxShortPath));
  56. try
  57. {
  58. GetFullPathName(path, fullPath);
  59. // Trim whitespace off the end of the string. Win32 normalization trims only U+0020.
  60. fullPath.TrimEnd(s_trimEndChars);
  61. if (fullPath.Length >= PathInternal.MaxLongPath)
  62. {
  63. // Fullpath is genuinely too long
  64. throw new PathTooLongException(SR.IO_PathTooLong);
  65. }
  66. // Checking path validity used to happen before getting the full path name. To avoid additional input allocation
  67. // (to trim trailing whitespace) we now do it after the Win32 call. This will allow legitimate paths through that
  68. // used to get kicked back (notably segments with invalid characters might get removed via "..").
  69. //
  70. // There is no way that GetLongPath can invalidate the path so we'll do this (cheaper) check before we attempt to
  71. // expand short file names.
  72. // Scan the path for:
  73. //
  74. // - Illegal path characters.
  75. // - Invalid UNC paths like \\, \\server, \\server\.
  76. // - Segments that are too long (over MaxComponentLength)
  77. // As the path could be > 30K, we'll combine the validity scan. None of these checks are performed by the Win32
  78. // GetFullPathName() API.
  79. bool possibleShortPath = false;
  80. bool foundTilde = false;
  81. // We can get UNCs as device paths through this code (e.g. \\.\UNC\), we won't validate them as there isn't
  82. // an easy way to normalize without extensive cost (we'd have to hunt down the canonical name for any device
  83. // path that contains UNC or to see if the path was doing something like \\.\GLOBALROOT\Device\Mup\,
  84. // \\.\GLOBAL\UNC\, \\.\GLOBALROOT\GLOBAL??\UNC\, etc.
  85. bool specialPath = fullPath.Length > 1 && fullPath[0] == '\\' && fullPath[1] == '\\';
  86. bool isDevice = PathInternal.IsDevice(fullPath);
  87. bool possibleBadUnc = specialPath && !isDevice;
  88. uint index = specialPath ? 2u : 0;
  89. uint lastSeparator = specialPath ? 1u : 0;
  90. uint segmentLength;
  91. char* start = fullPath.CharPointer;
  92. char current;
  93. while (index < fullPath.Length)
  94. {
  95. current = start[index];
  96. // Try to skip deeper analysis. '?' and higher are valid/ignorable except for '\', '|', and '~'
  97. if (current < '?' || current == '\\' || current == '|' || current == '~')
  98. {
  99. switch (current)
  100. {
  101. case '|':
  102. case '>':
  103. case '<':
  104. case '\"':
  105. if (checkInvalidCharacters) throw new ArgumentException(SR.Argument_InvalidPathChars);
  106. foundTilde = false;
  107. break;
  108. case '~':
  109. foundTilde = true;
  110. break;
  111. case '\\':
  112. segmentLength = index - lastSeparator - 1;
  113. if (segmentLength > (uint)PathInternal.MaxComponentLength)
  114. throw new PathTooLongException(SR.IO_PathTooLong + fullPath.ToString());
  115. lastSeparator = index;
  116. if (foundTilde)
  117. {
  118. if (segmentLength <= MaxShortName)
  119. {
  120. // Possibly a short path.
  121. possibleShortPath = true;
  122. }
  123. foundTilde = false;
  124. }
  125. if (possibleBadUnc)
  126. {
  127. // If we're at the end of the path and this is the first separator, we're missing the share.
  128. // Otherwise we're good, so ignore UNC tracking from here.
  129. if (index == fullPath.Length - 1)
  130. throw new ArgumentException(SR.Arg_PathIllegalUNC);
  131. else
  132. possibleBadUnc = false;
  133. }
  134. break;
  135. default:
  136. if (checkInvalidCharacters && current < ' ') throw new ArgumentException(SR.Argument_InvalidPathChars, nameof(path));
  137. break;
  138. }
  139. }
  140. index++;
  141. }
  142. if (possibleBadUnc)
  143. throw new ArgumentException(SR.Arg_PathIllegalUNC);
  144. segmentLength = fullPath.Length - lastSeparator - 1;
  145. if (segmentLength > (uint)PathInternal.MaxComponentLength)
  146. throw new PathTooLongException(SR.IO_PathTooLong);
  147. if (foundTilde && segmentLength <= MaxShortName)
  148. possibleShortPath = true;
  149. // Check for a short filename path and try and expand it. Technically you don't need to have a tilde for a short name, but
  150. // this is how we've always done this. This expansion is costly so we'll continue to let other short paths slide.
  151. if (expandShortPaths && possibleShortPath)
  152. {
  153. return TryExpandShortFileName(fullPath, originalPath: path);
  154. }
  155. else
  156. {
  157. if (fullPath.Length == (uint)path.Length && fullPath.StartsWith(path))
  158. {
  159. // If we have the exact same string we were passed in, don't bother to allocate another string from the StringBuilder.
  160. return path;
  161. }
  162. else
  163. {
  164. return fullPath.ToString();
  165. }
  166. }
  167. }
  168. finally
  169. {
  170. // Clear the buffer
  171. fullPath.Free();
  172. }
  173. }
  174. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  175. private static bool IsDosUnc(StringBuffer buffer)
  176. {
  177. return !PathInternal.IsDevice(buffer) && buffer.Length > 1 && buffer[0] == '\\' && buffer[1] == '\\';
  178. }
  179. private static void GetFullPathName(string path, StringBuffer fullPath)
  180. {
  181. // If the string starts with an extended prefix we would need to remove it from the path before we call GetFullPathName as
  182. // it doesn't root extended paths correctly. We don't currently resolve extended paths, so we'll just assert here.
  183. Debug.Assert(PathInternal.IsPartiallyQualified(path) || !PathInternal.IsExtended(path));
  184. // Historically we would skip leading spaces *only* if the path started with a drive " C:" or a UNC " \\"
  185. int startIndex = PathInternal.PathStartSkip(path);
  186. fixed (char* pathStart = path)
  187. {
  188. uint result = 0;
  189. while ((result = Interop.mincore.GetFullPathNameW(pathStart + startIndex, fullPath.CharCapacity, fullPath.GetHandle(), IntPtr.Zero)) > fullPath.CharCapacity)
  190. {
  191. // Reported size (which does not include the null) is greater than the buffer size. Increase the capacity.
  192. fullPath.EnsureCharCapacity(result);
  193. }
  194. if (result == 0)
  195. {
  196. // Failure, get the error and throw
  197. int errorCode = Marshal.GetLastWin32Error();
  198. if (errorCode == 0)
  199. errorCode = Interop.mincore.Errors.ERROR_BAD_PATHNAME;
  200. throw Win32Marshal.GetExceptionForWin32Error(errorCode, path);
  201. }
  202. fullPath.Length = result;
  203. }
  204. }
  205. private static uint GetInputBuffer(StringBuffer content, bool isDosUnc, out StringBuffer buffer)
  206. {
  207. uint length = content.Length;
  208. length += isDosUnc ? (uint)PathInternal.UncExtendedPrefixToInsert.Length : (uint)PathInternal.ExtendedPathPrefix.Length;
  209. buffer = new StringBuffer(length);
  210. if (isDosUnc)
  211. {
  212. buffer.CopyFrom(bufferIndex: 0, source: PathInternal.UncExtendedPathPrefix);
  213. uint prefixDifference = (uint)(PathInternal.UncExtendedPathPrefix.Length - PathInternal.UncPathPrefix.Length);
  214. content.CopyTo(bufferIndex: prefixDifference, destination: buffer, destinationIndex: (uint)PathInternal.ExtendedPathPrefix.Length, count: content.Length - prefixDifference);
  215. return prefixDifference;
  216. }
  217. else
  218. {
  219. uint prefixSize = (uint)PathInternal.ExtendedPathPrefix.Length;
  220. buffer.CopyFrom(bufferIndex: 0, source: PathInternal.ExtendedPathPrefix);
  221. content.CopyTo(bufferIndex: 0, destination: buffer, destinationIndex: prefixSize, count: content.Length);
  222. return prefixSize;
  223. }
  224. }
  225. private static string TryExpandShortFileName(StringBuffer outputBuffer, string originalPath)
  226. {
  227. // 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
  228. // avoid allocating like crazy we'll create only one input array and modify the contents with embedded nulls.
  229. Debug.Assert(!PathInternal.IsPartiallyQualified(outputBuffer), "should have resolved by now");
  230. // We'll have one of a few cases by now (the normalized path will have already:
  231. //
  232. // 1. Dos path (C:\)
  233. // 2. Dos UNC (\\Server\Share)
  234. // 3. Dos device path (\\.\C:\, \\?\C:\)
  235. //
  236. // We want to put the extended syntax on the front if it doesn't already have it, which may mean switching from \\.\.
  237. //
  238. // Note that we will never get \??\ here as GetFullPathName() does not recognize \??\ and will return it as C:\??\ (or whatever the current drive is).
  239. uint rootLength = PathInternal.GetRootLength(outputBuffer);
  240. bool isDevice = PathInternal.IsDevice(outputBuffer);
  241. StringBuffer inputBuffer = null;
  242. bool isDosUnc = false;
  243. uint rootDifference = 0;
  244. bool wasDotDevice = false;
  245. // Add the extended prefix before expanding to allow growth over MAX_PATH
  246. if (isDevice)
  247. {
  248. // We have one of the following (\\?\ or \\.\)
  249. inputBuffer = new StringBuffer();
  250. inputBuffer.Append(outputBuffer);
  251. if (outputBuffer[2] == '.')
  252. {
  253. wasDotDevice = true;
  254. inputBuffer[2] = '?';
  255. }
  256. }
  257. else
  258. {
  259. isDosUnc = IsDosUnc(outputBuffer);
  260. rootDifference = GetInputBuffer(outputBuffer, isDosUnc, out inputBuffer);
  261. }
  262. rootLength += rootDifference;
  263. uint inputLength = inputBuffer.Length;
  264. bool success = false;
  265. uint foundIndex = inputBuffer.Length - 1;
  266. while (!success)
  267. {
  268. uint result = Interop.mincore.GetLongPathNameW(inputBuffer.GetHandle(), outputBuffer.GetHandle(), outputBuffer.CharCapacity);
  269. // Replace any temporary null we added
  270. if (inputBuffer[foundIndex] == '\0') inputBuffer[foundIndex] = '\\';
  271. if (result == 0)
  272. {
  273. // Look to see if we couldn't find the file
  274. int error = Marshal.GetLastWin32Error();
  275. if (error != Interop.mincore.Errors.ERROR_FILE_NOT_FOUND && error != Interop.mincore.Errors.ERROR_PATH_NOT_FOUND)
  276. {
  277. // Some other failure, give up
  278. break;
  279. }
  280. // We couldn't find the path at the given index, start looking further back in the string.
  281. foundIndex--;
  282. for (; foundIndex > rootLength && inputBuffer[foundIndex] != '\\'; foundIndex--) ;
  283. if (foundIndex == rootLength)
  284. {
  285. // Can't trim the path back any further
  286. break;
  287. }
  288. else
  289. {
  290. // Temporarily set a null in the string to get Windows to look further up the path
  291. inputBuffer[foundIndex] = '\0';
  292. }
  293. }
  294. else if (result > outputBuffer.CharCapacity)
  295. {
  296. // Not enough space. The result count for this API does not include the null terminator.
  297. outputBuffer.EnsureCharCapacity(result);
  298. result = Interop.mincore.GetLongPathNameW(inputBuffer.GetHandle(), outputBuffer.GetHandle(), outputBuffer.CharCapacity);
  299. }
  300. else
  301. {
  302. // Found the path
  303. success = true;
  304. outputBuffer.Length = result;
  305. if (foundIndex < inputLength - 1)
  306. {
  307. // It was a partial find, put the non-existent part of the path back
  308. outputBuffer.Append(inputBuffer, foundIndex, inputBuffer.Length - foundIndex);
  309. }
  310. }
  311. }
  312. // Strip out the prefix and return the string
  313. StringBuffer bufferToUse = success ? outputBuffer : inputBuffer;
  314. if (wasDotDevice)
  315. bufferToUse[2] = '.';
  316. string returnValue = null;
  317. int newLength = (int)(bufferToUse.Length - rootDifference);
  318. if (isDosUnc)
  319. {
  320. // Need to go from \\?\UNC\ to \\?\UN\\
  321. bufferToUse[(uint)PathInternal.UncExtendedPathPrefix.Length - 1] = '\\';
  322. }
  323. // We now need to strip out any added characters at the front of the string
  324. if (bufferToUse.SubstringEquals(originalPath, rootDifference, newLength))
  325. {
  326. // Use the original path to avoid allocating
  327. returnValue = originalPath;
  328. }
  329. else
  330. {
  331. returnValue = bufferToUse.Substring(rootDifference, newLength);
  332. }
  333. inputBuffer.Dispose();
  334. return returnValue;
  335. }
  336. }
  337. }