.NET version
10.0.5
Did it work in .NET Framework?
Yes
Did it work in any of the earlier releases of .NET Core or .NET 5+?
I believe this worked in net6
Issue description
It doesn't seem like the assumption that RTF placed on the clipboard be null-terminated is correct?
|
// Can't find the explicit docs for CF_RTF, but we've always treated it as null terminated. |
RTF is transferred using the registered "Rich Text Format" clipboard format, which is treated as an opaque HGLOBAL buffer. The clipboard contract is size-based, not terminator-based, and only specific standard text formats (e.g., CF_TEXT and CF_UNICODETEXT) guarantee null termination.
https://learn.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats
As Raymond notes, registered formats have no structural guarantees beyond the size of the buffer - they are not required to be null-terminated:
https://devblogs.microsoft.com/oldnewthing/20251225-00/?p=111914
In practice, the framework's assumption appears to be invalid: RTF placed on the clipboard by applications like PowerPoint does not include a trailing null and must be read using the buffer size using GlobalSize, not by scanning for a \0.
Steps to reproduce
- Copy rich text onto the clipboard from an application like PowerPoint
- Author a .
NET10 app that calls Clipboard.GetText(TextDataFormat.Rtf)
-> Note that an empty string is returned
If, instead, you do the following, you'll notice that you get completely valid RTF:
var clipboardFormatRtf = user32.RegisterClipboardFormat("Rich Text Format");
if (clipboardFormatRtf == 0 || !user32.OpenClipboard(IntPtr.Zero))
{
return string.Empty;
}
try
{
if (user32.IsClipboardFormatAvailable(clipboardFormatRtf))
{
var hData = user32.GetClipboardData(clipboardFormatRtf);
if (hData == IntPtr.Zero)
{
return string.Empty;
}
var pData = kernel32.GlobalLock(hData);
if (pData == IntPtr.Zero)
{
return string.Empty;
}
try
{
var size = kernel32.GlobalSize(hData);
if (size <= 0 || size > int.MaxValue)
{
return string.Empty;
}
unsafe
{
var span = new ReadOnlySpan<byte>((byte*)pData, (int)size);
var nullIndex = span.IndexOf((byte)0);
if (nullIndex >= 0)
{
span = span[..nullIndex];
}
return new string((sbyte*)pData, 0, span.Length);
}
}
finally
{
kernel32.GlobalUnlock(hData);
}
}
}
finally
{
user32.CloseClipboard();
}
.NET version
10.0.5
Did it work in .NET Framework?
Yes
Did it work in any of the earlier releases of .NET Core or .NET 5+?
I believe this worked in net6
Issue description
It doesn't seem like the assumption that RTF placed on the clipboard be null-terminated is correct?
winforms/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/Composition.NativeToManagedAdapter.cs
Line 201 in 9bff4e6
RTF is transferred using the registered "Rich Text Format" clipboard format, which is treated as an opaque
HGLOBALbuffer. The clipboard contract is size-based, not terminator-based, and only specific standard text formats (e.g.,CF_TEXTandCF_UNICODETEXT) guarantee null termination.https://learn.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats
As Raymond notes, registered formats have no structural guarantees beyond the size of the buffer - they are not required to be null-terminated:
https://devblogs.microsoft.com/oldnewthing/20251225-00/?p=111914
In practice, the framework's assumption appears to be invalid: RTF placed on the clipboard by applications like PowerPoint does not include a trailing null and must be read using the buffer size using
GlobalSize, not by scanning for a\0.Steps to reproduce
NET10app that callsClipboard.GetText(TextDataFormat.Rtf)-> Note that an empty string is returned
If, instead, you do the following, you'll notice that you get completely valid RTF: