// // PacketStringBuilder.cs // // Copyright (c) František Boháček. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Buffers; using System.Text; using Remora.Results; namespace NosSmooth.PacketSerializer.Abstractions; /// /// String builder for packets. /// public ref struct PacketStringBuilder { private Span _buffer; private char[]? _bufferArray; private int _position; private StringBuilderLevel _currentLevel; private char? _insertSeparator; /// /// Initializes a new instance of the struct. /// /// The initial buffer to store the packet to. Will grow in size if needed. /// The top level separator. public PacketStringBuilder(Span initialBuffer, char separator = ' ') { _currentLevel = new StringBuilderLevel(null, separator); _insertSeparator = null; _buffer = initialBuffer; _position = 0; _bufferArray = null; } /// /// Sets the separator to search for only once. /// /// /// This separator will have the most priority. /// /// The separator to look for. public void SetAfterSeparatorOnce(char separator) { _currentLevel.SeparatorOnce = separator; } /// /// Prepare the given level that can be set when needed. /// /// The separator of the prepared level. public void PrepareLevel(char separator) { _currentLevel.PreparedLevelSeparator = separator; } /// /// Reset the prepared level, if there is any. /// public void RemovePreparedLevel() { _currentLevel.PreparedLevelSeparator = null; } /// /// Create next level with the separator given in the prepared level. /// /// /// Level of the current builder will stay the same. /// Will return null, if there is not a level prepared. /// /// An enumerator with the new level pushed. public bool PushPreparedLevel() { if (_currentLevel.PreparedLevelSeparator is null) { return false; } _currentLevel = new StringBuilderLevel(_currentLevel, _currentLevel.PreparedLevelSeparator.Value); return true; } /// /// Push new separator level to the stack. /// /// /// This will change the current enumerator. /// It has to be after parent level should be used. /// /// The separator of the new level. public void PushLevel(char separator) { _currentLevel = new StringBuilderLevel(_currentLevel, separator); } /// /// Pop the current level. /// /// Whether to replace the last separator with the parent one. /// A result that may or may not have succeeded. There will be an error if the current level is the top one. public Result PopLevel(bool replaceSeparator = true) { if (_currentLevel.Parent is null) { return new InvalidOperationError("The level cannot be popped, the stack is already at the top level."); } if (replaceSeparator) { ReplaceWithParentSeparator(); } _currentLevel = _currentLevel.Parent; return Result.FromSuccess(); } /// /// Appends a value that is span formattable. /// /// The value to append. /// The span formattable type. public void Append(T value) where T : ISpanFormattable { AppendSpanFormattable(value); } /// /// Appends the value to the string. /// /// The value to append. public void Append(ReadOnlySpan value) { BeforeAppend(); while (!value.TryCopyTo(_buffer.Slice(_position))) { GrowBuffer(value.Length); } _position += value.Length; AfterAppend(); } /// /// Appends the value to the string. /// /// The value to append. public void Append(int value) => AppendSpanFormattable(value); /// /// Appends the value to the string. /// /// The value to append. public void Append(uint value) => AppendSpanFormattable(value); /// /// Appends the value to the string. /// /// The value to append. public void Append(short value) => AppendSpanFormattable(value); /// /// Appends the value to the string. /// /// The value to append. public void Append(char value) => AppendSpanFormattable(value); /// /// Appends the value to the string. /// /// The value to append. public void Append(ushort value) => AppendSpanFormattable(value); /// /// Appends the value to the string. /// /// The value to append. public void Append(long value) => AppendSpanFormattable(value); /// /// Appends the value to the string. /// /// The value to append. public void Append(ulong value) => AppendSpanFormattable(value); /// /// Appends the value to the string. /// /// The value to append. public void Append(byte value) => AppendSpanFormattable(value); /// /// Appends the value to the string. /// /// The value to append. public void Append(sbyte value) => AppendSpanFormattable(value); /// /// Appends the value to the string. /// /// The value to append. public void Append(float value) => AppendSpanFormattable(value); /// /// Appends the value to the string. /// /// The value to append. public void Append(double value) => AppendSpanFormattable(value); /// /// Appends the value to the string. /// /// The value to append. public void Append(decimal value) => AppendSpanFormattable(value); /// /// Returns buffer to ArrayPool if it has been used. /// public void Dispose() { if (_bufferArray is not null) { ArrayPool.Shared.Return(_bufferArray); } } private void AppendSpanFormattable(T value) where T : ISpanFormattable { BeforeAppend(); int charsWritten; while (!value.TryFormat(_buffer.Slice(_position), out charsWritten, default, null)) { GrowBuffer(); } _position += charsWritten; AfterAppend(); } private void GrowBuffer(int needed = 0) { var sizeNeeded = _buffer.Length + needed; var doubleSize = _buffer.Length * 2; var newSize = Math.Max(doubleSize, sizeNeeded); var newBuffer = ArrayPool.Shared.Rent(newSize); _buffer.CopyTo(newBuffer); _buffer = newBuffer; if (_bufferArray is not null) { ArrayPool.Shared.Return(_bufferArray); } _bufferArray = newBuffer; } private void BeforeAppend() { if (_insertSeparator is not null) { if (_buffer.Length <= _position + 1) { GrowBuffer(); } _buffer[_position] = _insertSeparator.Value; _position += 1; _insertSeparator = null; } } private void AfterAppend() { _insertSeparator = _currentLevel.SeparatorOnce ?? _currentLevel.Separator; _currentLevel.SeparatorOnce = null; } /// /// Replace the last separator with the parent separator. /// public void ReplaceWithParentSeparator() { var parent = _currentLevel.Parent; if (_insertSeparator is null || parent is null) { return; } _insertSeparator = parent.SeparatorOnce ?? parent.Separator; parent.SeparatorOnce = null; } /// public override string ToString() { return _buffer.Slice(0, _position).ToString(); } private class StringBuilderLevel { public StringBuilderLevel(StringBuilderLevel? parent, char separator) { Parent = parent; Separator = separator; } public StringBuilderLevel? Parent { get; } public char? PreparedLevelSeparator { get; set; } public char Separator { get; } public char? SeparatorOnce { get; set; } } }