1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
5 using System.Diagnostics;
6 using System.Runtime.InteropServices;
12 /// Wrapper to help with path normalization.
14 internal class PathHelper
17 /// Normalize the given path.
20 /// Normalizes via Win32 GetFullPathName().
22 /// <param name="path">Path to normalize</param>
23 /// <exception cref="PathTooLongException">Thrown if we have a string that is too large to fit into a UNICODE_STRING.</exception>
24 /// <exception cref="IOException">Thrown if the path is empty.</exception>
25 /// <returns>Normalized path</returns>
26 internal static string Normalize(string path)
28 Span<char> initialBuffer = stackalloc char[PathInternal.MaxShortPath];
29 ValueStringBuilder builder = new ValueStringBuilder(initialBuffer);
32 GetFullPathName(path, ref builder);
34 // If we have the exact same string we were passed in, don't allocate another string.
35 // TryExpandShortName does this input identity check.
36 string result = builder.AsSpan().Contains('~')
37 ? TryExpandShortFileName(ref builder, originalPath: path)
38 : builder.AsSpan().EqualsOrdinal(path.AsSpan()) ? path : builder.ToString();
45 private static void GetFullPathName(string path, ref ValueStringBuilder builder)
47 // If the string starts with an extended prefix we would need to remove it from the path before we call GetFullPathName as
48 // it doesn't root extended paths correctly. We don't currently resolve extended paths, so we'll just assert here.
49 Debug.Assert(PathInternal.IsPartiallyQualified(path) || !PathInternal.IsExtended(path));
52 while ((result = Interop.Kernel32.GetFullPathNameW(path, (uint)builder.Capacity, ref builder.GetPinnableReference(), IntPtr.Zero)) > builder.Capacity)
54 // Reported size is greater than the buffer size. Increase the capacity.
55 builder.EnsureCapacity(checked((int)result));
60 // Failure, get the error and throw
61 int errorCode = Marshal.GetLastWin32Error();
63 errorCode = Interop.Errors.ERROR_BAD_PATHNAME;
64 throw Win32Marshal.GetExceptionForWin32Error(errorCode, path);
67 builder.Length = (int)result;
70 private static int PrependDevicePathChars(ref ValueStringBuilder content, bool isDosUnc, ref ValueStringBuilder buffer)
72 int length = content.Length;
75 ? PathInternal.UncExtendedPrefixLength - PathInternal.UncPrefixLength
76 : PathInternal.DevicePrefixLength;
78 buffer.EnsureCapacity(length + 1);
83 // Is a \\Server\Share, put \\?\UNC\ in the front
84 buffer.Append(PathInternal.UncExtendedPathPrefix);
86 // Copy Server\Share\... over to the buffer
87 buffer.Append(content.AsSpan(PathInternal.UncPrefixLength));
89 // Return the prefix difference
90 return PathInternal.UncExtendedPrefixLength - PathInternal.UncPrefixLength;
94 // Not an UNC, put the \\?\ prefix in front, then the original string
95 buffer.Append(PathInternal.ExtendedPathPrefix);
96 buffer.Append(content.AsSpan());
97 return PathInternal.DevicePrefixLength;
101 private static string TryExpandShortFileName(ref ValueStringBuilder outputBuilder, string originalPath)
103 // 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
104 // avoid allocating like crazy we'll create only one input array and modify the contents with embedded nulls.
106 Debug.Assert(!PathInternal.IsPartiallyQualified(outputBuilder.AsSpan()), "should have resolved by now");
108 // We'll have one of a few cases by now (the normalized path will have already:
111 // 2. Dos UNC (\\Server\Share)
112 // 3. Dos device path (\\.\C:\, \\?\C:\)
114 // 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 \\.\.
116 // Note that we will never get \??\ here as GetFullPathName() does not recognize \??\ and will return it as C:\??\ (or whatever the current drive is).
118 int rootLength = PathInternal.GetRootLength(outputBuilder.AsSpan());
119 bool isDevice = PathInternal.IsDevice(outputBuilder.AsSpan());
121 // As this is a corner case we're not going to add a stackalloc here to keep the stack pressure down.
122 ValueStringBuilder inputBuilder = new ValueStringBuilder();
124 bool isDosUnc = false;
125 int rootDifference = 0;
126 bool wasDotDevice = false;
128 // Add the extended prefix before expanding to allow growth over MAX_PATH
131 // We have one of the following (\\?\ or \\.\)
132 inputBuilder.Append(outputBuilder.AsSpan());
134 if (outputBuilder[2] == '.')
137 inputBuilder[2] = '?';
142 isDosUnc = !PathInternal.IsDevice(outputBuilder.AsSpan()) && outputBuilder.Length > 1 && outputBuilder[0] == '\\' && outputBuilder[1] == '\\';
143 rootDifference = PrependDevicePathChars(ref outputBuilder, isDosUnc, ref inputBuilder);
146 rootLength += rootDifference;
147 int inputLength = inputBuilder.Length;
149 bool success = false;
150 int foundIndex = inputBuilder.Length - 1;
152 // Need to null terminate the input builder
153 inputBuilder.Append('\0');
157 uint result = Interop.Kernel32.GetLongPathNameW(ref inputBuilder.GetPinnableReference(), ref outputBuilder.GetPinnableReference(), (uint)outputBuilder.Capacity);
159 // Replace any temporary null we added
160 if (inputBuilder[foundIndex] == '\0') inputBuilder[foundIndex] = '\\';
164 // Look to see if we couldn't find the file
165 int error = Marshal.GetLastWin32Error();
166 if (error != Interop.Errors.ERROR_FILE_NOT_FOUND && error != Interop.Errors.ERROR_PATH_NOT_FOUND)
168 // Some other failure, give up
172 // We couldn't find the path at the given index, start looking further back in the string.
175 for (; foundIndex > rootLength && inputBuilder[foundIndex] != '\\'; foundIndex--) ;
176 if (foundIndex == rootLength)
178 // Can't trim the path back any further
183 // Temporarily set a null in the string to get Windows to look further up the path
184 inputBuilder[foundIndex] = '\0';
187 else if (result > outputBuilder.Capacity)
189 // Not enough space. The result count for this API does not include the null terminator.
190 outputBuilder.EnsureCapacity(checked((int)result));
191 result = Interop.Kernel32.GetLongPathNameW(ref inputBuilder.GetPinnableReference(), ref outputBuilder.GetPinnableReference(), (uint)outputBuilder.Capacity);
197 outputBuilder.Length = checked((int)result);
198 if (foundIndex < inputLength - 1)
200 // It was a partial find, put the non-existent part of the path back
201 outputBuilder.Append(inputBuilder.AsSpan(foundIndex, inputBuilder.Length - foundIndex));
206 // Need to trim out the trailing separator in the input builder
207 inputBuilder.Length = inputBuilder.Length - 1;
209 // If we were able to expand the path, use it, otherwise use the original full path result
210 ref ValueStringBuilder builderToUse = ref (success ? ref outputBuilder : ref inputBuilder);
212 // Switch back from \\?\ to \\.\ if necessary
214 builderToUse[2] = '.';
216 // Change from \\?\UNC\ to \\?\UN\\ if needed
218 builderToUse[PathInternal.UncExtendedPrefixLength - PathInternal.UncPrefixLength] = '\\';
220 // Strip out any added characters at the front of the string
221 ReadOnlySpan<char> output = builderToUse.AsSpan(rootDifference);
223 string returnValue = output.EqualsOrdinal(originalPath.AsSpan())
224 ? originalPath : new string(output);
226 inputBuilder.Dispose();