File: System\Formats\Cbor\Writer\CborWriter.cs
Web Access
Project: src\src\libraries\System.Formats.Cbor\src\System.Formats.Cbor.csproj (System.Formats.Cbor)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
 
namespace System.Formats.Cbor
{
    /// <summary>A writer for Concise Binary Object Representation (CBOR) encoded data.</summary>
    public partial class CborWriter
    {
        private const int DefaultCapacitySentinel = -1;
        private static readonly ArrayPool<byte> s_bufferPool = ArrayPool<byte>.Create();
 
        private byte[] _buffer;
        private int _offset;
 
        private Stack<StackFrame>? _nestedDataItems;
        private CborMajorType? _currentMajorType; // major type of the current data item context
        private int? _definiteLength; // predetermined definite-length of current data item context
        private int _itemsWritten; // number of items written in the current context
        private int _frameOffset; // buffer offset particular to the current data item context
        private bool _isTagContext; // true if writer is expecting a tagged value
 
        // Map-specific book-keeping
        private int? _currentKeyOffset; // offset for the current key encoding
        private int? _currentValueOffset; // offset for the current value encoding
        private bool _keysRequireSorting; // tracks whether key/value pair encodings need to be sorted
        private List<KeyValuePairEncodingRange>? _keyValuePairEncodingRanges; // all key/value pair encoding ranges
        private HashSet<(int Offset, int Length)>? _keyEncodingRanges; // all key encoding ranges up to encoding equality
 
        /// <summary>Gets the conformance mode used by this writer.</summary>
        /// <value>One of the enumeration values that represent the conformance mode used by this writer.</value>
        public CborConformanceMode ConformanceMode { get; }
 
        /// <summary>Gets a value that indicates whether the writer automatically converts indefinite-length encodings into definite-length equivalents.</summary>
        /// <value><see langword="true" /> if the writer automatically converts indefinite-length encodings into definite-length equivalents; otherwise, <see langword="false" />.</value>
        public bool ConvertIndefiniteLengthEncodings { get; }
 
        /// <summary>Gets a value that indicates whether this writer allows multiple root-level CBOR data items.</summary>
        /// <value><see langword="true" /> if the writer allows multiple root-level CBOR data items; otherwise, <see langword="false" />.</value>
        public bool AllowMultipleRootLevelValues { get; }
 
        /// <summary>Gets the writer's current level of nestedness in the CBOR document.</summary>
        /// <value>A number that represents the current level of nestedness in the CBOR document.</value>
        public int CurrentDepth => _nestedDataItems is null ? 0 : _nestedDataItems.Count;
 
        /// <summary>Gets the total number of bytes that have been written to the buffer.</summary>
        /// <value>The total number of bytes that have been written to the buffer.</value>
        public int BytesWritten => _offset;
 
        /// <summary>Declares whether the writer has completed writing a complete root-level CBOR document, or sequence of root-level CBOR documents.</summary>
        /// <value><see langword="true" /> if the writer has completed writing a complete root-level CBOR document, or sequence of root-level CBOR documents; <see langword="false" /> otherwise.</value>
        public bool IsWriteCompleted => _currentMajorType is null && _itemsWritten > 0;
 
        /// <summary>Initializes a new instance of <see cref="CborWriter" /> class using the specified configuration.</summary>
        /// <param name="conformanceMode">One of the enumeration values that specifies the guidance on the conformance checks performed on the encoded data.
        /// Defaults to <see cref="CborConformanceMode.Strict" /> conformance mode.</param>
        /// <param name="convertIndefiniteLengthEncodings"><see langword="true" /> to enable automatically converting indefinite-length encodings into definite-length equivalents and allow use of indefinite-length write APIs in conformance modes that otherwise do not permit it; otherwise, <see langword="false" />.</param>
        /// <param name="allowMultipleRootLevelValues"><see langword="true" /> to allow multiple root-level values to be written by the writer; otherwise, <see langword="false" />.</param>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="conformanceMode" /> is not a defined <see cref="CborConformanceMode" />.</exception>
        [EditorBrowsable(EditorBrowsableState.Never)]
        public CborWriter(CborConformanceMode conformanceMode, bool convertIndefiniteLengthEncodings, bool allowMultipleRootLevelValues)
            : this(conformanceMode, convertIndefiniteLengthEncodings, allowMultipleRootLevelValues, DefaultCapacitySentinel)
        {
        }
 
        /// <summary>Initializes a new instance of <see cref="CborWriter" /> class using the specified configuration.</summary>
        /// <param name="conformanceMode">One of the enumeration values that specifies the guidance on the conformance checks performed on the encoded data.
        /// Defaults to <see cref="CborConformanceMode.Strict" /> conformance mode.</param>
        /// <param name="convertIndefiniteLengthEncodings"><see langword="true" /> to enable automatically converting indefinite-length encodings into definite-length equivalents and allow use of indefinite-length write APIs in conformance modes that otherwise do not permit it; otherwise, <see langword="false" />.</param>
        /// <param name="allowMultipleRootLevelValues"><see langword="true" /> to allow multiple root-level values to be written by the writer; otherwise, <see langword="false" />.</param>
        /// <param name="initialCapacity">The initial capacity of the underlying buffer. The value -1 can be used to use the default capacity.</param>
        /// <exception cref="ArgumentOutOfRangeException">
        /// <para><paramref name="conformanceMode" /> is not a defined <see cref="CborConformanceMode" />.</para>
        /// <para>-or-</para>
        /// <para><paramref name="initialCapacity"/> is not zero, positive, or the default value indicator -1.</para>
        /// </exception>
        public CborWriter(
            CborConformanceMode conformanceMode = CborConformanceMode.Strict,
            bool convertIndefiniteLengthEncodings = false,
            bool allowMultipleRootLevelValues = false,
            int initialCapacity = DefaultCapacitySentinel)
        {
            CborConformanceModeHelpers.Validate(conformanceMode);
 
            ConformanceMode = conformanceMode;
            ConvertIndefiniteLengthEncodings = convertIndefiniteLengthEncodings;
            AllowMultipleRootLevelValues = allowMultipleRootLevelValues;
            _definiteLength = allowMultipleRootLevelValues ? null : (int?)1;
 
            _buffer = initialCapacity switch
            {
                DefaultCapacitySentinel or 0 => Array.Empty<byte>(),
                < -1 => throw new ArgumentOutOfRangeException(nameof(initialCapacity)),
                _ => new byte[initialCapacity],
            };
        }
 
        /// <summary>Resets the writer to have no data, without releasing resources.</summary>
        public void Reset()
        {
            if (_offset > 0)
            {
                Array.Clear(_buffer, 0, _offset);
 
                _offset = 0;
                _nestedDataItems?.Clear();
                _currentMajorType = null;
                _definiteLength = null;
                _itemsWritten = 0;
                _frameOffset = 0;
                _isTagContext = false;
 
                _currentKeyOffset = null;
                _currentValueOffset = null;
                _keysRequireSorting = false;
                _keyValuePairEncodingRanges?.Clear();
                _keyEncodingRanges?.Clear();
            }
        }
 
        /// <summary>Writes a single CBOR data item which has already been encoded.</summary>
        /// <param name="encodedValue">The encoded value to write.</param>
        /// <exception cref="ArgumentException"><para><paramref name="encodedValue" /> is not a well-formed CBOR encoding.</para>
        /// <para>-or-</para>
        /// <para><paramref name="encodedValue" /> is not valid under the current conformance mode.</para></exception>
        public void WriteEncodedValue(ReadOnlySpan<byte> encodedValue)
        {
            ValidateEncoding(encodedValue, ConformanceMode);
            EnsureWriteCapacity(encodedValue.Length);
 
            // even though the encoding might be valid CBOR, it might not be valid within the current writer context.
            // E.g. we're at the end of a definite-length collection or writing integers in an indefinite-length string.
            // For this reason we write the initial byte separately and perform the usual validation.
            CborInitialByte initialByte = new CborInitialByte(encodedValue[0]);
            WriteInitialByte(initialByte);
 
            // now copy any remaining bytes
            encodedValue = encodedValue.Slice(1);
 
            if (!encodedValue.IsEmpty)
            {
                encodedValue.CopyTo(_buffer.AsSpan(_offset));
                _offset += encodedValue.Length;
            }
 
            AdvanceDataItemCounters();
 
            static unsafe void ValidateEncoding(ReadOnlySpan<byte> encodedValue, CborConformanceMode conformanceMode)
            {
                fixed (byte* ptr = &MemoryMarshal.GetReference(encodedValue))
                {
                    using var manager = new PointerMemoryManager<byte>(ptr, encodedValue.Length);
                    var reader = new CborReader(manager.Memory, conformanceMode: conformanceMode, allowMultipleRootLevelValues: false);
 
                    try
                    {
                        reader.SkipValue(disableConformanceModeChecks: false);
                    }
                    catch (CborContentException e)
                    {
                        throw new ArgumentException(SR.Cbor_Writer_PayloadIsNotValidCbor, e);
                    }
 
                    if (reader.BytesRemaining > 0)
                    {
                        throw new ArgumentException(SR.Cbor_Writer_PayloadIsNotValidCbor);
                    }
                }
 
            }
        }
 
        /// <summary>Returns a new array containing the encoded value.</summary>
        /// <returns>A precisely-sized array containing the encoded value.</returns>
        /// <exception cref="InvalidOperationException">The writer does not contain a complete CBOR value or sequence of root-level values.</exception>
        public byte[] Encode() => GetSpanEncoding().ToArray();
 
        /// <summary>Writes the encoded representation of the data to <paramref name="destination" />.</summary>
        /// <param name="destination">The buffer in which to write.</param>
        /// <returns>The number of bytes written to <paramref name="destination" />.</returns>
        /// <exception cref="InvalidOperationException">The writer does not contain a complete CBOR value or sequence of root-level values.</exception>
        /// <exception cref="ArgumentException">The destination buffer is not large enough to hold the encoded value.</exception>
        public int Encode(Span<byte> destination)
        {
            ReadOnlySpan<byte> encoding = GetSpanEncoding();
 
            if (encoding.Length > destination.Length)
            {
                throw new ArgumentException(SR.Argument_EncodeDestinationTooSmall, nameof(destination));
            }
 
            encoding.CopyTo(destination);
            return encoding.Length;
        }
 
        /// <summary>Attempts to write the encoded representation of the data to <paramref name="destination" />.</summary>
        /// <param name="destination">The buffer in which to write.</param>
        /// <param name="bytesWritten">When this method returns, contains the number of bytes written to <paramref name="destination" />.</param>
        /// <returns><see langword="true" /> if the encode succeeded, <see langword="false" /> if <paramref name="destination" /> is too small.</returns>
        /// <exception cref="InvalidOperationException">The writer does not contain a complete CBOR value or sequence of root-level values.</exception>
        public bool TryEncode(Span<byte> destination, out int bytesWritten)
        {
            ReadOnlySpan<byte> encoding = GetSpanEncoding();
 
            if (encoding.Length > destination.Length)
            {
                bytesWritten = 0;
                return false;
            }
 
            encoding.CopyTo(destination);
            bytesWritten = encoding.Length;
            return true;
        }
 
        private ReadOnlySpan<byte> GetSpanEncoding()
        {
            if (!IsWriteCompleted)
            {
                throw new InvalidOperationException(SR.Cbor_Writer_IncompleteCborDocument);
            }
 
            return new ReadOnlySpan<byte>(_buffer, 0, _offset);
        }
 
        private void EnsureWriteCapacity(int pendingCount)
        {
            if (pendingCount < 0)
            {
                throw new OverflowException();
            }
 
            int currentCapacity = _buffer.Length;
            int requiredCapacity = _offset + pendingCount;
            if (currentCapacity < requiredCapacity)
            {
                int newCapacity = currentCapacity == 0 ? 1024 : currentCapacity * 2;
                const uint MaxArrayLength = 0x7FFFFFC7; // Array.MaxLength
#if NET
                Debug.Assert(MaxArrayLength == Array.MaxLength);
#endif
                if ((uint)newCapacity > MaxArrayLength || newCapacity < requiredCapacity)
                {
                    newCapacity = requiredCapacity;
                }
 
                byte[] newBuffer = new byte[newCapacity];
                new ReadOnlySpan<byte>(_buffer, 0, _offset).CopyTo(newBuffer);
                _buffer = newBuffer;
            }
        }
 
        private void PushDataItem(CborMajorType newMajorType, int? definiteLength)
        {
            _nestedDataItems ??= new Stack<StackFrame>();
 
            var frame = new StackFrame(
                type: _currentMajorType,
                frameOffset: _frameOffset,
                definiteLength: _definiteLength,
                itemsWritten: _itemsWritten,
                currentKeyOffset: _currentKeyOffset,
                currentValueOffset: _currentValueOffset,
                keysRequireSorting: _keysRequireSorting,
                keyValuePairEncodingRanges: _keyValuePairEncodingRanges,
                keyEncodingRanges: _keyEncodingRanges
            );
 
            _nestedDataItems.Push(frame);
 
            _currentMajorType = newMajorType;
            _frameOffset = _offset;
            _definiteLength = definiteLength;
            _itemsWritten = 0;
            _currentKeyOffset = null;
            _currentValueOffset = null;
            _keysRequireSorting = false;
            _keyEncodingRanges = null;
            _keyValuePairEncodingRanges = null;
            _isTagContext = false;
        }
 
        private void PopDataItem(CborMajorType typeToPop)
        {
            // Validate that the pop operation can be performed
            if (typeToPop != _currentMajorType)
            {
                if (_currentMajorType.HasValue)
                {
                    throw new InvalidOperationException(SR.Format(SR.Cbor_PopMajorTypeMismatch, (int)_currentMajorType));
                }
                else
                {
                    throw new InvalidOperationException(SR.Cbor_Reader_IsAtRootContext);
                }
            }
 
            Debug.Assert(_nestedDataItems?.Count > 0); // implied by previous check
 
            if (_isTagContext)
            {
                // writer expecting value after a tag data item, cannot pop the current context
                throw new InvalidOperationException(SR.Format(SR.Cbor_PopMajorTypeMismatch, (int)CborMajorType.Tag));
            }
 
            if (_definiteLength - _itemsWritten > 0)
            {
                throw new InvalidOperationException(SR.Cbor_NotAtEndOfDefiniteLengthDataItem);
            }
 
            // Perform encoding fixups that require the current context and must be done before popping
            // NB map key sorting must happen _before_ indefinite-length patching
 
            if (typeToPop == CborMajorType.Map)
            {
                CompleteMapWrite();
            }
 
            if (_definiteLength == null)
            {
                CompleteIndefiniteLengthWrite(typeToPop);
            }
 
            // pop writer state
            StackFrame frame = _nestedDataItems.Pop();
            _currentMajorType = frame.MajorType;
            _frameOffset = frame.FrameOffset;
            _definiteLength = frame.DefiniteLength;
            _itemsWritten = frame.ItemsWritten;
            _currentKeyOffset = frame.CurrentKeyOffset;
            _currentValueOffset = frame.CurrentValueOffset;
            _keysRequireSorting = frame.KeysRequireSorting;
            _keyValuePairEncodingRanges = frame.KeyValuePairEncodingRanges;
            _keyEncodingRanges = frame.KeyEncodingRanges;
        }
 
        // Advance writer state after a data item has been written to the buffer
        private void AdvanceDataItemCounters()
        {
            if (_currentMajorType == CborMajorType.Map)
            {
                if (_itemsWritten % 2 == 0)
                {
                    HandleMapKeyWritten();
                }
                else
                {
                    HandleMapValueWritten();
                }
            }
 
            _itemsWritten++;
            _isTagContext = false;
        }
 
        private void WriteInitialByte(CborInitialByte initialByte)
        {
            if (_definiteLength - _itemsWritten == 0)
            {
                throw new InvalidOperationException(SR.Cbor_Writer_DefiniteLengthExceeded);
            }
 
            switch (_currentMajorType)
            {
                case CborMajorType.ByteString:
                case CborMajorType.TextString:
                    // Indefinite-length string contexts allow two possible data items:
                    // 1) Definite-length string chunks of the same major type OR
                    // 2) a break byte denoting the end of the indefinite-length string context.
                    // NB the second check is not needed here, as we use a separate mechanism to append the break byte
                    if (initialByte.MajorType != _currentMajorType ||
                        initialByte.AdditionalInfo == CborAdditionalInfo.IndefiniteLength)
                    {
                        throw new InvalidOperationException(SR.Cbor_Writer_CannotNestDataItemsInIndefiniteLengthStrings);
                    }
 
                    break;
            }
 
            _buffer[_offset++] = initialByte.InitialByte;
        }
 
        private void CompleteIndefiniteLengthWrite(CborMajorType type)
        {
            Debug.Assert(_definiteLength == null);
 
            if (ConvertIndefiniteLengthEncodings)
            {
                // indefinite-length not allowed, convert the encoding into definite-length
                switch (type)
                {
                    case CborMajorType.ByteString:
                    case CborMajorType.TextString:
                        PatchIndefiniteLengthString(type);
                        break;
                    case CborMajorType.Array:
                        PatchIndefiniteLengthCollection(CborMajorType.Array, _itemsWritten);
                        break;
                    case CborMajorType.Map:
                        Debug.Assert(_itemsWritten % 2 == 0);
                        PatchIndefiniteLengthCollection(CborMajorType.Map, _itemsWritten / 2);
                        break;
                    default:
                        Debug.Fail("Invalid CBOR major type pushed to stack.");
                        throw new Exception();
                }
            }
            else
            {
                // using indefinite-length encoding, append a break byte to the existing encoding
                EnsureWriteCapacity(1);
                _buffer[_offset++] = CborInitialByte.IndefiniteLengthBreakByte;
            }
        }
 
        private readonly struct StackFrame
        {
            public StackFrame(
                CborMajorType? type,
                int frameOffset,
                int? definiteLength,
                int itemsWritten,
                int? currentKeyOffset,
                int? currentValueOffset,
                bool keysRequireSorting,
                List<KeyValuePairEncodingRange>? keyValuePairEncodingRanges,
                HashSet<(int Offset, int Length)>? keyEncodingRanges)
            {
                MajorType = type;
                FrameOffset = frameOffset;
                DefiniteLength = definiteLength;
                ItemsWritten = itemsWritten;
                CurrentKeyOffset = currentKeyOffset;
                CurrentValueOffset = currentValueOffset;
                KeysRequireSorting = keysRequireSorting;
                KeyValuePairEncodingRanges = keyValuePairEncodingRanges;
                KeyEncodingRanges = keyEncodingRanges;
            }
 
            public CborMajorType? MajorType { get; }
            public int FrameOffset { get; }
            public int? DefiniteLength { get; }
            public int ItemsWritten { get; }
 
            public int? CurrentKeyOffset { get; }
            public int? CurrentValueOffset { get; }
            public bool KeysRequireSorting { get; }
            public List<KeyValuePairEncodingRange>? KeyValuePairEncodingRanges { get; }
            public HashSet<(int Offset, int Length)>? KeyEncodingRanges { get; }
        }
    }
}