AsReadOnlySpan -> AsSpan rename to fix build breaks
[platform/upstream/coreclr.git] / src / mscorlib / shared / System / IO / Path.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     // Provides methods for processing file system strings in a cross-platform manner.
12     // Most of the methods don't do a complete parsing (such as examining a UNC hostname), 
13     // but they will handle most string operations.
14     public static partial class Path
15     {
16         // Public static readonly variant of the separators. The Path implementation itself is using
17         // internal const variant of the separators for better performance.
18         public static readonly char DirectorySeparatorChar = PathInternal.DirectorySeparatorChar;
19         public static readonly char AltDirectorySeparatorChar = PathInternal.AltDirectorySeparatorChar;
20         public static readonly char VolumeSeparatorChar = PathInternal.VolumeSeparatorChar;
21         public static readonly char PathSeparator = PathInternal.PathSeparator;
22
23         // For generating random file names
24         // 8 random bytes provides 12 chars in our encoding for the 8.3 name.
25         private const int KeyLength = 8;
26
27         [Obsolete("Please use GetInvalidPathChars or GetInvalidFileNameChars instead.")]
28         public static readonly char[] InvalidPathChars = GetInvalidPathChars();
29
30         // Changes the extension of a file path. The path parameter
31         // specifies a file path, and the extension parameter
32         // specifies a file extension (with a leading period, such as
33         // ".exe" or ".cs").
34         //
35         // The function returns a file path with the same root, directory, and base
36         // name parts as path, but with the file extension changed to
37         // the specified extension. If path is null, the function
38         // returns null. If path does not contain a file extension,
39         // the new file extension is appended to the path. If extension
40         // is null, any existing extension is removed from path.
41         public static string ChangeExtension(string path, string extension)
42         {
43             if (path != null)
44             {
45                 string s = path;
46                 for (int i = path.Length - 1; i >= 0; i--)
47                 {
48                     char ch = path[i];
49                     if (ch == '.')
50                     {
51                         s = path.Substring(0, i);
52                         break;
53                     }
54                     if (PathInternal.IsDirectorySeparator(ch)) break;
55                 }
56
57                 if (extension != null && path.Length != 0)
58                 {
59                     s = (extension.Length == 0 || extension[0] != '.') ?
60                         s + "." + extension :
61                         s + extension;
62                 }
63
64                 return s;
65             }
66             return null;
67         }
68
69         /// <summary>
70         /// Returns the directory portion of a file path. This method effectively
71         /// removes the last segment of the given file path, i.e. it returns a
72         /// string consisting of all characters up to but not including the last
73         /// backslash ("\") in the file path. The returned value is null if the
74         /// specified path is null, empty, or a root (such as "\", "C:", or
75         /// "\\server\share").
76         /// </summary>
77         /// <remarks>
78         /// Directory separators are normalized in the returned string.
79         /// </remarks>
80         public static string GetDirectoryName(string path)
81         {
82             if (PathInternal.IsEffectivelyEmpty(path))
83                 return null;
84
85             int end = GetDirectoryNameOffset(path);
86             return end >= 0 ? PathInternal.NormalizeDirectorySeparators(path.Substring(0, end)) : null;
87         }
88
89         /// <summary>
90         /// Returns the directory portion of a file path. The returned value is empty
91         /// if the specified path is null, empty, or a root (such as "\", "C:", or
92         /// "\\server\share").
93         /// </summary>
94         /// <remarks>
95         /// Unlike the string overload, this method will not normalize directory separators.
96         /// </remarks>
97         public static ReadOnlySpan<char> GetDirectoryName(ReadOnlySpan<char> path)
98         {
99             if (PathInternal.IsEffectivelyEmpty(path))
100                 return ReadOnlySpan<char>.Empty;
101
102             int end = GetDirectoryNameOffset(path);
103             return end >= 0 ? path.Slice(0, end) : ReadOnlySpan<char>.Empty;
104         }
105
106         private static int GetDirectoryNameOffset(ReadOnlySpan<char> path)
107         {
108             int rootLength = PathInternal.GetRootLength(path);
109             int end = path.Length;
110             if (end <= rootLength)
111                 return -1;
112
113             while (end > rootLength && !PathInternal.IsDirectorySeparator(path[--end]));
114
115             // Trim off any remaining separators (to deal with C:\foo\\bar)
116             while (end > rootLength && PathInternal.IsDirectorySeparator(path[end - 1]))
117                 end--;
118
119             return end;
120         }
121
122         /// <summary>
123         /// Returns the extension of the given path. The returned value includes the period (".") character of the
124         /// extension except when you have a terminal period when you get string.Empty, such as ".exe" or ".cpp".
125         /// The returned value is null if the given path is null or empty if the given path does not include an
126         /// extension.
127         /// </summary>
128         public static string GetExtension(string path)
129         {
130             if (path == null)
131                 return null;
132
133             return new string(GetExtension(path.AsSpan()));
134         }
135
136         /// <summary>
137         /// Returns the extension of the given path.
138         /// </summary>
139         /// <remarks> 
140         /// The returned value is an empty ReadOnlySpan if the given path does not include an extension.
141         /// </remarks>
142         public static ReadOnlySpan<char> GetExtension(ReadOnlySpan<char> path)
143         {
144             int length = path.Length;
145
146             for (int i = length - 1; i >= 0; i--)
147             {
148                 char ch = path[i];
149                 if (ch == '.')
150                 {
151                     if (i != length - 1)
152                         return path.Slice(i, length - i);
153                     else
154                         return ReadOnlySpan<char>.Empty;
155                 }
156                 if (PathInternal.IsDirectorySeparator(ch))
157                     break;
158             }
159             return ReadOnlySpan<char>.Empty;
160         }
161
162         /// <summary>
163         /// Returns the name and extension parts of the given path. The resulting string contains
164         /// the characters of path that follow the last separator in path. The resulting string is
165         /// null if path is null.
166         /// </summary>
167         public static string GetFileName(string path)
168         {
169             if (path == null)
170                 return null;
171
172             ReadOnlySpan<char> result = GetFileName(path.AsSpan());
173             if (path.Length == result.Length)
174                 return path;
175
176             return new string(result);
177         }
178
179         /// <summary>
180         /// The returned ReadOnlySpan contains the characters of the path that follows the last separator in path.
181         /// </summary>
182         public static ReadOnlySpan<char> GetFileName(ReadOnlySpan<char> path)
183         {
184             int root = GetPathRoot(path).Length;
185
186             // We don't want to cut off "C:\file.txt:stream" (i.e. should be "file.txt:stream")
187             // but we *do* want "C:Foo" => "Foo". This necessitates checking for the root.
188
189             for (int i = path.Length; --i >= 0;)
190             {
191                 if (i < root || PathInternal.IsDirectorySeparator(path[i]))
192                     return path.Slice(i + 1, path.Length - i - 1);
193             }
194
195             return path;
196         }
197
198         public static string GetFileNameWithoutExtension(string path)
199         {
200             if (path == null)
201                 return null;
202
203             ReadOnlySpan<char> result = GetFileNameWithoutExtension(path.AsSpan());
204             if (path.Length == result.Length)
205                 return path;
206
207             return new string(result);
208         }
209
210         /// <summary>
211         /// Returns the characters between the last separator and last (.) in the path.
212         /// </summary>
213         public static ReadOnlySpan<char> GetFileNameWithoutExtension(ReadOnlySpan<char> path)
214         {
215             ReadOnlySpan<char> fileName = GetFileName(path);
216             int lastPeriod = fileName.LastIndexOf('.');
217             return lastPeriod == -1 ?
218                 fileName : // No extension was found
219                 fileName.Slice(0, lastPeriod);
220         }
221
222         /// <summary>
223         /// Returns a cryptographically strong random 8.3 string that can be
224         /// used as either a folder name or a file name.
225         /// </summary>
226         public static unsafe string GetRandomFileName()
227         {
228             byte* pKey = stackalloc byte[KeyLength];
229             Interop.GetRandomBytes(pKey, KeyLength);
230
231             const int RandomFileNameLength = 12;
232             char* pRandomFileName = stackalloc char[RandomFileNameLength];
233             Populate83FileNameFromRandomBytes(pKey, KeyLength, pRandomFileName, RandomFileNameLength);
234             return new string(pRandomFileName, 0, RandomFileNameLength);
235         }
236
237         /// <summary>
238         /// Returns true if the path is fixed to a specific drive or UNC path. This method does no
239         /// validation of the path (URIs will be returned as relative as a result).
240         /// Returns false if the path specified is relative to the current drive or working directory.
241         /// </summary>
242         /// <remarks>
243         /// Handles paths that use the alternate directory separator.  It is a frequent mistake to
244         /// assume that rooted paths <see cref="Path.IsPathRooted(string)"/> are not relative.  This isn't the case.
245         /// "C:a" is drive relative- meaning that it will be resolved against the current directory
246         /// for C: (rooted, but relative). "C:\a" is rooted and not relative (the current directory
247         /// will not be used to modify the path).
248         /// </remarks>
249         /// <exception cref="ArgumentNullException">
250         /// Thrown if <paramref name="path"/> is null.
251         /// </exception>
252         public static bool IsPathFullyQualified(string path)
253         {
254             if (path == null)
255                 throw new ArgumentNullException(nameof(path));
256
257             return IsPathFullyQualified(path.AsSpan());
258         }
259
260         public static bool IsPathFullyQualified(ReadOnlySpan<char> path)
261         {
262             return !PathInternal.IsPartiallyQualified(path);
263         }
264
265
266         /// <summary>
267         /// Tests if a path's file name includes a file extension. A trailing period
268         /// is not considered an extension.
269         /// </summary>
270         public static bool HasExtension(string path)
271         {
272             if (path != null)
273             {
274                 return HasExtension(path.AsSpan());
275             }
276             return false;
277         }
278
279         public static bool HasExtension(ReadOnlySpan<char> path)
280         {
281             for (int i = path.Length - 1; i >= 0; i--)
282             {
283                 char ch = path[i];
284                 if (ch == '.')
285                 {
286                     return i != path.Length - 1;
287                 }
288                 if (PathInternal.IsDirectorySeparator(ch))
289                     break;
290             }
291             return false;
292         }
293
294         public static string Combine(string path1, string path2)
295         {
296             if (path1 == null || path2 == null)
297                 throw new ArgumentNullException((path1 == null) ? nameof(path1) : nameof(path2));
298
299             return CombineNoChecks(path1, path2);
300         }
301
302         public static string Combine(string path1, string path2, string path3)
303         {
304             if (path1 == null || path2 == null || path3 == null)
305                 throw new ArgumentNullException((path1 == null) ? nameof(path1) : (path2 == null) ? nameof(path2) : nameof(path3));
306
307             return CombineNoChecks(path1, path2, path3);
308         }
309
310         public static string Combine(string path1, string path2, string path3, string path4)
311         {
312             if (path1 == null || path2 == null || path3 == null || path4 == null)
313                 throw new ArgumentNullException((path1 == null) ? nameof(path1) : (path2 == null) ? nameof(path2) : (path3 == null) ? nameof(path3) : nameof(path4));
314
315             return CombineNoChecks(path1, path2, path3, path4);
316         }
317
318         public static string Combine(params string[] paths)
319         {
320             if (paths == null)
321             {
322                 throw new ArgumentNullException(nameof(paths));
323             }
324
325             int finalSize = 0;
326             int firstComponent = 0;
327
328             // We have two passes, the first calculates how large a buffer to allocate and does some precondition
329             // checks on the paths passed in.  The second actually does the combination.
330
331             for (int i = 0; i < paths.Length; i++)
332             {
333                 if (paths[i] == null)
334                 {
335                     throw new ArgumentNullException(nameof(paths));
336                 }
337
338                 if (paths[i].Length == 0)
339                 {
340                     continue;
341                 }
342
343                 if (IsPathRooted(paths[i]))
344                 {
345                     firstComponent = i;
346                     finalSize = paths[i].Length;
347                 }
348                 else
349                 {
350                     finalSize += paths[i].Length;
351                 }
352
353                 char ch = paths[i][paths[i].Length - 1];
354                 if (!PathInternal.IsDirectorySeparator(ch))
355                     finalSize++;
356             }
357
358             StringBuilder finalPath = StringBuilderCache.Acquire(finalSize);
359
360             for (int i = firstComponent; i < paths.Length; i++)
361             {
362                 if (paths[i].Length == 0)
363                 {
364                     continue;
365                 }
366
367                 if (finalPath.Length == 0)
368                 {
369                     finalPath.Append(paths[i]);
370                 }
371                 else
372                 {
373                     char ch = finalPath[finalPath.Length - 1];
374                     if (!PathInternal.IsDirectorySeparator(ch))
375                     {
376                         finalPath.Append(PathInternal.DirectorySeparatorChar);
377                     }
378
379                     finalPath.Append(paths[i]);
380                 }
381             }
382
383             return StringBuilderCache.GetStringAndRelease(finalPath);
384         }
385
386         /// <summary>
387         /// Combines two paths. Does no validation of paths, only concatenates the paths
388         /// and places a directory separator between them if needed.
389         /// </summary>
390         private static string CombineNoChecks(ReadOnlySpan<char> first, ReadOnlySpan<char> second)
391         {
392             if (first.Length == 0)
393                 return second.Length == 0
394                     ? string.Empty
395                     : new string(second);
396
397             if (second.Length == 0)
398                 return new string(first);
399
400             if (IsPathRooted(second))
401                 return new string(second);
402
403             return CombineNoChecksInternal(first, second);
404         }
405
406         private static string CombineNoChecks(string first, string second)
407         {
408             if (string.IsNullOrEmpty(first))
409                 return second;
410
411             if (string.IsNullOrEmpty(second))
412                 return first;
413
414             if (IsPathRooted(second.AsSpan()))
415                 return second;
416
417             return CombineNoChecksInternal(first, second);
418         }
419
420         private static string CombineNoChecks(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third)
421         {
422             if (first.Length == 0)
423                 return CombineNoChecks(second, third);
424             if (second.Length == 0)
425                 return CombineNoChecks(first, third);
426             if (third.Length == 0)
427                 return CombineNoChecks(first, second);
428
429             if (IsPathRooted(third))
430                 return new string(third);
431             if (IsPathRooted(second))
432                 return CombineNoChecks(second, third);
433
434             return CombineNoChecksInternal(first, second, third);
435         }
436
437         private static string CombineNoChecks(string first, string second, string third)
438         {
439             if (string.IsNullOrEmpty(first))
440                 return CombineNoChecks(second, third);
441             if (string.IsNullOrEmpty(second))
442                 return CombineNoChecks(first, third);
443             if (string.IsNullOrEmpty(third))
444                 return CombineNoChecks(first, second);
445
446             if (IsPathRooted(third.AsSpan()))
447                 return third;
448             if (IsPathRooted(second.AsSpan()))
449                 return CombineNoChecks(second, third);
450
451             return CombineNoChecksInternal(first, second, third);
452         }
453
454         private static string CombineNoChecks(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third, ReadOnlySpan<char> fourth)
455         {
456             if (first.Length == 0)
457                 return CombineNoChecks(second, third, fourth);
458             if (second.Length == 0)
459                 return CombineNoChecks(first, third, fourth);
460             if (third.Length == 0)
461                 return CombineNoChecks(first, second, fourth);
462             if (fourth.Length == 0)
463                 return CombineNoChecks(first, second, third);
464
465             if (IsPathRooted(fourth))
466                 return new string(fourth);
467             if (IsPathRooted(third))
468                 return CombineNoChecks(third, fourth);
469             if (IsPathRooted(second))
470                 return CombineNoChecks(second, third, fourth);
471
472             return CombineNoChecksInternal(first, second, third, fourth);
473         }
474
475         private static string CombineNoChecks(string first, string second, string third, string fourth)
476         {
477             if (string.IsNullOrEmpty(first))
478                 return CombineNoChecks(second, third, fourth);
479             if (string.IsNullOrEmpty(second))
480                 return CombineNoChecks(first, third, fourth);
481             if (string.IsNullOrEmpty(third))
482                 return CombineNoChecks(first, second, fourth);
483             if (string.IsNullOrEmpty(fourth))
484                 return CombineNoChecks(first, second, third);
485
486             if (IsPathRooted(fourth.AsSpan()))
487                 return fourth;
488             if (IsPathRooted(third.AsSpan()))
489                 return CombineNoChecks(third, fourth);
490             if (IsPathRooted(second.AsSpan()))
491                 return CombineNoChecks(second, third, fourth);
492
493             return CombineNoChecksInternal(first, second, third, fourth);
494         }
495
496         private unsafe static string CombineNoChecksInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second)
497         {
498             Debug.Assert(first.Length > 0 && second.Length > 0, "should have dealt with empty paths");
499
500             bool hasSeparator = PathInternal.IsDirectorySeparator(first[first.Length - 1])
501                 || PathInternal.IsDirectorySeparator(second[0]);
502
503             fixed (char* f = &MemoryMarshal.GetReference(first), s = &MemoryMarshal.GetReference(second))
504             {
505                 return string.Create(
506                     first.Length + second.Length + (hasSeparator ? 0 : 1),
507                     (First: (IntPtr)f, FirstLength: first.Length, Second: (IntPtr)s, SecondLength: second.Length, HasSeparator: hasSeparator),
508                     (destination, state) =>
509                     {
510                         new Span<char>((char*)state.First, state.FirstLength).CopyTo(destination);
511                         if (!state.HasSeparator)
512                             destination[state.FirstLength] = PathInternal.DirectorySeparatorChar;
513                         new Span<char>((char*)state.Second, state.SecondLength).CopyTo(destination.Slice(state.FirstLength + (state.HasSeparator ? 0 : 1)));
514                     });
515             }
516         }
517
518         private unsafe static string CombineNoChecksInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third)
519         {
520             Debug.Assert(first.Length > 0 && second.Length > 0 && third.Length > 0, "should have dealt with empty paths");
521
522             bool firstHasSeparator = PathInternal.IsDirectorySeparator(first[first.Length - 1])
523                 || PathInternal.IsDirectorySeparator(second[0]);
524             bool thirdHasSeparator = PathInternal.IsDirectorySeparator(second[second.Length - 1])
525                 || PathInternal.IsDirectorySeparator(third[0]);
526
527             fixed (char* f = &MemoryMarshal.GetReference(first), s = &MemoryMarshal.GetReference(second), t = &MemoryMarshal.GetReference(third))
528             {
529                 return string.Create(
530                     first.Length + second.Length + third.Length + (firstHasSeparator ? 0 : 1) + (thirdHasSeparator ? 0 : 1),
531                     (First: (IntPtr)f, FirstLength: first.Length, Second: (IntPtr)s, SecondLength: second.Length,
532                         Third: (IntPtr)t, ThirdLength: third.Length, FirstHasSeparator: firstHasSeparator, ThirdHasSeparator: thirdHasSeparator),
533                     (destination, state) =>
534                     {
535                         new Span<char>((char*)state.First, state.FirstLength).CopyTo(destination);
536                         if (!state.FirstHasSeparator)
537                             destination[state.FirstLength] = PathInternal.DirectorySeparatorChar;
538                         new Span<char>((char*)state.Second, state.SecondLength).CopyTo(destination.Slice(state.FirstLength + (state.FirstHasSeparator ? 0 : 1)));
539                         if (!state.ThirdHasSeparator)
540                             destination[destination.Length - state.ThirdLength - 1] = PathInternal.DirectorySeparatorChar;
541                         new Span<char>((char*)state.Third, state.ThirdLength).CopyTo(destination.Slice(destination.Length - state.ThirdLength));
542                     });
543             }
544         }
545
546         private unsafe static string CombineNoChecksInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third, ReadOnlySpan<char> fourth)
547         {
548             Debug.Assert(first.Length > 0 && second.Length > 0 && third.Length > 0 && fourth.Length > 0, "should have dealt with empty paths");
549
550             bool firstHasSeparator = PathInternal.IsDirectorySeparator(first[first.Length - 1])
551                 || PathInternal.IsDirectorySeparator(second[0]);
552             bool thirdHasSeparator = PathInternal.IsDirectorySeparator(second[second.Length - 1])
553                 || PathInternal.IsDirectorySeparator(third[0]);
554             bool fourthHasSeparator = PathInternal.IsDirectorySeparator(third[third.Length - 1])
555                 || PathInternal.IsDirectorySeparator(fourth[0]);
556
557             fixed (char* f = &MemoryMarshal.GetReference(first), s = &MemoryMarshal.GetReference(second), t = &MemoryMarshal.GetReference(third), u = &MemoryMarshal.GetReference(fourth))
558             {
559                 return string.Create(
560                     first.Length + second.Length + third.Length + fourth.Length + (firstHasSeparator ? 0 : 1) + (thirdHasSeparator ? 0 : 1) + (fourthHasSeparator ? 0 : 1),
561                     (First: (IntPtr)f, FirstLength: first.Length, Second: (IntPtr)s, SecondLength: second.Length,
562                         Third: (IntPtr)t, ThirdLength: third.Length, Fourth: (IntPtr)u, FourthLength:fourth.Length,
563                         FirstHasSeparator: firstHasSeparator, ThirdHasSeparator: thirdHasSeparator, FourthHasSeparator: fourthHasSeparator),
564                     (destination, state) =>
565                     {
566                         new Span<char>((char*)state.First, state.FirstLength).CopyTo(destination);
567                         if (!state.FirstHasSeparator)
568                             destination[state.FirstLength] = PathInternal.DirectorySeparatorChar;
569                         new Span<char>((char*)state.Second, state.SecondLength).CopyTo(destination.Slice(state.FirstLength + (state.FirstHasSeparator ? 0 : 1)));
570                         if (!state.ThirdHasSeparator)
571                             destination[state.FirstLength + state.SecondLength + (state.FirstHasSeparator ? 0 : 1)] = PathInternal.DirectorySeparatorChar;
572                         new Span<char>((char*)state.Third, state.ThirdLength).CopyTo(destination.Slice(state.FirstLength + state.SecondLength + (state.FirstHasSeparator ? 0 : 1) + (state.ThirdHasSeparator ? 0 : 1)));
573                         if (!state.FourthHasSeparator)
574                             destination[destination.Length - state.FourthLength - 1] = PathInternal.DirectorySeparatorChar;
575                         new Span<char>((char*)state.Fourth, state.FourthLength).CopyTo(destination.Slice(destination.Length - state.FourthLength));
576                     });
577             }
578         }
579
580         private static readonly char[] s_base32Char = {
581                 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
582                 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
583                 'q', 'r', 's', 't', 'u', 'v', 'w', 'x',
584                 'y', 'z', '0', '1', '2', '3', '4', '5'};
585
586         private static unsafe void Populate83FileNameFromRandomBytes(byte* bytes, int byteCount, char* chars, int charCount)
587         {
588             Debug.Assert(bytes != null);
589             Debug.Assert(chars != null);
590
591             // This method requires bytes of length 8 and chars of length 12.
592             Debug.Assert(byteCount == 8, $"Unexpected {nameof(byteCount)}");
593             Debug.Assert(charCount == 12, $"Unexpected {nameof(charCount)}");
594
595             byte b0 = bytes[0];
596             byte b1 = bytes[1];
597             byte b2 = bytes[2];
598             byte b3 = bytes[3];
599             byte b4 = bytes[4];
600
601             // Consume the 5 Least significant bits of the first 5 bytes
602             chars[0] = s_base32Char[b0 & 0x1F];
603             chars[1] = s_base32Char[b1 & 0x1F];
604             chars[2] = s_base32Char[b2 & 0x1F];
605             chars[3] = s_base32Char[b3 & 0x1F];
606             chars[4] = s_base32Char[b4 & 0x1F];
607
608             // Consume 3 MSB of b0, b1, MSB bits 6, 7 of b3, b4
609             chars[5] = s_base32Char[(
610                     ((b0 & 0xE0) >> 5) |
611                     ((b3 & 0x60) >> 2))];
612
613             chars[6] = s_base32Char[(
614                     ((b1 & 0xE0) >> 5) |
615                     ((b4 & 0x60) >> 2))];
616
617             // Consume 3 MSB bits of b2, 1 MSB bit of b3, b4
618             b2 >>= 5;
619
620             Debug.Assert(((b2 & 0xF8) == 0), "Unexpected set bits");
621
622             if ((b3 & 0x80) != 0)
623                 b2 |= 0x08;
624             if ((b4 & 0x80) != 0)
625                 b2 |= 0x10;
626
627             chars[7] = s_base32Char[b2];
628
629             // Set the file extension separator
630             chars[8] = '.';
631
632             // Consume the 5 Least significant bits of the remaining 3 bytes
633             chars[9] = s_base32Char[(bytes[5] & 0x1F)];
634             chars[10] = s_base32Char[(bytes[6] & 0x1F)];
635             chars[11] = s_base32Char[(bytes[7] & 0x1F)];
636         }
637
638         /// <summary>
639         /// Try to remove relative segments from the given path (without combining with a root).
640         /// </summary>
641         /// <param name="skip">Skip the specified number of characters before evaluating.</param>
642         private static string RemoveRelativeSegments(string path, int skip = 0)
643         {
644             bool flippedSeparator = false;
645
646             // Remove "//", "/./", and "/../" from the path by copying each character to the output, 
647             // except the ones we're removing, such that the builder contains the normalized path 
648             // at the end.
649             StringBuilder sb = StringBuilderCache.Acquire(path.Length);
650             if (skip > 0)
651             {
652                 sb.Append(path, 0, skip);
653             }
654
655             for (int i = skip; i < path.Length; i++)
656             {
657                 char c = path[i];
658
659                 if (PathInternal.IsDirectorySeparator(c) && i + 1 < path.Length)
660                 {
661                     // Skip this character if it's a directory separator and if the next character is, too,
662                     // e.g. "parent//child" => "parent/child"
663                     if (PathInternal.IsDirectorySeparator(path[i + 1]))
664                     {
665                         continue;
666                     }
667
668                     // Skip this character and the next if it's referring to the current directory,
669                     // e.g. "parent/./child" => "parent/child"
670                     if ((i + 2 == path.Length || PathInternal.IsDirectorySeparator(path[i + 2])) &&
671                         path[i + 1] == '.')
672                     {
673                         i++;
674                         continue;
675                     }
676
677                     // Skip this character and the next two if it's referring to the parent directory,
678                     // e.g. "parent/child/../grandchild" => "parent/grandchild"
679                     if (i + 2 < path.Length &&
680                         (i + 3 == path.Length || PathInternal.IsDirectorySeparator(path[i + 3])) &&
681                         path[i + 1] == '.' && path[i + 2] == '.')
682                     {
683                         // Unwind back to the last slash (and if there isn't one, clear out everything).
684                         int s;
685                         for (s = sb.Length - 1; s >= 0; s--)
686                         {
687                             if (PathInternal.IsDirectorySeparator(sb[s]))
688                             {
689                                 sb.Length = s;
690                                 break;
691                             }
692                         }
693                         if (s < 0)
694                         {
695                             sb.Length = 0;
696                         }
697
698                         i += 2;
699                         continue;
700                     }
701                 }
702
703                 // Normalize the directory separator if needed
704                 if (c != PathInternal.DirectorySeparatorChar && c == PathInternal.AltDirectorySeparatorChar)
705                 {
706                     c = PathInternal.DirectorySeparatorChar;
707                     flippedSeparator = true;
708                 }
709
710                 sb.Append(c);
711             }
712
713             if (flippedSeparator || sb.Length != path.Length)
714             {
715                 return StringBuilderCache.GetStringAndRelease(sb);
716             }
717             else
718             {
719                 // We haven't changed the source path, return the original
720                 StringBuilderCache.Release(sb);
721                 return path;
722             }
723         }
724
725         /// <summary>
726         /// Create a relative path from one path to another. Paths will be resolved before calculating the difference.
727         /// Default path comparison for the active platform will be used (OrdinalIgnoreCase for Windows or Mac, Ordinal for Unix).
728         /// </summary>
729         /// <param name="relativeTo">The source path the output should be relative to. This path is always considered to be a directory.</param>
730         /// <param name="path">The destination path.</param>
731         /// <returns>The relative path or <paramref name="path"/> if the paths don't share the same root.</returns>
732         /// <exception cref="ArgumentNullException">Thrown if <paramref name="relativeTo"/> or <paramref name="path"/> is <c>null</c> or an empty string.</exception>
733         public static string GetRelativePath(string relativeTo, string path)
734         {
735             return GetRelativePath(relativeTo, path, StringComparison);
736         }
737
738         private static string GetRelativePath(string relativeTo, string path, StringComparison comparisonType)
739         {
740             if (string.IsNullOrEmpty(relativeTo)) throw new ArgumentNullException(nameof(relativeTo));
741             if (PathInternal.IsEffectivelyEmpty(path)) throw new ArgumentNullException(nameof(path));
742             Debug.Assert(comparisonType == StringComparison.Ordinal || comparisonType == StringComparison.OrdinalIgnoreCase);
743
744             relativeTo = GetFullPath(relativeTo);
745             path = GetFullPath(path);
746
747             // Need to check if the roots are different- if they are we need to return the "to" path.
748             if (!PathInternal.AreRootsEqual(relativeTo, path, comparisonType))
749                 return path;
750
751             int commonLength = PathInternal.GetCommonPathLength(relativeTo, path, ignoreCase: comparisonType == StringComparison.OrdinalIgnoreCase);
752
753             // If there is nothing in common they can't share the same root, return the "to" path as is.
754             if (commonLength == 0)
755                 return path;
756
757             // Trailing separators aren't significant for comparison
758             int relativeToLength = relativeTo.Length;
759             if (PathInternal.EndsInDirectorySeparator(relativeTo))
760                 relativeToLength--;
761
762             bool pathEndsInSeparator = PathInternal.EndsInDirectorySeparator(path);
763             int pathLength = path.Length;
764             if (pathEndsInSeparator)
765                 pathLength--;
766
767             // If we have effectively the same path, return "."
768             if (relativeToLength == pathLength && commonLength >= relativeToLength) return ".";
769
770             // We have the same root, we need to calculate the difference now using the
771             // common Length and Segment count past the length.
772             //
773             // Some examples:
774             //
775             //  C:\Foo C:\Bar L3, S1 -> ..\Bar
776             //  C:\Foo C:\Foo\Bar L6, S0 -> Bar
777             //  C:\Foo\Bar C:\Bar\Bar L3, S2 -> ..\..\Bar\Bar
778             //  C:\Foo\Foo C:\Foo\Bar L7, S1 -> ..\Bar
779
780             StringBuilder sb = StringBuilderCache.Acquire(Math.Max(relativeTo.Length, path.Length));
781
782             // Add parent segments for segments past the common on the "from" path
783             if (commonLength < relativeToLength)
784             {
785                 sb.Append(PathInternal.ParentDirectoryPrefix);
786
787                 for (int i = commonLength; i < relativeToLength; i++)
788                 {
789                     if (PathInternal.IsDirectorySeparator(relativeTo[i]))
790                     {
791                         sb.Append(PathInternal.ParentDirectoryPrefix);
792                     }
793                 }
794             }
795             else if (PathInternal.IsDirectorySeparator(path[commonLength]))
796             {
797                 // No parent segments and we need to eat the initial separator
798                 //  (C:\Foo C:\Foo\Bar case)
799                 commonLength++;
800             }
801
802             // Now add the rest of the "to" path, adding back the trailing separator
803             int count = pathLength - commonLength;
804             if (pathEndsInSeparator)
805                 count++;
806
807             sb.Append(path, commonLength, count);
808             return StringBuilderCache.GetStringAndRelease(sb);
809         }
810
811         /// <summary>Returns a comparison that can be used to compare file and directory names for equality.</summary>
812         internal static StringComparison StringComparison
813         {
814             get
815             {
816                 return IsCaseSensitive ?
817                     StringComparison.Ordinal :
818                     StringComparison.OrdinalIgnoreCase;
819             }
820         }
821     }
822 }