From 05cf8392188b57afdb4ad4f1bde33d36df4e2db5 Mon Sep 17 00:00:00 2001 From: "Simon Zhao (BEYONDSOFT CONSULTING INC)" Date: Fri, 10 Apr 2026 15:15:31 +0800 Subject: [PATCH 1/3] Fix issue 14458: Clipboard.GetText(TextDataFormat.Rtf) does not retrieve RTF that lacks a null terminating character --- .../Ole/Composition.NativeToManagedAdapter.cs | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/Composition.NativeToManagedAdapter.cs b/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/Composition.NativeToManagedAdapter.cs index 9799909b8dc..98ae1be7087 100644 --- a/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/Composition.NativeToManagedAdapter.cs +++ b/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/Composition.NativeToManagedAdapter.cs @@ -114,8 +114,8 @@ private static bool TryGetDataFromHGLOBAL( object? value = request.Format switch { - DataFormatNames.Text or DataFormatNames.Rtf or DataFormatNames.OemText => - ReadStringFromHGLOBAL(hglobal, unicode: false), + DataFormatNames.Text or DataFormatNames.OemText => ReadStringFromHGLOBAL(hglobal, unicode: false), + DataFormatNames.Rtf => ReadRegisteredFormatStringFromHGLOBAL(hglobal, Encoding.Default), DataFormatNames.Html or DataFormatNames.Xaml => ReadUtf8StringFromHGLOBAL(hglobal), DataFormatNames.UnicodeText => ReadStringFromHGLOBAL(hglobal, unicode: true), DataFormatNames.FileDrop => ReadFileListFromHDROP((HDROP)(nint)hglobal), @@ -242,6 +242,25 @@ private static unsafe string ReadStringFromHGLOBAL(HGLOBAL hglobal, bool unicode } } + private static unsafe string ReadRegisteredFormatStringFromHGLOBAL(HGLOBAL hglobal, Encoding encoding) + { + void* buffer = PInvokeCore.GlobalLock(hglobal); + if (buffer is null) throw new Win32Exception(); + try + { + int size = checked((int)PInvokeCore.GlobalSize(hglobal)); + if (size == 0) throw new Win32Exception(); + ReadOnlySpan bytes = new((byte*)buffer, size); + + // Trim trailing null bytes (optional for registered formats) + while (bytes.Length > 0 && bytes[^1] == 0) + bytes = bytes[..^1]; + + return bytes.IsEmpty ? string.Empty : encoding.GetString(bytes); + } + finally { PInvokeCore.GlobalUnlock(hglobal); } + } + private static string ReadUtf8StringFromHGLOBAL(HGLOBAL hglobal) { void* buffer = PInvokeCore.GlobalLock(hglobal); From 02c123f7c7d69fb606daa58a037a13c7a2173928 Mon Sep 17 00:00:00 2001 From: "Simon Zhao (BEYONDSOFT CONSULTING INC)" Date: Fri, 10 Apr 2026 16:22:28 +0800 Subject: [PATCH 2/3] Add test case --- .../Ole/Composition.NativeToManagedAdapter.cs | 19 ++++++++++++++++--- .../Ole/NativeToManagedAdapterTests.cs | 13 +++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/Composition.NativeToManagedAdapter.cs b/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/Composition.NativeToManagedAdapter.cs index 98ae1be7087..bd35f72aa74 100644 --- a/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/Composition.NativeToManagedAdapter.cs +++ b/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/Composition.NativeToManagedAdapter.cs @@ -245,20 +245,33 @@ private static unsafe string ReadStringFromHGLOBAL(HGLOBAL hglobal, bool unicode private static unsafe string ReadRegisteredFormatStringFromHGLOBAL(HGLOBAL hglobal, Encoding encoding) { void* buffer = PInvokeCore.GlobalLock(hglobal); - if (buffer is null) throw new Win32Exception(); + if (buffer is null) + { + throw new Win32Exception(); + } + try { int size = checked((int)PInvokeCore.GlobalSize(hglobal)); - if (size == 0) throw new Win32Exception(); + if (size == 0) + { + throw new Win32Exception(); + } + ReadOnlySpan bytes = new((byte*)buffer, size); // Trim trailing null bytes (optional for registered formats) while (bytes.Length > 0 && bytes[^1] == 0) + { bytes = bytes[..^1]; + } return bytes.IsEmpty ? string.Empty : encoding.GetString(bytes); } - finally { PInvokeCore.GlobalUnlock(hglobal); } + finally + { + PInvokeCore.GlobalUnlock(hglobal); + } } private static string ReadUtf8StringFromHGLOBAL(HGLOBAL hglobal) diff --git a/src/System.Private.Windows.Core/tests/System.Private.Windows.Core.Tests/System/Private/Windows/Ole/NativeToManagedAdapterTests.cs b/src/System.Private.Windows.Core/tests/System.Private.Windows.Core.Tests/System/Private/Windows/Ole/NativeToManagedAdapterTests.cs index 3c38cddcd41..2a6fde9468f 100644 --- a/src/System.Private.Windows.Core/tests/System.Private.Windows.Core.Tests/System/Private/Windows/Ole/NativeToManagedAdapterTests.cs +++ b/src/System.Private.Windows.Core/tests/System.Private.Windows.Core.Tests/System/Private/Windows/Ole/NativeToManagedAdapterTests.cs @@ -99,6 +99,19 @@ public void GetData_CustomType_BinaryFormattedData_AsSerializationRecord() data!.TypeName.AssemblyQualifiedName.Should().Be("System.Int32[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"); } + [Fact] + public void GetData_RtfWithoutNullTerminator_ReturnsRtfText() + { + const string rtf = "{\\rtf1\\ansi Test}"; + MemoryStream stream = new("{\\rtf1\\ansi Test}"u8.ToArray()); + using HGlobalNativeDataObject dataObject = new(stream, (ushort)DataFormats.GetOrAddFormat(DataFormatNames.Rtf).Id); + + var composition = Composition.Create(ComHelpers.GetComPointer(dataObject)); + object? data = composition.GetData(DataFormatNames.Rtf); + + data.Should().Be(rtf); + } + #if NET [Fact] public void GetData_CustomType_BinaryFormattedJson_AsSerializationRecord() From 0cac244dbd86f9ec2e63ab427cc7e63c36c69d3a Mon Sep 17 00:00:00 2001 From: "Simon Zhao (BEYONDSOFT CONSULTING INC)" Date: Fri, 10 Apr 2026 17:02:51 +0800 Subject: [PATCH 3/3] Handle feedback --- .../Ole/Composition.NativeToManagedAdapter.cs | 12 +++++------- .../Windows/Ole/NativeToManagedAdapterTests.cs | 5 ++++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/Composition.NativeToManagedAdapter.cs b/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/Composition.NativeToManagedAdapter.cs index bd35f72aa74..2483afb06ca 100644 --- a/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/Composition.NativeToManagedAdapter.cs +++ b/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/Composition.NativeToManagedAdapter.cs @@ -197,10 +197,6 @@ private static unsafe string ReadStringFromHGLOBAL(HGLOBAL hglobal, bool unicode // // CF_TEXT, CF_OEMTEXT, CF_UNICODETEXT, and CFSTR_FILENAME are supposed to have a null terminator. // If we cannot find one in the buffer, assume it is corrupted and return an empty string. - // - // Can't find the explicit docs for CF_RTF, but we've always treated it as null terminated. - // The RichText control itself null terminates but looks like it doesn't require it. - // Given our prior and "normal" behavior, we'll continue to expect a null terminator. try { @@ -260,10 +256,12 @@ private static unsafe string ReadRegisteredFormatStringFromHGLOBAL(HGLOBAL hglob ReadOnlySpan bytes = new((byte*)buffer, size); - // Trim trailing null bytes (optional for registered formats) - while (bytes.Length > 0 && bytes[^1] == 0) + // Registered format strings may be null-terminated, but the terminator is optional. + // If present, stop at the first null byte rather than decoding the entire allocation. + int nullIndex = bytes.IndexOf((byte)0); + if (nullIndex >= 0) { - bytes = bytes[..^1]; + bytes = bytes[..nullIndex]; } return bytes.IsEmpty ? string.Empty : encoding.GetString(bytes); diff --git a/src/System.Private.Windows.Core/tests/System.Private.Windows.Core.Tests/System/Private/Windows/Ole/NativeToManagedAdapterTests.cs b/src/System.Private.Windows.Core/tests/System.Private.Windows.Core.Tests/System/Private/Windows/Ole/NativeToManagedAdapterTests.cs index 2a6fde9468f..f917bf12932 100644 --- a/src/System.Private.Windows.Core/tests/System.Private.Windows.Core.Tests/System/Private/Windows/Ole/NativeToManagedAdapterTests.cs +++ b/src/System.Private.Windows.Core/tests/System.Private.Windows.Core.Tests/System/Private/Windows/Ole/NativeToManagedAdapterTests.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.Formats.Nrbf; using System.Private.Windows.BinaryFormat; + using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.System.Com; @@ -19,6 +20,8 @@ System.Private.Windows.Ole.TestFormat>; using DataFormats = System.Private.Windows.Ole.DataFormatsCore; +using System.Text; + namespace System.Private.Windows.Ole; public unsafe class NativeToManagedAdapterTests @@ -103,7 +106,7 @@ public void GetData_CustomType_BinaryFormattedData_AsSerializationRecord() public void GetData_RtfWithoutNullTerminator_ReturnsRtfText() { const string rtf = "{\\rtf1\\ansi Test}"; - MemoryStream stream = new("{\\rtf1\\ansi Test}"u8.ToArray()); + MemoryStream stream = new(Encoding.Default.GetBytes(rtf)); using HGlobalNativeDataObject dataObject = new(stream, (ushort)DataFormats.GetOrAddFormat(DataFormatNames.Rtf).Id); var composition = Composition.Create(ComHelpers.GetComPointer(dataObject));