File: System\Formats\Nrbf\SerializationRecord.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.Reflection.Metadata;
using System.Collections.Immutable;
 
namespace System.Formats.Nrbf;
 
/// <summary>
/// Abstract class that represents the serialization record.
/// </summary>
/// <remarks>
///  <para>
///   Every instance returned to the end user can be either <see cref="PrimitiveTypeRecord{T}"/>,
///   a <see cref="ClassRecord"/>, or an <see cref="ArrayRecord"/>.
///  </para>
/// </remarks>
[DebuggerDisplay("{RecordType}, {Id}")]
public abstract class SerializationRecord
{
    internal SerializationRecord() // others can't derive from this type
    {
    }
 
    /// <summary>
    /// Gets the type of the record.
    /// </summary>
    /// <value>The type of the record.</value>
    public abstract SerializationRecordType RecordType { get; }
 
    /// <summary>
    /// Gets the ID of the record.
    /// </summary>
    /// <value>The ID of the record.</value>
    public abstract SerializationRecordId Id { get; }
 
    /// <summary>
    /// Gets the name of the serialized type.
    /// </summary>
    /// <value>The name of the serialized type.</value>
    public abstract TypeName TypeName { get; }
 
    /// <summary>
    /// Compares the type name read from the payload against the specified type.
    /// </summary>
    /// <remarks>
    /// <para>This method ignores assembly names.</para>
    /// <para>This method does NOT take into account member names or their types.</para>
    /// </remarks>
    /// <param name="type">The type to compare against.</param>
    /// <returns><see langword="true" /> if the serialized type name matches the provided type; otherwise, <see langword="false" />.</returns>
    /// <exception cref="ArgumentNullException"><paramref name="type" /> is <see langword="null" />.</exception>
    public bool TypeNameMatches(Type type)
    {
#if NET
        ArgumentNullException.ThrowIfNull(type);
#else
        if (type is null)
        {
            throw new ArgumentNullException(nameof(type));
        }
#endif
 
        return Matches(type, TypeName);
    }
 
    private static bool Matches(Type type, TypeName typeName)
    {
        // We don't need to check for pointers and references to arrays,
        // as it's impossible to serialize them with BF.
        if (type.IsPointer || type.IsByRef)
        {
            return false;
        }
 
        // The TypeName.FullName property getter is recursive and backed by potentially hostile
        // input. See comments in that property getter for more information, including what defenses
        // are in place to prevent attacks.
        //
        // Note that the equality comparison below is worst-case O(n) since the adversary could ensure
        // that only the last char differs. Even if the strings have equal contents, we should still
        // expect the comparison to take O(n) time since RuntimeType.FullName and TypeName.FullName
        // will never reference the same string instance with current runtime implementations.
        //
        // Since a call to Matches could take place within a loop, and since TypeName.FullName could
        // be arbitrarily long (it's attacker-controlled and the NRBF protocol allows backtracking via
        // the ClassWithId record, providing a form of compression), this presents opportunity
        // for an algorithmic complexity attack, where a (2 * l)-length payload has an l-length type
        // name and an array with l elements, resulting in O(l^2) total work factor. Protection against
        // such attack is provided by the fact that the System.Type object is fully under the app's
        // control and is assumed to be trusted and a reasonable length. This brings the cumulative loop
        // work factor back down to O(l * RuntimeType.FullName), which is acceptable.
        //
        // The above statement assumes that "(string)m == (string)n" has worst-case complexity
        // O(min(m.Length, n.Length)). This is not stated in string's public docs, but it is
        // a guaranteed behavior for all built-in Ordinal string comparisons.
 
        // At first, check the non-allocating properties for mismatch.
        if (type.IsArray != typeName.IsArray || type.IsConstructedGenericType != typeName.IsConstructedGenericType
            || type.IsNested != typeName.IsNested
            || (type.IsArray && type.GetArrayRank() != typeName.GetArrayRank())
#if NET
            || type.IsSZArray != typeName.IsSZArray // int[] vs int[*]
#else
            || (type.IsArray && type.Name != typeName.Name)
#endif
            )
        {
            return false;
        }
 
        if (type.FullName == typeName.FullName)
        {
            return true; // The happy path with no type forwarding
        }
        else if (typeName.IsArray)
        {
            return Matches(type.GetElementType()!, typeName.GetElementType());
        }
        else if (type.IsConstructedGenericType)
        {
            if (!Matches(type.GetGenericTypeDefinition(), typeName.GetGenericTypeDefinition()))
            {
                return false;
            }
 
            ImmutableArray<TypeName> genericNames = typeName.GetGenericArguments();
            Type[] genericTypes = type.GetGenericArguments();
 
            if (genericNames.Length != genericTypes.Length)
            {
                return false;
            }
 
            for (int i = 0; i < genericTypes.Length; i++)
            {
                if (!Matches(genericTypes[i], genericNames[i]))
                {
                    return false;
                }
            }
 
            return true;
        }
 
        return false;
    }
 
    /// <summary>
    /// Gets the primitive, string, or null record value.
    /// For reference records, it returns the referenced record.
    /// For other records, it returns the records themselves.
    /// </summary>
    /// <remarks>
    /// Overrides of this method should take care not to allow
    /// the introduction of cycles, even in the face of adversarial
    /// edges in the object graph.
    /// </remarks>
    internal virtual object? GetValue() => this;
 
    internal virtual void HandleNextRecord(SerializationRecord nextRecord, NextInfo info)
        => throw new InvalidOperationException();
 
    internal virtual void HandleNextValue(object value, NextInfo info)
        => throw new InvalidOperationException();
}