Updates referencesource to .NET 4.7
[mono.git] / mcs / class / referencesource / mscorlib / system / io / pathinternal.cs
1 // Copyright (c) Microsoft. All rights reserved.
2 // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
4 using Microsoft.Win32;
5 using System;
6 using System.Diagnostics.Contracts;
7 using System.Runtime.CompilerServices;
8 using System.Runtime.InteropServices;
9 using System.Security;
10 using System.Text;
11
12 namespace System.IO
13 {
14     /// <summary>Contains internal path helpers that are shared between many projects.</summary>
15     internal static class PathInternal
16     {
17         internal const string ExtendedPathPrefix = @"\\?\";
18         internal const string UncPathPrefix = @"\\";
19         internal const string UncExtendedPrefixToInsert = @"?\UNC\";
20         internal const string UncExtendedPathPrefix = @"\\?\UNC\";
21         internal const string DevicePathPrefix = @"\\.\";
22         internal const int DevicePrefixLength = 4;
23         internal const int MaxShortPath = 260;
24         internal const int MaxShortDirectoryPath = 248;
25
26         // Windows is limited in long paths by the max size of its internal representation of a unicode string.
27         // UNICODE_STRING has a max length of USHORT in _bytes_ without a trailing null.
28         // https://msdn.microsoft.com/en-us/library/windows/hardware/ff564879.aspx
29         internal const int MaxLongPath = short.MaxValue;
30         internal static readonly int MaxComponentLength = 255;
31
32         internal static readonly char[] InvalidPathChars =
33         {
34             '\"', '<', '>', '|', '\0',
35             (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
36             (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
37             (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
38             (char)31
39         };
40
41         /// <summary>
42         /// Validates volume separator only occurs as C: or \\?\C:. This logic is meant to filter out Alternate Data Streams.
43         /// </summary>
44         /// <returns>True if the path has an invalid volume separator.</returns>
45         internal static bool HasInvalidVolumeSeparator(string path)
46         {
47             // Toss out paths with colons that aren't a valid drive specifier.
48             // Cannot start with a colon and can only be of the form "C:" or "\\?\C:".
49             // (Note that we used to explicitly check "http:" and "file:"- these are caught by this check now.)
50
51             // We don't care about skipping starting space for extended paths. Assume no knowledge of extended paths if we're forcing old path behavior.
52             bool isExtended = !AppContextSwitches.UseLegacyPathHandling && IsExtended(path);
53             int startIndex = isExtended ? ExtendedPathPrefix.Length : PathStartSkip(path);
54
55             // If we start with a colon
56             if ((path.Length > startIndex && path[startIndex] == Path.VolumeSeparatorChar)
57                 // Or have an invalid drive letter and colon
58                 || (path.Length >= startIndex + 2 && path[startIndex + 1] == Path.VolumeSeparatorChar && !IsValidDriveChar(path[startIndex]))
59                 // Or have any colons beyond the drive colon
60                 || (path.Length > startIndex + 2 && path.IndexOf(Path.VolumeSeparatorChar, startIndex + 2) != -1))
61             {
62                 return true;
63             }
64
65             return false;
66         }
67
68         /// <summary>
69         /// Returns true if the given StringBuilder starts with the given value.
70         /// </summary>
71         /// <param name="value">The string to compare against the start of the StringBuilder.</param>
72         internal static bool StartsWithOrdinal(StringBuilder builder, string value, bool ignoreCase = false)
73         {
74             if (value == null || builder.Length < value.Length)
75                 return false;
76
77             if (ignoreCase)
78             {
79                 for (int i = 0; i < value.Length; i++)
80                     if (char.ToUpperInvariant(builder[i]) != char.ToUpperInvariant(value[i])) return false;
81             }
82             else
83             {
84                 for (int i = 0; i < value.Length; i++)
85                     if (builder[i] != value[i]) return false;
86             }
87
88             return true;
89         }
90
91         /// <summary>
92         /// Returns true if the given character is a valid drive letter
93         /// </summary>
94         internal static bool IsValidDriveChar(char value)
95         {
96             return ((value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z'));
97         }
98
99         /// <summary>
100         /// Returns true if the path is too long
101         /// </summary>
102         internal static bool IsPathTooLong(string fullPath)
103         {
104             // We'll never know precisely what will fail as paths get changed internally in Windows and
105             // may grow beyond / shrink below exceed MaxLongPath.
106             if (AppContextSwitches.BlockLongPaths)
107             {
108                 // We allow paths of any length if extended (and not in compat mode)
109                 if (AppContextSwitches.UseLegacyPathHandling || !IsExtended(fullPath))
110                     return fullPath.Length >= MaxShortPath;
111             }
112
113             return fullPath.Length >= MaxLongPath;
114         }
115
116         /// <summary>
117         /// Return true if any path segments are too long
118         /// </summary>
119         internal static bool AreSegmentsTooLong(string fullPath)
120         {
121             int length = fullPath.Length;
122             int lastSeparator = 0;
123
124             for (int i = 0; i < length; i++)
125             {
126                 if (IsDirectorySeparator(fullPath[i]))
127                 {
128                     if (i - lastSeparator > MaxComponentLength)
129                         return true;
130                     lastSeparator = i;
131                 }
132             }
133
134             if (length - 1 - lastSeparator > MaxComponentLength)
135                 return true;
136
137             return false;
138         }
139
140         /// <summary>
141         /// Returns true if the directory is too long
142         /// </summary>
143         internal static bool IsDirectoryTooLong(string fullPath)
144         {
145             if (AppContextSwitches.BlockLongPaths)
146             {
147                 // We allow paths of any length if extended (and not in compat mode)
148                 if (AppContextSwitches.UseLegacyPathHandling || !IsExtended(fullPath))
149                     return (fullPath.Length >= MaxShortDirectoryPath);
150             }
151
152             return IsPathTooLong(fullPath);
153         }
154
155         /// <summary>
156         /// Adds the extended path prefix (\\?\) if not relative or already a device path.
157         /// </summary>
158         internal static string EnsureExtendedPrefix(string path)
159         {
160             // Putting the extended prefix on the path changes the processing of the path. It won't get normalized, which
161             // means adding to relative paths will prevent them from getting the appropriate current directory inserted.
162
163             // If it already has some variant of a device path (\??\, \\?\, \\.\, //./, etc.) we don't need to change it
164             // as it is either correct or we will be changing the behavior. When/if Windows supports long paths implicitly
165             // in the future we wouldn't want normalization to come back and break existing code.
166
167             // In any case, all internal usages should be hitting normalize path (Path.GetFullPath) before they hit this
168             // shimming method. (Or making a change that doesn't impact normalization, such as adding a filename to a
169             // normalized base path.)
170             if (IsPartiallyQualified(path) || IsDevice(path))
171                 return path;
172
173             // Given \\server\share in longpath becomes \\?\UNC\server\share
174             if (path.StartsWith(UncPathPrefix, StringComparison.OrdinalIgnoreCase))
175                 return path.Insert(2, UncExtendedPrefixToInsert);
176
177             return ExtendedPathPrefix + path;
178         }
179
180         /// <summary>
181         /// Removes the extended path prefix (\\?\) if present.
182         /// </summary>
183         internal static string RemoveExtendedPrefix(string path)
184         {
185             if (!IsExtended(path))
186                 return path;
187
188             // Given \\?\UNC\server\share we return \\server\share
189             if (IsExtendedUnc(path))
190                 return path.Remove(2, 6);
191
192             return path.Substring(DevicePrefixLength);
193         }
194
195         /// <summary>
196         /// Removes the extended path prefix (\\?\) if present.
197         /// </summary>
198         internal static StringBuilder RemoveExtendedPrefix(StringBuilder path)
199         {
200             if (!IsExtended(path))
201                 return path;
202
203             // Given \\?\UNC\server\share we return \\server\share
204             if (IsExtendedUnc(path))
205                 return path.Remove(2, 6);
206
207             return path.Remove(0, DevicePrefixLength);
208         }
209
210         /// <summary>
211         /// Returns true if the path uses any of the DOS device path syntaxes. ("\\.\", "\\?\", or "\??\")
212         /// </summary>
213         internal static bool IsDevice(string path)
214         {
215             // If the path begins with any two separators it will be recognized and normalized and prepped with
216             // "\??\" for internal usage correctly. "\??\" is recognized and handled, "/??/" is not.
217             return IsExtended(path)
218                 ||
219                 (
220                     path.Length >= DevicePrefixLength
221                     && IsDirectorySeparator(path[0])
222                     && IsDirectorySeparator(path[1])
223                     && (path[2] == '.' || path[2] == '?')
224                     && IsDirectorySeparator(path[3])
225                 );
226         }
227
228         /// <summary>
229         /// Returns true if the path uses any of the DOS device path syntaxes. ("\\.\", "\\?\", or "\??\")
230         /// </summary>
231         internal static bool IsDevice(StringBuffer path)
232         {
233             // If the path begins with any two separators it will be recognized and normalized and prepped with
234             // "\??\" for internal usage correctly. "\??\" is recognized and handled, "/??/" is not.
235             return IsExtended(path)
236                 ||
237                 (
238                     path.Length >= DevicePrefixLength
239                     && IsDirectorySeparator(path[0])
240                     && IsDirectorySeparator(path[1])
241                     && (path[2] == '.' || path[2] == '?')
242                     && IsDirectorySeparator(path[3])
243                 );
244         }
245
246         /// <summary>
247         /// Returns true if the path uses the canonical form of extended syntax ("\\?\" or "\??\"). If the
248         /// path matches exactly (cannot use alternate directory separators) Windows will skip normalization
249         /// and path length checks.
250         /// </summary>
251         internal static bool IsExtended(string path)
252         {
253             // While paths like "//?/C:/" will work, they're treated the same as "\\.\" paths.
254             // Skipping of normalization will *only* occur if back slashes ('\') are used.
255             return path.Length >= DevicePrefixLength
256                 && path[0] == '\\'
257                 && (path[1] == '\\' || path[1] == '?')
258                 && path[2] == '?'
259                 && path[3] == '\\';
260         }
261
262         /// <summary>
263         /// Returns true if the path uses the canonical form of extended syntax ("\\?\" or "\??\"). If the
264         /// path matches exactly (cannot use alternate directory separators) Windows will skip normalization
265         /// and path length checks.
266         /// </summary>
267         internal static bool IsExtended(StringBuilder path)
268         {
269             // While paths like "//?/C:/" will work, they're treated the same as "\\.\" paths.
270             // Skipping of normalization will *only* occur if back slashes ('\') are used.
271             return path.Length >= DevicePrefixLength
272                 && path[0] == '\\'
273                 && (path[1] == '\\' || path[1] == '?')
274                 && path[2] == '?'
275                 && path[3] == '\\';
276         }
277
278         /// <summary>
279         /// Returns true if the path uses the canonical form of extended syntax ("\\?\" or "\??\"). If the
280         /// path matches exactly (cannot use alternate directory separators) Windows will skip normalization
281         /// and path length checks.
282         /// </summary>
283         internal static bool IsExtended(StringBuffer path)
284         {
285             // While paths like "//?/C:/" will work, they're treated the same as "\\.\" paths.
286             // Skipping of normalization will *only* occur if back slashes ('\') are used.
287             return path.Length >= DevicePrefixLength
288                 && path[0] == '\\'
289                 && (path[1] == '\\' || path[1] == '?')
290                 && path[2] == '?'
291                 && path[3] == '\\';
292         }
293
294         /// <summary>
295         /// Returns true if the path uses the extended UNC syntax (\\?\UNC\ or \??\UNC\)
296         /// </summary>
297         internal static bool IsExtendedUnc(string path)
298         {
299             return path.Length >= UncExtendedPathPrefix.Length
300                 && IsExtended(path)
301                 && char.ToUpper(path[4]) == 'U'
302                 && char.ToUpper(path[5]) == 'N'
303                 && char.ToUpper(path[6]) == 'C'
304                 && path[7] == '\\';
305         }
306
307         /// <summary>
308         /// Returns true if the path uses the extended UNC syntax (\\?\UNC\ or \??\UNC\)
309         /// </summary>
310         internal static bool IsExtendedUnc(StringBuilder path)
311         {
312             return path.Length >= UncExtendedPathPrefix.Length
313                 && IsExtended(path)
314                 && char.ToUpper(path[4]) == 'U'
315                 && char.ToUpper(path[5]) == 'N'
316                 && char.ToUpper(path[6]) == 'C'
317                 && path[7] == '\\';
318         }
319
320         /// <summary>
321         /// Returns a value indicating if the given path contains invalid characters (", &lt;, &gt;, | 
322         /// NUL, or any ASCII char whose integer representation is in the range of 1 through 31).
323         /// Does not check for wild card characters ? and *.
324         ///
325         /// Will not check if the path is a device path and not in Legacy mode as many of these
326         /// characters are valid for devices (pipes for example).
327         /// </summary>
328         internal static bool HasIllegalCharacters(string path, bool checkAdditional = false)
329         {
330             if (!AppContextSwitches.UseLegacyPathHandling && IsDevice(path))
331             {
332                 return false;
333             }
334
335             return AnyPathHasIllegalCharacters(path, checkAdditional: checkAdditional);
336         }
337
338         /// <summary>
339         /// Version of HasIllegalCharacters that checks no AppContextSwitches. Only use if you know you need to skip switches and don't care
340         /// about proper device path handling.
341         /// </summary>
342         internal static bool AnyPathHasIllegalCharacters(string path, bool checkAdditional = false)
343         {
344             return path.IndexOfAny(InvalidPathChars) >= 0 || (checkAdditional && AnyPathHasWildCardCharacters(path));
345         }
346
347         /// <summary>
348         /// Check for ? and *.
349         /// </summary>
350         internal static bool HasWildCardCharacters(string path)
351         {
352             // Question mark is part of some device paths
353             int startIndex = AppContextSwitches.UseLegacyPathHandling ? 0 : IsDevice(path) ? ExtendedPathPrefix.Length : 0;
354             return AnyPathHasWildCardCharacters(path, startIndex: startIndex);
355         }
356
357         /// <summary>
358         /// Version of HasWildCardCharacters that checks no AppContextSwitches. Only use if you know you need to skip switches and don't care
359         /// about proper device path handling.
360         /// </summary>
361         internal static bool AnyPathHasWildCardCharacters(string path, int startIndex = 0)
362         {
363             char currentChar;
364             for (int i = startIndex; i < path.Length; i++)
365             {
366                 currentChar = path[i];
367                 if (currentChar == '*' || currentChar == '?') return true;
368             }
369             return false;
370         }
371
372         /// <summary>
373         /// Gets the length of the root of the path (drive, share, etc.).
374         /// </summary>
375         [System.Security.SecuritySafeCritical]
376         internal unsafe static int GetRootLength(string path)
377         {
378             fixed (char* value = path)
379             {
380                 return (int)GetRootLength(value, (ulong)path.Length);
381             }
382         }
383
384         /// <summary>
385         /// Gets the length of the root of the path (drive, share, etc.).
386         /// </summary>
387         [System.Security.SecuritySafeCritical]
388         internal unsafe static uint GetRootLength(StringBuffer path)
389         {
390             if (path.Length == 0) return 0;
391             return GetRootLength(path.CharPointer, path.Length);
392         }
393
394         [System.Security.SecurityCritical]
395         private unsafe static uint GetRootLength(char* path, ulong pathLength)
396         {
397             uint i = 0;
398             uint volumeSeparatorLength = 2;  // Length to the colon "C:"
399             uint uncRootLength = 2;          // Length to the start of the server name "\\"
400
401             bool extendedSyntax = StartsWithOrdinal(path, pathLength, ExtendedPathPrefix);
402             bool extendedUncSyntax = StartsWithOrdinal(path, pathLength, UncExtendedPathPrefix);
403             if (extendedSyntax)
404             {
405                 // Shift the position we look for the root from to account for the extended prefix
406                 if (extendedUncSyntax)
407                 {
408                     // "\\" -> "\\?\UNC\"
409                     uncRootLength = (uint)UncExtendedPathPrefix.Length;
410                 }
411                 else
412                 {
413                     // "C:" -> "\\?\C:"
414                     volumeSeparatorLength += (uint)ExtendedPathPrefix.Length;
415                 }
416             }
417
418             if ((!extendedSyntax || extendedUncSyntax) && pathLength > 0 && IsDirectorySeparator(path[0]))
419             {
420                 // UNC or simple rooted path (e.g. "\foo", NOT "\\?\C:\foo")
421
422                 i = 1; //  Drive rooted (\foo) is one character
423                 if (extendedUncSyntax || (pathLength > 1 && IsDirectorySeparator(path[1])))
424                 {
425                     // UNC (\\?\UNC\ or \\), scan past the next two directory separators at most
426                     // (e.g. to \\?\UNC\Server\Share or \\Server\Share\)
427                     i = uncRootLength;
428                     int n = 2; // Maximum separators to skip
429                     while (i < pathLength && (!IsDirectorySeparator(path[i]) || --n > 0)) i++;
430                 }
431             }
432             else if (pathLength >= volumeSeparatorLength && path[volumeSeparatorLength - 1] == Path.VolumeSeparatorChar)
433             {
434                 // Path is at least longer than where we expect a colon, and has a colon (\\?\A:, A:)
435                 // If the colon is followed by a directory separator, move past it
436                 i = volumeSeparatorLength;
437                 if (pathLength >= volumeSeparatorLength + 1 && IsDirectorySeparator(path[volumeSeparatorLength])) i++;
438             }
439             return i;
440         }
441
442         [System.Security.SecurityCritical]
443         private unsafe static bool StartsWithOrdinal(char* source, ulong sourceLength, string value)
444         {
445             if (sourceLength < (ulong)value.Length) return false;
446             for (int i = 0; i < value.Length; i++)
447             {
448                 if (value[i] != source[i]) return false;
449             }
450             return true;
451         }
452
453         /// <summary>
454         /// Returns true if the path specified is relative to the current drive or working directory.
455         /// Returns false if the path is fixed to a specific drive or UNC path.  This method does no
456         /// validation of the path (URIs will be returned as relative as a result).
457         /// </summary>
458         /// <remarks>
459         /// Handles paths that use the alternate directory separator.  It is a frequent mistake to
460         /// assume that rooted paths (Path.IsPathRooted) are not relative.  This isn't the case.
461         /// "C:a" is drive relative- meaning that it will be resolved against the current directory
462         /// for C: (rooted, but relative). "C:\a" is rooted and not relative (the current directory
463         /// will not be used to modify the path).
464         /// </remarks>
465         internal static bool IsPartiallyQualified(string path)
466         {
467             if (path.Length < 2)
468             {
469                 // It isn't fixed, it must be relative.  There is no way to specify a fixed
470                 // path with one character (or less).
471                 return true;
472             }
473
474             if (IsDirectorySeparator(path[0]))
475             {
476                 // There is no valid way to specify a relative path with two initial slashes or
477                 // \? as ? isn't valid for drive relative paths and \??\ is equivalent to \\?\
478                 return !(path[1] == '?' || IsDirectorySeparator(path[1]));
479             }
480
481             // The only way to specify a fixed path that doesn't begin with two slashes
482             // is the drive, colon, slash format- i.e. C:\
483             return !((path.Length >= 3)
484                 && (path[1] == Path.VolumeSeparatorChar)
485                 && IsDirectorySeparator(path[2])
486                 // To match old behavior we'll check the drive character for validity as the path is technically
487                 // not qualified if you don't have a valid drive. "=:\" is the "=" file's default data stream.
488                 && IsValidDriveChar(path[0]));
489         }
490
491         /// <summary>
492         /// Returns true if the path specified is relative to the current drive or working directory.
493         /// Returns false if the path is fixed to a specific drive or UNC path.  This method does no
494         /// validation of the path (URIs will be returned as relative as a result).
495         /// </summary>
496         /// <remarks>
497         /// Handles paths that use the alternate directory separator.  It is a frequent mistake to
498         /// assume that rooted paths (Path.IsPathRooted) are not relative.  This isn't the case.
499         /// "C:a" is drive relative- meaning that it will be resolved against the current directory
500         /// for C: (rooted, but relative). "C:\a" is rooted and not relative (the current directory
501         /// will not be used to modify the path).
502         /// </remarks>
503         internal static bool IsPartiallyQualified(StringBuffer path)
504         {
505             if (path.Length < 2)
506             {
507                 // It isn't fixed, it must be relative.  There is no way to specify a fixed
508                 // path with one character (or less).
509                 return true;
510             }
511
512             if (IsDirectorySeparator(path[0]))
513             {
514                 // There is no valid way to specify a relative path with two initial slashes or
515                 // \? as ? isn't valid for drive relative paths and \??\ is equivalent to \\?\
516                 return !(path[1] == '?' || IsDirectorySeparator(path[1]));
517             }
518
519             // The only way to specify a fixed path that doesn't begin with two slashes
520             // is the drive, colon, slash format- i.e. C:\
521             return !((path.Length >= 3)
522                 && (path[1] == Path.VolumeSeparatorChar)
523                 && IsDirectorySeparator(path[2])
524                 // To match old behavior we'll check the drive character for validity as the path is technically
525                 // not qualified if you don't have a valid drive. "=:\" is the "=" file's default data stream.
526                 && IsValidDriveChar(path[0]));
527         }
528
529         /// <summary>
530         /// Returns the characters to skip at the start of the path if it starts with space(s) and a drive or directory separator.
531         /// (examples are " C:", " \")
532         /// This is a legacy behavior of Path.GetFullPath().
533         /// </summary>
534         /// <remarks>
535         /// Note that this conflicts with IsPathRooted() which doesn't (and never did) such a skip.
536         /// </remarks>
537         internal static int PathStartSkip(string path)
538         {
539             int startIndex = 0;
540             while (startIndex < path.Length && path[startIndex] == ' ') startIndex++;
541
542             if (startIndex > 0 && (startIndex < path.Length && IsDirectorySeparator(path[startIndex]))
543                 || (startIndex + 1 < path.Length && path[startIndex + 1] == Path.VolumeSeparatorChar && IsValidDriveChar(path[startIndex])))
544             {
545                 // Go ahead and skip spaces as we're either " C:" or " \"
546                 return startIndex;
547             }
548
549             return 0;
550         }
551
552         /// <summary>
553         /// True if the given character is a directory separator.
554         /// </summary>
555         [MethodImpl(MethodImplOptions.AggressiveInlining)]
556         internal static bool IsDirectorySeparator(char c)
557         {
558             return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar;
559         }
560
561         /// <summary>
562         /// Normalize separators in the given path. Converts forward slashes into back slashes and compresses slash runs, keeping initial 2 if present.
563         /// Also trims initial whitespace in front of "rooted" paths (see PathStartSkip).
564         /// 
565         /// This effectively replicates the behavior of the legacy NormalizePath when it was called with fullCheck=false and expandShortpaths=false.
566         /// The current NormalizePath gets directory separator normalization from Win32's GetFullPathName(), which will resolve relative paths and as
567         /// such can't be used here (and is overkill for our uses).
568         /// 
569         /// Like the current NormalizePath this will not try and analyze periods/spaces within directory segments.
570         /// </summary>
571         /// <remarks>
572         /// The only callers that used to use Path.Normalize(fullCheck=false) were Path.GetDirectoryName() and Path.GetPathRoot(). Both usages do
573         /// not need trimming of trailing whitespace here.
574         /// 
575         /// GetPathRoot() could technically skip normalizing separators after the second segment- consider as a future optimization.
576         /// 
577         /// For legacy desktop behavior with ExpandShortPaths:
578         ///  - It has no impact on GetPathRoot() so doesn't need consideration.
579         ///  - It could impact GetDirectoryName(), but only if the path isn't relative (C:\ or \\Server\Share).
580         /// 
581         /// In the case of GetDirectoryName() the ExpandShortPaths behavior was undocumented and provided inconsistent results if the path was
582         /// fixed/relative. For example: "C:\PROGRA~1\A.TXT" would return "C:\Program Files" while ".\PROGRA~1\A.TXT" would return ".\PROGRA~1". If you
583         /// ultimately call GetFullPath() this doesn't matter, but if you don't or have any intermediate string handling could easily be tripped up by
584         /// this undocumented behavior.
585         /// </remarks>
586         internal static string NormalizeDirectorySeparators(string path)
587         {
588             if (string.IsNullOrEmpty(path)) return path;
589
590             char current;
591             int start = PathStartSkip(path);
592
593             if (start == 0)
594             {
595                 // Make a pass to see if we need to normalize so we can potentially skip allocating
596                 bool normalized = true;
597
598                 for (int i = 0; i < path.Length; i++)
599                 {
600                     current = path[i];
601                     if (IsDirectorySeparator(current)
602                         && (current != Path.DirectorySeparatorChar
603                             // Check for sequential separators past the first position (we need to keep initial two for UNC/extended)
604                             || (i > 0 && i + 1 < path.Length && IsDirectorySeparator(path[i + 1]))))
605                     {
606                         normalized = false;
607                         break;
608                     }
609                 }
610
611                 if (normalized) return path;
612             }
613
614             StringBuilder builder = StringBuilderCache.Acquire(path.Length);
615
616             if (IsDirectorySeparator(path[start]))
617             {
618                 start++;
619                 builder.Append(Path.DirectorySeparatorChar);
620             }
621
622             for (int i = start; i < path.Length; i++)
623             {
624                 current = path[i];
625
626                 // If we have a separator
627                 if (IsDirectorySeparator(current))
628                 {
629                     // If the next is a separator, skip adding this
630                     if (i + 1 < path.Length && IsDirectorySeparator(path[i + 1]))
631                     {
632                         continue;
633                     }
634
635                     // Ensure it is the primary separator
636                     current = Path.DirectorySeparatorChar;
637                 }
638
639                 builder.Append(current);
640             }
641
642             return StringBuilderCache.GetStringAndRelease(builder);
643         }
644     }
645 }