Wrapper to help with path normalization.
예제 #1
0
        [System.Security.SecurityCritical]  // auto-generated
        private unsafe static string NormalizePath(string path, bool fullCheck, int maxPathLength, bool expandShortPaths)
        {
            Contract.Requires(path != null, "path can't be null");

            // If the path is in extended syntax, we don't need to normalize, but we still do some basic validity checks
            if (PathInternal.IsExtended(path))
            {
                if (!ValidateExtendedPath(path, fullCheck))
                {
                    throw new ArgumentException(SR.Arg_PathIllegal);
                }

                // \\?\GLOBALROOT gives access to devices out of the scope of the current user, we
                // don't want to allow this for security reasons.
                // https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247.aspx#nt_namespaces
                if (path.StartsWith(@"\\?\globalroot", StringComparison.OrdinalIgnoreCase))
                    throw new ArgumentException(SR.Arg_PathGlobalRoot);

                return path;
            }

            // If we're doing a full path check, trim whitespace and look for
            // illegal path characters.
            if (fullCheck)
            {
                // Trim whitespace off the end of the string.
                // Win32 normalization trims only U+0020.
                path = path.TrimEnd(TrimEndChars);

                // Look for illegal path characters.
                PathInternal.CheckInvalidPathChars(path);
            }

            int index = 0;

            // We prefer to allocate on the stack for workingset/perf gain. If the 
            // starting path is less than MaxPath then we can stackalloc; otherwise we'll
            // use a StringBuilder (PathHelper does this under the hood). The latter may
            // happen in 2 cases:
            // 1. Starting path is greater than MaxPath but it normalizes down to MaxPath.
            // This is relevant for paths containing escape sequences. In this case, we
            // attempt to normalize down to MaxPath, but the caller pays a perf penalty 
            // since StringBuilder is used. 
            // 2. IsolatedStorage, which supports paths longer than MaxPath (value given 
            // by maxPathLength.
            PathHelper newBuffer = null;
            if (path.Length + 1 <= MaxPath)
            {
                char* m_arrayPtr = stackalloc char[MaxPath];
                newBuffer = new PathHelper(m_arrayPtr, MaxPath);
            }
            else
            {
                newBuffer = new PathHelper(path.Length + MaxPath, maxPathLength);
            }

            uint numSpaces = 0;
            uint numDots = 0;
            bool fixupDirectorySeparator = false;
            // Number of significant chars other than potentially suppressible
            // dots and spaces since the last directory or volume separator char
            uint numSigChars = 0;
            int lastSigChar = -1; // Index of last significant character.
            // Whether this segment of the path (not the complete path) started
            // with a volume separator char.  Reject "c:...".
            bool startedWithVolumeSeparator = false;
            bool firstSegment = true;
            int lastDirectorySeparatorPos = 0;

            bool mightBeShortFileName = false;

            // LEGACY: This code is here for backwards compatibility reasons. It 
            // ensures that \\foo.cs\bar.cs stays \\foo.cs\bar.cs instead of being
            // turned into \foo.cs\bar.cs.
            if (path.Length > 0 && PathInternal.IsDirectorySeparator(path[0]))
            {
                newBuffer.Append('\\');
                index++;
                lastSigChar = 0;
            }

            // Normalize the string, stripping out redundant dots, spaces, and 
            // slashes.
            while (index < path.Length)
            {
                char currentChar = path[index];

                // We handle both directory separators and dots specially.  For 
                // directory separators, we consume consecutive appearances.  
                // For dots, we consume all dots beyond the second in 
                // succession.  All other characters are added as is.  In 
                // addition we consume all spaces after the last other char
                // in a directory name up until the directory separator.

                if (PathInternal.IsDirectorySeparator(currentChar))
                {
                    // If we have a path like "123.../foo", remove the trailing dots.
                    // However, if we found "c:\temp\..\bar" or "c:\temp\...\bar", don't.
                    // Also remove trailing spaces from both files & directory names.
                    // This was agreed on with the OS team to fix undeletable directory
                    // names ending in spaces.

                    // If we saw a '\' as the previous last significant character and
                    // are simply going to write out dots, suppress them.
                    // If we only contain dots and slashes though, only allow
                    // a string like [dot]+ [space]*.  Ignore everything else.
                    // Legal: "\.. \", "\...\", "\. \"
                    // Illegal: "\.. .\", "\. .\", "\ .\"
                    if (numSigChars == 0)
                    {
                        // Dot and space handling
                        if (numDots > 0)
                        {
                            // Look for ".[space]*" or "..[space]*"
                            int start = lastSigChar + 1;
                            if (path[start] != '.')
                                throw new ArgumentException(SR.Arg_PathIllegal);

                            // Only allow "[dot]+[space]*", and normalize the 
                            // legal ones to "." or ".."
                            if (numDots >= 2)
                            {
                                // Reject "C:..."
                                if (startedWithVolumeSeparator && numDots > 2)
                                    throw new ArgumentException(SR.Arg_PathIllegal);

                                if (path[start + 1] == '.')
                                {
                                    // Search for a space in the middle of the
                                    // dots and throw
                                    for (int i = start + 2; i < start + numDots; i++)
                                    {
                                        if (path[i] != '.')
                                            throw new ArgumentException(SR.Arg_PathIllegal);
                                    }

                                    numDots = 2;
                                }
                                else
                                {
                                    if (numDots > 1)
                                        throw new ArgumentException(SR.Arg_PathIllegal);
                                    numDots = 1;
                                }
                            }

                            if (numDots == 2)
                            {
                                newBuffer.Append('.');
                            }

                            newBuffer.Append('.');
                            fixupDirectorySeparator = false;
                            // Continue in this case, potentially writing out '\'.
                        }

                        if (numSpaces > 0 && firstSegment)
                        {
                            // Handle strings like " \\server\share".
                            if (index + 1 < path.Length && PathInternal.IsDirectorySeparator(path[index + 1]))
                            {
                                newBuffer.Append(DirectorySeparatorChar);
                            }
                        }
                    }
                    numDots = 0;
                    numSpaces = 0;  // Suppress trailing spaces

                    if (!fixupDirectorySeparator)
                    {
                        fixupDirectorySeparator = true;
                        newBuffer.Append(DirectorySeparatorChar);
                    }
                    numSigChars = 0;
                    lastSigChar = index;
                    startedWithVolumeSeparator = false;
                    firstSegment = false;

                    // For short file names, we must try to expand each of them as
                    // soon as possible.  We need to allow people to specify a file
                    // name that doesn't exist using a path with short file names
                    // in it, such as this for a temp file we're trying to create:
                    // C:\DOCUME~1\USERNA~1.RED\LOCALS~1\Temp\bg3ylpzp
                    // We could try doing this afterwards piece by piece, but it's
                    // probably a lot simpler to do it here.
                    if (mightBeShortFileName)
                    {
                        newBuffer.TryExpandShortFileName();
                        mightBeShortFileName = false;
                    }

                    int thisPos = newBuffer.Length - 1;
                    if (thisPos - lastDirectorySeparatorPos > MaxComponentLength)
                    {
                        throw new PathTooLongException(SR.IO_PathTooLong);
                    }
                    lastDirectorySeparatorPos = thisPos;
                } // if (Found directory separator)
                else if (currentChar == '.')
                {
                    // Reduce only multiple .'s only after slash to 2 dots. For
                    // instance a...b is a valid file name.
                    numDots++;
                    // Don't flush out non-terminal spaces here, because they may in
                    // the end not be significant.  Turn "c:\ . .\foo" -> "c:\foo"
                    // which is the conclusion of removing trailing dots & spaces,
                    // as well as folding multiple '\' characters.
                }
                else if (currentChar == ' ')
                {
                    numSpaces++;
                }
                else
                {  // Normal character logic
                    if (currentChar == '~' && expandShortPaths)
                        mightBeShortFileName = true;

                    fixupDirectorySeparator = false;

                    // To reject strings like "C:...\foo" and "C  :\foo"
                    if (firstSegment && currentChar == VolumeSeparatorChar)
                    {
                        // Only accept "C:", not "c :" or ":"
                        // Get a drive letter or ' ' if index is 0.
                        char driveLetter = (index > 0) ? path[index - 1] : ' ';
                        bool validPath = ((numDots == 0) && (numSigChars >= 1) && (driveLetter != ' '));
                        if (!validPath)
                            throw new ArgumentException(SR.Arg_PathIllegal);

                        startedWithVolumeSeparator = true;
                        // We need special logic to make " c:" work, we should not fix paths like "  foo::$DATA"
                        if (numSigChars > 1)
                        { // Common case, simply do nothing
                            int spaceCount = 0; // How many spaces did we write out, numSpaces has already been reset.
                            while ((spaceCount < newBuffer.Length) && newBuffer[spaceCount] == ' ')
                                spaceCount++;
                            if (numSigChars - spaceCount == 1)
                            {
                                //Safe to update stack ptr directly
                                newBuffer.Length = 0;
                                newBuffer.Append(driveLetter); // Overwrite spaces, we need a special case to not break "  foo" as a relative path.
                            }
                        }
                        numSigChars = 0;
                    }
                    else
                    {
                        numSigChars += 1 + numDots + numSpaces;
                    }

                    // Copy any spaces & dots since the last significant character
                    // to here.  Note we only counted the number of dots & spaces,
                    // and don't know what order they're in.  Hence the copy.
                    if (numDots > 0 || numSpaces > 0)
                    {
                        int numCharsToCopy = (lastSigChar >= 0) ? index - lastSigChar - 1 : index;
                        if (numCharsToCopy > 0)
                        {
                            for (int i = 0; i < numCharsToCopy; i++)
                            {
                                newBuffer.Append(path[lastSigChar + 1 + i]);
                            }
                        }
                        numDots = 0;
                        numSpaces = 0;
                    }

                    newBuffer.Append(currentChar);
                    lastSigChar = index;
                }

                index++;
            } // end while

            if (newBuffer.Length - 1 - lastDirectorySeparatorPos > MaxComponentLength)
            {
                throw new PathTooLongException(SR.IO_PathTooLong);
            }

            // Drop any trailing dots and spaces from file & directory names, EXCEPT
            // we MUST make sure that "C:\foo\.." is correctly handled.
            // Also handle "C:\foo\." -> "C:\foo", while "C:\." -> "C:\"
            if (numSigChars == 0)
            {
                if (numDots > 0)
                {
                    // Look for ".[space]*" or "..[space]*"
                    int start = lastSigChar + 1;
                    if (path[start] != '.')
                        throw new ArgumentException(SR.Arg_PathIllegal);

                    // Only allow "[dot]+[space]*", and normalize the 
                    // legal ones to "." or ".."
                    if (numDots >= 2)
                    {
                        // Reject "C:..."
                        if (startedWithVolumeSeparator && numDots > 2)
                            throw new ArgumentException(SR.Arg_PathIllegal);

                        if (path[start + 1] == '.')
                        {
                            // Search for a space in the middle of the
                            // dots and throw
                            for (int i = start + 2; i < start + numDots; i++)
                            {
                                if (path[i] != '.')
                                    throw new ArgumentException(SR.Arg_PathIllegal);
                            }

                            numDots = 2;
                        }
                        else
                        {
                            if (numDots > 1)
                                throw new ArgumentException(SR.Arg_PathIllegal);
                            numDots = 1;
                        }
                    }

                    if (numDots == 2)
                    {
                        newBuffer.Append('.');
                    }

                    newBuffer.Append('.');
                }
            } // if (numSigChars == 0)

            // If we ended up eating all the characters, bail out.
            if (newBuffer.Length == 0)
                throw new ArgumentException(SR.Arg_PathIllegal);

            // Disallow URL's here.  Some of our other Win32 API calls will reject
            // them later, so we might be better off rejecting them here.
            // Note we've probably turned them into "file:\D:\foo.tmp" by now.
            // But for compatibility, ensure that callers that aren't doing a 
            // full check aren't rejected here.
            if (fullCheck)
            {
                if (newBuffer.OrdinalStartsWith("http:", false) ||
                    newBuffer.OrdinalStartsWith("file:", false))
                {
                    throw new ArgumentException(SR.Argument_PathUriFormatNotSupported);
                }
            }

            // If the last part of the path (file or directory name) had a tilde,
            // expand that too.
            if (mightBeShortFileName)
            {
                newBuffer.TryExpandShortFileName();
            }

            // Call the Win32 API to do the final canonicalization step.
            int result = 1;

            if (fullCheck)
            {
                // NOTE: Win32 GetFullPathName requires the input buffer to be big enough to fit the initial
                // path which is a concat of CWD and the relative path, this can be of an arbitrary
                // size and could be > MaxPath (which becomes an artificial limit at this point),
                // even though the final normalized path after fixing up the relative path syntax
                // might be well within the MaxPath restriction. For ex,
                // "c:\SomeReallyLongDirName(thinkGreaterThan_MAXPATH)\..\foo.txt" which actually requires a
                // buffer well with in the MaxPath as the normalized path is just "c:\foo.txt"
                // This buffer requirement seems wrong, it could be a bug or a perf optimization
                // like returning required buffer length quickly or avoid stratch buffer etc.
                // Either way we need to workaround it here...

                // Ideally we would get the required buffer length first by calling GetFullPathName
                // once without the buffer and use that in the later call but this doesn't always work
                // due to Win32 GetFullPathName bug. For instance, in Win2k, when the path we are trying to
                // fully qualify is a single letter name (such as "a", "1", ",") GetFullPathName
                // fails to return the right buffer size (i.e, resulting in insufficient buffer).
                // To workaround this bug we will start with MaxPath buffer and grow it once if the
                // return value is > MaxPath.

                result = newBuffer.GetFullPathName();

                // If we called GetFullPathName with something like "foo" and our
                // command window was in short file name mode (ie, by running edlin or
                // DOS versions of grep, etc), we might have gotten back a short file
                // name.  So, check to see if we need to expand it.
                mightBeShortFileName = false;
                for (int i = 0; i < newBuffer.Length && !mightBeShortFileName; i++)
                {
                    if (newBuffer[i] == '~' && expandShortPaths)
                        mightBeShortFileName = true;
                }

                if (mightBeShortFileName)
                {
                    bool r = newBuffer.TryExpandShortFileName();
                    // Consider how the path "Doesn'tExist" would expand.  If
                    // we add in the current directory, it too will need to be
                    // fully expanded, which doesn't happen if we use a file
                    // name that doesn't exist.
                    if (!r)
                    {
                        int lastSlash = -1;

                        for (int i = newBuffer.Length - 1; i >= 0; i--)
                        {
                            if (newBuffer[i] == DirectorySeparatorChar)
                            {
                                lastSlash = i;
                                break;
                            }
                        }

                        if (lastSlash >= 0)
                        {
                            // This bounds check is for safe memcpy but we should never get this far 
                            if (newBuffer.Length >= maxPathLength)
                                throw new PathTooLongException(SR.IO_PathTooLong);

                            int lenSavedName = newBuffer.Length - lastSlash - 1;
                            Debug.Assert(lastSlash < newBuffer.Length, "path unexpectedly ended in a '\'");

                            newBuffer.Fixup(lenSavedName, lastSlash);
                        }
                    }
                }
            }

            if (result != 0)
            {
                /* Throw an ArgumentException for paths like \\, \\server, \\server\
                   This check can only be properly done after normalizing, so
                   \\foo\.. will be properly rejected. */
                if (newBuffer.Length > 1 && newBuffer[0] == '\\' && newBuffer[1] == '\\')
                {
                    int startIndex = 2;
                    while (startIndex < result)
                    {
                        if (newBuffer[startIndex] == '\\')
                        {
                            startIndex++;
                            break;
                        }
                        else
                        {
                            startIndex++;
                        }
                    }
                    if (startIndex == result)
                        throw new ArgumentException(SR.Arg_PathIllegalUNC);
                }
            }

            // Check our result and form the managed string as necessary.
            if (newBuffer.Length >= maxPathLength)
                throw new PathTooLongException(SR.IO_PathTooLong);

            if (result == 0)
            {
                int errorCode = Marshal.GetLastWin32Error();
                if (errorCode == 0)
                    errorCode = Interop.mincore.Errors.ERROR_BAD_PATHNAME;
                throw Win32Marshal.GetExceptionForWin32Error(errorCode, path);
            }

            string returnVal = newBuffer.ToString();
            if (string.Equals(returnVal, path, StringComparison.Ordinal))
            {
                returnVal = path;
            }
            return returnVal;
        }
예제 #2
0
        private static string NormalizeStandardPath(string path, bool fullCheck, bool expandShortPaths)
        {
            // Technically this doesn't matter, but we used to throw for this case
            if (String.IsNullOrWhiteSpace(path))
                throw new ArgumentException(SR.Arg_PathIllegal);

            PathHelper helper = new PathHelper();
            string returnVal = helper.Normalize(path, fullCheck, expandShortPaths);

            if (String.Equals(returnVal, path, StringComparison.Ordinal))
            {
                returnVal = path;
            }
            return returnVal;
        }
예제 #3
0
        // Expands the given path to a fully qualified path.
        public static string GetFullPath(string path)
        {
            if (path == null)
            {
                throw new ArgumentNullException(nameof(path));
            }

            // Embedded null characters are the only invalid character case we want to check up front.
            // This is because the nulls will signal the end of the string to Win32 and therefore have
            // unpredictable results. Other invalid characters we give a chance to be normalized out.
            if (path.IndexOf('\0') != -1)
            {
                throw new ArgumentException(SR.Argument_InvalidPathChars, nameof(path));
            }

            if (PathInternal.IsExtended(path))
            {
                // We can't really know what is valid for all cases of extended paths.
                //
                //  - object names can include other characters as well (':', '/', etc.)
                //  - even file objects have different rules (pipe names can contain most characters)
                //
                // As such we will do no further analysis of extended paths to avoid blocking known and unknown
                // scenarios as well as minimizing compat breaks should we block now and need to unblock later.
                return(path);
            }

            bool isDevice = PathInternal.IsDevice(path);

            if (!isDevice)
            {
                // Toss out paths with colons that aren't a valid drive specifier.
                // Cannot start with a colon and can only be of the form "C:".
                // (Note that we used to explicitly check "http:" and "file:"- these are caught by this check now.)
                int startIndex = PathInternal.PathStartSkip(path);

                // Move past the colon
                startIndex += 2;

                if ((path.Length > 0 && path[0] == PathInternal.VolumeSeparatorChar) ||
                    (path.Length >= startIndex && path[startIndex - 1] == PathInternal.VolumeSeparatorChar && !PathInternal.IsValidDriveChar(path[startIndex - 2])) ||
                    (path.Length > startIndex && path.IndexOf(PathInternal.VolumeSeparatorChar, startIndex) != -1))
                {
                    throw new NotSupportedException(SR.Format(SR.Argument_PathFormatNotSupported_Path, path));
                }
            }

            // Technically this doesn't matter but we used to throw for this case
            if (string.IsNullOrWhiteSpace(path))
            {
                throw new ArgumentException(SR.Arg_PathEmpty, nameof(path));
            }

            // We don't want to check invalid characters for device format- see comments for extended above
            string fullPath = PathHelper.Normalize(path, checkInvalidCharacters: !isDevice, expandShortPaths: true);

            if (!isDevice)
            {
                // Emulate FileIOPermissions checks, retained for compatibility (normal invalid characters have already been checked)
                if (PathInternal.HasWildCardCharacters(fullPath))
                {
                    throw new ArgumentException(SR.Argument_InvalidPathChars, nameof(path));
                }
            }

            return(fullPath);
        }
예제 #4
0
        /// <summary>
        /// Normalize the path and check for bad characters or other invalid syntax.
        /// </summary>
        /// <remarks>
        /// The legacy NormalizePath
        /// </remarks>
        private static string NormalizeAndValidatePath(string path)
        {
            Debug.Assert(path != null, "path can't be null");

            // Embedded null characters are the only invalid character case we want to check up front.
            // This is because the nulls will signal the end of the string to Win32 and therefore have
            // unpredictable results. Other invalid characters we give a chance to be normalized out.
            if (path.IndexOf('\0') != -1)
            {
                throw new ArgumentException(SR.Argument_InvalidPathChars, "path");
            }

            // Toss out paths with colons that aren't a valid drive specifier.
            // Cannot start with a colon and can only be of the form "C:" or "\\?\C:".
            // (Note that we used to explicitly check "http:" and "file:"- these are caught by this check now.)
            int  startIndex = PathInternal.PathStartSkip(path);
            bool isExtended = path.Length >= PathInternal.ExtendedPathPrefix.Length + startIndex &&
                              path.IndexOf(PathInternal.ExtendedPathPrefix, startIndex, PathInternal.ExtendedPathPrefix.Length, StringComparison.Ordinal) >= 0;

            if (isExtended)
            {
                startIndex += PathInternal.ExtendedPathPrefix.Length;
            }

            // Move past the colon
            startIndex += 2;

            if ((path.Length > 0 && path[0] == VolumeSeparatorChar) ||
                (path.Length >= startIndex && path[startIndex - 1] == VolumeSeparatorChar && !PathInternal.IsValidDriveChar(path[startIndex - 2])) ||
                (path.Length > startIndex && path.IndexOf(VolumeSeparatorChar, startIndex) != -1))
            {
                throw new NotSupportedException(SR.Argument_PathFormatNotSupported);
            }

            if (isExtended)
            {
                // If the path is in extended syntax, we don't need to normalize, but we still do some basic validity checks
                if (!ValidateExtendedPath(path))
                {
                    throw new ArgumentException(SR.Arg_PathIllegal);
                }

                // \\?\GLOBALROOT gives access to devices out of the scope of the current user, we
                // don't want to allow this for security reasons.
                // https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247.aspx#nt_namespaces
                if (path.StartsWith(@"\\?\globalroot", StringComparison.OrdinalIgnoreCase))
                {
                    throw new ArgumentException(SR.Arg_PathGlobalRoot);
                }


                // Look for illegal path characters.
                PathInternal.CheckInvalidPathChars(path);

                return(path);
            }
            else
            {
                // Technically this doesn't matter but we used to throw for this case
                if (String.IsNullOrWhiteSpace(path))
                {
                    throw new ArgumentException(SR.Arg_PathIllegal);
                }

                return(PathHelper.Normalize(path, checkInvalidCharacters: true, expandShortPaths: true));
            }
        }
예제 #5
0
        [System.Security.SecurityCritical]  // auto-generated
        private unsafe static string NormalizePath(string path, bool fullCheck, int maxPathLength, bool expandShortPaths)
        {
            Contract.Requires(path != null, "path can't be null");

            // If the path is in extended syntax, we don't need to normalize, but we still do some basic validity checks
            if (PathInternal.IsExtended(path))
            {
                if (!ValidateExtendedPath(path, fullCheck))
                {
                    throw new ArgumentException(SR.Arg_PathIllegal);
                }

                // \\?\GLOBALROOT gives access to devices out of the scope of the current user, we
                // don't want to allow this for security reasons.
                // https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247.aspx#nt_namespaces
                if (path.StartsWith(@"\\?\globalroot", StringComparison.OrdinalIgnoreCase))
                {
                    throw new ArgumentException(SR.Arg_PathGlobalRoot);
                }

                return(path);
            }

            // If we're doing a full path check, trim whitespace and look for
            // illegal path characters.
            if (fullCheck)
            {
                // Trim whitespace off the end of the string.
                // Win32 normalization trims only U+0020.
                path = path.TrimEnd(TrimEndChars);

                // Look for illegal path characters.
                PathInternal.CheckInvalidPathChars(path);
            }

            int index = 0;

            // We prefer to allocate on the stack for workingset/perf gain. If the
            // starting path is less than MaxPath then we can stackalloc; otherwise we'll
            // use a StringBuilder (PathHelper does this under the hood). The latter may
            // happen in 2 cases:
            // 1. Starting path is greater than MaxPath but it normalizes down to MaxPath.
            // This is relevant for paths containing escape sequences. In this case, we
            // attempt to normalize down to MaxPath, but the caller pays a perf penalty
            // since StringBuilder is used.
            // 2. IsolatedStorage, which supports paths longer than MaxPath (value given
            // by maxPathLength.
            PathHelper newBuffer = new PathHelper(path.Length + MaxPath, maxPathLength);

            uint numSpaces = 0;
            uint numDots   = 0;
            bool fixupDirectorySeparator = false;
            // Number of significant chars other than potentially suppressible
            // dots and spaces since the last directory or volume separator char
            uint numSigChars = 0;
            int  lastSigChar = -1; // Index of last significant character.
            // Whether this segment of the path (not the complete path) started
            // with a volume separator char.  Reject "c:...".
            bool startedWithVolumeSeparator = false;
            bool firstSegment = true;
            int  lastDirectorySeparatorPos = 0;

            bool mightBeShortFileName = false;

            // LEGACY: This code is here for backwards compatibility reasons. It
            // ensures that \\foo.cs\bar.cs stays \\foo.cs\bar.cs instead of being
            // turned into \foo.cs\bar.cs.
            if (path.Length > 0 && PathInternal.IsDirectorySeparator(path[0]))
            {
                newBuffer.Append('\\');
                index++;
                lastSigChar = 0;
            }

            // Normalize the string, stripping out redundant dots, spaces, and
            // slashes.
            while (index < path.Length)
            {
                char currentChar = path[index];

                // We handle both directory separators and dots specially.  For
                // directory separators, we consume consecutive appearances.
                // For dots, we consume all dots beyond the second in
                // succession.  All other characters are added as is.  In
                // addition we consume all spaces after the last other char
                // in a directory name up until the directory separator.

                if (PathInternal.IsDirectorySeparator(currentChar))
                {
                    // If we have a path like "123.../foo", remove the trailing dots.
                    // However, if we found "c:\temp\..\bar" or "c:\temp\...\bar", don't.
                    // Also remove trailing spaces from both files & directory names.
                    // This was agreed on with the OS team to fix undeletable directory
                    // names ending in spaces.

                    // If we saw a '\' as the previous last significant character and
                    // are simply going to write out dots, suppress them.
                    // If we only contain dots and slashes though, only allow
                    // a string like [dot]+ [space]*.  Ignore everything else.
                    // Legal: "\.. \", "\...\", "\. \"
                    // Illegal: "\.. .\", "\. .\", "\ .\"
                    if (numSigChars == 0)
                    {
                        // Dot and space handling
                        if (numDots > 0)
                        {
                            // Look for ".[space]*" or "..[space]*"
                            int start = lastSigChar + 1;
                            if (path[start] != '.')
                            {
                                throw new ArgumentException(SR.Arg_PathIllegal);
                            }

                            // Only allow "[dot]+[space]*", and normalize the
                            // legal ones to "." or ".."
                            if (numDots >= 2)
                            {
                                // Reject "C:..."
                                if (startedWithVolumeSeparator && numDots > 2)
                                {
                                    throw new ArgumentException(SR.Arg_PathIllegal);
                                }

                                if (path[start + 1] == '.')
                                {
                                    // Search for a space in the middle of the
                                    // dots and throw
                                    for (int i = start + 2; i < start + numDots; i++)
                                    {
                                        if (path[i] != '.')
                                        {
                                            throw new ArgumentException(SR.Arg_PathIllegal);
                                        }
                                    }

                                    numDots = 2;
                                }
                                else
                                {
                                    if (numDots > 1)
                                    {
                                        throw new ArgumentException(SR.Arg_PathIllegal);
                                    }
                                    numDots = 1;
                                }
                            }

                            if (numDots == 2)
                            {
                                newBuffer.Append('.');
                            }

                            newBuffer.Append('.');
                            fixupDirectorySeparator = false;
                            // Continue in this case, potentially writing out '\'.
                        }

                        if (numSpaces > 0 && firstSegment)
                        {
                            // Handle strings like " \\server\share".
                            if (index + 1 < path.Length && PathInternal.IsDirectorySeparator(path[index + 1]))
                            {
                                newBuffer.Append(DirectorySeparatorChar);
                            }
                        }
                    }
                    numDots   = 0;
                    numSpaces = 0;  // Suppress trailing spaces

                    if (!fixupDirectorySeparator)
                    {
                        fixupDirectorySeparator = true;
                        newBuffer.Append(DirectorySeparatorChar);
                    }
                    numSigChars = 0;
                    lastSigChar = index;
                    startedWithVolumeSeparator = false;
                    firstSegment = false;

                    // For short file names, we must try to expand each of them as
                    // soon as possible.  We need to allow people to specify a file
                    // name that doesn't exist using a path with short file names
                    // in it, such as this for a temp file we're trying to create:
                    // C:\DOCUME~1\USERNA~1.RED\LOCALS~1\Temp\bg3ylpzp
                    // We could try doing this afterwards piece by piece, but it's
                    // probably a lot simpler to do it here.
                    if (mightBeShortFileName)
                    {
                        newBuffer.TryExpandShortFileName();
                        mightBeShortFileName = false;
                    }

                    int thisPos = newBuffer.Length - 1;
                    if (thisPos - lastDirectorySeparatorPos > MaxComponentLength + 1)
                    {
                        // Components can be up to 255 characters plus an additional null,
                        // so separators can be 256 characters apart
                        throw new PathTooLongException(SR.IO_PathTooLong);
                    }
                    lastDirectorySeparatorPos = thisPos;
                } // if (Found directory separator)
                else if (currentChar == '.')
                {
                    // Reduce only multiple .'s only after slash to 2 dots. For
                    // instance a...b is a valid file name.
                    numDots++;
                    // Don't flush out non-terminal spaces here, because they may in
                    // the end not be significant.  Turn "c:\ . .\foo" -> "c:\foo"
                    // which is the conclusion of removing trailing dots & spaces,
                    // as well as folding multiple '\' characters.
                }
                else if (currentChar == ' ')
                {
                    numSpaces++;
                }
                else
                {  // Normal character logic
                    if (currentChar == '~' && expandShortPaths)
                    {
                        mightBeShortFileName = true;
                    }

                    fixupDirectorySeparator = false;

                    // To reject strings like "C:...\foo" and "C  :\foo"
                    if (firstSegment && currentChar == VolumeSeparatorChar)
                    {
                        // Only accept "C:", not "c :" or ":"
                        // Get a drive letter or ' ' if index is 0.
                        char driveLetter = (index > 0) ? path[index - 1] : ' ';
                        bool validPath   = ((numDots == 0) && (numSigChars >= 1) && (driveLetter != ' '));
                        if (!validPath)
                        {
                            throw new ArgumentException(SR.Arg_PathIllegal);
                        }

                        startedWithVolumeSeparator = true;
                        // We need special logic to make " c:" work, we should not fix paths like "  foo::$DATA"
                        if (numSigChars > 1)
                        {                       // Common case, simply do nothing
                            int spaceCount = 0; // How many spaces did we write out, numSpaces has already been reset.
                            while ((spaceCount < newBuffer.Length) && newBuffer[spaceCount] == ' ')
                            {
                                spaceCount++;
                            }
                            if (numSigChars - spaceCount == 1)
                            {
                                //Safe to update stack ptr directly
                                newBuffer.Length = 0;
                                newBuffer.Append(driveLetter); // Overwrite spaces, we need a special case to not break "  foo" as a relative path.
                            }
                        }
                        numSigChars = 0;
                    }
                    else
                    {
                        numSigChars += 1 + numDots + numSpaces;
                    }

                    // Copy any spaces & dots since the last significant character
                    // to here.  Note we only counted the number of dots & spaces,
                    // and don't know what order they're in.  Hence the copy.
                    if (numDots > 0 || numSpaces > 0)
                    {
                        int numCharsToCopy = (lastSigChar >= 0) ? index - lastSigChar - 1 : index;
                        if (numCharsToCopy > 0)
                        {
                            for (int i = 0; i < numCharsToCopy; i++)
                            {
                                newBuffer.Append(path[lastSigChar + 1 + i]);
                            }
                        }
                        numDots   = 0;
                        numSpaces = 0;
                    }

                    newBuffer.Append(currentChar);
                    lastSigChar = index;
                }

                index++;
            } // end while

            // Components can be up to 255 characters plus an additional null
            if (newBuffer.Length - lastDirectorySeparatorPos > MaxComponentLength)
            {
                throw new PathTooLongException(SR.IO_PathTooLong);
            }

            // Drop any trailing dots and spaces from file & directory names, EXCEPT
            // we MUST make sure that "C:\foo\.." is correctly handled.
            // Also handle "C:\foo\." -> "C:\foo", while "C:\." -> "C:\"
            if (numSigChars == 0)
            {
                if (numDots > 0)
                {
                    // Look for ".[space]*" or "..[space]*"
                    int start = lastSigChar + 1;
                    if (path[start] != '.')
                    {
                        throw new ArgumentException(SR.Arg_PathIllegal);
                    }

                    // Only allow "[dot]+[space]*", and normalize the
                    // legal ones to "." or ".."
                    if (numDots >= 2)
                    {
                        // Reject "C:..."
                        if (startedWithVolumeSeparator && numDots > 2)
                        {
                            throw new ArgumentException(SR.Arg_PathIllegal);
                        }

                        if (path[start + 1] == '.')
                        {
                            // Search for a space in the middle of the
                            // dots and throw
                            for (int i = start + 2; i < start + numDots; i++)
                            {
                                if (path[i] != '.')
                                {
                                    throw new ArgumentException(SR.Arg_PathIllegal);
                                }
                            }

                            numDots = 2;
                        }
                        else
                        {
                            if (numDots > 1)
                            {
                                throw new ArgumentException(SR.Arg_PathIllegal);
                            }
                            numDots = 1;
                        }
                    }

                    if (numDots == 2)
                    {
                        newBuffer.Append('.');
                    }

                    newBuffer.Append('.');
                }
            } // if (numSigChars == 0)

            // If we ended up eating all the characters, bail out.
            if (newBuffer.Length == 0)
            {
                throw new ArgumentException(SR.Arg_PathIllegal);
            }

            // Disallow URL's here.  Some of our other Win32 API calls will reject
            // them later, so we might be better off rejecting them here.
            // Note we've probably turned them into "file:\D:\foo.tmp" by now.
            // But for compatibility, ensure that callers that aren't doing a
            // full check aren't rejected here.
            if (fullCheck)
            {
                if (newBuffer.OrdinalStartsWith("http:", false) ||
                    newBuffer.OrdinalStartsWith("file:", false))
                {
                    throw new ArgumentException(SR.Argument_PathUriFormatNotSupported);
                }
            }

            // If the last part of the path (file or directory name) had a tilde,
            // expand that too.
            if (mightBeShortFileName)
            {
                newBuffer.TryExpandShortFileName();
            }

            // Call the Win32 API to do the final canonicalization step.
            int result = 1;

            if (fullCheck)
            {
                // NOTE: Win32 GetFullPathName requires the input buffer to be big enough to fit the initial
                // path which is a concat of CWD and the relative path, this can be of an arbitrary
                // size and could be > MaxPath (which becomes an artificial limit at this point),
                // even though the final normalized path after fixing up the relative path syntax
                // might be well within the MaxPath restriction. For ex,
                // "c:\SomeReallyLongDirName(thinkGreaterThan_MAXPATH)\..\foo.txt" which actually requires a
                // buffer well with in the MaxPath as the normalized path is just "c:\foo.txt"
                // This buffer requirement seems wrong, it could be a bug or a perf optimization
                // like returning required buffer length quickly or avoid stratch buffer etc.
                // Either way we need to workaround it here...

                // Ideally we would get the required buffer length first by calling GetFullPathName
                // once without the buffer and use that in the later call but this doesn't always work
                // due to Win32 GetFullPathName bug. For instance, in Win2k, when the path we are trying to
                // fully qualify is a single letter name (such as "a", "1", ",") GetFullPathName
                // fails to return the right buffer size (i.e, resulting in insufficient buffer).
                // To workaround this bug we will start with MaxPath buffer and grow it once if the
                // return value is > MaxPath.

                result = newBuffer.GetFullPathName();

                // If we called GetFullPathName with something like "foo" and our
                // command window was in short file name mode (ie, by running edlin or
                // DOS versions of grep, etc), we might have gotten back a short file
                // name.  So, check to see if we need to expand it.
                mightBeShortFileName = false;
                for (int i = 0; i < newBuffer.Length && !mightBeShortFileName; i++)
                {
                    if (newBuffer[i] == '~' && expandShortPaths)
                    {
                        mightBeShortFileName = true;
                    }
                }

                if (mightBeShortFileName)
                {
                    bool r = newBuffer.TryExpandShortFileName();
                    // Consider how the path "Doesn'tExist" would expand.  If
                    // we add in the current directory, it too will need to be
                    // fully expanded, which doesn't happen if we use a file
                    // name that doesn't exist.
                    if (!r)
                    {
                        int lastSlash = -1;

                        for (int i = newBuffer.Length - 1; i >= 0; i--)
                        {
                            if (newBuffer[i] == DirectorySeparatorChar)
                            {
                                lastSlash = i;
                                break;
                            }
                        }

                        if (lastSlash >= 0)
                        {
                            // This bounds check is for safe memcpy but we should never get this far
                            if (newBuffer.Length >= maxPathLength)
                            {
                                throw new PathTooLongException(SR.IO_PathTooLong);
                            }

                            int lenSavedName = newBuffer.Length - lastSlash - 1;
                            Debug.Assert(lastSlash < newBuffer.Length, "path unexpectedly ended in a '\'");

                            newBuffer.Fixup(lenSavedName, lastSlash);
                        }
                    }
                }
            }

            if (result != 0)
            {
                /* Throw an ArgumentException for paths like \\, \\server, \\server\
                 * This check can only be properly done after normalizing, so
                 \\foo\.. will be properly rejected. */
                if (newBuffer.Length > 1 && newBuffer[0] == '\\' && newBuffer[1] == '\\')
                {
                    int startIndex = 2;
                    while (startIndex < result)
                    {
                        if (newBuffer[startIndex] == '\\')
                        {
                            startIndex++;
                            break;
                        }
                        else
                        {
                            startIndex++;
                        }
                    }
                    if (startIndex == result)
                    {
                        throw new ArgumentException(SR.Arg_PathIllegalUNC);
                    }
                }
            }

            // Check our result and form the managed string as necessary.
            if (newBuffer.Length >= maxPathLength)
            {
                throw new PathTooLongException(SR.IO_PathTooLong);
            }

            if (result == 0)
            {
                int errorCode = Marshal.GetLastWin32Error();
                if (errorCode == 0)
                {
                    errorCode = Interop.mincore.Errors.ERROR_BAD_PATHNAME;
                }
                throw Win32Marshal.GetExceptionForWin32Error(errorCode, path);
            }

            string returnVal = newBuffer.ToString();

            if (string.Equals(returnVal, path, StringComparison.Ordinal))
            {
                returnVal = path;
            }
            return(returnVal);
        }