if (path == null)
return false;
+ return IsPathRooted(path.AsReadOnlySpan());
+ }
+
+ public static bool IsPathRooted(ReadOnlySpan<char> path)
+ {
return path.Length > 0 && path[0] == PathInternal.DirectorySeparatorChar;
}
return IsPathRooted(path) ? PathInternal.DirectorySeparatorCharAsString : String.Empty;
}
+ public static ReadOnlySpan<char> GetPathRoot(ReadOnlySpan<char> path)
+ {
+ return PathInternal.IsEffectivelyEmpty(path) && IsPathRooted(path) ? PathInternal.DirectorySeparatorCharAsString.AsReadOnlySpan() : ReadOnlySpan<char>.Empty;
+ }
+
/// <summary>Gets whether the system is case-sensitive.</summary>
internal static bool IsCaseSensitive
{
// if it starts with a backslash ("\") or a valid drive letter and a colon (":").
public static bool IsPathRooted(string path)
{
- if (path != null)
- {
- int length = path.Length;
- if ((length >= 1 && PathInternal.IsDirectorySeparator(path[0])) ||
- (length >= 2 && PathInternal.IsValidDriveChar(path[0]) && path[1] == PathInternal.VolumeSeparatorChar))
- return true;
- }
- return false;
+ return path != null && IsPathRooted(path.AsReadOnlySpan());
+ }
+
+ public static bool IsPathRooted(ReadOnlySpan<char> path)
+ {
+ int length = path.Length;
+ return (length >= 1 && PathInternal.IsDirectorySeparator(path[0])) || (length >= 2 && PathInternal.IsValidDriveChar(path[0]) && path[1] == PathInternal.VolumeSeparatorChar);
}
// Returns the root portion of the given path. The resulting string
// only contains whitespace characters an ArgumentException gets thrown.
public static string GetPathRoot(string path)
{
- if (path == null) return null;
+ if (path == null)
+ return null;
+
if (PathInternal.IsEffectivelyEmpty(path))
throw new ArgumentException(SR.Arg_PathEmpty, nameof(path));
- // Need to return the normalized directory separator
- path = PathInternal.NormalizeDirectorySeparators(path);
+ ReadOnlySpan<char> result = GetPathRoot(path.AsReadOnlySpan());
+ if (path.Length == result.Length)
+ return PathInternal.NormalizeDirectorySeparators(path);
+
+ return PathInternal.NormalizeDirectorySeparators(new string(result));
+ }
+
+ /// <remarks>
+ /// Unlike the string overload, this method will not normalize directory separators.
+ /// </remarks>
+ public static ReadOnlySpan<char> GetPathRoot(ReadOnlySpan<char> path)
+ {
+ if (PathInternal.IsEffectivelyEmpty(path))
+ return ReadOnlySpan<char>.Empty;
int pathRoot = PathInternal.GetRootLength(path);
- return pathRoot <= 0 ? string.Empty : path.Substring(0, pathRoot);
+ return pathRoot <= 0 ? ReadOnlySpan<char>.Empty : path.Slice(0, pathRoot);
}
/// <summary>Gets whether the system is case-sensitive.</summary>
throw new ArgumentException(SR.Arg_PathEmpty, nameof(path));
path = PathInternal.NormalizeDirectorySeparators(path);
- int root = PathInternal.GetRootLength(path);
+ int end = PathInternal.GetDirectoryNameOffset(path);
+ return end >= 0 ? path.Substring(0, end) : null;
+ }
- int i = path.Length;
- if (i > root)
- {
- while (i > root && !PathInternal.IsDirectorySeparator(path[--i])) ;
- return path.Substring(0, i);
- }
-
- return null;
+ /// <summary>
+ /// Returns the directory path of a file path.
+ /// The returned value the an empty ReadOnlySpan if path is empty or
+ /// if the file path denotes as 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 = PathInternal.GetDirectoryNameOffset(path);
+ return end >= 0 ? path.Slice(0, end) : ReadOnlySpan<char>.Empty;
}
// Returns the extension of the given path. The returned value includes the
if (path == null)
return null;
+ return new string(GetExtension(path.AsReadOnlySpan()));
+ }
+
+ /// <summary>
+ /// Returns the extension of the given path.
+ /// </summary>
+ /// <remarks>
+ /// The returned value is an empty ReadOnlySpan if the given path doesnot 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.Substring(i, length - i);
+ return path.Slice(i, length - i);
else
- return string.Empty;
+ return ReadOnlySpan<char>.Empty;
}
if (PathInternal.IsDirectoryOrVolumeSeparator(ch))
break;
}
- return string.Empty;
+ return ReadOnlySpan<char>.Empty;
}
// Returns the name and extension parts of the given path. The resulting
if (path == null)
return null;
+ ReadOnlySpan<char> result = GetFileName(path.AsReadOnlySpan());
+ if (path.Length == result.Length)
+ return path;
+
+ return new string(result);
+ }
+
+ /// <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 offset = PathInternal.FindFileNameIndex(path);
int count = path.Length - offset;
- return path.Substring(offset, count);
+ return path.Slice(offset, count);
}
public static string GetFileNameWithoutExtension(string path)
if (path == null)
return null;
+ ReadOnlySpan<char> result = GetFileNameWithoutExtension(path.AsReadOnlySpan());
+ if (path.Length == result.Length)
+ return path;
+
+ return new string(result);
+ }
+
+ /// <summary>
+ /// Returns the characters between the last separator and last (.) in the path.
+ /// </summary>
+ public static ReadOnlySpan<char> GetFileNameWithoutExtension(ReadOnlySpan<char> path)
+ {
int length = path.Length;
int offset = PathInternal.FindFileNameIndex(path);
- int end = path.LastIndexOf('.', length - 1, length - offset);
+ int end = path.Slice(offset, length - offset).LastIndexOf('.');
return end == -1 ?
- path.Substring(offset) : // No extension was found
- path.Substring(offset, end - offset);
+ path.Slice(offset) : // No extension was found
+ path.Slice(offset, end);
}
// Returns a cryptographically strong random 8.3 string that can be
{
throw new ArgumentNullException(nameof(path));
}
+ return IsPathFullyQualified(path.AsReadOnlySpan());
+ }
+
+ public static bool IsPathFullyQualified(ReadOnlySpan<char> path)
+ {
return !PathInternal.IsPartiallyQualified(path);
}
{
if (path != null)
{
- for (int i = path.Length - 1; i >= 0; i--)
+ return HasExtension(path.AsReadOnlySpan());
+ }
+ return false;
+ }
+
+ public static bool HasExtension(ReadOnlySpan<char> path)
+ {
+ for (int i = path.Length - 1; i >= 0; i--)
+ {
+ char ch = path[i];
+ if (ch == '.')
{
- char ch = path[i];
- if (ch == '.')
- {
- return i != path.Length - 1;
- }
- if (PathInternal.IsDirectoryOrVolumeSeparator(ch)) break;
+ return i != path.Length - 1;
}
+ if (PathInternal.IsDirectoryOrVolumeSeparator(ch))
+ break;
}
return false;
}
using System.Diagnostics;
using System.Text;
+using System.Runtime.InteropServices;
namespace System.IO
{
internal const string ParentDirectoryPrefix = @"../";
- internal static int GetRootLength(string path)
+ internal static int GetRootLength(ReadOnlySpan<char> path)
{
return path.Length > 0 && IsDirectorySeparator(path[0]) ? 1 : 0;
}
/// </summary>
internal static string NormalizeDirectorySeparators(string path)
{
- if (string.IsNullOrEmpty(path)) return path;
+ if (string.IsNullOrEmpty(path))
+ return path;
// Make a pass to see if we need to normalize so we can potentially skip allocating
bool normalized = true;
}
}
- if (normalized) return path;
+ if (normalized)
+ return path;
StringBuilder builder = new StringBuilder(path.Length);
return builder.ToString();
}
-
+
/// <summary>
/// Returns true if the character is a directory or volume separator.
/// </summary>
return ch == DirectorySeparatorChar;
}
- internal static bool IsPartiallyQualified(string path)
+ internal static bool IsPartiallyQualified(ReadOnlySpan<char> path)
{
// This is much simpler than Windows where paths can be rooted, but not fully qualified (such as Drive Relative)
// As long as the path is rooted in Unix it doesn't use the current directory and therefore is fully qualified.
{
return string.IsNullOrEmpty(path);
}
+
+ internal static bool IsEffectivelyEmpty(ReadOnlySpan<char> path)
+ {
+ return path.IsEmpty;
+ }
}
}
/// <summary>
/// Gets the length of the root of the path (drive, share, etc.).
/// </summary>
- internal unsafe static int GetRootLength(ref StringBuffer path)
+ internal static int GetRootLength(ref StringBuffer path)
{
- if (path.Length == 0) return 0;
+ if (path.Length == 0)
+ return 0;
- fixed (char* value = path.UnderlyingArray)
- {
- return GetRootLength(value, path.Length);
- }
+ return GetRootLength(new ReadOnlySpan<char>(path.UnderlyingArray, 0, path.Length));
}
/// <summary>
using System.Diagnostics;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
using System.Text;
namespace System.IO
/// <summary>
/// Returns true if the path uses any of the DOS device path syntaxes. ("\\.\", "\\?\", or "\??\")
/// </summary>
- internal static bool IsDevice(string path)
+ internal static bool IsDevice(ReadOnlySpan<char> path)
{
// If the path begins with any two separators is will be recognized and normalized and prepped with
// "\??\" for internal usage correctly. "\??\" is recognized and handled, "/??/" is not.
/// path matches exactly (cannot use alternate directory separators) Windows will skip normalization
/// and path length checks.
/// </summary>
- internal static bool IsExtended(string path)
+ internal static bool IsExtended(ReadOnlySpan<char> path)
{
// While paths like "//?/C:/" will work, they're treated the same as "\\.\" paths.
// Skipping of normalization will *only* occur if back slashes ('\') are used.
/// <summary>
/// Check for known wildcard characters. '*' and '?' are the most common ones.
/// </summary>
- internal static bool HasWildCardCharacters(string path)
+ internal static bool HasWildCardCharacters(ReadOnlySpan<char> path)
{
// Question mark is part of dos device syntax so we have to skip if we are
int startIndex = IsDevice(path) ? ExtendedPathPrefix.Length : 0;
/// <summary>
/// Gets the length of the root of the path (drive, share, etc.).
/// </summary>
- internal unsafe static int GetRootLength(string path)
- {
- fixed (char* value = path)
- {
- return GetRootLength(value, path.Length);
- }
- }
-
- private unsafe static int GetRootLength(char* path, int pathLength)
+ internal static int GetRootLength(ReadOnlySpan<char> path)
{
+ int pathLength = path.Length;
int i = 0;
int volumeSeparatorLength = 2; // Length to the colon "C:"
int uncRootLength = 2; // Length to the start of the server name "\\"
- bool extendedSyntax = StartsWithOrdinal(path, pathLength, ExtendedPathPrefix);
- bool extendedUncSyntax = StartsWithOrdinal(path, pathLength, UncExtendedPathPrefix);
+ bool extendedSyntax = StartsWithOrdinal(path, ExtendedPathPrefix);
+ bool extendedUncSyntax = StartsWithOrdinal(path, UncExtendedPathPrefix);
if (extendedSyntax)
{
// Shift the position we look for the root from to account for the extended prefix
// (e.g. to \\?\UNC\Server\Share or \\Server\Share\)
i = uncRootLength;
int n = 2; // Maximum separators to skip
- while (i < pathLength && (!IsDirectorySeparator(path[i]) || --n > 0)) i++;
+ while (i < pathLength && (!IsDirectorySeparator(path[i]) || --n > 0))
+ i++;
}
}
else if (pathLength >= volumeSeparatorLength && path[volumeSeparatorLength - 1] == VolumeSeparatorChar)
// Path is at least longer than where we expect a colon, and has a colon (\\?\A:, A:)
// If the colon is followed by a directory separator, move past it
i = volumeSeparatorLength;
- if (pathLength >= volumeSeparatorLength + 1 && IsDirectorySeparator(path[volumeSeparatorLength])) i++;
+ if (pathLength >= volumeSeparatorLength + 1 && IsDirectorySeparator(path[volumeSeparatorLength]))
+ i++;
}
return i;
}
- private unsafe static bool StartsWithOrdinal(char* source, int sourceLength, string value)
+ private static bool StartsWithOrdinal(ReadOnlySpan<char> source, string value)
{
- if (sourceLength < value.Length) return false;
+ if (source.Length < value.Length)
+ return false;
+
for (int i = 0; i < value.Length; i++)
{
- if (value[i] != source[i]) return false;
+ if (value[i] != source[i])
+ return false;
}
return true;
}
/// for C: (rooted, but relative). "C:\a" is rooted and not relative (the current directory
/// will not be used to modify the path).
/// </remarks>
- internal static bool IsPartiallyQualified(string path)
+ internal static bool IsPartiallyQualified(ReadOnlySpan<char> path)
{
if (path.Length < 2)
{
/// <remarks>
/// Note that this conflicts with IsPathRooted() which doesn't (and never did) such a skip.
/// </remarks>
- internal static int PathStartSkip(string path)
+ internal static int PathStartSkip(ReadOnlySpan<char> path)
{
int startIndex = 0;
while (startIndex < path.Length && path[startIndex] == ' ') startIndex++;
/// </remarks>
internal static string NormalizeDirectorySeparators(string path)
{
- if (string.IsNullOrEmpty(path)) return path;
+ if (string.IsNullOrEmpty(path))
+ return path;
char current;
int start = PathStartSkip(path);
}
}
- if (normalized) return path;
+ if (normalized)
+ return path;
}
StringBuilder builder = new StringBuilder(path.Length);
/// For unix, this is empty or null. For Windows, this is empty, null, or
/// just spaces ((char)32).
/// </summary>
- internal static bool IsEffectivelyEmpty(string path)
+ internal static bool IsEffectivelyEmpty(ReadOnlySpan<char> path)
{
- if (string.IsNullOrEmpty(path))
+ if (path.IsEmpty)
return true;
foreach (char c in path)
/// it is not safe for being used to index
/// the string without additional verification.
/// </remarks>
- internal static int FindFileNameIndex(string path)
+ internal static int FindFileNameIndex(ReadOnlySpan<char> path)
{
- Debug.Assert(path != null);
-
for (int i = path.Length - 1; i >= 0; i--)
{
char ch = path[i];
searchPattern = searchPattern.Substring(index + 2);
}
}
+
+ internal static int GetDirectoryNameOffset(ReadOnlySpan<char> path)
+ {
+ int root = GetRootLength(path);
+ int i = path.Length;
+ if (i <= root)
+ return -1;
+
+ while (i > root && !IsDirectorySeparator(path[--i]));
+ return i;
+ }
}
}
source.Slice(startIndex + count).CopyTo(result.Slice(startIndex));
return result;
}
+
+ // Returns the index of the last occurrence of a specified character in the current instance.
+ public static int LastIndexOf(this ReadOnlySpan<char> source, char value)
+ {
+ if (source.Length == 0)
+ return -1;
+
+ for (int i = source.Length - 1; i >= 0; i--)
+ {
+ if (source[i] == value)
+ return i;
+ }
+
+ return -1;
+ }
}
}