// 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;
}
}
}