From d549089799bc7519f4537c256db30248072731ec Mon Sep 17 00:00:00 2001 From: Tanya Solyanik Date: Fri, 8 Nov 2024 14:20:51 -0800 Subject: [PATCH 1/8] Adding TryGetData API --- Winforms.sln | 1 + docs/list-of-diagnostics.md | 4 + src/Common/src/Obsoletions.cs | 9 +- .../TestUtilities/AppContextSwitchNames.cs | 8 +- .../BinaryFormatterInClipboardScope.cs | 27 + .../VisualBasic/MyServices/ClipboardProxy.vb | 22 +- .../src/Obsoletions.vb | 13 + .../src/PublicAPI.Unshipped.txt | 2 + .../VisualBasic/Devices/ComputerTests.cs | 2 - .../MyServices/ClipboardProxyTests.cs | 43 ++ .../Nrbf/SerializationRecordExtensions.cs | 47 ++ .../BinaryFormatWriterTests.cs | 4 +- .../SerializationRecordExtensionsTests.cs | 2 +- .../LocalAppContextSwitches.cs | 22 +- .../src/GlobalSuppressions.cs | 1 - .../src/PublicAPI.Unshipped.txt | 10 + .../Windows/Forms/Internal/Formatter.cs | 4 +- .../WinFormsSerializationRecordExtensions.cs | 16 + .../src/System/Windows/Forms/OLE/Clipboard.cs | 127 ++++- .../Forms/OLE/DataObject.BitmapBinder.cs | 25 +- ...bject.Composition.BinaryFormatUtilities.cs | 108 +++- .../OLE/DataObject.Composition.Binder.cs | 242 +++++++++ ...ect.Composition.NativeToWinFormsAdapter.cs | 347 ++++++++----- ...ect.Composition.WinFormsToNativeAdapter.cs | 2 +- .../Forms/OLE/DataObject.Composition.cs | 23 + .../Windows/Forms/OLE/DataObject.DataStore.cs | 85 ++- .../System/Windows/Forms/OLE/DataObject.cs | 158 ++++++ .../Windows/Forms/OLE/DragDropFormat.cs | 2 +- .../Windows/Forms/OLE/DragDropHelper.cs | 4 +- .../System/Windows/Forms/OLE/IDataObject.cs | 82 +++ .../ComDisabledTests/DataObjectComTests.cs | 5 + .../DesignBehaviorsTests.cs | 2 + .../UIIntegrationTests/DragDropTests.cs | 19 +- .../runtimeconfig.template.json | 1 + .../System.Windows.Forms.Tests.csproj | 1 + .../Forms/BinaryFormatUtilitiesTests.cs | 489 ++++++++++++++++-- .../System/Windows/Forms/ClipboardTests.cs | 125 ++++- .../Windows/Forms/DataObjectComTests.cs | 5 + .../Forms/DataObjectTests.ClipboardTests.cs | 7 +- .../System/Windows/Forms/DataObjectTests.cs | 222 +++++++- .../Windows/Forms/DragDropHelperTests.cs | 44 +- .../Windows/Forms/DragEventArgsTests.cs | 17 +- .../System/Windows/Forms/IDataObjectTests.cs | 146 ++++++ .../Forms/NativeToWinFormsAdapterTests.cs | 169 ++++++ .../Forms/RichTextBoxTests.ClipboardTests.cs | 2 + .../Windows/Forms/ToolStripItemTests.cs | 24 +- 46 files changed, 2429 insertions(+), 291 deletions(-) create mode 100644 src/Common/tests/TestUtilities/BinaryFormatterInClipboardScope.cs create mode 100644 src/Microsoft.VisualBasic.Forms/src/Obsoletions.vb create mode 100644 src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.Binder.cs create mode 100644 src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/IDataObjectTests.cs create mode 100644 src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/NativeToWinFormsAdapterTests.cs diff --git a/Winforms.sln b/Winforms.sln index 6ffe8d242b9..2e590cb4b85 100644 --- a/Winforms.sln +++ b/Winforms.sln @@ -55,6 +55,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "documentation", "documentat docs\developer-guide.md = docs\developer-guide.md docs\getting-started.md = docs\getting-started.md docs\issue-guide.md = docs\issue-guide.md + docs\list-of-diagnostics.md = docs\list-of-diagnostics.md docs\porting-guidelines.md = docs\porting-guidelines.md README.md = README.md docs\roadmap.md = docs\roadmap.md diff --git a/docs/list-of-diagnostics.md b/docs/list-of-diagnostics.md index f5ac1054037..a35266e6a34 100644 --- a/docs/list-of-diagnostics.md +++ b/docs/list-of-diagnostics.md @@ -13,6 +13,7 @@ The acceptance criteria for adding an obsoletion includes: * Add new constants to `src\Common\src\Obsoletions.cs`, following the existing conventions * A `...Message` const using the same description added to the table below * A `...DiagnosticId` const for the `WFDEV###` id +* If adding attribute to Microsoft.VisualBasic.Forms assembly, edit src\Microsoft.VisualBasic.Forms\src\Obsoletions.vb file * Annotate `src` files by referring to the constants defined from `Obsoletions.cs` * Specify the `UrlFormat = Obsoletions.SharedUrlFormat` * Example: `[Obsolete(Obsoletions.DomainUpDownAccessibleObjectMessage, DiagnosticId = Obsoletions.DomainUpDownAccessibleObjectDiagnosticId, UrlFormat = Obsoletions.SharedUrlFormat)]` @@ -39,6 +40,9 @@ The acceptance criteria for adding an obsoletion includes: | __`WFDEV002`__ | `DomainUpDown.DomainUpDownAccessibleObject` is no longer used to provide accessible support for `DomainUpDown` controls. Use `ControlAccessibleObject` instead. | | __`WFDEV003`__ | `DomainUpDown.DomainItemAccessibleObject` is no longer used to provide accessible support for `DomainUpDown` items. | | __`WFDEV004`__ | `Form.OnClosing`, `Form.OnClosed` and the corresponding events are obsolete. Use `Form.OnFormClosing`, `Form.OnFormClosed`, `Form.FormClosing` and `Form.FormClosed` instead. | +| __`WFDEV005`__ | `Clipboard.GetData(string)` method is obsolete. Use `Clipboard.TryGetData` instead. | +| __`WFDEV005`__ | `DataObject.GetData` methods are obsolete. Use the corresponding `DataObject.TryGetData` instead. | +| __`WFDEV005`__ | `ClipboardProxy.GetData(As String)` method is obsolete. Use `ClipboardProxy.TryGetData(Of T)(As String, As T)` instead. | ## Analyzer Warnings diff --git a/src/Common/src/Obsoletions.cs b/src/Common/src/Obsoletions.cs index 310c0c7626a..2f0b62c0369 100644 --- a/src/Common/src/Obsoletions.cs +++ b/src/Common/src/Obsoletions.cs @@ -9,7 +9,7 @@ internal static class Obsoletions { internal const string SharedUrlFormat = "https://aka.ms/winforms-warnings/{0}"; - // Please see docs\project\list-of-diagnostics.md for instructions on the steps required + // Please see docs\list-of-diagnostics.md for instructions on the steps required // to introduce a new obsoletion, apply it to downlevel builds, claim a diagnostic id, // and ensure the "aka.ms/dotnet-warnings/{0}" URL points to documentation for the obsoletion // The diagnostic ids reserved for obsoletions are WFDEV### (WFDEV001 - WFDEV999). @@ -24,4 +24,11 @@ internal static class Obsoletions internal const string FormOnClosingClosedMessage = "Form.OnClosing, Form.OnClosed and the corresponding events are obsolete. Use Form.OnFormClosing, Form.OnFormClosed, Form.FormClosing and Form.FormClosed instead."; internal const string FormOnClosingClosedDiagnosticId = "WFDEV004"; + + internal const string ClipboardGetDataMessage = "`Clipboard.GetData(string)` method is obsolete. Use `Clipboard.TryGetData` instead."; + internal const string ClipboardGetDataDiagnosticId = "WFDEV005"; + + internal const string DataObjectGetDataMessage = "`DataObject.GetData` methods are obsolete. Use the corresponding `DataObject.TryGetData` instead."; + + internal const string ClipboardProxyGetDataMessage = "`ClipboardProxy.GetData(As String)` method is obsolete. Use `ClipboardProxy.TryGetData(Of T)(As String, As T)` instead."; } diff --git a/src/Common/tests/TestUtilities/AppContextSwitchNames.cs b/src/Common/tests/TestUtilities/AppContextSwitchNames.cs index a6592b061b0..1665e3f47fc 100644 --- a/src/Common/tests/TestUtilities/AppContextSwitchNames.cs +++ b/src/Common/tests/TestUtilities/AppContextSwitchNames.cs @@ -14,8 +14,14 @@ public const string EnableUnsafeBinaryFormatterSerialization = "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization"; /// - /// Switch that controls switch caching. + /// Switch that controls switch caching. This switch is set to + /// in our test assemblies. /// public const string LocalAppContext_DisableCaching = "TestSwitch.LocalAppContext.DisableCaching"; + /// + /// The switch that controls whether or not the is enabled in the Clipboard. + /// + public const string ClipboardDragDropEnableUnsafeBinaryFormatterSerializationSwitchName + = "ClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization"; } diff --git a/src/Common/tests/TestUtilities/BinaryFormatterInClipboardScope.cs b/src/Common/tests/TestUtilities/BinaryFormatterInClipboardScope.cs new file mode 100644 index 00000000000..37fccaed8ef --- /dev/null +++ b/src/Common/tests/TestUtilities/BinaryFormatterInClipboardScope.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System; + +public readonly ref struct BinaryFormatterInClipboardScope +{ + private readonly AppContextSwitchScope _switchScope; + + public BinaryFormatterInClipboardScope(bool enable) + { + Monitor.Enter(typeof(BinaryFormatterInClipboardScope)); + _switchScope = new(AppContextSwitchNames.ClipboardDragDropEnableUnsafeBinaryFormatterSerializationSwitchName, enable); + } + + public void Dispose() + { + try + { + _switchScope.Dispose(); + } + finally + { + Monitor.Exit(typeof(BinaryFormatterInClipboardScope)); + } + } +} diff --git a/src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/MyServices/ClipboardProxy.vb b/src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/MyServices/ClipboardProxy.vb index 2a747f9d266..7c6f6e50852 100644 --- a/src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/MyServices/ClipboardProxy.vb +++ b/src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/MyServices/ClipboardProxy.vb @@ -6,6 +6,8 @@ Imports System.ComponentModel Imports System.Drawing Imports System.IO Imports System.Windows.Forms +Imports System.Reflection.Metadata +Imports System.Runtime.InteropServices Namespace Microsoft.VisualBasic.MyServices @@ -93,8 +95,16 @@ Namespace Microsoft.VisualBasic.MyServices ''' ''' The type of data being sought. ''' The data. + + Public Function GetData(format As String) As Object +#Disable Warning WFDEV005 ' Type or member is obsolete Return Clipboard.GetData(format) +#Enable Warning WFDEV005 End Function ''' @@ -183,6 +193,16 @@ Namespace Microsoft.VisualBasic.MyServices Clipboard.SetFileDropList(filePaths) End Sub + ''' + Public Function TryGetData(Of T)(format As String, resolver As Func(Of TypeName, Type), ByRef data As T) As Boolean + Return Clipboard.TryGetData(format, resolver, data) + End Function + + ''' + Public Function TryGetData(Of T)(format As String, ByRef data As T) As Boolean + Return Clipboard.TryGetData(format, data) + End Function + ''' ''' Saves the passed in to the clipboard. ''' @@ -192,7 +212,7 @@ Namespace Microsoft.VisualBasic.MyServices End Sub ''' - ''' Saves the passed in String to the clipboard. + ''' Saves the passed in to the clipboard. ''' ''' The to save. Public Sub SetText(text As String) diff --git a/src/Microsoft.VisualBasic.Forms/src/Obsoletions.vb b/src/Microsoft.VisualBasic.Forms/src/Obsoletions.vb new file mode 100644 index 00000000000..43a6ca6adf0 --- /dev/null +++ b/src/Microsoft.VisualBasic.Forms/src/Obsoletions.vb @@ -0,0 +1,13 @@ +' Licensed to the .NET Foundation under one or more agreements. +' The .NET Foundation licenses this file to you under the MIT license. + +Friend NotInheritable Class Obsoletions + + Friend Const SharedUrlFormat As String = "https://aka.ms/winforms-warnings/{0}" + + ' Please see docs\list-Of-diagnostics.md for how to claim a diagnostic id. + ' The diagnostic ids reserved for obsoletions are WFDEV### (WFDEV001 - WFDEV999). + + Friend Const ClipboardProxyGetDataMessage As String = "`ClipboardProxy.GetData(As String)` method is obsolete. Use `ClipboardProxy.TryGetData(Of T)(As String, As T)` instead." + Friend Const ClipboardGetDataDiagnosticId As String = "WFDEV005" +End Class diff --git a/src/Microsoft.VisualBasic.Forms/src/PublicAPI.Unshipped.txt b/src/Microsoft.VisualBasic.Forms/src/PublicAPI.Unshipped.txt index e69de29bb2d..65993b67a39 100644 --- a/src/Microsoft.VisualBasic.Forms/src/PublicAPI.Unshipped.txt +++ b/src/Microsoft.VisualBasic.Forms/src/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +Microsoft.VisualBasic.MyServices.ClipboardProxy.TryGetData(Of T)(format As String, ByRef data As T) -> Boolean +Microsoft.VisualBasic.MyServices.ClipboardProxy.TryGetData(Of T)(format As String, resolver As System.Func(Of System.Reflection.Metadata.TypeName, System.Type), ByRef data As T) -> Boolean \ No newline at end of file diff --git a/src/Microsoft.VisualBasic/tests/UnitTests/Microsoft/VisualBasic/Devices/ComputerTests.cs b/src/Microsoft.VisualBasic/tests/UnitTests/Microsoft/VisualBasic/Devices/ComputerTests.cs index 1f63c084fd7..0799e49d378 100644 --- a/src/Microsoft.VisualBasic/tests/UnitTests/Microsoft/VisualBasic/Devices/ComputerTests.cs +++ b/src/Microsoft.VisualBasic/tests/UnitTests/Microsoft/VisualBasic/Devices/ComputerTests.cs @@ -4,8 +4,6 @@ namespace Microsoft.VisualBasic.Devices.Tests; [Collection("Sequential")] -[CollectionDefinition("Sequential", DisableParallelization = true)] -[UISettings(MaxAttempts = 3)] // Try up to 3 times before failing. public class ComputerTests { [Fact] diff --git a/src/Microsoft.VisualBasic/tests/UnitTests/Microsoft/VisualBasic/MyServices/ClipboardProxyTests.cs b/src/Microsoft.VisualBasic/tests/UnitTests/Microsoft/VisualBasic/MyServices/ClipboardProxyTests.cs index 61d048046de..fd711b52f38 100644 --- a/src/Microsoft.VisualBasic/tests/UnitTests/Microsoft/VisualBasic/MyServices/ClipboardProxyTests.cs +++ b/src/Microsoft.VisualBasic/tests/UnitTests/Microsoft/VisualBasic/MyServices/ClipboardProxyTests.cs @@ -4,6 +4,7 @@ #nullable enable using System.Drawing; +using System.Reflection.Metadata; using Microsoft.VisualBasic.Devices; using DataFormats = System.Windows.Forms.DataFormats; using TextDataFormat = System.Windows.Forms.TextDataFormat; @@ -16,6 +17,7 @@ namespace Microsoft.VisualBasic.MyServices.Tests; [UISettings(MaxAttempts = 3)] // Try up to 3 times before failing. public class ClipboardProxyTests { +#pragma warning disable WFDEV005 // Type or member is obsolete private static string GetUniqueText() => Guid.NewGuid().ToString("D"); [WinFormsFact] @@ -83,4 +85,45 @@ public void Text() System.Windows.Forms.Clipboard.GetText(TextDataFormat.UnicodeText).Should().Be(clipboard.GetText(TextDataFormat.UnicodeText)); clipboard.GetText(TextDataFormat.UnicodeText).Should().Be(text); } + + [WinFormsFact] + public void DataOfT_CustomType_BinaryFormatterRequired() + { + var clipboard = new Computer().Clipboard; + DataWithObjectField data = new("thing1", "thing2"); + using BinaryFormatterScope scope = new(enable: true); + clipboard.SetData(typeof(DataWithObjectField).FullName!, data); + clipboard.TryGetData(typeof(DataWithObjectField).FullName!, DataResolver, out DataWithObjectField? actual).Should() + .Be(System.Windows.Forms.Clipboard.TryGetData(typeof(DataWithObjectField).FullName!, DataResolver, out DataWithObjectField? expected)); + actual.Should().BeEquivalentTo(expected); + } + + [Serializable] + private class DataWithObjectField + { + public DataWithObjectField(string text1, object object2) + { + _text1 = text1; + _object2 = object2; + } + + public string _text1; + public object _object2; + } + + private static Type DataResolver(TypeName typeName) + { + Type type = typeof(DataWithObjectField); + TypeName parsed = TypeName.Parse($"{type.FullName}, {type.Assembly.FullName}"); + + // Namespace-qualified type name. + if (typeName.FullName == parsed.FullName + // Ignore version, culture, and public key token in the assembly name. + && typeName.AssemblyName?.Name == parsed.AssemblyName?.Name) + { + return type; + } + + throw new NotSupportedException($"Unexpected type {typeName.AssemblyQualifiedName}."); + } } diff --git a/src/System.Private.Windows.Core/src/System/Private/Windows/Core/Nrbf/SerializationRecordExtensions.cs b/src/System.Private.Windows.Core/src/System/Private/Windows/Core/Nrbf/SerializationRecordExtensions.cs index b01463fafa0..843e884709e 100644 --- a/src/System.Private.Windows.Core/src/System/Private/Windows/Core/Nrbf/SerializationRecordExtensions.cs +++ b/src/System.Private.Windows.Core/src/System/Private/Windows/Core/Nrbf/SerializationRecordExtensions.cs @@ -5,6 +5,7 @@ using System.ComponentModel; using System.Drawing; using System.Formats.Nrbf; +using System.Private.Windows.Core.BinaryFormat; using System.Reflection; using System.Runtime.ExceptionServices; using System.Runtime.Serialization; @@ -41,6 +42,52 @@ internal static SerializationRecord Decode(this Stream stream) } } + internal static SerializationRecord Decode(this Stream stream, out IReadOnlyDictionary recordMap) + { + try + { + return NrbfDecoder.Decode(stream, out recordMap, leaveOpen: true); + } + catch (Exception ex) when (ex is ArgumentException or InvalidCastException or ArithmeticException or IOException) + { + // Make the exception easier to catch, but retain the original stack trace. + throw ex.ConvertToSerializationException(); + } + catch (TargetInvocationException ex) + { + throw ExceptionDispatchInfo.Capture(ex.InnerException!).SourceException.ConvertToSerializationException(); + } + } + + /// + /// Deserializes the to an object. + /// + [RequiresUnreferencedCode("Ultimately calls resolver for type names in the data.")] + public static object Deserialize( + this SerializationRecord rootRecord, + IReadOnlyDictionary recordMap, + ITypeResolver typeResolver) + { + DeserializationOptions options = new() + { + TypeResolver = typeResolver + }; + + try + { + return Deserializer.Deserialize(rootRecord.Id, recordMap, options); + } + catch (Exception ex) when (ex is ArgumentException or InvalidCastException or ArithmeticException or IOException) + { + // Make the exception easier to catch, but retain the original stack trace. + throw ex.ConvertToSerializationException(); + } + catch (TargetInvocationException ex) + { + throw ExceptionDispatchInfo.Capture(ex.InnerException!).SourceException.ConvertToSerializationException(); + } + } + internal delegate bool TryGetDelegate(SerializationRecord record, [NotNullWhen(true)] out object? value); internal static bool TryGet(TryGetDelegate get, SerializationRecord record, [NotNullWhen(true)] out object? value) diff --git a/src/System.Private.Windows.Core/tests/BinaryFormatTests/FormatTests/FormattedObject/BinaryFormatWriterTests.cs b/src/System.Private.Windows.Core/tests/BinaryFormatTests/FormatTests/FormattedObject/BinaryFormatWriterTests.cs index 771fd2fb064..046c76e8e9e 100644 --- a/src/System.Private.Windows.Core/tests/BinaryFormatTests/FormatTests/FormattedObject/BinaryFormatWriterTests.cs +++ b/src/System.Private.Windows.Core/tests/BinaryFormatTests/FormatTests/FormattedObject/BinaryFormatWriterTests.cs @@ -162,8 +162,8 @@ public void BinaryFormatWriter_TryWriteDrawingPrimitivesObject_UnsupportedObject stream.Position.Should().Be(0); } - public static IEnumerable TryWriteFrameworkObject_SupportedObjects_TestData => - ((IEnumerable)HashtableTests.Hashtables_TestData).Concat( + public static IEnumerable TryWriteFrameworkObject_SupportedObjects_TestData => + ((IEnumerable)HashtableTests.Hashtables_TestData).Concat( ListTests.PrimitiveLists_TestData).Concat( ListTests.ArrayLists_TestData).Concat( PrimitiveTypeTests.Primitive_Data).Concat( diff --git a/src/System.Private.Windows.Core/tests/BinaryFormatTests/SerializationRecordExtensionsTests.cs b/src/System.Private.Windows.Core/tests/BinaryFormatTests/SerializationRecordExtensionsTests.cs index 8008039091d..ab9c11d1b9c 100644 --- a/src/System.Private.Windows.Core/tests/BinaryFormatTests/SerializationRecordExtensionsTests.cs +++ b/src/System.Private.Windows.Core/tests/BinaryFormatTests/SerializationRecordExtensionsTests.cs @@ -11,7 +11,7 @@ namespace System.Windows.Forms.Nrbf.Tests; public class SerializationRecordExtensionsTests { - public static IEnumerable TryGetFrameworkObject_SupportedObjects_TestData => + public static IEnumerable TryGetFrameworkObject_SupportedObjects_TestData => BinaryFormatWriterTests.TryWriteFrameworkObject_SupportedObjects_TestData; [Theory] diff --git a/src/System.Windows.Forms.Primitives/src/System/LocalAppContextSwitches/LocalAppContextSwitches.cs b/src/System.Windows.Forms.Primitives/src/System/LocalAppContextSwitches/LocalAppContextSwitches.cs index 394794d313e..d310427cd01 100644 --- a/src/System.Windows.Forms.Primitives/src/System/LocalAppContextSwitches/LocalAppContextSwitches.cs +++ b/src/System.Windows.Forms.Primitives/src/System/LocalAppContextSwitches/LocalAppContextSwitches.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Runtime.CompilerServices; +using System.Runtime.Serialization.Formatters.Binary; using System.Runtime.Versioning; namespace System.Windows.Forms.Primitives; @@ -25,6 +26,7 @@ internal static partial class LocalAppContextSwitches internal const string NoClientNotificationsSwitchName = "Switch.System.Windows.Forms.AccessibleObject.NoClientNotifications"; internal const string EnableMsoComponentManagerSwitchName = "Switch.System.Windows.Forms.EnableMsoComponentManager"; internal const string TreeNodeCollectionAddRangeRespectsSortOrderSwitchName = "System.Windows.Forms.TreeNodeCollectionAddRangeRespectsSortOrder"; + internal const string ClipboardDragDropEnableUnsafeBinaryFormatterSerializationSwitchName = "ClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization"; private static int s_scaleTopLevelFormMinMaxSizeForDpi; private static int s_anchorLayoutV2; @@ -36,11 +38,12 @@ internal static partial class LocalAppContextSwitches private static int s_noClientNotifications; private static int s_enableMsoComponentManager; private static int s_treeNodeCollectionAddRangeRespectsSortOrder; + private static int s_clipboardDragDropEnableUnsafeBinaryFormatterSerialization; private static FrameworkName? s_targetFrameworkName; /// - /// When there is no exception handler registered for a thread, rethrows the exception. The exception will + /// When there is no exception handler registered for a thread, re-throws the exception. The exception will /// not be presented in a dialog or swallowed when not in interactive mode. This is always opt-in and is /// intended for scenarios where setting handlers for threads isn't practical. /// @@ -111,6 +114,11 @@ static bool GetSwitchDefaultValue(string switchName) return true; } + if (switchName == ClipboardDragDropEnableUnsafeBinaryFormatterSerializationSwitchName) + { + return false; + } + if (framework.Version.Major >= 8) { // Behavior changes added in .NET 8 @@ -219,4 +227,16 @@ public static bool TreeNodeCollectionAddRangeRespectsSortOrder [MethodImpl(MethodImplOptions.AggressiveInlining)] get => GetCachedSwitchValue(TreeNodeCollectionAddRangeRespectsSortOrderSwitchName, ref s_treeNodeCollectionAddRangeRespectsSortOrder); } + + /// + /// If , then Clipboard Get methods will use + /// to deserialize the payload if needed. To use this switch, application should also opt in into the + /// System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization option and reference the out-of-band + /// System.Runtime.Serialization.Formatters NuGet package. + /// + public static bool ClipboardDragDropEnableUnsafeBinaryFormatterSerialization + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => GetCachedSwitchValue(ClipboardDragDropEnableUnsafeBinaryFormatterSerializationSwitchName, ref s_clipboardDragDropEnableUnsafeBinaryFormatterSerialization); + } } diff --git a/src/System.Windows.Forms/src/GlobalSuppressions.cs b/src/System.Windows.Forms/src/GlobalSuppressions.cs index 10b5d06d35f..0e99c230d1d 100644 --- a/src/System.Windows.Forms/src/GlobalSuppressions.cs +++ b/src/System.Windows.Forms/src/GlobalSuppressions.cs @@ -229,7 +229,6 @@ [assembly: SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "Designer", Scope = "member", Target = "~M:System.Windows.Forms.MdiClient.ShouldSerializeLocation~System.Boolean")] // Ideally these should be different exceptions, but leaving them as shipped for compatibility -[assembly: SuppressMessage("Usage", "CA2201:Do not raise reserved exception types", Justification = "Compat", Scope = "member", Target = "~M:System.Windows.Forms.DataObject.Composition.NativeToWinFormsAdapter.GetDataFromHGLOBAL(Windows.Win32.Foundation.HGLOBAL,System.String)~System.Object")] [assembly: SuppressMessage("Usage", "CA2201:Do not raise reserved exception types", Justification = "Compat", Scope = "member", Target = "~M:System.Windows.Forms.DataObject.Composition.NativeToWinFormsAdapter.ReadByteStreamFromHGLOBAL(Windows.Win32.Foundation.HGLOBAL,System.Boolean@)~System.IO.MemoryStream")] [assembly: SuppressMessage("Usage", "CA2201:Do not raise reserved exception types", Justification = "Compat", Scope = "member", Target = "~M:System.Windows.Forms.DataObject.Composition.NativeToRuntimeAdapter.System#Runtime#InteropServices#ComTypes#IDataObject#EnumFormatEtc(System.Runtime.InteropServices.ComTypes.DATADIR)~System.Runtime.InteropServices.ComTypes.IEnumFORMATETC")] [assembly: SuppressMessage("Usage", "CA2201:Do not raise reserved exception types", Justification = "Compat", Scope = "member", Target = "~M:System.Windows.Forms.Clipboard.SetDataObject(System.Object,System.Boolean,System.Int32,System.Int32)")] diff --git a/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt b/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt index 1c427e0ef6c..ec1d31aaae3 100644 --- a/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt +++ b/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt @@ -1 +1,11 @@ +static System.Windows.Forms.Clipboard.TryGetData(string! format, out T data) -> bool +static System.Windows.Forms.Clipboard.TryGetData(string! format, System.Func! resolver, out T data) -> bool System.Windows.Forms.DataGridViewCellStyle.Font.get -> System.Drawing.Font? +System.Windows.Forms.IDataObject.TryGetData(string! format, out T data) -> bool +System.Windows.Forms.IDataObject.TryGetData(out T data) -> bool +System.Windows.Forms.IDataObject.TryGetData(string! format, bool autoConvert, out T data) -> bool +System.Windows.Forms.IDataObject.TryGetData(string! format, System.Func! resolver, bool autoConvert, out T data) -> bool +virtual System.Windows.Forms.DataObject.TryGetData(out T data) -> bool +virtual System.Windows.Forms.DataObject.TryGetData(string! format, bool autoConvert, out T data) -> bool +virtual System.Windows.Forms.DataObject.TryGetData(string! format, out T data) -> bool +virtual System.Windows.Forms.DataObject.TryGetData(string! format, System.Func! resolver, bool autoConvert, out T data) -> bool diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/Internal/Formatter.cs b/src/System.Windows.Forms/src/System/Windows/Forms/Internal/Formatter.cs index c19765724a3..d2a20f60f3b 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/Internal/Formatter.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/Internal/Formatter.cs @@ -538,9 +538,9 @@ public static bool IsNullData(object? value, object? dataSourceNullValue) } /// - /// Extract the inner type from a nullable type + /// Extract the inner type from a nullable type. /// - private static Type NullableUnwrap(Type type) + internal static Type NullableUnwrap(Type type) { if (type == s_stringType) // ...performance optimization for the most common case { diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/Nrbf/WinFormsSerializationRecordExtensions.cs b/src/System.Windows.Forms/src/System/Windows/Forms/Nrbf/WinFormsSerializationRecordExtensions.cs index 352bd28457f..23df9acebcd 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/Nrbf/WinFormsSerializationRecordExtensions.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/Nrbf/WinFormsSerializationRecordExtensions.cs @@ -64,4 +64,20 @@ public static bool TryGetResXObject(this SerializationRecord record, [NotNullWhe public static bool TryGetCommonObject(this SerializationRecord record, [NotNullWhen(true)] out object? value) => record.TryGetResXObject(out value) || record.TryGetDrawingPrimitivesObject(out value); + + public static bool TypeNameMatches(this SerializationRecord record) + { + Type type = typeof(T); + if (record.TypeNameMatches(type)) + { + return true; + } + + if (Formatter.NullableUnwrap(type) is { } unwrapped && record.TypeNameMatches(unwrapped)) + { + return true; + } + + return false; + } } diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/Clipboard.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/Clipboard.cs index bdfdd78f333..9ee0b1e913d 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/Clipboard.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/Clipboard.cs @@ -2,8 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Specialized; +using System.ComponentModel; using System.Drawing; +using System.Formats.Nrbf; +using System.Reflection.Metadata; using System.Runtime.InteropServices; +using System.Runtime.Serialization.Formatters.Binary; using Windows.Win32.System.Com; using Com = Windows.Win32.System.Com; @@ -81,7 +85,7 @@ public static unsafe void SetDataObject(object data, bool copy, int retryTimes, if (Application.OleRequired() != ApartmentState.STA) { // Only throw if a message loop was started. This makes the case of trying to query the clipboard from the - // finalizer or non-ui MTA thread silently fail, instead of making the application die. + // finalizer or non-UI MTA thread silently fail, instead of making the application die. return Application.MessageLoop ? throw new ThreadStateException(SR.ThreadMustBeSTA) : null; } @@ -218,7 +222,8 @@ public static bool ContainsText(TextDataFormat format) /// /// Retrieves an audio stream from the . /// - public static Stream? GetAudioStream() => GetData(DataFormats.WaveAudioConstant) as Stream; + public static Stream? GetAudioStream() => + TryGetData(DataFormats.WaveAudioConstant, out Stream? stream) ? stream : null; /// /// Retrieves data from the in the specified format. @@ -226,12 +231,100 @@ public static bool ContainsText(TextDataFormat format) /// /// The current thread is not in single-threaded apartment (STA) mode. /// + [Obsolete( + Obsoletions.ClipboardGetDataMessage, + error: false, + DiagnosticId = Obsoletions.ClipboardGetDataDiagnosticId, + UrlFormat = Obsoletions.SharedUrlFormat)] + [EditorBrowsable(EditorBrowsableState.Never)] public static object? GetData(string format) => string.IsNullOrWhiteSpace(format) ? null : GetData(format, autoConvert: false); private static object? GetData(string format, bool autoConvert) => GetDataObject() is IDataObject dataObject ? dataObject.GetData(format, autoConvert) : null; + /// + /// Retrieves data from the in the specified format if that data is of type . + /// This is a safer alternative to that does not use to deserialize the payload. + /// + /// The current thread is not in single-threaded apartment (STA) mode. + /// This value requires deserialization. + public static bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + string format, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) + { + data = default; + if (GetDataObject() is IDataObject dataObject) + { + // Custom IDataObjects should handle their own validation. + if (dataObject is DataObject && !DataObject.ValidateTryGetDataArguments(format)) + { + return false; + } + + return dataObject.TryGetData(format, DataObject.NotSupportedResolver, autoConvert: false, out data); + } + + return false; + } + + /// + /// Retrieves data from the in the specified format if that data is of type . + /// This is a safer alternative to that uses within constrains + /// defined by . + /// + /// Resolver is used only when deserializing non-OLE formats. It returns the type if is allowed or + /// throws a if type is not expected. It should not return a . + /// + /// + /// The current thread is not in single-threaded apartment (STA) mode. + /// + /// If application does not support and the object can't be deserialized otherwise, or + /// application supports but is an or not a concrete type, + /// or if does not resolve the actual payload type. + /// + /// + /// + /// + public static bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + string format, +#pragma warning disable CS3001 // Argument type is not CLS-compliant + Func resolver, +#pragma warning restore CS3001 + [NotNullWhen(true), MaybeNullWhen(false)] out T data) + { + data = default; + if (GetDataObject() is IDataObject dataObject) + { + return dataObject.TryGetData(format, resolver, autoConvert: false, out data); + } + + return false; + } + /// /// Retrieves a collection of file names from the . /// @@ -239,7 +332,12 @@ public static StringCollection GetFileDropList() { StringCollection result = []; - if (GetData(DataFormats.FileDropConstant, autoConvert: true) is string[] strings) + if (GetDataObject() is IDataObject dataObject + && dataObject.TryGetData( + DataFormats.FileDropConstant, + DataObject.NotSupportedResolver, + autoConvert: true, + out string[]? strings)) { result.AddRange(strings); } @@ -248,9 +346,26 @@ public static StringCollection GetFileDropList() } /// - /// Retrieves an image from the . + /// Retrieves a from the . /// - public static Image? GetImage() => GetData(DataFormats.Bitmap, autoConvert: true) as Image; + /// + /// s are re-hydrated from a by reading a byte array + /// but if that fails, is restricted by the . + /// + public static Image? GetImage() + { + if (GetDataObject() is IDataObject dataObject + && dataObject.TryGetData( + DataFormats.Bitmap, + DataObject.NotSupportedResolver, + autoConvert: true, + out Bitmap? image)) + { + return image; + } + + return null; + } /// /// Retrieves text data from the in the format. @@ -264,7 +379,7 @@ public static StringCollection GetFileDropList() public static string GetText(TextDataFormat format) { SourceGenerated.EnumValidator.Validate(format, nameof(format)); - return GetData(ConvertToDataFormats(format)) as string ?? string.Empty; + return TryGetData(ConvertToDataFormats(format), out string? text) ? text : string.Empty; } /// diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.BitmapBinder.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.BitmapBinder.cs index 44ac7aa28aa..2ed22156b55 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.BitmapBinder.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.BitmapBinder.cs @@ -1,8 +1,11 @@ // 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.Immutable; using System.Drawing; +using System.Private.Windows.Core.BinaryFormat; using System.Reflection; +using System.Reflection.Metadata; using System.Runtime.Serialization; namespace System.Windows.Forms; @@ -19,7 +22,7 @@ public partial class DataObject /// While there are more types allowed (such as , they are all safe. /// /// - private sealed class BitmapBinder : SerializationBinder + private sealed class BitmapBinder : SerializationBinder, ITypeResolver { // Bitmap type lives in different assemblies in the .NET Framework and in .NET Core. To support serialization // between both runtimes the .NET Framework identities are used. @@ -28,6 +31,7 @@ private sealed class BitmapBinder : SerializationBinder // .NET Framework PublicKeyToken=b03f5f7f11d50a3a private static ReadOnlySpan AllowedToken => [0xB0, 0x3F, 0x5F, 0x7F, 0x11, 0xD5, 0x0A, 0x3A]; + private static ImmutableArray AllowedTokenArray => AllowedToken.ToImmutableArray(); public override Type? BindToType(string assemblyName, string typeName) { @@ -55,7 +59,7 @@ private sealed class BitmapBinder : SerializationBinder public override void BindToName(Type serializedType, out string? assemblyName, out string? typeName) { - // Null values will follow the default codepath in BinaryFormatter. + // Null values will follow the default code path in BinaryFormatter. assemblyName = null; typeName = null; @@ -65,5 +69,22 @@ public override void BindToName(Type serializedType, out string? assemblyName, o throw new SerializationException(string.Format(SR.UnexpectedTypeForClipboardFormat, serializedType.FullName)); } } + + [RequiresUnreferencedCode("Calls System.Reflection.Assembly.GetType(String)")] + [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] + Type ITypeResolver.GetType(TypeName typeName) + { + if (AllowedTypeName.Equals(typeName.Name, StringComparison.Ordinal) + && typeName.AssemblyName is AssemblyNameInfo info) + { + if (AllowedAssemblyName.Equals(info.Name, StringComparison.Ordinal) + && AllowedTokenArray.SequenceEqual(info.PublicKeyOrToken)) + { + return typeof(Bitmap); + } + } + + throw new SerializationException(string.Format("Could not find type {0}", typeName.AssemblyQualifiedName)); + } } } diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.BinaryFormatUtilities.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.BinaryFormatUtilities.cs index dc646bc6913..f6463404f95 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.BinaryFormatUtilities.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.BinaryFormatUtilities.cs @@ -1,10 +1,15 @@ // 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.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; @@ -29,42 +34,123 @@ internal static void WriteObjectToStream(MemoryStream stream, object data, bool Debug.Fail($"Unexpected exception writing binary formatted data. {ex.Message}"); } -#pragma warning disable SYSLIB0011 // Type or member is obsolete if (!success) { - // 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. + // 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($"Using BinaryFormatter is not supported in WinForms Clipboard or drag and drop scenarios"); + } + stream.Position = position; +#pragma warning disable SYSLIB0011 // Type or member is obsolete new BinaryFormatter() { Binder = restrictSerialization ? new BitmapBinder() : null }.Serialize(stream, data); - } #pragma warning restore SYSLIB0011 + } } - internal static object ReadObjectFromStream(MemoryStream stream, bool restrictDeserialization) + internal static object? ReadObjectFromStream( + MemoryStream stream, + Func? resolver, + bool restrictDeserialization, + bool legacyMode) { long startPosition = stream.Position; + SerializationRecord? record; + + SerializationBinder binder = restrictDeserialization + ? new BitmapBinder() + : new DataObject.Composition.Binder(typeof(T), resolver, legacyMode); + + IReadOnlyDictionary recordMap; try { - if (stream.Decode().TryGetCommonObject(out object? value)) + record = stream.Decode(out recordMap); + } + catch (Exception ex) when (!ex.IsCriticalException()) + { + // Couldn't parse for some reason, let the BinaryFormatter try to handle the legacy invocation. + if (legacyMode && LocalAppContextSwitches.ClipboardDragDropEnableUnsafeBinaryFormatterSerialization) + { + stream.Position = startPosition; + return ReadObjectWithBinaryFormatter(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()) + throw new NotSupportedException("Clipboard content can't be validated.", ex); + } + + // For the new TryGet APIs, ensure that the stream contains the requested type, + // or type that can be assigned to the requested type. + if (!legacyMode && !record.TypeNameMatches()) + { +#if false // TODO(TanyaSo) - modify TryGetObjectFromJson to take a resolver and rename to HasJsonData??? + // Return true if the payload contains valid JsonData struct, type matches or not + // note: binder.GetType() throws and never returns null + // run isassignable in the json method + if (record.TryGetObjectFromJson(binder.GetType, out object? data)) + { + return data; + } +#endif + + if (resolver is null || !TypeNameIsAssignableToType(record.TypeName, typeof(T), resolver)) + { + return null; + } + } + + if (record.TryGetCommonObject(out object? value)) + { + return value; + } + + if (LocalAppContextSwitches.ClipboardDragDropEnableUnsafeBinaryFormatterSerialization) + { + if (!legacyMode && record.Deserialize(recordMap, (ITypeResolver)binder) is { } result) { - return value; + return result; } + + stream.Position = startPosition; + return ReadObjectWithBinaryFormatter(stream, binder); + } + + return null; + } + + // TanyaSo: this does not special-case the NotSupported exception, but we probably want to always deserialize it. + private static bool TypeNameIsAssignableToType(TypeName typeName, Type type, Func resolver) + { + Type? resolvedType = null; + try + { + resolvedType = resolver(typeName); } catch (Exception ex) when (!ex.IsCriticalException()) { - // Couldn't parse for some reason, let the BinaryFormatter try to handle it. + return false; } + return resolvedType?.IsAssignableTo(type) == true; + } + + private static object? ReadObjectWithBinaryFormatter(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, @@ -83,7 +169,7 @@ internal static object ReadObjectFromStream(MemoryStream stream, bool restrictDe // cs/dangerous-binary-deserialization return new BinaryFormatter() { - Binder = restrictDeserialization ? new BitmapBinder() : null, + 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 diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.Binder.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.Binder.cs new file mode 100644 index 00000000000..4ffbef08a39 --- /dev/null +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.Binder.cs @@ -0,0 +1,242 @@ +// 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.Immutable; +using System.Private.Windows.Core.BinaryFormat; +using System.Reflection.Metadata; +using System.Runtime.Serialization; +using Switches = System.Windows.Forms.Primitives.LocalAppContextSwitches; + +namespace System.Windows.Forms; + +public unsafe partial class DataObject +{ + internal unsafe partial class Composition + { + internal sealed class Binder : SerializationBinder, ITypeResolver + { + private readonly Func? _resolver; + private readonly Type _type; + private readonly bool _legacyMode; + + // This is needed to resolve fields of the requested type T when using deserializers. + private readonly Dictionary _mscorlibTypeCache = new() + { + { "System.Byte", typeof(byte) }, + { "System.SByte", typeof(sbyte) }, + { "System.Int16", typeof(short) }, + { "System.UInt16", typeof(ushort) }, + { "System.Int32", typeof(int) }, + { "System.UInt32", typeof(uint) }, + { "System.Int64", typeof(long) }, + { "System.UInt64", typeof(ulong) }, + { "System.Double", typeof(double) }, + { "System.Single", typeof(float) }, + { "System.Char", typeof(char) }, + { "System.Boolean", typeof(bool) }, + { "System.String", typeof(string) }, + { "System.Decimal", typeof(decimal) }, + { "System.DateTime", typeof(DateTime) }, + { "System.TimeSpan", typeof(TimeSpan) }, + { "System.IntPtr", typeof(IntPtr) }, + { "System.UIntPtr", typeof(UIntPtr) }, + { TypeInfo.NotSupportedExceptionType, typeof(NotSupportedException) }, + { "System.Collections.Generic.List`1[[System.Byte, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(List) }, + { "System.Collections.Generic.List`1[[System.SByte, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(List) }, + { "System.Collections.Generic.List`1[[System.Int16, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(List) }, + { "System.Collections.Generic.List`1[[System.UInt16, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(List) }, + { "System.Collections.Generic.List`1[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(List) }, + { "System.Collections.Generic.List`1[[System.UInt32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(List) }, + { "System.Collections.Generic.List`1[[System.Int64, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(List) }, + { "System.Collections.Generic.List`1[[System.UInt64, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(List) }, + { "System.Collections.Generic.List`1[[System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(List) }, + { "System.Collections.Generic.List`1[[System.Double, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(List) }, + { "System.Collections.Generic.List`1[[System.Char, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(List) }, + { "System.Collections.Generic.List`1[[System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(List) }, + { "System.Collections.Generic.List`1[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(List) }, + { "System.Collections.Generic.List`1[[System.Decimal, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(List) }, + { "System.Collections.Generic.List`1[[System.DateTime, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(List) }, + { "System.Collections.Generic.List`1[[System.TimeSpan, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(List) }, + { "System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(byte[]) }, + { "System.SByte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(sbyte[]) }, + { "System.Int16[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(short[]) }, + { "System.UInt16[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(ushort[]) }, + { "System.Int32[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(int[]) }, + { "System.UInt32[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(uint[]) }, + { "System.Int64[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(long[]) }, + { "System.UInt64[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(ulong[]) }, + { "System.Single[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(float[]) }, + { "System.Double[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(double[]) }, + { "System.Char[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(char[]) }, + { "System.Boolean[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(bool[]) }, + { "System.String[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(string[]) }, + { "System.Decimal[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(decimal[]) }, + { "System.DateTime[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(DateTime[]) }, + { "System.TimeSpan[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(TimeSpan[]) } + }; + + private readonly Dictionary<(string, string), Type> _commonTypes = new() + { + { ("System.Windows.Forms.ImageListStreamer", "System.Windows.Forms"), typeof(ImageListStreamer) }, + { ("System.Drawing.Bitmap", "System.Drawing"), typeof(Drawing.Bitmap) }, + // The following are exchange types, they are serialized with the .NET Framework assembly name. + // In .NET they are located in System.Drawing.Primitives. + { ("System.Drawing.RectangleF", "System.Drawing"), typeof(Drawing.RectangleF) }, + { ("System.Drawing.PointF", "System.Drawing"), typeof(Drawing.PointF) }, + { ("System.Drawing.SizeF", "System.Drawing"), typeof(Drawing.SizeF) }, + { ("System.Drawing.Rectangle", "System.Drawing"), typeof(Drawing.Rectangle) }, + { ("System.Drawing.Point", "System.Drawing"), typeof(Drawing.Point) }, + { ("System.Drawing.Size", "System.Drawing"), typeof(Drawing.Size) }, + { ("System.Drawing.Color", "System.Drawing"), typeof(Drawing.Color) } + }; + + public Binder(Type type, Func? resolver, bool legacyMode) + { + _resolver = resolver; + _type = type.OrThrowIfNull(); + + _legacyMode = legacyMode; + } + + public override Type? BindToType(string assemblyName, string typeName) + { + if (string.IsNullOrWhiteSpace(assemblyName)) + { + throw new ArgumentNullException(nameof(assemblyName)); + } + + if (string.IsNullOrWhiteSpace(typeName)) + { + throw new ArgumentNullException(nameof(typeName)); + } + + return GetType(assemblyName, typeName, null); + } + + private Type? GetType(string assemblyName, string fullTypeName, TypeName? typeName) + { + // We assume all built-in types are normalized to the mscorlib assembly, as BinaryFormatter + // and NRBF reader and deserializer are doing so for compatibility with .NET Framework. + if (assemblyName.Equals(TypeInfo.MscorlibAssemblyName, StringComparison.Ordinal) + && _mscorlibTypeCache.TryGetValue(fullTypeName, out Type? builtIn)) + { + return builtIn; + } + + // Ignore version, culture, and public key token and compare the short names. + string shortAssemblyName = assemblyName.Split(',')[0].Trim(); + if (_commonTypes.TryGetValue((fullTypeName, shortAssemblyName), out Type? knownType)) + { + return knownType; + } + + typeName ??= TypeName.Parse($"{fullTypeName}, {assemblyName}"); + if (Matches(_type, typeName)) + { + _commonTypes.Add((fullTypeName, shortAssemblyName), _type); + return _type; + } + + if (_legacyMode) + { + return Switches.ClipboardDragDropEnableUnsafeBinaryFormatterSerialization + ? null + : throw new NotSupportedException($"Using BinaryFormatter is not supported in WinForms Clipboard" + + $" or drag and drop scenarios."); + } + + if (_resolver is null) + { + throw new NotSupportedException($"'resolver' function is required in '{nameof(Clipboard.TryGetData)}'" + + $" method to resolve '{fullTypeName}' from '{assemblyName}'"); + } + + Type type = _resolver(typeName) + ?? throw new NotSupportedException($"'resolver' function provided in '{nameof(Clipboard.TryGetData)}'" + + $" method should never return a null. It should throw a '{nameof(NotSupportedException)}' when encountering unsupported types."); + + _commonTypes.Add((fullTypeName, shortAssemblyName), type); + return type; + } + + [RequiresUnreferencedCode("Calls System.Reflection.Assembly.GetType(String)")] + [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] + Type ITypeResolver.GetType(TypeName typeName) + { + if (typeName.AssemblyName is null || typeName.AssemblyName.FullName is not string fullName || string.IsNullOrWhiteSpace(fullName)) + { + throw new ArgumentException($"{nameof(TypeName.AssemblyName)} is missing.", nameof(typeName)); + } + + Type? type; + try + { + type = GetType(fullName, typeName.FullName, typeName); + } + catch (Exception e) + { + throw new SerializationException($"Could not find type {typeName.AssemblyQualifiedName}", e); + } + + return type ?? throw new SerializationException($"Could not find type {typeName.AssemblyQualifiedName}"); + } + + // Copied from https://github.com/dotnet/runtime/blob/79a71fc750652191eba18e19b3f98492e882cb5f/src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/SerializationRecord.cs#L68 + internal 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; + } + + if (type.IsArray != typeName.IsArray + || type.IsConstructedGenericType != typeName.IsConstructedGenericType + || type.IsNested != typeName.IsNested + || (type.IsArray && type.GetArrayRank() != typeName.GetArrayRank()) + || type.IsSZArray != typeName.IsSZArray // int[] vs int[*] + ) + { + 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 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; + } + } + } +} diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.NativeToWinFormsAdapter.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.NativeToWinFormsAdapter.cs index 147e8f6bc15..814d99f4523 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.NativeToWinFormsAdapter.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.NativeToWinFormsAdapter.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Drawing; +using System.Reflection.Metadata; using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; using System.Text; @@ -90,30 +91,41 @@ HRESULT Com.IDataObject.Interface.SetData(Com.FORMATETC* pformatetc, Com.STGMEDI /// /// Retrieves the specified format from the specified . /// - private static object? GetDataFromHGLOBAL(HGLOBAL hglobal, string format) + private static bool TryGetDataFromHGLOBAL(HGLOBAL hglobal, string format, Func? resolver, bool legacyMode, out T? data) { + data = default; if (hglobal == 0) { - return null; + return false; } - return format switch + object? value = format switch { - DataFormats.TextConstant or DataFormats.RtfConstant or DataFormats.OemTextConstant - => ReadStringFromHGLOBAL(hglobal, unicode: false), + DataFormats.TextConstant or DataFormats.RtfConstant or DataFormats.OemTextConstant => + ReadStringFromHGLOBAL(hglobal, unicode: false), DataFormats.HtmlConstant => ReadUtf8StringFromHGLOBAL(hglobal), DataFormats.UnicodeTextConstant => ReadStringFromHGLOBAL(hglobal, unicode: true), DataFormats.FileDropConstant => ReadFileListFromHDROP((HDROP)(nint)hglobal), CF_DEPRECATED_FILENAME => new string[] { ReadStringFromHGLOBAL(hglobal, unicode: false) }, CF_DEPRECATED_FILENAMEW => new string[] { ReadStringFromHGLOBAL(hglobal, unicode: true) }, - _ => ReadObjectFromHGLOBAL(hglobal, RestrictDeserializationToSafeTypes(format)) + _ => ReadObjectOrStreamFromHGLOBAL(hglobal, resolver, RestrictDeserializationToSafeTypes(format), legacyMode) }; - static object ReadObjectFromHGLOBAL(HGLOBAL hglobal, bool restrictDeserialization) + if (value is T t) { - MemoryStream stream = ReadByteStreamFromHGLOBAL(hglobal, out bool isSerializedObject); - return !isSerializedObject ? stream : BinaryFormatUtilities.ReadObjectFromStream(stream, restrictDeserialization); + data = t; + return true; } + + return false; + } + + private static object? ReadObjectOrStreamFromHGLOBAL(HGLOBAL hglobal, Func? resolver, bool restrictDeserialization, bool legacyMode) + { + MemoryStream stream = ReadByteStreamFromHGLOBAL(hglobal, out bool isSerializedObject); + return !isSerializedObject + ? stream + : BinaryFormatUtilities.ReadObjectFromStream(stream, resolver, restrictDeserialization, legacyMode); } private static unsafe MemoryStream ReadByteStreamFromHGLOBAL(HGLOBAL hglobal, out bool isSerializedObject) @@ -132,7 +144,7 @@ private static unsafe MemoryStream ReadByteStreamFromHGLOBAL(HGLOBAL hglobal, ou int index = 0; // The object here can either be a stream or a serialized object. We identify a serialized object - // by writing the bytes for the guid serializedObjectID at the front of the stream. + // by writing the bytes for the guid serializedObjectID at the start of the stream. if (isSerializedObject = bytes.AsSpan().StartsWith(s_serializedObjectID)) { @@ -213,22 +225,51 @@ private static unsafe string ReadUtf8StringFromHGLOBAL(HGLOBAL hglobal) /// /// A restricted type was encountered, do not continue trying to deserialize. /// - private static object? GetObjectFromDataObject(Com.IDataObject* dataObject, string format, out bool doNotContinue) + /// + /// + /// If contains that contains a serialized object, + /// we return that object cast to or null. If the is + /// not a serialized object, and a stream was requested, i.e. can be cast to + /// we return that . + /// + /// + private static bool TryGetObjectFromDataObject( + Com.IDataObject* dataObject, + string format, + Func? resolver, + bool legacyMode, + out bool doNotContinue, + out T? data) { - object? data = null; + data = default; doNotContinue = false; + bool result = false; try { // Try to get the data as a bitmap first. - data = TryGetBitmapData(dataObject, format); + if (typeof(Bitmap).IsAssignableTo(typeof(T)) && TryGetBitmapData(dataObject, format, out Bitmap? bitmap)) + { + data = (T)(object)bitmap; + return true; + } // Check for one of our standard data types. - data ??= TryGetHGLOBALData(dataObject, format, out doNotContinue); - - if (data is null && !doNotContinue) + result = TryGetHGLOBALData(dataObject, format, resolver, legacyMode, out doNotContinue, out data); + if (!result && !doNotContinue) { // Lastly check to see if the data is an IStream. - data = TryGetIStreamData(dataObject, format); + result = TryGetIStreamData(dataObject, format, resolver, legacyMode, out data); + } + } + catch (NotSupportedException nse) + { + // When BinaryFormatter is not available to write the managed object to the Clipboard, + // we write an exception with instructions how to modify the application to enable the binary formatter, + // doing the same thing when reading. + if (typeof(NotSupportedException).IsAssignableTo(typeof(T))) + { + data = (T)(object)nse; + return true; } } catch (Exception e) @@ -236,117 +277,122 @@ private static unsafe string ReadUtf8StringFromHGLOBAL(HGLOBAL hglobal) Debug.Fail(e.ToString()); } - return data; + return result; + } + + private static bool TryGetHGLOBALData(Com.IDataObject* dataObject, string format, Func? resolver, bool legacyMode, out bool doNotContinue, out T? data) + { + data = default; + doNotContinue = false; - static object? TryGetHGLOBALData(Com.IDataObject* dataObject, string format, out bool doNotContinue) + Com.FORMATETC formatetc = new() { - doNotContinue = false; + cfFormat = (ushort)DataFormats.GetFormat(format).Id, + dwAspect = (uint)Com.DVASPECT.DVASPECT_CONTENT, + lindex = -1, + tymed = (uint)Com.TYMED.TYMED_HGLOBAL + }; - Com.FORMATETC formatetc = new() - { - cfFormat = (ushort)DataFormats.GetFormat(format).Id, - dwAspect = (uint)Com.DVASPECT.DVASPECT_CONTENT, - lindex = -1, - tymed = (uint)Com.TYMED.TYMED_HGLOBAL - }; + if (dataObject->QueryGetData(formatetc).Failed) + { + return false; + } - if (dataObject->QueryGetData(formatetc).Failed) - { - return null; - } + HRESULT hr = dataObject->GetData(formatetc, out Com.STGMEDIUM medium); - object? data = null; - HRESULT hr = dataObject->GetData(formatetc, out Com.STGMEDIUM medium); + // One of the ways this can happen is when we attempt to put binary formatted data onto the + // clipboard, which will succeed as Windows ignores all errors when putting data on the clipboard. + // The data state, however, is not good, and this error will be returned by Windows when asking to + // get the data out. + Debug.WriteLineIf(hr == HRESULT.CLIPBRD_E_BAD_DATA, "CLIPBRD_E_BAD_DATA returned when trying to get clipboard data."); + Debug.WriteLineIf(hr == HRESULT.DV_E_TYMED, "DV_E_TYMED returned when trying to get clipboard data."); - // One of the ways this can happen is when we attempt to put binary formatted data onto the - // clipboard, which will succeed as Windows ignores all errors when putting data on the clipboard. - // The data state, however, is not good, and this error will be returned by Windows when asking to - // get the data out. - Debug.WriteLineIf(hr == HRESULT.CLIPBRD_E_BAD_DATA, "CLIPBRD_E_BAD_DATA returned when trying to get clipboard data."); - Debug.WriteLineIf(hr == HRESULT.DV_E_TYMED, "DV_E_TYMED returned when trying to get clipboard data."); - - try - { - if (medium.tymed == Com.TYMED.TYMED_HGLOBAL && !medium.hGlobal.IsNull) - { - data = GetDataFromHGLOBAL(medium.hGlobal, format); - } - } - catch (RestrictedTypeDeserializationException) - { - doNotContinue = true; - } - catch - { - } - finally + bool result = false; + try + { + if (medium.tymed == Com.TYMED.TYMED_HGLOBAL && !medium.hGlobal.IsNull) { - PInvoke.ReleaseStgMedium(ref medium); + result = TryGetDataFromHGLOBAL(medium.hGlobal, format, resolver, legacyMode, out data); } + } + catch (RestrictedTypeDeserializationException) + { + result = false; + data = default; + doNotContinue = true; + } + catch (Exception ex) when (ex is not NotSupportedException) + { + } + finally + { + PInvoke.ReleaseStgMedium(ref medium); + } + + return result; + } + + private static unsafe bool TryGetIStreamData(Com.IDataObject* dataObject, string format, Func? resolver, bool legacyMode, out T? data) + { + data = default; + Com.FORMATETC formatEtc = new() + { + cfFormat = (ushort)DataFormats.GetFormat(format).Id, + dwAspect = (uint)Com.DVASPECT.DVASPECT_CONTENT, + lindex = -1, + tymed = (uint)Com.TYMED.TYMED_ISTREAM + }; - return data; + // Limit the # of exceptions we may throw below. + if (dataObject->QueryGetData(formatEtc).Failed + || dataObject->GetData(formatEtc, out Com.STGMEDIUM medium).Failed) + { + return false; } - static unsafe object? TryGetIStreamData(Com.IDataObject* dataObject, string format) + HGLOBAL hglobal = default; + try { - Com.FORMATETC formatEtc = new() + if (medium.tymed != Com.TYMED.TYMED_ISTREAM || medium.hGlobal.IsNull) { - cfFormat = (ushort)DataFormats.GetFormat(format).Id, - dwAspect = (uint)Com.DVASPECT.DVASPECT_CONTENT, - lindex = -1, - tymed = (uint)Com.TYMED.TYMED_ISTREAM - }; - - // Limit the # of exceptions we may throw below. - if (dataObject->QueryGetData(formatEtc).Failed - || dataObject->GetData(formatEtc, out Com.STGMEDIUM medium).Failed) - { - return null; + return false; } - HGLOBAL hglobal = default; - try - { - if (medium.tymed != Com.TYMED.TYMED_ISTREAM || medium.hGlobal.IsNull) - { - return null; - } + using ComScope pStream = new((Com.IStream*)medium.hGlobal); + pStream.Value->Stat(out Com.STATSTG sstg, (uint)Com.STATFLAG.STATFLAG_DEFAULT); - using ComScope pStream = new((Com.IStream*)medium.hGlobal); - pStream.Value->Stat(out Com.STATSTG sstg, (uint)Com.STATFLAG.STATFLAG_DEFAULT); + hglobal = PInvokeCore.GlobalAlloc(GLOBAL_ALLOC_FLAGS.GMEM_MOVEABLE | GLOBAL_ALLOC_FLAGS.GMEM_ZEROINIT, (uint)sstg.cbSize); - hglobal = PInvokeCore.GlobalAlloc(GLOBAL_ALLOC_FLAGS.GMEM_MOVEABLE | GLOBAL_ALLOC_FLAGS.GMEM_ZEROINIT, (uint)sstg.cbSize); - - // Not throwing here because the other out of memory condition on GlobalAlloc - // happens inside innerData.GetData and gets turned into a null return value. - if (hglobal.IsNull) - { - return null; - } + // Not throwing here because the other out of memory condition on GlobalAlloc + // happens inside innerData.GetData and gets turned into a null return value. + if (hglobal.IsNull) + { + return false; + } - void* ptr = PInvokeCore.GlobalLock(hglobal); - pStream.Value->Read((byte*)ptr, (uint)sstg.cbSize, null); - PInvokeCore.GlobalUnlock(hglobal); + void* ptr = PInvokeCore.GlobalLock(hglobal); + pStream.Value->Read((byte*)ptr, (uint)sstg.cbSize, null); + PInvokeCore.GlobalUnlock(hglobal); - return GetDataFromHGLOBAL(hglobal, format); - } - finally + return TryGetDataFromHGLOBAL(hglobal, format, resolver, legacyMode, out data); + } + finally + { + if (!hglobal.IsNull) { - if (!hglobal.IsNull) - { - PInvokeCore.GlobalFree(hglobal); - } - - PInvoke.ReleaseStgMedium(ref medium); + PInvokeCore.GlobalFree(hglobal); } + + PInvoke.ReleaseStgMedium(ref medium); } } - private static Image? TryGetBitmapData(Com.IDataObject* dataObject, string format) + private static bool TryGetBitmapData(Com.IDataObject* dataObject, string format, [NotNullWhen(true)] out Bitmap? data) { + data = default; if (format != DataFormats.BitmapConstant) { - return null; + return false; } Com.FORMATETC formatEtc = new() @@ -369,18 +415,17 @@ private static unsafe string ReadUtf8StringFromHGLOBAL(HGLOBAL hglobal) Debug.WriteLineIf(hr == HRESULT.CLIPBRD_E_BAD_DATA, "CLIPBRD_E_BAD_DATA returned when trying to get clipboard data."); } - Image? data = null; - try { // GDI+ doesn't own this HBITMAP, but we can't delete it while the object is still around. So we // have to do the really expensive thing of cloning the image so we can release the HBITMAP. if ((uint)medium.tymed == (uint)TYMED.TYMED_GDI && !medium.hGlobal.IsNull - && Image.FromHbitmap(medium.hGlobal) is Image clipboardImage) + && Image.FromHbitmap(medium.hGlobal) is Bitmap clipboardBitmap) { - data = (Image)clipboardImage.Clone(); - clipboardImage.Dispose(); + data = (Bitmap)clipboardBitmap.Clone(); + clipboardBitmap.Dispose(); + return true; } } finally @@ -388,51 +433,97 @@ private static unsafe string ReadUtf8StringFromHGLOBAL(HGLOBAL hglobal) PInvoke.ReleaseStgMedium(ref medium); } - return data; + return false; } - #region IDataObject - - object? IDataObject.GetData(string format, bool autoConvert) + private bool TryGetDataInternal(string format, Func? resolver, bool autoConvert, bool legacyMode, out T? data) { + data = default; + using var nativeDataObject = _nativeDataObject.GetInterface(); - object? data = GetObjectFromDataObject(nativeDataObject, format, out bool doNotContinue); - if (doNotContinue - || !autoConvert - || (data is not null && data is not MemoryStream) - || GetMappedFormats(format) is not { } mappedFormats) + // If user code had not provided a resolver when using an overload that takes one, then we will throw at the point when + // resolver is required, thus we are not validating this parameter here. + bool result = TryGetObjectFromDataObject(nativeDataObject, format, resolver, legacyMode, out bool doNotContinue, out data); + + if (doNotContinue) { - return data; + // Specified format is a restricted one, but content required BinaryFormatter deserialization, not a supported scenario. + data = default; + return false; } - object? originalData = data; + if (result || !autoConvert || GetMappedFormats(format) is not { } mappedFormats) + { + return result; + } // Try to find a mapped format that works better. foreach (string mappedFormat in mappedFormats) { - if (!format.Equals(mappedFormat)) + if (format.Equals(mappedFormat)) { - data = GetObjectFromDataObject(nativeDataObject, mappedFormat, out doNotContinue); - if (doNotContinue) - { - break; - } + continue; + } - if (data is not null and not MemoryStream) - { - return data; - } + result = TryGetObjectFromDataObject(nativeDataObject, mappedFormat, resolver, legacyMode, out doNotContinue, out data); + if (doNotContinue) + { + Debug.Fail("All mapped formats must be either restricted or not restricted."); + break; + } + + if (result) + { + return true; } } - return originalData ?? data; + return result; + } + + #region IDataObject + + object? IDataObject.GetData(string format, bool autoConvert) + { + TryGetDataInternal(format, resolver: null, autoConvert, legacyMode: true, out object? data); + return data; } object? IDataObject.GetData(string format) => ((IDataObject)this).GetData(format, autoConvert: true); object? IDataObject.GetData(Type format) => ((IDataObject)this).GetData(format.FullName!); + bool IDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + string format, + Func resolver, + bool autoConvert, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) + { + if (!ValidateTryGetDataArguments(format, resolver)) + { + data = default; + return false; + } + + return TryGetDataInternal(format, resolver, autoConvert, legacyMode: false, out data); + } + + bool IDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + string format, + bool autoConvert, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) => + TryGetDataInternal(format, NotSupportedResolver, autoConvert, legacyMode: false, out data); + + bool IDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + string format, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) => + TryGetDataInternal(format, NotSupportedResolver, autoConvert: false, legacyMode: false, out data); + + bool IDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + [NotNullWhen(true), MaybeNullWhen(false)] out T data) => + TryGetDataInternal(typeof(T).FullName!, NotSupportedResolver, autoConvert: false, legacyMode: false, out data); + bool IDataObject.GetDataPresent(Type format) => GetDataPresent(format.FullName!); public bool GetDataPresent(string format, bool autoConvert) diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.WinFormsToNativeAdapter.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.WinFormsToNativeAdapter.cs index 02968c2e6f7..5e904abaa00 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.WinFormsToNativeAdapter.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.WinFormsToNativeAdapter.cs @@ -29,7 +29,7 @@ public WinFormsToNativeAdapter(IDataObject dataObject) } /// - /// Returns true if the tymed is useable. + /// Returns true if the tymed is usable. /// private static bool GetTymedUseable(TYMED tymed) => (tymed & AllowedTymeds) != 0; diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.cs index 95441e38688..09109a0eced 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection.Metadata; using System.Runtime.InteropServices.ComTypes; using Com = Windows.Win32.System.Com; using ComTypes = System.Runtime.InteropServices.ComTypes; @@ -107,6 +108,28 @@ or DataFormats.PaletteConstant object? IDataObject.GetData(string format, bool autoConvert) => _winFormsDataObject.GetData(format, autoConvert); object? IDataObject.GetData(string format) => _winFormsDataObject.GetData(format); object? IDataObject.GetData(Type format) => _winFormsDataObject.GetData(format); + bool IDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + string format, + Func resolver, + bool autoConvert, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) => + _winFormsDataObject.TryGetData(format, resolver, autoConvert, out data); + + bool IDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + string format, + bool autoConvert, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) => + _winFormsDataObject.TryGetData(format, NotSupportedResolver, autoConvert, out data); + + bool IDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + string format, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) => + _winFormsDataObject.TryGetData(format, NotSupportedResolver, autoConvert: false, out data); + + bool IDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + [NotNullWhen(true), MaybeNullWhen(false)] out T data) => + _winFormsDataObject.TryGetData(typeof(T).FullName!, NotSupportedResolver, autoConvert: false, out data); + bool IDataObject.GetDataPresent(string format, bool autoConvert) => _winFormsDataObject.GetDataPresent(format, autoConvert); bool IDataObject.GetDataPresent(string format) => _winFormsDataObject.GetDataPresent(format); bool IDataObject.GetDataPresent(Type format) => _winFormsDataObject.GetDataPresent(format); diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.DataStore.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.DataStore.cs index d957d245b78..faf520daed7 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.DataStore.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.DataStore.cs @@ -3,6 +3,7 @@ using System.Collections.Specialized; using System.Drawing; +using System.Reflection.Metadata; using System.Runtime.Serialization; namespace System.Windows.Forms; @@ -29,54 +30,82 @@ public DataStore() { } - public virtual object? GetData(string format, bool autoConvert) + private bool TryGetDataInternal( + string format, + bool autoConvert, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) { + data = default; if (string.IsNullOrWhiteSpace(format)) { - return null; + return false; } - object? baseVar = null; - if (_data.TryGetValue(format, out DataStoreEntry? dse)) + if (_data.TryGetValue(format, out DataStoreEntry? dse) && dse.Data is T t) { - baseVar = dse.Data; + data = t; + return true; } - object? original = baseVar; + if (!autoConvert + || !(dse is null || dse.AutoConvert) + || GetMappedFormats(format) is not { } mappedFormats) + { + return false; + } - if (autoConvert - && (dse is null || dse.AutoConvert) - && (baseVar is null || baseVar is MemoryStream)) + for (int i = 0; i < mappedFormats.Length; i++) { - string[]? mappedFormats = GetMappedFormats(format); - if (mappedFormats is not null) + if (format.Equals(mappedFormats[i])) { - for (int i = 0; i < mappedFormats.Length; i++) - { - if (!format.Equals(mappedFormats[i])) - { - if (_data.TryGetValue(mappedFormats[i], out DataStoreEntry? found)) - { - baseVar = found.Data; - } - - if (baseVar is not null and not MemoryStream) - { - original = null; - break; - } - } - } + continue; + } + + if (_data.TryGetValue(mappedFormats[i], out DataStoreEntry? found) && found.Data is T value) + { + data = value; + return true; } } - return original ?? baseVar; + return false; + } + + public virtual object? GetData(string format, bool autoConvert) + { + TryGetDataInternal(format, autoConvert, out object? data); + return data; } public virtual object? GetData(string format) => GetData(format, autoConvert: true); public virtual object? GetData(Type format) => GetData(format.FullName!); + public virtual bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + string format, + Func resolver, + bool autoConvert, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) + { + data = default; + return TryGetDataInternal(format, autoConvert, out data); + } + + public virtual bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + string format, + bool autoConvert, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) => + TryGetDataInternal(format, autoConvert, out data); + + public virtual bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + string format, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) => + TryGetDataInternal(format, autoConvert: false, out data); + + public virtual bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + [NotNullWhen(true), MaybeNullWhen(false)] out T data) => + TryGetDataInternal(typeof(T).FullName!, autoConvert: false, out data); + public virtual void SetData(string format, bool autoConvert, object? data) { if (string.IsNullOrWhiteSpace(format)) diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.cs index 8f23699bab7..b99b76acaf4 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.cs @@ -2,7 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Specialized; +using System.ComponentModel; using System.Drawing; +using System.Reflection.Metadata; using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; using Com = Windows.Win32.System.Com; @@ -26,6 +28,10 @@ public unsafe partial class DataObject : private readonly Composition _innerData; + internal static Type NotSupportedResolver(TypeName typeName) => + throw new NotSupportedException($"Using BinaryFormatter is not supported in WinForms Clipboard data deserialization." + + $" Can't resolve {typeName.AssemblyQualifiedName}."); + /// /// Initializes a new instance of the class, with the raw /// and the managed data object the raw pointer is associated with. @@ -92,13 +98,55 @@ internal IDataObject TryUnwrapInnerIDataObject() internal IDataObject? OriginalIDataObject => _innerData.OriginalIDataObject; #region IDataObject + [Obsolete( + Obsoletions.DataObjectGetDataMessage, + error: false, + DiagnosticId = Obsoletions.ClipboardGetDataDiagnosticId, + UrlFormat = Obsoletions.SharedUrlFormat)] + [EditorBrowsable(EditorBrowsableState.Never)] public virtual object? GetData(string format, bool autoConvert) => ((IDataObject)_innerData).GetData(format, autoConvert); + [Obsolete( + Obsoletions.DataObjectGetDataMessage, + error: false, + DiagnosticId = Obsoletions.ClipboardGetDataDiagnosticId, + UrlFormat = Obsoletions.SharedUrlFormat)] + [EditorBrowsable(EditorBrowsableState.Never)] public virtual object? GetData(string format) => GetData(format, autoConvert: true); + [Obsolete( + Obsoletions.DataObjectGetDataMessage, + error: false, + DiagnosticId = Obsoletions.ClipboardGetDataDiagnosticId, + UrlFormat = Obsoletions.SharedUrlFormat)] + [EditorBrowsable(EditorBrowsableState.Never)] public virtual object? GetData(Type format) => format is null ? null : GetData(format.FullName!); + public virtual bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + string format, +#pragma warning disable CS3001 // Argument type is not CLS-compliant + Func resolver, +#pragma warning restore CS3001 + bool autoConvert, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) => + ((IDataObject)_innerData).TryGetData(format, resolver, autoConvert, out data); + + public virtual bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + string format, + bool autoConvert, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) => + TryGetData(format, NotSupportedResolver, autoConvert, out data); + + public virtual bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + string format, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) => + TryGetData(format, autoConvert: false, out data); + + public virtual bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + [NotNullWhen(true), MaybeNullWhen(false)] out T data) => + TryGetData(typeof(T).FullName!, out data); + public virtual bool GetDataPresent(string format, bool autoConvert) => ((IDataObject)_innerData).GetDataPresent(format, autoConvert); @@ -135,6 +183,7 @@ public virtual bool ContainsText(TextDataFormat format) return GetDataPresent(ConvertToDataFormats(format), autoConvert: false); } +#pragma warning disable WFDEV005 // Type or member is obsolete public virtual Stream? GetAudioStream() => GetData(DataFormats.WaveAudio, autoConvert: false) as Stream; public virtual StringCollection GetFileDropList() @@ -158,6 +207,7 @@ public virtual string GetText(TextDataFormat format) SourceGenerated.EnumValidator.Validate(format, nameof(format)); return GetData(ConvertToDataFormats(format), false) is string text ? text : string.Empty; } +#pragma warning restore WFDEV005 public virtual void SetAudio(byte[] audioBytes) => SetAudio(new MemoryStream(audioBytes.OrThrowIfNull())); @@ -185,6 +235,114 @@ public virtual void SetText(string textData, TextDataFormat format) SetData(ConvertToDataFormats(format), false, textData); } + internal static bool ValidateTryGetDataArguments(string format, Func resolver) + { + if (!ValidateFormat(format)) + { + return false; + } + + if (resolver is null + && !IsRestrictedFormat(format) + && IsUnboundedType()) + { + // Tanyaso TODO: localize string + throw new NotSupportedException( + $"'{typeof(T).Name}' is not a concrete type, and could allow for " + + $"unbounded deserialization. Use a concrete type or define a resolver " + + $"function that supports types that you are retrieving from the Clipboard."); + } + + return true; + } + + internal static bool ValidateTryGetDataArguments(string format) + { + if (!ValidateFormat(format)) + { + return false; + } + + Type type = typeof(T); + if (!IsRestrictedFormat(format) + // check is a convenience for simple usages where you aren't passing a resolver explicitly. + && IsUnboundedType()) + { + // TODO: localize string + throw new NotSupportedException( + $"'{typeof(T).Name}' is not a concrete type, and could allow for " + + $"unbounded deserialization. Use a concrete type or define a resolver " + + $"function that supports types that you are retrieving from the Clipboard."); + } + + return true; + } + + private static bool IsUnboundedType() + { + if (typeof(T) == typeof(object)) + { + return true; + } + + Type type = typeof(T); + return type.IsInterface || type.IsAbstract; + } + + /// + /// For OLE formats, we support only a few known managed types. + /// For unknown formats, return true, they will be further validated when reading the data. + /// + private static bool ValidateFormat(string format) + { + if (string.IsNullOrWhiteSpace(format)) + { + return false; + } + + return format switch + { + DataFormats.TextConstant or + DataFormats.UnicodeTextConstant or + DataFormats.StringConstant or + DataFormats.RtfConstant or + DataFormats.HtmlConstant or + DataFormats.OemTextConstant => typeof(string) == typeof(T), + + DataFormats.FileDropConstant or + CF_DEPRECATED_FILENAME or + CF_DEPRECATED_FILENAMEW => typeof(string[]) == typeof(T), + + DataFormats.BitmapConstant or BitmapFullName => typeof(Bitmap) == typeof(T) || typeof(Image) == typeof(T), + _ => true + }; + } + + private static bool IsRestrictedFormat(string format) => + format is DataFormats.StringConstant + or BitmapFullName + or DataFormats.CsvConstant + or DataFormats.DibConstant + or DataFormats.DifConstant + or DataFormats.LocaleConstant + or DataFormats.PenDataConstant + or DataFormats.RiffConstant + or DataFormats.SymbolicLinkConstant + or DataFormats.TiffConstant + or DataFormats.WaveAudioConstant + or DataFormats.BitmapConstant + or DataFormats.EmfConstant + or DataFormats.PaletteConstant + or DataFormats.WmfConstant + or DataFormats.TextConstant + or DataFormats.UnicodeTextConstant + or DataFormats.RtfConstant + or DataFormats.HtmlConstant + or DataFormats.OemTextConstant + or DataFormats.FileDropConstant + or CF_DEPRECATED_FILENAME + or CF_DEPRECATED_FILENAMEW; + private static string ConvertToDataFormats(TextDataFormat format) => format switch { TextDataFormat.UnicodeText => DataFormats.UnicodeTextConstant, diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DragDropFormat.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DragDropFormat.cs index e6818b7df16..13c3ae10d2d 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DragDropFormat.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DragDropFormat.cs @@ -29,7 +29,7 @@ public DragDropFormat(ushort format, STGMEDIUM medium, bool copyData) } /// - /// Returns a copy of the storage mediumn in this instance. + /// Returns a copy of the storage medium in this instance. /// public STGMEDIUM GetData() => CopyData(_format, _medium); diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DragDropHelper.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DragDropHelper.cs index 3bb6e436816..25a1ec243ed 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DragDropHelper.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DragDropHelper.cs @@ -196,7 +196,7 @@ public static unsafe bool IsInDragLoop(IDataObject dataObject) ArgumentNullException.ThrowIfNull(dataObject); if (dataObject.GetDataPresent(PInvoke.CFSTR_INDRAGLOOP) - && dataObject.GetData(PInvoke.CFSTR_INDRAGLOOP) is DragDropFormat dragDropFormat) + && dataObject.TryGetData(PInvoke.CFSTR_INDRAGLOOP, out DragDropFormat? dragDropFormat)) { try { @@ -248,7 +248,7 @@ public static void ReleaseDragDropFormats(IComDataObject comDataObject) foreach (string format in dataObject.GetFormats()) { - if (dataObject.GetData(format) is DragDropFormat dragDropFormat) + if (dataObject.TryGetData(format, out DragDropFormat? dragDropFormat)) { dragDropFormat.Dispose(); } diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/IDataObject.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/IDataObject.cs index 33ba57a45f8..c873228eec4 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/IDataObject.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/IDataObject.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection.Metadata; + namespace System.Windows.Forms; /// @@ -24,6 +26,86 @@ public interface IDataObject /// object? GetData(Type format); + /// + /// Retrieves the data associated with the specified data format, using + /// to determine whether to convert the data to the format, + /// if that data is assignable to . + /// Will use with the binary formatter if needed. + /// is implemented by the user and should return the allowed types or + /// throw a . + /// + bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + string format, +#pragma warning disable CS3001 // Argument type is not CLS-compliant + Func resolver, +#pragma warning restore CS3001 + bool autoConvert, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) + { + data = default; + if (GetData(format, autoConvert) is T result) + { + data = result; + return true; + } + + return false; + } + + /// + /// Retrieves the data associated with the specified data format, using + /// to determine whether to convert the data to another format, + /// if that data is of type . + /// + bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + string format, + bool autoConvert, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) + { + data = default; + if (GetData(format, autoConvert) is T result) + { + data = result; + return true; + } + + return false; + } + + /// + /// Retrieves the data associated with the specified data format if that data is of type . + /// + bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + string format, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) + { + data = default; + if (GetData(format) is T result) + { + data = result; + return true; + } + + return false; + } + + /// + /// Retrieves the data associated with data format named after , + /// if that data is of type . + /// + bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + [NotNullWhen(true), MaybeNullWhen(false)] out T data) + { + data = default; + if (GetData(typeof(T)) is T result) + { + data = result; + return true; + } + + return false; + } + /// /// Determines whether data stored in this instance is associated with the /// specified format, using autoConvert to determine whether to convert the diff --git a/src/System.Windows.Forms/tests/ComDisabledTests/DataObjectComTests.cs b/src/System.Windows.Forms/tests/ComDisabledTests/DataObjectComTests.cs index 58923843eda..03cbbb2d049 100644 --- a/src/System.Windows.Forms/tests/ComDisabledTests/DataObjectComTests.cs +++ b/src/System.Windows.Forms/tests/ComDisabledTests/DataObjectComTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection.Metadata; using System.Runtime.InteropServices.ComTypes; using Com = Windows.Win32.System.Com; using IComDataObject = System.Runtime.InteropServices.ComTypes.IDataObject; @@ -52,6 +53,10 @@ private class CustomIDataObject : IDataObject public object GetData(string format, bool autoConvert) => throw new NotImplementedException(); public object GetData(string format) => throw new NotImplementedException(); public object GetData(Type format) => throw new NotImplementedException(); + public bool TryGetData(string format, Func resolver, bool autoConvert, out T data) => throw new NotImplementedException(); + public bool TryGetData(string format, bool autoConvert, out T data) => throw new NotImplementedException(); + public bool TryGetData(string format, out T data) => throw new NotImplementedException(); + public bool TryGetData(out T data) => throw new NotImplementedException(); public bool GetDataPresent(string format, bool autoConvert) => throw new NotImplementedException(); public bool GetDataPresent(string format) => throw new NotImplementedException(); public bool GetDataPresent(Type format) => throw new NotImplementedException(); diff --git a/src/System.Windows.Forms/tests/IntegrationTests/UIIntegrationTests/DesignBehaviorsTests.cs b/src/System.Windows.Forms/tests/IntegrationTests/UIIntegrationTests/DesignBehaviorsTests.cs index d424fc81e23..d0d268ac6b0 100644 --- a/src/System.Windows.Forms/tests/IntegrationTests/UIIntegrationTests/DesignBehaviorsTests.cs +++ b/src/System.Windows.Forms/tests/IntegrationTests/UIIntegrationTests/DesignBehaviorsTests.cs @@ -183,7 +183,9 @@ public ToolboxItem DeserializeToolboxItem(object serializedObject) public ToolboxItem DeserializeToolboxItem(object serializedObject, IDesignerHost? host) { +#pragma warning disable WFDEV005 // Type or member is obsolete ToolboxItem? item = ((DataObject)serializedObject)?.GetData(typeof(ToolboxItem)) as ToolboxItem; +#pragma warning restore WFDEV005 return item!; } diff --git a/src/System.Windows.Forms/tests/IntegrationTests/UIIntegrationTests/DragDropTests.cs b/src/System.Windows.Forms/tests/IntegrationTests/UIIntegrationTests/DragDropTests.cs index b2c96314870..4de728c011f 100644 --- a/src/System.Windows.Forms/tests/IntegrationTests/UIIntegrationTests/DragDropTests.cs +++ b/src/System.Windows.Forms/tests/IntegrationTests/UIIntegrationTests/DragDropTests.cs @@ -60,7 +60,7 @@ public async Task DragDrop_NonSerializedObject_ReturnsExpected_Async() // non-serialized object. Button? button = null; - object? data = null; + Button? data = null; await RunFormWithoutControlAsync(() => new Form(), async (form) => { form.AllowDrop = true; @@ -77,7 +77,7 @@ public async Task DragDrop_NonSerializedObject_ReturnsExpected_Async() if (e.Data?.GetDataPresent(typeof(Button)) ?? false) { // Get the non-serialized Button. - data = e.Data?.GetData(typeof(Button)); + data = (Button?)e.Data?.GetData(typeof(Button)); e.Effect = DragDropEffects.Copy; } }; @@ -116,9 +116,8 @@ await InputSimulator.SendAsync( }); Assert.NotNull(data); - Assert.True(data is Button); - Assert.Equal(button?.Name, ((Button)data).Name); - Assert.Equal(button?.Text, ((Button)data).Text); + Assert.Equal(button?.Name, data.Name); + Assert.Equal(button?.Text, data.Text); } [WinFormsFact(Skip = "Crashes dotnet.exe, see: https://github.com/dotnet/winforms/issues/8598")] @@ -275,7 +274,7 @@ public async Task DragDrop_SerializedObject_ReturnsExpected_Async() // Verifies that we can successfully drag and drop a serialized object. ListViewItem? listViewItem = null; - object? data = null; + ListViewItem? data = null; await RunFormWithoutControlAsync(() => new Form(), async (form) => { form.AllowDrop = true; @@ -292,7 +291,7 @@ public async Task DragDrop_SerializedObject_ReturnsExpected_Async() if (e.Data?.GetDataPresent(DataFormats.Serializable) ?? false) { // Get the serialized ListViewItem. - data = e.Data?.GetData(DataFormats.Serializable); + data = (ListViewItem?)e.Data?.GetData(DataFormats.Serializable); e.Effect = DragDropEffects.Copy; } }; @@ -331,8 +330,8 @@ await InputSimulator.SendAsync( Assert.NotNull(data); Assert.True(data is ListViewItem); - Assert.Equal(listViewItem?.Name, ((ListViewItem)data).Name); - Assert.Equal(listViewItem?.Text, ((ListViewItem)data).Text); + Assert.Equal(listViewItem?.Name, data.Name); + Assert.Equal(listViewItem?.Text, data.Text); } [WinFormsFact] @@ -861,7 +860,7 @@ private void ListDragSource_QueryContinueDrag(object? sender, QueryContinueDragE ((MousePosition.Y - _screenOffset.Y) < form.DesktopBounds.Top) || ((MousePosition.Y - _screenOffset.Y) > form.DesktopBounds.Bottom)) { - _testOutputHelper.WriteLine($"Cancelling drag."); + _testOutputHelper.WriteLine($"Canceling drag."); e.Action = DragAction.Cancel; } } diff --git a/src/System.Windows.Forms/tests/IntegrationTests/WinformsControlsTest/runtimeconfig.template.json b/src/System.Windows.Forms/tests/IntegrationTests/WinformsControlsTest/runtimeconfig.template.json index 6054a7041d9..722ffde3076 100644 --- a/src/System.Windows.Forms/tests/IntegrationTests/WinformsControlsTest/runtimeconfig.template.json +++ b/src/System.Windows.Forms/tests/IntegrationTests/WinformsControlsTest/runtimeconfig.template.json @@ -1,6 +1,7 @@ { "configProperties": { "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false, + "ClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization": false, "System.Windows.Forms.AnchorLayoutV2": false, "System.Windows.Forms.ApplyParentFontToMenus": true, "System.Windows.Forms.DataGridViewUIAStartRowCountAtZero": false, diff --git a/src/System.Windows.Forms/tests/UnitTests/System.Windows.Forms.Tests.csproj b/src/System.Windows.Forms/tests/UnitTests/System.Windows.Forms.Tests.csproj index 9852774b838..3d4859367fd 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System.Windows.Forms.Tests.csproj +++ b/src/System.Windows.Forms/tests/UnitTests/System.Windows.Forms.Tests.csproj @@ -25,6 +25,7 @@ + diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/BinaryFormatUtilitiesTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/BinaryFormatUtilitiesTests.cs index 3459743c23b..f5a151454a6 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/BinaryFormatUtilitiesTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/BinaryFormatUtilitiesTests.cs @@ -5,6 +5,7 @@ using System.Collections; using System.Drawing; +using System.Reflection.Metadata; using System.Runtime.Serialization; using Utilities = System.Windows.Forms.DataObject.Composition.BinaryFormatUtilities; @@ -18,25 +19,62 @@ public partial class BinaryFormatUtilitiesTests : IDisposable public void Dispose() => _stream.Dispose(); + private void WriteObjectToStream(object value, bool restrictSerialization = false) => + Utilities.WriteObjectToStream(_stream, value, restrictSerialization); + + private object? ReadObjectFromStream(bool restrictDeserialization = false) + { + _stream.Position = 0; + return Utilities.ReadObjectFromStream( + _stream, + resolver: null, + restrictDeserialization, + legacyMode: true); + } + + private object? ReadObjectFromStream(Func resolver, bool restrictDeserialization = false) + { + _stream.Position = 0; + return Utilities.ReadObjectFromStream(_stream, resolver, restrictDeserialization, legacyMode: false); + } + private object? RoundTripObject(object value) { - Utilities.WriteObjectToStream(_stream, value, restrictSerialization: false); + // This is equivalent to SetData/GetData methods with unbounded formats, and works with the BF AppCompat switches. + WriteObjectToStream(value); return ReadObjectFromStream(); } private object? RoundTripObject_RestrictedFormat(object value) { - Utilities.WriteObjectToStream(_stream, value, restrictSerialization: true); + // This is equivalent to SetData/GetData methods using registered OLE formats and thus the BitmapBinder, + // and works with the BF AppCompat switches. + WriteObjectToStream(value, restrictSerialization: true); return ReadObjectFromStream(restrictDeserialization: true); } - private void WriteObjectToStream(object value, bool restrictSerialization = false) => - Utilities.WriteObjectToStream(_stream, value, restrictSerialization); + private object? RoundTripOfType(object value) + { + // This is equivalent to SetData/TryGetData methods using unbounded OLE formats, + // and works with the BinaryFormatter AppCompat switches. + WriteObjectToStream(value); + return ReadObjectFromStream(DataObject.NotSupportedResolver); + } - private object? ReadObjectFromStream(bool restrictDeserialization = false) + private object? RoundTripOfType_RestrictedFormat(object value) { - _stream.Position = 0; - return Utilities.ReadObjectFromStream(_stream, restrictDeserialization); + // This is equivalent to SetData/TryGetData methods using OLE formats. Deserialization is restricted by + // BitmapBinder and the BF AppCompat switches. + WriteObjectToStream(value, restrictSerialization: true); + return ReadObjectFromStream(DataObject.NotSupportedResolver, restrictDeserialization: true); + } + + private object? RoundTripOfType(object value, Func resolver) + { + // This is equivalent to SetData/TryGetData methods using unbounded formats, + // serialization is restricted by the resolver and BF AppCompat switches. + WriteObjectToStream(value); + return ReadObjectFromStream(resolver); } // Primitive types as defined by the NRBF spec. @@ -54,17 +92,17 @@ private void WriteObjectToStream(object value, bool restrictSerialization = fals (float)9.0, 10.0, 'a', - true + true, + "string", + DateTime.Now, + TimeSpan.FromHours(1), + decimal.MaxValue ]; public static TheoryData KnownObjects_TheoryData => [ - "string", - DateTime.Now, - TimeSpan.FromHours(1), -(nint)11, (nuint)12, - decimal.MaxValue, new PointF(1, 2), new RectangleF(1, 2, 3, 4), new Point(-1, int.MaxValue), @@ -167,40 +205,42 @@ private void WriteObjectToStream(object value, bool restrictSerialization = fals [ new List(), new List(), - new List<(int, int)>() + new List<(int, int)>(), + new List { nint.MinValue, nint.MaxValue }, + new List { nuint.MinValue, nuint.MaxValue } ]; [Theory] [MemberData(nameof(PrimitiveObjects_TheoryData))] [MemberData(nameof(KnownObjects_TheoryData))] - public void BinaryFormatUtilities_RoundTrip_Simple(object value) => + public void RoundTrip_Simple(object value) => RoundTripObject(value).Should().Be(value); [Theory] [MemberData(nameof(PrimitiveObjects_TheoryData))] [MemberData(nameof(KnownObjects_TheoryData))] - public void BinaryFormatUtilities_RoundTripRestrictedFormat_Simple(object value) => + public void RoundTrip_RestrictedFormat_Simple(object value) => RoundTripObject_RestrictedFormat(value).Should().Be(value); [Theory] [MemberData(nameof(NotSupportedException_TestData))] - public void BinaryFormatUtilities_RoundTrip_NotSupportedException(NotSupportedException value) => + public void RoundTrip_NotSupportedException(NotSupportedException value) => RoundTripObject(value).Should().BeEquivalentTo(value); [Theory] [MemberData(nameof(NotSupportedException_TestData))] - public void BinaryFormatUtilities_RoundTripRestrictedFormat_NotSupportedException(NotSupportedException value) => + public void RoundTrip_RestrictedFormat_NotSupportedException(NotSupportedException value) => RoundTripObject_RestrictedFormat(value).Should().BeEquivalentTo(value); [Fact] - public void BinaryFormatUtilities_RoundTrip_NotSupportedException_DataLoss() + public void RoundTrip_NotSupportedException_DataLoss() { NotSupportedException value = new("Error message", new ArgumentException()); RoundTripObject(value).Should().BeEquivalentTo(new NotSupportedException("Error message", innerException: null)); } [Fact] - public void BinaryFormatUtilities_RoundTripRestrictedFormat_NotSupportedException_DataLoss() + public void RoundTrip_RestrictedFormat_NotSupportedException_DataLoss() { NotSupportedException value = new("Error message", new ArgumentException()); RoundTripObject_RestrictedFormat(value).Should().BeEquivalentTo(new NotSupportedException("Error message", innerException: null)); @@ -208,41 +248,41 @@ public void BinaryFormatUtilities_RoundTripRestrictedFormat_NotSupportedExceptio [Theory] [MemberData(nameof(PrimitiveListObjects_TheoryData))] - public void BinaryFormatUtilities_RoundTrip_PrimitiveList(IList value) => + public void RoundTrip_PrimitiveList(IList value) => RoundTripObject(value).Should().BeEquivalentTo(value); [Theory] [MemberData(nameof(PrimitiveListObjects_TheoryData))] - public void BinaryFormatUtilities_RoundTripRestrictedFormat_PrimitiveList(IList value) => + public void RoundTrip_RestrictedFormat_PrimitiveList(IList value) => RoundTripObject_RestrictedFormat(value).Should().BeEquivalentTo(value); [Theory] [MemberData(nameof(PrimitiveArrayObjects_TheoryData))] - public void BinaryFormatUtilities_RoundTrip_PrimitiveArray(Array value) => + public void RoundTrip_PrimitiveArray(Array value) => RoundTripObject(value).Should().BeEquivalentTo(value); [Theory] [MemberData(nameof(PrimitiveArrayListObjects_TheoryData))] - public void BinaryFormatUtilities_RoundTrip_PrimitiveArrayList(ArrayList value) => + public void RoundTrip_PrimitiveArrayList(ArrayList value) => RoundTripObject(value).Should().BeEquivalentTo(value); [Theory] [MemberData(nameof(PrimitiveArrayListObjects_TheoryData))] - public void BinaryFormatUtilities_RoundTripRestrictedFormat_PrimitiveArrayList(ArrayList value) => + public void RoundTrip_RestrictedFormat_PrimitiveArrayList(ArrayList value) => RoundTripObject_RestrictedFormat(value).Should().BeEquivalentTo(value); [Theory] [MemberData(nameof(PrimitiveTypeHashtables_TheoryData))] - public void BinaryFormatUtilities_RoundTrip_PrimitiveHashtable(Hashtable value) => + public void RoundTrip_PrimitiveHashtable(Hashtable value) => RoundTripObject(value).Should().BeEquivalentTo(value); [Theory] [MemberData(nameof(PrimitiveTypeHashtables_TheoryData))] - public void BinaryFormatUtilities_RoundTripRestrictedFormat_PrimitiveHashtable(Hashtable value) => + public void RoundTrip_RestrictedFormat_PrimitiveHashtable(Hashtable value) => RoundTripObject_RestrictedFormat(value).Should().BeEquivalentTo(value); [Fact] - public void BinaryFormatUtilities_RoundTrip_ImageList() + public void RoundTrip_ImageList() { using ImageList sourceList = new(); using Bitmap image = new(10, 10); @@ -257,7 +297,7 @@ public void BinaryFormatUtilities_RoundTrip_ImageList() } [Fact] - public void BinaryFormatUtilities_RoundTripRestrictedFormat_ImageList() + public void RoundTrip_RestrictedFormat_ImageList() { using ImageList sourceList = new(); using Bitmap image = new(10, 10); @@ -272,14 +312,14 @@ public void BinaryFormatUtilities_RoundTripRestrictedFormat_ImageList() } [Fact] - public void BinaryFormatUtilities_RoundTrip_Bitmap() + public void RoundTrip_Bitmap() { using Bitmap value = new(10, 10); RoundTripObject(value).Should().BeOfType().Subject.Size.Should().Be(value.Size); } [Fact] - public void BinaryFormatUtilities_RoundTripRestrictedFormat_Bitmap() + public void RoundTrip_RestrictedFormat_Bitmap() { using Bitmap value = new(10, 10); RoundTripObject_RestrictedFormat(value).Should().BeOfType().Subject.Size.Should().Be(value.Size); @@ -287,26 +327,409 @@ public void BinaryFormatUtilities_RoundTripRestrictedFormat_Bitmap() [Theory] [MemberData(nameof(Lists_UnsupportedTestData))] - public void BinaryFormatUtilities_RoundTrip_Unsupported(IList value) + public void RoundTrip_Unsupported(IList value) { ((Action)(() => WriteObjectToStream(value))).Should().Throw(); using (BinaryFormatterScope scope = new(enable: true)) { + ((Action)(() => WriteObjectToStream(value))).Should().Throw(); + + using BinaryFormatterInClipboardScope clipboardScope = new(enable: true); WriteObjectToStream(value); ReadObjectFromStream().Should().BeEquivalentTo(value); } - ((Action)(() => ReadObjectFromStream())).Should().Throw(); + // Doesn't attempt to access BinaryFormatter. + ReadObjectFromStream().Should().BeNull(); } [Theory] [MemberData(nameof(Lists_UnsupportedTestData))] - public void BinaryFormatUtilities_RoundTripRestrictedFormat_Unsupported(IList value) + public void RoundTrip_RestrictedFormat_Unsupported(IList value) { ((Action)(() => WriteObjectToStream(value, restrictSerialization: true))).Should().Throw(); using BinaryFormatterScope scope = new(enable: true); + using BinaryFormatterInClipboardScope clipboardScope = new(enable: true); ((Action)(() => WriteObjectToStream(value, restrictSerialization: true))).Should().Throw(); } + + [Fact] + public void RoundTrip_OffsetArray() + { + Array value = Array.CreateInstance(typeof(uint), lengths: [2, 3], lowerBounds: [1, 2]); + value.SetValue(101u, 1, 2); + value.SetValue(102u, 1, 3); + value.SetValue(103u, 1, 4); + value.SetValue(201u, 2, 2); + value.SetValue(202u, 2, 3); + value.SetValue(203u, 2, 4); + + using BinaryFormatterScope scope = new(enable: true); + using BinaryFormatterInClipboardScope clipboardScope = new(enable: true); + var result = RoundTripObject(value).Should().BeOfType().Subject; + + result.Rank.Should().Be(2); + result.GetLength(0).Should().Be(2); + result.GetLength(1).Should().Be(3); + result.GetLowerBound(0).Should().Be(1); + result.GetLowerBound(1).Should().Be(2); + result.GetValue(1, 2).Should().Be(101u); + result.GetValue(1, 3).Should().Be(102u); + result.GetValue(1, 4).Should().Be(103u); + result.GetValue(2, 2).Should().Be(201u); + result.GetValue(2, 3).Should().Be(202u); + result.GetValue(2, 4).Should().Be(203u); + } + + [Fact] + public void RoundTripOfType_Unsupported() + { + List value = new() { "text" }; + using (BinaryFormatterScope scope = new(enable: true)) + using (BinaryFormatterInClipboardScope clipboardScope = new(enable: true)) + { + WriteObjectToStream(value); + + var result = ReadObjectFromStream>(ObjectResolver).Should().BeOfType>().Subject; + result.Count.Should().Be(1); + result[0].Should().Be("text"); + } + + ReadObjectFromStream>(ObjectResolver).Should().BeNull(); + } + + private static Type ObjectResolver(TypeName typeName) + { + if (typeof(object).FullName! == typeName.FullName) + { + return typeof(object); + } + + throw new NotSupportedException($"Can't resolve {typeName.FullName}"); + } + + [Fact] + public void RoundTripOfType_AsUnmatchingType_Simple() + { + List value = [1, 2, 3]; + RoundTripOfType(value).Should().BeNull(); + } + + [Fact] + public void RoundTripOfType_RestrictedFormat_AsUnmatchingType_Simple() + { + Rectangle value = new(1, 1, 2, 2); + // We are setting up an invalid content scenario, Rectangle type can't be read as a restricted format, + // but in this case requested type will not match the payload type. + WriteObjectToStream(value); + + ReadObjectFromStream(DataObject.NotSupportedResolver, restrictDeserialization: true).Should().BeNull(); + + using BinaryFormatterScope scope = new(enable: true); + using BinaryFormatterInClipboardScope clipboardScope = new(enable: true); + ReadObjectFromStream(DataObject.NotSupportedResolver, restrictDeserialization: true).Should().BeNull(); + } + + [Fact] + public void RoundTripOfType_intNullable() => + RoundTripOfType(101, DataObject.NotSupportedResolver).Should().Be(101); + + [Fact] + public void RoundTripOfType_RestrictedFormat_intNullable() => + RoundTripOfType_RestrictedFormat(101).Should().Be(101); + + [Fact] + public void RoundTripOfType_intNullableArray_DefaultResolver() + { + int?[] value = [101, null, 303]; + + using BinaryFormatterScope scope = new(enable: true); + using BinaryFormatterInClipboardScope clipboardScope = new(enable: true); + ((Action)(() => RoundTripOfType(value))).Should().Throw(); + } + + [Fact] + public void RoundTripOfType_RestrictedFormat_intNullableArray_DefaultResolver() + { + int?[] value = [101, null, 303]; + + using BinaryFormatterScope scope = new(enable: true); + using BinaryFormatterInClipboardScope clipboardScope = new(enable: true); + ((Action)(() => RoundTripOfType_RestrictedFormat(value))).Should().Throw(); + } + + [Fact] + public void RoundTripOfType_OffsetArray_DefaultResolver() + { + Array value = Array.CreateInstance(typeof(uint), lengths: [2, 3], lowerBounds: [1, 2]); + value.SetValue(101u, 1, 2); + value.SetValue(102u, 1, 3); + value.SetValue(103u, 1, 4); + value.SetValue(201u, 2, 2); + value.SetValue(202u, 2, 3); + value.SetValue(203u, 2, 4); + + using BinaryFormatterScope scope = new(enable: true); + using BinaryFormatterInClipboardScope clipboardScope = new(enable: true); + ((Action)(() => RoundTripOfType(value))).Should().Throw(); + } + + [Fact] + public void RoundTripOfType_intNullableArray_CustomResolver() + { + int?[] value = [101, null, 303]; + + using BinaryFormatterScope scope = new(enable: true); + using BinaryFormatterInClipboardScope clipboardScope = new(enable: true); + RoundTripOfType(value, NullableIntArrayResolver).Should().BeEquivalentTo(value); + } + + private static Type NullableIntArrayResolver(TypeName typeName) + { + (string name, Type type)[] allowedTypes = + [ + ("System.Nullable`1[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(int?)) + ]; + + string fullName = typeName.FullName; + foreach (var (name, type) in allowedTypes) + { + // Namespace-qualified type name. + if (name == fullName) + { + return type; + } + } + + throw new NotSupportedException($"Can't resolve {fullName}"); + } + + [Fact] + public void RoundTripOfType_TestData_TestDataResolver() + { + TestData value = new(new(10, 10), 2); + + using BinaryFormatterScope scope = new(enable: true); + using BinaryFormatterInClipboardScope clipboardScope = new(enable: true); + + var result = RoundTripOfType(value, TestDataResolver).Should().BeOfType().Subject; + + result.Equals(value, value.Bitmap.Size); + } + + [Fact] + public void RoundTripOfType_TestData_InvalidResolver() + { + TestData value = new(new(10, 10), 2); + + using BinaryFormatterScope scope = new(enable: true); + using BinaryFormatterInClipboardScope clipboardScope = new(enable: true); + + WriteObjectToStream(value); + + // Resolver that returns a null is blocked in our SerializationBinder wrapper. + ((Action)(() => ReadObjectFromStream(InvalidResolver))).Should().Throw(); + } + + private static Type InvalidResolver(TypeName typeName) => null!; + + [Fact] + public void RoundTripOfType_Font_FontResolver() + { + using Font value = new("Microsoft Sans Serif", emSize: 10); + + using BinaryFormatterScope scope = new(enable: true); + using BinaryFormatterInClipboardScope clipboardScope = new(enable: true); + + using Font result = RoundTripOfType(value, FontResolver).Should().BeOfType().Subject; + result.Should().Be(value); + } + + private static Type FontResolver(TypeName typeName) + { + (string name, Type type)[] allowedTypes = + [ + (typeof(FontStyle).FullName!, typeof(FontStyle)), + (typeof(FontFamily).FullName!, typeof(FontFamily)), + (typeof(GraphicsUnit).FullName!, typeof(GraphicsUnit)), + ]; + + string fullName = typeName.FullName; + foreach (var (name, type) in allowedTypes) + { + // Namespace-qualified type name. + if (name == fullName) + { + return type; + } + } + + throw new NotSupportedException($"Can't resolve {fullName}"); + } + + [Fact] + public void RoundTripOfType_FlatData_DefaultResolver() + { + TestDataBase.InnerData value = new("simple class"); + + using BinaryFormatterScope scope = new(enable: true); + using BinaryFormatterInClipboardScope clipboardScope = new(enable: true); + + var result = RoundTripOfType(value).Should().BeOfType().Subject; + + result.Text.Should().Be("simple class"); + } + + [Serializable] + private class TestDataBase + { + public TestDataBase(Bitmap bitmap) + { + Bitmap = bitmap; + Inner = new InnerData("inner"); + } + + public Bitmap Bitmap; + public InnerData? Inner; + + [Serializable] + internal class InnerData + { + public InnerData(string text) + { + Text = text; + } + + public string Text; + } + } + + [Serializable] + private class TestData : TestDataBase + { + public TestData(Bitmap bitmap, int count) + : base(bitmap) + { + Count = count; + } + + // BinaryFormatter resolves primitive types or arrays of primitive types with no callback to the resolver. + public int? Count; + public DateTime? Today = DateTime.Now; + public float[] FloatArray = [1.0f, 2.0f, 3.0f]; + public TimeSpan[] TimeSpanArray = [TimeSpan.FromHours(1)]; + + // Common WinForms types are resolved using the binder based on the provided resolver. + public NotSupportedException Exception = new(); + public Point Point = new(1, 2); + public Rectangle Rectangle = new(1, 2, 3, 4); + public Size? Size = new(1, 2); + public SizeF SizeF = new(1, 2); + public Color Color = Color.Red; + public PointF PointF = new(1, 2); + public RectangleF RectangleF = new(1, 2, 3, 4); + public ImageListStreamer ImageList = new(new ImageList()); + + public List Bytes = new() { 1 }; + public List Sbytes = new() { 1 }; + public List Shorts = new() { 1 }; + public List Ushorts = new() { 1 }; + public List Ints = new() { 1, 2, 3 }; + public List Uints = new() { 1, 2, 3 }; + public List Longs = new() { 1, 2, 3 }; + public List Ulongs = new() { 1, 2, 3 }; + public List Floats = new() { 1.0f, 2.0f, 3.0f }; + public List Doubles = new() { 1.0, 2.0, 3.0 }; + public List Decimals = new() { 1.0m, 2.0m, 3.0m }; + public List DateTimes = new() { DateTime.Now }; + // System.Runtime.Serialization.SerializationException : Invalid BinaryFormatter stream. + // System.NotSupportedException : Can't resolve System.Collections.Generic.List`1[[System.TimeSpan, System.Private.CoreLib, Version=10.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]] + // Even though when serialized as a root record, TimeSpan is normalized to the framework assembly. + // public List TimeSpans = new() { TimeSpan.FromHours(1) }; + public List Strings = new() { "a", "b", "c" }; + + public void Equals(TestData other, Size bitmapSize) + { + Bitmap.Size.Should().Be(bitmapSize); + Inner.Should().BeEquivalentTo(other.Inner); + Count.Should().Be(other.Count); + Today.Should().Be(other.Today); + FloatArray.Should().BeEquivalentTo(other.FloatArray); + TimeSpanArray.Should().BeEquivalentTo(other.TimeSpanArray); + Exception.Should().BeEquivalentTo(other.Exception); + Point.Should().Be(other.Point); + Rectangle.Should().Be(other.Rectangle); + Size.Should().Be(other.Size); + SizeF.Should().Be(other.SizeF); + Color.Should().Be(other.Color); + PointF.Should().BeApproximately(other.PointF, Delta); + RectangleF.Should().BeApproximately(other.RectangleF, Delta); + using ImageList newList = new(); + newList.ImageStream = ImageList; + newList.Images.Count.Should().Be(0); + Bytes.Should().BeEquivalentTo(other.Bytes); + Sbytes.Should().BeEquivalentTo(other.Sbytes); + Shorts.Should().BeEquivalentTo(other.Shorts); + Ushorts.Should().BeEquivalentTo(other.Ushorts); + Ints.Should().BeEquivalentTo(other.Ints); + Uints.Should().BeEquivalentTo(other.Uints); + Longs.Should().BeEquivalentTo(other.Longs); + Ulongs.Should().BeEquivalentTo(other.Ulongs); + Floats.Should().BeEquivalentTo(other.Floats); + Doubles.Should().BeEquivalentTo(other.Doubles); + Decimals.Should().BeEquivalentTo(other.Decimals); + DateTimes.Should().BeEquivalentTo(other.DateTimes); + // TimeSpans.Should().BeEquivalentTo(other.TimeSpans); + Strings.Should().BeEquivalentTo(other.Strings); + } + } + + private const float Delta = 0.0003f; + + private static Type TestDataResolver(TypeName typeName) + { + (string name, Type type)[] allowedTypes = + [ + (typeof(TestData).FullName!, typeof(TestData)), + (typeof(TestDataBase.InnerData).FullName!, typeof(TestDataBase.InnerData)), + ]; + + string fullName = typeName.FullName; + foreach (var (name, type) in allowedTypes) + { + // Namespace-qualified type name. + if (name == fullName) + { + return type; + } + } + + throw new NotSupportedException($"Can't resolve {fullName}"); + } + + [Fact] + public void ReadFontSerializedOnNet481() + { + // This string was generated on net481. + // Clipboard.SetData("TestData", new Font("Arial", 12)); + // And the resulting stream was saved as a string + // string text = Convert.ToBase64String(stream.ToArray()); + string arielFont = + "AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJl" + + "PW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5G" + + "b250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAb" + + "U3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAABRNaWNyb3NvZnQgU2FucyBTZXJpZgAA" + + "IEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAAAAAAABfv///8bU3lz" + + "dGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAwAAAAs="; + + byte[] bytes = Convert.FromBase64String(arielFont); + using MemoryStream stream = new MemoryStream(bytes); + var result = Utilities.ReadObjectFromStream( + stream, + resolver: null, + restrictDeserialization: false, + legacyMode: true); + } } diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ClipboardTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ClipboardTests.cs index 3dc59552ed1..6b4c4fb5eaa 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ClipboardTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ClipboardTests.cs @@ -8,6 +8,7 @@ using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; +using System.Windows.Forms.Primitives; using Windows.Win32.System.Ole; using Com = Windows.Win32.System.Com; using ComTypes = System.Runtime.InteropServices.ComTypes; @@ -20,6 +21,8 @@ namespace System.Windows.Forms.Tests; [UISettings(MaxAttempts = 3)] // Try up to 3 times before failing. public class ClipboardTests { +#pragma warning disable WFDEV005 // Type or member is obsolete + [WinFormsFact] public void Clipboard_SetText_InvokeString_GetReturnsExpected() { @@ -424,7 +427,8 @@ public void Clipboard_SetImage_InvokeBitmap_GetReturnsExpected() using Bitmap bitmap = new(10, 10); bitmap.SetPixel(1, 2, Color.FromArgb(0x01, 0x02, 0x03, 0x04)); Clipboard.SetImage(bitmap); - Bitmap result = Assert.IsType(Clipboard.GetImage()); + + var result = Clipboard.GetImage().Should().BeOfType().Subject; result.Size.Should().Be(bitmap.Size); result.GetPixel(1, 2).Should().Be(Color.FromArgb(0xFF, 0xD2, 0xD2, 0xD2)); Clipboard.ContainsImage().Should().BeTrue(); @@ -433,19 +437,38 @@ public void Clipboard_SetImage_InvokeBitmap_GetReturnsExpected() [WinFormsFact] public void Clipboard_SetImage_InvokeMetafile_GetReturnsExpected() { - using Metafile metafile = new("bitmaps/telescope_01.wmf"); - Clipboard.SetImage(metafile); - Clipboard.GetImage().Should().BeNull(); - Clipboard.ContainsImage().Should().BeTrue(); + try + { + using Metafile metafile = new("bitmaps/telescope_01.wmf"); + using BinaryFormatterScope scope = new(enable: true); + // SetImage fails silently and corrupts the clipboard state for anything other than a bitmap. + Clipboard.SetImage(metafile); + + Clipboard.GetImage().Should().BeNull(); + Clipboard.ContainsImage().Should().BeTrue(); + } + finally + { + Clipboard.Clear(); + } } [WinFormsFact] public void Clipboard_SetImage_InvokeEnhancedMetafile_GetReturnsExpected() { - using Metafile metafile = new("bitmaps/milkmateya01.emf"); - Clipboard.SetImage(metafile); - Clipboard.GetImage().Should().BeNull(); - Clipboard.ContainsImage().Should().BeTrue(); + try + { + using Metafile metafile = new("bitmaps/milkmateya01.emf"); + // SetImage fails silently and corrupts the clipboard for everything other than a bitmap. + Clipboard.SetImage(metafile); + + Clipboard.GetImage().Should().BeNull(); + Clipboard.ContainsImage().Should().BeTrue(); + } + finally + { + Clipboard.Clear(); + } } [WinFormsFact] @@ -618,4 +641,88 @@ public unsafe void Clipboard_RawClipboard_SetClipboardData_ReturnsExpected() DataObject dataObject = Clipboard.GetDataObject().Should().BeOfType().Which; dataObject.GetData(DataFormats.Text).Should().Be(testString); } + + [WinFormsFact] + public void Clipboard_AppContextSwitch() + { + LocalAppContextSwitches.ClipboardDragDropEnableUnsafeBinaryFormatterSerialization.Should().BeFalse(); + + using (BinaryFormatterInClipboardScope scope = new(enable: true)) + { + LocalAppContextSwitches.ClipboardDragDropEnableUnsafeBinaryFormatterSerialization.Should().BeTrue(); + } + + LocalAppContextSwitches.ClipboardDragDropEnableUnsafeBinaryFormatterSerialization.Should().BeFalse(); + + using (BinaryFormatterInClipboardScope scope = new(enable: false)) + { + LocalAppContextSwitches.ClipboardDragDropEnableUnsafeBinaryFormatterSerialization.Should().BeFalse(); + } + } + + [WinFormsFact] + public void Clipboard_TryGetInt_ReturnsExpected() + { + int expected = 101; + using (BinaryFormatterScope scope = new(enable: true)) + { + Clipboard.SetData("TestData", expected); + } + + Clipboard.TryGetData("TestData", out int? data).Should().BeTrue(); + data.Should().Be(expected); + } + + [WinFormsFact] + public void Clipboard_TryGetTestData_ReturnsExpected() + { + DateTime date = DateTime.Now; + TestData expected = new(date); + using BinaryFormatterScope scope = new(enable: true); + using BinaryFormatterInClipboardScope clipboardScope = new(enable: true); + Clipboard.SetData("TestData", expected); + + Clipboard.TryGetData("TestData", out TestData? data).Should().BeTrue(); + var result = data.Should().BeOfType().Subject; + expected.Equals(result); + } + + [Serializable] + private class TestData + { + public TestData(DateTime dateTime) + { + _count = 2; + _dateTime = dateTime; + } + + private readonly int _count; + private readonly DateTime _dateTime; + + public void Equals(TestData actual) + { + _count.Should().Be(actual._count); + _dateTime.Should().Be(actual._dateTime); + } + } + + [WinFormsFact] + public void Clipboard_TryGetObject_Throws() + { + object expected = new(); + using BinaryFormatterScope scope = new(enable: true); + Clipboard.SetData("TestData", expected); + + ((Action)(() => Clipboard.TryGetData("TestData", null!, out object? data))).Should().Throw(); + } + + [WinFormsFact] + public void Clipboard_TryGetRectangleAsObject_Throws() + { + Rectangle expected = new(1, 1, 2, 2); + using BinaryFormatterScope scope = new(enable: true); + Clipboard.SetData("TestData", expected); + + ((Action)(() => Clipboard.TryGetData("TestData", null!, out object? data))).Should().Throw(); + } } diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectComTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectComTests.cs index 02c4b3a92d6..05c88b041df 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectComTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectComTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection.Metadata; using System.Runtime.InteropServices.ComTypes; using Com = Windows.Win32.System.Com; using IComDataObject = System.Runtime.InteropServices.ComTypes.IDataObject; @@ -53,6 +54,10 @@ private class CustomIDataObject : IDataObject public object GetData(string format, bool autoConvert) => throw new NotImplementedException(); public object GetData(string format) => throw new NotImplementedException(); public object GetData(Type format) => throw new NotImplementedException(); + public bool TryGetData(string format, Func resolver, bool autoConvert, out T data) => throw new NotImplementedException(); + public bool TryGetData(string format, bool autoConvert, out T data) => throw new NotImplementedException(); + public bool TryGetData(string format, out T data) => throw new NotImplementedException(); + public bool TryGetData(out T data) => throw new NotImplementedException(); public bool GetDataPresent(string format, bool autoConvert) => throw new NotImplementedException(); public bool GetDataPresent(string format) => throw new NotImplementedException(); public bool GetDataPresent(Type format) => throw new NotImplementedException(); diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectTests.ClipboardTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectTests.ClipboardTests.cs index 3cbb053e55a..e9d201ca9f2 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectTests.ClipboardTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectTests.ClipboardTests.cs @@ -9,8 +9,11 @@ namespace System.Windows.Forms.Tests; public partial class DataObjectTests { - [Collection("Sequential")] // Each registered Clipboard format is an OS singleton, - // and we should not run this test at the same time as other tests using the same format. +#pragma warning disable WFDEV005 // Type or member is obsolete + + // Each registered Clipboard format is an OS singleton, + // we should not run this test at the same time as other tests using the same format. + [Collection("Sequential")] [UISettings(MaxAttempts = 3)] // Try up to 3 times before failing. public class ClipboardTests { diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectTests.cs index b83564711eb..69dbef43d1e 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectTests.cs @@ -4,6 +4,7 @@ using System.Collections.Specialized; using System.ComponentModel; using System.Drawing; +using System.Reflection.Metadata; using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; using System.Runtime.Serialization; @@ -18,6 +19,7 @@ namespace System.Windows.Forms.Tests; // NB: doesn't require thread affinity public partial class DataObjectTests { + #pragma warning disable WFDEV005 // Type or member is obsolete private static readonly string[] s_restrictedClipboardFormats = [ DataFormats.CommaSeparatedValue, @@ -248,6 +250,8 @@ public void DataObject_GetData_InvokeStringDefault_ReturnsNull(string format) { DataObject dataObject = new(); dataObject.GetData(format).Should().BeNull(); + dataObject.TryGetData(format, out string text).Should().BeFalse(); + text.Should().BeNull(); } public static TheoryData GetData_InvokeStringMocked_TheoryData() @@ -363,6 +367,56 @@ public void DataObject_GetData_InvokeTypeMocked_CallsGetData(Type format, object mockDataObject.Verify(o => o.GetData(formatName), Times.Exactly(expectedCallCount)); } + [Fact] + public void DataObject_TryGetData_InvokeString_CallsTryGetData() + { + string data = "text"; + Mock mockDataObject = new(MockBehavior.Strict); + mockDataObject + .Setup(o => o.TryGetData(out data)) + .CallBase(); + string formatName = typeof(string).FullName!; + mockDataObject + .Setup(o => o.TryGetData(formatName, out data)) + .Returns(true) + .Verifiable(); + mockDataObject.Object.TryGetData(out data).Should().BeTrue(); + mockDataObject.Verify(o => o.TryGetData(formatName, out data), Times.Exactly(1)); + } + + [Fact] + public void DataObject_TryGetData_InvokeStringString_CallsTryGetData() + { + string data = "text"; + Mock mockDataObject = new(MockBehavior.Strict); + mockDataObject + .Setup(o => o.TryGetData("test format", out data)) + .CallBase(); + mockDataObject + .Setup(o => o.TryGetData("test format", false, out data)) + .Returns(true) + .Verifiable(); + mockDataObject.Object.TryGetData("test format", out data).Should().BeTrue(); + mockDataObject.Verify(o => o.TryGetData("test format", false, out data), Times.Exactly(1)); + } + + [Theory] + [BoolData] + public void DataObject_TryGetData_InvokeStringBoolString_CallsTryGetData(bool autoConvert) + { + string data = "text"; + Mock mockDataObject = new(MockBehavior.Strict); + mockDataObject + .Setup(o => o.TryGetData("test format", autoConvert, out data)) + .CallBase(); + mockDataObject + .Setup(o => o.TryGetData("test format", DataObject.NotSupportedResolver, autoConvert, out data)) + .Returns(true) + .Verifiable(); + mockDataObject.Object.TryGetData("test format", autoConvert, out data).Should().BeTrue(); + mockDataObject.Verify(o => o.TryGetData("test format", DataObject.NotSupportedResolver, autoConvert, out data), Times.Exactly(1)); + } + [Theory] [MemberData(nameof(GetData_String_TheoryData))] [MemberData(nameof(GetData_String_UnboundedFormat_TheoryData))] @@ -1011,6 +1065,71 @@ public void DataObject_SetData_NullData_ThrowsArgumentNullException() ((Action)(() => dataObject.SetData(null))).Should().Throw().WithParameterName("data"); } + public static TheoryData SetData_StringObject_TheoryData() + { + TheoryData theoryData = new(); + foreach (string format in s_restrictedClipboardFormats) + { + if (string.IsNullOrWhiteSpace(format) || format == typeof(Bitmap).FullName || format.StartsWith("FileName", StringComparison.Ordinal)) + { + continue; + } + + theoryData.Add(format, null, format == DataFormats.FileDrop, format == DataFormats.Bitmap); + theoryData.Add(format, "input", format == DataFormats.FileDrop, format == DataFormats.Bitmap); + } + + theoryData.Add(typeof(Bitmap).FullName, null, false, true); + theoryData.Add(typeof(Bitmap).FullName, "input", false, true); + + theoryData.Add("FileName", null, true, false); + theoryData.Add("FileName", "input", true, false); + + theoryData.Add("FileNameW", null, true, false); + theoryData.Add("FileNameW", "input", true, false); + + return theoryData; + } + + [Theory] + [MemberData(nameof(SetData_StringObject_TheoryData))] + private void DataObject_SetData_InvokeStringObject_GetReturnsExpected(string format, string input, bool expectedContainsFileDropList, bool expectedContainsImage) + { + DataObject dataObject = new(); + dataObject.SetData(format, input); + + dataObject.GetDataPresent(format).Should().BeTrue(); + dataObject.GetDataPresent(format, autoConvert: false).Should().BeTrue(); + dataObject.GetDataPresent(format, autoConvert: true).Should().BeTrue(); + + dataObject.GetData(format).Should().Be(input); + dataObject.GetData(format, autoConvert: false).Should().Be(input); + dataObject.GetData(format, autoConvert: true).Should().Be(input); + + dataObject.TryGetData(format, out object unboundedData).Should().Be(input is string); + unboundedData.Should().Be(input); + + dataObject.TryGetData(format, autoConvert: false, out unboundedData).Should().Be(input is string); + unboundedData.Should().Be(input); + dataObject.TryGetData(format, autoConvert: true, out unboundedData).Should().Be(input is string); + unboundedData.Should().Be(input); + + dataObject.TryGetData(format, resolver: null!, autoConvert: true, out unboundedData).Should().Be(input is string); + unboundedData.Should().Be(input); + dataObject.TryGetData(format, resolver: null!, autoConvert: false, out unboundedData).Should().Be(input is string); + unboundedData.Should().Be(input); + + dataObject.ContainsAudio().Should().Be(format == DataFormats.WaveAudio); + dataObject.ContainsFileDropList().Should().Be(expectedContainsFileDropList); + dataObject.ContainsImage().Should().Be(expectedContainsImage); + dataObject.ContainsText().Should().Be(format == DataFormats.UnicodeText); + dataObject.ContainsText(TextDataFormat.Text).Should().Be(format == DataFormats.UnicodeText); + dataObject.ContainsText(TextDataFormat.UnicodeText).Should().Be(format == DataFormats.UnicodeText); + dataObject.ContainsText(TextDataFormat.Rtf).Should().Be(format == DataFormats.Rtf); + dataObject.ContainsText(TextDataFormat.Html).Should().Be(format == DataFormats.Html); + dataObject.ContainsText(TextDataFormat.CommaSeparatedValue).Should().Be(format == DataFormats.CommaSeparatedValue); + } + [Theory] [InlineData(DataFormats.SerializableConstant, null)] [InlineData(DataFormats.SerializableConstant, "input")] @@ -1029,6 +1148,17 @@ private void DataObject_SetData_InvokeStringObject_Unbounded_GetReturnsExpected( dataObject.GetData(format, autoConvert: false).Should().Be(input); dataObject.GetData(format, autoConvert: true).Should().Be(input); + dataObject.TryGetData(format, out object unboundedData).Should().Be(input is string); + dataObject.TryGetData(format, autoConvert: false, out unboundedData).Should().Be(input is string); + dataObject.TryGetData(format, autoConvert: true, out unboundedData).Should().Be(input is string); + dataObject.TryGetData(format, resolver: null!, autoConvert: true, out unboundedData).Should().Be(input is string); + dataObject.TryGetData(format, resolver: null!, autoConvert: false, out unboundedData).Should().Be(input is string); + + dataObject.TryGetData(format, resolver: null!, autoConvert: false, out string data).Should().Be(input is not null); + data.Should().Be(input); + dataObject.TryGetData(format, resolver: null!, autoConvert: true, out data).Should().Be(input is not null); + data.Should().Be(input); + dataObject.ContainsAudio().Should().BeFalse(); dataObject.ContainsFileDropList().Should().BeFalse(); dataObject.ContainsImage().Should().BeFalse(); @@ -1089,6 +1219,79 @@ public void DataObject_SetData_InvokeStringObjectIDataObject_CallsSetData(string mockDataObject.Verify(o => o.SetData(format, data), Times.Once()); } + public static TheoryData SetData_StringBoolObject_TheoryData() + { + TheoryData theoryData = new(); + + foreach (string format in s_restrictedClipboardFormats) + { + if (string.IsNullOrWhiteSpace(format) || format == typeof(Bitmap).FullName || format.StartsWith("FileName", StringComparison.Ordinal)) + { + continue; + } + + foreach (bool autoConvert in new bool[] { true, false }) + { + theoryData.Add(format, autoConvert, null, format == DataFormats.FileDrop, format == DataFormats.Bitmap); + theoryData.Add(format, autoConvert, "input", format == DataFormats.FileDrop, format == DataFormats.Bitmap); + } + } + + theoryData.Add(typeof(Bitmap).FullName, false, null, false, false); + theoryData.Add(typeof(Bitmap).FullName, false, "input", false, false); + theoryData.Add(typeof(Bitmap).FullName, true, null, false, true); + theoryData.Add(typeof(Bitmap).FullName, true, "input", false, true); + + theoryData.Add("FileName", false, null, false, false); + theoryData.Add("FileName", false, "input", false, false); + theoryData.Add("FileName", true, null, true, false); + theoryData.Add("FileName", true, "input", true, false); + + theoryData.Add("FileNameW", false, null, false, false); + theoryData.Add("FileNameW", false, "input", false, false); + theoryData.Add("FileNameW", true, null, true, false); + theoryData.Add("FileNameW", true, "input", true, false); + + return theoryData; + } + + [Theory] + [MemberData(nameof(SetData_StringBoolObject_TheoryData))] + private void DataObject_SetData_InvokeStringBoolObject_GetReturnsExpected(string format, bool autoConvert, string input, bool expectedContainsFileDropList, bool expectedContainsImage) + { + DataObject dataObject = new(); + dataObject.SetData(format, autoConvert, input); + + dataObject.GetData(format, autoConvert: false).Should().Be(input); + dataObject.GetData(format, autoConvert: true).Should().Be(input); + + dataObject.TryGetData(format, out object unboundedData).Should().Be(input is string); + unboundedData.Should().Be(input); + + dataObject.TryGetData(format, autoConvert: false, out unboundedData).Should().Be(input is string); + unboundedData.Should().Be(input); + dataObject.TryGetData(format, autoConvert: true, out unboundedData).Should().Be(input is string); + unboundedData.Should().Be(input); + + dataObject.TryGetData(format, resolver: null!, autoConvert: false, out unboundedData).Should().Be(input is string); + unboundedData.Should().Be(input); + dataObject.TryGetData(format, resolver: null!, autoConvert: true, out unboundedData).Should().Be(input is string); + unboundedData.Should().Be(input); + + dataObject.GetDataPresent(format, autoConvert: true).Should().BeTrue(); + dataObject.GetDataPresent(format, autoConvert: false).Should().BeTrue(); + + dataObject.ContainsAudio().Should().Be(format == DataFormats.WaveAudio); + dataObject.ContainsFileDropList().Should().Be(expectedContainsFileDropList); + dataObject.ContainsImage().Should().Be(expectedContainsImage); + dataObject.ContainsText().Should().Be(format == DataFormats.UnicodeText); + dataObject.ContainsText(TextDataFormat.Text).Should().Be(format == DataFormats.UnicodeText); + dataObject.ContainsText(TextDataFormat.UnicodeText).Should().Be(format == DataFormats.UnicodeText); + dataObject.ContainsText(TextDataFormat.Rtf).Should().Be(format == DataFormats.Rtf); + dataObject.ContainsText(TextDataFormat.Html).Should().Be(format == DataFormats.Html); + dataObject.ContainsText(TextDataFormat.CommaSeparatedValue).Should().Be(format == DataFormats.CommaSeparatedValue); + } + [Theory] [InlineData("something custom", false, "input")] [InlineData("something custom", false, null)] @@ -1098,7 +1301,7 @@ public void DataObject_SetData_InvokeStringObjectIDataObject_CallsSetData(string [InlineData(DataFormats.SerializableConstant, false, null)] [InlineData(DataFormats.SerializableConstant, true, "input")] [InlineData(DataFormats.SerializableConstant, true, null)] - private void DataObject_SetData_InvokeStringBoolObject_Unbounded_GetReturnsExpected(string format, bool autoConvert, string input) + private void DataObject_SetData_InvokeStringBoolObject_Unbounded(string format, bool autoConvert, string input) { DataObject dataObject = new(); dataObject.SetData(format, autoConvert, input); @@ -1106,6 +1309,19 @@ private void DataObject_SetData_InvokeStringBoolObject_Unbounded_GetReturnsExpec dataObject.GetData(format, autoConvert: false).Should().Be(input); dataObject.GetData(format, autoConvert: true).Should().Be(input); + dataObject.TryGetData(format, out string data).Should().Be(input is string); + data.Should().Be(input); + + dataObject.TryGetData(format, autoConvert: false, out data).Should().Be(input is string); + data.Should().Be(input); + dataObject.TryGetData(format, autoConvert: true, out data).Should().Be(input is string); + data.Should().Be(input); + + dataObject.TryGetData(format, resolver: null!, autoConvert: false, out data).Should().Be(input is string); + data.Should().Be(input); + dataObject.TryGetData(format, resolver: null!, autoConvert: true, out data).Should().Be(input is string); + data.Should().Be(input); + dataObject.GetDataPresent(format, autoConvert: true).Should().BeTrue(); dataObject.GetDataPresent(format, autoConvert: false).Should().BeTrue(); @@ -2395,6 +2611,10 @@ private class CustomDataObject : IComDataObject, IDataObject public object GetData(string format, bool autoConvert) => throw new NotImplementedException(); public object GetData(string format) => throw new NotImplementedException(); public object GetData(Type format) => throw new NotImplementedException(); + public bool TryGetData(string format, Func resolver, bool autoConvert, out T data) => throw new NotImplementedException(); + public bool TryGetData(string format, bool autoConvert, out T data) => throw new NotImplementedException(); + public bool TryGetData(string format, out T data) => throw new NotImplementedException(); + public bool TryGetData(out T data) => throw new NotImplementedException(); public void GetDataHere(ref FORMATETC format, ref STGMEDIUM medium) => throw new NotImplementedException(); public bool GetDataPresent(string format, bool autoConvert) => throw new NotImplementedException(); public bool GetDataPresent(string format) => throw new NotImplementedException(); diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DragDropHelperTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DragDropHelperTests.cs index 4d7752ca9f0..efd63840fda 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DragDropHelperTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DragDropHelperTests.cs @@ -108,16 +108,18 @@ public unsafe void SetDragImage_DataObject_Bitmap_Point_bool_ReturnsExpected(Dat try { DragDropHelper.SetDragImage(dataObject, dragImage, cursorOffset, useDefaultDragImage); - DragDropFormat dragDropFormat = (DragDropFormat)dataObject.GetData(DragDropHelper.DRAGIMAGEBITS); + // This DataObject is backed up by the DataStore. + Assert.True(dataObject.TryGetData(DragDropHelper.DRAGIMAGEBITS, out DragDropFormat dragDropFormat)); + Assert.NotNull(dragDropFormat); void* basePtr = PInvokeCore.GlobalLock(dragDropFormat.Medium.hGlobal); SHDRAGIMAGE* pDragImage = (SHDRAGIMAGE*)basePtr; bool isDragImageNull = BitOperations.LeadingZeroCount((uint)(nint)pDragImage->hbmpDragImage).Equals(32); Size dragImageSize = pDragImage->sizeDragImage; Point offset = pDragImage->ptOffset; PInvokeCore.GlobalUnlock(dragDropFormat.Medium.hGlobal); - Assert.Equal(dragImage is null, isDragImageNull); - Assert.Equal(dragImage is null ? new Size(0, 0) : dragImage.Size, dragImageSize); - Assert.Equal(cursorOffset, offset); + (dragImage is null).Should().Be(isDragImageNull); + (dragImage is null ? new Size(0, 0) : dragImage.Size).Should().Be(dragImageSize); + cursorOffset.Should().Be(offset); } finally { @@ -132,16 +134,18 @@ public unsafe void SetDragImage_DataObject_GiveFeedbackEventArgs_ReturnsExpected try { DragDropHelper.SetDragImage(dataObject, e); - DragDropFormat dragDropFormat = (DragDropFormat)dataObject.GetData(DragDropHelper.DRAGIMAGEBITS); + // This DataObject is backed up by the DataStore. + dataObject.TryGetData(DragDropHelper.DRAGIMAGEBITS, out DragDropFormat dragDropFormat).Should().BeTrue(); + dragDropFormat.Should().NotBeNull(); void* basePtr = PInvokeCore.GlobalLock(dragDropFormat.Medium.hGlobal); SHDRAGIMAGE* pDragImage = (SHDRAGIMAGE*)basePtr; bool isDragImageNull = BitOperations.LeadingZeroCount((uint)(nint)pDragImage->hbmpDragImage).Equals(32); Size dragImageSize = pDragImage->sizeDragImage; Point offset = pDragImage->ptOffset; PInvokeCore.GlobalUnlock(dragDropFormat.Medium.hGlobal); - Assert.Equal(e.DragImage is null, isDragImageNull); - Assert.Equal(e.DragImage is null ? new Size(0, 0) : e.DragImage.Size, dragImageSize); - Assert.Equal(e.CursorOffset, offset); + (e.DragImage is null).Should().Be(isDragImageNull); + (e.DragImage is null ? new Size(0, 0) : e.DragImage.Size).Should().Be(dragImageSize); + e.CursorOffset.Should().Be(offset); } finally { @@ -153,15 +157,17 @@ public unsafe void SetDragImage_DataObject_GiveFeedbackEventArgs_ReturnsExpected public void SetDragImage_NonSTAThread_ThrowsInvalidOperationException() { Control.CheckForIllegalCrossThreadCalls = true; - Assert.Throws(() => DragDropHelper.SetDragImage(new DataObject(), new Bitmap(1, 1), new Point(0, 0), false)); + using Bitmap bitmap = new(1, 1); + Assert.Throws(() => DragDropHelper.SetDragImage(new DataObject(), bitmap, new Point(0, 0), false)); } [Fact] public void SetDragImage_NullDataObject_ThrowsArgumentNullException() { DataObject dataObject = null; + using Bitmap bitmap = new(1, 1); Assert.Throws(nameof(dataObject), - () => DragDropHelper.SetDragImage(dataObject, new Bitmap(1, 1), new Point(0, 0), false)); + () => DragDropHelper.SetDragImage(dataObject, bitmap, new Point(0, 0), false)); } [Fact] @@ -179,16 +185,17 @@ public unsafe void SetDropDescription_ClearDropDescription_ReturnsExpected(DataO { DragDropHelper.SetDropDescription(dataObject, dropImageType, message, messageReplacementToken); DragDropHelper.ClearDropDescription(dataObject); - DragDropFormat dragDropFormat = (DragDropFormat)dataObject.GetData(PInvoke.CFSTR_DROPDESCRIPTION); + dataObject.TryGetData(PInvoke.CFSTR_DROPDESCRIPTION, autoConvert: false, out DragDropFormat dragDropFormat).Should().BeTrue(); + dragDropFormat.Should().NotBeNull(); void* basePtr = PInvokeCore.GlobalLock(dragDropFormat.Medium.hGlobal); DROPDESCRIPTION* pDropDescription = (DROPDESCRIPTION*)basePtr; DROPIMAGETYPE type = pDropDescription->type; string szMessage = pDropDescription->szMessage.ToString(); string szInsert = pDropDescription->szInsert.ToString(); PInvokeCore.GlobalUnlock(dragDropFormat.Medium.hGlobal); - Assert.Equal(DROPIMAGETYPE.DROPIMAGE_INVALID, type); - Assert.Equal(string.Empty, szMessage); - Assert.Equal(string.Empty, szInsert); + type.Should().Be(DROPIMAGETYPE.DROPIMAGE_INVALID); + szMessage.Should().Be(string.Empty); + szInsert.Should().Be(string.Empty); } finally { @@ -245,9 +252,8 @@ public unsafe void SetDropDescription_ReleaseDragDropFormats_ReturnsExpected(Dat foreach (string format in dataObject.GetFormats()) { - if (dataObject.GetData(format) is DragDropFormat dragDropFormat) + if (dataObject.TryGetData(format, out DragDropFormat dragDropFormat)) { - Assert.Equal(0, (int)PInvokeCore.GlobalSize(dragDropFormat.Medium.hGlobal)); Assert.Equal(nint.Zero, (nint)dragDropFormat.Medium.pUnkForRelease); Assert.Equal(Com.TYMED.TYMED_NULL, dragDropFormat.Medium.tymed); Assert.Equal(nint.Zero, (nint)dragDropFormat.Medium.hGlobal); @@ -262,7 +268,7 @@ public unsafe void SetDropDescription_DragEventArgs_ReturnsExpected(DragEventArg try { DragDropHelper.SetDropDescription(e); - DragDropFormat dragDropFormat = (DragDropFormat)e.Data.GetData(PInvoke.CFSTR_DROPDESCRIPTION); + e.Data.TryGetData(PInvoke.CFSTR_DROPDESCRIPTION, out DragDropFormat dragDropFormat).Should().BeTrue(); void* basePtr = PInvokeCore.GlobalLock(dragDropFormat.Medium.hGlobal); DROPDESCRIPTION* pDropDescription = (DROPDESCRIPTION*)basePtr; DROPIMAGETYPE type = pDropDescription->type; @@ -289,7 +295,7 @@ public unsafe void SetDropDescription_DataObject_DropImageType_string_string_Ret try { DragDropHelper.SetDropDescription(dataObject, dropImageType, message, messageReplacementToken); - DragDropFormat dragDropFormat = (DragDropFormat)dataObject.GetData(PInvoke.CFSTR_DROPDESCRIPTION); + dataObject.TryGetData(PInvoke.CFSTR_DROPDESCRIPTION, autoConvert: false, out DragDropFormat dragDropFormat).Should().BeTrue(); void* basePtr = PInvokeCore.GlobalLock(dragDropFormat.Medium.hGlobal); DROPDESCRIPTION* pDropDescription = (DROPDESCRIPTION*)basePtr; DROPIMAGETYPE type = pDropDescription->type; @@ -320,7 +326,7 @@ public unsafe void SetInDragLoop_ReturnsExpected(DataObject dataObject, bool inD try { DragDropHelper.SetInDragLoop(dataObject, inDragLoop); - DragDropFormat dragDropFormat = (DragDropFormat)dataObject.GetData(PInvoke.CFSTR_INDRAGLOOP); + dataObject.TryGetData(PInvoke.CFSTR_INDRAGLOOP, out DragDropFormat dragDropFormat).Should().BeTrue(); void* basePtr = PInvokeCore.GlobalLock(dragDropFormat.Medium.hGlobal); bool inShellDragLoop = (basePtr is not null) && (*(BOOL*)basePtr == true); PInvokeCore.GlobalUnlock(dragDropFormat.Medium.hGlobal); diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DragEventArgsTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DragEventArgsTests.cs index a7ad2ba6475..cf84c21505d 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DragEventArgsTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DragEventArgsTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection.Metadata; + namespace System.Windows.Forms.Tests; // NB: doesn't require thread affinity @@ -99,27 +101,20 @@ public void MessageReplacementToken_Set_GetReturnsExpected(string value) private class CustomDataObject : IDataObject { public object GetData(string format, bool autoConvert) => throw new NotImplementedException(); - public object GetData(string format) => throw new NotImplementedException(); - public object GetData(Type format) => throw new NotImplementedException(); - + public bool TryGetData(string format, Func resolver, bool autoConvert, out T data) => throw new NotImplementedException(); + public bool TryGetData(string format, bool autoConvert, out T data) => throw new NotImplementedException(); + public bool TryGetData(string format, out T data) => throw new NotImplementedException(); + public bool TryGetData(out T data) => throw new NotImplementedException(); public void SetData(string format, bool autoConvert, object data) => throw new NotImplementedException(); - public void SetData(string format, object data) => throw new NotImplementedException(); - public void SetData(Type format, object data) => throw new NotImplementedException(); - public void SetData(object data) => throw new NotImplementedException(); - public bool GetDataPresent(string format, bool autoConvert) => throw new NotImplementedException(); - public bool GetDataPresent(string format) => throw new NotImplementedException(); - public bool GetDataPresent(Type format) => throw new NotImplementedException(); - public string[] GetFormats(bool autoConvert) => throw new NotImplementedException(); - public string[] GetFormats() => throw new NotImplementedException(); } } diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/IDataObjectTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/IDataObjectTests.cs new file mode 100644 index 00000000000..db04a6e318d --- /dev/null +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/IDataObjectTests.cs @@ -0,0 +1,146 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection.Metadata; + +namespace System.Windows.Forms.Tests; + +#nullable enable + +// Test default implementation of IDataObject.TryGetData overloads. +public partial class IDataObjectTests +{ + [Fact] + public void IDataObject_TryGetData_Invoke_ReturnsFalse() + { + DefaultTryGetMethodsDataObject dataObject = new(); + ((IDataObject)dataObject).TryGetData(out string? _).Should().BeFalse(); + dataObject.GetDataType_Count.Should().Be(1); + dataObject.GetDataString_Count.Should().Be(0); + dataObject.GetDataStringBool_Count.Should().Be(0); + } + + [Fact] + public void IDataObject_TryGetData_InvokeString_ReturnsFalse() + { + DefaultTryGetMethodsDataObject dataObject = new(); + ((IDataObject)dataObject).TryGetData("TryGetDataString", out string? _).Should().BeFalse(); + dataObject.GetDataType_Count.Should().Be(0); + dataObject.GetDataString_Count.Should().Be(1); + dataObject.GetDataStringBool_Count.Should().Be(0); + } + + [Theory] + [BoolData] + public void IDataObject_TryGetData_InvokeStringBool_ReturnsFalse(bool autoConvert) + { + DefaultTryGetMethodsDataObject dataObject = new(); + ((IDataObject)dataObject).TryGetData("TryGetDataStringBool", autoConvert, out string? _).Should().BeFalse(); + dataObject.GetDataType_Count.Should().Be(0); + dataObject.GetDataString_Count.Should().Be(0); + dataObject.GetDataStringBool_Count.Should().Be(1); + } + + [Theory] + [BoolData] + public void IDataObject_TryGetData_InvokeStringResolverBool_ReturnsFalse(bool autoConvert) + { + DefaultTryGetMethodsDataObject dataObject = new(); + ((IDataObject)dataObject).TryGetData("TryGetDataStringBool", Resolver, autoConvert, out string? _).Should().BeFalse(); + dataObject.GetDataType_Count.Should().Be(0); + dataObject.GetDataString_Count.Should().Be(0); + dataObject.GetDataStringBool_Count.Should().Be(1); + } + + [Fact] + public void IDataObject_TryGetData_Invoke_ReturnsTrue() + { + DefaultTryGetMethodsDataObject dataObject = new(); + ((IDataObject)dataObject).TryGetData(out TryGetData1? _).Should().BeTrue(); + dataObject.GetDataType_Count.Should().Be(1); + dataObject.GetDataString_Count.Should().Be(0); + dataObject.GetDataStringBool_Count.Should().Be(0); + } + + [Fact] + public void IDataObject_TryGetData_InvokeString_ReturnsTrue() + { + DefaultTryGetMethodsDataObject dataObject = new(); + ((IDataObject)dataObject).TryGetData("TryGetDataString", out TryGetDataString? _).Should().BeTrue(); + dataObject.GetDataType_Count.Should().Be(0); + dataObject.GetDataString_Count.Should().Be(1); + dataObject.GetDataStringBool_Count.Should().Be(0); + } + + [Theory] + [BoolData] + public void IDataObject_TryGetData_InvokeStringBool_ReturnsTrue(bool autoConvert) + { + DefaultTryGetMethodsDataObject dataObject = new(); + ((IDataObject)dataObject).TryGetData("TryGetDataStringBool", autoConvert, out TryGetDataStringBool? _).Should().BeTrue(); + dataObject.GetDataType_Count.Should().Be(0); + dataObject.GetDataString_Count.Should().Be(0); + dataObject.GetDataStringBool_Count.Should().Be(1); + } + + [Theory] + [BoolData] + public void IDataObject_TryGetData_InvokeStringResolverBool_ReturnsTrue(bool autoConvert) + { + DefaultTryGetMethodsDataObject dataObject = new(); + ((IDataObject)dataObject).TryGetData("TryGetDataStringBool", Resolver, autoConvert, out TryGetDataStringBool? _).Should().BeTrue(); + dataObject.GetDataType_Count.Should().Be(0); + dataObject.GetDataString_Count.Should().Be(0); + dataObject.GetDataStringBool_Count.Should().Be(1); + } + + private static Type Resolver(TypeName typeName) => throw new NotImplementedException(); + + internal class TryGetData1() { } + private class TryGetDataString() { } + private class TryGetDataStringBool() { } + + internal class DefaultTryGetMethodsDataObject : IDataObject + { + internal int GetDataStringBool_Count { get; set; } + /// + /// Invoked from + /// and from . + /// + public object? GetData(string format, bool autoConvert) + { + GetDataStringBool_Count++; + return format == "TryGetDataStringBool" ? new TryGetDataStringBool() : null; + } + + internal int GetDataString_Count { get; set; } + /// + /// Invoked from + /// + public object? GetData(string format) + { + GetDataString_Count++; + return format == "TryGetDataString" ? new TryGetDataString() : null; + } + + internal int GetDataType_Count { get; set; } + /// + /// Invoked from + /// + public object? GetData(Type format) + { + GetDataType_Count++; + return format == typeof(TryGetData1) ? new TryGetData1() : null; + } + + public bool GetDataPresent(string format, bool autoConvert) => throw new NotImplementedException(); + public bool GetDataPresent(string format) => throw new NotImplementedException(); + public bool GetDataPresent(Type format) => throw new NotImplementedException(); + public string[] GetFormats(bool autoConvert) => throw new NotImplementedException(); + public string[] GetFormats() => throw new NotImplementedException(); + public void SetData(string format, bool autoConvert, object? data) => throw new NotImplementedException(); + public void SetData(string format, object? data) => throw new NotImplementedException(); + public void SetData(Type format, object? data) => throw new NotImplementedException(); + public void SetData(object? data) => throw new NotImplementedException(); + } +} diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/NativeToWinFormsAdapterTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/NativeToWinFormsAdapterTests.cs new file mode 100644 index 00000000000..734d11c9dea --- /dev/null +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/NativeToWinFormsAdapterTests.cs @@ -0,0 +1,169 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +#nullable enable + +using System.Drawing; +using System.Reflection.Metadata; +using Com = Windows.Win32.System.Com; + +namespace System.Windows.Forms.Tests; + +public unsafe class NativeToWinFormsAdapterTests +{ + private static readonly string[] s_restrictedClipboardFormats = + [ + DataFormats.CommaSeparatedValue, + DataFormats.Dib, + DataFormats.Dif, + DataFormats.PenData, + DataFormats.Riff, + DataFormats.Tiff, + DataFormats.WaveAudio, + DataFormats.SymbolicLink, + DataFormats.StringFormat, + DataFormats.Bitmap, + DataFormats.EnhancedMetafile, + DataFormats.FileDrop, + DataFormats.Html, + DataFormats.MetafilePict, + DataFormats.OemText, + DataFormats.Palette, + DataFormats.Rtf, + DataFormats.Text, + DataFormats.UnicodeText, + "FileName", + "FileNameW", + "System.Drawing.Bitmap" + ]; + + private static readonly string[] s_unboundedClipboardFormats = + [ + DataFormats.Serializable, + "something custom" + ]; + + public static TheoryData RestrictedFormat_TheoryData() => s_restrictedClipboardFormats.ToTheoryData(); + + public static TheoryData UnboundedFormat_TheoryData() => s_unboundedClipboardFormats.ToTheoryData(); + + [Serializable] + private class TestData : AbstractBase + { + public TestData(Point point) + { + Location = point; + Inner = new InnerData("inner"); + } + + public Point Location; + public InnerData? Inner; + + public override void DoStuff() { } + + [Serializable] + internal class InnerData + { + public InnerData(string text) + { + Text = text; + } + + public string Text; + } + } + + internal abstract class AbstractBase + { + public abstract void DoStuff(); + } + + private static Type TestDataResolver(TypeName typeName) + { + (string name, Type type)[] allowedTypes = + [ + (typeof(TestData.InnerData).FullName!, typeof(TestData.InnerData)), + (typeof(AbstractBase).FullName!, typeof(AbstractBase)), + ]; + + string fullName = typeName.FullName; + foreach (var (name, type) in allowedTypes) + { + // Namespace-qualified type name. + if (name == fullName) + { + return type; + } + } + + throw new NotSupportedException($"Can't resolve {fullName}"); + } + + [WinFormsTheory] + [MemberData(nameof(UnboundedFormat_TheoryData))] + [MemberData(nameof(RestrictedFormat_TheoryData))] + public void TryGetData_AsObject_Fail(string format) + { + DataObject native = new(); + native.SetData(format, 1); + DataObject dataObject = new(ComHelpers.GetComPointer(native)); + + dataObject.TryGetData(format, out object? value).Should().BeFalse(); + value.Should().BeNull(); + } + + [WinFormsTheory] + [MemberData(nameof(UnboundedFormat_TheoryData))] + [MemberData(nameof(RestrictedFormat_TheoryData))] + public void TryGetData_AsInterface_Fail(string format) + { + DataObject native = new(); + native.SetData(format, new List() { 1 }); + DataObject dataObject = new(ComHelpers.GetComPointer(native)); + + dataObject.TryGetData(format, out IList? list).Should().BeFalse(); + list.Should().BeNull(); + } + + [WinFormsTheory] + [MemberData(nameof(UnboundedFormat_TheoryData))] + public void TryGetData_AsConcreteType(string format) + { + DataObject native = new(); + List value = new() { 1 }; + native.SetData(format, value); + DataObject dataObject = new(ComHelpers.GetComPointer(native)); + + dataObject.TryGetData(format, out List? list).Should().BeTrue(); + list.Should().BeEquivalentTo(value); + } + + [WinFormsTheory] + [MemberData(nameof(UnboundedFormat_TheoryData))] + [MemberData(nameof(RestrictedFormat_TheoryData))] + public void TryGetData_AsAbstract_Fail(string format) + { + DataObject native = new(); + using BinaryFormatterScope scope = new(enable: true); + native.SetData(format, new TestData(Point.Empty)); + DataObject dataObject = new(ComHelpers.GetComPointer(native)); + + dataObject.TryGetData(format, out AbstractBase? testData).Should().BeFalse(); + testData.Should().BeNull(); + } + + [WinFormsTheory] + [MemberData(nameof(RestrictedFormat_TheoryData))] + public void TryGetData_AsAbstractWithResolver_Fail(string format) + { + DataObject native = new(); + TestData value = new(Point.Empty); + using BinaryFormatterScope scope = new(enable: true); + native.SetData(format, value); + DataObject dataObject = new(ComHelpers.GetComPointer(native)); + + // This fails either in validation of restricted format against the requested type + // or in deserialization because restricted format is not letting through custom types. + dataObject.TryGetData(format, TestDataResolver, autoConvert: true, out AbstractBase? testData).Should().BeFalse(); + testData.Should().BeNull(); + } +} diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/RichTextBoxTests.ClipboardTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/RichTextBoxTests.ClipboardTests.cs index bd1df35e346..99aef11a35d 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/RichTextBoxTests.ClipboardTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/RichTextBoxTests.ClipboardTests.cs @@ -20,7 +20,9 @@ public void RichTextBox_OleObject_IncompleteOleObject_DoNothing() using MemoryStream memoryStream = new(); using Bitmap bitmap = new(100, 100); bitmap.Save(memoryStream, Drawing.Imaging.ImageFormat.Png); +#pragma warning disable WFDEV005 // Type or member is obsolete Clipboard.SetData("Embed Source", memoryStream); +#pragma warning restore WFDEV005 control.Text.Should().BeEmpty(); } diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ToolStripItemTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ToolStripItemTests.cs index 8d9fd409138..3626829c576 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ToolStripItemTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ToolStripItemTests.cs @@ -8,6 +8,7 @@ using System.Windows.Forms.TestUtilities; using IComDataObject = System.Runtime.InteropServices.ComTypes.IDataObject; using Size = System.Drawing.Size; +using System.Reflection.Metadata; namespace System.Windows.Forms.Tests; @@ -10238,28 +10239,21 @@ public void ToolStripItem_DoDragDrop_NullData_ThrowsArgumentNullException() private class CustomDataObject : IDataObject { public object GetData(string format, bool autoConvert) => throw new NotImplementedException(); - public object GetData(string format) => throw new NotImplementedException(); - public object GetData(Type format) => throw new NotImplementedException(); - + public bool TryGetData(string format, Func resolver, bool autoConvert, out T data) => throw new NotImplementedException(); + public bool TryGetData(string format, bool autoConvert, out T data) => throw new NotImplementedException(); + public bool TryGetData(string format, out T data) => throw new NotImplementedException(); + public bool TryGetData(out T data) => throw new NotImplementedException(); + public void SetData(string format, bool autoConvert, object data) => throw new NotImplementedException(); + public void SetData(string format, object data) => throw new NotImplementedException(); + public void SetData(Type format, object data) => throw new NotImplementedException(); + public void SetData(object data) => throw new NotImplementedException(); public bool GetDataPresent(string format, bool autoConvert) => throw new NotImplementedException(); - public bool GetDataPresent(string format) => throw new NotImplementedException(); - public bool GetDataPresent(Type format) => throw new NotImplementedException(); - public string[] GetFormats(bool autoConvert) => throw new NotImplementedException(); - public string[] GetFormats() => throw new NotImplementedException(); - - public void SetData(string format, bool autoConvert, object data) => throw new NotImplementedException(); - - public void SetData(string format, object data) => throw new NotImplementedException(); - - public void SetData(Type format, object data) => throw new NotImplementedException(); - - public void SetData(object data) => throw new NotImplementedException(); } private class CustomComDataObject : IComDataObject From ec31e6cf12d70783e6b74eab9d3312ed5395eb92 Mon Sep 17 00:00:00 2001 From: Tanya Solyanik Date: Wed, 6 Nov 2024 23:48:29 -0800 Subject: [PATCH 2/8] switch name --- src/Common/tests/TestUtilities/AppContextSwitchNames.cs | 2 +- .../System/LocalAppContextSwitches/LocalAppContextSwitches.cs | 2 +- .../WinformsControlsTest/runtimeconfig.template.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Common/tests/TestUtilities/AppContextSwitchNames.cs b/src/Common/tests/TestUtilities/AppContextSwitchNames.cs index 1665e3f47fc..1db4cbb89a5 100644 --- a/src/Common/tests/TestUtilities/AppContextSwitchNames.cs +++ b/src/Common/tests/TestUtilities/AppContextSwitchNames.cs @@ -23,5 +23,5 @@ public const string LocalAppContext_DisableCaching /// The switch that controls whether or not the is enabled in the Clipboard. /// public const string ClipboardDragDropEnableUnsafeBinaryFormatterSerializationSwitchName - = "ClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization"; + = "Windows.ClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization"; } diff --git a/src/System.Windows.Forms.Primitives/src/System/LocalAppContextSwitches/LocalAppContextSwitches.cs b/src/System.Windows.Forms.Primitives/src/System/LocalAppContextSwitches/LocalAppContextSwitches.cs index d310427cd01..f78faf2830f 100644 --- a/src/System.Windows.Forms.Primitives/src/System/LocalAppContextSwitches/LocalAppContextSwitches.cs +++ b/src/System.Windows.Forms.Primitives/src/System/LocalAppContextSwitches/LocalAppContextSwitches.cs @@ -26,7 +26,7 @@ internal static partial class LocalAppContextSwitches internal const string NoClientNotificationsSwitchName = "Switch.System.Windows.Forms.AccessibleObject.NoClientNotifications"; internal const string EnableMsoComponentManagerSwitchName = "Switch.System.Windows.Forms.EnableMsoComponentManager"; internal const string TreeNodeCollectionAddRangeRespectsSortOrderSwitchName = "System.Windows.Forms.TreeNodeCollectionAddRangeRespectsSortOrder"; - internal const string ClipboardDragDropEnableUnsafeBinaryFormatterSerializationSwitchName = "ClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization"; + internal const string ClipboardDragDropEnableUnsafeBinaryFormatterSerializationSwitchName = "Windows.ClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization"; private static int s_scaleTopLevelFormMinMaxSizeForDpi; private static int s_anchorLayoutV2; diff --git a/src/System.Windows.Forms/tests/IntegrationTests/WinformsControlsTest/runtimeconfig.template.json b/src/System.Windows.Forms/tests/IntegrationTests/WinformsControlsTest/runtimeconfig.template.json index 722ffde3076..7ce60692b73 100644 --- a/src/System.Windows.Forms/tests/IntegrationTests/WinformsControlsTest/runtimeconfig.template.json +++ b/src/System.Windows.Forms/tests/IntegrationTests/WinformsControlsTest/runtimeconfig.template.json @@ -1,7 +1,7 @@ { "configProperties": { "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false, - "ClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization": false, + "Windows.ClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization": false, "System.Windows.Forms.AnchorLayoutV2": false, "System.Windows.Forms.ApplyParentFontToMenus": true, "System.Windows.Forms.DataGridViewUIAStartRowCountAtZero": false, From 0d14b74eb2c5cbd6d97b21c74ab1bbb2a3217059 Mon Sep 17 00:00:00 2001 From: Tanya Solyanik Date: Wed, 6 Nov 2024 23:53:12 -0800 Subject: [PATCH 3/8] don't hide obsoleted methods from the intellisense --- .../src/Microsoft/VisualBasic/MyServices/ClipboardProxy.vb | 1 - .../src/System/Windows/Forms/OLE/Clipboard.cs | 2 -- .../src/System/Windows/Forms/OLE/DataObject.cs | 4 ---- 3 files changed, 7 deletions(-) diff --git a/src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/MyServices/ClipboardProxy.vb b/src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/MyServices/ClipboardProxy.vb index 7c6f6e50852..6187bc12fa5 100644 --- a/src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/MyServices/ClipboardProxy.vb +++ b/src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/MyServices/ClipboardProxy.vb @@ -100,7 +100,6 @@ Namespace Microsoft.VisualBasic.MyServices False, DiagnosticId:=Obsoletions.ClipboardGetDataDiagnosticId, UrlFormat:=Obsoletions.SharedUrlFormat)> - Public Function GetData(format As String) As Object #Disable Warning WFDEV005 ' Type or member is obsolete Return Clipboard.GetData(format) diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/Clipboard.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/Clipboard.cs index 9ee0b1e913d..34b07b53c9a 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/Clipboard.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/Clipboard.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Specialized; -using System.ComponentModel; using System.Drawing; using System.Formats.Nrbf; using System.Reflection.Metadata; @@ -236,7 +235,6 @@ public static bool ContainsText(TextDataFormat format) error: false, DiagnosticId = Obsoletions.ClipboardGetDataDiagnosticId, UrlFormat = Obsoletions.SharedUrlFormat)] - [EditorBrowsable(EditorBrowsableState.Never)] public static object? GetData(string format) => string.IsNullOrWhiteSpace(format) ? null : GetData(format, autoConvert: false); diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.cs index b99b76acaf4..cbb9a487992 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Specialized; -using System.ComponentModel; using System.Drawing; using System.Reflection.Metadata; using System.Runtime.InteropServices; @@ -103,7 +102,6 @@ internal IDataObject TryUnwrapInnerIDataObject() error: false, DiagnosticId = Obsoletions.ClipboardGetDataDiagnosticId, UrlFormat = Obsoletions.SharedUrlFormat)] - [EditorBrowsable(EditorBrowsableState.Never)] public virtual object? GetData(string format, bool autoConvert) => ((IDataObject)_innerData).GetData(format, autoConvert); @@ -112,7 +110,6 @@ internal IDataObject TryUnwrapInnerIDataObject() error: false, DiagnosticId = Obsoletions.ClipboardGetDataDiagnosticId, UrlFormat = Obsoletions.SharedUrlFormat)] - [EditorBrowsable(EditorBrowsableState.Never)] public virtual object? GetData(string format) => GetData(format, autoConvert: true); [Obsolete( @@ -120,7 +117,6 @@ internal IDataObject TryUnwrapInnerIDataObject() error: false, DiagnosticId = Obsoletions.ClipboardGetDataDiagnosticId, UrlFormat = Obsoletions.SharedUrlFormat)] - [EditorBrowsable(EditorBrowsableState.Never)] public virtual object? GetData(Type format) => format is null ? null : GetData(format.FullName!); public virtual bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( From 99fdc911f4c4492b53238828cd4c9137534a6f66 Mon Sep 17 00:00:00 2001 From: Tanya Solyanik Date: Fri, 8 Nov 2024 13:51:59 -0800 Subject: [PATCH 4/8] dedicated AppContext switch for NRBF deserializer --- .../TestUtilities/AppContextSwitchNames.cs | 7 +++++ .../NrbfSerializerInClipboardScope.cs | 27 +++++++++++++++++++ .../LocalAppContextSwitches.cs | 16 +++++++++-- ...bject.Composition.BinaryFormatUtilities.cs | 13 +++++---- .../runtimeconfig.template.json | 1 + .../System/Windows/Forms/ClipboardTests.cs | 20 +++++++++++++- 6 files changed, 76 insertions(+), 8 deletions(-) create mode 100644 src/Common/tests/TestUtilities/NrbfSerializerInClipboardScope.cs diff --git a/src/Common/tests/TestUtilities/AppContextSwitchNames.cs b/src/Common/tests/TestUtilities/AppContextSwitchNames.cs index 1db4cbb89a5..13e7b61b898 100644 --- a/src/Common/tests/TestUtilities/AppContextSwitchNames.cs +++ b/src/Common/tests/TestUtilities/AppContextSwitchNames.cs @@ -19,9 +19,16 @@ public const string EnableUnsafeBinaryFormatterSerialization /// public const string LocalAppContext_DisableCaching = "TestSwitch.LocalAppContext.DisableCaching"; + /// /// The switch that controls whether or not the is enabled in the Clipboard. /// public const string ClipboardDragDropEnableUnsafeBinaryFormatterSerializationSwitchName = "Windows.ClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization"; + + /// + /// The switch that controls whether or not the System.Windows.Forms.BinaryFormat.Deserializer is enabled in the Clipboard. + /// + public const string ClipboardDragDropEnableNrbfSerializationSwitchName + = "Windows.ClipboardDragDrop.EnableNrbfSerialization"; } diff --git a/src/Common/tests/TestUtilities/NrbfSerializerInClipboardScope.cs b/src/Common/tests/TestUtilities/NrbfSerializerInClipboardScope.cs new file mode 100644 index 00000000000..9511473c2a9 --- /dev/null +++ b/src/Common/tests/TestUtilities/NrbfSerializerInClipboardScope.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System; + +public readonly ref struct NrbfSerializerInClipboardScope +{ + private readonly AppContextSwitchScope _switchScope; + + public NrbfSerializerInClipboardScope(bool enable) + { + Monitor.Enter(typeof(NrbfSerializerInClipboardScope)); + _switchScope = new(AppContextSwitchNames.ClipboardDragDropEnableNrbfSerializationSwitchName, enable); + } + + public void Dispose() + { + try + { + _switchScope.Dispose(); + } + finally + { + Monitor.Exit(typeof(NrbfSerializerInClipboardScope)); + } + } +} diff --git a/src/System.Windows.Forms.Primitives/src/System/LocalAppContextSwitches/LocalAppContextSwitches.cs b/src/System.Windows.Forms.Primitives/src/System/LocalAppContextSwitches/LocalAppContextSwitches.cs index f78faf2830f..78a0f666b88 100644 --- a/src/System.Windows.Forms.Primitives/src/System/LocalAppContextSwitches/LocalAppContextSwitches.cs +++ b/src/System.Windows.Forms.Primitives/src/System/LocalAppContextSwitches/LocalAppContextSwitches.cs @@ -27,6 +27,7 @@ internal static partial class LocalAppContextSwitches internal const string EnableMsoComponentManagerSwitchName = "Switch.System.Windows.Forms.EnableMsoComponentManager"; internal const string TreeNodeCollectionAddRangeRespectsSortOrderSwitchName = "System.Windows.Forms.TreeNodeCollectionAddRangeRespectsSortOrder"; internal const string ClipboardDragDropEnableUnsafeBinaryFormatterSerializationSwitchName = "Windows.ClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization"; + internal const string ClipboardDragDropEnableNrbfSerializationSwitchName = "Windows.ClipboardDragDrop.EnableNrbfSerialization"; private static int s_scaleTopLevelFormMinMaxSizeForDpi; private static int s_anchorLayoutV2; @@ -39,6 +40,7 @@ internal static partial class LocalAppContextSwitches private static int s_enableMsoComponentManager; private static int s_treeNodeCollectionAddRangeRespectsSortOrder; private static int s_clipboardDragDropEnableUnsafeBinaryFormatterSerialization; + private static int s_clipboardDragDropEnableNrbfSerialization; private static FrameworkName? s_targetFrameworkName; @@ -229,8 +231,8 @@ public static bool TreeNodeCollectionAddRangeRespectsSortOrder } /// - /// If , then Clipboard Get methods will use - /// to deserialize the payload if needed. To use this switch, application should also opt in into the + /// If , then Clipboard and DataObject Get and Set methods will use + /// to serialize the payload if needed. To use this switch, application should also opt in into the /// System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization option and reference the out-of-band /// System.Runtime.Serialization.Formatters NuGet package. /// @@ -239,4 +241,14 @@ public static bool ClipboardDragDropEnableUnsafeBinaryFormatterSerialization [MethodImpl(MethodImplOptions.AggressiveInlining)] get => GetCachedSwitchValue(ClipboardDragDropEnableUnsafeBinaryFormatterSerializationSwitchName, ref s_clipboardDragDropEnableUnsafeBinaryFormatterSerialization); } + + /// + /// If , then Clipboard Get methods will use System.Windows.Forms.BinaryFormat.Deserializer + /// to deserialize the payload if needed. This is an alternative to deserialization using use . + /// + public static bool ClipboardDragDropEnableNrbfSerialization + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => GetCachedSwitchValue(ClipboardDragDropEnableNrbfSerializationSwitchName, ref s_clipboardDragDropEnableNrbfSerialization); + } } diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.BinaryFormatUtilities.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.BinaryFormatUtilities.cs index f6463404f95..323fafa4798 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.BinaryFormatUtilities.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.BinaryFormatUtilities.cs @@ -119,13 +119,16 @@ record = stream.Decode(out recordMap); return value; } - if (LocalAppContextSwitches.ClipboardDragDropEnableUnsafeBinaryFormatterSerialization) + // The legacy APIs do not provide resolver, resolver is required for the NRBF deserializer to work beyond the known types. + if (!legacyMode + && LocalAppContextSwitches.ClipboardDragDropEnableNrbfSerialization + && record.Deserialize(recordMap, (ITypeResolver)binder) is { } result) { - if (!legacyMode && record.Deserialize(recordMap, (ITypeResolver)binder) is { } result) - { - return result; - } + return result; + } + if (LocalAppContextSwitches.ClipboardDragDropEnableUnsafeBinaryFormatterSerialization) + { stream.Position = startPosition; return ReadObjectWithBinaryFormatter(stream, binder); } diff --git a/src/System.Windows.Forms/tests/IntegrationTests/WinformsControlsTest/runtimeconfig.template.json b/src/System.Windows.Forms/tests/IntegrationTests/WinformsControlsTest/runtimeconfig.template.json index 7ce60692b73..9437717726b 100644 --- a/src/System.Windows.Forms/tests/IntegrationTests/WinformsControlsTest/runtimeconfig.template.json +++ b/src/System.Windows.Forms/tests/IntegrationTests/WinformsControlsTest/runtimeconfig.template.json @@ -2,6 +2,7 @@ "configProperties": { "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false, "Windows.ClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization": false, + "Windows.ClipboardDragDrop.EnableNrbfSerialization": true, "System.Windows.Forms.AnchorLayoutV2": false, "System.Windows.Forms.ApplyParentFontToMenus": true, "System.Windows.Forms.DataGridViewUIAStartRowCountAtZero": false, diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ClipboardTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ClipboardTests.cs index 6b4c4fb5eaa..d5e2c1dfaeb 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ClipboardTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ClipboardTests.cs @@ -643,7 +643,7 @@ public unsafe void Clipboard_RawClipboard_SetClipboardData_ReturnsExpected() } [WinFormsFact] - public void Clipboard_AppContextSwitch() + public void Clipboard_BinaryFormatter_AppContextSwitch() { LocalAppContextSwitches.ClipboardDragDropEnableUnsafeBinaryFormatterSerialization.Should().BeFalse(); @@ -660,6 +660,24 @@ public void Clipboard_AppContextSwitch() } } + [WinFormsFact] + public void Clipboard_NrbfSerializer_AppContextSwitch() + { + LocalAppContextSwitches.ClipboardDragDropEnableNrbfSerialization.Should().BeFalse(); + + using (NrbfSerializerInClipboardScope scope = new(enable: true)) + { + LocalAppContextSwitches.ClipboardDragDropEnableNrbfSerialization.Should().BeTrue(); + } + + LocalAppContextSwitches.ClipboardDragDropEnableNrbfSerialization.Should().BeFalse(); + + using (NrbfSerializerInClipboardScope scope = new(enable: false)) + { + LocalAppContextSwitches.ClipboardDragDropEnableNrbfSerialization.Should().BeFalse(); + } + } + [WinFormsFact] public void Clipboard_TryGetInt_ReturnsExpected() { From 5094fae8a534d17169065d55b35ade65148ad387 Mon Sep 17 00:00:00 2001 From: Tanya Solyanik Date: Fri, 8 Nov 2024 15:33:44 -0800 Subject: [PATCH 5/8] test update from analyzers --- .../UIIntegrationTests/DragDropTests.cs | 1 - .../System/Windows/Forms/DataObjectTests.cs | 40 +++++++++---------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/System.Windows.Forms/tests/IntegrationTests/UIIntegrationTests/DragDropTests.cs b/src/System.Windows.Forms/tests/IntegrationTests/UIIntegrationTests/DragDropTests.cs index 4de728c011f..bfd30f7eadf 100644 --- a/src/System.Windows.Forms/tests/IntegrationTests/UIIntegrationTests/DragDropTests.cs +++ b/src/System.Windows.Forms/tests/IntegrationTests/UIIntegrationTests/DragDropTests.cs @@ -329,7 +329,6 @@ await InputSimulator.SendAsync( }); Assert.NotNull(data); - Assert.True(data is ListViewItem); Assert.Equal(listViewItem?.Name, data.Name); Assert.Equal(listViewItem?.Text, data.Text); } diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectTests.cs index 69dbef43d1e..3059bb5fedb 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectTests.cs @@ -1106,17 +1106,17 @@ private void DataObject_SetData_InvokeStringObject_GetReturnsExpected(string for dataObject.GetData(format, autoConvert: false).Should().Be(input); dataObject.GetData(format, autoConvert: true).Should().Be(input); - dataObject.TryGetData(format, out object unboundedData).Should().Be(input is string); + dataObject.TryGetData(format, out object unboundedData).Should().Be(input is not null); unboundedData.Should().Be(input); - dataObject.TryGetData(format, autoConvert: false, out unboundedData).Should().Be(input is string); + dataObject.TryGetData(format, autoConvert: false, out unboundedData).Should().Be(input is not null); unboundedData.Should().Be(input); - dataObject.TryGetData(format, autoConvert: true, out unboundedData).Should().Be(input is string); + dataObject.TryGetData(format, autoConvert: true, out unboundedData).Should().Be(input is not null); unboundedData.Should().Be(input); - dataObject.TryGetData(format, resolver: null!, autoConvert: true, out unboundedData).Should().Be(input is string); + dataObject.TryGetData(format, resolver: null!, autoConvert: true, out unboundedData).Should().Be(input is not null); unboundedData.Should().Be(input); - dataObject.TryGetData(format, resolver: null!, autoConvert: false, out unboundedData).Should().Be(input is string); + dataObject.TryGetData(format, resolver: null!, autoConvert: false, out unboundedData).Should().Be(input is not null); unboundedData.Should().Be(input); dataObject.ContainsAudio().Should().Be(format == DataFormats.WaveAudio); @@ -1148,11 +1148,11 @@ private void DataObject_SetData_InvokeStringObject_Unbounded_GetReturnsExpected( dataObject.GetData(format, autoConvert: false).Should().Be(input); dataObject.GetData(format, autoConvert: true).Should().Be(input); - dataObject.TryGetData(format, out object unboundedData).Should().Be(input is string); - dataObject.TryGetData(format, autoConvert: false, out unboundedData).Should().Be(input is string); - dataObject.TryGetData(format, autoConvert: true, out unboundedData).Should().Be(input is string); - dataObject.TryGetData(format, resolver: null!, autoConvert: true, out unboundedData).Should().Be(input is string); - dataObject.TryGetData(format, resolver: null!, autoConvert: false, out unboundedData).Should().Be(input is string); + dataObject.TryGetData(format, out object unboundedData).Should().Be(input is not null); + dataObject.TryGetData(format, autoConvert: false, out unboundedData).Should().Be(input is not null); + dataObject.TryGetData(format, autoConvert: true, out unboundedData).Should().Be(input is not null); + dataObject.TryGetData(format, resolver: null!, autoConvert: true, out unboundedData).Should().Be(input is not null); + dataObject.TryGetData(format, resolver: null!, autoConvert: false, out unboundedData).Should().Be(input is not null); dataObject.TryGetData(format, resolver: null!, autoConvert: false, out string data).Should().Be(input is not null); data.Should().Be(input); @@ -1265,17 +1265,17 @@ private void DataObject_SetData_InvokeStringBoolObject_GetReturnsExpected(string dataObject.GetData(format, autoConvert: false).Should().Be(input); dataObject.GetData(format, autoConvert: true).Should().Be(input); - dataObject.TryGetData(format, out object unboundedData).Should().Be(input is string); + dataObject.TryGetData(format, out object unboundedData).Should().Be(input is not null); unboundedData.Should().Be(input); - dataObject.TryGetData(format, autoConvert: false, out unboundedData).Should().Be(input is string); + dataObject.TryGetData(format, autoConvert: false, out unboundedData).Should().Be(input is not null); unboundedData.Should().Be(input); - dataObject.TryGetData(format, autoConvert: true, out unboundedData).Should().Be(input is string); + dataObject.TryGetData(format, autoConvert: true, out unboundedData).Should().Be(input is not null); unboundedData.Should().Be(input); - dataObject.TryGetData(format, resolver: null!, autoConvert: false, out unboundedData).Should().Be(input is string); + dataObject.TryGetData(format, resolver: null!, autoConvert: false, out unboundedData).Should().Be(input is not null); unboundedData.Should().Be(input); - dataObject.TryGetData(format, resolver: null!, autoConvert: true, out unboundedData).Should().Be(input is string); + dataObject.TryGetData(format, resolver: null!, autoConvert: true, out unboundedData).Should().Be(input is not null); unboundedData.Should().Be(input); dataObject.GetDataPresent(format, autoConvert: true).Should().BeTrue(); @@ -1309,17 +1309,17 @@ private void DataObject_SetData_InvokeStringBoolObject_Unbounded(string format, dataObject.GetData(format, autoConvert: false).Should().Be(input); dataObject.GetData(format, autoConvert: true).Should().Be(input); - dataObject.TryGetData(format, out string data).Should().Be(input is string); + dataObject.TryGetData(format, out string data).Should().Be(input is not null); data.Should().Be(input); - dataObject.TryGetData(format, autoConvert: false, out data).Should().Be(input is string); + dataObject.TryGetData(format, autoConvert: false, out data).Should().Be(input is not null); data.Should().Be(input); - dataObject.TryGetData(format, autoConvert: true, out data).Should().Be(input is string); + dataObject.TryGetData(format, autoConvert: true, out data).Should().Be(input is not null); data.Should().Be(input); - dataObject.TryGetData(format, resolver: null!, autoConvert: false, out data).Should().Be(input is string); + dataObject.TryGetData(format, resolver: null!, autoConvert: false, out data).Should().Be(input is not null); data.Should().Be(input); - dataObject.TryGetData(format, resolver: null!, autoConvert: true, out data).Should().Be(input is string); + dataObject.TryGetData(format, resolver: null!, autoConvert: true, out data).Should().Be(input is not null); data.Should().Be(input); dataObject.GetDataPresent(format, autoConvert: true).Should().BeTrue(); From 2dcbdfb622f80541984c754459a9fa4bf17fd061 Mon Sep 17 00:00:00 2001 From: Tanya Solyanik Date: Sun, 10 Nov 2024 10:20:02 -0800 Subject: [PATCH 6/8] ITypedDataObject --- .../src/PublicAPI.Unshipped.txt | 23 +- .../src/System/Windows/Forms/OLE/Clipboard.cs | 61 ++++- .../System/Windows/Forms/OLE/DataFormats.cs | 2 +- ...bject.Composition.BinaryFormatUtilities.cs | 8 +- ...ect.Composition.NativeToWinFormsAdapter.cs | 14 +- .../Forms/OLE/DataObject.Composition.cs | 32 +-- .../Windows/Forms/OLE/DataObject.DataStore.cs | 49 ++-- .../System/Windows/Forms/OLE/DataObject.cs | 61 +++-- .../Windows/Forms/OLE/DataObjectExtensions.cs | 83 ++++++ .../Windows/Forms/OLE/DragDropHelper.cs | 4 +- .../System/Windows/Forms/OLE/IDataObject.cs | 82 ------ .../Windows/Forms/OLE/ITypedDataObject.cs | 69 +++++ .../ComDisabledTests/ClipboardComTests.cs | 2 +- .../ComDisabledTests/DataObjectComTests.cs | 5 - .../System.Windows.Forms.Tests.csproj | 1 + .../Windows/Forms/DataObjectComTests.cs | 5 - .../Forms/DataObjectExtensionsTests.cs | 251 ++++++++++++++++++ .../System/Windows/Forms/DataObjectTests.cs | 129 ++++++--- .../Windows/Forms/DragEventArgsTests.cs | 6 - .../System/Windows/Forms/IDataObjectTests.cs | 146 ---------- 20 files changed, 646 insertions(+), 387 deletions(-) create mode 100644 src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObjectExtensions.cs create mode 100644 src/System.Windows.Forms/src/System/Windows/Forms/OLE/ITypedDataObject.cs create mode 100644 src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectExtensionsTests.cs delete mode 100644 src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/IDataObjectTests.cs diff --git a/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt b/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt index ec1d31aaae3..7a074610909 100644 --- a/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt +++ b/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt @@ -1,11 +1,18 @@ static System.Windows.Forms.Clipboard.TryGetData(string! format, out T data) -> bool static System.Windows.Forms.Clipboard.TryGetData(string! format, System.Func! resolver, out T data) -> bool +static System.Windows.Forms.DataObjectExtensions.TryGetData(this System.Windows.Forms.IDataObject! dataObject, out T data) -> bool +static System.Windows.Forms.DataObjectExtensions.TryGetData(this System.Windows.Forms.IDataObject! dataObject, string! format, bool autoConvert, out T data) -> bool +static System.Windows.Forms.DataObjectExtensions.TryGetData(this System.Windows.Forms.IDataObject! dataObject, string! format, out T data) -> bool +static System.Windows.Forms.DataObjectExtensions.TryGetData(this System.Windows.Forms.IDataObject! dataObject, string! format, System.Func! resolver, bool autoConvert, out T data) -> bool System.Windows.Forms.DataGridViewCellStyle.Font.get -> System.Drawing.Font? -System.Windows.Forms.IDataObject.TryGetData(string! format, out T data) -> bool -System.Windows.Forms.IDataObject.TryGetData(out T data) -> bool -System.Windows.Forms.IDataObject.TryGetData(string! format, bool autoConvert, out T data) -> bool -System.Windows.Forms.IDataObject.TryGetData(string! format, System.Func! resolver, bool autoConvert, out T data) -> bool -virtual System.Windows.Forms.DataObject.TryGetData(out T data) -> bool -virtual System.Windows.Forms.DataObject.TryGetData(string! format, bool autoConvert, out T data) -> bool -virtual System.Windows.Forms.DataObject.TryGetData(string! format, out T data) -> bool -virtual System.Windows.Forms.DataObject.TryGetData(string! format, System.Func! resolver, bool autoConvert, out T data) -> bool +System.Windows.Forms.DataObject.TryGetData(out T data) -> bool +System.Windows.Forms.DataObject.TryGetData(string! format, bool autoConvert, out T data) -> bool +System.Windows.Forms.DataObject.TryGetData(string! format, out T data) -> bool +System.Windows.Forms.DataObject.TryGetData(string! format, System.Func! resolver, bool autoConvert, out T data) -> bool +System.Windows.Forms.DataObjectExtensions +System.Windows.Forms.ITypedDataObject +System.Windows.Forms.ITypedDataObject.TryGetData(out T data) -> bool +System.Windows.Forms.ITypedDataObject.TryGetData(string! format, bool autoConvert, out T data) -> bool +System.Windows.Forms.ITypedDataObject.TryGetData(string! format, out T data) -> bool +System.Windows.Forms.ITypedDataObject.TryGetData(string! format, System.Func! resolver, bool autoConvert, out T data) -> bool +virtual System.Windows.Forms.DataObject.TryGetDataCore(string! format, System.Func! resolver, bool autoConvert, out T data) -> bool diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/Clipboard.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/Clipboard.cs index 34b07b53c9a..8c2afab2592 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/Clipboard.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/Clipboard.cs @@ -221,8 +221,21 @@ public static bool ContainsText(TextDataFormat format) /// /// Retrieves an audio stream from the . /// - public static Stream? GetAudioStream() => - TryGetData(DataFormats.WaveAudioConstant, out Stream? stream) ? stream : null; + public static Stream? GetAudioStream() + { + IDataObject? data = GetDataObject(); + if (data is ITypedDataObject typed) + { + return typed.TryGetData(DataFormats.WaveAudioConstant, out Stream? stream) ? stream : null; + } + + if (data is IDataObject dataObject && dataObject.GetData(DataFormats.WaveAudioConstant) is Stream stream1) + { + return stream1; + } + + return null; + } /// /// Retrieves data from the in the specified format. @@ -252,7 +265,7 @@ public static bool ContainsText(TextDataFormat format) [NotNullWhen(true), MaybeNullWhen(false)] out T data) { data = default; - if (GetDataObject() is IDataObject dataObject) + if (GetDataObject() is ITypedDataObject dataObject) { // Custom IDataObjects should handle their own validation. if (dataObject is DataObject && !DataObject.ValidateTryGetDataArguments(format)) @@ -315,7 +328,7 @@ public static bool ContainsText(TextDataFormat format) [NotNullWhen(true), MaybeNullWhen(false)] out T data) { data = default; - if (GetDataObject() is IDataObject dataObject) + if (GetDataObject() is ITypedDataObject dataObject) { return dataObject.TryGetData(format, resolver, autoConvert: false, out data); } @@ -330,12 +343,20 @@ public static StringCollection GetFileDropList() { StringCollection result = []; - if (GetDataObject() is IDataObject dataObject - && dataObject.TryGetData( + IDataObject? data = GetDataObject(); + if (data is ITypedDataObject typed) + { + if (typed.TryGetData( DataFormats.FileDropConstant, DataObject.NotSupportedResolver, autoConvert: true, out string[]? strings)) + { + result.AddRange(strings); + } + } + else if (data is IDataObject dataObject + && dataObject.GetData(DataFormats.FileDropConstant, autoConvert: true) is string[] strings) { result.AddRange(strings); } @@ -352,16 +373,18 @@ public static StringCollection GetFileDropList() /// public static Image? GetImage() { - if (GetDataObject() is IDataObject dataObject - && dataObject.TryGetData( - DataFormats.Bitmap, - DataObject.NotSupportedResolver, - autoConvert: true, - out Bitmap? image)) + IDataObject? data = GetDataObject(); + if (data is ITypedDataObject typed) { + typed.TryGetData(DataFormats.Bitmap, DataObject.NotSupportedResolver, autoConvert: true, out Image? image); return image; } + if (data is IDataObject dataObject && dataObject.GetData(DataFormats.Bitmap, autoConvert: true) is Image image1) + { + return image1; + } + return null; } @@ -377,7 +400,19 @@ public static StringCollection GetFileDropList() public static string GetText(TextDataFormat format) { SourceGenerated.EnumValidator.Validate(format, nameof(format)); - return TryGetData(ConvertToDataFormats(format), out string? text) ? text : string.Empty; + + IDataObject? data = GetDataObject(); + if (data is ITypedDataObject typed) + { + return typed.TryGetData(ConvertToDataFormats(format), out string? text) ? text : string.Empty; + } + + if (data is IDataObject dataObject) + { + return dataObject.GetData(ConvertToDataFormats(format), autoConvert: true) is string text ? text : string.Empty; + } + + return string.Empty; } /// diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataFormats.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataFormats.cs index fdb407af0ff..ae5a4bd8895 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataFormats.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataFormats.cs @@ -154,7 +154,7 @@ public static partial class DataFormats /// public static Format GetFormat(string format) { - ArgumentException.ThrowIfNullOrWhiteSpace(format); + ArgumentException.ThrowIfNullOrWhiteSpace(format, nameof(format)); lock (s_internalSyncObject) { diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.BinaryFormatUtilities.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.BinaryFormatUtilities.cs index 323fafa4798..111bc153294 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.BinaryFormatUtilities.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.BinaryFormatUtilities.cs @@ -163,10 +163,7 @@ private static bool TypeNameIsAssignableToType(TypeName typeName, Type type, Fun throw new NotSupportedException(SR.BinaryFormatterNotSupported); } - stream.Position = startPosition; - -#pragma warning disable SYSLIB0011 // Type or member is obsolete -#pragma warning disable SYSLIB0050 // Type or member is obsolete +#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 @@ -177,8 +174,7 @@ private static bool TypeNameIsAssignableToType(TypeName typeName, Type type, Fun }.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 -#pragma warning restore SYSLIB0011 +#pragma warning restore SYSLIB0050, SYSLIB0011 } } } diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.NativeToWinFormsAdapter.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.NativeToWinFormsAdapter.cs index 814d99f4523..d675b609956 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.NativeToWinFormsAdapter.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.NativeToWinFormsAdapter.cs @@ -17,7 +17,7 @@ internal unsafe partial class Composition /// /// Maps native pointer to . /// - private unsafe class NativeToWinFormsAdapter : IDataObject, Com.IDataObject.Interface + private unsafe class NativeToWinFormsAdapter : IDataObject, ITypedDataObject, Com.IDataObject.Interface { private readonly AgileComPointer _nativeDataObject; @@ -494,7 +494,9 @@ private bool TryGetDataInternal(string format, Func? resolver object? IDataObject.GetData(Type format) => ((IDataObject)this).GetData(format.FullName!); - bool IDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + bool IDataObject.GetDataPresent(Type format) => GetDataPresent(format.FullName!); + + bool ITypedDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( string format, Func resolver, bool autoConvert, @@ -509,23 +511,21 @@ private bool TryGetDataInternal(string format, Func? resolver return TryGetDataInternal(format, resolver, autoConvert, legacyMode: false, out data); } - bool IDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + bool ITypedDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( string format, bool autoConvert, [NotNullWhen(true), MaybeNullWhen(false)] out T data) => TryGetDataInternal(format, NotSupportedResolver, autoConvert, legacyMode: false, out data); - bool IDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + bool ITypedDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( string format, [NotNullWhen(true), MaybeNullWhen(false)] out T data) => TryGetDataInternal(format, NotSupportedResolver, autoConvert: false, legacyMode: false, out data); - bool IDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + bool ITypedDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( [NotNullWhen(true), MaybeNullWhen(false)] out T data) => TryGetDataInternal(typeof(T).FullName!, NotSupportedResolver, autoConvert: false, legacyMode: false, out data); - bool IDataObject.GetDataPresent(Type format) => GetDataPresent(format.FullName!); - public bool GetDataPresent(string format, bool autoConvert) { bool dataPresent = GetDataPresentInner(format); diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.cs index 09109a0eced..b688684a791 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.cs @@ -14,7 +14,7 @@ public unsafe partial class DataObject /// Contains the logic to move between , , /// and calls. /// - internal unsafe partial class Composition : IDataObject, Com.IDataObject.Interface, ComTypes.IDataObject + internal unsafe partial class Composition : IDataObject, ITypedDataObject, Com.IDataObject.Interface, ComTypes.IDataObject { private const Com.TYMED AllowedTymeds = Com.TYMED.TYMED_HGLOBAL | Com.TYMED.TYMED_ISTREAM | Com.TYMED.TYMED_GDI; @@ -108,37 +108,39 @@ or DataFormats.PaletteConstant object? IDataObject.GetData(string format, bool autoConvert) => _winFormsDataObject.GetData(format, autoConvert); object? IDataObject.GetData(string format) => _winFormsDataObject.GetData(format); object? IDataObject.GetData(Type format) => _winFormsDataObject.GetData(format); - bool IDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + bool IDataObject.GetDataPresent(string format, bool autoConvert) => _winFormsDataObject.GetDataPresent(format, autoConvert); + bool IDataObject.GetDataPresent(string format) => _winFormsDataObject.GetDataPresent(format); + bool IDataObject.GetDataPresent(Type format) => _winFormsDataObject.GetDataPresent(format); + string[] IDataObject.GetFormats(bool autoConvert) => _winFormsDataObject.GetFormats(autoConvert); + string[] IDataObject.GetFormats() => _winFormsDataObject.GetFormats(); + void IDataObject.SetData(string format, bool autoConvert, object? data) => _winFormsDataObject.SetData(format, autoConvert, data); + void IDataObject.SetData(string format, object? data) => _winFormsDataObject.SetData(format, data); + void IDataObject.SetData(Type format, object? data) => _winFormsDataObject.SetData(format, data); + void IDataObject.SetData(object? data) => _winFormsDataObject.SetData(data); + #endregion + + #region ITypedDataObject + bool ITypedDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( string format, Func resolver, bool autoConvert, [NotNullWhen(true), MaybeNullWhen(false)] out T data) => _winFormsDataObject.TryGetData(format, resolver, autoConvert, out data); - bool IDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + bool ITypedDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( string format, bool autoConvert, [NotNullWhen(true), MaybeNullWhen(false)] out T data) => _winFormsDataObject.TryGetData(format, NotSupportedResolver, autoConvert, out data); - bool IDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + bool ITypedDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( string format, [NotNullWhen(true), MaybeNullWhen(false)] out T data) => _winFormsDataObject.TryGetData(format, NotSupportedResolver, autoConvert: false, out data); - bool IDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + bool ITypedDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( [NotNullWhen(true), MaybeNullWhen(false)] out T data) => _winFormsDataObject.TryGetData(typeof(T).FullName!, NotSupportedResolver, autoConvert: false, out data); - - bool IDataObject.GetDataPresent(string format, bool autoConvert) => _winFormsDataObject.GetDataPresent(format, autoConvert); - bool IDataObject.GetDataPresent(string format) => _winFormsDataObject.GetDataPresent(format); - bool IDataObject.GetDataPresent(Type format) => _winFormsDataObject.GetDataPresent(format); - string[] IDataObject.GetFormats(bool autoConvert) => _winFormsDataObject.GetFormats(autoConvert); - string[] IDataObject.GetFormats() => _winFormsDataObject.GetFormats(); - void IDataObject.SetData(string format, bool autoConvert, object? data) => _winFormsDataObject.SetData(format, autoConvert, data); - void IDataObject.SetData(string format, object? data) => _winFormsDataObject.SetData(format, data); - void IDataObject.SetData(Type format, object? data) => _winFormsDataObject.SetData(format, data); - void IDataObject.SetData(object? data) => _winFormsDataObject.SetData(data); #endregion #region Com.IDataObject.Interface diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.DataStore.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.DataStore.cs index faf520daed7..c5e6579f3f1 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.DataStore.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.DataStore.cs @@ -10,7 +10,7 @@ namespace System.Windows.Forms; public partial class DataObject { - private class DataStore : IDataObject + private class DataStore : IDataObject, ITypedDataObject { private class DataStoreEntry { @@ -81,31 +81,6 @@ private bool TryGetDataInternal( public virtual object? GetData(Type format) => GetData(format.FullName!); - public virtual bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( - string format, - Func resolver, - bool autoConvert, - [NotNullWhen(true), MaybeNullWhen(false)] out T data) - { - data = default; - return TryGetDataInternal(format, autoConvert, out data); - } - - public virtual bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( - string format, - bool autoConvert, - [NotNullWhen(true), MaybeNullWhen(false)] out T data) => - TryGetDataInternal(format, autoConvert, out data); - - public virtual bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( - string format, - [NotNullWhen(true), MaybeNullWhen(false)] out T data) => - TryGetDataInternal(format, autoConvert: false, out data); - - public virtual bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( - [NotNullWhen(true), MaybeNullWhen(false)] out T data) => - TryGetDataInternal(typeof(T).FullName!, autoConvert: false, out data); - public virtual void SetData(string format, bool autoConvert, object? data) { if (string.IsNullOrWhiteSpace(format)) @@ -229,5 +204,27 @@ public virtual string[] GetFormats(bool autoConvert) } public virtual string[] GetFormats() => GetFormats(autoConvert: true); + + public virtual bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + string format, + Func resolver, + bool autoConvert, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) => + TryGetDataInternal(format, autoConvert, out data); + + public virtual bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + string format, + bool autoConvert, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) => + TryGetDataInternal(format, autoConvert, out data); + + public virtual bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + string format, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) => + TryGetDataInternal(format, autoConvert: false, out data); + + public virtual bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + [NotNullWhen(true), MaybeNullWhen(false)] out T data) => + TryGetDataInternal(typeof(T).FullName!, autoConvert: false, out data); } } diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.cs index cbb9a487992..4e28594f223 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.cs @@ -17,6 +17,7 @@ namespace System.Windows.Forms; [ClassInterface(ClassInterfaceType.None)] public unsafe partial class DataObject : IDataObject, + ITypedDataObject, Com.IDataObject.Interface, ComTypes.IDataObject, Com.IManagedWrapper @@ -119,30 +120,6 @@ internal IDataObject TryUnwrapInnerIDataObject() UrlFormat = Obsoletions.SharedUrlFormat)] public virtual object? GetData(Type format) => format is null ? null : GetData(format.FullName!); - public virtual bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( - string format, -#pragma warning disable CS3001 // Argument type is not CLS-compliant - Func resolver, -#pragma warning restore CS3001 - bool autoConvert, - [NotNullWhen(true), MaybeNullWhen(false)] out T data) => - ((IDataObject)_innerData).TryGetData(format, resolver, autoConvert, out data); - - public virtual bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( - string format, - bool autoConvert, - [NotNullWhen(true), MaybeNullWhen(false)] out T data) => - TryGetData(format, NotSupportedResolver, autoConvert, out data); - - public virtual bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( - string format, - [NotNullWhen(true), MaybeNullWhen(false)] out T data) => - TryGetData(format, autoConvert: false, out data); - - public virtual bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( - [NotNullWhen(true), MaybeNullWhen(false)] out T data) => - TryGetData(typeof(T).FullName!, out data); - public virtual bool GetDataPresent(string format, bool autoConvert) => ((IDataObject)_innerData).GetDataPresent(format, autoConvert); @@ -164,6 +141,33 @@ public virtual void SetData(string format, bool autoConvert, object? data) public virtual void SetData(object? data) => ((IDataObject)_innerData).SetData(data); #endregion + #region ITypedDataObject + public bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + string format, +#pragma warning disable CS3001 // Argument type is not CLS-compliant + Func resolver, +#pragma warning restore CS3001 + bool autoConvert, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) => + // TODO (TanyaSo) argument validation here?? + TryGetDataCore(format, resolver, autoConvert, out data); + + public bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + string format, + bool autoConvert, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) => + TryGetData(format, NotSupportedResolver, autoConvert, out data); + + public bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + string format, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) => + TryGetData(format, autoConvert: false, out data); + + public bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + [NotNullWhen(true), MaybeNullWhen(false)] out T data) => + TryGetData(typeof(T).FullName!, out data); + #endregion + public virtual bool ContainsAudio() => GetDataPresent(DataFormats.WaveAudioConstant, autoConvert: false); public virtual bool ContainsFileDropList() => GetDataPresent(DataFormats.FileDropConstant, autoConvert: true); @@ -231,6 +235,15 @@ public virtual void SetText(string textData, TextDataFormat format) SetData(ConvertToDataFormats(format), false, textData); } + protected virtual bool TryGetDataCore<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + string format, +#pragma warning disable CS3001 // Argument type is not CLS-compliant + Func resolver, +#pragma warning restore CS3001 + bool autoConvert, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) => + ((ITypedDataObject)_innerData).TryGetData(format, resolver, autoConvert, out data); + internal static bool ValidateTryGetDataArguments(string format, Func resolver) { if (!ValidateFormat(format)) diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObjectExtensions.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObjectExtensions.cs new file mode 100644 index 00000000000..d09f31b63d3 --- /dev/null +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObjectExtensions.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Windows.Forms; + +public static class DataObjectExtensions +{ + /// + /// if the does not implement . + /// if the is + public static bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + this IDataObject dataObject, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) + { + ArgumentNullException.ThrowIfNull(dataObject); + + if (dataObject is not ITypedDataObject typed) + { + throw new ArgumentException($"DataObject should implement {nameof(ITypedDataObject)} interface.", nameof(dataObject)); + } + + return typed.TryGetData(out data); + } + + /// + /// if the does not implement . + /// if the is + public static bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + this IDataObject dataObject, + string format, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) + { + ArgumentNullException.ThrowIfNull(dataObject); + + if (dataObject is not ITypedDataObject typed) + { + throw new ArgumentException($"DataObject should implement {nameof(ITypedDataObject)} interface.", nameof(dataObject)); + } + + return typed.TryGetData(format, out data); + } + + /// + /// if the does not implement . + /// if the is + public static bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + this IDataObject dataObject, + string format, + bool autoConvert, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) + { + ArgumentNullException.ThrowIfNull(dataObject); + + if (dataObject is not ITypedDataObject typed) + { + throw new ArgumentException($"DataObject should implement {nameof(ITypedDataObject)} interface.", nameof(dataObject)); + } + + return typed.TryGetData(format, autoConvert, out data); + } + + /// + /// if the does not implement . + /// if the is + public static bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + this IDataObject dataObject, + string format, +#pragma warning disable CS3001 // Argument type is not CLS-compliant + Func resolver, +#pragma warning restore CS3001 + bool autoConvert, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) + { + ArgumentNullException.ThrowIfNull(dataObject); + + if (dataObject is not ITypedDataObject typed) + { + throw new ArgumentException($"DataObject should implement {nameof(ITypedDataObject)} interface.", nameof(dataObject)); + } + + return typed.TryGetData(format, resolver, autoConvert, out data); + } +} diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DragDropHelper.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DragDropHelper.cs index 25a1ec243ed..43f84c98644 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DragDropHelper.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DragDropHelper.cs @@ -152,7 +152,7 @@ public static void Drop(DragEventArgs e) private static unsafe bool GetBooleanFormat(IComDataObject dataObject, string format) { ArgumentNullException.ThrowIfNull(dataObject); - ArgumentException.ThrowIfNullOrEmpty(format); + ArgumentException.ThrowIfNullOrEmpty(format, nameof(format)); ComTypes.STGMEDIUM medium = default; @@ -261,7 +261,7 @@ public static void ReleaseDragDropFormats(IComDataObject comDataObject) private static unsafe void SetBooleanFormat(IComDataObject dataObject, string format, bool value) { ArgumentNullException.ThrowIfNull(dataObject); - ArgumentException.ThrowIfNullOrEmpty(format); + ArgumentException.ThrowIfNullOrEmpty(format, nameof(format)); ComTypes.FORMATETC formatEtc = new() { diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/IDataObject.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/IDataObject.cs index c873228eec4..33ba57a45f8 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/IDataObject.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/IDataObject.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Reflection.Metadata; - namespace System.Windows.Forms; /// @@ -26,86 +24,6 @@ public interface IDataObject /// object? GetData(Type format); - /// - /// Retrieves the data associated with the specified data format, using - /// to determine whether to convert the data to the format, - /// if that data is assignable to . - /// Will use with the binary formatter if needed. - /// is implemented by the user and should return the allowed types or - /// throw a . - /// - bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( - string format, -#pragma warning disable CS3001 // Argument type is not CLS-compliant - Func resolver, -#pragma warning restore CS3001 - bool autoConvert, - [NotNullWhen(true), MaybeNullWhen(false)] out T data) - { - data = default; - if (GetData(format, autoConvert) is T result) - { - data = result; - return true; - } - - return false; - } - - /// - /// Retrieves the data associated with the specified data format, using - /// to determine whether to convert the data to another format, - /// if that data is of type . - /// - bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( - string format, - bool autoConvert, - [NotNullWhen(true), MaybeNullWhen(false)] out T data) - { - data = default; - if (GetData(format, autoConvert) is T result) - { - data = result; - return true; - } - - return false; - } - - /// - /// Retrieves the data associated with the specified data format if that data is of type . - /// - bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( - string format, - [NotNullWhen(true), MaybeNullWhen(false)] out T data) - { - data = default; - if (GetData(format) is T result) - { - data = result; - return true; - } - - return false; - } - - /// - /// Retrieves the data associated with data format named after , - /// if that data is of type . - /// - bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( - [NotNullWhen(true), MaybeNullWhen(false)] out T data) - { - data = default; - if (GetData(typeof(T)) is T result) - { - data = result; - return true; - } - - return false; - } - /// /// Determines whether data stored in this instance is associated with the /// specified format, using autoConvert to determine whether to convert the diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/ITypedDataObject.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/ITypedDataObject.cs new file mode 100644 index 00000000000..f2819b52690 --- /dev/null +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/ITypedDataObject.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection.Metadata; + +namespace System.Windows.Forms; + +public interface ITypedDataObject +{ + /// + /// Retrieves the data associated with data format named after , + /// if that data is of type . + /// + /// + /// if the data of this format is present and the value is + /// of a matching type and that value can be successfully retrieved, or + /// if the format is not present or the value is not of the right type. + /// + bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + [NotNullWhen(true), MaybeNullWhen(false)] out T data); + + /// + /// Retrieves the data associated with the specified data format if that data is of type . + /// + /// + /// if the data of this format is present and the value is + /// of a matching type and that value can be successfully retrieved, or + /// if the format is not present or the value is not of the right type. + /// + bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + string format, + [NotNullWhen(true), MaybeNullWhen(false)] out T data); + + /// + /// Retrieves the data associated with the specified data format, using + /// to determine whether to convert the data to another format, + /// if that data is of type . + /// + /// + /// if the data of this format is present and the value is + /// of a matching type and that value can be successfully retrieved, or + /// if the format is not present or the value is not of the right type. + /// + bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + string format, + bool autoConvert, + [NotNullWhen(true), MaybeNullWhen(false)] out T data); + + /// + /// Retrieves the data associated with the specified data format, using + /// to determine whether to convert the data to the format, + /// if that data is assignable to . + /// Will use with the binary formatter if needed. + /// is implemented by the user and should return the allowed types or + /// throw a . + /// + /// + /// if the data of this format is present and the value is + /// of a matching type and that value can be successfully retrieved, or + /// if the format is not present or the value is not of the right type. + /// + bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + string format, +#pragma warning disable CS3001 // Argument type is not CLS-compliant + Func resolver, +#pragma warning restore CS3001 + bool autoConvert, + [NotNullWhen(true), MaybeNullWhen(false)] out T data); +} diff --git a/src/System.Windows.Forms/tests/ComDisabledTests/ClipboardComTests.cs b/src/System.Windows.Forms/tests/ComDisabledTests/ClipboardComTests.cs index 55649a8b8eb..14ac5b500da 100644 --- a/src/System.Windows.Forms/tests/ComDisabledTests/ClipboardComTests.cs +++ b/src/System.Windows.Forms/tests/ComDisabledTests/ClipboardComTests.cs @@ -7,7 +7,7 @@ namespace System.Windows.Forms.Tests; [Collection("Sequential")] // Each registered Clipboard format is an OS singleton, // and we should not run this test at the same time as other tests using the same format. -[UISettings(MaxAttempts = 3)] // Try up to 3 times before failing. +// [UISettings(MaxAttempts = 3)] // Try up to 3 times before failing. public class ClipboardComTests { [WinFormsFact] diff --git a/src/System.Windows.Forms/tests/ComDisabledTests/DataObjectComTests.cs b/src/System.Windows.Forms/tests/ComDisabledTests/DataObjectComTests.cs index 03cbbb2d049..58923843eda 100644 --- a/src/System.Windows.Forms/tests/ComDisabledTests/DataObjectComTests.cs +++ b/src/System.Windows.Forms/tests/ComDisabledTests/DataObjectComTests.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Reflection.Metadata; using System.Runtime.InteropServices.ComTypes; using Com = Windows.Win32.System.Com; using IComDataObject = System.Runtime.InteropServices.ComTypes.IDataObject; @@ -53,10 +52,6 @@ private class CustomIDataObject : IDataObject public object GetData(string format, bool autoConvert) => throw new NotImplementedException(); public object GetData(string format) => throw new NotImplementedException(); public object GetData(Type format) => throw new NotImplementedException(); - public bool TryGetData(string format, Func resolver, bool autoConvert, out T data) => throw new NotImplementedException(); - public bool TryGetData(string format, bool autoConvert, out T data) => throw new NotImplementedException(); - public bool TryGetData(string format, out T data) => throw new NotImplementedException(); - public bool TryGetData(out T data) => throw new NotImplementedException(); public bool GetDataPresent(string format, bool autoConvert) => throw new NotImplementedException(); public bool GetDataPresent(string format) => throw new NotImplementedException(); public bool GetDataPresent(Type format) => throw new NotImplementedException(); diff --git a/src/System.Windows.Forms/tests/UnitTests/System.Windows.Forms.Tests.csproj b/src/System.Windows.Forms/tests/UnitTests/System.Windows.Forms.Tests.csproj index 3d4859367fd..c0c3b037a68 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System.Windows.Forms.Tests.csproj +++ b/src/System.Windows.Forms/tests/UnitTests/System.Windows.Forms.Tests.csproj @@ -5,6 +5,7 @@ $(TargetFramework)-windows7.0 true System.Windows.Forms.Tests + true true diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectComTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectComTests.cs index 05c88b041df..02c4b3a92d6 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectComTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectComTests.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Reflection.Metadata; using System.Runtime.InteropServices.ComTypes; using Com = Windows.Win32.System.Com; using IComDataObject = System.Runtime.InteropServices.ComTypes.IDataObject; @@ -54,10 +53,6 @@ private class CustomIDataObject : IDataObject public object GetData(string format, bool autoConvert) => throw new NotImplementedException(); public object GetData(string format) => throw new NotImplementedException(); public object GetData(Type format) => throw new NotImplementedException(); - public bool TryGetData(string format, Func resolver, bool autoConvert, out T data) => throw new NotImplementedException(); - public bool TryGetData(string format, bool autoConvert, out T data) => throw new NotImplementedException(); - public bool TryGetData(string format, out T data) => throw new NotImplementedException(); - public bool TryGetData(out T data) => throw new NotImplementedException(); public bool GetDataPresent(string format, bool autoConvert) => throw new NotImplementedException(); public bool GetDataPresent(string format) => throw new NotImplementedException(); public bool GetDataPresent(Type format) => throw new NotImplementedException(); diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectExtensionsTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectExtensionsTests.cs new file mode 100644 index 00000000000..dee86aa284e --- /dev/null +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectExtensionsTests.cs @@ -0,0 +1,251 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System.Diagnostics.CodeAnalysis; +using System.Drawing; +using System.Reflection.Metadata; + +namespace System.Windows.Forms.Tests; + +public class DataObjectExtensionsTests +{ + [Fact] + public void TryGetData_Throws_ArgumentNull() + { + ((Action)(() => DataObjectExtensions.TryGetData(null!, out _))).Should().Throw(); + ((Action)(() => DataObjectExtensions.TryGetData(null!, DataFormats.Text, out _))).Should().Throw(); + ((Action)(() => DataObjectExtensions.TryGetData(null!, DataFormats.CommaSeparatedValue, autoConvert: true, out _))).Should().Throw(); + ((Action)(() => DataObjectExtensions.TryGetData(null!, DataFormats.Serializable, autoConvert: false, out _))).Should().Throw(); + ((Action)(() => DataObjectExtensions.TryGetData(null!, DataFormats.UnicodeText, Resolver, autoConvert: true, out _))).Should().Throw(); + ((Action)(() => DataObjectExtensions.TryGetData(null!, DataFormats.Bitmap, Resolver, autoConvert: false, out _))).Should().Throw(); + } + + private static Type Resolver(TypeName typeName) => typeof(string); + + [Fact] + public void TryGetData_Throws_Argument() + { + UntypedDataObject dataObject = new(); + + ((Action)(() => dataObject.TryGetData(out _))).Should().Throw(); + dataObject.VerifyGetDataWasNotCalled(); + } + + [Fact] + public void TryGetData_String_Throws_Argument() + { + UntypedDataObject dataObject = new(); + ((Action)(() => dataObject.TryGetData(DataFormats.Text, out _))).Should().Throw(); + dataObject.VerifyGetDataWasNotCalled(); + } + + [Theory] + [BoolData] + public void TryGetData_StringBool_Throws_Argument(bool autoConvert) + { + UntypedDataObject dataObject = new(); + ((Action)(() => dataObject.TryGetData(DataFormats.CommaSeparatedValue, autoConvert, out _))).Should().Throw(); + dataObject.VerifyGetDataWasNotCalled(); + } + + [Theory] + [BoolData] + public void TryGetData_StringFuncBool_Throws_Argument(bool autoConvert) + { + UntypedDataObject dataObject = new(); + ((Action)(() => dataObject.TryGetData(DataFormats.UnicodeText, Resolver, autoConvert, out _))).Should().Throw(); + dataObject.VerifyGetDataWasNotCalled(); + } + + [Fact] + public void DataObject_ReturnFalse() + { + DataObject dataObject = new(); + ((IDataObject)dataObject).TryGetData(out string? text).Should().BeFalse(); + text.Should().BeNull(); + } + + [Fact] + public void DataObject_String_ReturnsFalse() + { + DataObject dataObject = new(); + ((IDataObject)dataObject).TryGetData(DataFormats.Dib, out Bitmap? bitmap).Should().BeFalse(); + bitmap.Should().BeNull(); + } + + [Theory] + [BoolData] + public void DataObject_StringBool_ReturnFalse(bool autoConvert) + { + DataObject dataObject = new(); + ((IDataObject)dataObject).TryGetData(DataFormats.Serializable, autoConvert, out Font? font).Should().BeFalse(); + font.Should().BeNull(); + } + + [Theory] + [BoolData] + public void DataObject_StringFuncBool_ReturnFalse(bool autoConvert) + { + DataObject dataObject = new(); + ((IDataObject)dataObject).TryGetData(DataFormats.SymbolicLink, Resolver, autoConvert, out DateTime? date).Should().BeFalse(); + date.Should().BeNull(); + } + + [Fact] + public void TypedDataObject_CallsITypedDataObject() + { + TypedDataObject dataObject = new(); + ((IDataObject)dataObject).TryGetData(out string? _).Should().BeFalse(); + dataObject.VerifyTryGetDataCalled(); + } + + [Fact] + public void TypedDataObject_String_CallsITypedDataObject() + { + TypedDataObject dataObject = new(); + ((IDataObject)dataObject).TryGetData(DataFormats.Dib, out Bitmap? _).Should().BeFalse(); + dataObject.VerifyTryGetDataStringCalled(); + } + + [Theory] + [BoolData] + public void TypedDataObject_StringBool_CallsITypedDataObject(bool autoConvert) + { + TypedDataObject dataObject = new(); + ((IDataObject)dataObject).TryGetData(DataFormats.FileDrop, autoConvert, out int? _).Should().BeFalse(); + dataObject.VerifyTryGetDataStringBoolCalled(); + } + + [Theory] + [BoolData] + public void TypedDataObject_StringFuncBool_CallsITypedDataObject(bool autoConvert) + { + TypedDataObject dataObject = new(); + ((IDataObject)dataObject).TryGetData(DataFormats.SymbolicLink, Resolver, autoConvert, out DateTime? date).Should().BeFalse(); + dataObject.VerifyTryGetDataStringFuncBoolCalled(); + } + + internal class UntypedDataObject : IDataObject + { + public void VerifyGetDataWasNotCalled() + { + GetDataType_Count.Should().Be(0); + GetDataString_Count.Should().Be(0); + GetDataStringBool_Count.Should().Be(0); + } + + private int GetDataStringBool_Count { get; set; } + public object? GetData(string format, bool autoConvert) + { + GetDataStringBool_Count++; + return null; + } + + private int GetDataString_Count { get; set; } + public object? GetData(string format) + { + GetDataString_Count++; + return null; + } + + private int GetDataType_Count { get; set; } + public object? GetData(Type format) + { + GetDataType_Count++; + return null; + } + + public bool GetDataPresent(string format, bool autoConvert) => throw new NotImplementedException(); + public bool GetDataPresent(string format) => throw new NotImplementedException(); + public bool GetDataPresent(Type format) => throw new NotImplementedException(); + public string[] GetFormats(bool autoConvert) => throw new NotImplementedException(); + public string[] GetFormats() => throw new NotImplementedException(); + public void SetData(string format, bool autoConvert, object? data) => throw new NotImplementedException(); + public void SetData(string format, object? data) => throw new NotImplementedException(); + public void SetData(Type format, object? data) => throw new NotImplementedException(); + public void SetData(object? data) => throw new NotImplementedException(); + } + + internal class TypedDataObject : IDataObject, ITypedDataObject + { + public object? GetData(string format, bool autoConvert) => throw new NotImplementedException(); + public object? GetData(string format) => throw new NotImplementedException(); + public object? GetData(Type format) => throw new NotImplementedException(); + public bool GetDataPresent(string format, bool autoConvert) => throw new NotImplementedException(); + public bool GetDataPresent(string format) => throw new NotImplementedException(); + public bool GetDataPresent(Type format) => throw new NotImplementedException(); + public string[] GetFormats(bool autoConvert) => throw new NotImplementedException(); + public string[] GetFormats() => throw new NotImplementedException(); + public void SetData(string format, bool autoConvert, object? data) => throw new NotImplementedException(); + public void SetData(string format, object? data) => throw new NotImplementedException(); + public void SetData(Type format, object? data) => throw new NotImplementedException(); + public void SetData(object? data) => throw new NotImplementedException(); + + private int _tryGetDataCalledCount; + private int _tryGetDataStringCalledCount; + private int _tryGetDataStringBoolCalledCount; + private int _tryGetDataStringFuncBoolCalledCount; + + public void VerifyTryGetDataCalled() + { + _tryGetDataCalledCount.Should().Be(1); + _tryGetDataStringCalledCount.Should().Be(0); + _tryGetDataStringBoolCalledCount.Should().Be(0); + _tryGetDataStringFuncBoolCalledCount.Should().Be(0); + } + + public void VerifyTryGetDataStringCalled() + { + _tryGetDataCalledCount.Should().Be(0); + _tryGetDataStringCalledCount.Should().Be(1); + _tryGetDataStringBoolCalledCount.Should().Be(0); + _tryGetDataStringFuncBoolCalledCount.Should().Be(0); + } + + public void VerifyTryGetDataStringBoolCalled() + { + _tryGetDataCalledCount.Should().Be(0); + _tryGetDataStringCalledCount.Should().Be(0); + _tryGetDataStringBoolCalledCount.Should().Be(1); + _tryGetDataStringFuncBoolCalledCount.Should().Be(0); + } + + public void VerifyTryGetDataStringFuncBoolCalled() + { + _tryGetDataCalledCount.Should().Be(0); + _tryGetDataStringCalledCount.Should().Be(0); + _tryGetDataStringBoolCalledCount.Should().Be(0); + _tryGetDataStringFuncBoolCalledCount.Should().Be(1); + } + + public bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>([MaybeNullWhen(false), NotNullWhen(true)] out T data) + { + _tryGetDataCalledCount++; + data = default; + return false; + } + + public bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(string format, [MaybeNullWhen(false), NotNullWhen(true)] out T data) + { + _tryGetDataStringCalledCount++; + data = default; + return false; + } + + public bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(string format, bool autoConvert, [MaybeNullWhen(false), NotNullWhen(true)] out T data) + { + _tryGetDataStringBoolCalledCount++; + data = default; + return false; + } + + public bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(string format, Func resolver, bool autoConvert, [MaybeNullWhen(false), NotNullWhen(true)] out T data) + { + _tryGetDataStringFuncBoolCalledCount++; + data = default; + return false; + } + } +} diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectTests.cs index 3059bb5fedb..ac97900cb22 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectTests.cs @@ -3,6 +3,7 @@ using System.Collections.Specialized; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Reflection.Metadata; using System.Runtime.InteropServices; @@ -19,7 +20,7 @@ namespace System.Windows.Forms.Tests; // NB: doesn't require thread affinity public partial class DataObjectTests { - #pragma warning disable WFDEV005 // Type or member is obsolete +#pragma warning disable WFDEV005 // Type or member is obsolete private static readonly string[] s_restrictedClipboardFormats = [ DataFormats.CommaSeparatedValue, @@ -367,54 +368,102 @@ public void DataObject_GetData_InvokeTypeMocked_CallsGetData(Type format, object mockDataObject.Verify(o => o.GetData(formatName), Times.Exactly(expectedCallCount)); } - [Fact] - public void DataObject_TryGetData_InvokeString_CallsTryGetData() + internal class DataObjectOverridesTryGetDataCore : DataObject { - string data = "text"; - Mock mockDataObject = new(MockBehavior.Strict); - mockDataObject - .Setup(o => o.TryGetData(out data)) - .CallBase(); - string formatName = typeof(string).FullName!; - mockDataObject - .Setup(o => o.TryGetData(formatName, out data)) - .Returns(true) - .Verifiable(); - mockDataObject.Object.TryGetData(out data).Should().BeTrue(); - mockDataObject.Verify(o => o.TryGetData(formatName, out data), Times.Exactly(1)); + private readonly string _format; + private readonly Func _resolver; + private readonly bool _autoConvert; + + public DataObjectOverridesTryGetDataCore(string format, Func resolver, bool autoConvert) : base() + { + _format = format; + _resolver = resolver; + _autoConvert = autoConvert; + } + + public int Count { get; private set; } + + protected override bool TryGetDataCore<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + string format, + Func resolver, + bool autoConvert, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) + { + format.Should().Be(_format); + resolver.Should().BeEquivalentTo(_resolver); + autoConvert.Should().Be(_autoConvert); + typeof(T).Should().Be(typeof(string)); + + Count++; + data = default; + return false; + } } [Fact] - public void DataObject_TryGetData_InvokeStringString_CallsTryGetData() + public void DataObject_TryGetData_InvokeString_CallsTryGetDataCore() { - string data = "text"; - Mock mockDataObject = new(MockBehavior.Strict); - mockDataObject - .Setup(o => o.TryGetData("test format", out data)) - .CallBase(); - mockDataObject - .Setup(o => o.TryGetData("test format", false, out data)) - .Returns(true) - .Verifiable(); - mockDataObject.Object.TryGetData("test format", out data).Should().BeTrue(); - mockDataObject.Verify(o => o.TryGetData("test format", false, out data), Times.Exactly(1)); + DataObjectOverridesTryGetDataCore dataObject = new(typeof(string).FullName, DataObject.NotSupportedResolver, autoConvert: false); + dataObject.Count.Should().Be(0); + + dataObject.TryGetData(out string data).Should().BeFalse(); + data.Should().BeNull(); + dataObject.Count.Should().Be(1); + } + + public static TheoryData RestrictedAndUnrestrictedFormats => + [ + DataFormats.Bitmap, + DataFormats.CommaSeparatedValue, + "something custom" + ]; + + [Theory] + [MemberData(nameof(RestrictedAndUnrestrictedFormats))] + public void DataObject_TryGetData_InvokeStringString_CallsTryGetDataCore(string format) + { + DataObjectOverridesTryGetDataCore dataObject = new(format, DataObject.NotSupportedResolver, autoConvert: false); + dataObject.Count.Should().Be(0); + + dataObject.TryGetData(format, out string data).Should().BeFalse(); + data.Should().BeNull(); + dataObject.Count.Should().Be(1); } + public static TheoryData FormatAndAutoConvert => new() + { + { DataFormats.Bitmap, true }, + { DataFormats.CommaSeparatedValue, true }, + { "something custom", true }, + { DataFormats.Bitmap, false }, + { DataFormats.CommaSeparatedValue, false }, + { "something custom", false } + }; + [Theory] - [BoolData] - public void DataObject_TryGetData_InvokeStringBoolString_CallsTryGetData(bool autoConvert) + [MemberData(nameof(FormatAndAutoConvert))] + public void DataObject_TryGetData_InvokeStringBoolString_CallsTryGetDataCore(string format, bool autoConvert) { - string data = "text"; - Mock mockDataObject = new(MockBehavior.Strict); - mockDataObject - .Setup(o => o.TryGetData("test format", autoConvert, out data)) - .CallBase(); - mockDataObject - .Setup(o => o.TryGetData("test format", DataObject.NotSupportedResolver, autoConvert, out data)) - .Returns(true) - .Verifiable(); - mockDataObject.Object.TryGetData("test format", autoConvert, out data).Should().BeTrue(); - mockDataObject.Verify(o => o.TryGetData("test format", DataObject.NotSupportedResolver, autoConvert, out data), Times.Exactly(1)); + DataObjectOverridesTryGetDataCore dataObject = new(format, DataObject.NotSupportedResolver, autoConvert); + dataObject.Count.Should().Be(0); + + dataObject.TryGetData(format, autoConvert, out string data).Should().BeFalse(); + data.Should().BeNull(); + dataObject.Count.Should().Be(1); + } + + [Theory] + [MemberData(nameof(FormatAndAutoConvert))] + public void DataObject_TryGetData_InvokeStringFuncBoolString_CallsTryGetDataCore(string format, bool autoConvert) + { + DataObjectOverridesTryGetDataCore dataObject = new(format, Resolver, autoConvert); + dataObject.Count.Should().Be(0); + + dataObject.TryGetData(format, Resolver, autoConvert, out string data).Should().BeFalse(); + data.Should().BeNull(); + dataObject.Count.Should().Be(1); + + static Type Resolver(TypeName typeName) => typeof(string); } [Theory] diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DragEventArgsTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DragEventArgsTests.cs index cf84c21505d..3962bf508e3 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DragEventArgsTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DragEventArgsTests.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Reflection.Metadata; - namespace System.Windows.Forms.Tests; // NB: doesn't require thread affinity @@ -103,10 +101,6 @@ private class CustomDataObject : IDataObject public object GetData(string format, bool autoConvert) => throw new NotImplementedException(); public object GetData(string format) => throw new NotImplementedException(); public object GetData(Type format) => throw new NotImplementedException(); - public bool TryGetData(string format, Func resolver, bool autoConvert, out T data) => throw new NotImplementedException(); - public bool TryGetData(string format, bool autoConvert, out T data) => throw new NotImplementedException(); - public bool TryGetData(string format, out T data) => throw new NotImplementedException(); - public bool TryGetData(out T data) => throw new NotImplementedException(); public void SetData(string format, bool autoConvert, object data) => throw new NotImplementedException(); public void SetData(string format, object data) => throw new NotImplementedException(); public void SetData(Type format, object data) => throw new NotImplementedException(); diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/IDataObjectTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/IDataObjectTests.cs deleted file mode 100644 index db04a6e318d..00000000000 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/IDataObjectTests.cs +++ /dev/null @@ -1,146 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Reflection.Metadata; - -namespace System.Windows.Forms.Tests; - -#nullable enable - -// Test default implementation of IDataObject.TryGetData overloads. -public partial class IDataObjectTests -{ - [Fact] - public void IDataObject_TryGetData_Invoke_ReturnsFalse() - { - DefaultTryGetMethodsDataObject dataObject = new(); - ((IDataObject)dataObject).TryGetData(out string? _).Should().BeFalse(); - dataObject.GetDataType_Count.Should().Be(1); - dataObject.GetDataString_Count.Should().Be(0); - dataObject.GetDataStringBool_Count.Should().Be(0); - } - - [Fact] - public void IDataObject_TryGetData_InvokeString_ReturnsFalse() - { - DefaultTryGetMethodsDataObject dataObject = new(); - ((IDataObject)dataObject).TryGetData("TryGetDataString", out string? _).Should().BeFalse(); - dataObject.GetDataType_Count.Should().Be(0); - dataObject.GetDataString_Count.Should().Be(1); - dataObject.GetDataStringBool_Count.Should().Be(0); - } - - [Theory] - [BoolData] - public void IDataObject_TryGetData_InvokeStringBool_ReturnsFalse(bool autoConvert) - { - DefaultTryGetMethodsDataObject dataObject = new(); - ((IDataObject)dataObject).TryGetData("TryGetDataStringBool", autoConvert, out string? _).Should().BeFalse(); - dataObject.GetDataType_Count.Should().Be(0); - dataObject.GetDataString_Count.Should().Be(0); - dataObject.GetDataStringBool_Count.Should().Be(1); - } - - [Theory] - [BoolData] - public void IDataObject_TryGetData_InvokeStringResolverBool_ReturnsFalse(bool autoConvert) - { - DefaultTryGetMethodsDataObject dataObject = new(); - ((IDataObject)dataObject).TryGetData("TryGetDataStringBool", Resolver, autoConvert, out string? _).Should().BeFalse(); - dataObject.GetDataType_Count.Should().Be(0); - dataObject.GetDataString_Count.Should().Be(0); - dataObject.GetDataStringBool_Count.Should().Be(1); - } - - [Fact] - public void IDataObject_TryGetData_Invoke_ReturnsTrue() - { - DefaultTryGetMethodsDataObject dataObject = new(); - ((IDataObject)dataObject).TryGetData(out TryGetData1? _).Should().BeTrue(); - dataObject.GetDataType_Count.Should().Be(1); - dataObject.GetDataString_Count.Should().Be(0); - dataObject.GetDataStringBool_Count.Should().Be(0); - } - - [Fact] - public void IDataObject_TryGetData_InvokeString_ReturnsTrue() - { - DefaultTryGetMethodsDataObject dataObject = new(); - ((IDataObject)dataObject).TryGetData("TryGetDataString", out TryGetDataString? _).Should().BeTrue(); - dataObject.GetDataType_Count.Should().Be(0); - dataObject.GetDataString_Count.Should().Be(1); - dataObject.GetDataStringBool_Count.Should().Be(0); - } - - [Theory] - [BoolData] - public void IDataObject_TryGetData_InvokeStringBool_ReturnsTrue(bool autoConvert) - { - DefaultTryGetMethodsDataObject dataObject = new(); - ((IDataObject)dataObject).TryGetData("TryGetDataStringBool", autoConvert, out TryGetDataStringBool? _).Should().BeTrue(); - dataObject.GetDataType_Count.Should().Be(0); - dataObject.GetDataString_Count.Should().Be(0); - dataObject.GetDataStringBool_Count.Should().Be(1); - } - - [Theory] - [BoolData] - public void IDataObject_TryGetData_InvokeStringResolverBool_ReturnsTrue(bool autoConvert) - { - DefaultTryGetMethodsDataObject dataObject = new(); - ((IDataObject)dataObject).TryGetData("TryGetDataStringBool", Resolver, autoConvert, out TryGetDataStringBool? _).Should().BeTrue(); - dataObject.GetDataType_Count.Should().Be(0); - dataObject.GetDataString_Count.Should().Be(0); - dataObject.GetDataStringBool_Count.Should().Be(1); - } - - private static Type Resolver(TypeName typeName) => throw new NotImplementedException(); - - internal class TryGetData1() { } - private class TryGetDataString() { } - private class TryGetDataStringBool() { } - - internal class DefaultTryGetMethodsDataObject : IDataObject - { - internal int GetDataStringBool_Count { get; set; } - /// - /// Invoked from - /// and from . - /// - public object? GetData(string format, bool autoConvert) - { - GetDataStringBool_Count++; - return format == "TryGetDataStringBool" ? new TryGetDataStringBool() : null; - } - - internal int GetDataString_Count { get; set; } - /// - /// Invoked from - /// - public object? GetData(string format) - { - GetDataString_Count++; - return format == "TryGetDataString" ? new TryGetDataString() : null; - } - - internal int GetDataType_Count { get; set; } - /// - /// Invoked from - /// - public object? GetData(Type format) - { - GetDataType_Count++; - return format == typeof(TryGetData1) ? new TryGetData1() : null; - } - - public bool GetDataPresent(string format, bool autoConvert) => throw new NotImplementedException(); - public bool GetDataPresent(string format) => throw new NotImplementedException(); - public bool GetDataPresent(Type format) => throw new NotImplementedException(); - public string[] GetFormats(bool autoConvert) => throw new NotImplementedException(); - public string[] GetFormats() => throw new NotImplementedException(); - public void SetData(string format, bool autoConvert, object? data) => throw new NotImplementedException(); - public void SetData(string format, object? data) => throw new NotImplementedException(); - public void SetData(Type format, object? data) => throw new NotImplementedException(); - public void SetData(object? data) => throw new NotImplementedException(); - } -} From c27b123b28f1abd956a2d7cec50b49260bfd224a Mon Sep 17 00:00:00 2001 From: Tanya Solyanik Date: Mon, 11 Nov 2024 19:33:45 -0800 Subject: [PATCH 7/8] removed [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] from T as it's not sufficient, it does not preserve types recursively and can't be used with the Func NRBF deserialization is on by default, user has to opt in into the BF deserialization and opt-out from the NRBF deserialization to get full compatibility clean up in xml-doc comments --- ...inaryFormatterInClipboardDragDropScope.cs} | 8 +- ...ipboardDragDropEnableNrbfSerialization.cs} | 8 +- .../MyServices/ClipboardProxyTests.cs | 5 +- .../Nrbf/SerializationRecordExtensions.cs | 14 +- .../LocalAppContextSwitches.cs | 10 +- .../src/System/Resources/ResXDataNode.cs | 2 +- .../src/System/Windows/Forms/OLE/Clipboard.cs | 157 ++++++++-------- .../System/Windows/Forms/OLE/DataFormats.cs | 2 +- .../Forms/OLE/DataObject.BitmapBinder.cs | 6 +- ...bject.Composition.BinaryFormatUtilities.cs | 20 +- .../OLE/DataObject.Composition.Binder.cs | 171 ++++++++---------- ...ect.Composition.NativeToWinFormsAdapter.cs | 42 +++-- ...DataObject.Composition.TypeNameComparer.cs | 114 ++++++++++++ .../Forms/OLE/DataObject.Composition.cs | 32 ++-- .../Windows/Forms/OLE/DataObject.DataStore.cs | 60 +++--- .../System/Windows/Forms/OLE/DataObject.cs | 169 ++++++++--------- .../Windows/Forms/OLE/DataObjectExtensions.cs | 77 +++----- .../Windows/Forms/OLE/DragDropHelper.cs | 8 +- .../System/Windows/Forms/OLE/IDataObject.cs | 10 + .../Windows/Forms/OLE/ITypedDataObject.cs | 77 +++++--- .../ComDisabledTests/ClipboardComTests.cs | 7 +- ...aryFormatUtilitiesTests.FullCompatScope.cs | 28 +++ .../Forms/BinaryFormatUtilitiesTests.cs | 139 ++++++++------ .../System/Windows/Forms/ClipboardTests.cs | 45 +++-- .../Forms/DataObjectExtensionsTests.cs | 26 +-- .../System/Windows/Forms/DataObjectTests.cs | 2 +- 26 files changed, 703 insertions(+), 536 deletions(-) rename src/Common/tests/TestUtilities/{BinaryFormatterInClipboardScope.cs => BinaryFormatterInClipboardDragDropScope.cs} (64%) rename src/Common/tests/TestUtilities/{NrbfSerializerInClipboardScope.cs => ClipboardDragDropEnableNrbfSerialization.cs} (63%) create mode 100644 src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.TypeNameComparer.cs create mode 100644 src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/BinaryFormatUtilitiesTests.FullCompatScope.cs diff --git a/src/Common/tests/TestUtilities/BinaryFormatterInClipboardScope.cs b/src/Common/tests/TestUtilities/BinaryFormatterInClipboardDragDropScope.cs similarity index 64% rename from src/Common/tests/TestUtilities/BinaryFormatterInClipboardScope.cs rename to src/Common/tests/TestUtilities/BinaryFormatterInClipboardDragDropScope.cs index 37fccaed8ef..e33bf87fbac 100644 --- a/src/Common/tests/TestUtilities/BinaryFormatterInClipboardScope.cs +++ b/src/Common/tests/TestUtilities/BinaryFormatterInClipboardDragDropScope.cs @@ -3,13 +3,13 @@ namespace System; -public readonly ref struct BinaryFormatterInClipboardScope +public readonly ref struct BinaryFormatterInClipboardDragDropScope { private readonly AppContextSwitchScope _switchScope; - public BinaryFormatterInClipboardScope(bool enable) + public BinaryFormatterInClipboardDragDropScope(bool enable) { - Monitor.Enter(typeof(BinaryFormatterInClipboardScope)); + Monitor.Enter(typeof(BinaryFormatterInClipboardDragDropScope)); _switchScope = new(AppContextSwitchNames.ClipboardDragDropEnableUnsafeBinaryFormatterSerializationSwitchName, enable); } @@ -21,7 +21,7 @@ public void Dispose() } finally { - Monitor.Exit(typeof(BinaryFormatterInClipboardScope)); + Monitor.Exit(typeof(BinaryFormatterInClipboardDragDropScope)); } } } diff --git a/src/Common/tests/TestUtilities/NrbfSerializerInClipboardScope.cs b/src/Common/tests/TestUtilities/ClipboardDragDropEnableNrbfSerialization.cs similarity index 63% rename from src/Common/tests/TestUtilities/NrbfSerializerInClipboardScope.cs rename to src/Common/tests/TestUtilities/ClipboardDragDropEnableNrbfSerialization.cs index 9511473c2a9..defa6a0bd48 100644 --- a/src/Common/tests/TestUtilities/NrbfSerializerInClipboardScope.cs +++ b/src/Common/tests/TestUtilities/ClipboardDragDropEnableNrbfSerialization.cs @@ -3,13 +3,13 @@ namespace System; -public readonly ref struct NrbfSerializerInClipboardScope +public readonly ref struct NrbfSerializerInClipboardDragDropScope { private readonly AppContextSwitchScope _switchScope; - public NrbfSerializerInClipboardScope(bool enable) + public NrbfSerializerInClipboardDragDropScope(bool enable) { - Monitor.Enter(typeof(NrbfSerializerInClipboardScope)); + Monitor.Enter(typeof(NrbfSerializerInClipboardDragDropScope)); _switchScope = new(AppContextSwitchNames.ClipboardDragDropEnableNrbfSerializationSwitchName, enable); } @@ -21,7 +21,7 @@ public void Dispose() } finally { - Monitor.Exit(typeof(NrbfSerializerInClipboardScope)); + Monitor.Exit(typeof(NrbfSerializerInClipboardDragDropScope)); } } } diff --git a/src/Microsoft.VisualBasic/tests/UnitTests/Microsoft/VisualBasic/MyServices/ClipboardProxyTests.cs b/src/Microsoft.VisualBasic/tests/UnitTests/Microsoft/VisualBasic/MyServices/ClipboardProxyTests.cs index fd711b52f38..2ac06a80c5f 100644 --- a/src/Microsoft.VisualBasic/tests/UnitTests/Microsoft/VisualBasic/MyServices/ClipboardProxyTests.cs +++ b/src/Microsoft.VisualBasic/tests/UnitTests/Microsoft/VisualBasic/MyServices/ClipboardProxyTests.cs @@ -114,12 +114,9 @@ public DataWithObjectField(string text1, object object2) private static Type DataResolver(TypeName typeName) { Type type = typeof(DataWithObjectField); - TypeName parsed = TypeName.Parse($"{type.FullName}, {type.Assembly.FullName}"); // Namespace-qualified type name. - if (typeName.FullName == parsed.FullName - // Ignore version, culture, and public key token in the assembly name. - && typeName.AssemblyName?.Name == parsed.AssemblyName?.Name) + if (type.FullName == typeName.FullName) { return type; } diff --git a/src/System.Private.Windows.Core/src/System/Private/Windows/Core/Nrbf/SerializationRecordExtensions.cs b/src/System.Private.Windows.Core/src/System/Private/Windows/Core/Nrbf/SerializationRecordExtensions.cs index 843e884709e..1398a9ad930 100644 --- a/src/System.Private.Windows.Core/src/System/Private/Windows/Core/Nrbf/SerializationRecordExtensions.cs +++ b/src/System.Private.Windows.Core/src/System/Private/Windows/Core/Nrbf/SerializationRecordExtensions.cs @@ -51,6 +51,7 @@ internal static SerializationRecord Decode(this Stream stream, out IReadOnlyDict catch (Exception ex) when (ex is ArgumentException or InvalidCastException or ArithmeticException or IOException) { // Make the exception easier to catch, but retain the original stack trace. + // TODO(TanyaSo) is this really converted to NotSupported up the stack?? throw ex.ConvertToSerializationException(); } catch (TargetInvocationException ex) @@ -63,7 +64,7 @@ internal static SerializationRecord Decode(this Stream stream, out IReadOnlyDict /// Deserializes the to an object. /// [RequiresUnreferencedCode("Ultimately calls resolver for type names in the data.")] - public static object Deserialize( + public static object? Deserialize( this SerializationRecord rootRecord, IReadOnlyDictionary recordMap, ITypeResolver typeResolver) @@ -77,15 +78,12 @@ public static object Deserialize( { return Deserializer.Deserialize(rootRecord.Id, recordMap, options); } - catch (Exception ex) when (ex is ArgumentException or InvalidCastException or ArithmeticException or IOException) - { - // Make the exception easier to catch, but retain the original stack trace. - throw ex.ConvertToSerializationException(); - } - catch (TargetInvocationException ex) + catch (Exception ex) when (ex is ArgumentException or InvalidCastException or ArithmeticException or IOException or TargetInvocationException or SerializationException) { - throw ExceptionDispatchInfo.Capture(ex.InnerException!).SourceException.ConvertToSerializationException(); + Debug.WriteLine(ex.ToString()); } + + return null; } internal delegate bool TryGetDelegate(SerializationRecord record, [NotNullWhen(true)] out object? value); diff --git a/src/System.Windows.Forms.Primitives/src/System/LocalAppContextSwitches/LocalAppContextSwitches.cs b/src/System.Windows.Forms.Primitives/src/System/LocalAppContextSwitches/LocalAppContextSwitches.cs index 78a0f666b88..9dd8c6d8699 100644 --- a/src/System.Windows.Forms.Primitives/src/System/LocalAppContextSwitches/LocalAppContextSwitches.cs +++ b/src/System.Windows.Forms.Primitives/src/System/LocalAppContextSwitches/LocalAppContextSwitches.cs @@ -121,6 +121,11 @@ static bool GetSwitchDefaultValue(string switchName) return false; } + if (switchName == ClipboardDragDropEnableNrbfSerializationSwitchName) + { + return true; + } + if (framework.Version.Major >= 8) { // Behavior changes added in .NET 8 @@ -245,10 +250,13 @@ public static bool ClipboardDragDropEnableUnsafeBinaryFormatterSerialization /// /// If , then Clipboard Get methods will use System.Windows.Forms.BinaryFormat.Deserializer /// to deserialize the payload if needed. This is an alternative to deserialization using use . + /// This option is enabled by default, disable it and enable the deserialization + /// to get full compatibility with the downlevel versions of .NET. /// public static bool ClipboardDragDropEnableNrbfSerialization { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => GetCachedSwitchValue(ClipboardDragDropEnableNrbfSerializationSwitchName, ref s_clipboardDragDropEnableNrbfSerialization); + get => + GetCachedSwitchValue(ClipboardDragDropEnableNrbfSerializationSwitchName, ref s_clipboardDragDropEnableNrbfSerialization); } } diff --git a/src/System.Windows.Forms/src/System/Resources/ResXDataNode.cs b/src/System.Windows.Forms/src/System/Resources/ResXDataNode.cs index de384a8e37e..c52897a0d83 100644 --- a/src/System.Windows.Forms/src/System/Resources/ResXDataNode.cs +++ b/src/System.Windows.Forms/src/System/Resources/ResXDataNode.cs @@ -160,7 +160,7 @@ public string Name } set { - ArgumentException.ThrowIfNullOrEmpty(value, nameof(Name)); + ArgumentException.ThrowIfNullOrEmpty(value); _name = value; } } diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/Clipboard.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/Clipboard.cs index 8c2afab2592..366afb96b57 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/Clipboard.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/Clipboard.cs @@ -32,6 +32,11 @@ public static void SetDataObject(object data, bool copy) => /// Places data on the system and uses copy to specify whether the data /// should remain on the after the application exits. /// + /// + /// + /// See remarks for for recommendations on how to implement custom . + /// + /// public static unsafe void SetDataObject(object data, bool copy, int retryTimes, int retryDelay) { if (Application.OleRequired() != ApartmentState.STA) @@ -221,21 +226,8 @@ public static bool ContainsText(TextDataFormat format) /// /// Retrieves an audio stream from the . /// - public static Stream? GetAudioStream() - { - IDataObject? data = GetDataObject(); - if (data is ITypedDataObject typed) - { - return typed.TryGetData(DataFormats.WaveAudioConstant, out Stream? stream) ? stream : null; - } - - if (data is IDataObject dataObject && dataObject.GetData(DataFormats.WaveAudioConstant) is Stream stream1) - { - return stream1; - } - - return null; - } + public static Stream? GetAudioStream() => + GetLegacyData(DataFormats.WaveAudioConstant); /// /// Retrieves data from the in the specified format. @@ -254,46 +246,69 @@ public static bool ContainsText(TextDataFormat format) private static object? GetData(string format, bool autoConvert) => GetDataObject() is IDataObject dataObject ? dataObject.GetData(format, autoConvert) : null; - /// - /// Retrieves data from the in the specified format if that data is of type . - /// This is a safer alternative to that does not use to deserialize the payload. - /// - /// The current thread is not in single-threaded apartment (STA) mode. - /// This value requires deserialization. - public static bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + /// + public static bool TryGetData( string format, [NotNullWhen(true), MaybeNullWhen(false)] out T data) { data = default; - if (GetDataObject() is ITypedDataObject dataObject) + var dataObject = GetDataObject(); + if (dataObject is null) + { + return false; + } + + if (dataObject is ITypedDataObject typed) { // Custom IDataObjects should handle their own validation. - if (dataObject is DataObject && !DataObject.ValidateTryGetDataArguments(format)) + if (typed is DataObject && !DataObject.ValidateTryGetDataArguments(format)) { return false; } - return dataObject.TryGetData(format, DataObject.NotSupportedResolver, autoConvert: false, out data); + return typed.TryGetData(format, DataObject.NotSupportedResolver, autoConvert: false, out data); } - return false; + throw new NotSupportedException($"{nameof(IDataObject)} on the {nameof(Clipboard)} does not" + + $" support {nameof(ITypedDataObject)} and can't be read using `TryGetData(string, out T)` method."); } /// /// Retrieves data from the in the specified format if that data is of type . - /// This is a safer alternative to that uses within constrains - /// defined by . - /// + /// This is an alternative to that uses only when application + /// opts-in into switch named "Windows.ClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization". + /// By default the NRBF deserializer attempts to deserialize the stream. + /// + /// + /// A string that specifies what format to retrieve the data as. + /// See the class for a set of predefined data formats. + /// + /// /// Resolver is used only when deserializing non-OLE formats. It returns the type if is allowed or /// throws a if type is not expected. It should not return a . - /// - /// - /// The current thread is not in single-threaded apartment (STA) mode. + /// Type requested by the user is resolved automatically, it does not have to be resolved by this function. + /// The following types are resolved automatically: + /// 1.NRBF primitive types + /// (bool, byte, char, decimal, double, short, int, long, sbyte, ushort, uint, ulong, float, string, TimeSpan, DateTime). + /// 2.System.Drawing.Primitive.dll exchange types (PointF, RectangleF, Point, Rectangle, SizeF, Size, Color). + /// 3.Types commonly used in WinForms applications (System.Drawing.Bitmap, System.Windows.Forms.ImageListStreamer, + /// System.NotSupportedException(only the message is re-hydrated), List{T} where T is an NRBF primitive type, arrays of NRBF primitive types). + /// + /// + /// An object that contains the data in the specified format, or if the data is unavailable in the specified format, + /// or is of a wrong . + /// + /// + /// if the data of this format is present and the value is + /// of a matching type and that value can be successfully retrieved, or + /// if the format is not present or the value is of a wrong type. + /// /// /// If application does not support and the object can't be deserialized otherwise, or - /// application supports but is an or not a concrete type, - /// or if does not resolve the actual payload type. - /// + /// application supports but is an , or not a concrete type, + /// or if does not resolve the actual payload type. Or the on + /// the does not implement interface. + /// /// /// /// - public static bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + [CLSCompliant(false)] + public static bool TryGetData( string format, -#pragma warning disable CS3001 // Argument type is not CLS-compliant Func resolver, -#pragma warning restore CS3001 [NotNullWhen(true), MaybeNullWhen(false)] out T data) { data = default; - if (GetDataObject() is ITypedDataObject dataObject) + var dataObject = GetDataObject(); + if (dataObject is ITypedDataObject typed) { - return dataObject.TryGetData(format, resolver, autoConvert: false, out data); + return typed.TryGetData(format, resolver, autoConvert: false, out data); } - return false; + throw new NotSupportedException($"{nameof(IDataObject)} on the {nameof(Clipboard)} does not" + + $" support {nameof(ITypedDataObject)} and can't be read using" + + $" `TryGetData(string, Func, out T)` method."); } /// @@ -343,20 +358,7 @@ public static StringCollection GetFileDropList() { StringCollection result = []; - IDataObject? data = GetDataObject(); - if (data is ITypedDataObject typed) - { - if (typed.TryGetData( - DataFormats.FileDropConstant, - DataObject.NotSupportedResolver, - autoConvert: true, - out string[]? strings)) - { - result.AddRange(strings); - } - } - else if (data is IDataObject dataObject - && dataObject.GetData(DataFormats.FileDropConstant, autoConvert: true) is string[] strings) + if (GetLegacyData(DataFormats.FileDropConstant) is string[] strings) { result.AddRange(strings); } @@ -371,22 +373,7 @@ public static StringCollection GetFileDropList() /// s are re-hydrated from a by reading a byte array /// but if that fails, is restricted by the . /// - public static Image? GetImage() - { - IDataObject? data = GetDataObject(); - if (data is ITypedDataObject typed) - { - typed.TryGetData(DataFormats.Bitmap, DataObject.NotSupportedResolver, autoConvert: true, out Image? image); - return image; - } - - if (data is IDataObject dataObject && dataObject.GetData(DataFormats.Bitmap, autoConvert: true) is Image image1) - { - return image1; - } - - return null; - } + public static Image? GetImage() => GetLegacyData(DataFormats.Bitmap); /// /// Retrieves text data from the in the format. @@ -401,18 +388,23 @@ public static string GetText(TextDataFormat format) { SourceGenerated.EnumValidator.Validate(format, nameof(format)); + return GetLegacyData(ConvertToDataFormats(format)) is string text ? text : string.Empty; + } + + private static T? GetLegacyData(string format) + { IDataObject? data = GetDataObject(); if (data is ITypedDataObject typed) { - return typed.TryGetData(ConvertToDataFormats(format), out string? text) ? text : string.Empty; + return typed.TryGetData(format, autoConvert: true, out T? value) ? value : default; } if (data is IDataObject dataObject) { - return dataObject.GetData(ConvertToDataFormats(format), autoConvert: true) is string text ? text : string.Empty; + return dataObject.GetData(format, autoConvert: true) is T value ? value : default; } - return string.Empty; + return default; } /// @@ -429,6 +421,11 @@ public static void SetAudio(Stream audioStream) => /// /// Clears the Clipboard and then adds data in the specified format. /// + /// + /// + /// See remarks for for recommendations on how to implement custom . + /// + /// public static void SetData(string format, object data) { if (string.IsNullOrWhiteSpace(format.OrThrowIfNull())) diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataFormats.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataFormats.cs index ae5a4bd8895..fdb407af0ff 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataFormats.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataFormats.cs @@ -154,7 +154,7 @@ public static partial class DataFormats /// public static Format GetFormat(string format) { - ArgumentException.ThrowIfNullOrWhiteSpace(format, nameof(format)); + ArgumentException.ThrowIfNullOrWhiteSpace(format); lock (s_internalSyncObject) { diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.BitmapBinder.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.BitmapBinder.cs index 2ed22156b55..8760a051e89 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.BitmapBinder.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.BitmapBinder.cs @@ -1,7 +1,6 @@ // 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.Immutable; using System.Drawing; using System.Private.Windows.Core.BinaryFormat; using System.Reflection; @@ -31,7 +30,6 @@ private sealed class BitmapBinder : SerializationBinder, ITypeResolver // .NET Framework PublicKeyToken=b03f5f7f11d50a3a private static ReadOnlySpan AllowedToken => [0xB0, 0x3F, 0x5F, 0x7F, 0x11, 0xD5, 0x0A, 0x3A]; - private static ImmutableArray AllowedTokenArray => AllowedToken.ToImmutableArray(); public override Type? BindToType(string assemblyName, string typeName) { @@ -72,13 +70,13 @@ public override void BindToName(Type serializedType, out string? assemblyName, o [RequiresUnreferencedCode("Calls System.Reflection.Assembly.GetType(String)")] [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] - Type ITypeResolver.GetType(TypeName typeName) + public Type GetType(TypeName typeName) { if (AllowedTypeName.Equals(typeName.Name, StringComparison.Ordinal) && typeName.AssemblyName is AssemblyNameInfo info) { if (AllowedAssemblyName.Equals(info.Name, StringComparison.Ordinal) - && AllowedTokenArray.SequenceEqual(info.PublicKeyOrToken)) + && AllowedToken.SequenceEqual(info.PublicKeyOrToken.AsSpan())) { return typeof(Bitmap); } diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.BinaryFormatUtilities.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.BinaryFormatUtilities.cs index 111bc153294..a82c9bd4079 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.BinaryFormatUtilities.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.BinaryFormatUtilities.cs @@ -101,7 +101,7 @@ record = stream.Decode(out recordMap); #if false // TODO(TanyaSo) - modify TryGetObjectFromJson to take a resolver and rename to HasJsonData??? // Return true if the payload contains valid JsonData struct, type matches or not // note: binder.GetType() throws and never returns null - // run isassignable in the json method + // run IsAssignable in the json method if (record.TryGetObjectFromJson(binder.GetType, out object? data)) { return data; @@ -119,15 +119,17 @@ record = stream.Decode(out recordMap); return value; } - // The legacy APIs do not provide resolver, resolver is required for the NRBF deserializer to work beyond the known types. - if (!legacyMode - && LocalAppContextSwitches.ClipboardDragDropEnableNrbfSerialization + // The legacy APIs do not provide resolver, even the default on because T is object. Resolver is required for the + // NRBF deserializer to work beyond the known types, so we are catching all exceptions here in order to fall back to the + // BinaryFormatter. NRBF deserializer is different from the BinaryFormatter in: + // 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) + if (LocalAppContextSwitches.ClipboardDragDropEnableNrbfSerialization && record.Deserialize(recordMap, (ITypeResolver)binder) is { } result) { return result; } - - if (LocalAppContextSwitches.ClipboardDragDropEnableUnsafeBinaryFormatterSerialization) + else if (LocalAppContextSwitches.ClipboardDragDropEnableUnsafeBinaryFormatterSerialization) { stream.Position = startPosition; return ReadObjectWithBinaryFormatter(stream, binder); @@ -136,20 +138,20 @@ record = stream.Decode(out recordMap); return null; } - // TanyaSo: this does not special-case the NotSupported exception, but we probably want to always deserialize it. + // TODO (TanyaSo): this does not special-case the NotSupported exception, but we probably want to always deserialize it. private static bool TypeNameIsAssignableToType(TypeName typeName, Type type, Func resolver) { Type? resolvedType = null; try { resolvedType = resolver(typeName); + return resolvedType?.IsAssignableTo(type) == true; } catch (Exception ex) when (!ex.IsCriticalException()) { - return false; } - return resolvedType?.IsAssignableTo(type) == true; + return false; } private static object? ReadObjectWithBinaryFormatter(MemoryStream stream, SerializationBinder binder) diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.Binder.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.Binder.cs index 4ffbef08a39..8d3375a2928 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.Binder.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.Binder.cs @@ -1,11 +1,12 @@ // 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.Immutable; using System.Private.Windows.Core.BinaryFormat; using System.Reflection.Metadata; using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; using Switches = System.Windows.Forms.Primitives.LocalAppContextSwitches; +using TypeInfo = System.Private.Windows.Core.BinaryFormat.TypeInfo; namespace System.Windows.Forms; @@ -13,6 +14,14 @@ public unsafe partial class DataObject { internal unsafe partial class Composition { + /// + /// A type resolver for use in the when processing + /// binary formatted stream contained in our class using the typed + /// consumption side APIs, such as. + /// This class resolves primitive types, exchange types from System.Drawing.Primitives, + /// and common WinForms types in addition to the requested by the user. + /// This type is used in and NRBF deserialization. + /// internal sealed class Binder : SerializationBinder, ITypeResolver { private readonly Func? _resolver; @@ -20,7 +29,7 @@ internal sealed class Binder : SerializationBinder, ITypeResolver private readonly bool _legacyMode; // This is needed to resolve fields of the requested type T when using deserializers. - private readonly Dictionary _mscorlibTypeCache = new() + private static readonly Dictionary s_mscorlibTypeCache = new() { { "System.Byte", typeof(byte) }, { "System.SByte", typeof(sbyte) }, @@ -75,7 +84,7 @@ internal sealed class Binder : SerializationBinder, ITypeResolver { "System.TimeSpan[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", typeof(TimeSpan[]) } }; - private readonly Dictionary<(string, string), Type> _commonTypes = new() + private static readonly Dictionary<(string, string), Type> s_commonTypes = new() { { ("System.Windows.Forms.ImageListStreamer", "System.Windows.Forms"), typeof(ImageListStreamer) }, { ("System.Drawing.Bitmap", "System.Drawing"), typeof(Drawing.Bitmap) }, @@ -90,51 +99,42 @@ internal sealed class Binder : SerializationBinder, ITypeResolver { ("System.Drawing.Color", "System.Drawing"), typeof(Drawing.Color) } }; + private readonly Dictionary _userTypes = new(TypeNameComparer.Default); + + /// + /// Type resolver for use with and NRBF deserializers to restrict types + /// that can be instantiated. + /// + /// that the user expects to read from the binary formatter stream. + /// + /// Provides the list of custom allowed types that user considered safe to + /// deserialize from the payload. Resolver should recognize the closure of all non-primitive and not known types + /// in the payload, such as field types and type in the inheritance hierarchy and the means to match + /// these types to the provided in the stream. + /// + /// + /// if the user had not requested any specific type, + /// i.e. the call originates from API family, + /// that returns an . if the user had requested a specific type, + /// using API family. + /// public Binder(Type type, Func? resolver, bool legacyMode) { _resolver = resolver; _type = type.OrThrowIfNull(); + _userTypes.Add(TypeName.Parse(type.AssemblyQualifiedName.OrThrowIfNull()), type); _legacyMode = legacyMode; } public override Type? BindToType(string assemblyName, string typeName) { - if (string.IsNullOrWhiteSpace(assemblyName)) - { - throw new ArgumentNullException(nameof(assemblyName)); - } - - if (string.IsNullOrWhiteSpace(typeName)) - { - throw new ArgumentNullException(nameof(typeName)); - } - - return GetType(assemblyName, typeName, null); - } + ArgumentException.ThrowIfNullOrWhiteSpace(assemblyName); + ArgumentException.ThrowIfNullOrWhiteSpace(typeName); - private Type? GetType(string assemblyName, string fullTypeName, TypeName? typeName) - { - // We assume all built-in types are normalized to the mscorlib assembly, as BinaryFormatter - // and NRBF reader and deserializer are doing so for compatibility with .NET Framework. - if (assemblyName.Equals(TypeInfo.MscorlibAssemblyName, StringComparison.Ordinal) - && _mscorlibTypeCache.TryGetValue(fullTypeName, out Type? builtIn)) + if (GetCachedType(assemblyName, typeName, null) is Type type) { - return builtIn; - } - - // Ignore version, culture, and public key token and compare the short names. - string shortAssemblyName = assemblyName.Split(',')[0].Trim(); - if (_commonTypes.TryGetValue((fullTypeName, shortAssemblyName), out Type? knownType)) - { - return knownType; - } - - typeName ??= TypeName.Parse($"{fullTypeName}, {assemblyName}"); - if (Matches(_type, typeName)) - { - _commonTypes.Add((fullTypeName, shortAssemblyName), _type); - return _type; + return type; } if (_legacyMode) @@ -148,94 +148,77 @@ public Binder(Type type, Func? resolver, bool legacyMode) if (_resolver is null) { throw new NotSupportedException($"'resolver' function is required in '{nameof(Clipboard.TryGetData)}'" + - $" method to resolve '{fullTypeName}' from '{assemblyName}'"); + $" method to resolve '{typeName}' from '{assemblyName}'"); } - Type type = _resolver(typeName) + TypeName parsed = TypeName.Parse($"{typeName}, {assemblyName}"); + Type resolved = _resolver(parsed) ?? throw new NotSupportedException($"'resolver' function provided in '{nameof(Clipboard.TryGetData)}'" + $" method should never return a null. It should throw a '{nameof(NotSupportedException)}' when encountering unsupported types."); - _commonTypes.Add((fullTypeName, shortAssemblyName), type); - return type; + _userTypes.Add(parsed, resolved); + return resolved; } - [RequiresUnreferencedCode("Calls System.Reflection.Assembly.GetType(String)")] - [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] - Type ITypeResolver.GetType(TypeName typeName) + private Type? GetCachedType(string assemblyName, string fullTypeName, TypeName? typeName) { - if (typeName.AssemblyName is null || typeName.AssemblyName.FullName is not string fullName || string.IsNullOrWhiteSpace(fullName)) + // We assume all built-in types are normalized to the mscorlib assembly, as BinaryFormatter + // and NRBF reader and deserializer are doing so for compatibility with .NET Framework. + if (assemblyName.Equals(TypeInfo.MscorlibAssemblyName, StringComparison.Ordinal) + && s_mscorlibTypeCache.TryGetValue(fullTypeName, out Type? builtIn)) { - throw new ArgumentException($"{nameof(TypeName.AssemblyName)} is missing.", nameof(typeName)); + return builtIn; } - Type? type; - try + // Ignore version, culture, and public key token and compare the short names. + string shortAssemblyName = assemblyName.Split(',')[0].Trim(); + if (s_commonTypes.TryGetValue((fullTypeName, shortAssemblyName), out Type? knownType)) { - type = GetType(fullName, typeName.FullName, typeName); + return knownType; } - catch (Exception e) + + typeName ??= TypeName.Parse($"{fullTypeName}, {assemblyName}"); + if (_userTypes.TryGetValue(typeName, out Type? userType)) { - throw new SerializationException($"Could not find type {typeName.AssemblyQualifiedName}", e); + return userType; } - return type ?? throw new SerializationException($"Could not find type {typeName.AssemblyQualifiedName}"); + return null; } - // Copied from https://github.com/dotnet/runtime/blob/79a71fc750652191eba18e19b3f98492e882cb5f/src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/SerializationRecord.cs#L68 - internal static bool Matches(Type type, TypeName typeName) + [RequiresUnreferencedCode("Calls System.Reflection.Assembly.GetType(String)")] + [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] + public Type GetType(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) + if (typeName.AssemblyName is not AssemblyNameInfo info || info.FullName is not string fullName || string.IsNullOrWhiteSpace(fullName)) { - return false; + throw new ArgumentException($"{nameof(TypeName.AssemblyName)} is missing.", nameof(typeName)); } - if (type.IsArray != typeName.IsArray - || type.IsConstructedGenericType != typeName.IsConstructedGenericType - || type.IsNested != typeName.IsNested - || (type.IsArray && type.GetArrayRank() != typeName.GetArrayRank()) - || type.IsSZArray != typeName.IsSZArray // int[] vs int[*] - ) + if (GetCachedType(fullName, typeName.FullName, typeName) is Type type) { - return false; + return type; } - if (type.FullName == typeName.FullName) - { - return true; // The happy path with no type forwarding - } - else if (typeName.IsArray) + if (_legacyMode) { - return Matches(type.GetElementType()!, typeName.GetElementType()); + throw new NotSupportedException($"Use '{nameof(Clipboard.TryGetData)}' with a 'resolver' function that defines the allowed types" + + $" to deserialize {typeName.AssemblyQualifiedName}."); } - else if (type.IsConstructedGenericType) + + if (_resolver is null) { - if (!Matches(type.GetGenericTypeDefinition(), typeName.GetGenericTypeDefinition())) - { - return false; - } - - ImmutableArray 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; + throw new NotSupportedException($"'resolver' function is required in '{nameof(Clipboard.TryGetData)}'" + + $" method to resolve '{typeName.AssemblyQualifiedName}'"); } - return false; + Type resolved = _resolver(typeName) + ?? throw new NotSupportedException($"'resolver' function provided in '{nameof(Clipboard.TryGetData)}'" + + $" method should never return a null. It should throw a '{nameof(NotSupportedException)}' when encountering an unsupported types."); + + _userTypes.Add(typeName, resolved); + + return resolved; } } } diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.NativeToWinFormsAdapter.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.NativeToWinFormsAdapter.cs index d675b609956..23458b20e02 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.NativeToWinFormsAdapter.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.NativeToWinFormsAdapter.cs @@ -108,7 +108,7 @@ private static bool TryGetDataFromHGLOBAL(HGLOBAL hglobal, string format, Fun DataFormats.FileDropConstant => ReadFileListFromHDROP((HDROP)(nint)hglobal), CF_DEPRECATED_FILENAME => new string[] { ReadStringFromHGLOBAL(hglobal, unicode: false) }, CF_DEPRECATED_FILENAMEW => new string[] { ReadStringFromHGLOBAL(hglobal, unicode: true) }, - _ => ReadObjectOrStreamFromHGLOBAL(hglobal, resolver, RestrictDeserializationToSafeTypes(format), legacyMode) + _ => ReadObjectOrStreamFromHGLOBAL(RestrictDeserializationToSafeTypes(format)) }; if (value is T t) @@ -118,14 +118,14 @@ private static bool TryGetDataFromHGLOBAL(HGLOBAL hglobal, string format, Fun } return false; - } - private static object? ReadObjectOrStreamFromHGLOBAL(HGLOBAL hglobal, Func? resolver, bool restrictDeserialization, bool legacyMode) - { - MemoryStream stream = ReadByteStreamFromHGLOBAL(hglobal, out bool isSerializedObject); - return !isSerializedObject - ? stream - : BinaryFormatUtilities.ReadObjectFromStream(stream, resolver, restrictDeserialization, legacyMode); + object? ReadObjectOrStreamFromHGLOBAL(bool restrictDeserialization) + { + MemoryStream stream = ReadByteStreamFromHGLOBAL(hglobal, out bool isSerializedObject); + return !isSerializedObject + ? stream + : BinaryFormatUtilities.ReadObjectFromStream(stream, resolver, restrictDeserialization, legacyMode); + } } private static unsafe MemoryStream ReadByteStreamFromHGLOBAL(HGLOBAL hglobal, out bool isSerializedObject) @@ -144,7 +144,7 @@ private static unsafe MemoryStream ReadByteStreamFromHGLOBAL(HGLOBAL hglobal, ou int index = 0; // The object here can either be a stream or a serialized object. We identify a serialized object - // by writing the bytes for the guid serializedObjectID at the start of the stream. + // by writing the bytes for the GUID serializedObjectID at the start of the stream. if (isSerializedObject = bytes.AsSpan().StartsWith(s_serializedObjectID)) { @@ -225,14 +225,19 @@ private static unsafe string ReadUtf8StringFromHGLOBAL(HGLOBAL hglobal) /// /// A restricted type was encountered, do not continue trying to deserialize. /// - /// - /// + /// + /// + /// if the managed object of was successfully created, + /// if the payload does not contain the specified format or the specified type. + /// + /// /// If contains that contains a serialized object, /// we return that object cast to or null. If the is /// not a serialized object, and a stream was requested, i.e. can be cast to /// we return that . - /// - /// + /// + /// + /// is deserialization failed. private static bool TryGetObjectFromDataObject( Com.IDataObject* dataObject, string format, @@ -244,10 +249,11 @@ private static bool TryGetObjectFromDataObject( data = default; doNotContinue = false; bool result = false; + try { // Try to get the data as a bitmap first. - if (typeof(Bitmap).IsAssignableTo(typeof(T)) && TryGetBitmapData(dataObject, format, out Bitmap? bitmap)) + if (typeof(Bitmap) == typeof(T) && TryGetBitmapData(dataObject, format, out Bitmap? bitmap)) { data = (T)(object)bitmap; return true; @@ -496,7 +502,7 @@ private bool TryGetDataInternal(string format, Func? resolver bool IDataObject.GetDataPresent(Type format) => GetDataPresent(format.FullName!); - bool ITypedDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + bool ITypedDataObject.TryGetData( string format, Func resolver, bool autoConvert, @@ -511,18 +517,18 @@ private bool TryGetDataInternal(string format, Func? resolver return TryGetDataInternal(format, resolver, autoConvert, legacyMode: false, out data); } - bool ITypedDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + bool ITypedDataObject.TryGetData( string format, bool autoConvert, [NotNullWhen(true), MaybeNullWhen(false)] out T data) => TryGetDataInternal(format, NotSupportedResolver, autoConvert, legacyMode: false, out data); - bool ITypedDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + bool ITypedDataObject.TryGetData( string format, [NotNullWhen(true), MaybeNullWhen(false)] out T data) => TryGetDataInternal(format, NotSupportedResolver, autoConvert: false, legacyMode: false, out data); - bool ITypedDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + bool ITypedDataObject.TryGetData( [NotNullWhen(true), MaybeNullWhen(false)] out T data) => TryGetDataInternal(typeof(T).FullName!, NotSupportedResolver, autoConvert: false, legacyMode: false, out data); diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.TypeNameComparer.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.TypeNameComparer.cs new file mode 100644 index 00000000000..3cc73b2ae4a --- /dev/null +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.TypeNameComparer.cs @@ -0,0 +1,114 @@ +// 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.Immutable; +using System.Reflection.Metadata; + +namespace System.Windows.Forms; +public unsafe partial class DataObject +{ + internal unsafe partial class Composition + { + internal class TypeNameComparer : IEqualityComparer + { + private TypeNameComparer() + { + } + + internal static IEqualityComparer Default { get; } = new TypeNameComparer(); + + public bool Equals(TypeName? x, TypeName? y) + { + if (x is null && y is null) + { + return true; + } + + if (x is null || y is null) + { + return false; + } + + return Matches(x, y); + } + + public int GetHashCode(TypeName obj) + { + if (obj is null) + { + return 0; + } + + if (obj.IsArray) + { + return HashCode.Combine(true, obj.GetArrayRank(), obj.GetElementType()); + } + + if (obj.IsConstructedGenericType) + { + int hashCode = HashCode.Combine("constructed", obj.GetGenericTypeDefinition()); + foreach (TypeName genericName in obj.GetGenericArguments()) + { + hashCode ^= GetHashCode(genericName); + } + + return hashCode; + } + + return obj.FullName.GetHashCode(); + } + + // Based on https://github.com/dotnet/runtime/blob/5d69e2dca30524a93b00cd613be218144b5f95d1/src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/SerializationRecord.cs#L54 + private static bool Matches(TypeName x, TypeName y) + { + if (x.IsArray != y.IsArray + || x.IsConstructedGenericType != y.IsConstructedGenericType + || x.IsNested != y.IsNested + || (x.IsArray && x.GetArrayRank() != y.GetArrayRank()) + || x.IsSZArray != y.IsSZArray // int[] vs int[*] + ) + { + return false; + } + + if (x.FullName == y.FullName) + { + return true; + } + + if (y.IsArray) + { + return Matches(x.GetElementType(), y.GetElementType()); + } + + if (x.IsConstructedGenericType) + { + if (!Matches(x.GetGenericTypeDefinition(), y.GetGenericTypeDefinition())) + { + return false; + } + + ImmutableArray genericNames2 = y.GetGenericArguments(); + ImmutableArray genericNames1 = x.GetGenericArguments(); + + if (genericNames1.Length != genericNames2.Length) + { + return false; + } + + for (int i = 0; i < genericNames1.Length; i++) + { + if (!Matches(genericNames1[i], genericNames2[i])) + { + return false; + } + } + + return true; + } + + return false; + } + } + } +} diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.cs index b688684a791..6ca2a686ef1 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.cs @@ -105,40 +105,40 @@ or DataFormats.PaletteConstant or DataFormats.WmfConstant; #region IDataObject - object? IDataObject.GetData(string format, bool autoConvert) => _winFormsDataObject.GetData(format, autoConvert); - object? IDataObject.GetData(string format) => _winFormsDataObject.GetData(format); - object? IDataObject.GetData(Type format) => _winFormsDataObject.GetData(format); - bool IDataObject.GetDataPresent(string format, bool autoConvert) => _winFormsDataObject.GetDataPresent(format, autoConvert); - bool IDataObject.GetDataPresent(string format) => _winFormsDataObject.GetDataPresent(format); - bool IDataObject.GetDataPresent(Type format) => _winFormsDataObject.GetDataPresent(format); - string[] IDataObject.GetFormats(bool autoConvert) => _winFormsDataObject.GetFormats(autoConvert); - string[] IDataObject.GetFormats() => _winFormsDataObject.GetFormats(); - void IDataObject.SetData(string format, bool autoConvert, object? data) => _winFormsDataObject.SetData(format, autoConvert, data); - void IDataObject.SetData(string format, object? data) => _winFormsDataObject.SetData(format, data); - void IDataObject.SetData(Type format, object? data) => _winFormsDataObject.SetData(format, data); - void IDataObject.SetData(object? data) => _winFormsDataObject.SetData(data); + public object? GetData(string format, bool autoConvert) => _winFormsDataObject.GetData(format, autoConvert); + public object? GetData(string format) => _winFormsDataObject.GetData(format); + public object? GetData(Type format) => _winFormsDataObject.GetData(format); + public bool GetDataPresent(string format, bool autoConvert) => _winFormsDataObject.GetDataPresent(format, autoConvert); + public bool GetDataPresent(string format) => _winFormsDataObject.GetDataPresent(format); + public bool GetDataPresent(Type format) => _winFormsDataObject.GetDataPresent(format); + public string[] GetFormats(bool autoConvert) => _winFormsDataObject.GetFormats(autoConvert); + public string[] GetFormats() => _winFormsDataObject.GetFormats(); + public void SetData(string format, bool autoConvert, object? data) => _winFormsDataObject.SetData(format, autoConvert, data); + public void SetData(string format, object? data) => _winFormsDataObject.SetData(format, data); + public void SetData(Type format, object? data) => _winFormsDataObject.SetData(format, data); + public void SetData(object? data) => _winFormsDataObject.SetData(data); #endregion #region ITypedDataObject - bool ITypedDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + public bool TryGetData( string format, Func resolver, bool autoConvert, [NotNullWhen(true), MaybeNullWhen(false)] out T data) => _winFormsDataObject.TryGetData(format, resolver, autoConvert, out data); - bool ITypedDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + public bool TryGetData( string format, bool autoConvert, [NotNullWhen(true), MaybeNullWhen(false)] out T data) => _winFormsDataObject.TryGetData(format, NotSupportedResolver, autoConvert, out data); - bool ITypedDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + public bool TryGetData( string format, [NotNullWhen(true), MaybeNullWhen(false)] out T data) => _winFormsDataObject.TryGetData(format, NotSupportedResolver, autoConvert: false, out data); - bool ITypedDataObject.TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + public bool TryGetData( [NotNullWhen(true), MaybeNullWhen(false)] out T data) => _winFormsDataObject.TryGetData(typeof(T).FullName!, NotSupportedResolver, autoConvert: false, out data); #endregion diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.DataStore.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.DataStore.cs index c5e6579f3f1..adad5ae496b 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.DataStore.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.DataStore.cs @@ -10,7 +10,7 @@ namespace System.Windows.Forms; public partial class DataObject { - private class DataStore : IDataObject, ITypedDataObject + private sealed class DataStore : IDataObject, ITypedDataObject { private class DataStoreEntry { @@ -24,7 +24,7 @@ public DataStoreEntry(object? data, bool autoConvert) } } - private readonly Dictionary _data = new(BackCompatibleStringComparer.Default); + private readonly Dictionary _mappedData = new(BackCompatibleStringComparer.Default); public DataStore() { @@ -41,7 +41,7 @@ private bool TryGetDataInternal( return false; } - if (_data.TryGetValue(format, out DataStoreEntry? dse) && dse.Data is T t) + if (_mappedData.TryGetValue(format, out DataStoreEntry? dse) && dse.Data is T t) { data = t; return true; @@ -61,7 +61,7 @@ private bool TryGetDataInternal( continue; } - if (_data.TryGetValue(mappedFormats[i], out DataStoreEntry? found) && found.Data is T value) + if (_mappedData.TryGetValue(mappedFormats[i], out DataStoreEntry? found) && found.Data is T value) { data = value; return true; @@ -71,17 +71,17 @@ private bool TryGetDataInternal( return false; } - public virtual object? GetData(string format, bool autoConvert) + public object? GetData(string format, bool autoConvert) { TryGetDataInternal(format, autoConvert, out object? data); return data; } - public virtual object? GetData(string format) => GetData(format, autoConvert: true); + public object? GetData(string format) => GetData(format, autoConvert: true); - public virtual object? GetData(Type format) => GetData(format.FullName!); + public object? GetData(Type format) => GetData(format.FullName!); - public virtual void SetData(string format, bool autoConvert, object? data) + public void SetData(string format, bool autoConvert, object? data) { if (string.IsNullOrWhiteSpace(format)) { @@ -104,23 +104,23 @@ public virtual void SetData(string format, bool autoConvert, object? data) } } - _data[format] = new DataStoreEntry(data, autoConvert); + _mappedData[format] = new DataStoreEntry(data, autoConvert); } - public virtual void SetData(string format, object? data) => SetData(format, autoConvert: true, data); + public void SetData(string format, object? data) => SetData(format, autoConvert: true, data); - public virtual void SetData(Type format, object? data) + public void SetData(Type format, object? data) { ArgumentNullException.ThrowIfNull(format); SetData(format.FullName!, data); } - public virtual void SetData(object? data) + public void SetData(object? data) { ArgumentNullException.ThrowIfNull(data); if (data is ISerializable - && !_data.ContainsKey(DataFormats.Serializable)) + && !_mappedData.ContainsKey(DataFormats.Serializable)) { SetData(DataFormats.Serializable, data); } @@ -128,12 +128,12 @@ public virtual void SetData(object? data) SetData(data.GetType(), data); } - public virtual bool GetDataPresent(Type format) + public bool GetDataPresent(Type format) { return GetDataPresent(format.FullName!); } - public virtual bool GetDataPresent(string format, bool autoConvert) + public bool GetDataPresent(string format, bool autoConvert) { if (string.IsNullOrWhiteSpace(format)) { @@ -142,8 +142,8 @@ public virtual bool GetDataPresent(string format, bool autoConvert) if (!autoConvert) { - Debug.Assert(_data is not null, "data must be non-null"); - return _data.ContainsKey(format); + Debug.Assert(_mappedData is not null, "data must be non-null"); + return _mappedData.ContainsKey(format); } else { @@ -163,15 +163,15 @@ public virtual bool GetDataPresent(string format, bool autoConvert) } } - public virtual bool GetDataPresent(string format) => GetDataPresent(format, autoConvert: true); + public bool GetDataPresent(string format) => GetDataPresent(format, autoConvert: true); - public virtual string[] GetFormats(bool autoConvert) + public string[] GetFormats(bool autoConvert) { - Debug.Assert(_data is not null, "data collection can't be null"); - Debug.Assert(_data.Keys is not null, "data Keys collection can't be null"); + Debug.Assert(_mappedData is not null, "data collection can't be null"); + Debug.Assert(_mappedData.Keys is not null, "data Keys collection can't be null"); - string[] baseVar = new string[_data.Keys.Count]; - _data.Keys.CopyTo(baseVar, 0); + string[] baseVar = new string[_mappedData.Keys.Count]; + _mappedData.Keys.CopyTo(baseVar, 0); Debug.Assert(baseVar is not null, "Collections should never return NULL arrays!!!"); if (autoConvert) @@ -181,8 +181,8 @@ public virtual string[] GetFormats(bool autoConvert) HashSet distinctFormats = new(baseVarLength); for (int i = 0; i < baseVarLength; i++) { - Debug.Assert(_data[baseVar[i]] is not null, $"Null item in data collection with key '{baseVar[i]}'"); - if (_data[baseVar[i]]!.AutoConvert) + Debug.Assert(_mappedData[baseVar[i]] is not null, $"Null item in data collection with key '{baseVar[i]}'"); + if (_mappedData[baseVar[i]]!.AutoConvert) { string[] cur = GetMappedFormats(baseVar[i])!; Debug.Assert(cur is not null, $"GetMappedFormats returned null for '{baseVar[i]}'"); @@ -203,27 +203,27 @@ public virtual string[] GetFormats(bool autoConvert) return baseVar; } - public virtual string[] GetFormats() => GetFormats(autoConvert: true); + public string[] GetFormats() => GetFormats(autoConvert: true); - public virtual bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + public bool TryGetData( string format, Func resolver, bool autoConvert, [NotNullWhen(true), MaybeNullWhen(false)] out T data) => TryGetDataInternal(format, autoConvert, out data); - public virtual bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + public bool TryGetData( string format, bool autoConvert, [NotNullWhen(true), MaybeNullWhen(false)] out T data) => TryGetDataInternal(format, autoConvert, out data); - public virtual bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + public bool TryGetData( string format, [NotNullWhen(true), MaybeNullWhen(false)] out T data) => TryGetDataInternal(format, autoConvert: false, out data); - public virtual bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + public bool TryGetData( [NotNullWhen(true), MaybeNullWhen(false)] out T data) => TryGetDataInternal(typeof(T).FullName!, autoConvert: false, out data); } diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.cs index 4e28594f223..6ca58b3076b 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.cs @@ -49,6 +49,15 @@ public DataObject() /// /// Initializes a new instance of the class, containing the specified data. /// + /// + /// + /// If implements an interface, + /// we strongly recommend implementing to support the + /// `TryGetData` API family that restricts deserialization to the requested and known types. + /// will throw + /// if is not implemented. + /// + /// public DataObject(object data) { if (data is DataObject dataObject) @@ -142,28 +151,27 @@ public virtual void SetData(string format, bool autoConvert, object? data) #endregion #region ITypedDataObject - public bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + [CLSCompliant(false)] + public bool TryGetData( string format, -#pragma warning disable CS3001 // Argument type is not CLS-compliant Func resolver, -#pragma warning restore CS3001 bool autoConvert, [NotNullWhen(true), MaybeNullWhen(false)] out T data) => - // TODO (TanyaSo) argument validation here?? + // TODO (TanyaSo) argument validation here?? TryGetDataCore(format, resolver, autoConvert, out data); - public bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + public bool TryGetData( string format, bool autoConvert, [NotNullWhen(true), MaybeNullWhen(false)] out T data) => TryGetData(format, NotSupportedResolver, autoConvert, out data); - public bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + public bool TryGetData( string format, [NotNullWhen(true), MaybeNullWhen(false)] out T data) => TryGetData(format, autoConvert: false, out data); - public bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + public bool TryGetData( [NotNullWhen(true), MaybeNullWhen(false)] out T data) => TryGetData(typeof(T).FullName!, out data); #endregion @@ -235,49 +243,37 @@ public virtual void SetText(string textData, TextDataFormat format) SetData(ConvertToDataFormats(format), false, textData); } - protected virtual bool TryGetDataCore<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + [CLSCompliant(false)] + protected virtual bool TryGetDataCore( string format, -#pragma warning disable CS3001 // Argument type is not CLS-compliant Func resolver, -#pragma warning restore CS3001 bool autoConvert, [NotNullWhen(true), MaybeNullWhen(false)] out T data) => ((ITypedDataObject)_innerData).TryGetData(format, resolver, autoConvert, out data); - internal static bool ValidateTryGetDataArguments(string format, Func resolver) + /// + /// Verify if the requested format is valid and compatible with the requested type . + /// If a custom is provided, it will be validated at the resolution time. + /// + internal static bool ValidateTryGetDataArguments(string format, Func? resolver = default) { - if (!ValidateFormat(format)) + if (string.IsNullOrWhiteSpace(format)) { return false; } - if (resolver is null - && !IsRestrictedFormat(format) - && IsUnboundedType()) + if (IsInvalidPredefinedFormatType(format)) { - // Tanyaso TODO: localize string throw new NotSupportedException( - $"'{typeof(T).Name}' is not a concrete type, and could allow for " + - $"unbounded deserialization. Use a concrete type or define a resolver " + - $"function that supports types that you are retrieving from the Clipboard."); + $"'{typeof(T).Name}' is not compatible with the specified format `{format}`."); } - return true; - } - - internal static bool ValidateTryGetDataArguments(string format) - { - if (!ValidateFormat(format)) - { - return false; - } - - Type type = typeof(T); - if (!IsRestrictedFormat(format) - // check is a convenience for simple usages where you aren't passing a resolver explicitly. - && IsUnboundedType()) + if (resolver is null + && !IsRestrictedFormat(format) + // Check is a convenience for simple usages where user isn't passing a resolver explicitly. + && IsUnboundedType()) { - // TODO: localize string + // Tanyaso TODO: localize string throw new NotSupportedException( $"'{typeof(T).Name}' is not a concrete type, and could allow for " + $"unbounded deserialization. Use a concrete type or define a resolver " + @@ -285,72 +281,61 @@ internal static bool ValidateTryGetDataArguments(string format) } return true; - } - private static bool IsUnboundedType() - { - if (typeof(T) == typeof(object)) + static bool IsUnboundedType() { - return true; - } + if (typeof(T) == typeof(object)) + { + return true; + } - Type type = typeof(T); - return type.IsInterface || type.IsAbstract; - } - - /// - /// For OLE formats, we support only a few known managed types. - /// For unknown formats, return true, they will be further validated when reading the data. - /// - private static bool ValidateFormat(string format) - { - if (string.IsNullOrWhiteSpace(format)) - { - return false; + Type type = typeof(T); + return type.IsInterface || type.IsAbstract; } - return format switch + static bool IsInvalidPredefinedFormatType(string format) => format switch { - DataFormats.TextConstant or - DataFormats.UnicodeTextConstant or - DataFormats.StringConstant or - DataFormats.RtfConstant or - DataFormats.HtmlConstant or - DataFormats.OemTextConstant => typeof(string) == typeof(T), - - DataFormats.FileDropConstant or - CF_DEPRECATED_FILENAME or - CF_DEPRECATED_FILENAMEW => typeof(string[]) == typeof(T), - - DataFormats.BitmapConstant or BitmapFullName => typeof(Bitmap) == typeof(T) || typeof(Image) == typeof(T), - _ => true + DataFormats.TextConstant + or DataFormats.UnicodeTextConstant + or DataFormats.StringConstant + or DataFormats.RtfConstant + or DataFormats.HtmlConstant + or DataFormats.OemTextConstant => typeof(string) != typeof(T), + + DataFormats.FileDropConstant + or CF_DEPRECATED_FILENAME + or CF_DEPRECATED_FILENAMEW => typeof(string[]) != typeof(T), + + DataFormats.BitmapConstant or BitmapFullName => + typeof(Bitmap) != typeof(T) && typeof(Image) != typeof(T), + _ => false }; - } - private static bool IsRestrictedFormat(string format) => - format is DataFormats.StringConstant - or BitmapFullName - or DataFormats.CsvConstant - or DataFormats.DibConstant - or DataFormats.DifConstant - or DataFormats.LocaleConstant - or DataFormats.PenDataConstant - or DataFormats.RiffConstant - or DataFormats.SymbolicLinkConstant - or DataFormats.TiffConstant - or DataFormats.WaveAudioConstant - or DataFormats.BitmapConstant - or DataFormats.EmfConstant - or DataFormats.PaletteConstant - or DataFormats.WmfConstant - or DataFormats.TextConstant - or DataFormats.UnicodeTextConstant - or DataFormats.RtfConstant - or DataFormats.HtmlConstant - or DataFormats.OemTextConstant - or DataFormats.FileDropConstant - or CF_DEPRECATED_FILENAME - or CF_DEPRECATED_FILENAMEW; + static bool IsRestrictedFormat(string format) => + format is DataFormats.StringConstant + or BitmapFullName + or DataFormats.CsvConstant + or DataFormats.DibConstant + or DataFormats.DifConstant + or DataFormats.LocaleConstant + or DataFormats.PenDataConstant + or DataFormats.RiffConstant + or DataFormats.SymbolicLinkConstant + or DataFormats.TiffConstant + or DataFormats.WaveAudioConstant + or DataFormats.BitmapConstant + or DataFormats.EmfConstant + or DataFormats.PaletteConstant + or DataFormats.WmfConstant + or DataFormats.TextConstant + or DataFormats.UnicodeTextConstant + or DataFormats.RtfConstant + or DataFormats.HtmlConstant + or DataFormats.OemTextConstant + or DataFormats.FileDropConstant + or CF_DEPRECATED_FILENAME + or CF_DEPRECATED_FILENAMEW; + } private static string ConvertToDataFormats(TextDataFormat format) => format switch { diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObjectExtensions.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObjectExtensions.cs index d09f31b63d3..fafd1f7daac 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObjectExtensions.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObjectExtensions.cs @@ -3,81 +3,58 @@ namespace System.Windows.Forms; +/// +/// Extension methods for data objects. +/// public static class DataObjectExtensions { - /// - /// if the does not implement . - /// if the is - public static bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( - this IDataObject dataObject, - [NotNullWhen(true), MaybeNullWhen(false)] out T data) + private static ITypedDataObject GetTypedDataObjectOrThrow(IDataObject dataObject) { ArgumentNullException.ThrowIfNull(dataObject); - if (dataObject is not ITypedDataObject typed) { - throw new ArgumentException($"DataObject should implement {nameof(ITypedDataObject)} interface.", nameof(dataObject)); + throw new NotSupportedException($"DataObject should implement {nameof(ITypedDataObject)} interface."); } - return typed.TryGetData(out data); + return typed; } + /// + /// if the does not implement . + /// if the is + public static bool TryGetData( + this IDataObject dataObject, + [NotNullWhen(true), MaybeNullWhen(false)] out T data) => + GetTypedDataObjectOrThrow(dataObject).TryGetData(out data); + /// - /// if the does not implement . + /// if the does not implement . /// if the is - public static bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + public static bool TryGetData( this IDataObject dataObject, string format, - [NotNullWhen(true), MaybeNullWhen(false)] out T data) - { - ArgumentNullException.ThrowIfNull(dataObject); - - if (dataObject is not ITypedDataObject typed) - { - throw new ArgumentException($"DataObject should implement {nameof(ITypedDataObject)} interface.", nameof(dataObject)); - } - - return typed.TryGetData(format, out data); - } + [NotNullWhen(true), MaybeNullWhen(false)] out T data) => + GetTypedDataObjectOrThrow(dataObject).TryGetData(format, out data); /// - /// if the does not implement . + /// if the does not implement . /// if the is - public static bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + public static bool TryGetData( this IDataObject dataObject, string format, bool autoConvert, - [NotNullWhen(true), MaybeNullWhen(false)] out T data) - { - ArgumentNullException.ThrowIfNull(dataObject); - - if (dataObject is not ITypedDataObject typed) - { - throw new ArgumentException($"DataObject should implement {nameof(ITypedDataObject)} interface.", nameof(dataObject)); - } - - return typed.TryGetData(format, autoConvert, out data); - } + [NotNullWhen(true), MaybeNullWhen(false)] out T data) => + GetTypedDataObjectOrThrow(dataObject).TryGetData(format, autoConvert, out data); /// - /// if the does not implement . + /// if the does not implement . /// if the is - public static bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + [CLSCompliant(false)] + public static bool TryGetData( this IDataObject dataObject, string format, -#pragma warning disable CS3001 // Argument type is not CLS-compliant Func resolver, -#pragma warning restore CS3001 bool autoConvert, - [NotNullWhen(true), MaybeNullWhen(false)] out T data) - { - ArgumentNullException.ThrowIfNull(dataObject); - - if (dataObject is not ITypedDataObject typed) - { - throw new ArgumentException($"DataObject should implement {nameof(ITypedDataObject)} interface.", nameof(dataObject)); - } - - return typed.TryGetData(format, resolver, autoConvert, out data); - } + [NotNullWhen(true), MaybeNullWhen(false)] out T data) => + GetTypedDataObjectOrThrow(dataObject).TryGetData(format, resolver, autoConvert, out data); } diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DragDropHelper.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DragDropHelper.cs index 43f84c98644..3bb6e436816 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DragDropHelper.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DragDropHelper.cs @@ -152,7 +152,7 @@ public static void Drop(DragEventArgs e) private static unsafe bool GetBooleanFormat(IComDataObject dataObject, string format) { ArgumentNullException.ThrowIfNull(dataObject); - ArgumentException.ThrowIfNullOrEmpty(format, nameof(format)); + ArgumentException.ThrowIfNullOrEmpty(format); ComTypes.STGMEDIUM medium = default; @@ -196,7 +196,7 @@ public static unsafe bool IsInDragLoop(IDataObject dataObject) ArgumentNullException.ThrowIfNull(dataObject); if (dataObject.GetDataPresent(PInvoke.CFSTR_INDRAGLOOP) - && dataObject.TryGetData(PInvoke.CFSTR_INDRAGLOOP, out DragDropFormat? dragDropFormat)) + && dataObject.GetData(PInvoke.CFSTR_INDRAGLOOP) is DragDropFormat dragDropFormat) { try { @@ -248,7 +248,7 @@ public static void ReleaseDragDropFormats(IComDataObject comDataObject) foreach (string format in dataObject.GetFormats()) { - if (dataObject.TryGetData(format, out DragDropFormat? dragDropFormat)) + if (dataObject.GetData(format) is DragDropFormat dragDropFormat) { dragDropFormat.Dispose(); } @@ -261,7 +261,7 @@ public static void ReleaseDragDropFormats(IComDataObject comDataObject) private static unsafe void SetBooleanFormat(IComDataObject dataObject, string format, bool value) { ArgumentNullException.ThrowIfNull(dataObject); - ArgumentException.ThrowIfNullOrEmpty(format, nameof(format)); + ArgumentException.ThrowIfNullOrEmpty(format); ComTypes.FORMATETC formatEtc = new() { diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/IDataObject.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/IDataObject.cs index 33ba57a45f8..38c83da0bf1 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/IDataObject.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/IDataObject.cs @@ -6,6 +6,16 @@ namespace System.Windows.Forms; /// /// Provides a format-independent mechanism for transferring data. /// +/// +/// +/// When implementing a custom type, implement +/// interface as well. This interface will ensure that only data of a specified +/// is exchanged. If is not implemented by a data object exchanged +/// in the clipboard or drag and drop scenarios, the APIs that specify a , +/// such as , will throw +/// a . +/// +/// public interface IDataObject { /// diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/ITypedDataObject.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/ITypedDataObject.cs index f2819b52690..b6fb18fb813 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/ITypedDataObject.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/ITypedDataObject.cs @@ -2,64 +2,81 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Reflection.Metadata; +using System.Runtime.Serialization.Formatters.Binary; namespace System.Windows.Forms; +/// +/// Provides a format-independent mechanism for reading data of a specified . +/// +/// +/// +/// Implement this interface to use your data object with +/// family of methods as well as in the drag and drop operations. This interface will ensure that only +/// data of the specified is exchanged. When implementing a custom +/// type, implement this interface as well. Otherwise the APIs that specify a will throw +/// a . +/// +/// public interface ITypedDataObject { /// - /// Retrieves the data associated with data format named after , + /// Retrieves data associated with data format named after , /// if that data is of type . /// - /// - /// if the data of this format is present and the value is - /// of a matching type and that value can be successfully retrieved, or - /// if the format is not present or the value is not of the right type. - /// - bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + /// + bool TryGetData( [NotNullWhen(true), MaybeNullWhen(false)] out T data); /// - /// Retrieves the data associated with the specified data format if that data is of type . + /// Retrieves data associated with the specified format if that data is of type . /// - /// - /// if the data of this format is present and the value is - /// of a matching type and that value can be successfully retrieved, or - /// if the format is not present or the value is not of the right type. - /// - bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + /// + /// A string that specifies what format to retrieve the data as. + /// See the class for a set of predefined data formats. + /// + /// + bool TryGetData( string format, [NotNullWhen(true), MaybeNullWhen(false)] out T data); /// - /// Retrieves the data associated with the specified data format, using - /// to determine whether to convert the data to another format, - /// if that data is of type . + /// Retrieves data in a specified format, optionally converting the data to the specified format. /// - /// - /// if the data of this format is present and the value is - /// of a matching type and that value can be successfully retrieved, or - /// if the format is not present or the value is not of the right type. - /// - bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + /// + bool TryGetData( string format, bool autoConvert, [NotNullWhen(true), MaybeNullWhen(false)] out T data); /// - /// Retrieves the data associated with the specified data format, using - /// to determine whether to convert the data to the format, - /// if that data is assignable to . - /// Will use with the binary formatter if needed. - /// is implemented by the user and should return the allowed types or - /// throw a . + /// compatible overload that retrieves typed data associated + /// with the specified data format. /// + /// + /// A user-provided function that defines a closure of s that can be retrieved from + /// the exchange medium. + /// + /// + /// to attempt to automatically convert the data to the specified format; + /// for no data format conversion. + /// + /// + /// A data object with the data in the specified format, or null if the data is not + /// available in the specified format or is of a wrong type. + /// /// /// if the data of this format is present and the value is /// of a matching type and that value can be successfully retrieved, or /// if the format is not present or the value is not of the right type. /// - bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + /// + /// + /// Implement this method for backward compatibility with binary formatted data + /// when binary formatters are enabled. + /// + /// + bool TryGetData( string format, #pragma warning disable CS3001 // Argument type is not CLS-compliant Func resolver, diff --git a/src/System.Windows.Forms/tests/ComDisabledTests/ClipboardComTests.cs b/src/System.Windows.Forms/tests/ComDisabledTests/ClipboardComTests.cs index 14ac5b500da..10de5202c2a 100644 --- a/src/System.Windows.Forms/tests/ComDisabledTests/ClipboardComTests.cs +++ b/src/System.Windows.Forms/tests/ComDisabledTests/ClipboardComTests.cs @@ -5,9 +5,10 @@ namespace System.Windows.Forms.Tests; -[Collection("Sequential")] // Each registered Clipboard format is an OS singleton, - // and we should not run this test at the same time as other tests using the same format. -// [UISettings(MaxAttempts = 3)] // Try up to 3 times before failing. +// Each registered Clipboard format is an OS singleton, +// and we should not run this test at the same time as other tests using the same format. +[Collection("Sequential")] +[UISettings(MaxAttempts = 3)] // Try up to 3 times before failing. public class ClipboardComTests { [WinFormsFact] diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/BinaryFormatUtilitiesTests.FullCompatScope.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/BinaryFormatUtilitiesTests.FullCompatScope.cs new file mode 100644 index 00000000000..4317b2aad71 --- /dev/null +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/BinaryFormatUtilitiesTests.FullCompatScope.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Windows.Forms.Tests; + +public partial class BinaryFormatUtilitiesTests +{ + internal readonly ref struct FullCompatScope : IDisposable + { + private readonly BinaryFormatterScope _binaryFormatterScope; + private readonly BinaryFormatterInClipboardDragDropScope _binaryFormatterInClipboardDragDropScope; + private readonly NrbfSerializerInClipboardDragDropScope _nrbfSerializerInClipboardDragDropScope; + + public FullCompatScope() + { + _binaryFormatterScope = new BinaryFormatterScope(enable: true); + _binaryFormatterInClipboardDragDropScope = new BinaryFormatterInClipboardDragDropScope(enable: true); + _nrbfSerializerInClipboardDragDropScope = new NrbfSerializerInClipboardDragDropScope(enable: false); + } + + public void Dispose() + { + _binaryFormatterScope.Dispose(); + _binaryFormatterInClipboardDragDropScope.Dispose(); + _nrbfSerializerInClipboardDragDropScope.Dispose(); + } + } +} diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/BinaryFormatUtilitiesTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/BinaryFormatUtilitiesTests.cs index f5a151454a6..a7e710e5b7c 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/BinaryFormatUtilitiesTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/BinaryFormatUtilitiesTests.cs @@ -331,16 +331,21 @@ public void RoundTrip_Unsupported(IList value) { ((Action)(() => WriteObjectToStream(value))).Should().Throw(); - using (BinaryFormatterScope scope = new(enable: true)) + using (NrbfSerializerInClipboardDragDropScope nrbfScope = new(enable: false)) { - ((Action)(() => WriteObjectToStream(value))).Should().Throw(); + using (BinaryFormatterScope scope = new(enable: true)) + { + ((Action)(() => WriteObjectToStream(value))).Should().Throw(); - using BinaryFormatterInClipboardScope clipboardScope = new(enable: true); - WriteObjectToStream(value); - ReadObjectFromStream().Should().BeEquivalentTo(value); + using BinaryFormatterInClipboardDragDropScope clipboardDragDropScope = new(enable: true); + WriteObjectToStream(value); + ReadObjectFromStream().Should().BeEquivalentTo(value); + } + + ((Action)(() => ReadObjectFromStream())).Should().Throw(); } - // Doesn't attempt to access BinaryFormatter. + // Don't attempt to access BinaryFormatter, use NRBF deserializer instead. ReadObjectFromStream().Should().BeNull(); } @@ -350,8 +355,7 @@ public void RoundTrip_RestrictedFormat_Unsupported(IList value) { ((Action)(() => WriteObjectToStream(value, restrictSerialization: true))).Should().Throw(); - using BinaryFormatterScope scope = new(enable: true); - using BinaryFormatterInClipboardScope clipboardScope = new(enable: true); + using FullCompatScope scope = new(); ((Action)(() => WriteObjectToStream(value, restrictSerialization: true))).Should().Throw(); } @@ -366,8 +370,7 @@ public void RoundTrip_OffsetArray() value.SetValue(202u, 2, 3); value.SetValue(203u, 2, 4); - using BinaryFormatterScope scope = new(enable: true); - using BinaryFormatterInClipboardScope clipboardScope = new(enable: true); + using FullCompatScope scope = new(); var result = RoundTripObject(value).Should().BeOfType().Subject; result.Rank.Should().Be(2); @@ -386,30 +389,34 @@ public void RoundTrip_OffsetArray() [Fact] public void RoundTripOfType_Unsupported() { - List value = new() { "text" }; - using (BinaryFormatterScope scope = new(enable: true)) - using (BinaryFormatterInClipboardScope clipboardScope = new(enable: true)) + List value = ["text"]; + using (FullCompatScope scope = new()) { WriteObjectToStream(value); - var result = ReadObjectFromStream>(ObjectResolver).Should().BeOfType>().Subject; - result.Count.Should().Be(1); - result[0].Should().Be("text"); + ReadAndValidate(); } - ReadObjectFromStream>(ObjectResolver).Should().BeNull(); - } - - private static Type ObjectResolver(TypeName typeName) - { - if (typeof(object).FullName! == typeName.FullName) + using (NrbfSerializerInClipboardDragDropScope nrbfScope = new(enable: false)) { - return typeof(object); + ReadObjectFromStream>(ObjectResolver).Should().BeNull(); } - throw new NotSupportedException($"Can't resolve {typeName.FullName}"); + ReadAndValidate(); + + void ReadAndValidate() + { + var result = ReadObjectFromStream>(ObjectResolver).Should().BeOfType>().Subject; + result.Count.Should().Be(1); + result[0].Should().Be("text"); + } } + private static Type ObjectResolver(TypeName typeName) => + typeof(object).FullName == typeName.FullName + ? typeof(object) + : throw new NotSupportedException($"Can't resolve {typeName.FullName}"); + [Fact] public void RoundTripOfType_AsUnmatchingType_Simple() { @@ -427,8 +434,7 @@ public void RoundTripOfType_RestrictedFormat_AsUnmatchingType_Simple() ReadObjectFromStream(DataObject.NotSupportedResolver, restrictDeserialization: true).Should().BeNull(); - using BinaryFormatterScope scope = new(enable: true); - using BinaryFormatterInClipboardScope clipboardScope = new(enable: true); + using FullCompatScope scope = new(); ReadObjectFromStream(DataObject.NotSupportedResolver, restrictDeserialization: true).Should().BeNull(); } @@ -445,8 +451,7 @@ public void RoundTripOfType_intNullableArray_DefaultResolver() { int?[] value = [101, null, 303]; - using BinaryFormatterScope scope = new(enable: true); - using BinaryFormatterInClipboardScope clipboardScope = new(enable: true); + using FullCompatScope scope = new(); ((Action)(() => RoundTripOfType(value))).Should().Throw(); } @@ -455,8 +460,7 @@ public void RoundTripOfType_RestrictedFormat_intNullableArray_DefaultResolver() { int?[] value = [101, null, 303]; - using BinaryFormatterScope scope = new(enable: true); - using BinaryFormatterInClipboardScope clipboardScope = new(enable: true); + using FullCompatScope scope = new(); ((Action)(() => RoundTripOfType_RestrictedFormat(value))).Should().Throw(); } @@ -471,8 +475,7 @@ public void RoundTripOfType_OffsetArray_DefaultResolver() value.SetValue(202u, 2, 3); value.SetValue(203u, 2, 4); - using BinaryFormatterScope scope = new(enable: true); - using BinaryFormatterInClipboardScope clipboardScope = new(enable: true); + using FullCompatScope scope = new(); ((Action)(() => RoundTripOfType(value))).Should().Throw(); } @@ -481,8 +484,7 @@ public void RoundTripOfType_intNullableArray_CustomResolver() { int?[] value = [101, null, 303]; - using BinaryFormatterScope scope = new(enable: true); - using BinaryFormatterInClipboardScope clipboardScope = new(enable: true); + using FullCompatScope scope = new(); RoundTripOfType(value, NullableIntArrayResolver).Should().BeEquivalentTo(value); } @@ -511,8 +513,7 @@ public void RoundTripOfType_TestData_TestDataResolver() { TestData value = new(new(10, 10), 2); - using BinaryFormatterScope scope = new(enable: true); - using BinaryFormatterInClipboardScope clipboardScope = new(enable: true); + using FullCompatScope scope = new(); var result = RoundTripOfType(value, TestDataResolver).Should().BeOfType().Subject; @@ -524,8 +525,7 @@ public void RoundTripOfType_TestData_InvalidResolver() { TestData value = new(new(10, 10), 2); - using BinaryFormatterScope scope = new(enable: true); - using BinaryFormatterInClipboardScope clipboardScope = new(enable: true); + using FullCompatScope scope = new(); WriteObjectToStream(value); @@ -540,8 +540,7 @@ public void RoundTripOfType_Font_FontResolver() { using Font value = new("Microsoft Sans Serif", emSize: 10); - using BinaryFormatterScope scope = new(enable: true); - using BinaryFormatterInClipboardScope clipboardScope = new(enable: true); + using FullCompatScope scope = new(); using Font result = RoundTripOfType(value, FontResolver).Should().BeOfType().Subject; result.Should().Be(value); @@ -549,11 +548,11 @@ public void RoundTripOfType_Font_FontResolver() private static Type FontResolver(TypeName typeName) { - (string name, Type type)[] allowedTypes = + (string? name, Type type)[] allowedTypes = [ - (typeof(FontStyle).FullName!, typeof(FontStyle)), - (typeof(FontFamily).FullName!, typeof(FontFamily)), - (typeof(GraphicsUnit).FullName!, typeof(GraphicsUnit)), + (typeof(FontStyle).FullName, typeof(FontStyle)), + (typeof(FontFamily).FullName, typeof(FontFamily)), + (typeof(GraphicsUnit).FullName, typeof(GraphicsUnit)), ]; string fullName = typeName.FullName; @@ -574,8 +573,7 @@ public void RoundTripOfType_FlatData_DefaultResolver() { TestDataBase.InnerData value = new("simple class"); - using BinaryFormatterScope scope = new(enable: true); - using BinaryFormatterInClipboardScope clipboardScope = new(enable: true); + using FullCompatScope scope = new(); var result = RoundTripOfType(value).Should().BeOfType().Subject; @@ -615,6 +613,8 @@ public TestData(Bitmap bitmap, int count) Count = count; } + private const float Delta = 0.0003f; + // BinaryFormatter resolves primitive types or arrays of primitive types with no callback to the resolver. public int? Count; public DateTime? Today = DateTime.Now; @@ -686,8 +686,6 @@ public void Equals(TestData other, Size bitmapSize) } } - private const float Delta = 0.0003f; - private static Type TestDataResolver(TypeName typeName) { (string name, Type type)[] allowedTypes = @@ -713,10 +711,10 @@ private static Type TestDataResolver(TypeName typeName) public void ReadFontSerializedOnNet481() { // This string was generated on net481. - // Clipboard.SetData("TestData", new Font("Arial", 12)); + // Clipboard.SetData("TestData", new Font("Microsoft Sans Serif", 10)); // And the resulting stream was saved as a string // string text = Convert.ToBase64String(stream.ToArray()); - string arielFont = + string font = "AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJl" + "PW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5G" + "b250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAb" @@ -724,12 +722,45 @@ public void ReadFontSerializedOnNet481() + "IEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAAAAAAABfv///8bU3lz" + "dGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAwAAAAs="; - byte[] bytes = Convert.FromBase64String(arielFont); - using MemoryStream stream = new MemoryStream(bytes); - var result = Utilities.ReadObjectFromStream( + byte[] bytes = Convert.FromBase64String(font); + using MemoryStream stream = new(bytes); + + stream.Position = 0; + // Default deserialization with the NRBF deserializer. + var result = Utilities.ReadObjectFromStream( + stream, + resolver: FontResolver, + restrictDeserialization: false, + legacyMode: false).Should().BeOfType().Subject; + result.Name.Should().Be("Microsoft Sans Serif"); + result.Size.Should().Be(10); + + stream.Position = 0; + using (FullCompatScope scope = new()) + { + result = Utilities.ReadObjectFromStream( + stream, + resolver: null, + restrictDeserialization: false, + legacyMode: true).Should().BeOfType().Subject; + result.Name.Should().Be("Microsoft Sans Serif"); + result.Size.Should().Be(10); + + stream.Position = 0; + result = Utilities.ReadObjectFromStream( + stream, + resolver: FontResolver, + restrictDeserialization: false, + legacyMode: false).Should().BeOfType().Subject; + result.Name.Should().Be("Microsoft Sans Serif"); + result.Size.Should().Be(10); + } + + using NrbfSerializerInClipboardDragDropScope nrbfScope = new(enable: false); + Utilities.ReadObjectFromStream( stream, resolver: null, restrictDeserialization: false, - legacyMode: true); + legacyMode: true).Should().BeNull(); } } diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ClipboardTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ClipboardTests.cs index d5e2c1dfaeb..8dfaa4d1ef9 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ClipboardTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ClipboardTests.cs @@ -645,36 +645,38 @@ public unsafe void Clipboard_RawClipboard_SetClipboardData_ReturnsExpected() [WinFormsFact] public void Clipboard_BinaryFormatter_AppContextSwitch() { + // Test the switch to ensure it works as expected in the context of this test assembly. LocalAppContextSwitches.ClipboardDragDropEnableUnsafeBinaryFormatterSerialization.Should().BeFalse(); - using (BinaryFormatterInClipboardScope scope = new(enable: true)) + using (BinaryFormatterInClipboardDragDropScope scope = new(enable: true)) { LocalAppContextSwitches.ClipboardDragDropEnableUnsafeBinaryFormatterSerialization.Should().BeTrue(); } LocalAppContextSwitches.ClipboardDragDropEnableUnsafeBinaryFormatterSerialization.Should().BeFalse(); - using (BinaryFormatterInClipboardScope scope = new(enable: false)) + using (BinaryFormatterInClipboardDragDropScope scope = new(enable: false)) { LocalAppContextSwitches.ClipboardDragDropEnableUnsafeBinaryFormatterSerialization.Should().BeFalse(); } } - [WinFormsFact] + [WinFormsFact] public void Clipboard_NrbfSerializer_AppContextSwitch() { - LocalAppContextSwitches.ClipboardDragDropEnableNrbfSerialization.Should().BeFalse(); + // Test the switch to ensure it works as expected in the context of this test assembly. + LocalAppContextSwitches.ClipboardDragDropEnableNrbfSerialization.Should().BeTrue(); - using (NrbfSerializerInClipboardScope scope = new(enable: true)) + using (NrbfSerializerInClipboardDragDropScope scope = new(enable: false)) { - LocalAppContextSwitches.ClipboardDragDropEnableNrbfSerialization.Should().BeTrue(); + LocalAppContextSwitches.ClipboardDragDropEnableNrbfSerialization.Should().BeFalse(); } - LocalAppContextSwitches.ClipboardDragDropEnableNrbfSerialization.Should().BeFalse(); + LocalAppContextSwitches.ClipboardDragDropEnableNrbfSerialization.Should().BeTrue(); - using (NrbfSerializerInClipboardScope scope = new(enable: false)) + using (NrbfSerializerInClipboardDragDropScope scope = new(enable: true)) { - LocalAppContextSwitches.ClipboardDragDropEnableNrbfSerialization.Should().BeFalse(); + LocalAppContextSwitches.ClipboardDragDropEnableNrbfSerialization.Should().BeTrue(); } } @@ -696,13 +698,20 @@ public void Clipboard_TryGetTestData_ReturnsExpected() { DateTime date = DateTime.Now; TestData expected = new(date); - using BinaryFormatterScope scope = new(enable: true); - using BinaryFormatterInClipboardScope clipboardScope = new(enable: true); - Clipboard.SetData("TestData", expected); + using (BinaryFormatterScope scope = new(enable: true)) + using (NrbfSerializerInClipboardDragDropScope nrbfScope = new(enable: false)) + using (BinaryFormatterInClipboardDragDropScope clipboardDragDropScope = new(enable: true)) + { + Clipboard.SetData("TestData", expected); + + Clipboard.TryGetData("TestData", out TestData? data).Should().BeTrue(); + var result = data.Should().BeOfType().Subject; + expected.Equals(result); + } - Clipboard.TryGetData("TestData", out TestData? data).Should().BeTrue(); - var result = data.Should().BeOfType().Subject; - expected.Equals(result); + using NrbfSerializerInClipboardDragDropScope nrbfScope1 = new(enable: true); + Clipboard.TryGetData("TestData", out TestData? testData).Should().BeTrue(); + expected.Equals(testData.Should().BeOfType().Subject); } [Serializable] @@ -732,6 +741,9 @@ public void Clipboard_TryGetObject_Throws() Clipboard.SetData("TestData", expected); ((Action)(() => Clipboard.TryGetData("TestData", null!, out object? data))).Should().Throw(); + + using NrbfSerializerInClipboardDragDropScope nrbfScope = new(enable: true); + ((Action)(() => Clipboard.TryGetData("TestData", null!, out object? data))).Should().Throw(); } [WinFormsFact] @@ -742,5 +754,8 @@ public void Clipboard_TryGetRectangleAsObject_Throws() Clipboard.SetData("TestData", expected); ((Action)(() => Clipboard.TryGetData("TestData", null!, out object? data))).Should().Throw(); + + using NrbfSerializerInClipboardDragDropScope nrbfScope = new(enable: true); + ((Action)(() => Clipboard.TryGetData("TestData", null!, out object? data))).Should().Throw(); } } diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectExtensionsTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectExtensionsTests.cs index dee86aa284e..52ea178e117 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectExtensionsTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectExtensionsTests.cs @@ -12,7 +12,7 @@ namespace System.Windows.Forms.Tests; public class DataObjectExtensionsTests { [Fact] - public void TryGetData_Throws_ArgumentNull() + public void TryGetData_Throws_ArgumentNullException() { ((Action)(() => DataObjectExtensions.TryGetData(null!, out _))).Should().Throw(); ((Action)(() => DataObjectExtensions.TryGetData(null!, DataFormats.Text, out _))).Should().Throw(); @@ -25,37 +25,37 @@ public void TryGetData_Throws_ArgumentNull() private static Type Resolver(TypeName typeName) => typeof(string); [Fact] - public void TryGetData_Throws_Argument() + public void TryGetData_Throws_NotSupportedException() { UntypedDataObject dataObject = new(); - ((Action)(() => dataObject.TryGetData(out _))).Should().Throw(); + ((Action)(() => dataObject.TryGetData(out _))).Should().Throw(); dataObject.VerifyGetDataWasNotCalled(); } [Fact] - public void TryGetData_String_Throws_Argument() + public void TryGetData_String_Throws_NotSupportedException() { UntypedDataObject dataObject = new(); - ((Action)(() => dataObject.TryGetData(DataFormats.Text, out _))).Should().Throw(); + ((Action)(() => dataObject.TryGetData(DataFormats.Text, out _))).Should().Throw(); dataObject.VerifyGetDataWasNotCalled(); } [Theory] [BoolData] - public void TryGetData_StringBool_Throws_Argument(bool autoConvert) + public void TryGetData_StringBool_Throws_NotSupportedException(bool autoConvert) { UntypedDataObject dataObject = new(); - ((Action)(() => dataObject.TryGetData(DataFormats.CommaSeparatedValue, autoConvert, out _))).Should().Throw(); + ((Action)(() => dataObject.TryGetData(DataFormats.CommaSeparatedValue, autoConvert, out _))).Should().Throw(); dataObject.VerifyGetDataWasNotCalled(); } [Theory] [BoolData] - public void TryGetData_StringFuncBool_Throws_Argument(bool autoConvert) + public void TryGetData_StringFuncBool_Throws_NotSupportedException(bool autoConvert) { UntypedDataObject dataObject = new(); - ((Action)(() => dataObject.TryGetData(DataFormats.UnicodeText, Resolver, autoConvert, out _))).Should().Throw(); + ((Action)(() => dataObject.TryGetData(DataFormats.UnicodeText, Resolver, autoConvert, out _))).Should().Throw(); dataObject.VerifyGetDataWasNotCalled(); } @@ -220,28 +220,28 @@ public void VerifyTryGetDataStringFuncBoolCalled() _tryGetDataStringFuncBoolCalledCount.Should().Be(1); } - public bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>([MaybeNullWhen(false), NotNullWhen(true)] out T data) + public bool TryGetData([MaybeNullWhen(false), NotNullWhen(true)] out T data) { _tryGetDataCalledCount++; data = default; return false; } - public bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(string format, [MaybeNullWhen(false), NotNullWhen(true)] out T data) + public bool TryGetData(string format, [MaybeNullWhen(false), NotNullWhen(true)] out T data) { _tryGetDataStringCalledCount++; data = default; return false; } - public bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(string format, bool autoConvert, [MaybeNullWhen(false), NotNullWhen(true)] out T data) + public bool TryGetData(string format, bool autoConvert, [MaybeNullWhen(false), NotNullWhen(true)] out T data) { _tryGetDataStringBoolCalledCount++; data = default; return false; } - public bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(string format, Func resolver, bool autoConvert, [MaybeNullWhen(false), NotNullWhen(true)] out T data) + public bool TryGetData(string format, Func resolver, bool autoConvert, [MaybeNullWhen(false), NotNullWhen(true)] out T data) { _tryGetDataStringFuncBoolCalledCount++; data = default; diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectTests.cs index ac97900cb22..d6cdce17cf0 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectTests.cs @@ -383,7 +383,7 @@ public DataObjectOverridesTryGetDataCore(string format, Func res public int Count { get; private set; } - protected override bool TryGetDataCore<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + protected override bool TryGetDataCore( string format, Func resolver, bool autoConvert, From b7a8ed8828d5005a2947f0c6b970ab726fd3e4ed Mon Sep 17 00:00:00 2001 From: Tanya Solyanik Date: Wed, 13 Nov 2024 22:44:28 -0800 Subject: [PATCH 8/8] When AppContext switch caching is disabled, default switch value was not stored, we were lucky with switches that have "false" as a default --- .../TestUtilities/AppContextSwitchNames.cs | 12 --- .../LocalAppContextSwitches.cs | 77 ++++++++++--------- ...BinaryFormatterInClipboardDragDropScope.cs | 4 +- ...lipboardDragDropEnableNrbfSerialization.cs | 4 +- .../WinFormsAppContextSwitchNames.cs | 14 ++++ .../WinFormsAppContextSwitchScope.cs | 54 +++++++++++++ .../System/Windows/Forms/ClipboardTests.cs | 4 + 7 files changed, 116 insertions(+), 53 deletions(-) rename src/{Common => System.Windows.Forms}/tests/TestUtilities/BinaryFormatterInClipboardDragDropScope.cs (74%) rename src/{Common => System.Windows.Forms}/tests/TestUtilities/ClipboardDragDropEnableNrbfSerialization.cs (76%) create mode 100644 src/System.Windows.Forms/tests/TestUtilities/WinFormsAppContextSwitchScope.cs diff --git a/src/Common/tests/TestUtilities/AppContextSwitchNames.cs b/src/Common/tests/TestUtilities/AppContextSwitchNames.cs index 13e7b61b898..a20183a408e 100644 --- a/src/Common/tests/TestUtilities/AppContextSwitchNames.cs +++ b/src/Common/tests/TestUtilities/AppContextSwitchNames.cs @@ -19,16 +19,4 @@ public const string EnableUnsafeBinaryFormatterSerialization /// public const string LocalAppContext_DisableCaching = "TestSwitch.LocalAppContext.DisableCaching"; - - /// - /// The switch that controls whether or not the is enabled in the Clipboard. - /// - public const string ClipboardDragDropEnableUnsafeBinaryFormatterSerializationSwitchName - = "Windows.ClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization"; - - /// - /// The switch that controls whether or not the System.Windows.Forms.BinaryFormat.Deserializer is enabled in the Clipboard. - /// - public const string ClipboardDragDropEnableNrbfSerializationSwitchName - = "Windows.ClipboardDragDrop.EnableNrbfSerialization"; } diff --git a/src/System.Windows.Forms.Primitives/src/System/LocalAppContextSwitches/LocalAppContextSwitches.cs b/src/System.Windows.Forms.Primitives/src/System/LocalAppContextSwitches/LocalAppContextSwitches.cs index 9dd8c6d8699..9b172c4ea53 100644 --- a/src/System.Windows.Forms.Primitives/src/System/LocalAppContextSwitches/LocalAppContextSwitches.cs +++ b/src/System.Windows.Forms.Primitives/src/System/LocalAppContextSwitches/LocalAppContextSwitches.cs @@ -96,58 +96,62 @@ private static bool GetSwitchValue(string switchName, ref int cachedSwitchValue) { cachedSwitchValue = isSwitchEnabled ? 1 /*true*/ : -1 /*false*/; } + else if (!hasSwitch) + { + AppContext.SetSwitch(switchName, isSwitchEnabled); + } return isSwitchEnabled; + } - static bool GetSwitchDefaultValue(string switchName) + private static bool GetSwitchDefaultValue(string switchName) + { + if (TargetFrameworkName is not { } framework) { - if (TargetFrameworkName is not { } framework) - { - return false; - } + return false; + } - if (switchName == NoClientNotificationsSwitchName) - { - return false; - } + if (switchName == NoClientNotificationsSwitchName) + { + return false; + } - if (switchName == TreeNodeCollectionAddRangeRespectsSortOrderSwitchName) - { - return true; - } + if (switchName == TreeNodeCollectionAddRangeRespectsSortOrderSwitchName) + { + return true; + } - if (switchName == ClipboardDragDropEnableUnsafeBinaryFormatterSerializationSwitchName) + if (switchName == ClipboardDragDropEnableUnsafeBinaryFormatterSerializationSwitchName) + { + return false; + } + + if (switchName == ClipboardDragDropEnableNrbfSerializationSwitchName) + { + return true; + } + + if (framework.Version.Major >= 8) + { + // Behavior changes added in .NET 8 + + if (switchName == ScaleTopLevelFormMinMaxSizeForDpiSwitchName) { - return false; + return true; } - if (switchName == ClipboardDragDropEnableNrbfSerializationSwitchName) + if (switchName == TrackBarModernRenderingSwitchName) { return true; } - if (framework.Version.Major >= 8) + if (switchName == ServicePointManagerCheckCrlSwitchName) { - // Behavior changes added in .NET 8 - - if (switchName == ScaleTopLevelFormMinMaxSizeForDpiSwitchName) - { - return true; - } - - if (switchName == TrackBarModernRenderingSwitchName) - { - return true; - } - - if (switchName == ServicePointManagerCheckCrlSwitchName) - { - return true; - } + return true; } - - return false; } + + return false; } /// @@ -256,7 +260,6 @@ public static bool ClipboardDragDropEnableUnsafeBinaryFormatterSerialization public static bool ClipboardDragDropEnableNrbfSerialization { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => - GetCachedSwitchValue(ClipboardDragDropEnableNrbfSerializationSwitchName, ref s_clipboardDragDropEnableNrbfSerialization); + get => GetCachedSwitchValue(ClipboardDragDropEnableNrbfSerializationSwitchName, ref s_clipboardDragDropEnableNrbfSerialization); } } diff --git a/src/Common/tests/TestUtilities/BinaryFormatterInClipboardDragDropScope.cs b/src/System.Windows.Forms/tests/TestUtilities/BinaryFormatterInClipboardDragDropScope.cs similarity index 74% rename from src/Common/tests/TestUtilities/BinaryFormatterInClipboardDragDropScope.cs rename to src/System.Windows.Forms/tests/TestUtilities/BinaryFormatterInClipboardDragDropScope.cs index e33bf87fbac..6f27508016f 100644 --- a/src/Common/tests/TestUtilities/BinaryFormatterInClipboardDragDropScope.cs +++ b/src/System.Windows.Forms/tests/TestUtilities/BinaryFormatterInClipboardDragDropScope.cs @@ -5,12 +5,12 @@ namespace System; public readonly ref struct BinaryFormatterInClipboardDragDropScope { - private readonly AppContextSwitchScope _switchScope; + private readonly WinFormsAppContextSwitchScope _switchScope; public BinaryFormatterInClipboardDragDropScope(bool enable) { Monitor.Enter(typeof(BinaryFormatterInClipboardDragDropScope)); - _switchScope = new(AppContextSwitchNames.ClipboardDragDropEnableUnsafeBinaryFormatterSerializationSwitchName, enable); + _switchScope = new(WinFormsAppContextSwitchNames.ClipboardDragDropEnableUnsafeBinaryFormatterSerializationSwitchName, enable); } public void Dispose() diff --git a/src/Common/tests/TestUtilities/ClipboardDragDropEnableNrbfSerialization.cs b/src/System.Windows.Forms/tests/TestUtilities/ClipboardDragDropEnableNrbfSerialization.cs similarity index 76% rename from src/Common/tests/TestUtilities/ClipboardDragDropEnableNrbfSerialization.cs rename to src/System.Windows.Forms/tests/TestUtilities/ClipboardDragDropEnableNrbfSerialization.cs index defa6a0bd48..ebfc7e8ff6a 100644 --- a/src/Common/tests/TestUtilities/ClipboardDragDropEnableNrbfSerialization.cs +++ b/src/System.Windows.Forms/tests/TestUtilities/ClipboardDragDropEnableNrbfSerialization.cs @@ -5,12 +5,12 @@ namespace System; public readonly ref struct NrbfSerializerInClipboardDragDropScope { - private readonly AppContextSwitchScope _switchScope; + private readonly WinFormsAppContextSwitchScope _switchScope; public NrbfSerializerInClipboardDragDropScope(bool enable) { Monitor.Enter(typeof(NrbfSerializerInClipboardDragDropScope)); - _switchScope = new(AppContextSwitchNames.ClipboardDragDropEnableNrbfSerializationSwitchName, enable); + _switchScope = new(WinFormsAppContextSwitchNames.ClipboardDragDropEnableNrbfSerializationSwitchName, enable); } public void Dispose() diff --git a/src/System.Windows.Forms/tests/TestUtilities/WinFormsAppContextSwitchNames.cs b/src/System.Windows.Forms/tests/TestUtilities/WinFormsAppContextSwitchNames.cs index 8bc32a947c0..6558d15abb4 100644 --- a/src/System.Windows.Forms/tests/TestUtilities/WinFormsAppContextSwitchNames.cs +++ b/src/System.Windows.Forms/tests/TestUtilities/WinFormsAppContextSwitchNames.cs @@ -1,6 +1,8 @@ // 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.Serialization.Formatters.Binary; + namespace System; public static class WinFormsAppContextSwitchNames @@ -50,4 +52,16 @@ public const string ServicePointManagerCheckCrl /// public const string TreeNodeCollectionAddRangeRespectsSortOrder = "System.Windows.Forms.ApplyParentFontToMenus"; + + /// + /// The switch that controls whether or not the is enabled in the Clipboard. + /// + public const string ClipboardDragDropEnableUnsafeBinaryFormatterSerializationSwitchName + = "Windows.ClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization"; + + /// + /// The switch that controls whether or not the System.Windows.Forms.BinaryFormat.Deserializer is enabled in the Clipboard. + /// + public const string ClipboardDragDropEnableNrbfSerializationSwitchName + = "Windows.ClipboardDragDrop.EnableNrbfSerialization"; } diff --git a/src/System.Windows.Forms/tests/TestUtilities/WinFormsAppContextSwitchScope.cs b/src/System.Windows.Forms/tests/TestUtilities/WinFormsAppContextSwitchScope.cs new file mode 100644 index 00000000000..f531ee16829 --- /dev/null +++ b/src/System.Windows.Forms/tests/TestUtilities/WinFormsAppContextSwitchScope.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Windows.Forms.Primitives; + +namespace System; + +/// +/// Scope for temporarily setting an switch. Use in a statement. +/// +/// +/// +/// It is recommended to create wrappers for this struct for both simplicity and to allow adding synchronization. +/// See for an example of doing this. +/// +/// +public readonly ref struct WinFormsAppContextSwitchScope +{ + private readonly string _switchName; + private readonly bool _originalState; + + public WinFormsAppContextSwitchScope(string switchName, bool enable) + { + if (!AppContext.TryGetSwitch(AppContextSwitchNames.LocalAppContext_DisableCaching, out bool isEnabled) + || !isEnabled) + { + // It doesn't make sense to try messing with AppContext switches if they are going to be cached. + throw new InvalidOperationException("LocalAppContext switch caching is not disabled."); + } + + if (!AppContext.TryGetSwitch(switchName, out _originalState)) + { + var getDefaultValue = typeof(LocalAppContextSwitches).TestAccessor().CreateDelegate>("GetSwitchDefaultValue"); + _originalState = getDefaultValue(switchName); + } + + AppContext.SetSwitch(switchName, enable); + if (!AppContext.TryGetSwitch(switchName, out isEnabled) || isEnabled != enable) + { + throw new InvalidOperationException($"Could not set {switchName} to {enable}."); + } + + _switchName = switchName; + } + + public void Dispose() + { + AppContext.SetSwitch(_switchName, _originalState); + if (!AppContext.TryGetSwitch(_switchName, out bool isEnabled) || isEnabled != _originalState) + { + throw new InvalidOperationException($"Could not reset {_switchName} to {_originalState}."); + } + } +} diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ClipboardTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ClipboardTests.cs index 8dfaa4d1ef9..be4596186eb 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ClipboardTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ClipboardTests.cs @@ -659,6 +659,8 @@ public void Clipboard_BinaryFormatter_AppContextSwitch() { LocalAppContextSwitches.ClipboardDragDropEnableUnsafeBinaryFormatterSerialization.Should().BeFalse(); } + + LocalAppContextSwitches.ClipboardDragDropEnableUnsafeBinaryFormatterSerialization.Should().BeFalse(); } [WinFormsFact] @@ -678,6 +680,8 @@ public void Clipboard_NrbfSerializer_AppContextSwitch() { LocalAppContextSwitches.ClipboardDragDropEnableNrbfSerialization.Should().BeTrue(); } + + LocalAppContextSwitches.ClipboardDragDropEnableNrbfSerialization.Should().BeTrue(); } [WinFormsFact]