|
// Licensed to the .NET Foundation under one or more agreements.
// 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;
using Com = Windows.Win32.System.Com;
namespace System.Windows.Forms;
public unsafe partial class DataObject
{
internal unsafe partial class Composition
{
/// <summary>
/// Maps native pointer <see cref="Com.IDataObject"/> to <see cref="IDataObject"/>.
/// </summary>
private unsafe class NativeToWinFormsAdapter : ITypedDataObject, Com.IDataObject.Interface
{
private readonly AgileComPointer<Com.IDataObject> _nativeDataObject;
public NativeToWinFormsAdapter(Com.IDataObject* dataObject)
{
#if DEBUG
_nativeDataObject = new(dataObject, takeOwnership: true, trackDisposal: false);
#else
_nativeDataObject = new(dataObject, takeOwnership: true);
#endif
}
#region Com.IDataObject.Interface
HRESULT Com.IDataObject.Interface.DAdvise(Com.FORMATETC* pformatetc, uint advf, Com.IAdviseSink* pAdvSink, uint* pdwConnection)
{
using var nativeDataObject = _nativeDataObject.GetInterface();
return nativeDataObject.Value->DAdvise(pformatetc, advf, pAdvSink, pdwConnection);
}
HRESULT Com.IDataObject.Interface.DUnadvise(uint dwConnection)
{
using var nativeDataObject = _nativeDataObject.GetInterface();
return nativeDataObject.Value->DUnadvise(dwConnection);
}
HRESULT Com.IDataObject.Interface.EnumDAdvise(Com.IEnumSTATDATA** ppenumAdvise)
{
using var nativeDataObject = _nativeDataObject.GetInterface();
return nativeDataObject.Value->EnumDAdvise(ppenumAdvise);
}
HRESULT Com.IDataObject.Interface.EnumFormatEtc(uint dwDirection, Com.IEnumFORMATETC** ppenumFormatEtc)
{
using var nativeDataObject = _nativeDataObject.GetInterface();
return nativeDataObject.Value->EnumFormatEtc(dwDirection, ppenumFormatEtc);
}
HRESULT Com.IDataObject.Interface.GetData(Com.FORMATETC* pformatetcIn, Com.STGMEDIUM* pmedium)
{
using var nativeDataObject = _nativeDataObject.GetInterface();
return nativeDataObject.Value->GetData(pformatetcIn, pmedium);
}
HRESULT Com.IDataObject.Interface.GetDataHere(Com.FORMATETC* pformatetc, Com.STGMEDIUM* pmedium)
{
using var nativeDataObject = _nativeDataObject.GetInterface();
return nativeDataObject.Value->GetDataHere(pformatetc, pmedium);
}
HRESULT Com.IDataObject.Interface.QueryGetData(Com.FORMATETC* pformatetc)
{
using var nativeDataObject = _nativeDataObject.GetInterface();
return nativeDataObject.Value->QueryGetData(pformatetc);
}
HRESULT Com.IDataObject.Interface.GetCanonicalFormatEtc(Com.FORMATETC* pformatectIn, Com.FORMATETC* pformatetcOut)
{
using var nativeDataObject = _nativeDataObject.GetInterface();
return nativeDataObject.Value->GetCanonicalFormatEtc(pformatectIn, pformatetcOut);
}
HRESULT Com.IDataObject.Interface.SetData(Com.FORMATETC* pformatetc, Com.STGMEDIUM* pmedium, BOOL fRelease)
{
using var nativeDataObject = _nativeDataObject.GetInterface();
return nativeDataObject.Value->SetData(pformatetc, pmedium, fRelease);
}
#endregion
/// <summary>
/// Retrieves the specified format from the specified <paramref name="hglobal"/>.
/// </summary>
private static bool TryGetDataFromHGLOBAL<T>(
HGLOBAL hglobal,
string format,
Func<TypeName, Type>? resolver,
bool legacyMode,
[NotNullWhen(true)] out T? data)
{
data = default;
if (hglobal == 0)
{
return false;
}
object? value = format switch
{
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) },
_ => ReadObjectOrStreamFromHGLOBAL(hglobal, RestrictDeserializationToSafeTypes(format), resolver, legacyMode)
};
if (value is T t)
{
data = t;
return true;
}
return false;
static object? ReadObjectOrStreamFromHGLOBAL(
HGLOBAL hglobal,
bool restrictDeserialization,
Func<TypeName, Type>? resolver,
bool legacyMode)
{
MemoryStream stream = ReadByteStreamFromHGLOBAL(hglobal, out bool isSerializedObject);
return !isSerializedObject
? stream
: restrictDeserialization
? BinaryFormatUtilities.ReadRestrictedObjectFromStream<T>(stream, resolver, legacyMode)
: BinaryFormatUtilities.ReadObjectFromStream<T>(stream, resolver, legacyMode);
}
}
private static unsafe MemoryStream ReadByteStreamFromHGLOBAL(HGLOBAL hglobal, out bool isSerializedObject)
{
void* buffer = PInvokeCore.GlobalLock(hglobal);
if (buffer is null)
{
throw new ExternalException(SR.ExternalException, (int)HRESULT.E_OUTOFMEMORY);
}
try
{
int size = (int)PInvokeCore.GlobalSize(hglobal);
byte[] bytes = new byte[size];
Marshal.Copy((nint)buffer, bytes, 0, size);
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.
if (isSerializedObject = bytes.AsSpan().StartsWith(s_serializedObjectID))
{
index = s_serializedObjectID.Length;
}
return new MemoryStream(bytes, index, bytes.Length - index);
}
finally
{
PInvokeCore.GlobalUnlock(hglobal);
}
}
private static unsafe string ReadStringFromHGLOBAL(HGLOBAL hglobal, bool unicode)
{
string? stringData = null;
void* buffer = PInvokeCore.GlobalLock(hglobal);
try
{
stringData = unicode ? new string((char*)buffer) : new string((sbyte*)buffer);
}
finally
{
PInvokeCore.GlobalUnlock(hglobal);
}
return stringData;
}
private static unsafe string ReadUtf8StringFromHGLOBAL(HGLOBAL hglobal)
{
void* buffer = PInvokeCore.GlobalLock(hglobal);
try
{
int size = (int)PInvokeCore.GlobalSize(hglobal);
return Encoding.UTF8.GetString((byte*)buffer, size - 1);
}
finally
{
PInvokeCore.GlobalUnlock(hglobal);
}
}
private static unsafe string[]? ReadFileListFromHDROP(HDROP hdrop)
{
uint count = PInvoke.DragQueryFile(hdrop, iFile: 0xFFFFFFFF, lpszFile: null, cch: 0);
if (count == 0)
{
return null;
}
Span<char> fileName = stackalloc char[(int)PInvokeCore.MAX_PATH + 1];
string[] files = new string[count];
fixed (char* buffer = fileName)
{
for (uint i = 0; i < count; i++)
{
uint charactersCopied = PInvoke.DragQueryFile(hdrop, i, buffer, (uint)fileName.Length);
if (charactersCopied == 0)
{
continue;
}
string s = fileName[..(int)charactersCopied].ToString();
files[i] = s;
}
}
return files;
}
/// <summary>
/// Extracts a managed object from <see cref="Com.IDataObject"/> of the specified format.
/// </summary>
/// <param name="doNotContinue">
/// A restricted type was encountered, do not continue trying to deserialize.
/// </param>
/// <returns>
/// <para>
/// <see langword="true"/> if the managed object of <see cref="Type"/> <typeparamref name="T"/> was successfully
/// created, <see langword="false"/> if the payload does not contain the specified format or the specified type.
/// </para>
/// <para>
/// If <paramref name="dataObject"/> contains <see cref="MemoryStream"/> that contains a serialized object,
/// we return that object cast to <typeparamref name="T"/> or null. If that <see cref="MemoryStream"/> is
/// not a serialized object, and a stream was requested, i.e. can be assigned to <typeparamref name="T"/>
/// we return that <see cref="MemoryStream"/>.
/// </para>
/// </returns>
/// <exception cref="NotSupportedException"> is deserialization failed.</exception>
private static bool TryGetObjectFromDataObject<T>(
Com.IDataObject* dataObject,
string format,
Func<TypeName, Type>? resolver,
bool legacyMode,
out bool doNotContinue,
[NotNullWhen(true)] out T? data)
{
data = default;
doNotContinue = false;
bool result = false;
try
{
// Try to get the data as a bitmap first.
if ((typeof(Bitmap) == typeof(T) || typeof(Image) == typeof(T))
&& TryGetBitmapData(dataObject, format, out Bitmap? bitmap))
{
data = (T)(object)bitmap;
return true;
}
result = TryGetHGLOBALData(dataObject, format, resolver, legacyMode, out doNotContinue, out data);
if (!result && !doNotContinue)
{
// Lastly check to see if the data is an IStream.
result = TryGetIStreamData(dataObject, format, resolver, legacyMode, out data);
}
}
catch (Exception e) when (legacyMode || e is not NotSupportedException)
{
Debug.Fail(e.ToString());
}
return result;
}
private static bool TryGetHGLOBALData<T>(
Com.IDataObject* dataObject,
string format,
Func<TypeName, Type>? resolver,
bool legacyMode,
out bool doNotContinue,
[NotNullWhen(true)] out T? data)
{
data = default;
doNotContinue = false;
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;
}
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.");
// This happens in copy == false case when the managed type does not have the [Serializable] attribute.
Debug.WriteLineIf(hr == HRESULT.E_UNEXPECTED, "E_UNEXPECTED returned when trying to get clipboard data.");
Debug.WriteLineIf(hr == HRESULT.COR_E_SERIALIZATION,
"COR_E_SERIALIZATION returned when trying to get clipboard data, for example, BinaryFormatter threw SerializationException.");
bool result = false;
try
{
if (medium.tymed == Com.TYMED.TYMED_HGLOBAL && !medium.hGlobal.IsNull && hr != HRESULT.COR_E_SERIALIZATION)
{
result = TryGetDataFromHGLOBAL(medium.hGlobal, format, resolver, legacyMode, out data);
}
}
catch (RestrictedTypeDeserializationException)
{
result = false;
data = default;
doNotContinue = true;
}
catch (Exception ex) when (legacyMode || ex is not NotSupportedException)
{
Debug.WriteLine(ex.ToString());
}
finally
{
PInvoke.ReleaseStgMedium(ref medium);
}
return result;
}
private static unsafe bool TryGetIStreamData<T>(
Com.IDataObject* dataObject,
string format,
Func<TypeName, Type>? resolver,
bool legacyMode,
[NotNullWhen(true)] 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
};
// Limit the # of exceptions we may throw below.
if (dataObject->QueryGetData(formatEtc).Failed
|| dataObject->GetData(formatEtc, out Com.STGMEDIUM medium).Failed)
{
return false;
}
HGLOBAL hglobal = default;
try
{
if (medium.tymed != Com.TYMED.TYMED_ISTREAM || medium.hGlobal.IsNull)
{
return false;
}
using ComScope<Com.IStream> 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);
// 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);
return TryGetDataFromHGLOBAL(hglobal, format, resolver, legacyMode, out data);
}
finally
{
if (!hglobal.IsNull)
{
PInvokeCore.GlobalFree(hglobal);
}
PInvoke.ReleaseStgMedium(ref medium);
}
}
private static bool TryGetBitmapData(Com.IDataObject* dataObject, string format, [NotNullWhen(true)] out Bitmap? data)
{
data = default;
if (format != DataFormats.BitmapConstant)
{
return false;
}
Com.FORMATETC formatEtc = new()
{
cfFormat = (ushort)DataFormats.GetFormat(format).Id,
dwAspect = (uint)Com.DVASPECT.DVASPECT_CONTENT,
lindex = -1,
tymed = (uint)Com.TYMED.TYMED_GDI
};
Com.STGMEDIUM medium = default;
if (dataObject->QueryGetData(formatEtc).Succeeded)
{
HRESULT hr = dataObject->GetData(formatEtc, out 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.");
}
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 Bitmap clipboardBitmap)
{
data = (Bitmap)clipboardBitmap.Clone();
clipboardBitmap.Dispose();
return true;
}
}
finally
{
PInvoke.ReleaseStgMedium(ref medium);
}
return false;
}
private static void ThrowIfFormatAndTypeRequireResolver<T>(string format)
{
// Restricted format is either read directly from the HGLOBAL or serialization record is read manually.
if (!IsRestrictedFormat(format)
// This check is a convenience for simple usages if TryGetData APIs that don't take the resolver.
&& IsUnboundedType())
{
throw new NotSupportedException(string.Format(
SR.ClipboardOrDragDrop_InvalidType,
typeof(T).FullName));
}
static bool IsUnboundedType()
{
if (typeof(T) == typeof(object))
{
return true;
}
Type type = typeof(T);
// Image is a special case because we are reading Bitmaps directly from the SerializationRecord.
return type.IsInterface || (typeof(T) != typeof(Image) && type.IsAbstract);
}
}
private bool TryGetDataInternal<T>(
string format,
Func<TypeName, Type>? resolver,
bool autoConvert,
bool legacyMode,
[NotNullWhen(true)] out T? data)
{
data = default;
if (!legacyMode && resolver is null)
{
// DataObject.GetData methods do not validate format string, but the typed methods do.
// This validation is specific to the WinForms DataObject implementation, it's not executed for
// overridden methods.
ThrowIfFormatAndTypeRequireResolver<T>(format);
}
using var nativeDataObject = _nativeDataObject.GetInterface();
bool result = TryGetObjectFromDataObject(
nativeDataObject, format, resolver, legacyMode, out bool doNotContinue, out data);
if (doNotContinue)
{
// Specified format is a restricted one, as only restricted formats set doNotContinue,
// but content required BinaryFormatter deserialization, as doNotContinue is set when
// BinaryFormatter fails, legacy methods return null.
data = default;
return false;
}
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))
{
continue;
}
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 result;
}
}
return result;
}
#region IDataObject
public object? GetData(string format, bool autoConvert)
{
TryGetDataInternal(format, resolver: null, autoConvert, legacyMode: true, out object? data);
return data;
}
public object? GetData(string format) => GetData(format, autoConvert: true);
public object? GetData(Type format) => GetData(format.FullName!);
public bool GetDataPresent(Type format) => GetDataPresent(format.FullName!);
public bool GetDataPresent(string format, bool autoConvert)
{
bool dataPresent = GetDataPresentInner(format);
if (dataPresent || !autoConvert || GetMappedFormats(format) is not { } mappedFormats)
{
return dataPresent;
}
foreach (string mappedFormat in mappedFormats)
{
if (!format.Equals(mappedFormat) && (dataPresent = GetDataPresentInner(mappedFormat)))
{
break;
}
}
return dataPresent;
}
public bool GetDataPresent(string format) => GetDataPresent(format, autoConvert: true);
public string[] GetFormats(bool autoConvert)
{
using var nativeDataObject = _nativeDataObject.GetInterface();
Debug.Assert(!nativeDataObject.IsNull, "You must have an innerData on all DataObjects");
using ComScope<Com.IEnumFORMATETC> enumFORMATETC = new(null);
nativeDataObject.Value->EnumFormatEtc((uint)DATADIR.DATADIR_GET, enumFORMATETC).AssertSuccess();
if (enumFORMATETC.IsNull)
{
return [];
}
// Since we are only adding elements to the HashSet, the order will be preserved.
HashSet<string> distinctFormats = [];
enumFORMATETC.Value->Reset();
Com.FORMATETC formatEtc = default;
while (enumFORMATETC.Value->Next(1, &formatEtc) == HRESULT.S_OK)
{
string name = DataFormats.GetFormat(formatEtc.cfFormat).Name;
if (autoConvert)
{
string[] mappedFormats = GetMappedFormats(name)!;
for (int i = 0; i < mappedFormats.Length; i++)
{
distinctFormats.Add(mappedFormats[i]);
}
}
else
{
distinctFormats.Add(name);
}
formatEtc = default;
}
return [.. distinctFormats];
}
public string[] GetFormats() => GetFormats(autoConvert: true);
public void SetData(string format, bool autoConvert, object? data) { }
public void SetData(string format, object? data) { }
public void SetData(Type format, object? data) { }
public void SetData(object? data) { }
#endregion
#region ITypedDataObject
public bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(
string format,
Func<TypeName, Type> resolver,
bool autoConvert,
[NotNullWhen(true), MaybeNullWhen(false)] out T data) =>
TryGetDataInternal(format, resolver, autoConvert, legacyMode: false, out data);
public bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(
string format,
bool autoConvert,
[NotNullWhen(true), MaybeNullWhen(false)] out T data) =>
TryGetDataInternal(format, resolver: null!, autoConvert, legacyMode: false, out data);
public bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(
string format,
[NotNullWhen(true), MaybeNullWhen(false)] out T data) =>
TryGetDataInternal(format, resolver: null!, autoConvert: true, legacyMode: false, out data);
public bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(
[NotNullWhen(true), MaybeNullWhen(false)] out T data) =>
TryGetDataInternal(typeof(T).FullName!, resolver: null!, autoConvert: true, legacyMode: false, out data);
#endregion
private bool GetDataPresentInner(string format)
{
Com.FORMATETC formatEtc = new()
{
cfFormat = (ushort)(DataFormats.GetFormat(format).Id),
dwAspect = (uint)Com.DVASPECT.DVASPECT_CONTENT,
lindex = -1,
tymed = (uint)AllowedTymeds
};
using var nativeDataObject = _nativeDataObject.GetInterface();
HRESULT hr = nativeDataObject.Value->QueryGetData(formatEtc);
// APIs will return S_FALSE, which is "success"
return hr == HRESULT.S_OK;
}
}
}
}
|