|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Text;
namespace System.IO
{
// Provides methods for processing file system strings in a cross-platform manner.
// Most of the methods don't do a complete parsing (such as examining a UNC hostname),
// but they will handle most string operations.
public static partial class Path
{
// Public static readonly variant of the separators. The Path implementation itself is using
// internal const variant of the separators for better performance.
public static readonly char DirectorySeparatorChar = PathInternal.DirectorySeparatorChar;
public static readonly char AltDirectorySeparatorChar = PathInternal.AltDirectorySeparatorChar;
public static readonly char VolumeSeparatorChar = PathInternal.VolumeSeparatorChar;
public static readonly char PathSeparator = PathInternal.PathSeparator;
// For generating random file names
// 8 random bytes provides 12 chars in our encoding for the 8.3 name.
private const int KeyLength = 8;
[Obsolete("Path.InvalidPathChars has been deprecated. Use GetInvalidPathChars or GetInvalidFileNameChars instead.")]
public static readonly char[] InvalidPathChars = GetInvalidPathChars();
// Changes the extension of a file path. The path parameter
// specifies a file path, and the extension parameter
// specifies a file extension (with a leading period, such as
// ".exe" or ".cs").
//
// The function returns a file path with the same root, directory, and base
// name parts as path, but with the file extension changed to
// the specified extension. If path is null, the function
// returns null. If path does not contain a file extension,
// the new file extension is appended to the path. If extension
// is null, any existing extension is removed from path.
[return: NotNullIfNotNull(nameof(path))]
public static string? ChangeExtension(string? path, string? extension)
{
if (path == null)
return null;
int subLength = path.Length;
if (subLength == 0)
return string.Empty;
for (int i = path.Length - 1; i >= 0; i--)
{
char ch = path[i];
if (ch == '.')
{
subLength = i;
break;
}
if (PathInternal.IsDirectorySeparator(ch))
{
break;
}
}
if (extension == null)
{
return path.Substring(0, subLength);
}
ReadOnlySpan<char> subpath = path.AsSpan(0, subLength);
return extension.StartsWith('.') ?
string.Concat(subpath, extension) :
string.Concat(subpath, ".", extension);
}
/// <summary>
/// Determines whether the specified file or directory exists.
/// </summary>
/// <remarks>
/// Unlike <see cref="File.Exists(string?)"/> it returns true for existing, non-regular files like pipes.
/// If the path targets an existing link, but the target of the link does not exist, it returns true.
/// </remarks>
/// <param name="path">The path to check</param>
/// <returns>
/// <see langword="true" /> if the caller has the required permissions and <paramref name="path" /> contains
/// the name of an existing file or directory; otherwise, <see langword="false" />.
/// This method also returns <see langword="false" /> if <paramref name="path" /> is <see langword="null" />,
/// an invalid path, or a zero-length string. If the caller does not have sufficient permissions to read the specified path,
/// no exception is thrown and the method returns <see langword="false" /> regardless of the existence of <paramref name="path" />.
/// </returns>
public static bool Exists([NotNullWhen(true)] string? path)
{
if (string.IsNullOrEmpty(path))
{
return false;
}
string? fullPath;
try
{
fullPath = GetFullPath(path);
}
catch (Exception ex) when (ex is ArgumentException or IOException or UnauthorizedAccessException)
{
return false;
}
bool result = ExistsCore(fullPath, out bool isDirectory);
if (result && PathInternal.IsDirectorySeparator(fullPath[^1]))
{
// Some sys-calls remove all trailing slashes and may give false positives for existing files.
// We want to make sure that if the path ends in a trailing slash, it's truly a directory.
return isDirectory;
}
return result;
}
/// <summary>
/// Returns the directory portion of a file path. This method effectively
/// removes the last segment of the given file path, i.e. it returns a
/// string consisting of all characters up to but not including the last
/// backslash ("\") in the file path. The returned value is null if the
/// specified path is null, empty, or a root (such as "\", "C:", or
/// "\\server\share").
/// </summary>
/// <remarks>
/// Directory separators are normalized in the returned string.
/// </remarks>
public static string? GetDirectoryName(string? path)
{
if (path == null || PathInternal.IsEffectivelyEmpty(path.AsSpan()))
return null;
int end = GetDirectoryNameOffset(path.AsSpan());
return end >= 0 ? PathInternal.NormalizeDirectorySeparators(path.Substring(0, end)) : null;
}
/// <summary>
/// Returns the directory portion of a file path. The returned value is empty
/// if the specified path is null, empty, or a root (such as "\", "C:", or
/// "\\server\share").
/// </summary>
/// <remarks>
/// Unlike the string overload, this method will not normalize directory separators.
/// </remarks>
public static ReadOnlySpan<char> GetDirectoryName(ReadOnlySpan<char> path)
{
if (PathInternal.IsEffectivelyEmpty(path))
return ReadOnlySpan<char>.Empty;
int end = GetDirectoryNameOffset(path);
return end >= 0 ? path.Slice(0, end) : ReadOnlySpan<char>.Empty;
}
internal static int GetDirectoryNameOffset(ReadOnlySpan<char> path)
{
int rootLength = PathInternal.GetRootLength(path);
int end = path.Length;
if (end <= rootLength)
return -1;
while (end > rootLength && !PathInternal.IsDirectorySeparator(path[--end])) ;
// Trim off any remaining separators (to deal with C:\foo\\bar)
while (end > rootLength && PathInternal.IsDirectorySeparator(path[end - 1]))
end--;
return end;
}
/// <summary>
/// Returns the extension of the given path. The returned value includes the period (".") character of the
/// extension except when you have a terminal period when you get string.Empty, such as ".exe" or ".cpp".
/// The returned value is null if the given path is null or empty if the given path does not include an
/// extension.
/// </summary>
[return: NotNullIfNotNull(nameof(path))]
public static string? GetExtension(string? path)
{
if (path == null)
return null;
return GetExtension(path.AsSpan()).ToString();
}
/// <summary>
/// Returns the extension of the given path.
/// </summary>
/// <remarks>
/// The returned value is an empty ReadOnlySpan if the given path does not include an extension.
/// </remarks>
public static ReadOnlySpan<char> GetExtension(ReadOnlySpan<char> path)
{
int length = path.Length;
for (int i = length - 1; i >= 0; i--)
{
char ch = path[i];
if (ch == '.')
{
if (i != length - 1)
return path.Slice(i, length - i);
else
return ReadOnlySpan<char>.Empty;
}
if (PathInternal.IsDirectorySeparator(ch))
break;
}
return ReadOnlySpan<char>.Empty;
}
/// <summary>
/// Returns the name and extension parts of the given path. The resulting string contains
/// the characters of path that follow the last separator in path. The resulting string is
/// null if path is null.
/// </summary>
[return: NotNullIfNotNull(nameof(path))]
public static string? GetFileName(string? path)
{
if (path == null)
return null;
ReadOnlySpan<char> result = GetFileName(path.AsSpan());
if (path.Length == result.Length)
return path;
return result.ToString();
}
/// <summary>
/// The returned ReadOnlySpan contains the characters of the path that follows the last separator in path.
/// </summary>
public static ReadOnlySpan<char> GetFileName(ReadOnlySpan<char> path)
{
int root = GetPathRoot(path).Length;
// We don't want to cut off "C:\file.txt:stream" (i.e. should be "file.txt:stream")
// but we *do* want "C:Foo" => "Foo". This necessitates checking for the root.
int i = PathInternal.DirectorySeparatorChar == PathInternal.AltDirectorySeparatorChar ?
path.LastIndexOf(PathInternal.DirectorySeparatorChar) :
path.LastIndexOfAny(PathInternal.DirectorySeparatorChar, PathInternal.AltDirectorySeparatorChar);
return path.Slice(i < root ? root : i + 1);
}
[return: NotNullIfNotNull(nameof(path))]
public static string? GetFileNameWithoutExtension(string? path)
{
if (path == null)
return null;
ReadOnlySpan<char> result = GetFileNameWithoutExtension(path.AsSpan());
if (path.Length == result.Length)
return path;
return result.ToString();
}
/// <summary>
/// Returns the characters between the last separator and last (.) in the path.
/// </summary>
public static ReadOnlySpan<char> GetFileNameWithoutExtension(ReadOnlySpan<char> path)
{
ReadOnlySpan<char> fileName = GetFileName(path);
int lastPeriod = fileName.LastIndexOf('.');
return lastPeriod < 0 ?
fileName : // No extension was found
fileName.Slice(0, lastPeriod);
}
/// <summary>
/// Returns a cryptographically strong random 8.3 string that can be
/// used as either a folder name or a file name.
/// </summary>
public static unsafe string GetRandomFileName()
{
byte* pKey = stackalloc byte[KeyLength];
Interop.GetRandomBytes(pKey, KeyLength);
return string.Create(
12, (IntPtr)pKey, (span, key) => // 12 == 8 + 1 (for period) + 3
Populate83FileNameFromRandomBytes((byte*)key, KeyLength, span));
}
/// <summary>
/// Returns true if the path is fixed to a specific drive or UNC path. This method does no
/// validation of the path (URIs will be returned as relative as a result).
/// Returns false if the path specified is relative to the current drive or working directory.
/// </summary>
/// <remarks>
/// Handles paths that use the alternate directory separator. It is a frequent mistake to
/// assume that rooted paths <see cref="IsPathRooted(string)"/> are not relative. This isn't the case.
/// "C:a" is drive relative- meaning that it will be resolved against the current directory
/// for C: (rooted, but relative). "C:\a" is rooted and not relative (the current directory
/// will not be used to modify the path).
/// </remarks>
/// <exception cref="ArgumentNullException">
/// Thrown if <paramref name="path"/> is null.
/// </exception>
public static bool IsPathFullyQualified(string path)
{
ArgumentNullException.ThrowIfNull(path);
return IsPathFullyQualified(path.AsSpan());
}
public static bool IsPathFullyQualified(ReadOnlySpan<char> path)
{
return !PathInternal.IsPartiallyQualified(path);
}
/// <summary>
/// Tests if a path's file name includes a file extension. A trailing period
/// is not considered an extension.
/// </summary>
public static bool HasExtension([NotNullWhen(true)] string? path)
{
if (path != null)
{
return HasExtension(path.AsSpan());
}
return false;
}
public static bool HasExtension(ReadOnlySpan<char> path)
{
for (int i = path.Length - 1; i >= 0; i--)
{
char ch = path[i];
if (ch == '.')
{
return i != path.Length - 1;
}
if (PathInternal.IsDirectorySeparator(ch))
break;
}
return false;
}
public static string Combine(string path1, string path2)
{
ArgumentNullException.ThrowIfNull(path1);
ArgumentNullException.ThrowIfNull(path2);
return CombineInternal(path1, path2);
}
public static string Combine(string path1, string path2, string path3)
{
ArgumentNullException.ThrowIfNull(path1);
ArgumentNullException.ThrowIfNull(path2);
ArgumentNullException.ThrowIfNull(path3);
return CombineInternal(path1, path2, path3);
}
public static string Combine(string path1, string path2, string path3, string path4)
{
ArgumentNullException.ThrowIfNull(path1);
ArgumentNullException.ThrowIfNull(path2);
ArgumentNullException.ThrowIfNull(path3);
ArgumentNullException.ThrowIfNull(path4);
return CombineInternal(path1, path2, path3, path4);
}
public static string Combine(params string[] paths)
{
ArgumentNullException.ThrowIfNull(paths);
return Combine((ReadOnlySpan<string>)paths);
}
/// <summary>
/// Combines a span of strings into a path.
/// </summary>
/// <param name="paths">A span of parts of the path.</param>
/// <returns>The combined paths.</returns>
public static string Combine(params ReadOnlySpan<string> paths)
{
int maxSize = 0;
int firstComponent = 0;
// We have two passes, the first calculates how large a buffer to allocate and does some precondition
// checks on the paths passed in. The second actually does the combination.
for (int i = 0; i < paths.Length; i++)
{
ArgumentNullException.ThrowIfNull(paths[i], nameof(paths));
if (paths[i].Length == 0)
{
continue;
}
if (IsPathRooted(paths[i]))
{
firstComponent = i;
maxSize = paths[i].Length;
}
else
{
maxSize += paths[i].Length;
}
char ch = paths[i][^1];
if (!PathInternal.IsDirectorySeparator(ch))
maxSize++;
}
var builder = new ValueStringBuilder(stackalloc char[260]); // MaxShortPath on Windows
builder.EnsureCapacity(maxSize);
for (int i = firstComponent; i < paths.Length; i++)
{
if (paths[i].Length == 0)
{
continue;
}
if (builder.Length == 0)
{
builder.Append(paths[i]);
}
else
{
char ch = builder[^1];
if (!PathInternal.IsDirectorySeparator(ch))
{
builder.Append(PathInternal.DirectorySeparatorChar);
}
builder.Append(paths[i]);
}
}
return builder.ToString();
}
// Unlike Combine(), Join() methods do not consider rooting. They simply combine paths, ensuring that there
// is a directory separator between them.
public static string Join(ReadOnlySpan<char> path1, ReadOnlySpan<char> path2)
{
if (path1.Length == 0)
return path2.ToString();
if (path2.Length == 0)
return path1.ToString();
return JoinInternal(path1, path2);
}
public static string Join(ReadOnlySpan<char> path1, ReadOnlySpan<char> path2, ReadOnlySpan<char> path3)
{
if (path1.Length == 0)
return Join(path2, path3);
if (path2.Length == 0)
return Join(path1, path3);
if (path3.Length == 0)
return Join(path1, path2);
return JoinInternal(path1, path2, path3);
}
public static string Join(ReadOnlySpan<char> path1, ReadOnlySpan<char> path2, ReadOnlySpan<char> path3, ReadOnlySpan<char> path4)
{
if (path1.Length == 0)
return Join(path2, path3, path4);
if (path2.Length == 0)
return Join(path1, path3, path4);
if (path3.Length == 0)
return Join(path1, path2, path4);
if (path4.Length == 0)
return Join(path1, path2, path3);
return JoinInternal(path1, path2, path3, path4);
}
public static string Join(string? path1, string? path2)
{
if (string.IsNullOrEmpty(path1))
return path2 ?? string.Empty;
if (string.IsNullOrEmpty(path2))
return path1;
return JoinInternal(path1, path2);
}
public static string Join(string? path1, string? path2, string? path3)
{
if (string.IsNullOrEmpty(path1))
return Join(path2, path3);
if (string.IsNullOrEmpty(path2))
return Join(path1, path3);
if (string.IsNullOrEmpty(path3))
return Join(path1, path2);
return JoinInternal(path1, path2, path3);
}
public static string Join(string? path1, string? path2, string? path3, string? path4)
{
if (string.IsNullOrEmpty(path1))
return Join(path2, path3, path4);
if (string.IsNullOrEmpty(path2))
return Join(path1, path3, path4);
if (string.IsNullOrEmpty(path3))
return Join(path1, path2, path4);
if (string.IsNullOrEmpty(path4))
return Join(path1, path2, path3);
return JoinInternal(path1, path2, path3, path4);
}
public static string Join(params string?[] paths)
{
ArgumentNullException.ThrowIfNull(paths);
return Join((ReadOnlySpan<string?>)paths);
}
/// <summary>
/// Concatenates a span of paths into a single path.
/// </summary>
/// <param name="paths">A span of paths.</param>
/// <returns>The concatenated path.</returns>
public static string Join(params ReadOnlySpan<string?> paths)
{
if (paths.IsEmpty)
{
return string.Empty;
}
int maxSize = 0;
foreach (string? path in paths)
{
maxSize += path?.Length ?? 0;
}
maxSize += paths.Length - 1;
var builder = new ValueStringBuilder(stackalloc char[260]); // MaxShortPath on Windows
builder.EnsureCapacity(maxSize);
for (int i = 0; i < paths.Length; i++)
{
string? path = paths[i];
if (string.IsNullOrEmpty(path))
{
continue;
}
if (builder.Length == 0)
{
builder.Append(path);
}
else
{
if (!PathInternal.IsDirectorySeparator(builder[^1]) && !PathInternal.IsDirectorySeparator(path[0]))
{
builder.Append(PathInternal.DirectorySeparatorChar);
}
builder.Append(path);
}
}
return builder.ToString();
}
public static bool TryJoin(ReadOnlySpan<char> path1, ReadOnlySpan<char> path2, Span<char> destination, out int charsWritten)
{
charsWritten = 0;
if (path1.Length == 0 && path2.Length == 0)
return true;
if (path1.Length == 0 || path2.Length == 0)
{
ref ReadOnlySpan<char> pathToUse = ref path1.Length == 0 ? ref path2 : ref path1;
if (destination.Length < pathToUse.Length)
{
return false;
}
pathToUse.CopyTo(destination);
charsWritten = pathToUse.Length;
return true;
}
bool needsSeparator = !(EndsInDirectorySeparator(path1) || PathInternal.StartsWithDirectorySeparator(path2));
int charsNeeded = path1.Length + path2.Length + (needsSeparator ? 1 : 0);
if (destination.Length < charsNeeded)
return false;
path1.CopyTo(destination);
if (needsSeparator)
destination[path1.Length] = DirectorySeparatorChar;
path2.CopyTo(destination.Slice(path1.Length + (needsSeparator ? 1 : 0)));
charsWritten = charsNeeded;
return true;
}
public static bool TryJoin(ReadOnlySpan<char> path1, ReadOnlySpan<char> path2, ReadOnlySpan<char> path3, Span<char> destination, out int charsWritten)
{
charsWritten = 0;
if (path1.Length == 0 && path2.Length == 0 && path3.Length == 0)
return true;
if (path1.Length == 0)
return TryJoin(path2, path3, destination, out charsWritten);
if (path2.Length == 0)
return TryJoin(path1, path3, destination, out charsWritten);
if (path3.Length == 0)
return TryJoin(path1, path2, destination, out charsWritten);
int neededSeparators = EndsInDirectorySeparator(path1) || PathInternal.StartsWithDirectorySeparator(path2) ? 0 : 1;
bool needsSecondSeparator = !(EndsInDirectorySeparator(path2) || PathInternal.StartsWithDirectorySeparator(path3));
if (needsSecondSeparator)
neededSeparators++;
int charsNeeded = path1.Length + path2.Length + path3.Length + neededSeparators;
if (destination.Length < charsNeeded)
return false;
bool result = TryJoin(path1, path2, destination, out charsWritten);
Debug.Assert(result, "should never fail joining first two paths");
if (needsSecondSeparator)
destination[charsWritten++] = DirectorySeparatorChar;
path3.CopyTo(destination.Slice(charsWritten));
charsWritten += path3.Length;
return true;
}
private static string CombineInternal(string first, string second)
{
if (string.IsNullOrEmpty(first))
return second;
if (string.IsNullOrEmpty(second))
return first;
if (IsPathRooted(second.AsSpan()))
return second;
return JoinInternal(first.AsSpan(), second.AsSpan());
}
private static string CombineInternal(string first, string second, string third)
{
if (string.IsNullOrEmpty(first))
return CombineInternal(second, third);
if (string.IsNullOrEmpty(second))
return CombineInternal(first, third);
if (string.IsNullOrEmpty(third))
return CombineInternal(first, second);
if (IsPathRooted(third.AsSpan()))
return third;
if (IsPathRooted(second.AsSpan()))
return CombineInternal(second, third);
return JoinInternal(first.AsSpan(), second.AsSpan(), third.AsSpan());
}
private static string CombineInternal(string first, string second, string third, string fourth)
{
if (string.IsNullOrEmpty(first))
return CombineInternal(second, third, fourth);
if (string.IsNullOrEmpty(second))
return CombineInternal(first, third, fourth);
if (string.IsNullOrEmpty(third))
return CombineInternal(first, second, fourth);
if (string.IsNullOrEmpty(fourth))
return CombineInternal(first, second, third);
if (IsPathRooted(fourth.AsSpan()))
return fourth;
if (IsPathRooted(third.AsSpan()))
return CombineInternal(third, fourth);
if (IsPathRooted(second.AsSpan()))
return CombineInternal(second, third, fourth);
return JoinInternal(first.AsSpan(), second.AsSpan(), third.AsSpan(), fourth.AsSpan());
}
private static string JoinInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second)
{
Debug.Assert(first.Length > 0 && second.Length > 0, "should have dealt with empty paths");
bool hasSeparator = PathInternal.IsDirectorySeparator(first[^1]) || PathInternal.IsDirectorySeparator(second[0]);
return hasSeparator ?
string.Concat(first, second) :
string.Concat(first, PathInternal.DirectorySeparatorCharAsString, second);
}
private static string JoinInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third)
{
Debug.Assert(first.Length > 0 && second.Length > 0 && third.Length > 0, "should have dealt with empty paths");
bool firstHasSeparator = PathInternal.IsDirectorySeparator(first[^1]) || PathInternal.IsDirectorySeparator(second[0]);
bool secondHasSeparator = PathInternal.IsDirectorySeparator(second[^1]) || PathInternal.IsDirectorySeparator(third[0]);
return (firstHasSeparator, secondHasSeparator) switch
{
(false, false) => string.Concat(first, PathInternal.DirectorySeparatorCharAsString, second, PathInternal.DirectorySeparatorCharAsString, third),
(false, true) => string.Concat(first, PathInternal.DirectorySeparatorCharAsString, second, third),
(true, false) => string.Concat(first, second, PathInternal.DirectorySeparatorCharAsString, third),
(true, true) => string.Concat(first, second, third),
};
}
private static unsafe string JoinInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third, ReadOnlySpan<char> fourth)
{
Debug.Assert(first.Length > 0 && second.Length > 0 && third.Length > 0 && fourth.Length > 0, "should have dealt with empty paths");
var state = new JoinInternalState
{
ReadOnlySpanPtr1 = (IntPtr)(&first),
ReadOnlySpanPtr2 = (IntPtr)(&second),
ReadOnlySpanPtr3 = (IntPtr)(&third),
ReadOnlySpanPtr4 = (IntPtr)(&fourth),
NeedSeparator1 = PathInternal.IsDirectorySeparator(first[^1]) || PathInternal.IsDirectorySeparator(second[0]) ? (byte)0 : (byte)1,
NeedSeparator2 = PathInternal.IsDirectorySeparator(second[^1]) || PathInternal.IsDirectorySeparator(third[0]) ? (byte)0 : (byte)1,
NeedSeparator3 = PathInternal.IsDirectorySeparator(third[^1]) || PathInternal.IsDirectorySeparator(fourth[0]) ? (byte)0 : (byte)1,
};
return string.Create(
first.Length + second.Length + third.Length + fourth.Length + state.NeedSeparator1 + state.NeedSeparator2 + state.NeedSeparator3,
state,
static (destination, state) =>
{
ReadOnlySpan<char> first = *(ReadOnlySpan<char>*)state.ReadOnlySpanPtr1;
first.CopyTo(destination);
destination = destination.Slice(first.Length);
if (state.NeedSeparator1 != 0)
{
destination[0] = PathInternal.DirectorySeparatorChar;
destination = destination.Slice(1);
}
ReadOnlySpan<char> second = *(ReadOnlySpan<char>*)state.ReadOnlySpanPtr2;
second.CopyTo(destination);
destination = destination.Slice(second.Length);
if (state.NeedSeparator2 != 0)
{
destination[0] = PathInternal.DirectorySeparatorChar;
destination = destination.Slice(1);
}
ReadOnlySpan<char> third = *(ReadOnlySpan<char>*)state.ReadOnlySpanPtr3;
third.CopyTo(destination);
destination = destination.Slice(third.Length);
if (state.NeedSeparator3 != 0)
{
destination[0] = PathInternal.DirectorySeparatorChar;
destination = destination.Slice(1);
}
ReadOnlySpan<char> fourth = *(ReadOnlySpan<char>*)state.ReadOnlySpanPtr4;
Debug.Assert(fourth.Length == destination.Length);
fourth.CopyTo(destination);
});
}
private struct JoinInternalState // used to avoid rooting ValueTuple`7
{
public IntPtr ReadOnlySpanPtr1, ReadOnlySpanPtr2, ReadOnlySpanPtr3, ReadOnlySpanPtr4;
public byte NeedSeparator1, NeedSeparator2, NeedSeparator3;
}
private static ReadOnlySpan<byte> Base32Char => "abcdefghijklmnopqrstuvwxyz012345"u8;
internal static unsafe void Populate83FileNameFromRandomBytes(byte* bytes, int byteCount, Span<char> chars)
{
// This method requires bytes of length 8 and chars of length 12.
Debug.Assert(bytes != null);
Debug.Assert(byteCount == 8, $"Unexpected {nameof(byteCount)}");
Debug.Assert(chars.Length == 12, $"Unexpected {nameof(chars)}.Length");
byte b0 = bytes[0];
byte b1 = bytes[1];
byte b2 = bytes[2];
byte b3 = bytes[3];
byte b4 = bytes[4];
// write to chars[11] first in order to eliminate redundant bounds checks
chars[11] = (char)Base32Char[bytes[7] & 0x1F];
// Consume the 5 Least significant bits of the first 5 bytes
chars[0] = (char)Base32Char[b0 & 0x1F];
chars[1] = (char)Base32Char[b1 & 0x1F];
chars[2] = (char)Base32Char[b2 & 0x1F];
chars[3] = (char)Base32Char[b3 & 0x1F];
chars[4] = (char)Base32Char[b4 & 0x1F];
// Consume 3 MSB of b0, b1, MSB bits 6, 7 of b3, b4
chars[5] = (char)Base32Char[
((b0 & 0xE0) >> 5) |
((b3 & 0x60) >> 2)];
chars[6] = (char)Base32Char[
((b1 & 0xE0) >> 5) |
((b4 & 0x60) >> 2)];
// Consume 3 MSB bits of b2, 1 MSB bit of b3, b4
b2 >>= 5;
Debug.Assert((b2 & 0xF8) == 0, "Unexpected set bits");
if ((b3 & 0x80) != 0)
b2 |= 0x08;
if ((b4 & 0x80) != 0)
b2 |= 0x10;
chars[7] = (char)Base32Char[b2];
// Set the file extension separator
chars[8] = '.';
// Consume the 5 Least significant bits of the remaining 3 bytes
chars[9] = (char)Base32Char[bytes[5] & 0x1F];
chars[10] = (char)Base32Char[bytes[6] & 0x1F];
}
/// <summary>
/// Create a relative path from one path to another. Paths will be resolved before calculating the difference.
/// Default path comparison for the active platform will be used (OrdinalIgnoreCase for Windows or Mac, Ordinal for Unix).
/// </summary>
/// <param name="relativeTo">The source path the output should be relative to. This path is always considered to be a directory.</param>
/// <param name="path">The destination path.</param>
/// <returns>The relative path or <paramref name="path"/> if the paths don't share the same root.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="relativeTo"/> or <paramref name="path"/> is <c>null</c> or an empty string.</exception>
public static string GetRelativePath(string relativeTo, string path)
{
return GetRelativePath(relativeTo, path, PathInternal.StringComparison);
}
private static string GetRelativePath(string relativeTo, string path, StringComparison comparisonType)
{
ArgumentNullException.ThrowIfNull(relativeTo);
ArgumentNullException.ThrowIfNull(path);
if (PathInternal.IsEffectivelyEmpty(relativeTo.AsSpan()))
throw new ArgumentException(SR.Arg_PathEmpty, nameof(relativeTo));
if (PathInternal.IsEffectivelyEmpty(path.AsSpan()))
throw new ArgumentException(SR.Arg_PathEmpty, nameof(path));
Debug.Assert(comparisonType == StringComparison.Ordinal || comparisonType == StringComparison.OrdinalIgnoreCase);
relativeTo = GetFullPath(relativeTo);
path = GetFullPath(path);
// Need to check if the roots are different- if they are we need to return the "to" path.
if (!PathInternal.AreRootsEqual(relativeTo, path, comparisonType))
return path;
int commonLength = PathInternal.GetCommonPathLength(relativeTo, path, ignoreCase: comparisonType == StringComparison.OrdinalIgnoreCase);
// If there is nothing in common they can't share the same root, return the "to" path as is.
if (commonLength == 0)
return path;
// Trailing separators aren't significant for comparison
int relativeToLength = relativeTo.Length;
if (EndsInDirectorySeparator(relativeTo.AsSpan()))
relativeToLength--;
bool pathEndsInSeparator = EndsInDirectorySeparator(path.AsSpan());
int pathLength = path.Length;
if (pathEndsInSeparator)
pathLength--;
// If we have effectively the same path, return "."
if (relativeToLength == pathLength && commonLength >= relativeToLength) return ".";
// We have the same root, we need to calculate the difference now using the
// common Length and Segment count past the length.
//
// Some examples:
//
// C:\Foo C:\Bar L3, S1 -> ..\Bar
// C:\Foo C:\Foo\Bar L6, S0 -> Bar
// C:\Foo\Bar C:\Bar\Bar L3, S2 -> ..\..\Bar\Bar
// C:\Foo\Foo C:\Foo\Bar L7, S1 -> ..\Bar
var sb = new ValueStringBuilder(stackalloc char[260]);
sb.EnsureCapacity(Math.Max(relativeTo.Length, path.Length));
// Add parent segments for segments past the common on the "from" path
if (commonLength < relativeToLength)
{
sb.Append("..");
for (int i = commonLength + 1; i < relativeToLength; i++)
{
if (PathInternal.IsDirectorySeparator(relativeTo[i]))
{
sb.Append(DirectorySeparatorChar);
sb.Append("..");
}
}
}
else if (PathInternal.IsDirectorySeparator(path[commonLength]))
{
// No parent segments and we need to eat the initial separator
// (C:\Foo C:\Foo\Bar case)
commonLength++;
}
// Now add the rest of the "to" path, adding back the trailing separator
int differenceLength = pathLength - commonLength;
if (pathEndsInSeparator)
differenceLength++;
if (differenceLength > 0)
{
if (sb.Length > 0)
{
sb.Append(DirectorySeparatorChar);
}
sb.Append(path.AsSpan(commonLength, differenceLength));
}
return sb.ToString();
}
/// <summary>
/// Trims one trailing directory separator beyond the root of the path.
/// </summary>
public static string TrimEndingDirectorySeparator(string path) => PathInternal.TrimEndingDirectorySeparator(path);
/// <summary>
/// Trims one trailing directory separator beyond the root of the path.
/// </summary>
public static ReadOnlySpan<char> TrimEndingDirectorySeparator(ReadOnlySpan<char> path) => PathInternal.TrimEndingDirectorySeparator(path);
/// <summary>
/// Returns true if the path ends in a directory separator.
/// </summary>
public static bool EndsInDirectorySeparator(ReadOnlySpan<char> path) => PathInternal.EndsInDirectorySeparator(path);
/// <summary>
/// Returns true if the path ends in a directory separator.
/// </summary>
public static bool EndsInDirectorySeparator([NotNullWhen(true)] string? path) => PathInternal.EndsInDirectorySeparator(path);
}
}
|