// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
using System.Diagnostics;
namespace System.Formats.Cbor
{
/// <summary>A stateful, forward-only reader for Concise Binary Object Representation (CBOR) encoded data.</summary>
public partial class CborReader
{
private const int DefaultMaxDepth = 64;
private ReadOnlyMemory<byte> _data;
private int _offset;
private bool _isFinalBlock = true; // false iff the caller has declared that more data may follow via SlideData
private Stack<StackFrame>? _nestedDataItems;
private CborMajorType? _currentMajorType; // major type of the currently written data item. Null iff at the root context
private int? _definiteLength; // predetermined definite-length of current data item context
private int _itemsRead; // number of items read in the current context
private int _frameOffset; // buffer offset particular to the current data item context
private bool _isTagContext; // true if reader is expecting a tagged value
// Map-specific book-keeping
private int? _currentKeyOffset; // offset for the current key encoding
private (int Offset, int Length)? _previousKeyEncodingRange; // previous key encoding range
private HashSet<(int Offset, int Length)>? _keyEncodingRanges; // all key encoding ranges up to encoding equality
// flag used to temporarily disable conformance mode checks,
// e.g. during a skip operation over nonconforming encodings.
private bool _isConformanceModeCheckEnabled = true;
// keeps a cached copy of the reader state; 'None' denotes uncomputed state
private CborReaderState _cachedState = CborReaderState.Undefined;
/// <summary>Gets the conformance mode used by this reader.</summary>
/// <value>One of the enumeration values that represents the conformance mode used by this reader.</value>
public CborConformanceMode ConformanceMode { get; }
/// <summary>Gets a value that indicates whether this reader allows multiple root-level CBOR data items.</summary>
/// <value><see langword="true" /> if this reader allows multiple root-level CBOR data items; <see langword="false" /> otherwise.</value>
public bool AllowMultipleRootLevelValues { get; }
/// <summary>Gets the reader'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 maximum depth allowed when reading CBOR data.</summary>
/// <value>The maximum depth allowed when reading CBOR data. The default is 64.</value>
public int MaxDepth { get; }
/// <summary>Gets the total number of unread bytes in the buffer.</summary>
/// <value>The total number of unread bytes in the buffer.</value>
public int BytesRemaining => _data.Length - _offset;
/// <summary>Initializes a <see cref="CborReader" /> instance over the specified <paramref name="data" /> with the given options.</summary>
/// <param name="data">The CBOR-encoded data to read.</param>
/// <param name="options">The options that control reading behavior.</param>
public CborReader(ReadOnlyMemory<byte> data, CborReaderOptions? options)
: this(data, options, isFinalBlock: true)
{
}
/// <summary>Initializes a <see cref="CborReader" /> instance over the specified <paramref name="data" /> with the given options.</summary>
/// <param name="data">The CBOR-encoded data to read.</param>
/// <param name="options">The options that control reading behavior.</param>
/// <param name="isFinalBlock"><see langword="true" /> to indicate that <paramref name="data" /> contains the complete remainder of the document(s) to read;
/// <see langword="false" /> if more data may be supplied using <see cref="SlideData" />.</param>
/// <exception cref="ArgumentException"><paramref name="isFinalBlock" /> is <see langword="false" /> and the conformance mode is not <see cref="CborConformanceMode.Lax" />.</exception>
public CborReader(ReadOnlyMemory<byte> data, CborReaderOptions? options, bool isFinalBlock)
{
CborConformanceMode conformanceMode = CborConformanceMode.Strict;
bool allowMultipleRootLevelValues = false;
int maxDepth = DefaultMaxDepth;
if (options is not null)
{
conformanceMode = options.ConformanceMode;
allowMultipleRootLevelValues = options.AllowMultipleRootLevelValues;
maxDepth = options.MaxDepth;
Debug.Assert(maxDepth >= -1);
}
ValidateIsFinalBlock(isFinalBlock, conformanceMode);
_data = data;
_isFinalBlock = isFinalBlock;
ConformanceMode = conformanceMode;
AllowMultipleRootLevelValues = allowMultipleRootLevelValues;
MaxDepth = maxDepth < 0 ? DefaultMaxDepth : maxDepth;
_definiteLength = allowMultipleRootLevelValues ? null : 1;
}
/// <summary>Initializes a <see cref="CborReader" /> instance over the specified <paramref name="data" /> with the given configuration.</summary>
/// <param name="data">The CBOR-encoded data to read.</param>
/// <param name="conformanceMode">One of the enumeration values to specify a conformance mode guiding the checks performed on the encoded data.
/// Defaults to <see cref="CborConformanceMode.Strict" /> conformance mode.</param>
/// <param name="allowMultipleRootLevelValues"><see langword="true" /> to indicate that multiple root-level values are supported by the reader; otherwise, <see langword="false" />.</param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="conformanceMode" /> is not defined.</exception>
public CborReader(ReadOnlyMemory<byte> data, CborConformanceMode conformanceMode = CborConformanceMode.Strict, bool allowMultipleRootLevelValues = false)
{
CborConformanceModeHelpers.Validate(conformanceMode);
_data = data;
ConformanceMode = conformanceMode;
AllowMultipleRootLevelValues = allowMultipleRootLevelValues;
MaxDepth = DefaultMaxDepth;
_definiteLength = allowMultipleRootLevelValues ? null : 1;
}
/// <summary>Reads the next CBOR data item, returning a <see cref="ReadOnlyMemory{T}" /> view of the encoded value. For indefinite length encodings this includes the break byte.</summary>
/// <param name="disableConformanceModeChecks"><see langword="true" /> to disable conformance mode validation for the read value, equivalent to using <see cref="CborConformanceMode.Lax" />; otherwise, <see langword="false" />.</param>
/// <returns>A view of the encoded value as a contiguous region of memory.</returns>
/// <exception cref="CborContentException"><para>The data item is not a valid CBOR data item encoding.</para>
/// <para>-or-</para>
/// <para>The CBOR encoding is not valid under the current conformance mode.</para></exception>
/// <remarks>The returned <see cref="ReadOnlyMemory{T}" /> is a slice over the reader's input buffer. Callers are responsible
/// for preserving the integrity of the value for as long as it is persisted. When using <c>CborReader</c> in streaming mode,
/// callers are advised to read the value immediately, make a defensive copy, or avoid reusing the same memory
/// across calls to <see cref="SlideData" />.</remarks>
public ReadOnlyMemory<byte> ReadEncodedValue(bool disableConformanceModeChecks = false)
{
// keep a snapshot of the current offset
int initialOffset = _offset;
// call skip to read and validate the next value
SkipValue(disableConformanceModeChecks);
// return the slice corresponding to the consumed value
return _data.Slice(initialOffset, _offset - initialOffset);
}
/// <summary>
/// Resets the <see cref="CborReader"/> instance over the specified <paramref name="data"/> with unchanged configuration.
/// <see cref="ConformanceMode"/> and <see cref="AllowMultipleRootLevelValues"/> are unchanged.
/// </summary>
/// <param name="data">The CBOR-encoded data to read.</param>
/// <remarks><paramref name="data" /> is treated as the final block: subsequent calls to <see cref="SlideData" /> throw.
/// Use <see cref="Reset(ReadOnlyMemory{byte}, bool)" /> to start reading a new document incrementally.</remarks>
public void Reset(ReadOnlyMemory<byte> data)
{
Reset(data, isFinalBlock: true);
}
/// <summary>
/// Resets the <see cref="CborReader"/> instance over the specified <paramref name="data"/> with unchanged configuration.
/// <see cref="ConformanceMode"/> and <see cref="AllowMultipleRootLevelValues"/> are unchanged.
/// </summary>
/// <param name="data">The CBOR-encoded data to read.</param>
/// <param name="isFinalBlock"><see langword="true" /> to indicate that <paramref name="data" /> contains the complete remainder of the document(s) to read;
/// <see langword="false" /> if more data may be supplied using <see cref="SlideData" />.</param>
/// <exception cref="ArgumentException"><paramref name="isFinalBlock" /> is <see langword="false" /> and the conformance mode is not <see cref="CborConformanceMode.Lax" />.</exception>
public void Reset(ReadOnlyMemory<byte> data, bool isFinalBlock)
{
// ConformanceMode and AllowMultipleRootLevelValues are set in ctor, they remain unchanged.
ValidateIsFinalBlock(isFinalBlock, ConformanceMode);
_data = data;
_offset = 0;
_isFinalBlock = isFinalBlock;
_nestedDataItems?.Clear();
_currentMajorType = default;
_definiteLength = AllowMultipleRootLevelValues ? null : 1;
_itemsRead = default;
_frameOffset = default;
_isTagContext = default;
_currentKeyOffset = default;
_previousKeyEncodingRange = default;
_keyEncodingRanges?.Clear();
_isConformanceModeCheckEnabled = true;
_cachedState = CborReaderState.Undefined;
// We don't need to clear the reusable instances in _pooledKeyEncodingRangeAllocations
// or _indefiniteLengthStringRangeAllocation.
}
/// <summary>
/// Replaces the buffer with data that continues from the reader's current position, preserving the nesting context.
/// </summary>
/// <param name="data">The CBOR-encoded data to continue reading from. It must start with the unconsumed bytes
/// of the previous buffer (see <see cref="BytesRemaining" />), followed by any newly available data.</param>
/// <param name="isFinalBlock"><see langword="true" /> to indicate that <paramref name="data" /> contains the complete remainder of the document(s) to read;
/// <see langword="false" /> if more data may be supplied by a subsequent call to this method.</param>
/// <exception cref="InvalidOperationException">The reader's current data was supplied as the final block.</exception>
/// <exception cref="ArgumentException"><paramref name="data" /> is shorter than the unconsumed bytes of the current buffer (see <see cref="BytesRemaining" />).</exception>
/// <remarks>
/// <para>The caller is responsible for preserving all unread bytes, in order, at the beginning of <paramref name="data" />.
/// Only the length of the new buffer is validated, not its contents.</para>
/// <para>NOTE: Some members, such as <see cref="ReadEncodedValue" />, return a <see cref="ReadOnlyMemory{T}" /> as a slice
/// over the input buffer. Callers are responsible for preserving the integrity of those values for as long as they are persisted.
/// When using <c>CborReader</c> in streaming mode, callers are advised to read such values immediately, make defensive copies,
/// or avoid reusing the same memory across calls to <c>SlideData</c>.</para>
/// <para>Calling this method after a complete document has been read does not resume reading; the reader continues to report
/// <see cref="CborReaderState.Finished" />. Use <see cref="Reset(ReadOnlyMemory{byte}, bool)" /> to begin reading a new document.</para>
/// </remarks>
public void SlideData(ReadOnlyMemory<byte> data, bool isFinalBlock)
{
if (_isFinalBlock)
{
throw new InvalidOperationException(SR.Cbor_Reader_CannotSlideDataOnFinalBlock);
}
if (data.Length < BytesRemaining)
{
throw new ArgumentException(SR.Cbor_Reader_SlideDataBufferTooSmall, nameof(data));
}
// Conformance bookkeeping (frame offsets, key encoding ranges) is buffer-relative
// and becomes stale after a slide. This is safe because non-final mode requires
// Lax conformance, which never dereferences it; enforced in the constructor
// and Reset, asserted below. A conformance mode that reads this bookkeeping
// must rebase it here before supporting non-final blocks.
Debug.Assert(ConformanceMode == CborConformanceMode.Lax);
Debug.Assert(_keyEncodingRanges is null);
_data = data;
_offset = 0;
_isFinalBlock = isFinalBlock;
_cachedState = CborReaderState.Undefined;
}
private CborInitialByte PeekInitialByte()
{
if (_definiteLength - _itemsRead == 0)
{
throw new InvalidOperationException(SR.Cbor_Reader_NoMoreDataItemsToRead);
}
if (_offset == _data.Length)
{
if (!_isFinalBlock)
{
// more data may follow; PeekState reports this position as NeedsMoreData
throw new CborContentException(SR.Cbor_Reader_InvalidCbor_UnexpectedEndOfBuffer);
}
// check _itemsRead in addition to _offset since SlideData resets the offset to 0
if (_currentMajorType is null && _definiteLength is null && (_offset > 0 || _itemsRead > 0))
{
// we are at the end of a well-formed sequence of root-level CBOR values
throw new InvalidOperationException(SR.Cbor_Reader_NoMoreDataItemsToRead);
}
throw new CborContentException(SR.Cbor_Reader_InvalidCbor_UnexpectedEndOfBuffer);
}
var nextByte = new CborInitialByte(_data.Span[_offset]);
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.
if (nextByte.InitialByte == CborInitialByte.IndefiniteLengthBreakByte ||
nextByte.MajorType == _currentMajorType.Value &&
nextByte.AdditionalInfo != CborAdditionalInfo.IndefiniteLength)
{
break;
}
throw new CborContentException(SR.Format(SR.Cbor_Reader_InvalidCbor_IndefiniteLengthStringContainsInvalidDataItem, (int)nextByte.MajorType));
}
return nextByte;
}
private CborInitialByte PeekInitialByte(CborMajorType expectedType)
{
CborInitialByte result = PeekInitialByte();
if (expectedType != result.MajorType)
{
throw new InvalidOperationException(SR.Format(SR.Cbor_Reader_MajorTypeMismatch, (int)result.MajorType));
}
return result;
}
private void ValidateNextByteIsBreakByte()
{
CborInitialByte result = PeekInitialByte();
if (result.InitialByte != CborInitialByte.IndefiniteLengthBreakByte)
{
throw new InvalidOperationException(SR.Cbor_NotAtEndOfIndefiniteLengthDataItem);
}
}
private void EnsureMaxDepthNotExceeded()
{
if (CurrentDepth >= MaxDepth)
{
throw new CborContentException(SR.Format(SR.Cbor_Reader_MaximumDepthExceeded, MaxDepth));
}
}
private void PushDataItem(CborMajorType majorType, int? definiteLength)
{
Debug.Assert(CurrentDepth < MaxDepth);
_nestedDataItems ??= new Stack<StackFrame>();
var frame = new StackFrame(
type: _currentMajorType,
frameOffset: _frameOffset,
definiteLength: _definiteLength,
itemsRead: _itemsRead,
currentKeyOffset: _currentKeyOffset,
previousKeyEncodingRange: _previousKeyEncodingRange,
keyEncodingRanges: _keyEncodingRanges
);
_nestedDataItems.Push(frame);
_currentMajorType = majorType;
_definiteLength = definiteLength;
_itemsRead = 0;
_frameOffset = _offset;
_isTagContext = false;
_currentKeyOffset = null;
_previousKeyEncodingRange = null;
_keyEncodingRanges = null;
}
private void PopDataItem(CborMajorType expectedType)
{
if (_currentMajorType is null)
{
throw new InvalidOperationException(SR.Cbor_Reader_IsAtRootContext);
}
Debug.Assert(_nestedDataItems?.Count > 0);
if (expectedType != _currentMajorType)
{
throw new InvalidOperationException(SR.Format(SR.Cbor_PopMajorTypeMismatch, (int)_currentMajorType.Value));
}
if (_definiteLength - _itemsRead > 0)
{
throw new InvalidOperationException(SR.Cbor_NotAtEndOfDefiniteLengthDataItem);
}
if (_isTagContext)
{
throw new CborContentException(SR.Cbor_Reader_InvalidCbor_TagNotFollowedByValue);
}
if (_currentMajorType == CborMajorType.Map)
{
ReturnKeyEncodingRangeAllocation(_keyEncodingRanges);
}
StackFrame frame = _nestedDataItems.Pop();
RestoreStackFrame(in frame);
}
private void AdvanceDataItemCounters()
{
Debug.Assert(_definiteLength is null || _definiteLength - _itemsRead > 0);
if (_currentMajorType == CborMajorType.Map)
{
if (_itemsRead % 2 == 0)
{
HandleMapKeyRead();
}
else
{
HandleMapValueRead();
}
}
_itemsRead++;
_isTagContext = false;
}
private ReadOnlySpan<byte> GetRemainingBytes() => _data.Span.Slice(_offset);
private void AdvanceBuffer(int length)
{
Debug.Assert(_offset + length <= _data.Length);
_offset += length;
// invalidate the state cache
_cachedState = CborReaderState.Undefined;
}
private void ResetBuffer(int position)
{
Debug.Assert(position <= _data.Length);
_offset = position;
// invalidate the state cache
_cachedState = CborReaderState.Undefined;
}
private void EnsureReadCapacity(int length)
{
if (_data.Length - _offset < length)
{
throw new CborContentException(SR.Cbor_Reader_InvalidCbor_UnexpectedEndOfBuffer);
}
}
private static void EnsureReadCapacity(ReadOnlySpan<byte> buffer, int requiredLength)
{
if (buffer.Length < requiredLength)
{
throw new CborContentException(SR.Cbor_Reader_InvalidCbor_UnexpectedEndOfBuffer);
}
}
private static void ValidateIsFinalBlock(bool isFinalBlock, CborConformanceMode conformanceMode)
{
// Non-Lax modes currently track map key encodings as offsets into the buffer, which
// become stale when SlideData discards consumed bytes. Supporting them requires reader-owned
// key copies; so we are currently restricting incremental reading to Lax.
if (!isFinalBlock && conformanceMode != CborConformanceMode.Lax)
{
throw new ArgumentException(SR.Cbor_Reader_NotFinalBlockRequiresLaxConformance, nameof(isFinalBlock));
}
}
private readonly struct StackFrame
{
public StackFrame(
CborMajorType? type,
int frameOffset,
int? definiteLength,
int itemsRead,
int? currentKeyOffset,
(int Offset, int Length)? previousKeyEncodingRange,
HashSet<(int Offset, int Length)>? keyEncodingRanges)
{
MajorType = type;
FrameOffset = frameOffset;
DefiniteLength = definiteLength;
ItemsRead = itemsRead;
CurrentKeyOffset = currentKeyOffset;
PreviousKeyEncodingRange = previousKeyEncodingRange;
KeyEncodingRanges = keyEncodingRanges;
}
public CborMajorType? MajorType { get; }
public int FrameOffset { get; }
public int? DefiniteLength { get; }
public int ItemsRead { get; }
public int? CurrentKeyOffset { get; }
public (int Offset, int Length)? PreviousKeyEncodingRange { get; }
public HashSet<(int Offset, int Length)>? KeyEncodingRanges { get; }
}
private void RestoreStackFrame(in StackFrame frame)
{
_currentMajorType = frame.MajorType;
_frameOffset = frame.FrameOffset;
_definiteLength = frame.DefiniteLength;
_itemsRead = frame.ItemsRead;
_currentKeyOffset = frame.CurrentKeyOffset;
_previousKeyEncodingRange = frame.PreviousKeyEncodingRange;
_keyEncodingRanges = frame.KeyEncodingRanges;
// Popping items from the stack can change the reader state
// without necessarily needing to advance the buffer
// (e.g. we're at the end of a definite-length collection).
// We therefore need to invalidate the cache here.
_cachedState = CborReaderState.Undefined;
}
// Struct containing checkpoint data for rolling back reader state in the event of a failure
// NB checkpoints do not contain stack information, so we can only roll back provided that the
// reader is within the original context in which the checkpoint was created
private readonly struct Checkpoint
{
public Checkpoint(
int depth,
int offset,
int frameOffset,
int itemsRead,
bool isTagContext,
int? currentKeyOffset,
(int Offset, int Length)? previousKeyEncodingRange)
{
Depth = depth;
Offset = offset;
FrameOffset = frameOffset;
ItemsRead = itemsRead;
IsTagContext = isTagContext;
CurrentKeyOffset = currentKeyOffset;
PreviousKeyEncodingRange = previousKeyEncodingRange;
}
public int Depth { get; }
public int Offset { get; }
public int FrameOffset { get; }
public int ItemsRead { get; }
public bool IsTagContext { get; }
public int? CurrentKeyOffset { get; }
public (int Offset, int Length)? PreviousKeyEncodingRange { get; }
}
private Checkpoint CreateCheckpoint()
{
return new Checkpoint(
depth: CurrentDepth,
offset: _offset,
frameOffset: _frameOffset,
itemsRead: _itemsRead,
isTagContext: _isTagContext,
currentKeyOffset: _currentKeyOffset,
previousKeyEncodingRange: _previousKeyEncodingRange);
}
private void RestoreCheckpoint(in Checkpoint checkpoint)
{
int restoreHeight = CurrentDepth - checkpoint.Depth;
Debug.Assert(restoreHeight >= 0, "Attempting to restore checkpoint outside of its original context.");
if (restoreHeight > 0)
{
// pop any nested contexts added after the checkpoint
Debug.Assert(_nestedDataItems != null);
Debug.Assert(_nestedDataItems.ToArray()[restoreHeight - 1].FrameOffset == checkpoint.FrameOffset,
"Attempting to restore checkpoint outside of its original context.");
StackFrame frame;
for (int i = 0; i < restoreHeight - 1; i++)
{
frame = _nestedDataItems.Pop();
ReturnKeyEncodingRangeAllocation(frame.KeyEncodingRanges);
}
frame = _nestedDataItems.Pop();
RestoreStackFrame(in frame);
}
else
{
Debug.Assert(checkpoint.FrameOffset == _frameOffset, "Attempting to restore checkpoint outside of its original context.");
}
// Remove any key encodings added after the current checkpoint.
// This is only needed when rolling back key reads in the Strict conformance mode.
if (_keyEncodingRanges != null && _itemsRead > checkpoint.ItemsRead)
{
int checkpointOffset = checkpoint.Offset;
_keyEncodingRanges.RemoveWhere(key => key.Offset >= checkpointOffset);
}
_offset = checkpoint.Offset;
_itemsRead = checkpoint.ItemsRead;
_isTagContext = checkpoint.IsTagContext;
_previousKeyEncodingRange = checkpoint.PreviousKeyEncodingRange;
_currentKeyOffset = checkpoint.CurrentKeyOffset;
_cachedState = CborReaderState.Undefined;
Debug.Assert(CurrentDepth == checkpoint.Depth);
}
}
}