PathInternal.Windows.cs 19 KB

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