Remove StringBuffer and use ValueStringBuilder (#16512)
authorJeremy Kuhne <jeremy.kuhne@microsoft.com>
Fri, 23 Feb 2018 18:37:17 +0000 (10:37 -0800)
committerGitHub <noreply@github.com>
Fri, 23 Feb 2018 18:37:17 +0000 (10:37 -0800)
* Remove StringBuffer and use ValueStringBuilder

* Address initial feedback

* Address further feedback

* Address some more feedback

* Put back the temporary builder for output.

* Make the temp var ref

src/mscorlib/shared/Interop/Windows/Kernel32/Interop.GetFullPathNameW.cs
src/mscorlib/shared/Interop/Windows/Kernel32/Interop.GetLongPathNameW.cs
src/mscorlib/shared/System.Private.CoreLib.Shared.projitems
src/mscorlib/shared/System/IO/PathHelper.Windows.cs
src/mscorlib/shared/System/Runtime/InteropServices/StringBuffer.cs [deleted file]
src/mscorlib/shared/System/Text/ValueStringBuilder.cs

index 15dd581..b4d4170 100644 (file)
@@ -13,6 +13,6 @@ internal partial class Interop
         /// WARNING: This method does not implicitly handle long paths. Use GetFullPathName or PathHelper.
         /// </summary>
         [DllImport(Libraries.Kernel32, SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false, ExactSpelling = true)]
-        unsafe internal static extern uint GetFullPathNameW(char* path, uint numBufferChars, char[] buffer, IntPtr mustBeZero);
+        internal static extern uint GetFullPathNameW(string path, uint numBufferChars, ref char buffer, IntPtr mustBeZero);
     }
 }
index ce04078..81b4d09 100644 (file)
@@ -13,6 +13,6 @@ internal partial class Interop
         /// WARNING: This method does not implicitly handle long paths. Use GetFullPath/PathHelper.
         /// </summary>
         [DllImport(Libraries.Kernel32, SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false, ExactSpelling = true)]
-        internal static extern uint GetLongPathNameW(char[] lpszShortPath, char[] lpszLongPath, uint cchBuffer);
+        internal static extern uint GetLongPathNameW(ref char lpszShortPath, ref char lpszLongPath, uint cchBuffer);
     }
 }
index 0ce28f5..4ecd5fe 100644 (file)
     <Compile Include="$(MSBuildThisFileDirectory)System\Runtime\InteropServices\OutAttribute.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Runtime\InteropServices\PreserveSigAttribute.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Runtime\InteropServices\SafeBuffer.cs" />
-    <Compile Include="$(MSBuildThisFileDirectory)System\Runtime\InteropServices\StringBuffer.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Runtime\InteropServices\StructLayoutAttribute.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Runtime\InteropServices\UnmanagedFunctionPointerAttribute.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Runtime\InteropServices\UnmanagedType.cs" />
index bde11fd..dda9b2c 100644 (file)
@@ -4,6 +4,7 @@
 
 using System.Diagnostics;
 using System.Runtime.InteropServices;
+using System.Text;
 
 namespace System.IO
 {
@@ -24,63 +25,49 @@ namespace System.IO
         /// <returns>Normalized path</returns>
         internal static string Normalize(string path)
         {
+            Span<char> initialBuffer = stackalloc char[PathInternal.MaxShortPath];
+            ValueStringBuilder builder = new ValueStringBuilder(initialBuffer);
+
             // Get the full path
-            StringBuffer fullPath = new StringBuffer(path.Length);
+            GetFullPathName(path, ref builder);
 
-            try
-            {
-                GetFullPathName(path, ref fullPath);
+            // If we have the exact same string we were passed in, don't allocate another string.
+            // TryExpandShortName does this input identity check.
+            string result = builder.AsSpan().Contains('~')
+                ? TryExpandShortFileName(ref builder, originalPath: path)
+                : builder.AsSpan().Equals(path.AsReadOnlySpan()) ? path : builder.ToString();
 
-                if (fullPath.Contains('~'))
-                {
-                    return TryExpandShortFileName(ref fullPath, originalPath: path);
-                }
-                else
-                {
-                    if (fullPath.Length == path.Length && fullPath.StartsWith(path))
-                    {
-                        // If we have the exact same string we were passed in, don't bother to allocate another string from the StringBuffer.
-                        return path;
-                    }
-                    return fullPath.ToString();
-                }
-            }
-            finally
-            {
-                // Clear the buffer
-                fullPath.Free();
-            }
+            // Clear the buffer
+            builder.Dispose();
+            return result;
         }
 
-        private static unsafe void GetFullPathName(string path, ref StringBuffer fullPath)
+        private static void GetFullPathName(string path, ref ValueStringBuilder builder)
         {
             // If the string starts with an extended prefix we would need to remove it from the path before we call GetFullPathName as
             // it doesn't root extended paths correctly. We don't currently resolve extended paths, so we'll just assert here.
             Debug.Assert(PathInternal.IsPartiallyQualified(path) || !PathInternal.IsExtended(path));
 
-            fixed (char* pathStart = path)
+            uint result = 0;
+            while ((result = Interop.Kernel32.GetFullPathNameW(path, (uint)builder.Capacity, ref builder.GetPinnableReference(), IntPtr.Zero)) > builder.Capacity)
             {
-                uint result = 0;
-                while ((result = Interop.Kernel32.GetFullPathNameW(pathStart, (uint)fullPath.Capacity, fullPath.UnderlyingArray, IntPtr.Zero)) > fullPath.Capacity)
-                {
-                    // Reported size is greater than the buffer size. Increase the capacity.
-                    fullPath.EnsureCapacity(checked((int)result));
-                }
-
-                if (result == 0)
-                {
-                    // Failure, get the error and throw
-                    int errorCode = Marshal.GetLastWin32Error();
-                    if (errorCode == 0)
-                        errorCode = Interop.Errors.ERROR_BAD_PATHNAME;
-                    throw Win32Marshal.GetExceptionForWin32Error(errorCode, path);
-                }
+                // Reported size is greater than the buffer size. Increase the capacity.
+                builder.EnsureCapacity(checked((int)result));
+            }
 
-                fullPath.Length = checked((int)result);
+            if (result == 0)
+            {
+                // Failure, get the error and throw
+                int errorCode = Marshal.GetLastWin32Error();
+                if (errorCode == 0)
+                    errorCode = Interop.Errors.ERROR_BAD_PATHNAME;
+                throw Win32Marshal.GetExceptionForWin32Error(errorCode, path);
             }
+
+            builder.Length = (int)result;
         }
 
-        private static int GetInputBuffer(ref StringBuffer content, bool isDosUnc, ref StringBuffer buffer)
+        private static int PrependDevicePathChars(ref ValueStringBuilder content, bool isDosUnc, ref ValueStringBuilder buffer)
         {
             int length = content.Length;
 
@@ -89,37 +76,34 @@ namespace System.IO
                 : PathInternal.DevicePrefixLength;
 
             buffer.EnsureCapacity(length + 1);
+            buffer.Length = 0;
 
             if (isDosUnc)
             {
-                // Put the extended UNC prefix (\\?\UNC\) in front of the path
-                buffer.CopyFrom(bufferIndex: 0, source: PathInternal.UncExtendedPathPrefix);
+                // Is a \\Server\Share, put \\?\UNC\ in the front
+                buffer.Append(PathInternal.UncExtendedPathPrefix);
 
-                // Copy the source buffer over after the existing UNC prefix
-                content.CopyTo(
-                    bufferIndex: PathInternal.UncPrefixLength,
-                    destination: ref buffer,
-                    destinationIndex: PathInternal.UncExtendedPrefixLength,
-                    count: content.Length - PathInternal.UncPrefixLength);
+                // Copy Server\Share\... over to the buffer
+                buffer.Append(content.AsSpan().Slice(PathInternal.UncPrefixLength));
 
                 // Return the prefix difference
                 return PathInternal.UncExtendedPrefixLength - PathInternal.UncPrefixLength;
             }
             else
             {
-                int prefixSize = PathInternal.ExtendedPathPrefix.Length;
-                buffer.CopyFrom(bufferIndex: 0, source: PathInternal.ExtendedPathPrefix);
-                content.CopyTo(bufferIndex: 0, destination: ref buffer, destinationIndex: prefixSize, count: content.Length);
-                return prefixSize;
+                // Not an UNC, put the \\?\ prefix in front, then the original string
+                buffer.Append(PathInternal.ExtendedPathPrefix);
+                buffer.Append(content.AsSpan());
+                return PathInternal.DevicePrefixLength;
             }
         }
 
-        private static string TryExpandShortFileName(ref StringBuffer outputBuffer, string originalPath)
+        private static string TryExpandShortFileName(ref ValueStringBuilder outputBuilder, string originalPath)
         {
             // We guarantee we'll expand short names for paths that only partially exist. As such, we need to find the part of the path that actually does exist. To
             // avoid allocating like crazy we'll create only one input array and modify the contents with embedded nulls.
 
-            Debug.Assert(!PathInternal.IsPartiallyQualified(outputBuffer.AsSpan()), "should have resolved by now");
+            Debug.Assert(!PathInternal.IsPartiallyQualified(outputBuilder.AsSpan()), "should have resolved by now");
 
             // We'll have one of a few cases by now (the normalized path will have already:
             //
@@ -127,135 +111,117 @@ namespace System.IO
             //  2. Dos UNC (\\Server\Share)
             //  3. Dos device path (\\.\C:\, \\?\C:\)
             //
-            // We want to put the extended syntax on the front if it doesn't already have it, which may mean switching from \\.\.
+            // We want to put the extended syntax on the front if it doesn't already have it (for long path support and speed), which may mean switching from \\.\.
             //
             // Note that we will never get \??\ here as GetFullPathName() does not recognize \??\ and will return it as C:\??\ (or whatever the current drive is).
 
-            int rootLength = PathInternal.GetRootLength(outputBuffer.AsSpan());
-            bool isDevice = PathInternal.IsDevice(outputBuffer.AsSpan());
+            int rootLength = PathInternal.GetRootLength(outputBuilder.AsSpan());
+            bool isDevice = PathInternal.IsDevice(outputBuilder.AsSpan());
 
-            StringBuffer inputBuffer = new StringBuffer(0);
-            try
-            {
-                bool isDosUnc = false;
-                int rootDifference = 0;
-                bool wasDotDevice = false;
+            // As this is a corner case we're not going to add a stackalloc here to keep the stack pressure down.
+            ValueStringBuilder inputBuilder = new ValueStringBuilder();
 
-                // Add the extended prefix before expanding to allow growth over MAX_PATH
-                if (isDevice)
-                {
-                    // We have one of the following (\\?\ or \\.\)
-                    inputBuffer.Append(ref outputBuffer);
+            bool isDosUnc = false;
+            int rootDifference = 0;
+            bool wasDotDevice = false;
 
-                    if (outputBuffer[2] == '.')
-                    {
-                        wasDotDevice = true;
-                        inputBuffer[2] = '?';
-                    }
-                }
-                else
+            // Add the extended prefix before expanding to allow growth over MAX_PATH
+            if (isDevice)
+            {
+                // We have one of the following (\\?\ or \\.\)
+                inputBuilder.Append(outputBuilder.AsSpan());
+
+                if (outputBuilder[2] == '.')
                 {
-                    isDosUnc = !PathInternal.IsDevice(outputBuffer.AsSpan()) && outputBuffer.Length > 1 && outputBuffer[0] == '\\' && outputBuffer[1] == '\\';
-                    rootDifference = GetInputBuffer(ref outputBuffer, isDosUnc, ref inputBuffer);
+                    wasDotDevice = true;
+                    inputBuilder[2] = '?';
                 }
+            }
+            else
+            {
+                isDosUnc = !PathInternal.IsDevice(outputBuilder.AsSpan()) && outputBuilder.Length > 1 && outputBuilder[0] == '\\' && outputBuilder[1] == '\\';
+                rootDifference = PrependDevicePathChars(ref outputBuilder, isDosUnc, ref inputBuilder);
+            }
 
-                rootLength += rootDifference;
-                int inputLength = inputBuffer.Length;
+            rootLength += rootDifference;
+            int inputLength = inputBuilder.Length;
 
-                bool success = false;
-                int foundIndex = inputBuffer.Length - 1;
+            bool success = false;
+            int foundIndex = inputBuilder.Length - 1;
 
-                while (!success)
-                {
-                    uint result = Interop.Kernel32.GetLongPathNameW(inputBuffer.UnderlyingArray, outputBuffer.UnderlyingArray, (uint)outputBuffer.Capacity);
+            // Need to null terminate the input builder
+            inputBuilder.Append('\0');
 
-                    // Replace any temporary null we added
-                    if (inputBuffer[foundIndex] == '\0') inputBuffer[foundIndex] = '\\';
+            while (!success)
+            {
+                uint result = Interop.Kernel32.GetLongPathNameW(ref inputBuilder.GetPinnableReference(), ref outputBuilder.GetPinnableReference(), (uint)outputBuilder.Capacity);
+
+                // Replace any temporary null we added
+                if (inputBuilder[foundIndex] == '\0') inputBuilder[foundIndex] = '\\';
 
-                    if (result == 0)
+                if (result == 0)
+                {
+                    // Look to see if we couldn't find the file
+                    int error = Marshal.GetLastWin32Error();
+                    if (error != Interop.Errors.ERROR_FILE_NOT_FOUND && error != Interop.Errors.ERROR_PATH_NOT_FOUND)
                     {
-                        // Look to see if we couldn't find the file
-                        int error = Marshal.GetLastWin32Error();
-                        if (error != Interop.Errors.ERROR_FILE_NOT_FOUND && error != Interop.Errors.ERROR_PATH_NOT_FOUND)
-                        {
-                            // Some other failure, give up
-                            break;
-                        }
-
-                        // We couldn't find the path at the given index, start looking further back in the string.
-                        foundIndex--;
-
-                        for (; foundIndex > rootLength && inputBuffer[foundIndex] != '\\'; foundIndex--) ;
-                        if (foundIndex == rootLength)
-                        {
-                            // Can't trim the path back any further
-                            break;
-                        }
-                        else
-                        {
-                            // Temporarily set a null in the string to get Windows to look further up the path
-                            inputBuffer[foundIndex] = '\0';
-                        }
+                        // Some other failure, give up
+                        break;
                     }
-                    else if (result > outputBuffer.Capacity)
+
+                    // We couldn't find the path at the given index, start looking further back in the string.
+                    foundIndex--;
+
+                    for (; foundIndex > rootLength && inputBuilder[foundIndex] != '\\'; foundIndex--) ;
+                    if (foundIndex == rootLength)
                     {
-                        // Not enough space. The result count for this API does not include the null terminator.
-                        outputBuffer.EnsureCapacity(checked((int)result));
-                        result = Interop.Kernel32.GetLongPathNameW(inputBuffer.UnderlyingArray, outputBuffer.UnderlyingArray, (uint)outputBuffer.Capacity);
+                        // Can't trim the path back any further
+                        break;
                     }
                     else
                     {
-                        // Found the path
-                        success = true;
-                        outputBuffer.Length = checked((int)result);
-                        if (foundIndex < inputLength - 1)
-                        {
-                            // It was a partial find, put the non-existent part of the path back
-                            outputBuffer.Append(ref inputBuffer, foundIndex, inputBuffer.Length - foundIndex);
-                        }
+                        // Temporarily set a null in the string to get Windows to look further up the path
+                        inputBuilder[foundIndex] = '\0';
                     }
                 }
-
-                // Strip out the prefix and return the string
-                ref StringBuffer bufferToUse = ref Choose(success, ref outputBuffer, ref inputBuffer);
-
-                // Switch back from \\?\ to \\.\ if necessary
-                if (wasDotDevice)
-                    bufferToUse[2] = '.';
-
-                string returnValue = null;
-
-                int newLength = bufferToUse.Length - rootDifference;
-                if (isDosUnc)
-                {
-                    // Need to go from \\?\UNC\ to \\?\UN\\
-                    bufferToUse[PathInternal.UncExtendedPrefixLength - PathInternal.UncPrefixLength] = '\\';
-                }
-
-                // We now need to strip out any added characters at the front of the string
-                if (bufferToUse.SubstringEquals(originalPath, rootDifference, newLength))
+                else if (result > outputBuilder.Capacity)
                 {
-                    // Use the original path to avoid allocating
-                    returnValue = originalPath;
+                    // Not enough space. The result count for this API does not include the null terminator.
+                    outputBuilder.EnsureCapacity(checked((int)result));
+                    result = Interop.Kernel32.GetLongPathNameW(ref inputBuilder.GetPinnableReference(), ref outputBuilder.GetPinnableReference(), (uint)outputBuilder.Capacity);
                 }
                 else
                 {
-                    returnValue = bufferToUse.Substring(rootDifference, newLength);
+                    // Found the path
+                    success = true;
+                    outputBuilder.Length = checked((int)result);
+                    if (foundIndex < inputLength - 1)
+                    {
+                        // It was a partial find, put the non-existent part of the path back
+                        outputBuilder.Append(inputBuilder.AsSpan().Slice(foundIndex, inputBuilder.Length - foundIndex));
+                    }
                 }
-
-                return returnValue;
-            }
-            finally
-            {
-                inputBuffer.Free();
             }
-        }
 
-        // Helper method to workaround lack of operator ? support for ref values
-        private static ref StringBuffer Choose(bool condition, ref StringBuffer s1, ref StringBuffer s2)
-        {
-            if (condition) return ref s1;
-            else return ref s2;
+            // If we were able to expand the path, use it, otherwise use the original full path result
+            ref ValueStringBuilder builderToUse = ref (success ? ref outputBuilder : ref inputBuilder);
+
+            // Switch back from \\?\ to \\.\ if necessary
+            if (wasDotDevice)
+                builderToUse[2] = '.';
+
+            // Change from \\?\UNC\ to \\?\UN\\ if needed
+            if (isDosUnc)
+                builderToUse[PathInternal.UncExtendedPrefixLength - PathInternal.UncPrefixLength] = '\\';
+
+            // Strip out any added characters at the front of the string
+            ReadOnlySpan<char> output = builderToUse.AsSpan().Slice(rootDifference);
+
+            string returnValue = output.Equals(originalPath.AsReadOnlySpan())
+                ? originalPath : new string(output);
+
+            inputBuilder.Dispose();
+            return returnValue;
         }
     }
 }
diff --git a/src/mscorlib/shared/System/Runtime/InteropServices/StringBuffer.cs b/src/mscorlib/shared/System/Runtime/InteropServices/StringBuffer.cs
deleted file mode 100644 (file)
index 42bf9cc..0000000
+++ /dev/null
@@ -1,304 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-using System.Buffers;
-using System.Runtime.CompilerServices;
-
-namespace System.Runtime.InteropServices
-{
-    /// <summary>
-    /// Buffer that deals in char size increments. Dispose to free memory. Always makes ordinal
-    /// comparisons. Not thread safe.
-    ///
-    /// A more performant replacement for StringBuilder when performing native interop.
-    /// 
-    /// "No copy" valuetype. Has to be passed as "ref".
-    /// 
-    /// </summary>
-    /// <remarks>
-    /// Suggested use through P/Invoke: define DllImport arguments that take a character buffer as SafeHandle and pass StringBuffer.GetHandle().
-    /// </remarks>
-    internal struct StringBuffer
-    {
-        private char[] _buffer;
-        private int _length;
-
-        /// <summary>
-        /// Instantiate the buffer with capacity for at least the specified number of characters. Capacity
-        /// includes the trailing null character.
-        /// </summary>
-        public StringBuffer(int initialCapacity)
-        {
-            _buffer = ArrayPool<char>.Shared.Rent(initialCapacity);
-            _length = 0;
-        }
-
-        /// <summary>
-        /// Get/set the character at the given index.
-        /// </summary>
-        /// <exception cref="ArgumentOutOfRangeException">Thrown if attempting to index outside of the buffer length.</exception>
-        public char this[int index]
-        {
-            [MethodImpl(MethodImplOptions.AggressiveInlining)]
-            get
-            {
-                if (index >= _length) throw new ArgumentOutOfRangeException(nameof(index));
-                return _buffer[index];
-            }
-            [MethodImpl(MethodImplOptions.AggressiveInlining)]
-            set
-            {
-                if (index >= _length) throw new ArgumentOutOfRangeException(nameof(index));
-                _buffer[index] = value;
-            }
-        }
-
-        /// <summary>
-        /// Underlying storage of the buffer. Used for interop.
-        /// </summary>
-        public char[] UnderlyingArray => _buffer;
-
-        /// <summary>
-        /// Character capacity of the buffer. Includes the count for the trailing null character.
-        /// </summary>
-        public int Capacity => _buffer.Length;
-
-        /// <summary>
-        /// Ensure capacity in characters is at least the given minimum.
-        /// </summary>
-        /// <exception cref="OutOfMemoryException">Thrown if unable to allocate memory when setting.</exception>
-        public void EnsureCapacity(int minCapacity)
-        {
-            if (minCapacity > Capacity)
-            {
-                char[] oldBuffer = _buffer;
-                _buffer = ArrayPool<char>.Shared.Rent(minCapacity);
-                Array.Copy(oldBuffer, 0, _buffer, 0, oldBuffer.Length);
-                ArrayPool<char>.Shared.Return(oldBuffer);
-            }
-        }
-
-        /// <summary>
-        /// The logical length of the buffer in characters. (Does not include the final null.) Will automatically attempt to increase capacity.
-        /// This is where the usable data ends.
-        /// </summary>
-        /// <exception cref="OutOfMemoryException">Thrown if unable to allocate memory when setting.</exception>
-        /// <exception cref="ArgumentOutOfRangeException">Thrown if the set size in bytes is int.MaxValue (as space is implicitly reserved for the trailing null).</exception>
-        public int Length
-        {
-            get { return _length; }
-            set
-            {
-                // Null terminate
-                EnsureCapacity(checked(value + 1));
-                _buffer[value] = '\0';
-
-                _length = value;
-            }
-        }
-
-        /// <summary>
-        /// True if the buffer contains the given character.
-        /// </summary>
-        public unsafe bool Contains(char value)
-        {
-            fixed (char* start = _buffer)
-            {
-                int length = _length;
-                for (int i = 0; i < length; i++)
-                {
-                    if (start[i] == value) return true;
-                }
-            }
-
-            return false;
-        }
-
-        /// <summary>
-        /// Returns true if the buffer starts with the given string.
-        /// </summary>
-        public bool StartsWith(string value)
-        {
-            if (value == null) throw new ArgumentNullException(nameof(value));
-            if (_length < value.Length) return false;
-            return SubstringEquals(value, startIndex: 0, count: value.Length);
-        }
-
-        /// <summary>
-        /// Returns true if the specified StringBuffer substring equals the given value.
-        /// </summary>
-        /// <param name="value">The value to compare against the specified substring.</param>
-        /// <param name="startIndex">Start index of the sub string.</param>
-        /// <param name="count">Length of the substring, or -1 to check all remaining.</param>
-        /// <exception cref="ArgumentOutOfRangeException">
-        /// Thrown if <paramref name="startIndex"/> or <paramref name="count"/> are outside the range
-        /// of the buffer's length.
-        /// </exception>
-        public unsafe bool SubstringEquals(string value, int startIndex = 0, int count = -1)
-        {
-            if (value == null) return false;
-            if (count < -1) throw new ArgumentOutOfRangeException(nameof(count));
-            if (startIndex > _length) throw new ArgumentOutOfRangeException(nameof(startIndex));
-
-            int realCount = count == -1 ? _length - startIndex : (int)count;
-            if (checked(startIndex + realCount) > _length) throw new ArgumentOutOfRangeException(nameof(count));
-
-            int length = value.Length;
-
-            // Check the substring length against the input length
-            if (realCount != length) return false;
-
-            fixed (char* valueStart = value)
-            fixed (char* bufferStart = _buffer)
-            {
-                char* subStringStart = bufferStart + startIndex;
-
-                for (int i = 0; i < length; i++)
-                {
-                    if (subStringStart[i] != valueStart[i]) return false;
-                }
-            }
-
-            return true;
-        }
-
-        /// <summary>
-        /// Append the given buffer.
-        /// </summary>
-        /// <param name="value">The buffer to append.</param>
-        /// <param name="startIndex">The index in the input buffer to start appending from.</param>
-        /// <param name="count">The count of characters to copy from the buffer string.</param>
-        /// <exception cref="ArgumentNullException">Thrown if <paramref name="value"/> is null.</exception>
-        /// <exception cref="ArgumentOutOfRangeException">
-        /// Thrown if <paramref name="startIndex"/> or <paramref name="count"/> are outside the range
-        /// of <paramref name="value"/> characters.
-        /// </exception>
-        public void Append(ref StringBuffer value, int startIndex = 0)
-        {
-            if (value.Length == 0) return;
-
-            value.CopyTo(
-                bufferIndex: startIndex,
-                destination: ref this,
-                destinationIndex: _length,
-                count: value.Length);
-        }
-
-        /// <summary>
-        /// Append the given buffer.
-        /// </summary>
-        /// <param name="value">The buffer to append.</param>
-        /// <param name="startIndex">The index in the input buffer to start appending from.</param>
-        /// <param name="count">The count of characters to copy from the buffer string.</param>
-        /// <exception cref="ArgumentNullException">Thrown if <paramref name="value"/> is null.</exception>
-        /// <exception cref="ArgumentOutOfRangeException">
-        /// Thrown if <paramref name="startIndex"/> or <paramref name="count"/> are outside the range
-        /// of <paramref name="value"/> characters.
-        /// </exception>
-        public void Append(ref StringBuffer value, int startIndex, int count)
-        {
-            if (count == 0) return;
-
-            value.CopyTo(
-                bufferIndex: startIndex,
-                destination: ref this,
-                destinationIndex: _length,
-                count: count);
-        }
-
-        /// <summary>
-        /// Copy contents to the specified buffer. Destination index must be within current destination length.
-        /// Will grow the destination buffer if needed.
-        /// </summary>
-        /// <exception cref="ArgumentOutOfRangeException">
-        /// Thrown if <paramref name="bufferIndex"/> or <paramref name="destinationIndex"/> or <paramref name="count"/> are outside the range
-        /// of <paramref name="value"/> characters.
-        /// </exception>
-        /// <exception cref="ArgumentNullException">Thrown if <paramref name="destination"/> is null.</exception>
-        public void CopyTo(int bufferIndex, ref StringBuffer destination, int destinationIndex, int count)
-        {
-            if (destinationIndex > destination._length) throw new ArgumentOutOfRangeException(nameof(destinationIndex));
-            if (bufferIndex >= _length) throw new ArgumentOutOfRangeException(nameof(bufferIndex));
-            if (_length < checked(bufferIndex + count)) throw new ArgumentOutOfRangeException(nameof(count));
-
-            if (count == 0) return;
-            int lastIndex = checked(destinationIndex + count);
-            if (destination.Length < lastIndex) destination.Length = lastIndex;
-
-            Array.Copy(UnderlyingArray, bufferIndex, destination.UnderlyingArray, destinationIndex, count);
-        }
-
-        /// <summary>
-        /// Copy contents from the specified string into the buffer at the given index. Start index must be within the current length of
-        /// the buffer, will grow as necessary.
-        /// </summary>
-        public void CopyFrom(int bufferIndex, string source, int sourceIndex = 0, int count = -1)
-        {
-            if (source == null) throw new ArgumentNullException(nameof(source));
-            if (bufferIndex > _length) throw new ArgumentOutOfRangeException(nameof(bufferIndex));
-            if (sourceIndex < 0 || sourceIndex > source.Length) throw new ArgumentOutOfRangeException(nameof(sourceIndex));
-            if (count == -1) count = source.Length - sourceIndex;
-            if (count < 0 || source.Length - count < sourceIndex) throw new ArgumentOutOfRangeException(nameof(count));
-
-            if (count == 0) return;
-            int lastIndex = bufferIndex + (int)count;
-            if (_length < lastIndex) Length = lastIndex;
-
-            source.CopyTo(sourceIndex, UnderlyingArray, bufferIndex, count);
-        }
-
-        /// <summary>
-        /// Trim the specified values from the end of the buffer. If nothing is specified, nothing is trimmed.
-        /// </summary>
-        public void TrimEnd(char[] values)
-        {
-            if (values == null || values.Length == 0 || _length == 0) return;
-
-            while (_length > 0 && Array.IndexOf(values, _buffer[_length - 1]) >= 0)
-            {
-                Length = _length - 1;
-            }
-        }
-
-        /// <summary>
-        /// String representation of the entire buffer. If the buffer is larger than the maximum size string (int.MaxValue) this will throw.
-        /// </summary>
-        /// <exception cref="InvalidOperationException">Thrown if the buffer is too big to fit into a string.</exception>
-        public override string ToString()
-        {
-            return new string(_buffer, startIndex: 0, length: _length);
-        }
-
-        /// <summary>
-        /// Get the given substring in the buffer.
-        /// </summary>
-        /// <param name="count">Count of characters to take, or remaining characters from <paramref name="startIndex"/> if -1.</param>
-        /// <exception cref="ArgumentOutOfRangeException">
-        /// Thrown if <paramref name="startIndex"/> or <paramref name="count"/> are outside the range of the buffer's length
-        /// or count is greater than the maximum string size (int.MaxValue).
-        /// </exception>
-        public string Substring(int startIndex, int count = -1)
-        {
-            if (startIndex > (_length == 0 ? 0 : _length - 1)) throw new ArgumentOutOfRangeException(nameof(startIndex));
-            if (count < -1) throw new ArgumentOutOfRangeException(nameof(count));
-
-            int realCount = count == -1 ? _length - startIndex : (int)count;
-            if (realCount > int.MaxValue || checked(startIndex + realCount) > _length) throw new ArgumentOutOfRangeException(nameof(count));
-
-            // The buffer could be bigger than will fit into a string, but the substring might fit. As the starting
-            // index might be bigger than int we need to index ourselves.
-            return new string(_buffer, startIndex: startIndex, length: realCount);
-        }
-
-        public void Free()
-        {
-            ArrayPool<char>.Shared.Return(_buffer);
-            _buffer = null;
-            _length = 0;
-        }
-
-        public ReadOnlySpan<char> AsSpan()
-            => new ReadOnlySpan<char>(UnderlyingArray, 0, Length);
-    }
-}
index 05cd401..18d5648 100644 (file)
@@ -5,6 +5,7 @@
 using System.Buffers;
 using System.Diagnostics;
 using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
 
 namespace System.Text
 {
@@ -21,7 +22,25 @@ namespace System.Text
             _pos = 0;
         }
 
-        public int Length => _pos;
+        public int Length
+        {
+            get => _pos;
+            set
+            {
+                Debug.Assert(value <= _chars.Length);
+                _pos = value;
+            }
+        }
+
+        public int Capacity => _chars.Length;
+
+        public void EnsureCapacity(int capacity)
+        {
+            if (capacity > _chars.Length)
+                Grow(capacity - _chars.Length);
+        }
+
+        public ref char GetPinnableReference() => ref MemoryMarshal.GetReference(_chars);
 
         public ref char this[int index]
         {
@@ -39,6 +58,8 @@ namespace System.Text
             return s;
         }
 
+        public ReadOnlySpan<char> AsSpan() => _chars.Slice(0, _pos);
+
         public bool TryCopyTo(Span<char> destination, out int charsWritten)
         {
             if (_chars.Slice(0, _pos).TryCopyTo(destination))
@@ -141,6 +162,18 @@ namespace System.Text
             _pos += length;
         }
 
+        public unsafe void Append(ReadOnlySpan<char> value)
+        {
+            int pos = _pos;
+            if (pos > _chars.Length - value.Length)
+            {
+                Grow(value.Length);
+            }
+
+            value.CopyTo(_chars.Slice(_pos));
+            _pos += value.Length;
+        }
+
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public Span<char> AppendSpan(int length)
         {
@@ -164,7 +197,7 @@ namespace System.Text
         [MethodImpl(MethodImplOptions.NoInlining)]
         private void Grow(int requiredAdditionalCapacity)
         {
-            Debug.Assert(requiredAdditionalCapacity > _chars.Length - _pos);
+            Debug.Assert(requiredAdditionalCapacity > 0);
 
             char[] poolArray = ArrayPool<char>.Shared.Rent(Math.Max(_pos + requiredAdditionalCapacity, _chars.Length * 2));