// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Buffers.Binary; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using EpicGames.Core; namespace EpicGames.Serialization { /// /// Exception for /// public class CbWriterException : Exception { /// /// Constructor /// /// public CbWriterException(string message) : base(message) { } /// /// Constructor /// /// /// public CbWriterException(string message, Exception? ex) : base(message, ex) { } } /// /// Interface for compact binary writers /// public interface ICbWriter { /// /// Begin writing an object field /// /// Name of the field. May be empty for fields that are not part of another object. void BeginObject(CbFieldName name); /// /// End the current object /// void EndObject(); /// /// Begin writing a named array field /// /// Name of the field, or an empty string. /// Type of the field. May be for non-uniform arrays. void BeginArray(CbFieldName name, CbFieldType elementType); /// /// End the current array /// void EndArray(); /// /// Writes the header for a named field /// /// Type of the field /// Name of the field. May be empty for fields that are not part of another object. /// Length of data for the field Span WriteField(CbFieldType type, CbFieldName name, int length); /// /// Writes a reference to an external binary field into the output stream /// /// Data to reference void WriteReference(ReadOnlyMemory data); } /// /// Wrapper for a field name, which can come from an encoded UTF8 string or regular C# string /// public record struct CbFieldName(Utf8String Text) { /// /// Implicit conversion from a string /// public static implicit operator CbFieldName(string text) => new CbFieldName(new Utf8String(text)); /// /// Implicit conversion from a utf8 string /// public static implicit operator CbFieldName(Utf8String text) => new CbFieldName(text); } /// /// Base class for implementations. Tracks structural data for fixing up lengths and offsets, without managing any buffers for field data. /// public abstract class CbWriterBase : ICbWriter { /// /// Stores information about an object or array scope within the written buffer which requires a header to be inserted containing /// the size or number of elements when copied to an output buffer /// class Scope { public CbFieldType _fieldType; public bool _writeFieldType; public CbFieldType _uniformFieldType; public Utf8String _name; public int _itemCount; public ReadOnlyMemory _data; public int _length; public Scope? _firstChild; public Scope? _lastChild; public Scope? _nextSibling; public void Reset() { _fieldType = CbFieldType.None; _writeFieldType = true; _uniformFieldType = CbFieldType.None; _name = default; _itemCount = 0; _data = default; _length = 0; _firstChild = null; _lastChild = null; _nextSibling = null; } public void AddChild(Scope child) { if (_lastChild == null) { _firstChild = child; } else { _lastChild._nextSibling = child; } _lastChild = child; } } readonly Scope _rootScope = new Scope { _fieldType = CbFieldType.Array }; readonly Stack _openScopes = new Stack(); readonly Stack _freeScopes = new Stack(); Memory _buffer = Memory.Empty; int _bufferPos = 0; // Offset of the first field in the current buffer int _bufferEnd = 0; // Current end of the buffer /// /// Constructor /// protected CbWriterBase() { _openScopes.Push(_rootScope); } /// /// Resets the current contents of the writer /// protected void Reset() { AddChildrenToFreeList(_rootScope); _rootScope.Reset(); } void AddChildrenToFreeList(Scope root) { for (Scope? child = root._firstChild; child != null; child = child._nextSibling) { AddChildrenToFreeList(child); child.Reset(); _freeScopes.Push(child); } } /// /// Allocate a scope object /// /// New scope object Scope AllocScope() { Scope? scope; if (!_freeScopes.TryPop(out scope)) { scope = new Scope(); } return scope; } /// /// Creates a scope containing leaf data /// /// void AddLeafData(ReadOnlyMemory data) { Scope scope = AllocScope(); scope._data = data; scope._length = data.Length; Scope currentScope = _openScopes.Peek(); currentScope.AddChild(scope); } /// /// Insert a new scope /// Scope EnterScope(CbFieldType fieldType, CbFieldName name) { Scope currentScope = _openScopes.Peek(); Scope scope = AllocScope(); scope._fieldType = fieldType; scope._writeFieldType = currentScope._uniformFieldType == CbFieldType.None; scope._name = name.Text; currentScope.AddChild(scope); _openScopes.Push(scope); return scope; } /// /// Pop a scope from the current open list /// void LeaveScope() { WriteFields(); Scope scope = _openScopes.Peek(); // Measure the length of all children int childrenLength = 0; for (Scope? child = scope._firstChild; child != null; child = child._nextSibling) { childrenLength += child._length; } // Measure the total length of this scope if (scope._fieldType != CbFieldType.None) { // Measure the size of the field header int headerLength = 0; if (scope._writeFieldType) { headerLength++; if (!scope._name.IsEmpty) { headerLength += VarInt.MeasureUnsigned(scope._name.Length) + scope._name.Length; } } // Measure the size of the payload int payloadLength = 0; if (CbFieldUtils.IsArray(scope._fieldType)) { payloadLength += VarInt.MeasureUnsigned(scope._itemCount); } if (scope._fieldType == CbFieldType.UniformObject || scope._fieldType == CbFieldType.UniformArray) { payloadLength++; } // Measure the size of writing the payload int payloadLengthBytes = VarInt.MeasureUnsigned(payloadLength + childrenLength); // Allocate the header Memory header = Allocate(headerLength + payloadLengthBytes + payloadLength); scope._data = header; _bufferPos = _bufferEnd; // Write all the fields to the header buffer Span span = header.Span; if (scope._writeFieldType) { if (scope._name.IsEmpty) { span[0] = (byte)scope._fieldType; span = span.Slice(1); } else { span[0] = (byte)(scope._fieldType | CbFieldType.HasFieldName); span = span.Slice(1); int bytesWritten = VarInt.WriteUnsigned(span, scope._name.Length); span = span.Slice(bytesWritten); scope._name.Span.CopyTo(span); span = span.Slice(scope._name.Length); } } VarInt.WriteUnsigned(span, payloadLength + childrenLength); span = span.Slice(payloadLengthBytes); if (CbFieldUtils.IsArray(scope._fieldType)) { int itemCountBytes = VarInt.WriteUnsigned(span, scope._itemCount); span = span.Slice(itemCountBytes); } // Write the type for uniform arrays if (scope._fieldType == CbFieldType.UniformObject || scope._fieldType == CbFieldType.UniformArray) { span[0] = (byte)scope._uniformFieldType; span = span.Slice(1); } Debug.Assert(span.Length == 0); } // Set the final size of this scope, and pop it from the current open stack scope._length = childrenLength + scope._data.Length; _openScopes.Pop(); } /// /// Begin writing an object field /// /// Name of the field public void BeginObject(CbFieldName name) { WriteFields(); Scope parentScope = _openScopes.Peek(); parentScope._itemCount++; EnterScope(CbFieldType.Object, name); } /// /// End the current object /// public void EndObject() => LeaveScope(); /// /// Begin writing a named array field /// /// /// Type of elements in the array public void BeginArray(CbFieldName name, CbFieldType elementType) { WriteFields(); Scope parentScope = _openScopes.Peek(); parentScope._itemCount++; Scope scope = EnterScope((elementType == CbFieldType.None) ? CbFieldType.Array : CbFieldType.UniformArray, name); scope._uniformFieldType = elementType; } /// /// End the current array /// public void EndArray() => LeaveScope(); void WriteFields() { if (_bufferPos < _bufferEnd) { AddLeafData(_buffer.Slice(_bufferPos, _bufferEnd - _bufferPos)); _bufferPos = _bufferEnd; } } private Memory Allocate(int length) { if (_bufferEnd + length > _buffer.Length) { WriteFields(); _buffer = AllocateChunk(length); _bufferPos = 0; _bufferEnd = 0; } Memory data = _buffer.Slice(_bufferEnd, length); _bufferEnd += length; return data; } /// /// Allocates a chunk of data for storing CB fragments. This should be relatively coarse; the returned chunk will be reused for subsequent writes until full. /// /// Minimum size of the chunk /// New chunk of memory protected abstract Memory AllocateChunk(int minSize); /// /// Writes the header for a named field /// /// Type of the field /// Name of the field /// Size of the field public Span WriteField(CbFieldType type, CbFieldName name, int size) { WriteFieldHeader(type, name.Text); Span span = Allocate(size).Span; if (_openScopes.Count == 1) { // If this field is at the root, flush it immediately WriteFields(); } return span; } void WriteFieldHeader(CbFieldType type, Utf8String name) { Scope scope = _openScopes.Peek(); if (name.IsEmpty) { CbFieldType scopeType = scope._fieldType; if (!CbFieldUtils.IsArray(scopeType)) { throw new CbWriterException($"Anonymous fields are not allowed within fields of type {scopeType}"); } CbFieldType elementType = scope._uniformFieldType; if (elementType == CbFieldType.None) { Allocate(1).Span[0] = (byte)type; } else if (elementType != type) { throw new CbWriterException($"Mismatched type for uniform array - expected {elementType}, not {type}"); } scope._itemCount++; } else { CbFieldType scopeType = scope._fieldType; if (!CbFieldUtils.IsObject(scopeType)) { throw new CbWriterException($"Named fields are not allowed within fields of type {scopeType}"); } CbFieldType elementType = scope._uniformFieldType; int nameVarIntLength = VarInt.MeasureUnsigned(name.Length); if (elementType == CbFieldType.None) { Span buffer = Allocate(1 + nameVarIntLength + name.Length).Span; buffer[0] = (byte)(type | CbFieldType.HasFieldName); WriteBinaryPayload(buffer[1..], name.Span); } else { if (elementType != type) { throw new CbWriterException($"Mismatched type for uniform object - expected {elementType}, not {type}"); } Memory buffer = Allocate(name.Length); WriteBinaryPayload(buffer.Span, name.Span); } scope._itemCount++; } } /// public void WriteReference(ReadOnlyMemory data) { WriteFields(); AddLeafData(data); } /// /// Writes the payload for a binary value /// /// Output buffer /// Value to be written static void WriteBinaryPayload(Span output, ReadOnlySpan value) { int varIntLength = VarInt.WriteUnsigned(output, value.Length); output = output[varIntLength..]; value.CopyTo(output); } /// /// Gets the size of the serialized data /// /// public int GetSize() { if (_openScopes.Count > 1) { throw new CbWriterException("Unfinished scope in writer"); } int length = 0; for (Scope? child = _rootScope._firstChild; child != null; child = child._nextSibling) { length += child._length; } return length; } /// /// Copy the data from this writer to a buffer /// /// public void CopyTo(Span buffer) { Copy(_rootScope, buffer); } static Span Copy(Scope scope, Span span) { if (scope._data.Length > 0) { scope._data.Span.CopyTo(span); span = span.Slice(scope._data.Length); } for (Scope? child = scope._firstChild; child != null; child = child._nextSibling) { span = Copy(child, span); } return span; } /// /// Computes the hash for this object /// /// Hash for the object public IoHash ComputeHash() { using (Blake3.Hasher hasher = Blake3.Hasher.New()) { foreach (ReadOnlyMemory segment in GetSegments()) { hasher.Update(segment.Span); } return IoHash.FromBlake3(hasher); } } /// /// Convert the data into a compact binary object /// /// public CbObject ToObject() { return new CbObject(ToByteArray()); } /// /// Convert the data into a flat array /// /// public byte[] ToByteArray() { byte[] buffer = new byte[GetSize()]; CopyTo(buffer); return buffer; } /// /// Enumerate all the segments in the data that has been written /// /// Sequence of segments public List> GetSegments() { List> segments = []; GetSegments(_rootScope, segments); return segments; } static void GetSegments(Scope scope, List> segments) { if (scope._data.Length > 0) { segments.Add(scope._data); } for (Scope? child = scope._firstChild; child != null; child = child._nextSibling) { GetSegments(child, segments); } } /// /// Gets the contents of this writer as a stream /// /// New stream for the contents of this object public Stream AsStream() => new ReadStream(GetSegments().GetEnumerator(), GetSize()); class ReadStream(IEnumerator> segments, long length) : Stream { ReadOnlyMemory _segment; long _positionInternal; /// public override bool CanRead => true; /// public override bool CanSeek => false; /// public override bool CanWrite => false; /// public override long Length { get; } = length; /// public override long Position { get => _positionInternal; set => throw new NotSupportedException(); } /// public override void Flush() { } /// public override int Read(Span buffer) { int readLength = 0; while (readLength < buffer.Length) { while (_segment.Length == 0) { if (!segments.MoveNext()) { return readLength; } _segment = segments.Current; } int copyLength = Math.Min(_segment.Length, buffer.Length); _segment.Span.Slice(0, copyLength).CopyTo(buffer.Slice(readLength)); _segment = _segment.Slice(copyLength); _positionInternal += copyLength; readLength += copyLength; } return readLength; } /// public override int Read(byte[] buffer, int offset, int count) => Read(buffer.AsSpan(offset, count)); /// public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); /// public override void SetLength(long value) => throw new NotSupportedException(); /// public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); } } /// /// Forward-only writer for compact binary objects /// public class CbWriter : CbWriterBase { /// /// Size of data to preallocate by default /// public const int DefaultChunkSize = 1024; readonly List _chunks = []; readonly List _freeChunks = []; /// /// Constructor /// public CbWriter() { } /// /// Constructor /// /// Amount of data to reserve for output public CbWriter(int reserve) { _freeChunks.Add(new byte[reserve]); } /// /// Clear the current contents of the writer /// public void Clear() { Reset(); _freeChunks.AddRange(_chunks); _chunks.Clear(); } /// /// Ensures that the required space is available in a contiguous chunk /// /// Minimum required space public void Reserve(int reserve) { int allocate = Math.Max(DefaultChunkSize, reserve); if (!_freeChunks.Any(x => x.Length >= reserve)) { _freeChunks.Add(new byte[allocate + 4096]); } } /// protected override Memory AllocateChunk(int minSize) { for (int idx = 0; idx < _freeChunks.Count; idx++) { byte[] data = _freeChunks[idx]; if (data.Length >= minSize) { _freeChunks.RemoveAt(idx); return data; } } return new byte[Math.Max(minSize, DefaultChunkSize)]; } } /// /// Extension methods for /// public static class CbWriterExtensions { static int MeasureFieldWithLength(int length) => length + VarInt.MeasureUnsigned(length); static Span WriteFieldWithLength(this ICbWriter writer, CbFieldType type, CbFieldName name, int length) { int fullLength = MeasureFieldWithLength(length); Span buffer = writer.WriteField(type, name, fullLength); int lengthLength = VarInt.WriteUnsigned(buffer, length); return buffer.Slice(lengthLength); } /// /// Begin writing an object field /// /// Writer for output data public static void BeginObject(this ICbWriter writer) => writer.BeginObject(default); /// /// Begin writing an array field /// /// Writer for output data public static void BeginArray(this ICbWriter writer) => writer.BeginArray(default, CbFieldType.None); /// /// Begin writing a named array field /// /// Writer for output data /// Name of the field public static void BeginArray(this ICbWriter writer, CbFieldName name) => writer.BeginArray(name, CbFieldType.None); /// /// Begin writing a uniform array field /// /// Writer for output data /// The field type for elements in the array public static void BeginUniformArray(this ICbWriter writer, CbFieldType fieldType) => BeginUniformArray(writer, default, fieldType); /// /// Begin writing a named uniform array field /// /// Writer for output data /// Name of the field /// The field type for elements in the array public static void BeginUniformArray(this ICbWriter writer, CbFieldName name, CbFieldType fieldType) => writer.BeginArray(name, fieldType); /// /// End writing a uniform array field /// /// Writer for output data public static void EndUniformArray(this ICbWriter writer) => writer.EndArray(); /// /// Copies an entire field value to the output /// /// Writer for output data /// public static void WriteFieldValue(this ICbWriter writer, CbField field) => WriteField(writer, default, field); /// /// Copies an entire field value to the output, using the name from the field /// /// Writer for output data /// public static void WriteField(this ICbWriter writer, CbField field) => WriteField(writer, field.GetName(), field); /// /// Copies an entire field value to the output /// /// Writer for output data /// Name of the field /// public static void WriteField(this ICbWriter writer, CbFieldName name, CbField field) { ReadOnlySpan source = field.GetPayloadView().Span; Span target = writer.WriteField(field.GetType(), name, source.Length); source.CopyTo(target); } /// /// Write a null field /// /// Writer for output data public static void WriteNullValue(this ICbWriter writer) => WriteNull(writer, default); /// /// Write a named null field /// /// Writer for output data /// Name of the field public static void WriteNull(this ICbWriter writer, CbFieldName name) => writer.WriteField(CbFieldType.Null, name, 0); /// /// Writes a boolean value /// /// Writer for output data /// Value to be written public static void WriteBoolValue(this ICbWriter writer, bool value) => WriteBool(writer, default, value); /// /// Writes a boolean value /// /// Writer for output data /// Name of the field /// Value to be written public static void WriteBool(this ICbWriter writer, CbFieldName name, bool value) => writer.WriteField(value ? CbFieldType.BoolTrue : CbFieldType.BoolFalse, name, 0); /// /// Writes an unnamed integer field /// /// Writer for output data /// Value to be written public static void WriteIntegerValue(this ICbWriter writer, int value) => WriteInteger(writer, default, value); /// /// Writes an unnamed integer field /// /// Writer for output data /// Value to be written public static void WriteIntegerValue(this ICbWriter writer, long value) => WriteInteger(writer, default, value); /// /// Writes an named integer field /// /// Writer for output data /// Name of the field /// Value to be written public static void WriteInteger(this ICbWriter writer, CbFieldName name, int value) => WriteInteger(writer, name, (long)value); /// /// Writes an named integer field /// /// Writer for output data /// Name of the field /// Value to be written public static void WriteInteger(this ICbWriter writer, CbFieldName name, long value) { if (value >= 0) { int length = VarInt.MeasureUnsigned((ulong)value); Span data = writer.WriteField(CbFieldType.IntegerPositive, name, length); VarInt.WriteUnsigned(data, (ulong)value); } else { int length = VarInt.MeasureUnsigned((ulong)-value); Span data = writer.WriteField(CbFieldType.IntegerNegative, name, length); VarInt.WriteUnsigned(data, (ulong)-value); } } /// /// Writes an unnamed integer field /// /// Writer for output data /// Value to be written public static void WriteIntegerValue(this ICbWriter writer, ulong value) => WriteInteger(writer, default, value); /// /// Writes a named integer field /// /// Writer for output data /// Name of the field /// Value to be written public static void WriteInteger(this ICbWriter writer, CbFieldName name, ulong value) { int length = VarInt.MeasureUnsigned((ulong)value); Span data = writer.WriteField(CbFieldType.IntegerPositive, name, length); VarInt.WriteUnsigned(data, (ulong)value); } /// /// Writes an unnamed double field /// /// Writer for output data /// Value to be written public static void WriteDoubleValue(this ICbWriter writer, double value) => WriteDouble(writer, default, value); /// /// Writes a named double field /// /// Writer for output data /// Name of the field /// Value to be written public static void WriteDouble(this ICbWriter writer, CbFieldName name, double value) { Span buffer = writer.WriteField(CbFieldType.Float64, name, sizeof(double)); BinaryPrimitives.WriteDoubleBigEndian(buffer, value); } /// /// Writes an unnamed field /// /// Writer for output data /// Value to be written public static void WriteDateTimeValue(this ICbWriter writer, DateTime value) => WriteDateTime(writer, default, value); /// /// Writes a named field /// /// Writer for output data /// Name of the field /// Value to be written public static void WriteDateTime(this ICbWriter writer, CbFieldName name, DateTime value) { Span buffer = writer.WriteField(CbFieldType.DateTime, name, sizeof(long)); BinaryPrimitives.WriteInt64BigEndian(buffer, value.Ticks); } /// /// Writes an unnamed field /// /// Writer for output data /// Value to be written public static void WriteHashValue(this ICbWriter writer, IoHash value) => WriteHash(writer, default, value); /// /// Writes a named field /// /// Writer for output data /// Name of the field /// Value to be written public static void WriteHash(this ICbWriter writer, CbFieldName name, IoHash value) { Span buffer = writer.WriteField(CbFieldType.Hash, name, IoHash.NumBytes); value.CopyTo(buffer); } /// /// Writes an unnamed reference to a binary attachment /// /// Writer for output data /// Hash of the attachment public static void WriteBinaryAttachmentValue(this ICbWriter writer, IoHash hash) => WriteBinaryAttachment(writer, default, hash); /// /// Writes a named reference to a binary attachment /// /// Writer for output data /// Name of the field /// Hash of the attachment public static void WriteBinaryAttachment(this ICbWriter writer, CbFieldName name, IoHash hash) { Span buffer = writer.WriteField(CbFieldType.BinaryAttachment, name, IoHash.NumBytes); hash.CopyTo(buffer); } /// /// Writes an object directly into the writer /// /// Writer for output data /// Object to write public static void WriteObject(this ICbWriter writer, CbObject obj) => WriteObject(writer, default, obj); /// /// Writes an object directly into the writer /// /// Writer for output data /// Name of the object /// Object to write public static void WriteObject(this ICbWriter writer, CbFieldName name, CbObject obj) { ReadOnlyMemory view = obj.AsField().Payload; Span buffer = writer.WriteField(CbFieldType.Object, name, view.Length); view.Span.CopyTo(buffer); } /// /// Writes an unnamed reference to an object attachment /// /// Writer for output data /// Hash of the attachment public static void WriteObjectAttachmentValue(this ICbWriter writer, IoHash hash) => WriteObjectAttachment(writer, default, hash); /// /// Writes a named reference to an object attachment /// /// Writer for output data /// Name of the field /// Hash of the attachment public static void WriteObjectAttachment(this ICbWriter writer, CbFieldName name, IoHash hash) { Span buffer = writer.WriteField(CbFieldType.ObjectAttachment, name, IoHash.NumBytes); hash.CopyTo(buffer); } /// /// Writes a named object id /// /// Writer for output data /// Name of the field /// The object id /// public static void WriteObjectId(this ICbWriter writer, CbFieldName name, CbObjectId objectId) { Span buffer = writer.WriteField(CbFieldType.ObjectId, name, 12); objectId.CopyTo(buffer); } /// /// Writes a unnamed object id /// /// Writer for output data /// The object id /// public static void WriteObjectIdValue(this ICbWriter writer, CbObjectId objectId) { Span buffer = writer.WriteField(CbFieldType.ObjectId, default, 12); objectId.CopyTo(buffer); } /// /// Writes an unnamed string value /// /// Writer for output data /// Value to be written public static void WriteStringValue(this ICbWriter writer, string value) => WriteUtf8StringValue(writer, new Utf8String(value)); /// /// Writes a named string value /// /// Writer for output data /// Name of the field /// Value to be written public static void WriteString(this ICbWriter writer, CbFieldName name, string? value) { if (value != null) { writer.WriteUtf8String(name, new Utf8String(value)); } } /// /// Writes an unnamed string value /// /// Writer for output data /// Value to be written public static void WriteUtf8StringValue(this ICbWriter writer, Utf8String value) => WriteUtf8String(writer, default, value); /// /// Writes a named string value /// /// Writer for output data /// Name of the field /// Value to be written public static void WriteUtf8String(this ICbWriter writer, CbFieldName name, Utf8String value) { Span buffer = WriteFieldWithLength(writer, CbFieldType.String, name, value.Length); value.Span.CopyTo(buffer); } /// /// Writes an external binary value into the output stream /// /// Writer for output data /// Name of the field /// Data to reference public static void WriteBinaryReference(this ICbWriter writer, CbFieldName name, ReadOnlyMemory data) { int lengthBytes = VarInt.MeasureUnsigned(data.Length); Span span = writer.WriteField(CbFieldType.Binary, name, lengthBytes); VarInt.WriteUnsigned(span, data.Length); writer.WriteReference(data); } /// /// Writes an unnamed binary value /// /// Writer for output data /// Value to be written public static void WriteBinarySpanValue(this ICbWriter writer, ReadOnlySpan value) => WriteBinarySpan(writer, default, value); /// /// Writes a named binary value /// /// Writer for output data /// Name of the field /// Value to be written public static void WriteBinarySpan(this ICbWriter writer, CbFieldName name, ReadOnlySpan value) { Span buffer = WriteFieldWithLength(writer, CbFieldType.Binary, name, value.Length); value.CopyTo(buffer); } /// /// Writes an unnamed binary value /// /// Writer for output data /// Value to be written public static void WriteBinaryValue(this ICbWriter writer, ReadOnlyMemory value) => writer.WriteBinarySpanValue(value.Span); /// /// Writes a named binary value /// /// Writer for output data /// Name of the field /// Value to be written public static void WriteBinary(this ICbWriter writer, CbFieldName name, ReadOnlyMemory value) => writer.WriteBinarySpan(name, value.Span); /// /// Writes an unnamed binary value /// /// Writer for output data /// Value to be written public static void WriteBinaryArrayValue(this ICbWriter writer, ReadOnlyMemory value) => writer.WriteBinarySpanValue(value.Span); /// /// Writes a named binary value /// /// Writer for output data /// Name of the field /// Value to be written public static void WriteBinaryArray(this ICbWriter writer, CbFieldName name, ReadOnlyMemory value) => writer.WriteBinarySpan(name, value.Span); } }