Add and apply nullable attributes (dotnet/coreclr#24679)
[mono-project.git] / netcore / System.Private.CoreLib / shared / System / IO / Path.cs
blobb0c3df34160d51513692dfd08d9602ad6b3bdfcc
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 #nullable enable
6 using System.Diagnostics;
7 using System.Diagnostics.CodeAnalysis;
8 using System.Runtime.InteropServices;
9 using System.Text;
11 #if MS_IO_REDIST
12 using System;
13 using System.IO;
15 namespace Microsoft.IO
16 #else
17 namespace System.IO
18 #endif
20 // Provides methods for processing file system strings in a cross-platform manner.
21 // Most of the methods don't do a complete parsing (such as examining a UNC hostname),
22 // but they will handle most string operations.
23 public static partial class Path
25 // Public static readonly variant of the separators. The Path implementation itself is using
26 // internal const variant of the separators for better performance.
27 public static readonly char DirectorySeparatorChar = PathInternal.DirectorySeparatorChar;
28 public static readonly char AltDirectorySeparatorChar = PathInternal.AltDirectorySeparatorChar;
29 public static readonly char VolumeSeparatorChar = PathInternal.VolumeSeparatorChar;
30 public static readonly char PathSeparator = PathInternal.PathSeparator;
32 // For generating random file names
33 // 8 random bytes provides 12 chars in our encoding for the 8.3 name.
34 private const int KeyLength = 8;
36 [Obsolete("Please use GetInvalidPathChars or GetInvalidFileNameChars instead.")]
37 public static readonly char[] InvalidPathChars = GetInvalidPathChars();
39 // Changes the extension of a file path. The path parameter
40 // specifies a file path, and the extension parameter
41 // specifies a file extension (with a leading period, such as
42 // ".exe" or ".cs").
44 // The function returns a file path with the same root, directory, and base
45 // name parts as path, but with the file extension changed to
46 // the specified extension. If path is null, the function
47 // returns null. If path does not contain a file extension,
48 // the new file extension is appended to the path. If extension
49 // is null, any existing extension is removed from path.
50 [return: NotNullIfNotNull("path")]
51 public static string? ChangeExtension(string? path, string? extension)
53 if (path == null)
54 return null;
56 int subLength = path.Length;
57 if (subLength == 0)
58 return string.Empty;
60 for (int i = path.Length - 1; i >= 0; i--)
62 char ch = path[i];
64 if (ch == '.')
66 subLength = i;
67 break;
70 if (PathInternal.IsDirectorySeparator(ch))
72 break;
76 if (extension == null)
78 return path.Substring(0, subLength);
81 ReadOnlySpan<char> subpath = path.AsSpan(0, subLength);
82 #if MS_IO_REDIST
83 return extension.Length != 0 && extension[0] == '.' ?
84 StringExtensions.Concat(subpath, extension.AsSpan()) :
85 StringExtensions.Concat(subpath, ".".AsSpan(), extension.AsSpan());
86 #else
87 return extension.StartsWith('.') ?
88 string.Concat(subpath, extension) :
89 string.Concat(subpath, ".", extension);
90 #endif
93 /// <summary>
94 /// Returns the directory portion of a file path. This method effectively
95 /// removes the last segment of the given file path, i.e. it returns a
96 /// string consisting of all characters up to but not including the last
97 /// backslash ("\") in the file path. The returned value is null if the
98 /// specified path is null, empty, or a root (such as "\", "C:", or
99 /// "\\server\share").
100 /// </summary>
101 /// <remarks>
102 /// Directory separators are normalized in the returned string.
103 /// </remarks>
104 public static string? GetDirectoryName(string? path)
106 if (path == null || PathInternal.IsEffectivelyEmpty(path.AsSpan()))
107 return null;
109 int end = GetDirectoryNameOffset(path.AsSpan());
110 return end >= 0 ? PathInternal.NormalizeDirectorySeparators(path.Substring(0, end)) : null;
113 /// <summary>
114 /// Returns the directory portion of a file path. The returned value is empty
115 /// if the specified path is null, empty, or a root (such as "\", "C:", or
116 /// "\\server\share").
117 /// </summary>
118 /// <remarks>
119 /// Unlike the string overload, this method will not normalize directory separators.
120 /// </remarks>
121 public static ReadOnlySpan<char> GetDirectoryName(ReadOnlySpan<char> path)
123 if (PathInternal.IsEffectivelyEmpty(path))
124 return ReadOnlySpan<char>.Empty;
126 int end = GetDirectoryNameOffset(path);
127 return end >= 0 ? path.Slice(0, end) : ReadOnlySpan<char>.Empty;
130 private static int GetDirectoryNameOffset(ReadOnlySpan<char> path)
132 int rootLength = PathInternal.GetRootLength(path);
133 int end = path.Length;
134 if (end <= rootLength)
135 return -1;
137 while (end > rootLength && !PathInternal.IsDirectorySeparator(path[--end]));
139 // Trim off any remaining separators (to deal with C:\foo\\bar)
140 while (end > rootLength && PathInternal.IsDirectorySeparator(path[end - 1]))
141 end--;
143 return end;
146 /// <summary>
147 /// Returns the extension of the given path. The returned value includes the period (".") character of the
148 /// extension except when you have a terminal period when you get string.Empty, such as ".exe" or ".cpp".
149 /// The returned value is null if the given path is null or empty if the given path does not include an
150 /// extension.
151 /// </summary>
152 [return: NotNullIfNotNull("path")]
153 public static string? GetExtension(string? path)
155 if (path == null)
156 return null;
158 return GetExtension(path.AsSpan()).ToString();
161 /// <summary>
162 /// Returns the extension of the given path.
163 /// </summary>
164 /// <remarks>
165 /// The returned value is an empty ReadOnlySpan if the given path does not include an extension.
166 /// </remarks>
167 public static ReadOnlySpan<char> GetExtension(ReadOnlySpan<char> path)
169 int length = path.Length;
171 for (int i = length - 1; i >= 0; i--)
173 char ch = path[i];
174 if (ch == '.')
176 if (i != length - 1)
177 return path.Slice(i, length - i);
178 else
179 return ReadOnlySpan<char>.Empty;
181 if (PathInternal.IsDirectorySeparator(ch))
182 break;
184 return ReadOnlySpan<char>.Empty;
187 /// <summary>
188 /// Returns the name and extension parts of the given path. The resulting string contains
189 /// the characters of path that follow the last separator in path. The resulting string is
190 /// null if path is null.
191 /// </summary>
192 [return: NotNullIfNotNull("path")]
193 public static string? GetFileName(string? path)
195 if (path == null)
196 return null;
198 ReadOnlySpan<char> result = GetFileName(path.AsSpan());
199 if (path.Length == result.Length)
200 return path;
202 return result.ToString();
205 /// <summary>
206 /// The returned ReadOnlySpan contains the characters of the path that follows the last separator in path.
207 /// </summary>
208 public static ReadOnlySpan<char> GetFileName(ReadOnlySpan<char> path)
210 int root = GetPathRoot(path).Length;
212 // We don't want to cut off "C:\file.txt:stream" (i.e. should be "file.txt:stream")
213 // but we *do* want "C:Foo" => "Foo". This necessitates checking for the root.
215 for (int i = path.Length; --i >= 0;)
217 if (i < root || PathInternal.IsDirectorySeparator(path[i]))
218 return path.Slice(i + 1, path.Length - i - 1);
221 return path;
224 [return: NotNullIfNotNull("path")]
225 public static string? GetFileNameWithoutExtension(string? path)
227 if (path == null)
228 return null;
230 ReadOnlySpan<char> result = GetFileNameWithoutExtension(path.AsSpan());
231 if (path.Length == result.Length)
232 return path;
234 return result.ToString();
237 /// <summary>
238 /// Returns the characters between the last separator and last (.) in the path.
239 /// </summary>
240 public static ReadOnlySpan<char> GetFileNameWithoutExtension(ReadOnlySpan<char> path)
242 ReadOnlySpan<char> fileName = GetFileName(path);
243 int lastPeriod = fileName.LastIndexOf('.');
244 return lastPeriod == -1 ?
245 fileName : // No extension was found
246 fileName.Slice(0, lastPeriod);
249 /// <summary>
250 /// Returns a cryptographically strong random 8.3 string that can be
251 /// used as either a folder name or a file name.
252 /// </summary>
253 public static unsafe string GetRandomFileName()
255 byte* pKey = stackalloc byte[KeyLength];
256 Interop.GetRandomBytes(pKey, KeyLength);
258 #if MS_IO_REDIST
259 return StringExtensions.Create(
260 #else
261 return string.Create(
262 #endif
263 12, (IntPtr)pKey, (span, key) => // 12 == 8 + 1 (for period) + 3
264 Populate83FileNameFromRandomBytes((byte*)key, KeyLength, span));
267 /// <summary>
268 /// Returns true if the path is fixed to a specific drive or UNC path. This method does no
269 /// validation of the path (URIs will be returned as relative as a result).
270 /// Returns false if the path specified is relative to the current drive or working directory.
271 /// </summary>
272 /// <remarks>
273 /// Handles paths that use the alternate directory separator. It is a frequent mistake to
274 /// assume that rooted paths <see cref="Path.IsPathRooted(string)"/> are not relative. This isn't the case.
275 /// "C:a" is drive relative- meaning that it will be resolved against the current directory
276 /// for C: (rooted, but relative). "C:\a" is rooted and not relative (the current directory
277 /// will not be used to modify the path).
278 /// </remarks>
279 /// <exception cref="ArgumentNullException">
280 /// Thrown if <paramref name="path"/> is null.
281 /// </exception>
282 public static bool IsPathFullyQualified(string path)
284 if (path == null)
285 throw new ArgumentNullException(nameof(path));
287 return IsPathFullyQualified(path.AsSpan());
290 public static bool IsPathFullyQualified(ReadOnlySpan<char> path)
292 return !PathInternal.IsPartiallyQualified(path);
295 /// <summary>
296 /// Tests if a path's file name includes a file extension. A trailing period
297 /// is not considered an extension.
298 /// </summary>
299 public static bool HasExtension(string? path)
301 if (path != null)
303 return HasExtension(path.AsSpan());
305 return false;
308 public static bool HasExtension(ReadOnlySpan<char> path)
310 for (int i = path.Length - 1; i >= 0; i--)
312 char ch = path[i];
313 if (ch == '.')
315 return i != path.Length - 1;
317 if (PathInternal.IsDirectorySeparator(ch))
318 break;
320 return false;
323 public static string Combine(string path1, string path2)
325 if (path1 == null || path2 == null)
326 throw new ArgumentNullException((path1 == null) ? nameof(path1) : nameof(path2));
328 return CombineInternal(path1, path2);
331 public static string Combine(string path1, string path2, string path3)
333 if (path1 == null || path2 == null || path3 == null)
334 throw new ArgumentNullException((path1 == null) ? nameof(path1) : (path2 == null) ? nameof(path2) : nameof(path3));
336 return CombineInternal(path1, path2, path3);
339 public static string Combine(string path1, string path2, string path3, string path4)
341 if (path1 == null || path2 == null || path3 == null || path4 == null)
342 throw new ArgumentNullException((path1 == null) ? nameof(path1) : (path2 == null) ? nameof(path2) : (path3 == null) ? nameof(path3) : nameof(path4));
344 return CombineInternal(path1, path2, path3, path4);
347 public static string Combine(params string[] paths)
349 if (paths == null)
351 throw new ArgumentNullException(nameof(paths));
354 int maxSize = 0;
355 int firstComponent = 0;
357 // We have two passes, the first calculates how large a buffer to allocate and does some precondition
358 // checks on the paths passed in. The second actually does the combination.
360 for (int i = 0; i < paths.Length; i++)
362 if (paths[i] == null)
364 throw new ArgumentNullException(nameof(paths));
367 if (paths[i].Length == 0)
369 continue;
372 if (IsPathRooted(paths[i]))
374 firstComponent = i;
375 maxSize = paths[i].Length;
377 else
379 maxSize += paths[i].Length;
382 char ch = paths[i][paths[i].Length - 1];
383 if (!PathInternal.IsDirectorySeparator(ch))
384 maxSize++;
387 Span<char> initialBuffer = stackalloc char[260]; // MaxShortPath on Windows
388 var builder = new ValueStringBuilder(initialBuffer);
389 builder.EnsureCapacity(maxSize);
391 for (int i = firstComponent; i < paths.Length; i++)
393 if (paths[i].Length == 0)
395 continue;
398 if (builder.Length == 0)
400 builder.Append(paths[i]);
402 else
404 char ch = builder[builder.Length - 1];
405 if (!PathInternal.IsDirectorySeparator(ch))
407 builder.Append(PathInternal.DirectorySeparatorChar);
410 builder.Append(paths[i]);
414 return builder.ToString();
417 // Unlike Combine(), Join() methods do not consider rooting. They simply combine paths, ensuring that there
418 // is a directory separator between them.
420 public static string Join(ReadOnlySpan<char> path1, ReadOnlySpan<char> path2)
422 if (path1.Length == 0)
423 return path2.ToString();
424 if (path2.Length == 0)
425 return path1.ToString();
427 return JoinInternal(path1, path2);
430 public static string Join(ReadOnlySpan<char> path1, ReadOnlySpan<char> path2, ReadOnlySpan<char> path3)
432 if (path1.Length == 0)
433 return Join(path2, path3);
435 if (path2.Length == 0)
436 return Join(path1, path3);
438 if (path3.Length == 0)
439 return Join(path1, path2);
441 return JoinInternal(path1, path2, path3);
444 public static string Join(ReadOnlySpan<char> path1, ReadOnlySpan<char> path2, ReadOnlySpan<char> path3, ReadOnlySpan<char> path4)
446 if (path1.Length == 0)
447 return Join(path2, path3, path4);
449 if (path2.Length == 0)
450 return Join(path1, path3, path4);
452 if (path3.Length == 0)
453 return Join(path1, path2, path4);
455 if (path4.Length == 0)
456 return Join(path1, path2, path3);
458 return JoinInternal(path1, path2, path3, path4);
461 public static string Join(string? path1, string? path2)
463 return Join(path1.AsSpan(), path2.AsSpan());
466 public static string Join(string? path1, string? path2, string? path3)
468 return Join(path1.AsSpan(), path2.AsSpan(), path3.AsSpan());
471 public static string Join(string? path1, string? path2, string? path3, string? path4)
473 return Join(path1.AsSpan(), path2.AsSpan(), path3.AsSpan(), path4.AsSpan());
476 public static string Join(params string?[] paths)
478 if (paths == null)
480 throw new ArgumentNullException(nameof(paths));
483 if (paths.Length == 0)
485 return string.Empty;
488 int maxSize = 0;
489 foreach (string? path in paths)
491 maxSize += path?.Length ?? 0;
493 maxSize += paths.Length - 1;
495 Span<char> initialBuffer = stackalloc char[260]; // MaxShortPath on Windows
496 var builder = new ValueStringBuilder(initialBuffer);
497 builder.EnsureCapacity(maxSize);
499 for (int i = 0; i < paths.Length; i++)
501 string? path = paths[i];
502 if (path == null || path.Length == 0)
504 continue;
507 if (builder.Length == 0)
509 builder.Append(path);
511 else
513 if (!PathInternal.IsDirectorySeparator(builder[builder.Length - 1]) && !PathInternal.IsDirectorySeparator(path[0]))
515 builder.Append(PathInternal.DirectorySeparatorChar);
518 builder.Append(path);
522 return builder.ToString();
525 public static bool TryJoin(ReadOnlySpan<char> path1, ReadOnlySpan<char> path2, Span<char> destination, out int charsWritten)
527 charsWritten = 0;
528 if (path1.Length == 0 && path2.Length == 0)
529 return true;
531 if (path1.Length == 0 || path2.Length == 0)
533 ref ReadOnlySpan<char> pathToUse = ref path1.Length == 0 ? ref path2 : ref path1;
534 if (destination.Length < pathToUse.Length)
536 return false;
539 pathToUse.CopyTo(destination);
540 charsWritten = pathToUse.Length;
541 return true;
544 bool needsSeparator = !(PathInternal.EndsInDirectorySeparator(path1) || PathInternal.StartsWithDirectorySeparator(path2));
545 int charsNeeded = path1.Length + path2.Length + (needsSeparator ? 1 : 0);
546 if (destination.Length < charsNeeded)
547 return false;
549 path1.CopyTo(destination);
550 if (needsSeparator)
551 destination[path1.Length] = DirectorySeparatorChar;
553 path2.CopyTo(destination.Slice(path1.Length + (needsSeparator ? 1 : 0)));
555 charsWritten = charsNeeded;
556 return true;
559 public static bool TryJoin(ReadOnlySpan<char> path1, ReadOnlySpan<char> path2, ReadOnlySpan<char> path3, Span<char> destination, out int charsWritten)
561 charsWritten = 0;
562 if (path1.Length == 0 && path2.Length == 0 && path3.Length == 0)
563 return true;
565 if (path1.Length == 0)
566 return TryJoin(path2, path3, destination, out charsWritten);
567 if (path2.Length == 0)
568 return TryJoin(path1, path3, destination, out charsWritten);
569 if (path3.Length == 0)
570 return TryJoin(path1, path2, destination, out charsWritten);
572 int neededSeparators = PathInternal.EndsInDirectorySeparator(path1) || PathInternal.StartsWithDirectorySeparator(path2) ? 0 : 1;
573 bool needsSecondSeparator = !(PathInternal.EndsInDirectorySeparator(path2) || PathInternal.StartsWithDirectorySeparator(path3));
574 if (needsSecondSeparator)
575 neededSeparators++;
577 int charsNeeded = path1.Length + path2.Length + path3.Length + neededSeparators;
578 if (destination.Length < charsNeeded)
579 return false;
581 bool result = TryJoin(path1, path2, destination, out charsWritten);
582 Debug.Assert(result, "should never fail joining first two paths");
584 if (needsSecondSeparator)
585 destination[charsWritten++] = DirectorySeparatorChar;
587 path3.CopyTo(destination.Slice(charsWritten));
588 charsWritten += path3.Length;
590 return true;
593 private static string CombineInternal(string first, string second)
595 if (string.IsNullOrEmpty(first))
596 return second;
598 if (string.IsNullOrEmpty(second))
599 return first;
601 if (IsPathRooted(second.AsSpan()))
602 return second;
604 return JoinInternal(first.AsSpan(), second.AsSpan());
607 private static string CombineInternal(string first, string second, string third)
609 if (string.IsNullOrEmpty(first))
610 return CombineInternal(second, third);
611 if (string.IsNullOrEmpty(second))
612 return CombineInternal(first, third);
613 if (string.IsNullOrEmpty(third))
614 return CombineInternal(first, second);
616 if (IsPathRooted(third.AsSpan()))
617 return third;
618 if (IsPathRooted(second.AsSpan()))
619 return CombineInternal(second, third);
621 return JoinInternal(first.AsSpan(), second.AsSpan(), third.AsSpan());
624 private static string CombineInternal(string first, string second, string third, string fourth)
626 if (string.IsNullOrEmpty(first))
627 return CombineInternal(second, third, fourth);
628 if (string.IsNullOrEmpty(second))
629 return CombineInternal(first, third, fourth);
630 if (string.IsNullOrEmpty(third))
631 return CombineInternal(first, second, fourth);
632 if (string.IsNullOrEmpty(fourth))
633 return CombineInternal(first, second, third);
635 if (IsPathRooted(fourth.AsSpan()))
636 return fourth;
637 if (IsPathRooted(third.AsSpan()))
638 return CombineInternal(third, fourth);
639 if (IsPathRooted(second.AsSpan()))
640 return CombineInternal(second, third, fourth);
642 return JoinInternal(first.AsSpan(), second.AsSpan(), third.AsSpan(), fourth.AsSpan());
645 private static unsafe string JoinInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second)
647 Debug.Assert(first.Length > 0 && second.Length > 0, "should have dealt with empty paths");
649 bool hasSeparator = PathInternal.IsDirectorySeparator(first[first.Length - 1])
650 || PathInternal.IsDirectorySeparator(second[0]);
652 fixed (char* f = &MemoryMarshal.GetReference(first), s = &MemoryMarshal.GetReference(second))
654 #if MS_IO_REDIST
655 return StringExtensions.Create(
656 #else
657 return string.Create(
658 #endif
659 first.Length + second.Length + (hasSeparator ? 0 : 1),
660 (First: (IntPtr)f, FirstLength: first.Length, Second: (IntPtr)s, SecondLength: second.Length, HasSeparator: hasSeparator),
661 (destination, state) =>
663 new Span<char>((char*)state.First, state.FirstLength).CopyTo(destination);
664 if (!state.HasSeparator)
665 destination[state.FirstLength] = PathInternal.DirectorySeparatorChar;
666 new Span<char>((char*)state.Second, state.SecondLength).CopyTo(destination.Slice(state.FirstLength + (state.HasSeparator ? 0 : 1)));
671 private static unsafe string JoinInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third)
673 Debug.Assert(first.Length > 0 && second.Length > 0 && third.Length > 0, "should have dealt with empty paths");
675 bool firstHasSeparator = PathInternal.IsDirectorySeparator(first[first.Length - 1])
676 || PathInternal.IsDirectorySeparator(second[0]);
677 bool thirdHasSeparator = PathInternal.IsDirectorySeparator(second[second.Length - 1])
678 || PathInternal.IsDirectorySeparator(third[0]);
680 fixed (char* f = &MemoryMarshal.GetReference(first), s = &MemoryMarshal.GetReference(second), t = &MemoryMarshal.GetReference(third))
682 #if MS_IO_REDIST
683 return StringExtensions.Create(
684 #else
685 return string.Create(
686 #endif
687 first.Length + second.Length + third.Length + (firstHasSeparator ? 0 : 1) + (thirdHasSeparator ? 0 : 1),
688 (First: (IntPtr)f, FirstLength: first.Length, Second: (IntPtr)s, SecondLength: second.Length,
689 Third: (IntPtr)t, ThirdLength: third.Length, FirstHasSeparator: firstHasSeparator, ThirdHasSeparator: thirdHasSeparator),
690 (destination, state) =>
692 new Span<char>((char*)state.First, state.FirstLength).CopyTo(destination);
693 if (!state.FirstHasSeparator)
694 destination[state.FirstLength] = PathInternal.DirectorySeparatorChar;
695 new Span<char>((char*)state.Second, state.SecondLength).CopyTo(destination.Slice(state.FirstLength + (state.FirstHasSeparator ? 0 : 1)));
696 if (!state.ThirdHasSeparator)
697 destination[destination.Length - state.ThirdLength - 1] = PathInternal.DirectorySeparatorChar;
698 new Span<char>((char*)state.Third, state.ThirdLength).CopyTo(destination.Slice(destination.Length - state.ThirdLength));
703 private static unsafe string JoinInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third, ReadOnlySpan<char> fourth)
705 Debug.Assert(first.Length > 0 && second.Length > 0 && third.Length > 0 && fourth.Length > 0, "should have dealt with empty paths");
707 bool firstHasSeparator = PathInternal.IsDirectorySeparator(first[first.Length - 1])
708 || PathInternal.IsDirectorySeparator(second[0]);
709 bool thirdHasSeparator = PathInternal.IsDirectorySeparator(second[second.Length - 1])
710 || PathInternal.IsDirectorySeparator(third[0]);
711 bool fourthHasSeparator = PathInternal.IsDirectorySeparator(third[third.Length - 1])
712 || PathInternal.IsDirectorySeparator(fourth[0]);
714 fixed (char* f = &MemoryMarshal.GetReference(first), s = &MemoryMarshal.GetReference(second), t = &MemoryMarshal.GetReference(third), u = &MemoryMarshal.GetReference(fourth))
717 #if MS_IO_REDIST
718 return StringExtensions.Create(
719 #else
720 return string.Create(
721 #endif
722 first.Length + second.Length + third.Length + fourth.Length + (firstHasSeparator ? 0 : 1) + (thirdHasSeparator ? 0 : 1) + (fourthHasSeparator ? 0 : 1),
723 (First: (IntPtr)f, FirstLength: first.Length, Second: (IntPtr)s, SecondLength: second.Length,
724 Third: (IntPtr)t, ThirdLength: third.Length, Fourth: (IntPtr)u, FourthLength:fourth.Length,
725 FirstHasSeparator: firstHasSeparator, ThirdHasSeparator: thirdHasSeparator, FourthHasSeparator: fourthHasSeparator),
726 (destination, state) =>
728 new Span<char>((char*)state.First, state.FirstLength).CopyTo(destination);
729 if (!state.FirstHasSeparator)
730 destination[state.FirstLength] = PathInternal.DirectorySeparatorChar;
731 new Span<char>((char*)state.Second, state.SecondLength).CopyTo(destination.Slice(state.FirstLength + (state.FirstHasSeparator ? 0 : 1)));
732 if (!state.ThirdHasSeparator)
733 destination[state.FirstLength + state.SecondLength + (state.FirstHasSeparator ? 0 : 1)] = PathInternal.DirectorySeparatorChar;
734 new Span<char>((char*)state.Third, state.ThirdLength).CopyTo(destination.Slice(state.FirstLength + state.SecondLength + (state.FirstHasSeparator ? 0 : 1) + (state.ThirdHasSeparator ? 0 : 1)));
735 if (!state.FourthHasSeparator)
736 destination[destination.Length - state.FourthLength - 1] = PathInternal.DirectorySeparatorChar;
737 new Span<char>((char*)state.Fourth, state.FourthLength).CopyTo(destination.Slice(destination.Length - state.FourthLength));
742 private static ReadOnlySpan<byte> Base32Char => new byte[32] { // uses C# compiler's optimization for static byte[] data
743 (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', (byte)'h',
744 (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', (byte)'o', (byte)'p',
745 (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', (byte)'v', (byte)'w', (byte)'x',
746 (byte)'y', (byte)'z', (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5'};
748 private static unsafe void Populate83FileNameFromRandomBytes(byte* bytes, int byteCount, Span<char> chars)
750 // This method requires bytes of length 8 and chars of length 12.
751 Debug.Assert(bytes != null);
752 Debug.Assert(byteCount == 8, $"Unexpected {nameof(byteCount)}");
753 Debug.Assert(chars.Length == 12, $"Unexpected {nameof(chars)}.Length");
755 byte b0 = bytes[0];
756 byte b1 = bytes[1];
757 byte b2 = bytes[2];
758 byte b3 = bytes[3];
759 byte b4 = bytes[4];
761 // write to chars[11] first in order to eliminate redundant bounds checks
762 chars[11] = (char)Base32Char[bytes[7] & 0x1F];
764 // Consume the 5 Least significant bits of the first 5 bytes
765 chars[0] = (char)Base32Char[b0 & 0x1F];
766 chars[1] = (char)Base32Char[b1 & 0x1F];
767 chars[2] = (char)Base32Char[b2 & 0x1F];
768 chars[3] = (char)Base32Char[b3 & 0x1F];
769 chars[4] = (char)Base32Char[b4 & 0x1F];
771 // Consume 3 MSB of b0, b1, MSB bits 6, 7 of b3, b4
772 chars[5] = (char)Base32Char[
773 ((b0 & 0xE0) >> 5) |
774 ((b3 & 0x60) >> 2)];
776 chars[6] = (char)Base32Char[
777 ((b1 & 0xE0) >> 5) |
778 ((b4 & 0x60) >> 2)];
780 // Consume 3 MSB bits of b2, 1 MSB bit of b3, b4
781 b2 >>= 5;
783 Debug.Assert(((b2 & 0xF8) == 0), "Unexpected set bits");
785 if ((b3 & 0x80) != 0)
786 b2 |= 0x08;
787 if ((b4 & 0x80) != 0)
788 b2 |= 0x10;
790 chars[7] = (char)Base32Char[b2];
792 // Set the file extension separator
793 chars[8] = '.';
795 // Consume the 5 Least significant bits of the remaining 3 bytes
796 chars[9] = (char)Base32Char[bytes[5] & 0x1F];
797 chars[10] = (char)Base32Char[bytes[6] & 0x1F];
800 /// <summary>
801 /// Create a relative path from one path to another. Paths will be resolved before calculating the difference.
802 /// Default path comparison for the active platform will be used (OrdinalIgnoreCase for Windows or Mac, Ordinal for Unix).
803 /// </summary>
804 /// <param name="relativeTo">The source path the output should be relative to. This path is always considered to be a directory.</param>
805 /// <param name="path">The destination path.</param>
806 /// <returns>The relative path or <paramref name="path"/> if the paths don't share the same root.</returns>
807 /// <exception cref="ArgumentNullException">Thrown if <paramref name="relativeTo"/> or <paramref name="path"/> is <c>null</c> or an empty string.</exception>
808 public static string GetRelativePath(string relativeTo, string path)
810 return GetRelativePath(relativeTo, path, StringComparison);
813 private static string GetRelativePath(string relativeTo, string path, StringComparison comparisonType)
815 if (string.IsNullOrEmpty(relativeTo)) throw new ArgumentNullException(nameof(relativeTo));
816 if (PathInternal.IsEffectivelyEmpty(path.AsSpan())) throw new ArgumentNullException(nameof(path));
817 Debug.Assert(comparisonType == StringComparison.Ordinal || comparisonType == StringComparison.OrdinalIgnoreCase);
819 relativeTo = GetFullPath(relativeTo);
820 path = GetFullPath(path);
822 // Need to check if the roots are different- if they are we need to return the "to" path.
823 if (!PathInternal.AreRootsEqual(relativeTo, path, comparisonType))
824 return path;
826 int commonLength = PathInternal.GetCommonPathLength(relativeTo, path, ignoreCase: comparisonType == StringComparison.OrdinalIgnoreCase);
828 // If there is nothing in common they can't share the same root, return the "to" path as is.
829 if (commonLength == 0)
830 return path;
832 // Trailing separators aren't significant for comparison
833 int relativeToLength = relativeTo.Length;
834 if (PathInternal.EndsInDirectorySeparator(relativeTo.AsSpan()))
835 relativeToLength--;
837 bool pathEndsInSeparator = PathInternal.EndsInDirectorySeparator(path.AsSpan());
838 int pathLength = path.Length;
839 if (pathEndsInSeparator)
840 pathLength--;
842 // If we have effectively the same path, return "."
843 if (relativeToLength == pathLength && commonLength >= relativeToLength) return ".";
845 // We have the same root, we need to calculate the difference now using the
846 // common Length and Segment count past the length.
848 // Some examples:
850 // C:\Foo C:\Bar L3, S1 -> ..\Bar
851 // C:\Foo C:\Foo\Bar L6, S0 -> Bar
852 // C:\Foo\Bar C:\Bar\Bar L3, S2 -> ..\..\Bar\Bar
853 // C:\Foo\Foo C:\Foo\Bar L7, S1 -> ..\Bar
855 StringBuilder sb = StringBuilderCache.Acquire(Math.Max(relativeTo.Length, path.Length));
857 // Add parent segments for segments past the common on the "from" path
858 if (commonLength < relativeToLength)
860 sb.Append("..");
862 for (int i = commonLength + 1; i < relativeToLength; i++)
864 if (PathInternal.IsDirectorySeparator(relativeTo[i]))
866 sb.Append(DirectorySeparatorChar);
867 sb.Append("..");
871 else if (PathInternal.IsDirectorySeparator(path[commonLength]))
873 // No parent segments and we need to eat the initial separator
874 // (C:\Foo C:\Foo\Bar case)
875 commonLength++;
878 // Now add the rest of the "to" path, adding back the trailing separator
879 int differenceLength = pathLength - commonLength;
880 if (pathEndsInSeparator)
881 differenceLength++;
883 if (differenceLength > 0)
885 if (sb.Length > 0)
887 sb.Append(DirectorySeparatorChar);
890 sb.Append(path, commonLength, differenceLength);
893 return StringBuilderCache.GetStringAndRelease(sb);
896 /// <summary>Returns a comparison that can be used to compare file and directory names for equality.</summary>
897 internal static StringComparison StringComparison
901 return IsCaseSensitive ?
902 StringComparison.Ordinal :
903 StringComparison.OrdinalIgnoreCase;