Span overload added for path apis (#15608)
authorAnirudh Agnihotry <anirudhagnihotry098@gmail.com>
Wed, 31 Jan 2018 23:36:07 +0000 (15:36 -0800)
committerGitHub <noreply@github.com>
Wed, 31 Jan 2018 23:36:07 +0000 (15:36 -0800)
Span overload path apis

src/mscorlib/shared/System/IO/Path.Unix.cs
src/mscorlib/shared/System/IO/Path.Windows.cs
src/mscorlib/shared/System/IO/Path.cs
src/mscorlib/shared/System/IO/PathInternal.Unix.cs
src/mscorlib/shared/System/IO/PathInternal.Windows.StringBuffer.cs
src/mscorlib/shared/System/IO/PathInternal.Windows.cs
src/mscorlib/shared/System/IO/PathInternal.cs
src/mscorlib/shared/System/StringSpanHelpers.cs

index 1143c05..81a796b 100644 (file)
@@ -175,6 +175,11 @@ namespace System.IO
             if (path == null)
                 return false;
 
+            return IsPathRooted(path.AsReadOnlySpan());
+        }
+
+        public static bool IsPathRooted(ReadOnlySpan<char> path)
+        {
             return path.Length > 0 && path[0] == PathInternal.DirectorySeparatorChar;
         }
 
@@ -189,6 +194,11 @@ namespace System.IO
             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
         {
index c5cb0d8..862617d 100644 (file)
@@ -113,14 +113,13 @@ namespace System.IO
         // 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
@@ -134,15 +133,29 @@ namespace System.IO
         // 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>
index 9f3f486..3814a92 100644 (file)
@@ -80,16 +80,25 @@ namespace System.IO
                 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
@@ -101,21 +110,33 @@ namespace System.IO
             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
@@ -126,9 +147,21 @@ namespace System.IO
             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)
@@ -136,13 +169,25 @@ namespace System.IO
             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 
@@ -179,6 +224,11 @@ namespace System.IO
             {
                 throw new ArgumentNullException(nameof(path));
             }
+            return IsPathFullyQualified(path.AsReadOnlySpan());
+        }
+
+        public static bool IsPathFullyQualified(ReadOnlySpan<char> path)
+        {
             return !PathInternal.IsPartiallyQualified(path);
         }
 
@@ -190,15 +240,22 @@ namespace System.IO
         {
             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;
         }
index 2f65a42..186882a 100644 (file)
@@ -4,6 +4,7 @@
 
 using System.Diagnostics;
 using System.Text;
+using System.Runtime.InteropServices;
 
 namespace System.IO
 {
@@ -22,7 +23,7 @@ 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;
         }
@@ -40,7 +41,8 @@ namespace System.IO
         /// </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;
@@ -55,7 +57,8 @@ namespace System.IO
                 }
             }
 
-            if (normalized) return path;
+            if (normalized)
+                return path;
 
             StringBuilder builder = new StringBuilder(path.Length);
 
@@ -73,7 +76,7 @@ namespace System.IO
 
             return builder.ToString();
         }
-        
+
         /// <summary>
         /// Returns true if the character is a directory or volume separator.
         /// </summary>
@@ -87,7 +90,7 @@ namespace System.IO
             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.
@@ -108,5 +111,10 @@ namespace System.IO
         {
             return string.IsNullOrEmpty(path);
         }
+
+        internal static bool IsEffectivelyEmpty(ReadOnlySpan<char> path)
+        {
+            return path.IsEmpty;
+        }
     }
 }
index 84953df..ea13242 100644 (file)
@@ -27,14 +27,12 @@ namespace System.IO
         /// <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>
index f315f43..98a81f1 100644 (file)
@@ -4,6 +4,7 @@
 
 using System.Diagnostics;
 using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
 using System.Text;
 
 namespace System.IO
@@ -115,7 +116,7 @@ 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.
@@ -135,7 +136,7 @@ namespace System.IO
         /// 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.
@@ -149,7 +150,7 @@ namespace System.IO
         /// <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;
@@ -172,22 +173,15 @@ namespace System.IO
         /// <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
@@ -214,7 +208,8 @@ namespace System.IO
                     // (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)
@@ -222,17 +217,21 @@ namespace System.IO
                 // 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;
         }
@@ -249,7 +248,7 @@ namespace System.IO
         /// 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)
             {
@@ -283,7 +282,7 @@ namespace System.IO
         /// <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++;
@@ -341,7 +340,8 @@ namespace System.IO
         /// </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);
@@ -364,7 +364,8 @@ namespace System.IO
                     }
                 }
 
-                if (normalized) return path;
+                if (normalized)
+                    return path;
             }
 
             StringBuilder builder = new StringBuilder(path.Length);
@@ -412,9 +413,9 @@ namespace System.IO
         /// 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)
index bfd69e9..3b6bcaa 100644 (file)
@@ -22,10 +22,8 @@ namespace System.IO
         /// 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];
@@ -135,5 +133,16 @@ namespace System.IO
                 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;
+        }
     }
 }
index 1c127b1..d708b79 100644 (file)
@@ -125,5 +125,20 @@ namespace System
             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;
+        }
     }
 }