internal static string Normalize(string path, bool checkInvalidCharacters, bool expandShortPaths)
{
// Get the full path
StringBuffer fullPath = t_fullPathBuffer ?? (t_fullPathBuffer = new StringBuffer(PathInternal.MaxShortPath));
try
{
GetFullPathName(path, fullPath);
// Trim whitespace off the end of the string. Win32 normalization trims only U+0020.
fullPath.TrimEnd(s_trimEndChars);
if (fullPath.Length >= PathInternal.MaxLongPath)
{
// Fullpath is genuinely too long
throw new PathTooLongException(SR.IO_PathTooLong);
}
// Checking path validity used to happen before getting the full path name. To avoid additional input allocation
// (to trim trailing whitespace) we now do it after the Win32 call. This will allow legitimate paths through that
// used to get kicked back (notably segments with invalid characters might get removed via "..").
//
// There is no way that GetLongPath can invalidate the path so we'll do this (cheaper) check before we attempt to
// expand short file names.
// Scan the path for:
//
// - Illegal path characters.
// - Invalid UNC paths like \\, \\server, \\server\.
// - Segments that are too long (over MaxComponentLength)
// As the path could be > 30K, we'll combine the validity scan. None of these checks are performed by the Win32
// GetFullPathName() API.
bool possibleShortPath = false;
bool foundTilde = false;
// We can get UNCs as device paths through this code (e.g. \\.\UNC\), we won't validate them as there isn't
// an easy way to normalize without extensive cost (we'd have to hunt down the canonical name for any device
// path that contains UNC or to see if the path was doing something like \\.\GLOBALROOT\Device\Mup\,
// \\.\GLOBAL\UNC\, \\.\GLOBALROOT\GLOBAL??\UNC\, etc.
bool specialPath = fullPath.Length > 1 && fullPath[0] == '\\' && fullPath[1] == '\\';
bool isDevice = PathInternal.IsDevice(fullPath);
bool possibleBadUnc = specialPath && !isDevice;
uint index = specialPath ? 2u : 0;
uint lastSeparator = specialPath ? 1u : 0;
uint segmentLength;
char* start = fullPath.CharPointer;
char current;
while (index < fullPath.Length)
{
current = start[index];
// Try to skip deeper analysis. '?' and higher are valid/ignorable except for '\', '|', and '~'
if (current < '?' || current == '\\' || current == '|' || current == '~')
{
switch (current)
{
case '|':
case '>':
case '<':
case '\"':
if (checkInvalidCharacters) throw new ArgumentException(SR.Argument_InvalidPathChars);
foundTilde = false;
break;
case '~':
foundTilde = true;
break;
case '\\':
segmentLength = index - lastSeparator - 1;
if (segmentLength > (uint)PathInternal.MaxComponentLength)
throw new PathTooLongException(SR.IO_PathTooLong + fullPath.ToString());
lastSeparator = index;
if (foundTilde)
{
if (segmentLength <= MaxShortName)
{
// Possibly a short path.
possibleShortPath = true;
}
foundTilde = false;
}
if (possibleBadUnc)
{
// If we're at the end of the path and this is the first separator, we're missing the share.
// Otherwise we're good, so ignore UNC tracking from here.
if (index == fullPath.Length - 1)
throw new ArgumentException(SR.Arg_PathIllegalUNC);
else
possibleBadUnc = false;
}
break;
default:
if (checkInvalidCharacters && current < ' ') throw new ArgumentException(SR.Argument_InvalidPathChars, nameof(path));
break;
}
}
index++;
}
if (possibleBadUnc)
throw new ArgumentException(SR.Arg_PathIllegalUNC);
segmentLength = fullPath.Length - lastSeparator - 1;
if (segmentLength > (uint)PathInternal.MaxComponentLength)
throw new PathTooLongException(SR.IO_PathTooLong);
if (foundTilde && segmentLength <= MaxShortName)
possibleShortPath = true;
// Check for a short filename path and try and expand it. Technically you don't need to have a tilde for a short name, but
// this is how we've always done this. This expansion is costly so we'll continue to let other short paths slide.
if (expandShortPaths && possibleShortPath)
{
return TryExpandShortFileName(fullPath, originalPath: path);
}
else
{
if (fullPath.Length == (uint)path.Length && fullPath.StartsWith(path))
{
// If we have the exact same string we were passed in, don't bother to allocate another string from the StringBuilder.
return path;
}
else
{
return fullPath.ToString();
}
}
}
finally
{
// Clear the buffer
fullPath.Free();
}
}