// 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. namespace System.Runtime.InteropServices { /// /// Native buffer that deals in char size increments. Dispose to free memory. Allows buffers larger /// than a maximum size string to enable working with very large string arrays. Always makes ordinal /// comparisons. /// /// A more performant replacement for StringBuilder when performing native interop. /// /// /// Suggested use through P/Invoke: define DllImport arguments that take a character buffer as SafeHandle and pass StringBuffer.GetHandle(). /// internal class StringBuffer : NativeBuffer { private uint _length; /// /// Instantiate the buffer with capacity for at least the specified number of characters. Capacity /// includes the trailing null character. /// public StringBuffer(uint initialCapacity = 0) : base(initialCapacity * (ulong)sizeof(char)) { } /// /// Get/set the character at the given index. /// /// Thrown if attempting to index outside of the buffer length. public unsafe char this[uint index] { get { if (index >= _length) throw new ArgumentOutOfRangeException(nameof(index)); return CharPointer[index]; } set { if (index >= _length) throw new ArgumentOutOfRangeException(nameof(index)); CharPointer[index] = value; } } /// /// Character capacity of the buffer. Includes the count for the trailing null character. /// public uint CharCapacity { get { ulong byteCapacity = ByteCapacity; ulong charCapacity = byteCapacity == 0 ? 0 : byteCapacity / sizeof(char); return charCapacity > uint.MaxValue ? uint.MaxValue : (uint)charCapacity; } } /// /// Ensure capacity in characters is at least the given minimum. /// /// Thrown if unable to allocate memory when setting. public void EnsureCharCapacity(uint minCapacity) { EnsureByteCapacity(minCapacity * (ulong)sizeof(char)); } /// /// 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 uint.MaxValue (as space is implicitly reserved for the trailing null). public unsafe uint Length { get { return _length; } set { if (value == uint.MaxValue) throw new ArgumentOutOfRangeException(nameof(Length)); // Null terminate EnsureCharCapacity(value + 1); CharPointer[value] = '\0'; _length = value; } } /// /// For use when the native api null terminates but doesn't return a length. /// If no null is found, the length will not be changed. /// public unsafe void SetLengthToFirstNull() { char* buffer = CharPointer; uint capacity = CharCapacity; for (uint i = 0; i < capacity; i++) { if (buffer[i] == '\0') { _length = i; break; } } } internal unsafe char* CharPointer { get { return (char*)VoidPointer; } } /// /// True if the buffer contains the given character. /// public unsafe bool Contains(char value) { char* start = CharPointer; uint length = _length; for (uint i = 0; i < length; i++) { if (*start++ == 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 < (uint)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, uint 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)); uint realCount = count == -1 ? _length - startIndex : (uint)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 != (uint)length) return false; fixed (char* valueStart = value) { char* bufferStart = CharPointer + startIndex; for (int i = 0; i < length; i++) { // Note that indexing in this case generates faster code than trying to copy the pointer and increment it if (*bufferStart++ != valueStart[i]) return false; } } return true; } /// /// Append the given string. /// /// The string to append. /// The index in the input string to start appending from. /// The count of characters to copy from the input string, or -1 for all remaining. /// Thrown if is null. /// /// Thrown if or are outside the range /// of characters. /// public void Append(string value, int startIndex = 0, int count = -1) { CopyFrom( bufferIndex: _length, source: value, sourceIndex: startIndex, count: count); } /// /// 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(StringBuffer value, uint startIndex = 0) { if (value == null) throw new ArgumentNullException(nameof(value)); if (value.Length == 0) return; value.CopyTo( bufferIndex: startIndex, destination: 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(StringBuffer value, uint startIndex, uint count) { if (value == null) throw new ArgumentNullException(nameof(value)); if (count == 0) return; value.CopyTo( bufferIndex: startIndex, destination: 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 unsafe void CopyTo(uint bufferIndex, StringBuffer destination, uint destinationIndex, uint count) { if (destination == null) throw new ArgumentNullException(nameof(destination)); 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; uint lastIndex = checked(destinationIndex + count); if (destination._length < lastIndex) destination.Length = lastIndex; Buffer.MemoryCopy( source: CharPointer + bufferIndex, destination: destination.CharPointer + destinationIndex, destinationSizeInBytes: checked((long)(destination.ByteCapacity - (destinationIndex * sizeof(char)))), sourceBytesToCopy: checked((long)count * sizeof(char))); } /// /// 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 unsafe void CopyFrom(uint 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; uint lastIndex = bufferIndex + (uint)count; if (_length < lastIndex) Length = lastIndex; fixed (char* content = source) { Buffer.MemoryCopy( source: content + sourceIndex, destination: CharPointer + bufferIndex, destinationSizeInBytes: checked((long)(ByteCapacity - (bufferIndex * sizeof(char)))), sourceBytesToCopy: (long)count * sizeof(char)); } } /// /// Trim the specified values from the end of the buffer. If nothing is specified, nothing is trimmed. /// public unsafe void TrimEnd(char[] values) { if (values == null || values.Length == 0 || _length == 0) return; char* end = CharPointer + _length - 1; while (_length > 0 && Array.IndexOf(values, *end) >= 0) { Length = _length - 1; end--; } } /// /// 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 unsafe override string ToString() { if (_length == 0) return string.Empty; if (_length > int.MaxValue) throw new InvalidOperationException(); return new string(CharPointer, startIndex: 0, length: (int)_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 unsafe string Substring(uint startIndex, int count = -1) { if (startIndex > (_length == 0 ? 0 : _length - 1)) throw new ArgumentOutOfRangeException(nameof(startIndex)); if (count < -1) throw new ArgumentOutOfRangeException(nameof(count)); uint realCount = count == -1 ? _length - startIndex : (uint)count; if (realCount > int.MaxValue || checked(startIndex + realCount) > _length) throw new ArgumentOutOfRangeException(nameof(count)); if (realCount == 0) return string.Empty; // 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(value: CharPointer + startIndex, startIndex: 0, length: (int)realCount); } public override void Free() { base.Free(); _length = 0; } } }