// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
namespace System.IO
{
/// Contains internal path helpers that are shared between many projects.
internal static partial class PathInternal
{
// 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
// DosDevice symbolic links in the object tree to actual File objects under \Devices. To illustrate, this is what happens with a typical
// path "Foo" passed as a filename to any Win32 API:
//
// 1. "Foo" is recognized as a relative path and is appended to the current directory (say, "C:\" in our example)
// 2. "C:\Foo" is prepended with the DosDevice namespace "\??\"
// 3. CreateFile tries to create an object handle to the requested file "\??\C:\Foo"
// 4. The Object Manager recognizes the DosDevices prefix and looks
// a. First in the current session DosDevices ("\Sessions\1\DosDevices\" for example, mapped network drives go here)
// b. If not found in the session, it looks in the Global DosDevices ("\GLOBAL??\")
// 5. "C:" is found in DosDevices (in our case "\GLOBAL??\C:", which is a symbolic link to "\Device\HarddiskVolume6")
// 6. The full path is now "\Device\HarddiskVolume6\Foo", "\Device\HarddiskVolume6" is a File object and parsing is handed off
// to the registered parsing method for Files
// 7. The registered open method for File objects is invoked to create the file handle which is then returned
//
// There are multiple ways to directly specify a DosDevices path. The final format of "\??\" is one way. It can also be specified
// as "\\.\" (the most commonly documented way) and "\\?\". If the question mark syntax is used the path will skip normalization
// (essentially GetFullPathName()) and path length checks.
// Windows Kernel-Mode Object Manager
// https://msdn.microsoft.com/en-us/library/windows/hardware/ff565763.aspx
// https://channel9.msdn.com/Shows/Going+Deep/Windows-NT-Object-Manager
//
// Introduction to MS-DOS Device Names
// https://msdn.microsoft.com/en-us/library/windows/hardware/ff548088.aspx
//
// Local and Global MS-DOS Device Names
// https://msdn.microsoft.com/en-us/library/windows/hardware/ff554302.aspx
internal const char DirectorySeparatorChar = '\\';
internal const char AltDirectorySeparatorChar = '/';
internal const char VolumeSeparatorChar = ':';
internal const char PathSeparator = ';';
internal const string DirectorySeparatorCharAsString = "\\";
internal const string ExtendedPathPrefix = @"\\?\";
internal const string UncPathPrefix = @"\\";
internal const string UncExtendedPrefixToInsert = @"?\UNC\";
internal const string UncExtendedPathPrefix = @"\\?\UNC\";
internal const string DevicePathPrefix = @"\\.\";
internal const string ParentDirectoryPrefix = @"..\";
internal const int MaxShortPath = 260;
internal const int MaxShortDirectoryPath = 248;
internal const int MaxLongPath = short.MaxValue;
// \\?\, \\.\, \??\
internal const int DevicePrefixLength = 4;
// \\
internal const int UncPrefixLength = 2;
// \\?\UNC\, \\.\UNC\
internal const int UncExtendedPrefixLength = 8;
internal const int MaxComponentLength = 255;
///
/// Returns true if the given character is a valid drive letter
///
internal static bool IsValidDriveChar(char value)
{
return ((value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z'));
}
///
/// Adds the extended path prefix (\\?\) if not already a device path, IF the path is not relative,
/// AND the path is more than 259 characters. (> MAX_PATH + null)
///
internal static string EnsureExtendedPrefixOverMaxPath(string path)
{
if (path != null && path.Length >= MaxShortPath)
{
return EnsureExtendedPrefix(path);
}
else
{
return path;
}
}
///
/// Adds the extended path prefix (\\?\) if not relative or already a device path.
///
internal static string EnsureExtendedPrefix(string path)
{
// Putting the extended prefix on the path changes the processing of the path. It won't get normalized, which
// means adding to relative paths will prevent them from getting the appropriate current directory inserted.
// If it already has some variant of a device path (\??\, \\?\, \\.\, //./, etc.) we don't need to change it
// as it is either correct or we will be changing the behavior. When/if Windows supports long paths implicitly
// in the future we wouldn't want normalization to come back and break existing code.
// In any case, all internal usages should be hitting normalize path (Path.GetFullPath) before they hit this
// shimming method. (Or making a change that doesn't impact normalization, such as adding a filename to a
// normalized base path.)
if (IsPartiallyQualified(path) || IsDevice(path))
return path;
// Given \\server\share in longpath becomes \\?\UNC\server\share
if (path.StartsWith(UncPathPrefix, StringComparison.OrdinalIgnoreCase))
return path.Insert(2, UncExtendedPrefixToInsert);
return ExtendedPathPrefix + path;
}
///
/// Returns true if the path uses any of the DOS device path syntaxes. ("\\.\", "\\?\", or "\??\")
///
internal static bool IsDevice(string path)
{
// If the path begins with any two separators is will be recognized and normalized and prepped with
// "\??\" for internal usage correctly. "\??\" is recognized and handled, "/??/" is not.
return IsExtended(path)
||
(
path.Length >= DevicePrefixLength
&& IsDirectorySeparator(path[0])
&& IsDirectorySeparator(path[1])
&& (path[2] == '.' || path[2] == '?')
&& IsDirectorySeparator(path[3])
);
}
///
/// Returns true if the path uses the canonical form of extended syntax ("\\?\" or "\??\"). If the
/// path matches exactly (cannot use alternate directory separators) Windows will skip normalization
/// and path length checks.
///
internal static bool IsExtended(string path)
{
// While paths like "//?/C:/" will work, they're treated the same as "\\.\" paths.
// Skipping of normalization will *only* occur if back slashes ('\') are used.
return path.Length >= DevicePrefixLength
&& path[0] == '\\'
&& (path[1] == '\\' || path[1] == '?')
&& path[2] == '?'
&& path[3] == '\\';
}
///
/// Returns a value indicating if the given path contains invalid characters (", <, >, |
/// NUL, or any ASCII char whose integer representation is in the range of 1 through 31).
/// Does not check for wild card characters ? and *.
///
internal static bool HasIllegalCharacters(string path)
{
// This is equivalent to IndexOfAny(InvalidPathChars) >= 0,
// except faster since IndexOfAny grows slower as the input
// array grows larger.
// Since we know that some of the characters we're looking
// for are contiguous in the alphabet-- the path cannot contain
// characters 0-31-- we can optimize this for our specific use
// case and use simple comparison operations.
for (int i = 0; i < path.Length; i++)
{
char c = path[i];
if (c <= '|') // fast path for common case - '|' is highest illegal character
{
if (c <= '\u001f' || c == '|')
{
return true;
}
}
}
return false;
}
///
/// Check for known wildcard characters. '*' and '?' are the most common ones.
///
internal static bool HasWildCardCharacters(string path)
{
// Question mark is part of dos device syntax so we have to skip if we are
int startIndex = IsDevice(path) ? ExtendedPathPrefix.Length : 0;
// [MS - FSA] 2.1.4.4 Algorithm for Determining if a FileName Is in an Expression
// https://msdn.microsoft.com/en-us/library/ff469270.aspx
for (int i = startIndex; i < path.Length; i++)
{
char c = path[i];
if (c <= '?') // fast path for common case - '?' is highest wildcard character
{
if (c == '\"' || c == '<' || c == '>' || c == '*' || c == '?')
return true;
}
}
return false;
}
///
/// Gets the length of the root of the path (drive, share, etc.).
///
internal unsafe static int GetRootLength(string path)
{
fixed(char* value = path)
{
return GetRootLength(value, path.Length);
}
}
private unsafe static int GetRootLength(char* path, int pathLength)
{
int i = 0;
int volumeSeparatorLength = 2; // Length to the colon "C:"
int uncRootLength = 2; // Length to the start of the server name "\\"
bool extendedSyntax = StartsWithOrdinal(path, pathLength, ExtendedPathPrefix);
bool extendedUncSyntax = StartsWithOrdinal(path, pathLength, UncExtendedPathPrefix);
if (extendedSyntax)
{
// Shift the position we look for the root from to account for the extended prefix
if (extendedUncSyntax)
{
// "\\" -> "\\?\UNC\"
uncRootLength = UncExtendedPathPrefix.Length;
}
else
{
// "C:" -> "\\?\C:"
volumeSeparatorLength += ExtendedPathPrefix.Length;
}
}
if ((!extendedSyntax || extendedUncSyntax) && pathLength > 0 && IsDirectorySeparator(path[0]))
{
// UNC or simple rooted path (e.g. "\foo", NOT "\\?\C:\foo")
i = 1; // Drive rooted (\foo) is one character
if (extendedUncSyntax || (pathLength > 1 && IsDirectorySeparator(path[1])))
{
// UNC (\\?\UNC\ or \\), scan past the next two directory separators at most
// (e.g. to \\?\UNC\Server\Share or \\Server\Share\)
i = uncRootLength;
int n = 2; // Maximum separators to skip
while (i < pathLength && (!IsDirectorySeparator(path[i]) || --n > 0)) i++;
}
}
else if (pathLength >= volumeSeparatorLength && path[volumeSeparatorLength - 1] == VolumeSeparatorChar)
{
// Path is at least longer than where we expect a colon, and has a colon (\\?\A:, A:)
// If the colon is followed by a directory separator, move past it
i = volumeSeparatorLength;
if (pathLength >= volumeSeparatorLength + 1 && IsDirectorySeparator(path[volumeSeparatorLength])) i++;
}
return i;
}
private unsafe static bool StartsWithOrdinal(char* source, int sourceLength, string value)
{
if (sourceLength < value.Length) return false;
for (int i = 0; i < value.Length; i++)
{
if (value[i] != source[i]) return false;
}
return true;
}
///
/// Returns true if the path specified is relative to the current drive or working directory.
/// Returns false if the path is fixed to a specific drive or UNC path. This method does no
/// validation of the path (URIs will be returned as relative as a result).
///
///
/// Handles paths that use the alternate directory separator. It is a frequent mistake to
/// assume that rooted paths (Path.IsPathRooted) are not relative. This isn't the case.
/// "C:a" is drive relative- meaning that it will be resolved against the current directory
/// for C: (rooted, but relative). "C:\a" is rooted and not relative (the current directory
/// will not be used to modify the path).
///
internal static bool IsPartiallyQualified(string path)
{
if (path.Length < 2)
{
// It isn't fixed, it must be relative. There is no way to specify a fixed
// path with one character (or less).
return true;
}
if (IsDirectorySeparator(path[0]))
{
// There is no valid way to specify a relative path with two initial slashes or
// \? as ? isn't valid for drive relative paths and \??\ is equivalent to \\?\
return !(path[1] == '?' || IsDirectorySeparator(path[1]));
}
// The only way to specify a fixed path that doesn't begin with two slashes
// is the drive, colon, slash format- i.e. C:\
return !((path.Length >= 3)
&& (path[1] == VolumeSeparatorChar)
&& IsDirectorySeparator(path[2])
// To match old behavior we'll check the drive character for validity as the path is technically
// not qualified if you don't have a valid drive. "=:\" is the "=" file's default data stream.
&& IsValidDriveChar(path[0]));
}
///
/// Returns the characters to skip at the start of the path if it starts with space(s) and a drive or directory separator.
/// (examples are " C:", " \")
/// This is a legacy behavior of Path.GetFullPath().
///
///
/// Note that this conflicts with IsPathRooted() which doesn't (and never did) such a skip.
///
internal static int PathStartSkip(string path)
{
int startIndex = 0;
while (startIndex < path.Length && path[startIndex] == ' ') startIndex++;
if (startIndex > 0 && (startIndex < path.Length && IsDirectorySeparator(path[startIndex]))
|| (startIndex + 1 < path.Length && path[startIndex + 1] == ':' && IsValidDriveChar(path[startIndex])))
{
// Go ahead and skip spaces as we're either " C:" or " \"
return startIndex;
}
return 0;
}
///
/// True if the given character is a directory separator.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool IsDirectorySeparator(char c)
{
return c == DirectorySeparatorChar || c == AltDirectorySeparatorChar;
}
///
/// Normalize separators in the given path. Converts forward slashes into back slashes and compresses slash runs, keeping initial 2 if present.
/// Also trims initial whitespace in front of "rooted" paths (see PathStartSkip).
///
/// This effectively replicates the behavior of the legacy NormalizePath when it was called with fullCheck=false and expandShortpaths=false.
/// The current NormalizePath gets directory separator normalization from Win32's GetFullPathName(), which will resolve relative paths and as
/// such can't be used here (and is overkill for our uses).
///
/// Like the current NormalizePath this will not try and analyze periods/spaces within directory segments.
///
///
/// The only callers that used to use Path.Normalize(fullCheck=false) were Path.GetDirectoryName() and Path.GetPathRoot(). Both usages do
/// not need trimming of trailing whitespace here.
///
/// GetPathRoot() could technically skip normalizing separators after the second segment- consider as a future optimization.
///
/// For legacy desktop behavior with ExpandShortPaths:
/// - It has no impact on GetPathRoot() so doesn't need consideration.
/// - It could impact GetDirectoryName(), but only if the path isn't relative (C:\ or \\Server\Share).
///
/// In the case of GetDirectoryName() the ExpandShortPaths behavior was undocumented and provided inconsistent results if the path was
/// 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
/// ultimately call GetFullPath() this doesn't matter, but if you don't or have any intermediate string handling could easily be tripped up by
/// this undocumented behavior.
///
/// We won't match this old behavior because:
///
/// 1. It was undocumented
/// 2. It was costly (extremely so if it actually contained '~')
/// 3. Doesn't play nice with string logic
/// 4. Isn't a cross-plat friendly concept/behavior
///
internal static string NormalizeDirectorySeparators(string path)
{
if (string.IsNullOrEmpty(path)) return path;
char current;
int start = PathStartSkip(path);
if (start == 0)
{
// Make a pass to see if we need to normalize so we can potentially skip allocating
bool normalized = true;
for (int i = 0; i < path.Length; i++)
{
current = path[i];
if (IsDirectorySeparator(current)
&& (current != DirectorySeparatorChar
// Check for sequential separators past the first position (we need to keep initial two for UNC/extended)
|| (i > 0 && i + 1 < path.Length && IsDirectorySeparator(path[i + 1]))))
{
normalized = false;
break;
}
}
if (normalized) return path;
}
StringBuilder builder = new StringBuilder(path.Length);
if (IsDirectorySeparator(path[start]))
{
start++;
builder.Append(DirectorySeparatorChar);
}
for (int i = start; i < path.Length; i++)
{
current = path[i];
// If we have a separator
if (IsDirectorySeparator(current))
{
// If the next is a separator, skip adding this
if (i + 1 < path.Length && IsDirectorySeparator(path[i + 1]))
{
continue;
}
// Ensure it is the primary separator
current = DirectorySeparatorChar;
}
builder.Append(current);
}
return builder.ToString();
}
///
/// Returns true if the character is a directory or volume separator.
///
/// The character to test.
internal static bool IsDirectoryOrVolumeSeparator(char ch)
{
return IsDirectorySeparator(ch) || VolumeSeparatorChar == ch;
}
}
}