// 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.InteropServices; using System.Text; namespace System.IO { /// /// Wrapper to help with path normalization. /// internal class PathHelper { /// /// Normalize the given path. /// /// /// Normalizes via Win32 GetFullPathName(). /// /// Path to normalize /// Thrown if we have a string that is too large to fit into a UNICODE_STRING. /// Thrown if the path is empty. /// Normalized path internal static string Normalize(string path) { Span initialBuffer = stackalloc char[PathInternal.MaxShortPath]; var builder = new ValueStringBuilder(initialBuffer); // Get the full path GetFullPathName(path.AsSpan(), ref builder); // If we have the exact same string we were passed in, don't allocate another string. // TryExpandShortName does this input identity check. string result = builder.AsSpan().IndexOf('~') >= 0 ? TryExpandShortFileName(ref builder, originalPath: path) : builder.AsSpan().Equals(path.AsSpan(), StringComparison.Ordinal) ? path : builder.ToString(); // Clear the buffer builder.Dispose(); return result; } /// /// Normalize the given path. /// /// /// Exceptions are the same as the string overload. /// internal static string Normalize(ref ValueStringBuilder path) { Span initialBuffer = stackalloc char[PathInternal.MaxShortPath]; var builder = new ValueStringBuilder(initialBuffer); // Get the full path GetFullPathName(path.AsSpan(terminate: true), ref builder); string result = builder.AsSpan().IndexOf('~') >= 0 ? TryExpandShortFileName(ref builder, originalPath: null) : builder.ToString(); // Clear the buffer builder.Dispose(); return result; } /// /// Calls GetFullPathName on the given path. /// /// The path name. MUST be null terminated after the span. /// Builder that will store the result. private static void GetFullPathName(ReadOnlySpan path, ref ValueStringBuilder builder) { // If the string starts with an extended prefix we would need to remove it from the path before we call GetFullPathName as // it doesn't root extended paths correctly. We don't currently resolve extended paths, so we'll just assert here. Debug.Assert(PathInternal.IsPartiallyQualified(path) || !PathInternal.IsExtended(path)); uint result = 0; while ((result = Interop.Kernel32.GetFullPathNameW(ref MemoryMarshal.GetReference(path), (uint)builder.Capacity, ref builder.GetPinnableReference(), IntPtr.Zero)) > builder.Capacity) { // Reported size is greater than the buffer size. Increase the capacity. builder.EnsureCapacity(checked((int)result)); } if (result == 0) { // Failure, get the error and throw int errorCode = Marshal.GetLastWin32Error(); if (errorCode == 0) errorCode = Interop.Errors.ERROR_BAD_PATHNAME; throw Win32Marshal.GetExceptionForWin32Error(errorCode, path.ToString()); } builder.Length = (int)result; } internal static int PrependDevicePathChars(ref ValueStringBuilder content, bool isDosUnc, ref ValueStringBuilder buffer) { int length = content.Length; length += isDosUnc ? PathInternal.UncExtendedPrefixLength - PathInternal.UncPrefixLength : PathInternal.DevicePrefixLength; buffer.EnsureCapacity(length + 1); buffer.Length = 0; if (isDosUnc) { // Is a \\Server\Share, put \\?\UNC\ in the front buffer.Append(PathInternal.UncExtendedPathPrefix); // Copy Server\Share\... over to the buffer buffer.Append(content.AsSpan(PathInternal.UncPrefixLength)); // Return the prefix difference return PathInternal.UncExtendedPrefixLength - PathInternal.UncPrefixLength; } else { // Not an UNC, put the \\?\ prefix in front, then the original string buffer.Append(PathInternal.ExtendedPathPrefix); buffer.Append(content.AsSpan()); return PathInternal.DevicePrefixLength; } } internal static string TryExpandShortFileName(ref ValueStringBuilder outputBuilder, string originalPath) { // We guarantee we'll expand short names for paths that only partially exist. As such, we need to find the part of the path that actually does exist. To // avoid allocating like crazy we'll create only one input array and modify the contents with embedded nulls. Debug.Assert(!PathInternal.IsPartiallyQualified(outputBuilder.AsSpan()), "should have resolved by now"); // We'll have one of a few cases by now (the normalized path will have already: // // 1. Dos path (C:\) // 2. Dos UNC (\\Server\Share) // 3. Dos device path (\\.\C:\, \\?\C:\) // // We want to put the extended syntax on the front if it doesn't already have it (for long path support and speed), which may mean switching from \\.\. // // Note that we will never get \??\ here as GetFullPathName() does not recognize \??\ and will return it as C:\??\ (or whatever the current drive is). int rootLength = PathInternal.GetRootLength(outputBuilder.AsSpan()); bool isDevice = PathInternal.IsDevice(outputBuilder.AsSpan()); // As this is a corner case we're not going to add a stackalloc here to keep the stack pressure down. var inputBuilder = new ValueStringBuilder(); bool isDosUnc = false; int rootDifference = 0; bool wasDotDevice = false; // Add the extended prefix before expanding to allow growth over MAX_PATH if (isDevice) { // We have one of the following (\\?\ or \\.\) inputBuilder.Append(outputBuilder.AsSpan()); if (outputBuilder[2] == '.') { wasDotDevice = true; inputBuilder[2] = '?'; } } else { isDosUnc = !PathInternal.IsDevice(outputBuilder.AsSpan()) && outputBuilder.Length > 1 && outputBuilder[0] == '\\' && outputBuilder[1] == '\\'; rootDifference = PrependDevicePathChars(ref outputBuilder, isDosUnc, ref inputBuilder); } rootLength += rootDifference; int inputLength = inputBuilder.Length; bool success = false; int foundIndex = inputBuilder.Length - 1; while (!success) { uint result = Interop.Kernel32.GetLongPathNameW( ref inputBuilder.GetPinnableReference(terminate: true), ref outputBuilder.GetPinnableReference(), (uint)outputBuilder.Capacity); // Replace any temporary null we added if (inputBuilder[foundIndex] == '\0') inputBuilder[foundIndex] = '\\'; if (result == 0) { // Look to see if we couldn't find the file int error = Marshal.GetLastWin32Error(); if (error != Interop.Errors.ERROR_FILE_NOT_FOUND && error != Interop.Errors.ERROR_PATH_NOT_FOUND) { // Some other failure, give up break; } // We couldn't find the path at the given index, start looking further back in the string. foundIndex--; for (; foundIndex > rootLength && inputBuilder[foundIndex] != '\\'; foundIndex--) ; if (foundIndex == rootLength) { // Can't trim the path back any further break; } else { // Temporarily set a null in the string to get Windows to look further up the path inputBuilder[foundIndex] = '\0'; } } else if (result > outputBuilder.Capacity) { // Not enough space. The result count for this API does not include the null terminator. outputBuilder.EnsureCapacity(checked((int)result)); result = Interop.Kernel32.GetLongPathNameW(ref inputBuilder.GetPinnableReference(), ref outputBuilder.GetPinnableReference(), (uint)outputBuilder.Capacity); } else { // Found the path success = true; outputBuilder.Length = checked((int)result); if (foundIndex < inputLength - 1) { // It was a partial find, put the non-existent part of the path back outputBuilder.Append(inputBuilder.AsSpan(foundIndex, inputBuilder.Length - foundIndex)); } } } // If we were able to expand the path, use it, otherwise use the original full path result ref ValueStringBuilder builderToUse = ref (success ? ref outputBuilder : ref inputBuilder); // Switch back from \\?\ to \\.\ if necessary if (wasDotDevice) builderToUse[2] = '.'; // Change from \\?\UNC\ to \\?\UN\\ if needed if (isDosUnc) builderToUse[PathInternal.UncExtendedPrefixLength - PathInternal.UncPrefixLength] = '\\'; // Strip out any added characters at the front of the string ReadOnlySpan output = builderToUse.AsSpan(rootDifference); string returnValue = ((originalPath != null) && output.Equals(originalPath.AsSpan(), StringComparison.Ordinal)) ? originalPath : output.ToString(); inputBuilder.Dispose(); return returnValue; } } }