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.
6 using System
.Diagnostics
;
7 using System
.Diagnostics
.CodeAnalysis
;
8 using System
.Runtime
.InteropServices
;
15 namespace Microsoft
.IO
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
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
)
56 int subLength
= path
.Length
;
60 for (int i
= path
.Length
- 1; i
>= 0; i
--)
70 if (PathInternal
.IsDirectorySeparator(ch
))
76 if (extension
== null)
78 return path
.Substring(0, subLength
);
81 ReadOnlySpan
<char> subpath
= path
.AsSpan(0, subLength
);
83 return extension
.Length
!= 0 && extension
[0] == '.' ?
84 StringExtensions
.Concat(subpath
, extension
.AsSpan()) :
85 StringExtensions
.Concat(subpath
, ".".AsSpan(), extension
.AsSpan());
87 return extension
.StartsWith('.') ?
88 string.Concat(subpath
, extension
) :
89 string.Concat(subpath
, ".", extension
);
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").
102 /// Directory separators are normalized in the returned string.
104 public static string? GetDirectoryName(string? path
)
106 if (path
== null || PathInternal
.IsEffectivelyEmpty(path
.AsSpan()))
109 int end
= GetDirectoryNameOffset(path
.AsSpan());
110 return end
>= 0 ? PathInternal
.NormalizeDirectorySeparators(path
.Substring(0, end
)) : null;
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").
119 /// Unlike the string overload, this method will not normalize directory separators.
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
)
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]))
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
152 [return: NotNullIfNotNull("path")]
153 public static string? GetExtension(string? path
)
158 return GetExtension(path
.AsSpan()).ToString();
162 /// Returns the extension of the given path.
165 /// The returned value is an empty ReadOnlySpan if the given path does not include an extension.
167 public static ReadOnlySpan
<char> GetExtension(ReadOnlySpan
<char> path
)
169 int length
= path
.Length
;
171 for (int i
= length
- 1; i
>= 0; i
--)
177 return path
.Slice(i
, length
- i
);
179 return ReadOnlySpan
<char>.Empty
;
181 if (PathInternal
.IsDirectorySeparator(ch
))
184 return ReadOnlySpan
<char>.Empty
;
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.
192 [return: NotNullIfNotNull("path")]
193 public static string? GetFileName(string? path
)
198 ReadOnlySpan
<char> result
= GetFileName(path
.AsSpan());
199 if (path
.Length
== result
.Length
)
202 return result
.ToString();
206 /// The returned ReadOnlySpan contains the characters of the path that follows the last separator in path.
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);
224 [return: NotNullIfNotNull("path")]
225 public static string? GetFileNameWithoutExtension(string? path
)
230 ReadOnlySpan
<char> result
= GetFileNameWithoutExtension(path
.AsSpan());
231 if (path
.Length
== result
.Length
)
234 return result
.ToString();
238 /// Returns the characters between the last separator and last (.) in the path.
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
);
250 /// Returns a cryptographically strong random 8.3 string that can be
251 /// used as either a folder name or a file name.
253 public static unsafe string GetRandomFileName()
255 byte* pKey
= stackalloc byte[KeyLength
];
256 Interop
.GetRandomBytes(pKey
, KeyLength
);
259 return StringExtensions
.Create(
261 return string.Create(
263 12, (IntPtr
)pKey
, (span
, key
) => // 12 == 8 + 1 (for period) + 3
264 Populate83FileNameFromRandomBytes((byte*)key
, KeyLength
, span
));
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.
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).
279 /// <exception cref="ArgumentNullException">
280 /// Thrown if <paramref name="path"/> is null.
282 public static bool IsPathFullyQualified(string path
)
285 throw new ArgumentNullException(nameof(path
));
287 return IsPathFullyQualified(path
.AsSpan());
290 public static bool IsPathFullyQualified(ReadOnlySpan
<char> path
)
292 return !PathInternal
.IsPartiallyQualified(path
);
296 /// Tests if a path's file name includes a file extension. A trailing period
297 /// is not considered an extension.
299 public static bool HasExtension(string? path
)
303 return HasExtension(path
.AsSpan());
308 public static bool HasExtension(ReadOnlySpan
<char> path
)
310 for (int i
= path
.Length
- 1; i
>= 0; i
--)
315 return i
!= path
.Length
- 1;
317 if (PathInternal
.IsDirectorySeparator(ch
))
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
)
351 throw new ArgumentNullException(nameof(paths
));
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)
372 if (IsPathRooted(paths
[i
]))
375 maxSize
= paths
[i
].Length
;
379 maxSize
+= paths
[i
].Length
;
382 char ch
= paths
[i
][paths
[i
].Length
- 1];
383 if (!PathInternal
.IsDirectorySeparator(ch
))
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)
398 if (builder
.Length
== 0)
400 builder
.Append(paths
[i
]);
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
)
480 throw new ArgumentNullException(nameof(paths
));
483 if (paths
.Length
== 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)
507 if (builder
.Length
== 0)
509 builder
.Append(path
);
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
)
528 if (path1
.Length
== 0 && path2
.Length
== 0)
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
)
539 pathToUse
.CopyTo(destination
);
540 charsWritten
= pathToUse
.Length
;
544 bool needsSeparator
= !(PathInternal
.EndsInDirectorySeparator(path1
) || PathInternal
.StartsWithDirectorySeparator(path2
));
545 int charsNeeded
= path1
.Length
+ path2
.Length
+ (needsSeparator
? 1 : 0);
546 if (destination
.Length
< charsNeeded
)
549 path1
.CopyTo(destination
);
551 destination
[path1
.Length
] = DirectorySeparatorChar
;
553 path2
.CopyTo(destination
.Slice(path1
.Length
+ (needsSeparator
? 1 : 0)));
555 charsWritten
= charsNeeded
;
559 public static bool TryJoin(ReadOnlySpan
<char> path1
, ReadOnlySpan
<char> path2
, ReadOnlySpan
<char> path3
, Span
<char> destination
, out int charsWritten
)
562 if (path1
.Length
== 0 && path2
.Length
== 0 && path3
.Length
== 0)
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
)
577 int charsNeeded
= path1
.Length
+ path2
.Length
+ path3
.Length
+ neededSeparators
;
578 if (destination
.Length
< charsNeeded
)
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
;
593 private static string CombineInternal(string first
, string second
)
595 if (string.IsNullOrEmpty(first
))
598 if (string.IsNullOrEmpty(second
))
601 if (IsPathRooted(second
.AsSpan()))
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()))
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()))
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
))
655 return StringExtensions
.Create(
657 return string.Create(
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
))
683 return StringExtensions
.Create(
685 return string.Create(
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
))
718 return StringExtensions
.Create(
720 return string.Create(
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");
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
[
776 chars
[6] = (char)Base32Char
[
780 // Consume 3 MSB bits of b2, 1 MSB bit of b3, b4
783 Debug
.Assert(((b2
& 0xF8) == 0), "Unexpected set bits");
785 if ((b3
& 0x80) != 0)
787 if ((b4
& 0x80) != 0)
790 chars
[7] = (char)Base32Char
[b2
];
792 // Set the file extension separator
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];
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).
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
))
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)
832 // Trailing separators aren't significant for comparison
833 int relativeToLength
= relativeTo
.Length
;
834 if (PathInternal
.EndsInDirectorySeparator(relativeTo
.AsSpan()))
837 bool pathEndsInSeparator
= PathInternal
.EndsInDirectorySeparator(path
.AsSpan());
838 int pathLength
= path
.Length
;
839 if (pathEndsInSeparator
)
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.
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
)
862 for (int i
= commonLength
+ 1; i
< relativeToLength
; i
++)
864 if (PathInternal
.IsDirectorySeparator(relativeTo
[i
]))
866 sb
.Append(DirectorySeparatorChar
);
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)
878 // Now add the rest of the "to" path, adding back the trailing separator
879 int differenceLength
= pathLength
- commonLength
;
880 if (pathEndsInSeparator
)
883 if (differenceLength
> 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
;