Skip to content

Commit f96a54f

Browse files
authored
Merge pull request #12745 from lonitra/backport12738
[release/9.0] Only Unwrap If Given IDataObject
2 parents 135e39a + f629c98 commit f96a54f

File tree

5 files changed

+207
-61
lines changed

5 files changed

+207
-61
lines changed

src/System.Private.Windows.Core/src/Windows/Win32/System/Com/ComHelpers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ internal static bool SupportsInterface<T>(object? @object) where T : unmanaged,
132132
/// <summary>
133133
/// Attempts to unwrap a ComWrapper CCW as a particular managed object.
134134
/// </summary>
135-
private static bool TryUnwrapComWrapperCCW<TWrapper>(
135+
public static bool TryUnwrapComWrapperCCW<TWrapper>(
136136
IUnknown* unknown,
137137
[NotNullWhen(true)] out TWrapper? @interface) where TWrapper : class
138138
{

src/System.Windows.Forms/src/System/Windows/Forms/OLE/Clipboard.cs

Lines changed: 15 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,13 @@ public static unsafe void SetDataObject(object data, bool copy, int retryTimes,
4040
ArgumentOutOfRangeException.ThrowIfNegative(retryTimes);
4141
ArgumentOutOfRangeException.ThrowIfNegative(retryDelay);
4242

43-
// Always wrap the data in our DataObject since we know how to retrieve our DataObject from the proxy OleGetClipboard returns.
44-
DataObject wrappedData = data is DataObject { IsWrappedForClipboard: true } alreadyWrapped
45-
? alreadyWrapped
46-
: new DataObject(data) { IsWrappedForClipboard = true };
47-
using var dataObject = ComHelpers.GetComScope<Com.IDataObject>(wrappedData);
43+
// Always wrap the data if not already a DataObject. Mark whether the data is an IDataObject so we unwrap it properly on retrieval.
44+
DataObject dataObject = data as DataObject ?? new DataObject(data) { IsOriginalNotIDataObject = data is not IDataObject };
45+
using var iDataObject = ComHelpers.GetComScope<Com.IDataObject>(dataObject);
4846

4947
HRESULT hr;
5048
int retry = retryTimes;
51-
while ((hr = PInvoke.OleSetClipboard(dataObject)).Failed)
49+
while ((hr = PInvoke.OleSetClipboard(iDataObject)).Failed)
5250
{
5351
if (--retry < 0)
5452
{
@@ -100,52 +98,24 @@ public static unsafe void SetDataObject(object data, bool copy, int retryTimes,
10098

10199
// OleGetClipboard always returns a proxy. The proxy forwards all IDataObject method calls to the real data object,
102100
// without giving out the real data object. If the data placed on the clipboard is not one of our CCWs or the clipboard
103-
// has been flushed, marshal will create a wrapper around the proxy for us to use. However, if the data placed on
101+
// has been flushed, a wrapper around the proxy for us to use will be given. However, if the data placed on
104102
// the clipboard is one of our own and the clipboard has not been flushed, we need to retrieve the real data object
105-
// pointer in order to retrieve the original managed object via ComWrappers. To do this, we must query for an
106-
// interface that is not known to the proxy e.g. IComCallableWrapper. If we are able to query for IComCallableWrapper
107-
// it means that the real data object is one of our CCWs and we've retrieved it successfully,
103+
// pointer in order to retrieve the original managed object via ComWrappers if an IDataObject was set on the clipboard.
104+
// To do this, we must query for an interface that is not known to the proxy e.g. IComCallableWrapper.
105+
// If we are able to query for IComCallableWrapper it means that the real data object is one of our CCWs and we've retrieved it successfully,
108106
// otherwise it is not ours and we will use the wrapped proxy.
109-
IUnknown* target = default;
110107
var realDataObject = proxyDataObject.TryQuery<IComCallableWrapper>(out hr);
111-
if (hr.Succeeded)
112-
{
113-
target = realDataObject.AsUnknown;
114-
}
115-
else
116-
{
117-
target = proxyDataObject.AsUnknown;
118-
}
119-
120-
if (!ComHelpers.TryGetObjectForIUnknown(target, out object? managedDataObject))
121-
{
122-
target->Release();
123-
return null;
124-
}
125-
126-
if (managedDataObject is not Com.IDataObject.Interface dataObject)
127-
{
128-
// We always wrap data set on the Clipboard in a DataObject, so if we do not have
129-
// a IDataObject.Interface this means built-in com support is turned off and
130-
// we have a proxy where there is no way to retrieve the original data object
131-
// pointer from it likely because either the clipboard was flushed or the data on the
132-
// clipboard is from another process. We need to mimic built-in com behavior and wrap the proxy ourselves.
133-
// DataObject will ref count proxyDataObject properly to take ownership.
134-
return new DataObject(proxyDataObject.Value);
135-
}
136108

137-
if (dataObject is DataObject { IsWrappedForClipboard: true } wrappedData)
109+
if (hr.Succeeded
110+
&& ComHelpers.TryUnwrapComWrapperCCW(realDataObject.AsUnknown, out DataObject? dataObject)
111+
&& !dataObject.IsOriginalNotIDataObject)
138112
{
139-
// There is a DataObject on the clipboard that we placed there. If the real data object
140-
// implements IDataObject, we want to unwrap it and return it. Otherwise return
141-
// the DataObject as is.
142-
return wrappedData.TryUnwrapInnerIDataObject();
113+
// An IDataObject was given to us to place on the clipboard. We want to unwrap and return it instead of a proxy.
114+
return dataObject.TryUnwrapInnerIDataObject();
143115
}
144116

145-
// We did not place the data on the clipboard. Fall back to old behavior.
146-
return dataObject is IDataObject ido && !Marshal.IsComObject(dataObject)
147-
? ido
148-
: new DataObject(dataObject);
117+
// Original data given wasn't an IDataObject, give the proxy value back.
118+
return new DataObject(proxyDataObject.Value);
149119
}
150120

151121
/// <summary>

src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,9 @@ public DataObject(object data)
7373
internal DataObject(string format, bool autoConvert, object data) : this() => SetData(format, autoConvert, data);
7474

7575
/// <summary>
76-
/// Flags that the original data was wrapped for clipboard purposes.
76+
/// Flags that the original data was not a user passed <see cref="IDataObject"/>.
7777
/// </summary>
78-
internal bool IsWrappedForClipboard { get; init; }
78+
internal bool IsOriginalNotIDataObject { get; init; }
7979

8080
/// <summary>
8181
/// Returns the inner data that the <see cref="DataObject"/> was created with if the original data implemented
@@ -84,7 +84,7 @@ public DataObject(object data)
8484
/// </summary>
8585
internal IDataObject TryUnwrapInnerIDataObject()
8686
{
87-
Debug.Assert(IsWrappedForClipboard, "This method should only be used for clipboard purposes.");
87+
Debug.Assert(!IsOriginalNotIDataObject, "This method should only be used for clipboard purposes.");
8888
return _innerData.OriginalIDataObject is { } original ? original : this;
8989
}
9090

src/System.Windows.Forms/tests/ComDisabledTests/ClipboardComTests.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
#nullable enable
55

66
using System.Drawing;
7+
using System.Runtime.InteropServices;
8+
using ComTypes = System.Runtime.InteropServices.ComTypes;
79

810
namespace System.Windows.Forms.Tests;
911

@@ -28,4 +30,68 @@ public void Clipboard_SetData_CustomFormat_Color()
2830
Clipboard.ContainsData(format).Should().BeTrue();
2931
Clipboard.GetData(format).Should().Be(Color.Black);
3032
}
33+
34+
[WinFormsFact]
35+
public void Clipboard_GetSet_IDataObject_RoundTrip_ReturnsExpected()
36+
{
37+
CustomDataObject realDataObject = new();
38+
Clipboard.SetDataObject(realDataObject);
39+
40+
IDataObject clipboardDataObject = Clipboard.GetDataObject().Should().BeAssignableTo<IDataObject>().Subject;
41+
clipboardDataObject.Should().BeSameAs(realDataObject);
42+
clipboardDataObject.GetDataPresent("Foo").Should().BeTrue();
43+
clipboardDataObject.GetData("Foo").Should().Be("Bar");
44+
}
45+
46+
[WinFormsFact]
47+
public void Clipboard_SetDataObject_DerivedDataObject_ReturnsExpected()
48+
{
49+
DerivedDataObject derived = new();
50+
Clipboard.SetDataObject(derived);
51+
Clipboard.GetDataObject().Should().BeSameAs(derived);
52+
}
53+
54+
private class DerivedDataObject : DataObject { }
55+
56+
private class CustomDataObject : IDataObject, ComTypes.IDataObject
57+
{
58+
[DllImport("shell32.dll")]
59+
public static extern int SHCreateStdEnumFmtEtc(uint cfmt, ComTypes.FORMATETC[] afmt, out ComTypes.IEnumFORMATETC ppenumFormatEtc);
60+
61+
int ComTypes.IDataObject.DAdvise(ref ComTypes.FORMATETC pFormatetc, ComTypes.ADVF advf, ComTypes.IAdviseSink adviseSink, out int connection) => throw new NotImplementedException();
62+
void ComTypes.IDataObject.DUnadvise(int connection) => throw new NotImplementedException();
63+
int ComTypes.IDataObject.EnumDAdvise(out ComTypes.IEnumSTATDATA enumAdvise) => throw new NotImplementedException();
64+
ComTypes.IEnumFORMATETC ComTypes.IDataObject.EnumFormatEtc(ComTypes.DATADIR direction)
65+
{
66+
if (direction == ComTypes.DATADIR.DATADIR_GET)
67+
{
68+
// Create enumerator and return it
69+
ComTypes.IEnumFORMATETC enumerator;
70+
if (SHCreateStdEnumFmtEtc(0, [], out enumerator) == 0)
71+
{
72+
return enumerator;
73+
}
74+
}
75+
76+
throw new NotImplementedException();
77+
}
78+
79+
int ComTypes.IDataObject.GetCanonicalFormatEtc(ref ComTypes.FORMATETC formatIn, out ComTypes.FORMATETC formatOut) => throw new NotImplementedException();
80+
object IDataObject.GetData(string format, bool autoConvert) => format == "Foo" ? "Bar" : null!;
81+
object IDataObject.GetData(string format) => format == "Foo" ? "Bar" : null!;
82+
object IDataObject.GetData(Type format) => null!;
83+
void ComTypes.IDataObject.GetData(ref ComTypes.FORMATETC format, out ComTypes.STGMEDIUM medium) => throw new NotImplementedException();
84+
void ComTypes.IDataObject.GetDataHere(ref ComTypes.FORMATETC format, ref ComTypes.STGMEDIUM medium) => throw new NotImplementedException();
85+
bool IDataObject.GetDataPresent(string format, bool autoConvert) => format == "Foo";
86+
bool IDataObject.GetDataPresent(string format) => format == "Foo";
87+
bool IDataObject.GetDataPresent(Type format) => false;
88+
string[] IDataObject.GetFormats(bool autoConvert) => ["Foo"];
89+
string[] IDataObject.GetFormats() => ["Foo"];
90+
int ComTypes.IDataObject.QueryGetData(ref ComTypes.FORMATETC format) => throw new NotImplementedException();
91+
void IDataObject.SetData(string format, bool autoConvert, object? data) => throw new NotImplementedException();
92+
void IDataObject.SetData(string format, object? data) => throw new NotImplementedException();
93+
void IDataObject.SetData(Type format, object? data) => throw new NotImplementedException();
94+
void IDataObject.SetData(object? data) => throw new NotImplementedException();
95+
void ComTypes.IDataObject.SetData(ref ComTypes.FORMATETC formatIn, ref ComTypes.STGMEDIUM medium, bool release) => throw new NotImplementedException();
96+
}
3197
}

src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ClipboardTests.cs

Lines changed: 122 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Runtime.InteropServices;
1111
using System.Runtime.Serialization.Formatters.Binary;
1212
using Com = Windows.Win32.System.Com;
13+
using Windows.Win32.System.Ole;
1314
using ComTypes = System.Runtime.InteropServices.ComTypes;
1415

1516
namespace System.Windows.Forms.Tests;
@@ -520,20 +521,14 @@ public unsafe void Clipboard_GetClipboard_ReturnsProxy()
520521
((nint)dataUnknown.Value).Should().Be((nint)realDataPointerUnknown.Value);
521522
}
522523

524+
private class DerivedDataObject : DataObject { }
525+
523526
[WinFormsFact]
524-
public void Clipboard_Set_DoesNotWrapTwice()
527+
public void Clipboard_SetDataObject_DerivedDataObject_ReturnsExpected()
525528
{
526-
string realDataObject = string.Empty;
527-
Clipboard.SetDataObject(realDataObject);
528-
529-
IDataObject? clipboardDataObject = Clipboard.GetDataObject();
530-
var dataObject = clipboardDataObject.Should().BeOfType<DataObject>().Which;
531-
dataObject.IsWrappedForClipboard.Should().BeTrue();
532-
533-
Clipboard.SetDataObject(clipboardDataObject!);
534-
IDataObject? clipboardDataObject2 = Clipboard.GetDataObject();
535-
clipboardDataObject2.Should().NotBeNull();
536-
clipboardDataObject2.Should().BeSameAs(clipboardDataObject);
529+
DerivedDataObject derived = new();
530+
Clipboard.SetDataObject(derived);
531+
Clipboard.GetDataObject().Should().BeSameAs(derived);
537532
}
538533

539534
[WinFormsFact]
@@ -589,4 +584,119 @@ ComTypes.IEnumFORMATETC ComTypes.IDataObject.EnumFormatEtc(ComTypes.DATADIR dire
589584
void IDataObject.SetData(object? data) => throw new NotImplementedException();
590585
void ComTypes.IDataObject.SetData(ref ComTypes.FORMATETC formatIn, ref ComTypes.STGMEDIUM medium, bool release) => throw new NotImplementedException();
591586
}
587+
588+
[DllImport("user32.dll")]
589+
private static extern bool CloseClipboard();
590+
591+
[DllImport("user32.dll")]
592+
private static extern bool OpenClipboard(HWND hWndNewOwner);
593+
594+
[DllImport("user32.dll")]
595+
private static extern bool SetClipboardData(uint uFormat, HANDLE data);
596+
597+
[WinFormsFact]
598+
public unsafe void Clipboard_RawClipboard_SetClipboardData_ReturnsExpected()
599+
{
600+
OpenClipboard(HWND.Null).Should().BeTrue();
601+
string testString = "test";
602+
SetClipboardData((uint)CLIPBOARD_FORMAT.CF_UNICODETEXT, (HANDLE)Marshal.StringToHGlobalUni(testString));
603+
CloseClipboard().Should().BeTrue();
604+
605+
DataObject dataObject = Clipboard.GetDataObject().Should().BeOfType<DataObject>().Which;
606+
dataObject.GetData(DataFormats.Text).Should().Be(testString);
607+
608+
Clipboard.ContainsText().Should().BeTrue();
609+
Clipboard.ContainsData(DataFormats.Text).Should().BeTrue();
610+
Clipboard.ContainsData(DataFormats.UnicodeText).Should().BeTrue();
611+
612+
Clipboard.GetText().Should().Be(testString);
613+
Clipboard.GetText(TextDataFormat.Text).Should().Be(testString);
614+
Clipboard.GetText(TextDataFormat.UnicodeText).Should().Be(testString);
615+
616+
Clipboard.GetData("System.String").Should().BeNull();
617+
}
618+
619+
[WinFormsFact]
620+
public void Clipboard_SetData_Text_Format_AllUpper()
621+
{
622+
// The fact that casing on input matters is likely incorrect, but behavior has been this way.
623+
Clipboard.SetData("TEXT", "Hello, World!");
624+
Clipboard.ContainsText().Should().BeTrue();
625+
Clipboard.ContainsData("TEXT").Should().BeTrue();
626+
Clipboard.ContainsData(DataFormats.Text).Should().BeTrue();
627+
Clipboard.ContainsData(DataFormats.UnicodeText).Should().BeTrue();
628+
629+
IDataObject dataObject = Clipboard.GetDataObject().Should().BeAssignableTo<IDataObject>().Subject;
630+
string[] formats = dataObject.GetFormats();
631+
formats.Should().BeEquivalentTo(["System.String", "UnicodeText", "Text"]);
632+
633+
formats = dataObject.GetFormats(autoConvert: false);
634+
formats.Should().BeEquivalentTo(["Text"]);
635+
636+
// CLIPBRD_E_BAD_DATA returned when trying to get clipboard data.
637+
Clipboard.GetText().Should().BeEmpty();
638+
Clipboard.GetText(TextDataFormat.Text).Should().BeEmpty();
639+
Clipboard.GetText(TextDataFormat.UnicodeText).Should().BeEmpty();
640+
641+
Clipboard.GetData("System.String").Should().BeNull();
642+
Clipboard.GetData("TEXT").Should().BeNull();
643+
}
644+
645+
[WinFormsFact]
646+
public void Clipboard_SetData_Text_Format_CanonicalCase()
647+
{
648+
string expected = "Hello, World!";
649+
Clipboard.SetData("Text", expected);
650+
Clipboard.ContainsText().Should().BeTrue();
651+
Clipboard.ContainsData("TEXT").Should().BeTrue();
652+
Clipboard.ContainsData(DataFormats.Text).Should().BeTrue();
653+
Clipboard.ContainsData(DataFormats.UnicodeText).Should().BeTrue();
654+
655+
IDataObject dataObject = Clipboard.GetDataObject().Should().BeAssignableTo<IDataObject>().Subject;
656+
string[] formats = dataObject.GetFormats();
657+
formats.Should().BeEquivalentTo(["System.String", "UnicodeText", "Text"]);
658+
659+
formats = dataObject.GetFormats(autoConvert: false);
660+
formats.Should().BeEquivalentTo(["System.String", "UnicodeText", "Text"]);
661+
662+
Clipboard.GetText().Should().Be(expected);
663+
Clipboard.GetText(TextDataFormat.Text).Should().Be(expected);
664+
Clipboard.GetText(TextDataFormat.UnicodeText).Should().Be(expected);
665+
666+
Clipboard.GetData("System.String").Should().Be(expected);
667+
668+
// Case sensitivity matters so we end up reading stream/object from HGLOBAL instead of string.
669+
MemoryStream stream = Clipboard.GetData("TEXT").Should().BeOfType<MemoryStream>().Subject;
670+
byte[] array = stream.ToArray();
671+
array.Should().BeEquivalentTo("Hello, World!\0"u8.ToArray());
672+
}
673+
674+
[WinFormsFact]
675+
public void Clipboard_SetDataObject_Text()
676+
{
677+
string expected = "Hello, World!";
678+
Clipboard.SetDataObject(expected);
679+
Clipboard.ContainsText().Should().BeTrue();
680+
Clipboard.ContainsData("TEXT").Should().BeTrue();
681+
Clipboard.ContainsData(DataFormats.Text).Should().BeTrue();
682+
Clipboard.ContainsData(DataFormats.UnicodeText).Should().BeTrue();
683+
684+
IDataObject dataObject = Clipboard.GetDataObject().Should().BeAssignableTo<IDataObject>().Subject;
685+
string[] formats = dataObject.GetFormats();
686+
formats.Should().BeEquivalentTo(["System.String", "UnicodeText", "Text"]);
687+
688+
formats = dataObject.GetFormats(autoConvert: false);
689+
formats.Should().BeEquivalentTo(["System.String", "UnicodeText", "Text"]);
690+
691+
Clipboard.GetText().Should().Be(expected);
692+
Clipboard.GetText(TextDataFormat.Text).Should().Be(expected);
693+
Clipboard.GetText(TextDataFormat.UnicodeText).Should().Be(expected);
694+
695+
Clipboard.GetData("System.String").Should().Be(expected);
696+
697+
// Case sensitivity matters so we end up reading stream/object from HGLOBAL instead of string.
698+
MemoryStream stream = Clipboard.GetData("TEXT").Should().BeOfType<MemoryStream>().Subject;
699+
byte[] array = stream.ToArray();
700+
array.Should().BeEquivalentTo("Hello, World!\0"u8.ToArray());
701+
}
592702
}

0 commit comments

Comments
 (0)