Reimplement clipboard text normalizer to use the correct buffers

This commit is contained in:
Soreepeong 2023-12-08 23:28:12 +09:00
parent 0c3ebd4b5b
commit 8967174cd9
2 changed files with 193 additions and 41 deletions

View file

@ -1,4 +1,9 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Dalamud.Interface.Utility;
using ImGuiNET;
namespace Dalamud.Interface.Internal;
@ -21,60 +26,209 @@ namespace Dalamud.Interface.Internal;
/// works for both ImGui and XIV.
/// </para>
/// </remarks>
internal static class ImGuiClipboardConfig
[ServiceManager.EarlyLoadedService]
internal sealed unsafe class ImGuiClipboardConfig : IServiceType, IDisposable
{
private delegate void SetClipboardTextDelegate(IntPtr userData, string text);
private delegate string GetClipboardTextDelegate();
private readonly nint clipboardUserDataOriginal;
private readonly delegate* unmanaged<nint, byte*, void> setTextOriginal;
private readonly delegate* unmanaged<nint, byte*> getTextOriginal;
private static SetClipboardTextDelegate? _setTextOriginal = null;
private static GetClipboardTextDelegate? _getTextOriginal = null;
[ServiceManager.ServiceConstructor]
private ImGuiClipboardConfig(InterfaceManager.InterfaceManagerWithScene imws)
{
// Effectively waiting for ImGui to become available.
_ = imws;
Debug.Assert(ImGuiHelpers.IsImGuiInitialized, "IMWS initialized but IsImGuiInitialized is false?");
// These must exist as variables to prevent them from being GC'd
private static SetClipboardTextDelegate? _setText = null;
private static GetClipboardTextDelegate? _getText = null;
var io = ImGui.GetIO();
this.setTextOriginal = (delegate* unmanaged<nint, byte*, void>)io.SetClipboardTextFn;
this.getTextOriginal = (delegate* unmanaged<nint, byte*>)io.GetClipboardTextFn;
this.clipboardUserDataOriginal = io.ClipboardUserData;
io.SetClipboardTextFn = (nint)(delegate* unmanaged<nint, byte*, void>)(&StaticSetClipboardTextImpl);
io.GetClipboardTextFn = (nint)(delegate* unmanaged<nint, byte*>)&StaticGetClipboardTextImpl;
io.ClipboardUserData = GCHandle.ToIntPtr(GCHandle.Alloc(this));
return;
public static void Apply()
[UnmanagedCallersOnly]
static void StaticSetClipboardTextImpl(nint userData, byte* text) =>
((ImGuiClipboardConfig)GCHandle.FromIntPtr(userData).Target)!.SetClipboardTextImpl(text);
[UnmanagedCallersOnly]
static byte* StaticGetClipboardTextImpl(nint userData) =>
((ImGuiClipboardConfig)GCHandle.FromIntPtr(userData).Target)!.GetClipboardTextImpl();
}
/// <summary>
/// Finalizes an instance of the <see cref="ImGuiClipboardConfig"/> class.
/// </summary>
~ImGuiClipboardConfig() => this.ReleaseUnmanagedResources();
[SuppressMessage("ReSharper", "AssignNullToNotNullAttribute", Justification = "If it's null, it's crashworthy")]
private static ImVectorWrapper<byte> ImGuiCurrentContextClipboardHandlerData =>
new((ImVector*)(ImGui.GetCurrentContext() + 0x5520));
/// <inheritdoc/>
public void Dispose()
{
this.ReleaseUnmanagedResources();
GC.SuppressFinalize(this);
}
private void ReleaseUnmanagedResources()
{
var io = ImGui.GetIO();
if (_setTextOriginal == null)
{
_setTextOriginal =
Marshal.GetDelegateForFunctionPointer<SetClipboardTextDelegate>(io.SetClipboardTextFn);
}
if (io.ClipboardUserData == default)
return;
if (_getTextOriginal == null)
{
_getTextOriginal =
Marshal.GetDelegateForFunctionPointer<GetClipboardTextDelegate>(io.GetClipboardTextFn);
}
_setText = new SetClipboardTextDelegate(SetClipboardText);
_getText = new GetClipboardTextDelegate(GetClipboardText);
io.SetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_setText);
io.GetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_getText);
GCHandle.FromIntPtr(io.ClipboardUserData).Free();
io.SetClipboardTextFn = (nint)this.setTextOriginal;
io.GetClipboardTextFn = (nint)this.getTextOriginal;
io.ClipboardUserData = this.clipboardUserDataOriginal;
}
public static void Unapply()
private void SetClipboardTextImpl(byte* text)
{
var io = ImGui.GetIO();
if (_setTextOriginal != null)
{
io.SetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_setTextOriginal);
}
if (_getTextOriginal != null)
{
io.GetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_getTextOriginal);
}
var buffer = ImGuiCurrentContextClipboardHandlerData;
Utf8Utils.SetFromNullTerminatedBytes(ref buffer, text);
Utf8Utils.Normalize(ref buffer);
Utf8Utils.AddNullTerminatorIfMissing(ref buffer);
this.setTextOriginal(this.clipboardUserDataOriginal, buffer.Data);
}
private static void SetClipboardText(IntPtr userData, string text)
private byte* GetClipboardTextImpl()
{
_setTextOriginal!(userData, text.ReplaceLineEndings("\r\n"));
_ = this.getTextOriginal(this.clipboardUserDataOriginal);
var buffer = ImGuiCurrentContextClipboardHandlerData;
Utf8Utils.TrimNullTerminator(ref buffer);
Utf8Utils.Normalize(ref buffer);
Utf8Utils.AddNullTerminatorIfMissing(ref buffer);
return buffer.Data;
}
private static string GetClipboardText()
private static class Utf8Utils
{
return _getTextOriginal!().ReplaceLineEndings("\r\n");
/// <summary>
/// Sets <paramref name="buf"/> from <paramref name="psz"/>, a null terminated UTF-8 string.
/// </summary>
/// <param name="buf">The target buffer. It will not contain a null terminator.</param>
/// <param name="psz">The pointer to the null-terminated UTF-8 string.</param>
public static void SetFromNullTerminatedBytes(ref ImVectorWrapper<byte> buf, byte* psz)
{
var len = 0;
while (psz[len] != 0)
len++;
buf.Clear();
buf.AddRange(new Span<byte>(psz, len));
}
/// <summary>
/// Removes the null terminator.
/// </summary>
/// <param name="buf">The UTF-8 string buffer.</param>
public static void TrimNullTerminator(ref ImVectorWrapper<byte> buf)
{
while (buf.Length > 0 && buf[^1] == 0)
buf.LengthUnsafe--;
}
/// <summary>
/// Adds a null terminator to the buffer.
/// </summary>
/// <param name="buf">The buffer.</param>
public static void AddNullTerminatorIfMissing(ref ImVectorWrapper<byte> buf)
{
if (buf.Length > 0 && buf[^1] == 0)
return;
buf.Add(0);
}
/// <summary>
/// Counts the number of bytes for the UTF-8 character.
/// </summary>
/// <param name="b">The bytes.</param>
/// <param name="avail">Available number of bytes.</param>
/// <returns>Number of bytes taken, or -1 if the byte was invalid.</returns>
public static int CountBytes(byte* b, int avail)
{
if (avail <= 0)
return 0;
if ((b[0] & 0x80) == 0)
return 1;
if ((b[0] & 0xE0) == 0xC0 && avail >= 2)
return (b[1] & 0xC0) == 0x80 ? 2 : -1;
if ((b[0] & 0xF0) == 0xE0 && avail >= 3)
return (b[1] & 0xC0) == 0x80 && (b[2] & 0xC0) == 0x80 ? 3 : -1;
if ((b[0] & 0xF8) == 0xF0 && avail >= 4)
return (b[1] & 0xC0) == 0x80 && (b[2] & 0xC0) == 0x80 && (b[3] & 0xC0) == 0x80 ? 4 : -1;
return -1;
}
/// <summary>
/// Gets the codepoint.
/// </summary>
/// <param name="b">The bytes.</param>
/// <param name="cb">The result from <see cref="CountBytes"/>.</param>
/// <returns>The codepoint, or \xFFFD replacement character if failed.</returns>
public static int GetCodepoint(byte* b, int cb) => cb switch
{
1 => b[0],
2 => ((b[0] & 0x8F) << 6) | (b[1] & 0x3F),
3 => ((b[0] & 0x0F) << 12) | ((b[1] & 0x3F) << 6) | (b[2] & 0x3F),
4 => ((b[0] & 0x0F) << 18) | ((b[1] & 0x3F) << 12) | ((b[2] & 0x3F) << 6) | (b[3] & 0x3F),
_ => 0xFFFD,
};
/// <summary>
/// Normalize the given text for our use case.
/// </summary>
/// <param name="buf">The buffer.</param>
public static void Normalize(ref ImVectorWrapper<byte> buf)
{
for (var i = 0; i < buf.Length;)
{
// Already correct?
if (buf[i] is 0x0D && buf[i + 1] is 0x0A)
{
i += 2;
continue;
}
var cb = CountBytes(buf.Data + i, buf.Length - i);
var currInt = GetCodepoint(buf.Data + i, cb);
switch (currInt)
{
case 0xFFFF: // Simply invalid
case > char.MaxValue: // ImWchar is same size with char; does not support
case >= 0xD800 and <= 0xDBFF: // UTF-16 surrogate; does not support
// Replace with \uFFFD in UTF-8: EF BF BD
buf[i++] = 0xEF;
buf.Insert(i++, 0xBF);
buf.Insert(i++, 0xBD);
break;
// See String.Manipulation.cs: IndexOfNewlineChar.
case '\r': // CR; Carriage Return
case '\n': // LF; Line Feed
case '\f': // FF; Form Feed
buf[i++] = 0x0D;
buf.Insert(i++, 0x0A);
break;
case '\u0085': // NEL; Next Line
case '\u2028': // LS; Line Separator
case '\u2029': // PS; Paragraph Separator
buf[i++] = 0x0D;
buf[i++] = 0x0A;
break;
default:
// Not a newline char.
i += cb;
break;
}
}
}
}
}