File: System\Private\Windows\Core\BinaryFormat\Deserializer\Deserializer.cs
Web Access
Project: src\src\System.Private.Windows.Core\src\System.Private.Windows.Core.csproj (System.Private.Windows.Core)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Formats.Nrbf;
using System.Private.Windows.Core.Resources;
 
namespace System.Private.Windows.Core.BinaryFormat;
 
#pragma warning disable SYSLIB0050 // Type or member is obsolete
 
/// <summary>
///  General binary format deserializer.
/// </summary>
/// <remarks>
///  <para>
///   This has some constraints over the BinaryFormatter. Notably it does not support all <see cref="IObjectReference"/>
///   usages or surrogates that replace object instances. This greatly simplifies the deserialization. It also does not
///   allow offset arrays (arrays that have lower bounds other than zero) or multidimensional arrays that have more
///   than <see cref="int.MaxValue"/> elements.
///  </para>
///  <para>
///   This deserializer ensures that all value types are assigned to fields or populated in <see cref="SerializationInfo"/>
///   callbacks with their final state, throwing if that is impossible to attain due to graph cycles or data corruption.
///   The value type instance may contain references to uncompleted reference types when there are cycles in the graph.
///   In general it is risky to dereference reference types in <see cref="ISerializable"/> constructors or in
///   <see cref="ISerializationSurrogate"/> call backs if there is any risk of the objects enabling a cycle.
///  </para>
///  <para>
///   If you need to dereference reference types in <see cref="SerializationInfo"/> waiting for final state by
///   implementing <see cref="IDeserializationCallback"/> or <see cref="OnDeserializedAttribute"/> is the safer way to
///   do so. This deserializer does not fire completed events until the entire graph has been deserialized. If a
///   surrogate (<see cref="ISerializationSurrogate"/>) needs to dereference with potential cycles it would require
///   tracking instances by stashing them in a provided <see cref="StreamingContext"/> to handle after invoking the
///   deserializer.
///  </para>
/// </remarks>
/// <devdoc>
///  <see cref="IObjectReference"/> makes deserializing difficult as you don't know the final type until you've finished
///  populating the serialized type. If <see cref="SerializationInfo"/> is involved and you have a cycle you may never
///  be able to complete the deserialization as the reference type values in the <see cref="SerializationInfo"/> can't
///  get the final object.
///
///  <see cref="IObjectReference"/> is really the only practical way to represent singletons. A common pattern is to
///  nest an <see cref="IObjectReference"/> object in an <see cref="ISerializable"/> object. Specifying the nested
///  type when <see cref="ISerializable.GetObjectData(SerializationInfo, StreamingContext)"/> is called by invoking
///  <see cref="SerializationInfo.SetType(Type)"/> will get that type info serialized into the stream.
/// </devdoc>
internal sealed partial class Deserializer : IDeserializer
{
    private readonly IReadOnlyDictionary<SerializationRecordId, SerializationRecord> _recordMap;
    private readonly ITypeResolver _typeResolver;
    ITypeResolver IDeserializer.TypeResolver => _typeResolver;
 
    /// <inheritdoc cref="IDeserializer.Options"/>
    private DeserializationOptions Options { get; }
    DeserializationOptions IDeserializer.Options => Options;
 
    /// <inheritdoc cref="IDeserializer.DeserializedObjects"/>
    private readonly Dictionary<SerializationRecordId, object> _deserializedObjects = [];
    IDictionary<SerializationRecordId, object> IDeserializer.DeserializedObjects => _deserializedObjects;
 
    // Surrogate cache.
    private readonly Dictionary<Type, ISerializationSurrogate?>? _surrogates;
 
    // Queue of SerializationInfo objects that need to be applied. These are in depth first order,
    // if there are no cycles in the graph this ensures that all objects are available when the
    // SerializationInfo is applied.
    //
    // We also keep a hashset for quickly checking to make sure we do not complete objects before we
    // actually apply the SerializationInfo. While we could mark them in the incomplete dependencies
    // dictionary, to do so we'd need to know if any referenced object is going to get to this state
    // even if it hasn't finished parsing, which isn't easy to do with cycles involved.
    private Queue<PendingSerializationInfo>? _pendingSerializationInfo;
    private HashSet<SerializationRecordId>? _pendingSerializationInfoIds;
 
    private readonly Stack<ObjectRecordDeserializer> _parserStack = [];
 
    /// <inheritdoc cref="IDeserializer.IncompleteObjects"/>
    private readonly HashSet<SerializationRecordId> _incompleteObjects = [];
    public HashSet<SerializationRecordId> IncompleteObjects => _incompleteObjects;
 
    // For a given object id, the set of ids that it is waiting on to complete.
    private Dictionary<SerializationRecordId, HashSet<SerializationRecordId>>? _incompleteDependencies;
 
    // The pending value updaters. Scanned each time an object is completed.
    private HashSet<ValueUpdater>? _pendingUpdates;
 
    // Kept as a field to avoid allocating a new one every time we complete objects.
    private readonly Queue<SerializationRecordId> _pendingCompletions = [];
 
    private readonly SerializationRecordId _rootId;
 
    // We group individual object events here to fire them all when we complete the graph.
    private event Action<object?>? OnDeserialization;
    private event Action<StreamingContext>? OnDeserialized;
 
    private Deserializer(
        SerializationRecordId rootId,
        IReadOnlyDictionary<SerializationRecordId, SerializationRecord> recordMap,
        DeserializationOptions options)
    {
        _rootId = rootId;
        _recordMap = recordMap;
        _typeResolver = options.TypeResolver ?? new DefaultTypeResolver(options);
        Options = options;
 
        if (Options.SurrogateSelector is not null)
        {
            _surrogates = [];
        }
    }
 
    /// <summary>
    ///  Deserializes the object graph for the given <paramref name="recordMap"/> and <paramref name="rootId"/>.
    /// </summary>
    [RequiresUnreferencedCode("Calls System.Windows.Forms.BinaryFormat.Deserializer.Deserializer.Deserialize()")]
    internal static object Deserialize(
        SerializationRecordId rootId,
        IReadOnlyDictionary<SerializationRecordId, SerializationRecord> recordMap,
        DeserializationOptions options)
    {
        var deserializer = new Deserializer(rootId, recordMap, options);
        return deserializer.Deserialize();
    }
 
    [RequiresUnreferencedCode("Calls System.Windows.Forms.BinaryFormat.Deserializer.Deserializer.DeserializeRoot(SerializationRecordId)")]
    private object Deserialize()
    {
        DeserializeRoot(_rootId);
 
        // Complete all pending SerializationInfo objects.
        int pendingCount = _pendingSerializationInfo?.Count ?? 0;
        while (_pendingSerializationInfo is not null && _pendingSerializationInfo.Count > 0)
        {
            PendingSerializationInfo? pending = _pendingSerializationInfo.Dequeue();
 
            // Using pendingCount to only requeue on the first pass.
            if (--pendingCount >= 0
                && _pendingSerializationInfo.Count != 0
                && _incompleteDependencies is not null
                && _incompleteDependencies.TryGetValue(pending.ObjectId, out HashSet<SerializationRecordId>? dependencies))
            {
                // We can get here with nested ISerializable value types.
 
                // Hopefully another pass will complete this.
                if (dependencies.Count > 0)
                {
                    _pendingSerializationInfo.Enqueue(pending);
                    continue;
                }
 
                Debug.Fail("Completed dependencies should have been removed from the dictionary.");
            }
 
            // All _pendingSerializationInfo objects are considered incomplete.
            pending.Populate(_deserializedObjects, Options.StreamingContext);
            _pendingSerializationInfoIds?.Remove(pending.ObjectId);
            ((IDeserializer)this).CompleteObject(pending.ObjectId);
        }
 
        if (_incompleteObjects.Count > 0 || (_pendingUpdates is not null && _pendingUpdates.Count > 0))
        {
            // This should never happen outside of corrupted data.
            throw new SerializationException(SR.Serialization_Incomplete);
        }
 
        // Notify [OnDeserialized] instance methods for all relevant deserialized objects,
        // then callback IDeserializationCallback on all objects that implement it.
        OnDeserialized?.Invoke(Options.StreamingContext);
        OnDeserialization?.Invoke(null);
 
        return _deserializedObjects[_rootId];
    }
 
    [RequiresUnreferencedCode("Calls DeserializeNew(SerializationRecordId)")]
    private void DeserializeRoot(SerializationRecordId rootId)
    {
        object root = DeserializeNew(rootId);
        if (root is not ObjectRecordDeserializer parser)
        {
            return;
        }
 
        _parserStack.Push(parser);
 
        while (_parserStack.Count > 0)
        {
            ObjectRecordDeserializer? currentParser = _parserStack.Pop();
 
            SerializationRecordId requiredId;
            while (!(requiredId = currentParser.Continue()).Equals(default))
            {
                // Beside ObjectRecordDeserializer, DeserializeNew can return a raw value like int, string or an array.
                if (DeserializeNew(requiredId) is ObjectRecordDeserializer requiredParser)
                {
                    // The required object is not complete.
 
                    // Push our current parser.
                    _parserStack.Push(currentParser);
 
                    // Push the required parser so we can complete it.
                    _parserStack.Push(requiredParser);
 
                    break;
                }
            }
        }
 
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        [RequiresUnreferencedCode("Calls System.Windows.Forms.BinaryFormat.Deserializer.ObjectRecordParser.Create(SerializationRecordId, IRecord, IDeserializer)")]
        object DeserializeNew(SerializationRecordId id)
        {
            // Strings, string arrays, and primitive arrays can be completed without creating a
            // parser object. Single primitives don't normally show up as records unless they are top
            // level or are boxed into an interface reference. Checking for these requires costly
            // string matches and as such we'll just create the parser object.
 
            SerializationRecord record = _recordMap[id];
 
            object? value = record.RecordType switch
            {
                SerializationRecordType.BinaryObjectString => ((PrimitiveTypeRecord<string>)record).Value,
                SerializationRecordType.MemberPrimitiveTyped => ((PrimitiveTypeRecord)record).Value,
                SerializationRecordType.ArraySingleString => ((SZArrayRecord<string>)record).GetArray(),
                SerializationRecordType.ArraySinglePrimitive => ArrayRecordDeserializer.GetArraySinglePrimitive(record),
                SerializationRecordType.BinaryArray => ArrayRecordDeserializer.GetSimpleBinaryArray((ArrayRecord)record, _typeResolver),
                _ => null
            };
 
            if (value is not null)
            {
                _deserializedObjects.Add(record.Id, value);
                return value;
            }
 
            // Not a simple case, need to do a full deserialization of the record.
            if (!_incompleteObjects.Add(id))
            {
                // All objects should be available before they're asked for a second time.
                throw new SerializationException(SR.Serialization_Cycle);
            }
 
            var deserializer = ObjectRecordDeserializer.Create(record, this);
 
            // Add the object as soon as possible to support circular references.
            _deserializedObjects.Add(id, deserializer.Object);
            return deserializer;
        }
    }
 
    ISerializationSurrogate? IDeserializer.GetSurrogate(Type type)
    {
        // If we decide not to cache, this method could be moved to the callsite.
 
        if (_surrogates is null)
        {
            return null;
        }
 
        Debug.Assert(Options.SurrogateSelector is not null);
 
        if (!_surrogates.TryGetValue(type, out ISerializationSurrogate? surrogate))
        {
            surrogate = Options.SurrogateSelector.GetSurrogate(type, Options.StreamingContext, out _);
            _surrogates[type] = surrogate;
        }
 
        return surrogate;
    }
 
    void IDeserializer.PendSerializationInfo(PendingSerializationInfo pending)
    {
        _pendingSerializationInfo ??= new();
        _pendingSerializationInfo.Enqueue(pending);
        _pendingSerializationInfoIds ??= [];
        _pendingSerializationInfoIds.Add(pending.ObjectId);
    }
 
    void IDeserializer.PendValueUpdater(ValueUpdater updater)
    {
        // Add the pending update and update the dependencies list.
 
        _pendingUpdates ??= [];
        _pendingUpdates.Add(updater);
 
        _incompleteDependencies ??= [];
 
        if (_incompleteDependencies.TryGetValue(updater.ObjectId, out HashSet<SerializationRecordId>? dependencies))
        {
            dependencies.Add(updater.ValueId);
        }
        else
        {
            _incompleteDependencies.Add(updater.ObjectId, [updater.ValueId]);
        }
    }
 
    [UnconditionalSuppressMessage(
        "Trimming",
        "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
        Justification = "The type is already in the cache of the TypeResolver, no need to mark this one again.")]
    void IDeserializer.CompleteObject(SerializationRecordId id)
    {
        // Need to use a queue as Completion is recursive.
 
        _pendingCompletions.Enqueue(id);
        SerializationRecordId completed = default;
 
        while (_pendingCompletions.Count > 0)
        {
            SerializationRecordId completedId = _pendingCompletions.Dequeue();
            _incompleteObjects.Remove(completedId);
 
            // When we've recursed, we've done so because there are no more dependencies for the current id, so we can
            // remove it from the dictionary. We have to pend as we can't remove while we're iterating the dictionary.
            if (!completed.Equals(default))
            {
                _incompleteDependencies?.Remove(completed);
 
                if (_pendingSerializationInfoIds is not null && _pendingSerializationInfoIds.Contains(completed))
                {
                    // We came back for an object that has no remaining direct dependencies, but still has
                    // PendingSerializationInfo. As such it cannot be considered completed yet.
                    continue;
                }
 
                completed = default;
            }
 
            if (_recordMap[completedId] is ClassRecord classRecord
                && (_incompleteDependencies is null || !_incompleteDependencies.ContainsKey(completedId)))
            {
                // There are no remaining dependencies. Hook any finished events for this object.
                // Doing at the end of deserialization for simplicity.
 
                Type type = _typeResolver.GetType(classRecord.TypeName);
                object @object = _deserializedObjects[completedId];
 
                OnDeserialized += SerializationEvents.GetOnDeserializedForType(type, @object);
 
                if (@object is IDeserializationCallback callback)
                {
                    OnDeserialization += callback.OnDeserialization;
                }
 
                if (@object is IObjectReference objectReference)
                {
                    _deserializedObjects[completedId] = objectReference.GetRealObject(Options.StreamingContext);
                }
            }
 
            if (_incompleteDependencies is null)
            {
                continue;
            }
 
            Debug.Assert(_pendingUpdates is not null);
 
            foreach (KeyValuePair<SerializationRecordId, HashSet<SerializationRecordId>> pair in _incompleteDependencies)
            {
                SerializationRecordId incompleteId = pair.Key;
                HashSet<SerializationRecordId> dependencies = pair.Value;
 
                if (!dependencies.Remove(completedId))
                {
                    continue;
                }
 
                // Search for fixups that need to be applied for this dependency.
                int removals = _pendingUpdates.RemoveWhere((ValueUpdater updater) =>
                {
                    if (!updater.ValueId.Equals(completedId))
                    {
                        return false;
                    }
 
                    updater.UpdateValue(_deserializedObjects);
                    return true;
                });
 
                if (dependencies.Count != 0)
                {
                    continue;
                }
 
                // No more dependencies, enqueue for completion
                completed = incompleteId;
                _pendingCompletions.Enqueue(incompleteId);
            }
        }
    }
}
 
#pragma warning restore SYSLIB0050 // Type or member is obsolete