File: System\Formats\Nrbf\Utils\BinaryReaderExtensions.cs
Web Access
Project: src\src\libraries\System.Formats.Nrbf\src\System.Formats.Nrbf.csproj (System.Formats.Nrbf)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Reflection.Metadata;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Threading;
 
namespace System.Formats.Nrbf.Utils;
 
internal static class BinaryReaderExtensions
{
    private static object? s_baseAmbiguousDstDateTime;
 
    internal static SerializationRecordType ReadSerializationRecordType(this BinaryReader reader, AllowedRecordTypes allowed)
    {
        byte nextByte = reader.ReadByte();
        if (nextByte > (byte)SerializationRecordType.MethodReturn // MethodReturn is the last defined value.
            || (nextByte > (byte)SerializationRecordType.ArraySingleString && nextByte < (byte)SerializationRecordType.MethodCall) // not part of the spec
            || ((uint)allowed & (1u << nextByte)) == 0) // valid, but not allowed
        {
            ThrowHelper.ThrowForUnexpectedRecordType(nextByte);
        }
 
        return (SerializationRecordType)nextByte;
    }
 
    internal static BinaryArrayType ReadArrayType(this BinaryReader reader)
    {
        // To simplify the behavior and security review of the BinaryArrayRecord type, we
        // do not support reading non-zero-offset arrays. If this should change in the
        // future, the NrbfDecoder.DecodeBinaryArrayRecord method and supporting infrastructure
        // will need re-review.
 
        byte arrayType = reader.ReadByte();
        // Rectangular is the last defined value.
        if (arrayType > (byte)BinaryArrayType.Rectangular)
        {
            // Custom offset arrays
            if (arrayType >= 3 && arrayType <= 5)
            {
                throw new NotSupportedException(SR.NotSupported_NonZeroOffsets);
            }
 
            ThrowHelper.ThrowInvalidValue(arrayType);
        }
 
        return (BinaryArrayType)arrayType;
    }
 
    internal static BinaryType ReadBinaryType(this BinaryReader reader)
    {
        byte binaryType = reader.ReadByte();
        // PrimitiveArray is the last defined value.
        if (binaryType > (byte)BinaryType.PrimitiveArray)
        {
            ThrowHelper.ThrowInvalidValue(binaryType);
        }
        return (BinaryType)binaryType;
    }
 
    internal static PrimitiveType ReadPrimitiveType(this BinaryReader reader)
    {
        byte primitiveType = reader.ReadByte();
        // Boolean is the first valid value (1), UInt64 (16) is the last one. 4 is not used at all.
        if (primitiveType is 4 or < (byte)PrimitiveType.Boolean or > (byte)PrimitiveType.UInt64)
        {
            ThrowHelper.ThrowInvalidValue(primitiveType);
        }
        return (PrimitiveType)primitiveType;
    }
 
    /// <summary>
    ///  Reads a primitive of <paramref name="primitiveType"/> from the given <paramref name="reader"/>.
    /// </summary>
    internal static object ReadPrimitiveValue(this BinaryReader reader, PrimitiveType primitiveType)
        => primitiveType switch
        {
            PrimitiveType.Boolean => reader.ReadBoolean(),
            PrimitiveType.Byte => reader.ReadByte(),
            PrimitiveType.SByte => reader.ReadSByte(),
            PrimitiveType.Char => reader.ParseChar(),
            PrimitiveType.Int16 => reader.ReadInt16(),
            PrimitiveType.UInt16 => reader.ReadUInt16(),
            PrimitiveType.Int32 => reader.ReadInt32(),
            PrimitiveType.UInt32 => reader.ReadUInt32(),
            PrimitiveType.Int64 => reader.ReadInt64(),
            PrimitiveType.UInt64 => reader.ReadUInt64(),
            PrimitiveType.Single => reader.ReadSingle(),
            PrimitiveType.Double => reader.ReadDouble(),
            PrimitiveType.Decimal => reader.ParseDecimal(),
            PrimitiveType.DateTime => CreateDateTimeFromData(reader.ReadUInt64()),
            PrimitiveType.TimeSpan => new TimeSpan(reader.ReadInt64()),
            _ => throw new InvalidOperationException(),
        };
 
    // BinaryFormatter serializes decimals as strings and we can't BinaryReader.ReadDecimal.
    internal static decimal ParseDecimal(this BinaryReader reader)
    {
        // The spec (MS NRBF 2.1.1.6, https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrbf/10b218f5-9b2b-4947-b4b7-07725a2c8127)
        // says that the length of LengthPrefixedString must be of optimal size (using as few bytes as possible).
        // BinaryReader.ReadString does not enforce that and we are OK with that,
        // as it takes care of handling multiple edge cases and we don't want to re-implement it.
 
        string text = reader.ReadString();
        if (!decimal.TryParse(text, NumberStyles.Number, CultureInfo.InvariantCulture, out decimal result))
        {
            ThrowHelper.ThrowInvalidFormat();
        }
 
        return result;
    }
 
    internal static char ParseChar(this BinaryReader reader)
    {
        try
        {
            return reader.ReadChar();
        }
        catch (ArgumentException) // A surrogate character was read.
        {
            throw new SerializationException(SR.Serialization_SurrogateCharacter);
        }
    }
 
    internal static char[] ParseChars(this BinaryReader reader, int count)
    {
        char[]? result;
        try
        {
            result = reader.ReadChars(count);
        }
        catch (ArgumentException) // A surrogate character was read.
        {
            throw new SerializationException(SR.Serialization_SurrogateCharacter);
        }
 
        if (result.Length != count)
        {
            // We might hit EOF before fully reading the requested
            // number of chars. This means that ReadChars(count) could return a char[] with
            // *fewer* than 'count' elements.
            ThrowHelper.ThrowEndOfStreamException();
        }
 
        return result;
    }
 
    /// <summary>
    ///  Creates a <see cref="DateTime"/> object from raw data with validation.
    /// </summary>
    /// <exception cref="SerializationException"><paramref name="dateData"/> was invalid.</exception>
    internal static DateTime CreateDateTimeFromData(ulong dateData)
    {
        ulong ticks = dateData & 0x3FFFFFFF_FFFFFFFFUL;
        DateTimeKind kind = (DateTimeKind)(dateData >> 62);
 
        try
        {
            return ((uint)kind <= (uint)DateTimeKind.Local) ? new DateTime((long)ticks, kind) : CreateFromAmbiguousDst(ticks);
        }
        catch (ArgumentException ex)
        {
            throw new SerializationException(ex.Message, ex);
        }
 
        [MethodImpl(MethodImplOptions.NoInlining)]
        static DateTime CreateFromAmbiguousDst(ulong ticks)
        {
            // There's no public API to create a DateTime from an ambiguous DST, and we
            // can't use private reflection to access undocumented .NET Framework APIs.
            // However, the ISerializable pattern *is* a documented protocol, so we can
            // use DateTime's serialization ctor to create a zero-tick "ambiguous" instance,
            // then keep reusing it as the base to which we can add our tick offsets.
 
            if (s_baseAmbiguousDstDateTime is not DateTime baseDateTime)
            {
#pragma warning disable SYSLIB0050 // Type or member is obsolete
                SerializationInfo si = new(typeof(DateTime), new FormatterConverter());
                // We don't know the value of "ticks", so we don't specify it.
                // If the code somehow runs on a very old runtime that does not know the concept of "dateData"
                // (it should not be possible as the library targets .NET Standard 2.0)
                // the ctor is going to throw rather than silently return an invalid value.
                si.AddValue("dateData", 0xC0000000_00000000UL); // new value (serialized as ulong)
 
#if NET
                baseDateTime = CallPrivateSerializationConstructor(si, new StreamingContext(StreamingContextStates.All));
#else
                ConstructorInfo ci = typeof(DateTime).GetConstructor(
                    BindingFlags.Instance | BindingFlags.NonPublic,
                    binder: null,
                    new Type[] { typeof(SerializationInfo), typeof(StreamingContext) },
                    modifiers: null);
 
                baseDateTime = (DateTime)ci.Invoke(new object[] { si, new StreamingContext(StreamingContextStates.All) });
#endif
 
#pragma warning restore SYSLIB0050 // Type or member is obsolete
                Volatile.Write(ref s_baseAmbiguousDstDateTime, baseDateTime); // it's ok if two threads race here
            }
 
            return baseDateTime.AddTicks((long)ticks);
        }
 
#if NET
        [UnsafeAccessor(UnsafeAccessorKind.Constructor)]
        extern static DateTime CallPrivateSerializationConstructor(SerializationInfo si, StreamingContext ct);
#endif
    }
 
    internal static bool? IsDataAvailable(this BinaryReader reader, long requiredBytes)
    {
        Debug.Assert(requiredBytes >= 0);
 
        if (!reader.BaseStream.CanSeek)
        {
            return null;
        }
 
        try
        {
            // If the values are equal, it's still not enough, as every NRBF payload
            // needs to end with EndMessageByte and requiredBytes does not take it into account.
            return (reader.BaseStream.Length - reader.BaseStream.Position) > requiredBytes;
        }
        catch
        {
            // seekable Stream can still throw when accessing Length and Position
            return null;
        }
    }
}