// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Buffers; using System.Collections.Generic; namespace EpicGames.Core { /// /// Class for building byte sequences, similar to StringBuilder. Allocates memory in chunks to avoid copying data. /// public class ByteArrayBuilder : IMemoryWriter { class Chunk { public readonly int RunningIndex; public readonly byte[] Data; public int Length { get; set; } public Chunk(int runningIndex, int size) { RunningIndex = runningIndex; Data = new byte[size]; } public ReadOnlySpan WrittenSpan => Data.AsSpan(0, Length); public ReadOnlyMemory WrittenMemory => Data.AsMemory(0, Length); } readonly List _chunks = []; readonly int _chunkSize; Chunk _currentChunk; /// /// Length of the current sequence /// public int Length { get; private set; } /// /// Constructor /// /// public ByteArrayBuilder(int chunkSize) : this(chunkSize, chunkSize) { } /// /// Constructor /// /// Size of the initial chunk /// Default size for subsequent chunks public ByteArrayBuilder(int initialSize = 4096, int chunkSize = 4096) { _currentChunk = new Chunk(0, initialSize); _chunks.Add(_currentChunk); _chunkSize = chunkSize; } /// /// Clear the current builder /// public void Clear() { _currentChunk = _chunks[0]; _currentChunk.Length = 0; _chunks.RemoveRange(1, _chunks.Count - 1); Length = 0; } /// public Span GetSpan(int sizeHint) => GetMemory(sizeHint).Span; /// public Memory GetMemory(int sizeHint) { int requiredSize = _currentChunk.Length + Math.Max(sizeHint, 1); if (requiredSize > _currentChunk.Data.Length) { _currentChunk = new Chunk(_currentChunk.RunningIndex + _currentChunk.Length, Math.Max(sizeHint, _chunkSize)); _chunks.Add(_currentChunk); } return _currentChunk.Data.AsMemory(_currentChunk.Length); } /// public void Advance(int length) { if (length < 0 || _currentChunk.Length + length > _currentChunk.Data.Length) { throw new ArgumentException("Length exceeds possible size of data written to output buffer", nameof(length)); } _currentChunk.Length += length; Length += length; } /// /// Appends data in this builder to the given sequence /// /// Sequence builder public void AppendTo(ReadOnlySequenceBuilder builder) { foreach (Chunk chunk in _chunks) { builder.Append(chunk.WrittenMemory); } } /// /// Gets a contiguous, non-owned buffer containing the current data. Will merge together current chunks if necessary. /// /// public ReadOnlyMemory AsMemory() { if (_chunks.Count == 1) { return _chunks[0].WrittenMemory; } else { return ToByteArray(); } } /// /// Gets a sequence representing the bytes that have been written so far /// /// Sequence of bytes public ReadOnlySequence AsSequence() { ReadOnlySequenceBuilder builder = new ReadOnlySequenceBuilder(); AppendTo(builder); return builder.Construct(); } /// /// Gets a sequence representing the bytes that have been written so far, starting at the given offset /// /// Offset to start from /// Sequence of bytes public ReadOnlySequence AsSequence(int offset) { // TODO: could do a binary search for offset and work forwards from there return AsSequence().Slice(offset); } /// /// Gets a sequence representing the bytes that have been written so far, starting at the given offset and /// /// Offset to start from /// Length of the sequence to return /// Sequence of bytes public ReadOnlySequence AsSequence(int offset, int length) { // TODO: could do a binary search for offset and work fowards from there return AsSequence().Slice(offset, length); } /// /// Copies the data to the given output span /// /// Span to write to public void CopyTo(Span span) { foreach (Chunk chunk in _chunks) { Span output = span.Slice(chunk.RunningIndex); chunk.WrittenSpan.CopyTo(output); } } /// /// Create a byte array from the sequence /// /// Byte array containing the current buffer data public byte[] ToByteArray() { byte[] data = new byte[Length]; CopyTo(data); return data; } } }