1 // Copyright (c) Microsoft. All rights reserved.
2 // Licensed under the MIT license. See LICENSE file in the project root for full license information.
6 using System.Diagnostics.Contracts;
7 using System.Runtime.CompilerServices;
8 using System.Runtime.InteropServices;
14 /// <summary>Contains internal path helpers that are shared between many projects.</summary>
15 internal static class PathInternal
17 internal const string ExtendedPathPrefix = @"\\?\";
18 internal const string UncPathPrefix = @"\\";
19 internal const string UncExtendedPrefixToInsert = @"?\UNC\";
20 internal const string UncExtendedPathPrefix = @"\\?\UNC\";
21 internal const string DevicePathPrefix = @"\\.\";
22 internal const int DevicePrefixLength = 4;
23 internal const int MaxShortPath = 260;
24 internal const int MaxShortDirectoryPath = 248;
26 // Windows is limited in long paths by the max size of its internal representation of a unicode string.
27 // UNICODE_STRING has a max length of USHORT in _bytes_ without a trailing null.
28 // https://msdn.microsoft.com/en-us/library/windows/hardware/ff564879.aspx
29 internal const int MaxLongPath = short.MaxValue;
30 internal static readonly int MaxComponentLength = 255;
32 internal static readonly char[] InvalidPathChars =
34 '\"', '<', '>', '|', '\0',
35 (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
36 (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
37 (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
42 /// Validates volume separator only occurs as C: or \\?\C:. This logic is meant to filter out Alternate Data Streams.
44 /// <returns>True if the path has an invalid volume separator.</returns>
45 internal static bool HasInvalidVolumeSeparator(string path)
47 // Toss out paths with colons that aren't a valid drive specifier.
48 // Cannot start with a colon and can only be of the form "C:" or "\\?\C:".
49 // (Note that we used to explicitly check "http:" and "file:"- these are caught by this check now.)
51 // We don't care about skipping starting space for extended paths. Assume no knowledge of extended paths if we're forcing old path behavior.
52 bool isExtended = !AppContextSwitches.UseLegacyPathHandling && IsExtended(path);
53 int startIndex = isExtended ? ExtendedPathPrefix.Length : PathStartSkip(path);
55 // If we start with a colon
56 if ((path.Length > startIndex && path[startIndex] == Path.VolumeSeparatorChar)
57 // Or have an invalid drive letter and colon
58 || (path.Length >= startIndex + 2 && path[startIndex + 1] == Path.VolumeSeparatorChar && !IsValidDriveChar(path[startIndex]))
59 // Or have any colons beyond the drive colon
60 || (path.Length > startIndex + 2 && path.IndexOf(Path.VolumeSeparatorChar, startIndex + 2) != -1))
69 /// Returns true if the given StringBuilder starts with the given value.
71 /// <param name="value">The string to compare against the start of the StringBuilder.</param>
72 internal static bool StartsWithOrdinal(StringBuilder builder, string value, bool ignoreCase = false)
74 if (value == null || builder.Length < value.Length)
79 for (int i = 0; i < value.Length; i++)
80 if (char.ToUpperInvariant(builder[i]) != char.ToUpperInvariant(value[i])) return false;
84 for (int i = 0; i < value.Length; i++)
85 if (builder[i] != value[i]) return false;
92 /// Returns true if the given character is a valid drive letter
94 internal static bool IsValidDriveChar(char value)
96 return ((value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z'));
100 /// Returns true if the path is too long
102 internal static bool IsPathTooLong(string fullPath)
104 // We'll never know precisely what will fail as paths get changed internally in Windows and
105 // may grow beyond / shrink below exceed MaxLongPath.
106 if (AppContextSwitches.BlockLongPaths)
108 // We allow paths of any length if extended (and not in compat mode)
109 if (AppContextSwitches.UseLegacyPathHandling || !IsExtended(fullPath))
110 return fullPath.Length >= MaxShortPath;
113 return fullPath.Length >= MaxLongPath;
117 /// Return true if any path segments are too long
119 internal static bool AreSegmentsTooLong(string fullPath)
121 int length = fullPath.Length;
122 int lastSeparator = 0;
124 for (int i = 0; i < length; i++)
126 if (IsDirectorySeparator(fullPath[i]))
128 if (i - lastSeparator > MaxComponentLength)
134 if (length - 1 - lastSeparator > MaxComponentLength)
141 /// Returns true if the directory is too long
143 internal static bool IsDirectoryTooLong(string fullPath)
145 if (AppContextSwitches.BlockLongPaths)
147 // We allow paths of any length if extended (and not in compat mode)
148 if (AppContextSwitches.UseLegacyPathHandling || !IsExtended(fullPath))
149 return (fullPath.Length >= MaxShortDirectoryPath);
152 return IsPathTooLong(fullPath);
156 /// Adds the extended path prefix (\\?\) if not relative or already a device path.
158 internal static string EnsureExtendedPrefix(string path)
160 // Putting the extended prefix on the path changes the processing of the path. It won't get normalized, which
161 // means adding to relative paths will prevent them from getting the appropriate current directory inserted.
163 // If it already has some variant of a device path (\??\, \\?\, \\.\, //./, etc.) we don't need to change it
164 // as it is either correct or we will be changing the behavior. When/if Windows supports long paths implicitly
165 // in the future we wouldn't want normalization to come back and break existing code.
167 // In any case, all internal usages should be hitting normalize path (Path.GetFullPath) before they hit this
168 // shimming method. (Or making a change that doesn't impact normalization, such as adding a filename to a
169 // normalized base path.)
170 if (IsPartiallyQualified(path) || IsDevice(path))
173 // Given \\server\share in longpath becomes \\?\UNC\server\share
174 if (path.StartsWith(UncPathPrefix, StringComparison.OrdinalIgnoreCase))
175 return path.Insert(2, UncExtendedPrefixToInsert);
177 return ExtendedPathPrefix + path;
181 /// Removes the extended path prefix (\\?\) if present.
183 internal static string RemoveExtendedPrefix(string path)
185 if (!IsExtended(path))
188 // Given \\?\UNC\server\share we return \\server\share
189 if (IsExtendedUnc(path))
190 return path.Remove(2, 6);
192 return path.Substring(DevicePrefixLength);
196 /// Removes the extended path prefix (\\?\) if present.
198 internal static StringBuilder RemoveExtendedPrefix(StringBuilder path)
200 if (!IsExtended(path))
203 // Given \\?\UNC\server\share we return \\server\share
204 if (IsExtendedUnc(path))
205 return path.Remove(2, 6);
207 return path.Remove(0, DevicePrefixLength);
211 /// Returns true if the path uses any of the DOS device path syntaxes. ("\\.\", "\\?\", or "\??\")
213 internal static bool IsDevice(string path)
215 // If the path begins with any two separators it will be recognized and normalized and prepped with
216 // "\??\" for internal usage correctly. "\??\" is recognized and handled, "/??/" is not.
217 return IsExtended(path)
220 path.Length >= DevicePrefixLength
221 && IsDirectorySeparator(path[0])
222 && IsDirectorySeparator(path[1])
223 && (path[2] == '.' || path[2] == '?')
224 && IsDirectorySeparator(path[3])
229 /// Returns true if the path uses any of the DOS device path syntaxes. ("\\.\", "\\?\", or "\??\")
231 internal static bool IsDevice(StringBuffer path)
233 // If the path begins with any two separators it will be recognized and normalized and prepped with
234 // "\??\" for internal usage correctly. "\??\" is recognized and handled, "/??/" is not.
235 return IsExtended(path)
238 path.Length >= DevicePrefixLength
239 && IsDirectorySeparator(path[0])
240 && IsDirectorySeparator(path[1])
241 && (path[2] == '.' || path[2] == '?')
242 && IsDirectorySeparator(path[3])
247 /// Returns true if the path uses the canonical form of extended syntax ("\\?\" or "\??\"). If the
248 /// path matches exactly (cannot use alternate directory separators) Windows will skip normalization
249 /// and path length checks.
251 internal static bool IsExtended(string path)
253 // While paths like "//?/C:/" will work, they're treated the same as "\\.\" paths.
254 // Skipping of normalization will *only* occur if back slashes ('\') are used.
255 return path.Length >= DevicePrefixLength
257 && (path[1] == '\\' || path[1] == '?')
263 /// Returns true if the path uses the canonical form of extended syntax ("\\?\" or "\??\"). If the
264 /// path matches exactly (cannot use alternate directory separators) Windows will skip normalization
265 /// and path length checks.
267 internal static bool IsExtended(StringBuilder path)
269 // While paths like "//?/C:/" will work, they're treated the same as "\\.\" paths.
270 // Skipping of normalization will *only* occur if back slashes ('\') are used.
271 return path.Length >= DevicePrefixLength
273 && (path[1] == '\\' || path[1] == '?')
279 /// Returns true if the path uses the canonical form of extended syntax ("\\?\" or "\??\"). If the
280 /// path matches exactly (cannot use alternate directory separators) Windows will skip normalization
281 /// and path length checks.
283 internal static bool IsExtended(StringBuffer path)
285 // While paths like "//?/C:/" will work, they're treated the same as "\\.\" paths.
286 // Skipping of normalization will *only* occur if back slashes ('\') are used.
287 return path.Length >= DevicePrefixLength
289 && (path[1] == '\\' || path[1] == '?')
295 /// Returns true if the path uses the extended UNC syntax (\\?\UNC\ or \??\UNC\)
297 internal static bool IsExtendedUnc(string path)
299 return path.Length >= UncExtendedPathPrefix.Length
301 && char.ToUpper(path[4]) == 'U'
302 && char.ToUpper(path[5]) == 'N'
303 && char.ToUpper(path[6]) == 'C'
308 /// Returns true if the path uses the extended UNC syntax (\\?\UNC\ or \??\UNC\)
310 internal static bool IsExtendedUnc(StringBuilder path)
312 return path.Length >= UncExtendedPathPrefix.Length
314 && char.ToUpper(path[4]) == 'U'
315 && char.ToUpper(path[5]) == 'N'
316 && char.ToUpper(path[6]) == 'C'
321 /// Returns a value indicating if the given path contains invalid characters (", <, >, |
322 /// NUL, or any ASCII char whose integer representation is in the range of 1 through 31).
323 /// Does not check for wild card characters ? and *.
325 /// Will not check if the path is a device path and not in Legacy mode as many of these
326 /// characters are valid for devices (pipes for example).
328 internal static bool HasIllegalCharacters(string path, bool checkAdditional = false)
330 if (!AppContextSwitches.UseLegacyPathHandling && IsDevice(path))
335 return AnyPathHasIllegalCharacters(path, checkAdditional: checkAdditional);
339 /// Version of HasIllegalCharacters that checks no AppContextSwitches. Only use if you know you need to skip switches and don't care
340 /// about proper device path handling.
342 internal static bool AnyPathHasIllegalCharacters(string path, bool checkAdditional = false)
344 return path.IndexOfAny(InvalidPathChars) >= 0 || (checkAdditional && AnyPathHasWildCardCharacters(path));
348 /// Check for ? and *.
350 internal static bool HasWildCardCharacters(string path)
352 // Question mark is part of some device paths
353 int startIndex = AppContextSwitches.UseLegacyPathHandling ? 0 : IsDevice(path) ? ExtendedPathPrefix.Length : 0;
354 return AnyPathHasWildCardCharacters(path, startIndex: startIndex);
358 /// Version of HasWildCardCharacters that checks no AppContextSwitches. Only use if you know you need to skip switches and don't care
359 /// about proper device path handling.
361 internal static bool AnyPathHasWildCardCharacters(string path, int startIndex = 0)
364 for (int i = startIndex; i < path.Length; i++)
366 currentChar = path[i];
367 if (currentChar == '*' || currentChar == '?') return true;
373 /// Gets the length of the root of the path (drive, share, etc.).
375 [System.Security.SecuritySafeCritical]
376 internal unsafe static int GetRootLength(string path)
378 fixed (char* value = path)
380 return (int)GetRootLength(value, (ulong)path.Length);
385 /// Gets the length of the root of the path (drive, share, etc.).
387 [System.Security.SecuritySafeCritical]
388 internal unsafe static uint GetRootLength(StringBuffer path)
390 if (path.Length == 0) return 0;
391 return GetRootLength(path.CharPointer, path.Length);
394 [System.Security.SecurityCritical]
395 private unsafe static uint GetRootLength(char* path, ulong pathLength)
398 uint volumeSeparatorLength = 2; // Length to the colon "C:"
399 uint uncRootLength = 2; // Length to the start of the server name "\\"
401 bool extendedSyntax = StartsWithOrdinal(path, pathLength, ExtendedPathPrefix);
402 bool extendedUncSyntax = StartsWithOrdinal(path, pathLength, UncExtendedPathPrefix);
405 // Shift the position we look for the root from to account for the extended prefix
406 if (extendedUncSyntax)
408 // "\\" -> "\\?\UNC\"
409 uncRootLength = (uint)UncExtendedPathPrefix.Length;
414 volumeSeparatorLength += (uint)ExtendedPathPrefix.Length;
418 if ((!extendedSyntax || extendedUncSyntax) && pathLength > 0 && IsDirectorySeparator(path[0]))
420 // UNC or simple rooted path (e.g. "\foo", NOT "\\?\C:\foo")
422 i = 1; // Drive rooted (\foo) is one character
423 if (extendedUncSyntax || (pathLength > 1 && IsDirectorySeparator(path[1])))
425 // UNC (\\?\UNC\ or \\), scan past the next two directory separators at most
426 // (e.g. to \\?\UNC\Server\Share or \\Server\Share\)
428 int n = 2; // Maximum separators to skip
429 while (i < pathLength && (!IsDirectorySeparator(path[i]) || --n > 0)) i++;
432 else if (pathLength >= volumeSeparatorLength && path[volumeSeparatorLength - 1] == Path.VolumeSeparatorChar)
434 // Path is at least longer than where we expect a colon, and has a colon (\\?\A:, A:)
435 // If the colon is followed by a directory separator, move past it
436 i = volumeSeparatorLength;
437 if (pathLength >= volumeSeparatorLength + 1 && IsDirectorySeparator(path[volumeSeparatorLength])) i++;
442 [System.Security.SecurityCritical]
443 private unsafe static bool StartsWithOrdinal(char* source, ulong sourceLength, string value)
445 if (sourceLength < (ulong)value.Length) return false;
446 for (int i = 0; i < value.Length; i++)
448 if (value[i] != source[i]) return false;
454 /// Returns true if the path specified is relative to the current drive or working directory.
455 /// Returns false if the path is fixed to a specific drive or UNC path. This method does no
456 /// validation of the path (URIs will be returned as relative as a result).
459 /// Handles paths that use the alternate directory separator. It is a frequent mistake to
460 /// assume that rooted paths (Path.IsPathRooted) are not relative. This isn't the case.
461 /// "C:a" is drive relative- meaning that it will be resolved against the current directory
462 /// for C: (rooted, but relative). "C:\a" is rooted and not relative (the current directory
463 /// will not be used to modify the path).
465 internal static bool IsPartiallyQualified(string path)
469 // It isn't fixed, it must be relative. There is no way to specify a fixed
470 // path with one character (or less).
474 if (IsDirectorySeparator(path[0]))
476 // There is no valid way to specify a relative path with two initial slashes or
477 // \? as ? isn't valid for drive relative paths and \??\ is equivalent to \\?\
478 return !(path[1] == '?' || IsDirectorySeparator(path[1]));
481 // The only way to specify a fixed path that doesn't begin with two slashes
482 // is the drive, colon, slash format- i.e. C:\
483 return !((path.Length >= 3)
484 && (path[1] == Path.VolumeSeparatorChar)
485 && IsDirectorySeparator(path[2])
486 // To match old behavior we'll check the drive character for validity as the path is technically
487 // not qualified if you don't have a valid drive. "=:\" is the "=" file's default data stream.
488 && IsValidDriveChar(path[0]));
492 /// Returns true if the path specified is relative to the current drive or working directory.
493 /// Returns false if the path is fixed to a specific drive or UNC path. This method does no
494 /// validation of the path (URIs will be returned as relative as a result).
497 /// Handles paths that use the alternate directory separator. It is a frequent mistake to
498 /// assume that rooted paths (Path.IsPathRooted) are not relative. This isn't the case.
499 /// "C:a" is drive relative- meaning that it will be resolved against the current directory
500 /// for C: (rooted, but relative). "C:\a" is rooted and not relative (the current directory
501 /// will not be used to modify the path).
503 internal static bool IsPartiallyQualified(StringBuffer path)
507 // It isn't fixed, it must be relative. There is no way to specify a fixed
508 // path with one character (or less).
512 if (IsDirectorySeparator(path[0]))
514 // There is no valid way to specify a relative path with two initial slashes or
515 // \? as ? isn't valid for drive relative paths and \??\ is equivalent to \\?\
516 return !(path[1] == '?' || IsDirectorySeparator(path[1]));
519 // The only way to specify a fixed path that doesn't begin with two slashes
520 // is the drive, colon, slash format- i.e. C:\
521 return !((path.Length >= 3)
522 && (path[1] == Path.VolumeSeparatorChar)
523 && IsDirectorySeparator(path[2])
524 // To match old behavior we'll check the drive character for validity as the path is technically
525 // not qualified if you don't have a valid drive. "=:\" is the "=" file's default data stream.
526 && IsValidDriveChar(path[0]));
530 /// Returns the characters to skip at the start of the path if it starts with space(s) and a drive or directory separator.
531 /// (examples are " C:", " \")
532 /// This is a legacy behavior of Path.GetFullPath().
535 /// Note that this conflicts with IsPathRooted() which doesn't (and never did) such a skip.
537 internal static int PathStartSkip(string path)
540 while (startIndex < path.Length && path[startIndex] == ' ') startIndex++;
542 if (startIndex > 0 && (startIndex < path.Length && IsDirectorySeparator(path[startIndex]))
543 || (startIndex + 1 < path.Length && path[startIndex + 1] == Path.VolumeSeparatorChar && IsValidDriveChar(path[startIndex])))
545 // Go ahead and skip spaces as we're either " C:" or " \"
553 /// True if the given character is a directory separator.
555 [MethodImpl(MethodImplOptions.AggressiveInlining)]
556 internal static bool IsDirectorySeparator(char c)
558 return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar;
562 /// Normalize separators in the given path. Converts forward slashes into back slashes and compresses slash runs, keeping initial 2 if present.
563 /// Also trims initial whitespace in front of "rooted" paths (see PathStartSkip).
565 /// This effectively replicates the behavior of the legacy NormalizePath when it was called with fullCheck=false and expandShortpaths=false.
566 /// The current NormalizePath gets directory separator normalization from Win32's GetFullPathName(), which will resolve relative paths and as
567 /// such can't be used here (and is overkill for our uses).
569 /// Like the current NormalizePath this will not try and analyze periods/spaces within directory segments.
572 /// The only callers that used to use Path.Normalize(fullCheck=false) were Path.GetDirectoryName() and Path.GetPathRoot(). Both usages do
573 /// not need trimming of trailing whitespace here.
575 /// GetPathRoot() could technically skip normalizing separators after the second segment- consider as a future optimization.
577 /// For legacy desktop behavior with ExpandShortPaths:
578 /// - It has no impact on GetPathRoot() so doesn't need consideration.
579 /// - It could impact GetDirectoryName(), but only if the path isn't relative (C:\ or \\Server\Share).
581 /// In the case of GetDirectoryName() the ExpandShortPaths behavior was undocumented and provided inconsistent results if the path was
582 /// 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
583 /// ultimately call GetFullPath() this doesn't matter, but if you don't or have any intermediate string handling could easily be tripped up by
584 /// this undocumented behavior.
586 internal static string NormalizeDirectorySeparators(string path)
588 if (string.IsNullOrEmpty(path)) return path;
591 int start = PathStartSkip(path);
595 // Make a pass to see if we need to normalize so we can potentially skip allocating
596 bool normalized = true;
598 for (int i = 0; i < path.Length; i++)
601 if (IsDirectorySeparator(current)
602 && (current != Path.DirectorySeparatorChar
603 // Check for sequential separators past the first position (we need to keep initial two for UNC/extended)
604 || (i > 0 && i + 1 < path.Length && IsDirectorySeparator(path[i + 1]))))
611 if (normalized) return path;
614 StringBuilder builder = StringBuilderCache.Acquire(path.Length);
616 if (IsDirectorySeparator(path[start]))
619 builder.Append(Path.DirectorySeparatorChar);
622 for (int i = start; i < path.Length; i++)
626 // If we have a separator
627 if (IsDirectorySeparator(current))
629 // If the next is a separator, skip adding this
630 if (i + 1 < path.Length && IsDirectorySeparator(path[i + 1]))
635 // Ensure it is the primary separator
636 current = Path.DirectorySeparatorChar;
639 builder.Append(current);
642 return StringBuilderCache.GetStringAndRelease(builder);