From 6b27fa740ad44962093c5af4e6060011ec454a46 Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Fri, 23 Feb 2018 10:37:17 -0800 Subject: [PATCH] Remove StringBuffer and use ValueStringBuilder (#16512) * 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 --- .../Windows/Kernel32/Interop.GetFullPathNameW.cs | 2 +- .../Windows/Kernel32/Interop.GetLongPathNameW.cs | 2 +- .../shared/System.Private.CoreLib.Shared.projitems | 1 - .../shared/System/IO/PathHelper.Windows.cs | 280 +++++++++---------- .../System/Runtime/InteropServices/StringBuffer.cs | 304 --------------------- .../shared/System/Text/ValueStringBuilder.cs | 37 ++- 6 files changed, 160 insertions(+), 466 deletions(-) delete mode 100644 src/mscorlib/shared/System/Runtime/InteropServices/StringBuffer.cs diff --git a/src/mscorlib/shared/Interop/Windows/Kernel32/Interop.GetFullPathNameW.cs b/src/mscorlib/shared/Interop/Windows/Kernel32/Interop.GetFullPathNameW.cs index 15dd581..b4d4170 100644 --- a/src/mscorlib/shared/Interop/Windows/Kernel32/Interop.GetFullPathNameW.cs +++ b/src/mscorlib/shared/Interop/Windows/Kernel32/Interop.GetFullPathNameW.cs @@ -13,6 +13,6 @@ internal partial class Interop /// WARNING: This method does not implicitly handle long paths. Use GetFullPathName or PathHelper. /// [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); } } diff --git a/src/mscorlib/shared/Interop/Windows/Kernel32/Interop.GetLongPathNameW.cs b/src/mscorlib/shared/Interop/Windows/Kernel32/Interop.GetLongPathNameW.cs index ce04078..81b4d09 100644 --- a/src/mscorlib/shared/Interop/Windows/Kernel32/Interop.GetLongPathNameW.cs +++ b/src/mscorlib/shared/Interop/Windows/Kernel32/Interop.GetLongPathNameW.cs @@ -13,6 +13,6 @@ internal partial class Interop /// WARNING: This method does not implicitly handle long paths. Use GetFullPath/PathHelper. /// [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); } } diff --git a/src/mscorlib/shared/System.Private.CoreLib.Shared.projitems b/src/mscorlib/shared/System.Private.CoreLib.Shared.projitems index 0ce28f5..4ecd5fe 100644 --- a/src/mscorlib/shared/System.Private.CoreLib.Shared.projitems +++ b/src/mscorlib/shared/System.Private.CoreLib.Shared.projitems @@ -445,7 +445,6 @@ - diff --git a/src/mscorlib/shared/System/IO/PathHelper.Windows.cs b/src/mscorlib/shared/System/IO/PathHelper.Windows.cs index bde11fd..dda9b2c 100644 --- a/src/mscorlib/shared/System/IO/PathHelper.Windows.cs +++ b/src/mscorlib/shared/System/IO/PathHelper.Windows.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Runtime.InteropServices; +using System.Text; namespace System.IO { @@ -24,63 +25,49 @@ namespace System.IO /// Normalized path internal static string Normalize(string path) { + Span 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 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 index 42bf9cc..0000000 --- a/src/mscorlib/shared/System/Runtime/InteropServices/StringBuffer.cs +++ /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 -{ - /// - /// 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". - /// - /// - /// - /// Suggested use through P/Invoke: define DllImport arguments that take a character buffer as SafeHandle and pass StringBuffer.GetHandle(). - /// - internal struct StringBuffer - { - private char[] _buffer; - private int _length; - - /// - /// Instantiate the buffer with capacity for at least the specified number of characters. Capacity - /// includes the trailing null character. - /// - public StringBuffer(int initialCapacity) - { - _buffer = ArrayPool.Shared.Rent(initialCapacity); - _length = 0; - } - - /// - /// Get/set the character at the given index. - /// - /// Thrown if attempting to index outside of the buffer length. - 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; - } - } - - /// - /// Underlying storage of the buffer. Used for interop. - /// - public char[] UnderlyingArray => _buffer; - - /// - /// Character capacity of the buffer. Includes the count for the trailing null character. - /// - public int Capacity => _buffer.Length; - - /// - /// Ensure capacity in characters is at least the given minimum. - /// - /// Thrown if unable to allocate memory when setting. - public void EnsureCapacity(int minCapacity) - { - if (minCapacity > Capacity) - { - char[] oldBuffer = _buffer; - _buffer = ArrayPool.Shared.Rent(minCapacity); - Array.Copy(oldBuffer, 0, _buffer, 0, oldBuffer.Length); - ArrayPool.Shared.Return(oldBuffer); - } - } - - /// - /// 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. - /// - /// Thrown if unable to allocate memory when setting. - /// Thrown if the set size in bytes is int.MaxValue (as space is implicitly reserved for the trailing null). - public int Length - { - get { return _length; } - set - { - // Null terminate - EnsureCapacity(checked(value + 1)); - _buffer[value] = '\0'; - - _length = value; - } - } - - /// - /// True if the buffer contains the given character. - /// - 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; - } - - /// - /// Returns true if the buffer starts with the given string. - /// - 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); - } - - /// - /// Returns true if the specified StringBuffer substring equals the given value. - /// - /// The value to compare against the specified substring. - /// Start index of the sub string. - /// Length of the substring, or -1 to check all remaining. - /// - /// Thrown if or are outside the range - /// of the buffer's length. - /// - 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; - } - - /// - /// Append the given buffer. - /// - /// The buffer to append. - /// The index in the input buffer to start appending from. - /// The count of characters to copy from the buffer string. - /// Thrown if is null. - /// - /// Thrown if or are outside the range - /// of characters. - /// - 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); - } - - /// - /// Append the given buffer. - /// - /// The buffer to append. - /// The index in the input buffer to start appending from. - /// The count of characters to copy from the buffer string. - /// Thrown if is null. - /// - /// Thrown if or are outside the range - /// of characters. - /// - 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); - } - - /// - /// Copy contents to the specified buffer. Destination index must be within current destination length. - /// Will grow the destination buffer if needed. - /// - /// - /// Thrown if or or are outside the range - /// of characters. - /// - /// Thrown if is null. - 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); - } - - /// - /// 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. - /// - 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); - } - - /// - /// Trim the specified values from the end of the buffer. If nothing is specified, nothing is trimmed. - /// - 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; - } - } - - /// - /// String representation of the entire buffer. If the buffer is larger than the maximum size string (int.MaxValue) this will throw. - /// - /// Thrown if the buffer is too big to fit into a string. - public override string ToString() - { - return new string(_buffer, startIndex: 0, length: _length); - } - - /// - /// Get the given substring in the buffer. - /// - /// Count of characters to take, or remaining characters from if -1. - /// - /// Thrown if or are outside the range of the buffer's length - /// or count is greater than the maximum string size (int.MaxValue). - /// - 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.Shared.Return(_buffer); - _buffer = null; - _length = 0; - } - - public ReadOnlySpan AsSpan() - => new ReadOnlySpan(UnderlyingArray, 0, Length); - } -} diff --git a/src/mscorlib/shared/System/Text/ValueStringBuilder.cs b/src/mscorlib/shared/System/Text/ValueStringBuilder.cs index 05cd401..18d5648 100644 --- a/src/mscorlib/shared/System/Text/ValueStringBuilder.cs +++ b/src/mscorlib/shared/System/Text/ValueStringBuilder.cs @@ -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 AsSpan() => _chars.Slice(0, _pos); + public bool TryCopyTo(Span 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 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 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.Shared.Rent(Math.Max(_pos + requiredAdditionalCapacity, _chars.Length * 2)); -- 2.7.4