PathInternal.Windows.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  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.CodeAnalysis;
  6. using System.Runtime.CompilerServices;
  7. using System.Text;
  8. namespace System.IO
  9. {
  10. /// <summary>Contains internal path helpers that are shared between many projects.</summary>
  11. internal static partial class PathInternal
  12. {
  13. // All paths in Win32 ultimately end up becoming a path to a File object in the Windows object manager. Passed in paths get mapped through
  14. // DosDevice symbolic links in the object tree to actual File objects under \Devices. To illustrate, this is what happens with a typical
  15. // path "Foo" passed as a filename to any Win32 API:
  16. //
  17. // 1. "Foo" is recognized as a relative path and is appended to the current directory (say, "C:\" in our example)
  18. // 2. "C:\Foo" is prepended with the DosDevice namespace "\??\"
  19. // 3. CreateFile tries to create an object handle to the requested file "\??\C:\Foo"
  20. // 4. The Object Manager recognizes the DosDevices prefix and looks
  21. // a. First in the current session DosDevices ("\Sessions\1\DosDevices\" for example, mapped network drives go here)
  22. // b. If not found in the session, it looks in the Global DosDevices ("\GLOBAL??\")
  23. // 5. "C:" is found in DosDevices (in our case "\GLOBAL??\C:", which is a symbolic link to "\Device\HarddiskVolume6")
  24. // 6. The full path is now "\Device\HarddiskVolume6\Foo", "\Device\HarddiskVolume6" is a File object and parsing is handed off
  25. // to the registered parsing method for Files
  26. // 7. The registered open method for File objects is invoked to create the file handle which is then returned
  27. //
  28. // There are multiple ways to directly specify a DosDevices path. The final format of "\??\" is one way. It can also be specified
  29. // as "\\.\" (the most commonly documented way) and "\\?\". If the question mark syntax is used the path will skip normalization
  30. // (essentially GetFullPathName()) and path length checks.
  31. // Windows Kernel-Mode Object Manager
  32. // https://msdn.microsoft.com/en-us/library/windows/hardware/ff565763.aspx
  33. // https://channel9.msdn.com/Shows/Going+Deep/Windows-NT-Object-Manager
  34. //
  35. // Introduction to MS-DOS Device Names
  36. // https://msdn.microsoft.com/en-us/library/windows/hardware/ff548088.aspx
  37. //
  38. // Local and Global MS-DOS Device Names
  39. // https://msdn.microsoft.com/en-us/library/windows/hardware/ff554302.aspx
  40. internal const char DirectorySeparatorChar = '\\';
  41. internal const char AltDirectorySeparatorChar = '/';
  42. internal const char VolumeSeparatorChar = ':';
  43. internal const char PathSeparator = ';';
  44. internal const string DirectorySeparatorCharAsString = "\\";
  45. internal const string ExtendedPathPrefix = @"\\?\";
  46. internal const string UncPathPrefix = @"\\";
  47. internal const string UncExtendedPrefixToInsert = @"?\UNC\";
  48. internal const string UncExtendedPathPrefix = @"\\?\UNC\";
  49. internal const string DevicePathPrefix = @"\\.\";
  50. internal const string ParentDirectoryPrefix = @"..\";
  51. internal const int MaxShortPath = 260;
  52. internal const int MaxShortDirectoryPath = 248;
  53. // \\?\, \\.\, \??\
  54. internal const int DevicePrefixLength = 4;
  55. // \\
  56. internal const int UncPrefixLength = 2;
  57. // \\?\UNC\, \\.\UNC\
  58. internal const int UncExtendedPrefixLength = 8;
  59. /// <summary>
  60. /// Returns true if the given character is a valid drive letter
  61. /// </summary>
  62. internal static bool IsValidDriveChar(char value)
  63. {
  64. return ((value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z'));
  65. }
  66. internal static bool EndsWithPeriodOrSpace(string? path)
  67. {
  68. if (string.IsNullOrEmpty(path))
  69. return false;
  70. char c = path[path.Length - 1];
  71. return c == ' ' || c == '.';
  72. }
  73. /// <summary>
  74. /// Adds the extended path prefix (\\?\) if not already a device path, IF the path is not relative,
  75. /// AND the path is more than 259 characters. (> MAX_PATH + null). This will also insert the extended
  76. /// prefix if the path ends with a period or a space. Trailing periods and spaces are normally eaten
  77. /// away from paths during normalization, but if we see such a path at this point it should be
  78. /// normalized and has retained the final characters. (Typically from one of the *Info classes)
  79. /// </summary>
  80. [return: NotNullIfNotNull("path")]
  81. internal static string? EnsureExtendedPrefixIfNeeded(string? path)
  82. {
  83. if (path != null && (path.Length >= MaxShortPath || EndsWithPeriodOrSpace(path)))
  84. {
  85. return EnsureExtendedPrefix(path);
  86. }
  87. else
  88. {
  89. return path;
  90. }
  91. }
  92. /// <summary>
  93. /// Adds the extended path prefix (\\?\) if not relative or already a device path.
  94. /// </summary>
  95. internal static string EnsureExtendedPrefix(string path)
  96. {
  97. // Putting the extended prefix on the path changes the processing of the path. It won't get normalized, which
  98. // means adding to relative paths will prevent them from getting the appropriate current directory inserted.
  99. // If it already has some variant of a device path (\??\, \\?\, \\.\, //./, etc.) we don't need to change it
  100. // as it is either correct or we will be changing the behavior. When/if Windows supports long paths implicitly
  101. // in the future we wouldn't want normalization to come back and break existing code.
  102. // In any case, all internal usages should be hitting normalize path (Path.GetFullPath) before they hit this
  103. // shimming method. (Or making a change that doesn't impact normalization, such as adding a filename to a
  104. // normalized base path.)
  105. if (IsPartiallyQualified(path.AsSpan()) || IsDevice(path.AsSpan()))
  106. return path;
  107. // Given \\server\share in longpath becomes \\?\UNC\server\share
  108. if (path.StartsWith(UncPathPrefix, StringComparison.OrdinalIgnoreCase))
  109. return path.Insert(2, UncExtendedPrefixToInsert);
  110. return ExtendedPathPrefix + path;
  111. }
  112. /// <summary>
  113. /// Returns true if the path uses any of the DOS device path syntaxes. ("\\.\", "\\?\", or "\??\")
  114. /// </summary>
  115. internal static bool IsDevice(ReadOnlySpan<char> path)
  116. {
  117. // If the path begins with any two separators is will be recognized and normalized and prepped with
  118. // "\??\" for internal usage correctly. "\??\" is recognized and handled, "/??/" is not.
  119. return IsExtended(path)
  120. ||
  121. (
  122. path.Length >= DevicePrefixLength
  123. && IsDirectorySeparator(path[0])
  124. && IsDirectorySeparator(path[1])
  125. && (path[2] == '.' || path[2] == '?')
  126. && IsDirectorySeparator(path[3])
  127. );
  128. }
  129. /// <summary>
  130. /// Returns true if the path is a device UNC (\\?\UNC\, \\.\UNC\)
  131. /// </summary>
  132. internal static bool IsDeviceUNC(ReadOnlySpan<char> path)
  133. {
  134. return path.Length >= UncExtendedPrefixLength
  135. && IsDevice(path)
  136. && IsDirectorySeparator(path[7])
  137. && path[4] == 'U'
  138. && path[5] == 'N'
  139. && path[6] == 'C';
  140. }
  141. /// <summary>
  142. /// Returns true if the path uses the canonical form of extended syntax ("\\?\" or "\??\"). If the
  143. /// path matches exactly (cannot use alternate directory separators) Windows will skip normalization
  144. /// and path length checks.
  145. /// </summary>
  146. internal static bool IsExtended(ReadOnlySpan<char> path)
  147. {
  148. // While paths like "//?/C:/" will work, they're treated the same as "\\.\" paths.
  149. // Skipping of normalization will *only* occur if back slashes ('\') are used.
  150. return path.Length >= DevicePrefixLength
  151. && path[0] == '\\'
  152. && (path[1] == '\\' || path[1] == '?')
  153. && path[2] == '?'
  154. && path[3] == '\\';
  155. }
  156. /// <summary>
  157. /// Check for known wildcard characters. '*' and '?' are the most common ones.
  158. /// </summary>
  159. internal static bool HasWildCardCharacters(ReadOnlySpan<char> path)
  160. {
  161. // Question mark is part of dos device syntax so we have to skip if we are
  162. int startIndex = IsDevice(path) ? ExtendedPathPrefix.Length : 0;
  163. // [MS - FSA] 2.1.4.4 Algorithm for Determining if a FileName Is in an Expression
  164. // https://msdn.microsoft.com/en-us/library/ff469270.aspx
  165. for (int i = startIndex; i < path.Length; i++)
  166. {
  167. char c = path[i];
  168. if (c <= '?') // fast path for common case - '?' is highest wildcard character
  169. {
  170. if (c == '\"' || c == '<' || c == '>' || c == '*' || c == '?')
  171. return true;
  172. }
  173. }
  174. return false;
  175. }
  176. /// <summary>
  177. /// Gets the length of the root of the path (drive, share, etc.).
  178. /// </summary>
  179. internal static int GetRootLength(ReadOnlySpan<char> path)
  180. {
  181. int pathLength = path.Length;
  182. int i = 0;
  183. bool deviceSyntax = IsDevice(path);
  184. bool deviceUnc = deviceSyntax && IsDeviceUNC(path);
  185. if ((!deviceSyntax || deviceUnc) && pathLength > 0 && IsDirectorySeparator(path[0]))
  186. {
  187. // UNC or simple rooted path (e.g. "\foo", NOT "\\?\C:\foo")
  188. if (deviceUnc || (pathLength > 1 && IsDirectorySeparator(path[1])))
  189. {
  190. // UNC (\\?\UNC\ or \\), scan past server\share
  191. // Start past the prefix ("\\" or "\\?\UNC\")
  192. i = deviceUnc ? UncExtendedPrefixLength : UncPrefixLength;
  193. // Skip two separators at most
  194. int n = 2;
  195. while (i < pathLength && (!IsDirectorySeparator(path[i]) || --n > 0))
  196. i++;
  197. }
  198. else
  199. {
  200. // Current drive rooted (e.g. "\foo")
  201. i = 1;
  202. }
  203. }
  204. else if (deviceSyntax)
  205. {
  206. // Device path (e.g. "\\?\.", "\\.\")
  207. // Skip any characters following the prefix that aren't a separator
  208. i = DevicePrefixLength;
  209. while (i < pathLength && !IsDirectorySeparator(path[i]))
  210. i++;
  211. // If there is another separator take it, as long as we have had at least one
  212. // non-separator after the prefix (e.g. don't take "\\?\\", but take "\\?\a\")
  213. if (i < pathLength && i > DevicePrefixLength && IsDirectorySeparator(path[i]))
  214. i++;
  215. }
  216. else if (pathLength >= 2
  217. && path[1] == VolumeSeparatorChar
  218. && IsValidDriveChar(path[0]))
  219. {
  220. // Valid drive specified path ("C:", "D:", etc.)
  221. i = 2;
  222. // If the colon is followed by a directory separator, move past it (e.g "C:\")
  223. if (pathLength > 2 && IsDirectorySeparator(path[2]))
  224. i++;
  225. }
  226. return i;
  227. }
  228. /// <summary>
  229. /// Returns true if the path specified is relative to the current drive or working directory.
  230. /// Returns false if the path is fixed to a specific drive or UNC path. This method does no
  231. /// validation of the path (URIs will be returned as relative as a result).
  232. /// </summary>
  233. /// <remarks>
  234. /// Handles paths that use the alternate directory separator. It is a frequent mistake to
  235. /// assume that rooted paths (Path.IsPathRooted) are not relative. This isn't the case.
  236. /// "C:a" is drive relative- meaning that it will be resolved against the current directory
  237. /// for C: (rooted, but relative). "C:\a" is rooted and not relative (the current directory
  238. /// will not be used to modify the path).
  239. /// </remarks>
  240. internal static bool IsPartiallyQualified(ReadOnlySpan<char> path)
  241. {
  242. if (path.Length < 2)
  243. {
  244. // It isn't fixed, it must be relative. There is no way to specify a fixed
  245. // path with one character (or less).
  246. return true;
  247. }
  248. if (IsDirectorySeparator(path[0]))
  249. {
  250. // There is no valid way to specify a relative path with two initial slashes or
  251. // \? as ? isn't valid for drive relative paths and \??\ is equivalent to \\?\
  252. return !(path[1] == '?' || IsDirectorySeparator(path[1]));
  253. }
  254. // The only way to specify a fixed path that doesn't begin with two slashes
  255. // is the drive, colon, slash format- i.e. C:\
  256. return !((path.Length >= 3)
  257. && (path[1] == VolumeSeparatorChar)
  258. && IsDirectorySeparator(path[2])
  259. // To match old behavior we'll check the drive character for validity as the path is technically
  260. // not qualified if you don't have a valid drive. "=:\" is the "=" file's default data stream.
  261. && IsValidDriveChar(path[0]));
  262. }
  263. /// <summary>
  264. /// True if the given character is a directory separator.
  265. /// </summary>
  266. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  267. internal static bool IsDirectorySeparator(char c)
  268. {
  269. return c == DirectorySeparatorChar || c == AltDirectorySeparatorChar;
  270. }
  271. /// <summary>
  272. /// Normalize separators in the given path. Converts forward slashes into back slashes and compresses slash runs, keeping initial 2 if present.
  273. /// Also trims initial whitespace in front of "rooted" paths (see PathStartSkip).
  274. ///
  275. /// This effectively replicates the behavior of the legacy NormalizePath when it was called with fullCheck=false and expandShortpaths=false.
  276. /// The current NormalizePath gets directory separator normalization from Win32's GetFullPathName(), which will resolve relative paths and as
  277. /// such can't be used here (and is overkill for our uses).
  278. ///
  279. /// Like the current NormalizePath this will not try and analyze periods/spaces within directory segments.
  280. /// </summary>
  281. /// <remarks>
  282. /// The only callers that used to use Path.Normalize(fullCheck=false) were Path.GetDirectoryName() and Path.GetPathRoot(). Both usages do
  283. /// not need trimming of trailing whitespace here.
  284. ///
  285. /// GetPathRoot() could technically skip normalizing separators after the second segment- consider as a future optimization.
  286. ///
  287. /// For legacy desktop behavior with ExpandShortPaths:
  288. /// - It has no impact on GetPathRoot() so doesn't need consideration.
  289. /// - It could impact GetDirectoryName(), but only if the path isn't relative (C:\ or \\Server\Share).
  290. ///
  291. /// In the case of GetDirectoryName() the ExpandShortPaths behavior was undocumented and provided inconsistent results if the path was
  292. /// fixed/relative. For example: "C:\PROGRA~1\A.TXT" would return "C:\Program Files" while ".\PROGRA~1\A.TXT" would return ".\PROGRA~1". If you
  293. /// ultimately call GetFullPath() this doesn't matter, but if you don't or have any intermediate string handling could easily be tripped up by
  294. /// this undocumented behavior.
  295. ///
  296. /// We won't match this old behavior because:
  297. ///
  298. /// 1. It was undocumented
  299. /// 2. It was costly (extremely so if it actually contained '~')
  300. /// 3. Doesn't play nice with string logic
  301. /// 4. Isn't a cross-plat friendly concept/behavior
  302. /// </remarks>
  303. internal static string NormalizeDirectorySeparators(string path)
  304. {
  305. if (string.IsNullOrEmpty(path))
  306. return path;
  307. char current;
  308. // Make a pass to see if we need to normalize so we can potentially skip allocating
  309. bool normalized = true;
  310. for (int i = 0; i < path.Length; i++)
  311. {
  312. current = path[i];
  313. if (IsDirectorySeparator(current)
  314. && (current != DirectorySeparatorChar
  315. // Check for sequential separators past the first position (we need to keep initial two for UNC/extended)
  316. || (i > 0 && i + 1 < path.Length && IsDirectorySeparator(path[i + 1]))))
  317. {
  318. normalized = false;
  319. break;
  320. }
  321. }
  322. if (normalized)
  323. return path;
  324. Span<char> initialBuffer = stackalloc char[MaxShortPath];
  325. ValueStringBuilder builder = new ValueStringBuilder(initialBuffer);
  326. int start = 0;
  327. if (IsDirectorySeparator(path[start]))
  328. {
  329. start++;
  330. builder.Append(DirectorySeparatorChar);
  331. }
  332. for (int i = start; i < path.Length; i++)
  333. {
  334. current = path[i];
  335. // If we have a separator
  336. if (IsDirectorySeparator(current))
  337. {
  338. // If the next is a separator, skip adding this
  339. if (i + 1 < path.Length && IsDirectorySeparator(path[i + 1]))
  340. {
  341. continue;
  342. }
  343. // Ensure it is the primary separator
  344. current = DirectorySeparatorChar;
  345. }
  346. builder.Append(current);
  347. }
  348. return builder.ToString();
  349. }
  350. /// <summary>
  351. /// Returns true if the path is effectively empty for the current OS.
  352. /// For unix, this is empty or null. For Windows, this is empty, null, or
  353. /// just spaces ((char)32).
  354. /// </summary>
  355. internal static bool IsEffectivelyEmpty(ReadOnlySpan<char> path)
  356. {
  357. if (path.IsEmpty)
  358. return true;
  359. foreach (char c in path)
  360. {
  361. if (c != ' ')
  362. return false;
  363. }
  364. return true;
  365. }
  366. }
  367. }