Collapse AsSpan().Slice(...) into .AsSpan(...) (dotnet/corefx#27867)
[platform/upstream/coreclr.git] / src / mscorlib / shared / System / IO / PathHelper.Windows.cs
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.
4
5 using System.Diagnostics;
6 using System.Runtime.InteropServices;
7 using System.Text;
8
9 namespace System.IO
10 {
11     /// <summary>
12     /// Wrapper to help with path normalization.
13     /// </summary>
14     internal class PathHelper
15     {
16         /// <summary>
17         /// Normalize the given path.
18         /// </summary>
19         /// <remarks>
20         /// Normalizes via Win32 GetFullPathName().
21         /// </remarks>
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)
27         {
28             Span<char> initialBuffer = stackalloc char[PathInternal.MaxShortPath];
29             ValueStringBuilder builder = new ValueStringBuilder(initialBuffer);
30
31             // Get the full path
32             GetFullPathName(path, ref builder);
33
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();
39
40             // Clear the buffer
41             builder.Dispose();
42             return result;
43         }
44
45         private static void GetFullPathName(string path, ref ValueStringBuilder builder)
46         {
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));
50
51             uint result = 0;
52             while ((result = Interop.Kernel32.GetFullPathNameW(path, (uint)builder.Capacity, ref builder.GetPinnableReference(), IntPtr.Zero)) > builder.Capacity)
53             {
54                 // Reported size is greater than the buffer size. Increase the capacity.
55                 builder.EnsureCapacity(checked((int)result));
56             }
57
58             if (result == 0)
59             {
60                 // Failure, get the error and throw
61                 int errorCode = Marshal.GetLastWin32Error();
62                 if (errorCode == 0)
63                     errorCode = Interop.Errors.ERROR_BAD_PATHNAME;
64                 throw Win32Marshal.GetExceptionForWin32Error(errorCode, path);
65             }
66
67             builder.Length = (int)result;
68         }
69
70         private static int PrependDevicePathChars(ref ValueStringBuilder content, bool isDosUnc, ref ValueStringBuilder buffer)
71         {
72             int length = content.Length;
73
74             length += isDosUnc
75                 ? PathInternal.UncExtendedPrefixLength - PathInternal.UncPrefixLength
76                 : PathInternal.DevicePrefixLength;
77
78             buffer.EnsureCapacity(length + 1);
79             buffer.Length = 0;
80
81             if (isDosUnc)
82             {
83                 // Is a \\Server\Share, put \\?\UNC\ in the front
84                 buffer.Append(PathInternal.UncExtendedPathPrefix);
85
86                 // Copy Server\Share\... over to the buffer
87                 buffer.Append(content.AsSpan(PathInternal.UncPrefixLength));
88
89                 // Return the prefix difference
90                 return PathInternal.UncExtendedPrefixLength - PathInternal.UncPrefixLength;
91             }
92             else
93             {
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;
98             }
99         }
100
101         private static string TryExpandShortFileName(ref ValueStringBuilder outputBuilder, string originalPath)
102         {
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.
105
106             Debug.Assert(!PathInternal.IsPartiallyQualified(outputBuilder.AsSpan()), "should have resolved by now");
107
108             // We'll have one of a few cases by now (the normalized path will have already:
109             //
110             //  1. Dos path (C:\)
111             //  2. Dos UNC (\\Server\Share)
112             //  3. Dos device path (\\.\C:\, \\?\C:\)
113             //
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 \\.\.
115             //
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).
117
118             int rootLength = PathInternal.GetRootLength(outputBuilder.AsSpan());
119             bool isDevice = PathInternal.IsDevice(outputBuilder.AsSpan());
120
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();
123
124             bool isDosUnc = false;
125             int rootDifference = 0;
126             bool wasDotDevice = false;
127
128             // Add the extended prefix before expanding to allow growth over MAX_PATH
129             if (isDevice)
130             {
131                 // We have one of the following (\\?\ or \\.\)
132                 inputBuilder.Append(outputBuilder.AsSpan());
133
134                 if (outputBuilder[2] == '.')
135                 {
136                     wasDotDevice = true;
137                     inputBuilder[2] = '?';
138                 }
139             }
140             else
141             {
142                 isDosUnc = !PathInternal.IsDevice(outputBuilder.AsSpan()) && outputBuilder.Length > 1 && outputBuilder[0] == '\\' && outputBuilder[1] == '\\';
143                 rootDifference = PrependDevicePathChars(ref outputBuilder, isDosUnc, ref inputBuilder);
144             }
145
146             rootLength += rootDifference;
147             int inputLength = inputBuilder.Length;
148
149             bool success = false;
150             int foundIndex = inputBuilder.Length - 1;
151
152             // Need to null terminate the input builder
153             inputBuilder.Append('\0');
154
155             while (!success)
156             {
157                 uint result = Interop.Kernel32.GetLongPathNameW(ref inputBuilder.GetPinnableReference(), ref outputBuilder.GetPinnableReference(), (uint)outputBuilder.Capacity);
158
159                 // Replace any temporary null we added
160                 if (inputBuilder[foundIndex] == '\0') inputBuilder[foundIndex] = '\\';
161
162                 if (result == 0)
163                 {
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)
167                     {
168                         // Some other failure, give up
169                         break;
170                     }
171
172                     // We couldn't find the path at the given index, start looking further back in the string.
173                     foundIndex--;
174
175                     for (; foundIndex > rootLength && inputBuilder[foundIndex] != '\\'; foundIndex--) ;
176                     if (foundIndex == rootLength)
177                     {
178                         // Can't trim the path back any further
179                         break;
180                     }
181                     else
182                     {
183                         // Temporarily set a null in the string to get Windows to look further up the path
184                         inputBuilder[foundIndex] = '\0';
185                     }
186                 }
187                 else if (result > outputBuilder.Capacity)
188                 {
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);
192                 }
193                 else
194                 {
195                     // Found the path
196                     success = true;
197                     outputBuilder.Length = checked((int)result);
198                     if (foundIndex < inputLength - 1)
199                     {
200                         // It was a partial find, put the non-existent part of the path back
201                         outputBuilder.Append(inputBuilder.AsSpan(foundIndex, inputBuilder.Length - foundIndex));
202                     }
203                 }
204             }
205
206             // Need to trim out the trailing separator in the input builder
207             inputBuilder.Length = inputBuilder.Length - 1;
208
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);
211
212             // Switch back from \\?\ to \\.\ if necessary
213             if (wasDotDevice)
214                 builderToUse[2] = '.';
215
216             // Change from \\?\UNC\ to \\?\UN\\ if needed
217             if (isDosUnc)
218                 builderToUse[PathInternal.UncExtendedPrefixLength - PathInternal.UncPrefixLength] = '\\';
219
220             // Strip out any added characters at the front of the string
221             ReadOnlySpan<char> output = builderToUse.AsSpan(rootDifference);
222
223             string returnValue = output.EqualsOrdinal(originalPath.AsSpan())
224                 ? originalPath : new string(output);
225
226             inputBuilder.Dispose();
227             return returnValue;
228         }
229     }
230 }