File: System\Runtime\Serialization\BinaryFormat\BinaryArrayRecord.cs
Web Access
Project: src\src\libraries\System.Runtime.Serialization.BinaryFormat\src\System.Runtime.Serialization.BinaryFormat.csproj (System.Runtime.Serialization.BinaryFormat)
// 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.CodeAnalysis;
using System.IO;
using System.Reflection.Metadata;
using System.Runtime.Serialization.BinaryFormat.Utils;
 
namespace System.Runtime.Serialization.BinaryFormat;
 
/// <summary>
/// Represents an array other than single dimensional array of primitive types or <see cref="object"/>.
/// </summary>
/// <remarks>
/// BinaryArray records are described in <see href="https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrbf/9c62c928-db4e-43ca-aeba-146256ef67c2">[MS-NRBF] 2.4.3.1</see>.
/// </remarks>
internal sealed class BinaryArrayRecord : ArrayRecord
{
    private static HashSet<Type> PrimitiveTypes { get; } =
    [
        typeof(bool), typeof(char), typeof(byte), typeof(sbyte),
        typeof(short), typeof(ushort), typeof(int), typeof(uint),
        typeof(long), typeof(ulong), typeof(IntPtr), typeof(UIntPtr),
        typeof(float), typeof(double), typeof(decimal), typeof(DateTime),
        typeof(TimeSpan), typeof(string), typeof(object)
    ];
 
    private TypeName? _elementTypeName;
 
    private BinaryArrayRecord(ArrayInfo arrayInfo, MemberTypeInfo memberTypeInfo)
        : base(arrayInfo)
    {
        MemberTypeInfo = memberTypeInfo;
        Values = [];
    }
 
    public override RecordType RecordType => RecordType.BinaryArray;
 
    /// <inheritdoc/>
    public override ReadOnlySpan<int> Lengths => new int[1] { Length };
 
    public override TypeName ElementTypeName
        => _elementTypeName ??= MemberTypeInfo.GetElementTypeName();
 
    private int Length => ArrayInfo.GetSZArrayLength();
 
    private MemberTypeInfo MemberTypeInfo { get; }
 
    private List<object> Values { get; }
 
    [RequiresDynamicCode("May call Array.CreateInstance() and Type.MakeArrayType().")]
    private protected override Array Deserialize(Type arrayType, bool allowNulls)
    {
        // We can not deserialize non-primitive types.
        // This method returns arrays of ClassRecord for arrays of complex types.
        Type elementType = MapElementType(arrayType, out bool isClassRecord);
        Type actualElementType = arrayType.GetElementType()!;
        Array array =
#if NET9_0_OR_GREATER
            isClassRecord
                ? Array.CreateInstance(elementType, Length)
                : Array.CreateInstanceFromArrayType(arrayType, Length);
#else
            Array.CreateInstance(elementType, Length);
#endif
 
        int resultIndex = 0;
        foreach (object value in Values)
        {
            object item = value is MemberReferenceRecord referenceRecord
                ? referenceRecord.GetReferencedRecord()
                : value;
 
            if (item is not SerializationRecord record)
            {
                array.SetValue(item, resultIndex++);
                continue;
            }
 
            switch (record.RecordType)
            {
                case RecordType.BinaryArray:
                case RecordType.ArraySinglePrimitive:
                case RecordType.ArraySingleObject:
                case RecordType.ArraySingleString:
                    ArrayRecord nestedArrayRecord = (ArrayRecord)record;
                    Array nestedArray = nestedArrayRecord.GetArray(actualElementType, allowNulls);
                    array.SetValue(nestedArray, resultIndex++);
                    break;
                case RecordType.ObjectNull:
                case RecordType.ObjectNullMultiple256:
                case RecordType.ObjectNullMultiple:
                    if (!allowNulls)
                    {
                        ThrowHelper.ThrowArrayContainedNulls();
                    }
 
                    int nullCount = ((NullsRecord)item).NullCount;
                    do
                    {
                        array.SetValue(null, resultIndex++);
                        nullCount--;
                    }
                    while (nullCount > 0);
                    break;
                default:
                    array.SetValue(record.GetValue(), resultIndex++);
                    break;
            }
        }
 
        return array;
    }
 
    internal static ArrayRecord Decode(BinaryReader reader, RecordMap recordMap, PayloadOptions options)
    {
        int objectId = reader.ReadInt32();
        BinaryArrayType arrayType = reader.ReadArrayType();
        int rank = reader.ReadInt32();
 
        bool isRectangular = arrayType is BinaryArrayType.Rectangular or BinaryArrayType.RectangularOffset;
 
        // It is an arbitrary limit in the current CoreCLR type loader.
        const int MaxSupportedArrayRank = 32;
 
        if (rank < 1 || rank > MaxSupportedArrayRank
            || (rank != 1 && !isRectangular)
            || (rank == 1 && isRectangular))
        {
            ThrowHelper.ThrowInvalidValue(rank);
        }
 
        int[] lengths = new int[rank]; // adversary-controlled, but acceptable since upper limit of 32
        long totalElementCount = 1;
        for (int i = 0; i < lengths.Length; i++)
        {
            lengths[i] = ArrayInfo.ParseValidArrayLength(reader);
            totalElementCount *= lengths[i];
 
            if (totalElementCount > uint.MaxValue)
            {
                ThrowHelper.ThrowInvalidValue(lengths[i]); // max array size exceeded
            }
        }
 
        int[] offsets = new int[rank]; // zero-init; adversary-controlled, but acceptable since upper limit of 32
        bool hasCustomOffset = false;
        if (arrayType is BinaryArrayType.SingleOffset or BinaryArrayType.JaggedOffset or BinaryArrayType.RectangularOffset)
        {
            for (int i = 0; i < offsets.Length; i++)
            {
                int offset = reader.ReadInt32();
 
                if (offset < 0)
                {
                    ThrowHelper.ThrowInvalidValue(offset);
                }
                else if (offset > 0)
                {
                    hasCustomOffset = true;
 
                    long maxIndex = lengths[i] + offset;
                    if (maxIndex > int.MaxValue)
                    {
                        ThrowHelper.ThrowInvalidValue(maxIndex);
                    }
                }
 
                offsets[i] = offset;
            }
        }
 
        MemberTypeInfo memberTypeInfo = MemberTypeInfo.Decode(reader, 1, options, recordMap);
        ArrayInfo arrayInfo = new(objectId, totalElementCount, arrayType, rank);
 
        if (isRectangular || hasCustomOffset)
        {
            return RectangularOrCustomOffsetArrayRecord.Create(reader, arrayInfo, memberTypeInfo, lengths, offsets);
        }
 
        return memberTypeInfo.ShouldBeRepresentedAsArrayOfClassRecords()
            ? new ArrayOfClassesRecord(arrayInfo, memberTypeInfo)
            : new BinaryArrayRecord(arrayInfo, memberTypeInfo);
    }
 
    private protected override void AddValue(object value) => Values.Add(value);
 
    internal override (AllowedRecordTypes allowed, PrimitiveType primitiveType) GetAllowedRecordType()
    {
        (AllowedRecordTypes allowed, PrimitiveType primitiveType) = MemberTypeInfo.GetNextAllowedRecordType(0);
 
        if (allowed != AllowedRecordTypes.None)
        {
            // It's an array, it can also contain multiple nulls
            return (allowed | AllowedRecordTypes.Nulls, primitiveType);
        }
 
        return (allowed, primitiveType);
    }
 
    internal override bool IsElementType(Type typeElement)
        => MemberTypeInfo.IsElementType(typeElement);
 
    /// <summary>
    /// Complex types must not be instantiated, but represented as ClassRecord.
    /// For arrays of primitive types like int, string and object this method returns the element type.
    /// For array of complex types, it returns ClassRecord.
    /// It takes arrays of arrays into account:
    /// - int[][] => int[]
    /// - MyClass[][][] => ClassRecord[][]
    /// </summary>
    [RequiresDynamicCode("May call Type.MakeArrayType().")]
    private static Type MapElementType(Type arrayType, out bool isClassRecord)
    {
        Type elementType = arrayType;
        int arrayNestingDepth = 0;
 
        while (elementType.IsArray)
        {
            elementType = elementType.GetElementType()!;
            arrayNestingDepth++;
        }
 
        if (PrimitiveTypes.Contains(elementType) || (Nullable.GetUnderlyingType(elementType) is Type nullable && PrimitiveTypes.Contains(nullable)))
        {
            isClassRecord = false;
            return arrayNestingDepth == 1 ? elementType : arrayType.GetElementType()!;
        }
 
        // Complex types are never instantiated, but represented as ClassRecord
        isClassRecord = true;
        Type complexType = typeof(ClassRecord);
        for (int i = 1; i < arrayNestingDepth; i++)
        {
            complexType = complexType.MakeArrayType();
        }
 
        return complexType;
    }
}