// 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.Core.BinaryFormat;
using System.Reflection.Metadata;
using System.Runtime.ExceptionServices;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters;
using System.Runtime.Serialization.Formatters.Binary;
using System.Windows.Forms.BinaryFormat;
using System.Windows.Forms.Nrbf;
using System.Windows.Forms.Primitives;

namespace System.Windows.Forms;

public unsafe partial class DataObject
{
    internal unsafe partial class Composition
    {
        internal static class BinaryFormatUtilities
        {
            internal static void WriteObjectToStream(MemoryStream stream, object data, bool restrictSerialization)
            {
                long position = stream.Position;

                try
                {
                    if (WinFormsBinaryFormatWriter.TryWriteCommonObject(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 (!EnableUnsafeBinaryFormatterInNativeObjectSerialization)
                {
                    throw new NotSupportedException(SR.BinaryFormatterNotSupported);
                }

                if (!LocalAppContextSwitches.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,
                Func<TypeName, Type>? resolver,
                bool legacyMode)
            {
                long startPosition = stream.Position;
                SerializationRecord? record;
                SerializationBinder binder = new Binder(typeof(T), resolver, legacyMode);
                IReadOnlyDictionary<SerializationRecordId, SerializationRecord> recordMap;

                try
                {
                    record = stream.Decode(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 (legacyMode && LocalAppContextSwitches.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 (!legacyMode && !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(exception.Message);
                        }

                        return null;
                    }
                }

                if (record.TryGetCommonObject(out object? value))
                {
                    return value;
                }

                if (!LocalAppContextSwitches.ClipboardDragDropEnableUnsafeBinaryFormatterSerialization)
                {
                    throw new NotSupportedException(string.Format(
                        SR.BinaryFormatter_NotSupported_InClipboardOrDragDrop_UseTypedAPI,
                        typeof(T).FullName));
                }

                // NRBF deserializer is more secure than the BinaryFormatter is:
                // 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 (LocalAppContextSwitches.ClipboardDragDropEnableNrbfSerialization)
                {
                    try
                    {
                        return record.Deserialize(recordMap, (ITypeResolver)binder);
                    }
                    catch (Exception ex) when (!ex.IsCriticalException() && legacyMode)
                    {
                    }
                }

                stream.Position = startPosition;
                return ReadObjectWithBinaryFormatter<T>(stream, binder);
            }

            internal static object? ReadRestrictedObjectFromStream<T>(
                MemoryStream stream,
                Func<TypeName, Type>? resolver,
                bool legacyMode)
            {
                long startPosition = stream.Position;
                SerializationRecord? record;

                try
                {
                    record = stream.Decode();
                }
                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 (!legacyMode && !type.MatchExceptAssemblyVersion(record.TypeName))
                {
                    if (!TypeNameIsAssignableToType(record.TypeName, type, new Binder(type, resolver, legacyMode)))
                    {
                        // 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 record.TryGetCommonObject(out object? value)
                    ? value
                    : throw new RestrictedTypeDeserializationException(SR.UnexpectedClipboardType);
            }

            private static bool TypeNameIsAssignableToType(TypeName typeName, Type type, ITypeResolver resolver)
            {
                try
                {
                    return resolver.GetType(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 (!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
                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 CA2300
#pragma warning restore CA2302
#pragma warning restore SYSLIB0050, SYSLIB0011
            }
        }
    }
}
