File: System\Formats\Cbor\Reader\CborReader.PeekState.cs
Web Access
Project: 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.Binary;
using System.Diagnostics;

namespace System.Formats.Cbor
{
    public partial class CborReader
    {
        /// <summary>Reads the next CBOR token, without advancing the reader.</summary>
        /// <returns>An object that represents the current CBOR reader state.</returns>
        /// <exception cref="CborContentException">The underlying data is not a well-formed CBOR encoding.</exception>
        /// <remarks>
        /// <para>If the reader's current data is not the final block, <see cref="CborReaderState.NeedsMoreData" /> is returned
        /// when the next token is incomplete in the current buffer, or when <see cref="AllowMultipleRootLevelValues" /> is enabled
        /// and the buffer ends after a complete root-level value, instead of throwing <see cref="CborContentException" />.</para>
        /// <para>Any other returned token state guarantees that the method reading that single token will not fail
        /// due to an unexpected end of the data, unless the token declares a definite length that can never fit
        /// a single buffer, in which case the reading method throws <see cref="CborContentException" />.</para>
        /// <para>Methods consuming multiple tokens at once, such as <see cref="ReadByteString" /> on indefinite-length strings
        /// or <see cref="ReadEncodedValue" />, can still throw <see cref="CborContentException" /> on truncated data.</para>
        /// </remarks>
        public CborReaderState PeekState()
        {
            if (_cachedState == CborReaderState.Undefined)
            {
                _cachedState = PeekStateCore();
            }

            return _cachedState;
        }

        private CborReaderState PeekStateCore()
        {
            if (_definiteLength - _itemsRead == 0)
            {
                // is at the end of a definite-length context
                switch (_currentMajorType)
                {
                    case null:
                        // finished reading root-level document
                        Debug.Assert(!AllowMultipleRootLevelValues);
                        return CborReaderState.Finished;

                    case CborMajorType.Array: return CborReaderState.EndArray;
                    case CborMajorType.Map: return CborReaderState.EndMap;
                    default:
                        Debug.Fail("CborReader internal error. Invalid CBOR major type pushed to stack.");
                        throw new Exception();
                }
            }

            if (_offset == _data.Length)
            {
                // is at the end of the read buffer
                if (!_isFinalBlock)
                {
                    // more data may follow, even at the end of a sequence of root-level values
                    return CborReaderState.NeedsMoreData;
                }

                if (_currentMajorType is null && _definiteLength is null)
                {
                    // is at the end of a well-defined sequence of root-level values
                    return CborReaderState.Finished;
                }
                else
                {
                    // incomplete CBOR document(s)
                    throw new CborContentException(SR.Cbor_Reader_InvalidCbor_UnexpectedEndOfBuffer);
                }
            }

            // peek the next initial byte
            var initialByte = new CborInitialByte(_data.Span[_offset]);

            if (initialByte.InitialByte == CborInitialByte.IndefiniteLengthBreakByte)
            {
                if (_isTagContext)
                {
                    throw new CborContentException(SR.Cbor_Reader_InvalidCbor_TagNotFollowedByValue);
                }

                if (_definiteLength is null)
                {
                    switch (_currentMajorType)
                    {
                        case null:
                            // found a break byte at the end of a root-level data item sequence
                            Debug.Assert(AllowMultipleRootLevelValues);
                            throw new CborContentException(SR.Cbor_Reader_InvalidCbor_UnexpectedBreakByte);

                        case CborMajorType.ByteString: return CborReaderState.EndIndefiniteLengthByteString;
                        case CborMajorType.TextString: return CborReaderState.EndIndefiniteLengthTextString;
                        case CborMajorType.Array: return CborReaderState.EndArray;
                        case CborMajorType.Map when _itemsRead % 2 == 0: return CborReaderState.EndMap;
                        case CborMajorType.Map:
                            throw new CborContentException(SR.Cbor_Reader_InvalidCbor_KeyMissingValue);
                        default:
                            Debug.Fail("CborReader internal error. Invalid CBOR major type pushed to stack.");
                            throw new Exception();
                    };
                }
                else
                {
                    throw new CborContentException(SR.Cbor_Reader_InvalidCbor_UnexpectedBreakByte);
                }
            }

            if (_definiteLength is null && _currentMajorType != null)
            {
                // is at indefinite-length nested data item
                switch (_currentMajorType.Value)
                {
                    case CborMajorType.ByteString:
                    case CborMajorType.TextString:
                        if (initialByte.MajorType != _currentMajorType.Value)
                        {
                            throw new CborContentException(SR.Cbor_Reader_InvalidCbor_IndefiniteLengthStringContainsInvalidDataItem);
                        }

                        break;
                }
            }

            if (!_isFinalBlock && !IsNextTokenFullyAvailable(GetRemainingBytes()))
            {
                // the next token is truncated, but more data may follow
                return CborReaderState.NeedsMoreData;
            }

            switch (initialByte.MajorType)
            {
                case CborMajorType.UnsignedInteger: return CborReaderState.UnsignedInteger;
                case CborMajorType.NegativeInteger: return CborReaderState.NegativeInteger;
                case CborMajorType.ByteString:
                    return (initialByte.AdditionalInfo == CborAdditionalInfo.IndefiniteLength) ?
                            CborReaderState.StartIndefiniteLengthByteString :
                            CborReaderState.ByteString;

                case CborMajorType.TextString:
                    return (initialByte.AdditionalInfo == CborAdditionalInfo.IndefiniteLength) ?
                            CborReaderState.StartIndefiniteLengthTextString :
                            CborReaderState.TextString;

                case CborMajorType.Array: return CborReaderState.StartArray;
                case CborMajorType.Map: return CborReaderState.StartMap;
                case CborMajorType.Tag: return CborReaderState.Tag;
                case CborMajorType.Simple: return MapSimpleValueDataToReaderState(initialByte.AdditionalInfo);
                default:
                    Debug.Fail("CborReader internal error. Invalid CBOR major type.");
                    throw new Exception();
            };

            static CborReaderState MapSimpleValueDataToReaderState(CborAdditionalInfo value)
            {
                // https://tools.ietf.org/html/rfc7049#section-2.3

                switch (value)
                {
                    case (CborAdditionalInfo)CborSimpleValue.Null:
                        return CborReaderState.Null;
                    case (CborAdditionalInfo)CborSimpleValue.True:
                    case (CborAdditionalInfo)CborSimpleValue.False:
                        return CborReaderState.Boolean;
                    case CborAdditionalInfo.Additional16BitData:
                        return CborReaderState.HalfPrecisionFloat;
                    case CborAdditionalInfo.Additional32BitData:
                        return CborReaderState.SinglePrecisionFloat;
                    case CborAdditionalInfo.Additional64BitData:
                        return CborReaderState.DoublePrecisionFloat;
                    default:
                        return CborReaderState.SimpleValue;
                }
            }
        }

        // Checks that the token starting at remaining[0] is fully available in the buffer:
        // its header, the header's argument, and, for definite-length strings, their contents.
        // Checks availability only; malformed encodings are reported as available,
        // deferring to the corresponding Read method to throw.
        private static bool IsNextTokenFullyAvailable(ReadOnlySpan<byte> remaining)
        {
            Debug.Assert(!remaining.IsEmpty);

            var initialByte = new CborInitialByte(remaining[0]);
            int argumentWidth;

            switch (initialByte.AdditionalInfo)
            {
                case CborAdditionalInfo x when (x < CborAdditionalInfo.Additional8BitData):
                    // Don't just return true, because byte strings and text
                    // strings have additional validation.
                    argumentWidth = 0;
                    break;
                case CborAdditionalInfo.Additional8BitData:
                    argumentWidth = sizeof(byte);
                    break;
                case CborAdditionalInfo.Additional16BitData:
                    argumentWidth = sizeof(ushort);
                    break;
                case CborAdditionalInfo.Additional32BitData:
                    argumentWidth = sizeof(uint);
                    break;
                case CborAdditionalInfo.Additional64BitData:
                    argumentWidth = sizeof(ulong);
                    break;
                default:
                    // Indefinite-length tokens are the initial byte only; the reserved values 28-30
                    // are malformed regardless of any additional data.
                    return true;
            }

            int requiredBytes = 1 + argumentWidth;

            if (remaining.Length < requiredBytes)
            {
                return false;
            }

            switch (initialByte.MajorType)
            {
                case CborMajorType.ByteString:
                case CborMajorType.TextString:
                    // definite-length string tokens include their contents
                    ulong length = argumentWidth switch
                    {
                        0 => (ulong)initialByte.AdditionalInfo,
                        sizeof(byte) => remaining[1],
                        sizeof(ushort) => BinaryPrimitives.ReadUInt16BigEndian(remaining.Slice(1)),
                        sizeof(uint) => BinaryPrimitives.ReadUInt32BigEndian(remaining.Slice(1)),
                        _ => BinaryPrimitives.ReadUInt64BigEndian(remaining.Slice(1)),
                    };

                    if (length > (ulong)(int.MaxValue - requiredBytes))
                    {
                        // can never fit a single buffer, so this is not a truncation condition
                        return true;
                    }

                    return (ulong)(remaining.Length - requiredBytes) >= length;

                default:
                    // For all other tokens the argument concludes the token: integers and float or
                    // simple-value payloads are encoded in the argument itself, and the contents of
                    // tags, arrays and maps are separate tokens.
                    return true;
            }
        }
    }
}