Path.Windows.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  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.Text;
  7. #if MS_IO_REDIST
  8. using System;
  9. using System.IO;
  10. namespace Microsoft.IO
  11. #else
  12. namespace System.IO
  13. #endif
  14. {
  15. public static partial class Path
  16. {
  17. public static char[] GetInvalidFileNameChars() => new char[]
  18. {
  19. '\"', '<', '>', '|', '\0',
  20. (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
  21. (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
  22. (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
  23. (char)31, ':', '*', '?', '\\', '/'
  24. };
  25. public static char[] GetInvalidPathChars() => new char[]
  26. {
  27. '|', '\0',
  28. (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
  29. (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
  30. (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
  31. (char)31
  32. };
  33. // Expands the given path to a fully qualified path.
  34. public static string GetFullPath(string path)
  35. {
  36. if (path == null)
  37. throw new ArgumentNullException(nameof(path));
  38. // If the path would normalize to string empty, we'll consider it empty
  39. if (PathInternal.IsEffectivelyEmpty(path.AsSpan()))
  40. throw new ArgumentException(SR.Arg_PathEmpty, nameof(path));
  41. // Embedded null characters are the only invalid character case we trully care about.
  42. // This is because the nulls will signal the end of the string to Win32 and therefore have
  43. // unpredictable results.
  44. if (path.Contains('\0'))
  45. throw new ArgumentException(SR.Argument_InvalidPathChars, nameof(path));
  46. if (PathInternal.IsExtended(path.AsSpan()))
  47. {
  48. // \\?\ paths are considered normalized by definition. Windows doesn't normalize \\?\
  49. // paths and neither should we. Even if we wanted to GetFullPathName does not work
  50. // properly with device paths. If one wants to pass a \\?\ path through normalization
  51. // one can chop off the prefix, pass it to GetFullPath and add it again.
  52. return path;
  53. }
  54. return PathHelper.Normalize(path);
  55. }
  56. public static string GetFullPath(string path, string basePath)
  57. {
  58. if (path == null)
  59. throw new ArgumentNullException(nameof(path));
  60. if (basePath == null)
  61. throw new ArgumentNullException(nameof(basePath));
  62. if (!IsPathFullyQualified(basePath))
  63. throw new ArgumentException(SR.Arg_BasePathNotFullyQualified, nameof(basePath));
  64. if (basePath.Contains('\0') || path.Contains('\0'))
  65. throw new ArgumentException(SR.Argument_InvalidPathChars);
  66. if (IsPathFullyQualified(path))
  67. return GetFullPath(path);
  68. if (PathInternal.IsEffectivelyEmpty(path.AsSpan()))
  69. return basePath;
  70. int length = path.Length;
  71. string? combinedPath = null;
  72. if ((length >= 1 && PathInternal.IsDirectorySeparator(path[0])))
  73. {
  74. // Path is current drive rooted i.e. starts with \:
  75. // "\Foo" and "C:\Bar" => "C:\Foo"
  76. // "\Foo" and "\\?\C:\Bar" => "\\?\C:\Foo"
  77. combinedPath = Join(GetPathRoot(basePath.AsSpan()), path.AsSpan(1)); // Cut the separator to ensure we don't end up with two separators when joining with the root.
  78. }
  79. else if (length >= 2 && PathInternal.IsValidDriveChar(path[0]) && path[1] == PathInternal.VolumeSeparatorChar)
  80. {
  81. // Drive relative paths
  82. Debug.Assert(length == 2 || !PathInternal.IsDirectorySeparator(path[2]));
  83. if (GetVolumeName(path.AsSpan()).EqualsOrdinal(GetVolumeName(basePath.AsSpan())))
  84. {
  85. // Matching root
  86. // "C:Foo" and "C:\Bar" => "C:\Bar\Foo"
  87. // "C:Foo" and "\\?\C:\Bar" => "\\?\C:\Bar\Foo"
  88. combinedPath = Join(basePath.AsSpan(), path.AsSpan(2));
  89. }
  90. else
  91. {
  92. // No matching root, root to specified drive
  93. // "D:Foo" and "C:\Bar" => "D:Foo"
  94. // "D:Foo" and "\\?\C:\Bar" => "\\?\D:\Foo"
  95. combinedPath = !PathInternal.IsDevice(basePath.AsSpan())
  96. ? path.Insert(2, @"\")
  97. : length == 2
  98. ? JoinInternal(basePath.AsSpan(0, 4), path.AsSpan(), @"\".AsSpan())
  99. : JoinInternal(basePath.AsSpan(0, 4), path.AsSpan(0, 2), @"\".AsSpan(), path.AsSpan(2));
  100. }
  101. }
  102. else
  103. {
  104. // "Simple" relative path
  105. // "Foo" and "C:\Bar" => "C:\Bar\Foo"
  106. // "Foo" and "\\?\C:\Bar" => "\\?\C:\Bar\Foo"
  107. combinedPath = JoinInternal(basePath.AsSpan(), path.AsSpan());
  108. }
  109. // Device paths are normalized by definition, so passing something of this format (i.e. \\?\C:\.\tmp, \\.\C:\foo)
  110. // to Windows APIs won't do anything by design. Additionally, GetFullPathName() in Windows doesn't root
  111. // them properly. As such we need to manually remove segments and not use GetFullPath().
  112. return PathInternal.IsDevice(combinedPath.AsSpan())
  113. ? PathInternal.RemoveRelativeSegments(combinedPath, PathInternal.GetRootLength(combinedPath.AsSpan()))
  114. : GetFullPath(combinedPath);
  115. }
  116. public static string GetTempPath()
  117. {
  118. Span<char> initialBuffer = stackalloc char[PathInternal.MaxShortPath];
  119. var builder = new ValueStringBuilder(initialBuffer);
  120. GetTempPath(ref builder);
  121. string path = PathHelper.Normalize(ref builder);
  122. builder.Dispose();
  123. return path;
  124. }
  125. private static void GetTempPath(ref ValueStringBuilder builder)
  126. {
  127. uint result = 0;
  128. while ((result = Interop.Kernel32.GetTempPathW(builder.Capacity, ref builder.GetPinnableReference())) > builder.Capacity)
  129. {
  130. // Reported size is greater than the buffer size. Increase the capacity.
  131. builder.EnsureCapacity(checked((int)result));
  132. }
  133. if (result == 0)
  134. throw Win32Marshal.GetExceptionForLastWin32Error();
  135. builder.Length = (int)result;
  136. }
  137. // Returns a unique temporary file name, and creates a 0-byte file by that
  138. // name on disk.
  139. public static string GetTempFileName()
  140. {
  141. Span<char> initialTempPathBuffer = stackalloc char[PathInternal.MaxShortPath];
  142. ValueStringBuilder tempPathBuilder = new ValueStringBuilder(initialTempPathBuffer);
  143. GetTempPath(ref tempPathBuilder);
  144. Span<char> initialBuffer = stackalloc char[PathInternal.MaxShortPath];
  145. var builder = new ValueStringBuilder(initialBuffer);
  146. uint result = Interop.Kernel32.GetTempFileNameW(
  147. ref tempPathBuilder.GetPinnableReference(), "tmp", 0, ref builder.GetPinnableReference());
  148. tempPathBuilder.Dispose();
  149. if (result == 0)
  150. throw Win32Marshal.GetExceptionForLastWin32Error();
  151. builder.Length = builder.RawChars.IndexOf('\0');
  152. string path = PathHelper.Normalize(ref builder);
  153. builder.Dispose();
  154. return path;
  155. }
  156. // Tests if the given path contains a root. A path is considered rooted
  157. // if it starts with a backslash ("\") or a valid drive letter and a colon (":").
  158. public static bool IsPathRooted(string? path)
  159. {
  160. return path != null && IsPathRooted(path.AsSpan());
  161. }
  162. public static bool IsPathRooted(ReadOnlySpan<char> path)
  163. {
  164. int length = path.Length;
  165. return (length >= 1 && PathInternal.IsDirectorySeparator(path[0]))
  166. || (length >= 2 && PathInternal.IsValidDriveChar(path[0]) && path[1] == PathInternal.VolumeSeparatorChar);
  167. }
  168. // Returns the root portion of the given path. The resulting string
  169. // consists of those rightmost characters of the path that constitute the
  170. // root of the path. Possible patterns for the resulting string are: An
  171. // empty string (a relative path on the current drive), "\" (an absolute
  172. // path on the current drive), "X:" (a relative path on a given drive,
  173. // where X is the drive letter), "X:\" (an absolute path on a given drive),
  174. // and "\\server\share" (a UNC path for a given server and share name).
  175. // The resulting string is null if path is null. If the path is empty or
  176. // only contains whitespace characters an ArgumentException gets thrown.
  177. public static string? GetPathRoot(string? path)
  178. {
  179. if (PathInternal.IsEffectivelyEmpty(path.AsSpan()))
  180. return null;
  181. ReadOnlySpan<char> result = GetPathRoot(path.AsSpan());
  182. if (path!.Length == result.Length)
  183. return PathInternal.NormalizeDirectorySeparators(path);
  184. return PathInternal.NormalizeDirectorySeparators(result.ToString());
  185. }
  186. /// <remarks>
  187. /// Unlike the string overload, this method will not normalize directory separators.
  188. /// </remarks>
  189. public static ReadOnlySpan<char> GetPathRoot(ReadOnlySpan<char> path)
  190. {
  191. if (PathInternal.IsEffectivelyEmpty(path))
  192. return ReadOnlySpan<char>.Empty;
  193. int pathRoot = PathInternal.GetRootLength(path);
  194. return pathRoot <= 0 ? ReadOnlySpan<char>.Empty : path.Slice(0, pathRoot);
  195. }
  196. /// <summary>Gets whether the system is case-sensitive.</summary>
  197. internal static bool IsCaseSensitive { get { return false; } }
  198. /// <summary>
  199. /// Returns the volume name for dos, UNC and device paths.
  200. /// </summary>
  201. internal static ReadOnlySpan<char> GetVolumeName(ReadOnlySpan<char> path)
  202. {
  203. // 3 cases: UNC ("\\server\share"), Device ("\\?\C:\"), or Dos ("C:\")
  204. ReadOnlySpan<char> root = GetPathRoot(path);
  205. if (root.Length == 0)
  206. return root;
  207. // Cut from "\\?\UNC\Server\Share" to "Server\Share"
  208. // Cut from "\\Server\Share" to "Server\Share"
  209. int startOffset = GetUncRootLength(path);
  210. if (startOffset == -1)
  211. {
  212. if (PathInternal.IsDevice(path))
  213. {
  214. startOffset = 4; // Cut from "\\?\C:\" to "C:"
  215. }
  216. else
  217. {
  218. startOffset = 0; // e.g. "C:"
  219. }
  220. }
  221. ReadOnlySpan<char> pathToTrim = root.Slice(startOffset);
  222. return Path.EndsInDirectorySeparator(pathToTrim) ? pathToTrim.Slice(0, pathToTrim.Length - 1) : pathToTrim;
  223. }
  224. /// <summary>
  225. /// Returns offset as -1 if the path is not in Unc format, otherwise returns the root length.
  226. /// </summary>
  227. /// <param name="path"></param>
  228. /// <returns></returns>
  229. internal static int GetUncRootLength(ReadOnlySpan<char> path)
  230. {
  231. bool isDevice = PathInternal.IsDevice(path);
  232. if (!isDevice && path.Slice(0, 2).EqualsOrdinal(@"\\".AsSpan()) )
  233. return 2;
  234. else if (isDevice && path.Length >= 8
  235. && (path.Slice(0, 8).EqualsOrdinal(PathInternal.UncExtendedPathPrefix.AsSpan())
  236. || path.Slice(5, 4).EqualsOrdinal(@"UNC\".AsSpan())))
  237. return 8;
  238. return -1;
  239. }
  240. }
  241. }