File: System\Private\Windows\Ole\BinaryFormatUtilities.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.Formats.Nrbf;
using System.Private.Windows.BinaryFormat;
using System.Private.Windows.Nrbf;
using System.Reflection.Metadata;
using System.Runtime.ExceptionServices;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters;
using System.Runtime.Serialization.Formatters.Binary;
 
namespace System.Private.Windows.Ole;
 
internal static class BinaryFormatUtilities<TNrbfSerializer> where TNrbfSerializer : INrbfSerializer
{
    internal static void WriteObjectToStream(MemoryStream stream, object data, bool restrictSerialization)
    {
        long position = stream.Position;
 
        try
        {
            if (TNrbfSerializer.TryWriteObject(stream, data))
            {
                return;
            }
        }
        catch (Exception ex) when (!ex.IsCriticalException())
        {
            // Being extra cautious here, but the Try method above should never throw in normal circumstances.
            Debug.Fail($"Unexpected exception writing binary formatted data. {ex.Message}");
        }
 
        if (restrictSerialization)
        {
            throw new SerializationException(string.Format(SR.UnexpectedTypeForClipboardFormat, data.GetType().FullName));
        }
 
        // This check is to help in trimming scenarios with a trim warning on a call to
        // BinaryFormatter.Serialize(), which has a RequiresUnreferencedCode annotation.
        // If the flag is false, the trimmer will not generate a warning, since BinaryFormatter.Serialize(),
        // will not be called,
        // If the flag is true, the trimmer will generate a warning for calling a method that has a
        // RequiresUnreferencedCode annotation.
        if (!FeatureSwitches.EnableUnsafeBinaryFormatterInNativeObjectSerialization)
        {
            throw new NotSupportedException(SR.BinaryFormatterNotSupported);
        }
 
        if (!CoreAppContextSwitches.ClipboardDragDropEnableUnsafeBinaryFormatterSerialization)
        {
            throw new NotSupportedException(SR.BinaryFormatter_NotSupported_InClipboardOrDragDrop);
        }
 
        stream.Position = position;
#pragma warning disable SYSLIB0011 // Type or member is obsolete
        new BinaryFormatter().Serialize(stream, data);
#pragma warning restore SYSLIB0011
    }
 
    internal static object? ReadObjectFromStream<T>(MemoryStream stream, ref readonly DataRequest request)
    {
        long startPosition = stream.Position;
        SerializationRecord? record;
        SerializationBinder binder = new TypeBinder<TNrbfSerializer>(typeof(T), in request);
        IReadOnlyDictionary<SerializationRecordId, SerializationRecord> recordMap;
 
        try
        {
            record = stream.DecodeNrbf(out recordMap);
        }
        catch (Exception ex) when (!ex.IsCriticalException())
        {
            // Couldn't parse for some reason, let BinaryFormatter handle the legacy invocation.
            // The typed APIs can't compare the specified type when the root record is not available.
            if (request.UntypedRequest && CoreAppContextSwitches.ClipboardDragDropEnableUnsafeBinaryFormatterSerialization)
            {
                stream.Position = startPosition;
                return ReadObjectWithBinaryFormatter<T>(stream, binder);
            }
 
            // For example offset arrays throw from the decoder -
            // https://learn.microsoft.com/dotnet/api/system.array.createinstance?#system-array-createinstance(system-type-system-int32()-system-int32())
            if (ex is NotSupportedException)
            {
                throw;
            }
 
            throw ExceptionDispatchInfo.SetRemoteStackTrace(
                new NotSupportedException(ex.Message, ex), ex.StackTrace ?? string.Empty);
        }
 
        // For the new TryGet APIs, ensure that the stream contains the requested type,
        // or type that can be assigned to the requested type.
        Type type = typeof(T);
        if (!request.UntypedRequest && !type.MatchExceptAssemblyVersion(record.TypeName))
        {
            if (record.TryGetObjectFromJson<T>((ITypeResolver)binder, out object? data))
            {
                return data;
            }
 
            if (!TypeNameIsAssignableToType(record.TypeName, type, (ITypeResolver)binder))
            {
                // If clipboard contains an exception from SetData, we will get its message and throw.
                if (record.TypeName.FullName == typeof(NotSupportedException).FullName
                    && record.TryGetNotSupportedException(out object? @object)
                    && @object is NotSupportedException exception)
                {
                    throw new NotSupportedException(string.Format(SR.NotSupportedExceptionOnClipboard, exception.Message));
                }
 
                return null;
            }
        }
 
        if (TNrbfSerializer.TryGetObject(record, out object? value))
        {
            return value;
        }
 
        if (!CoreAppContextSwitches.ClipboardDragDropEnableUnsafeBinaryFormatterSerialization)
        {
            throw new NotSupportedException(string.Format(
                SR.BinaryFormatter_NotSupported_InClipboardOrDragDrop_UseTypedAPI,
                typeof(T).FullName));
        }
 
        // NRBF deserializer avoids some known BinaryFormatter issues:
        //
        //  1. Doesn't allow arrays that have a non-zero base index (can't create these in C# or VB)
        //  2. Only allows IObjectReference types that contain primitives (to avoid observable cycle
        //     dependencies to indeterminate state)
        //
        // But it usually requires a resolver. Resolver is not available in the legacy mode,
        // so we will fall back to BinaryFormatter in that case.
        if (CoreAppContextSwitches.ClipboardDragDropEnableNrbfSerialization)
        {
            try
            {
                return record.Deserialize(recordMap, (ITypeResolver)binder);
            }
            catch (Exception ex) when (!ex.IsCriticalException() && request.UntypedRequest)
            {
            }
        }
 
        stream.Position = startPosition;
        return ReadObjectWithBinaryFormatter<T>(stream, binder);
    }
 
    internal static object? ReadRestrictedObjectFromStream<T>(
        MemoryStream stream,
        ref readonly DataRequest request)
    {
        SerializationRecord? record;
 
        try
        {
            record = stream.DecodeNrbf();
        }
        catch (Exception ex) when (!ex.IsCriticalException())
        {
            throw new RestrictedTypeDeserializationException(SR.UnexpectedClipboardType);
        }
 
        // For the new TryGet APIs, ensure that the stream contains the requested type,
        // or type that can be assigned to the requested type.
        Type type = typeof(T);
        if (!request.UntypedRequest && !type.MatchExceptAssemblyVersion(record.TypeName))
        {
            if (!TypeNameIsAssignableToType(record.TypeName, type, new TypeBinder<TNrbfSerializer>(type, in request)))
            {
                // If clipboard contains an exception from SetData, we will get its message and throw.
                if (record.TypeName.FullName == typeof(NotSupportedException).FullName
                    && record.TryGetNotSupportedException(out object? @object)
                    && @object is NotSupportedException exception)
                {
                    throw new NotSupportedException(exception.Message);
                }
 
                return null;
            }
        }
 
        return TNrbfSerializer.TryGetObject(record, out object? value)
            ? value
            : throw new RestrictedTypeDeserializationException(SR.UnexpectedClipboardType);
    }
 
    private static bool TypeNameIsAssignableToType(TypeName typeName, Type type, ITypeResolver resolver)
    {
        try
        {
            return resolver.BindToType(typeName)?.IsAssignableTo(type) == true;
        }
        catch (Exception ex) when (!ex.IsCriticalException())
        {
            // Clipboard contains a wrong type, we want the typed API to return false to the caller.
        }
 
        return false;
    }
 
    private static object? ReadObjectWithBinaryFormatter<T>(MemoryStream stream, SerializationBinder binder)
    {
        // This check is to help in trimming scenarios with a trim warning on a call to BinaryFormatter.Deserialize(),
        // which has a RequiresUnreferencedCode annotation.
        // If the flag is false, the trimmer will not generate a warning, since BinaryFormatter.Deserialize() will not be called,
        // If the flag is true, the trimmer will generate a warning for calling a method that has a RequiresUnreferencedCode annotation.
        if (!FeatureSwitches.EnableUnsafeBinaryFormatterInNativeObjectSerialization)
        {
            throw new NotSupportedException(SR.BinaryFormatterNotSupported);
        }
 
#pragma warning disable SYSLIB0011, SYSLIB0050 // Type or member is obsolete
#pragma warning disable CA2300 // Do not use insecure deserializer BinaryFormatter
#pragma warning disable CA2302 // Ensure BinaryFormatter.Binder is set before calling BinaryFormatter.Deserialize
        // cs/dangerous-binary-deserialization
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
        return new BinaryFormatter()
        {
            Binder = binder,
            AssemblyFormat = FormatterAssemblyStyle.Simple
        }.Deserialize(stream); // CodeQL[SM03722] : BinaryFormatter is intended to be used as a fallback for unsupported types. Users must explicitly opt into this behavior.
#pragma warning restore IL2026
#pragma warning restore CA2300
#pragma warning restore CA2302
#pragma warning restore SYSLIB0050, SYSLIB0011
    }
}