4d54eabdac
General code-quality and robustness pass across the plugin: thread- safety on IPC state, resource-disposal cleanups, input validation, defensive null-checks and a few small UX glitches. Compliance docs (THIRD_PARTY_NOTICES, PRIVACY, COPYRIGHT) refreshed to v1.0.3. Highlights - ExtraChat IPC state synchronised across threads - ChatLogWindow autocomplete no longer leaks the unmanaged ImGuiListClipper allocation - ChatLogWindow + Popout style stack stays balanced when config toggles mid-frame - Retention sweep and privacy cleanup wait for the actual filter pass instead of the fire-and-forget Task that started it - Configuration.LatestVersion bumped to 13 to match the active migration path - GameFunctions placeholder buffer guarded against oversized replacement names - TellTarget.IsSet, ResolveTempInputChannel, InputPreview, IconUtil, Lender, Payloads, ExtraPayload all hardened against null / empty / EOF / cycle inputs - FontManager Lodestone download stays in scope for a follow-up (timeout + lazy init pending) - AutoTranslate replaced the msvcrt.dll memcmp P/Invoke with a managed Span comparison - Privacy cleanup worker thread marked IsBackground = true - Database cleanup now removes both legacy files in one click - Tell-target name redacted in the verbose debug log Compliance - THIRD_PARTY_NOTICES: last-reviewed bumped to v1.0.3, Pidgin 3.5.1, SQLitePCLRaw.lib.e_sqlite3 3.50.3 listed as direct dependency with CVE-2025-6965 / CVE-2025-7709 rationale - PRIVACY: last-reviewed bumped to v1.0.3, BetterTTV trigger wording clarified (list fetch at startup vs. on-demand image fetch) - COPYRIGHT: upstream attribution range widened Build: 0 warnings, 0 errors. No behavioural changes that would alter existing user configuration or stored chat history.
182 lines
5.5 KiB
C#
Executable File
182 lines
5.5 KiB
C#
Executable File
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
using SixLabors.ImageSharp;
|
|
using SixLabors.ImageSharp.PixelFormats;
|
|
|
|
namespace HellionChat.Util;
|
|
|
|
// From Kizer: https://github.com/Soreepeong/Dalamud/blob/feature/log-wordwrap/Dalamud/Interface/Spannables/Internal/GfdFileView.cs
|
|
public readonly unsafe ref struct GfdFileView
|
|
{
|
|
private readonly ReadOnlySpan<byte> Span;
|
|
private readonly bool DirectLookup;
|
|
|
|
/// <summary>Initializes a new instance of the <see cref="GfdFileView"/> struct.</summary>
|
|
/// <param name="span">The data.</param>
|
|
public GfdFileView(ReadOnlySpan<byte> span)
|
|
{
|
|
Span = span;
|
|
if (span.Length < sizeof(GfdHeader))
|
|
throw new InvalidDataException($"Not enough space for a {nameof(GfdHeader)}");
|
|
if (span.Length < sizeof(GfdHeader) + (Header.Count * sizeof(GfdEntry)))
|
|
throw new InvalidDataException($"Not enough space for all the {nameof(GfdEntry)}");
|
|
|
|
var entries = Entries;
|
|
DirectLookup = true;
|
|
for (var i = 0; i < entries.Length && DirectLookup; i++)
|
|
DirectLookup &= i + 1 == entries[i].Id;
|
|
}
|
|
|
|
/// <summary>Gets the header.</summary>
|
|
private ref readonly GfdHeader Header => ref MemoryMarshal.AsRef<GfdHeader>(Span);
|
|
|
|
/// <summary>Gets the entries.</summary>
|
|
private ReadOnlySpan<GfdEntry> Entries => MemoryMarshal.Cast<byte, GfdEntry>(Span[sizeof(GfdHeader)..]);
|
|
|
|
/// <summary>Attempts to get an entry.</summary>
|
|
/// <param name="iconId">The icon ID.</param>
|
|
/// <param name="entry">The entry.</param>
|
|
/// <param name="followRedirect">Whether to follow redirects.</param>
|
|
/// <returns><c>true</c> if found.</returns>
|
|
public bool TryGetEntry(uint iconId, out GfdEntry entry, bool followRedirect = true)
|
|
{
|
|
if (iconId == 0)
|
|
{
|
|
entry = default;
|
|
return false;
|
|
}
|
|
|
|
var entries = Entries;
|
|
if (DirectLookup)
|
|
{
|
|
// Resolve redirects on the direct-lookup path too — the binary-search
|
|
// path follows them, and skipping them here was inconsistent for
|
|
// contiguous ID sets.
|
|
var visited = 0;
|
|
while (iconId <= entries.Length)
|
|
{
|
|
entry = entries[(int)(iconId - 1)];
|
|
if (followRedirect && entry.Redirect != 0 && entry.Redirect != iconId)
|
|
{
|
|
if (++visited > entries.Length)
|
|
break; // cycle guard
|
|
iconId = entry.Redirect;
|
|
continue;
|
|
}
|
|
|
|
return !entry.IsEmpty;
|
|
}
|
|
|
|
entry = default;
|
|
return false;
|
|
}
|
|
|
|
if (entries.Length == 0)
|
|
{
|
|
entry = default;
|
|
return false;
|
|
}
|
|
|
|
var lo = 0;
|
|
var hi = entries.Length - 1;
|
|
while (lo <= hi)
|
|
{
|
|
var i = lo + ((hi - lo) >> 1);
|
|
if (entries[i].Id == iconId)
|
|
{
|
|
if (followRedirect && entries[i].Redirect != 0)
|
|
{
|
|
iconId = entries[i].Redirect;
|
|
lo = 0;
|
|
hi = entries.Length - 1;
|
|
continue;
|
|
}
|
|
|
|
entry = entries[i];
|
|
return !entry.IsEmpty;
|
|
}
|
|
|
|
if (entries[i].Id < iconId)
|
|
lo = i + 1;
|
|
else
|
|
hi = i - 1;
|
|
}
|
|
|
|
entry = default;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>Header of a .gfd file.</summary>
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public struct GfdHeader
|
|
{
|
|
/// <summary>Signature: "gftd0100".</summary>
|
|
public fixed byte Signature[8];
|
|
|
|
/// <summary>Number of entries.</summary>
|
|
public int Count;
|
|
|
|
/// <summary>Unused/unknown.</summary>
|
|
public fixed byte Padding[4];
|
|
}
|
|
|
|
/// <summary>An entry of a .gfd file.</summary>
|
|
[StructLayout(LayoutKind.Sequential, Size = 0x10)]
|
|
public struct GfdEntry
|
|
{
|
|
/// <summary>ID of the entry.</summary>
|
|
public ushort Id;
|
|
|
|
/// <summary>The left offset of the entry.</summary>
|
|
public ushort Left;
|
|
|
|
/// <summary>The top offset of the entry.</summary>
|
|
public ushort Top;
|
|
|
|
/// <summary>The width of the entry.</summary>
|
|
public ushort Width;
|
|
|
|
/// <summary>The height of the entry.</summary>
|
|
public ushort Height;
|
|
|
|
/// <summary>Unknown/unused.</summary>
|
|
public ushort Unk0A;
|
|
|
|
/// <summary>The redirected entry, maybe.</summary>
|
|
public ushort Redirect;
|
|
|
|
/// <summary>Unknown/unused.</summary>
|
|
public ushort Unk0E;
|
|
|
|
/// <summary>Gets a value indicating whether this entry is effectively empty.</summary>
|
|
public bool IsEmpty => Width == 0 || Height == 0;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
internal static class IconUtil
|
|
{
|
|
private static byte[]? GfdFile;
|
|
public static GfdFileView GfdFileView
|
|
{
|
|
get
|
|
{
|
|
if (GfdFile is null)
|
|
{
|
|
var file = Plugin.DataManager.GetFile("common/font/gfdata.gfd")
|
|
?? throw new FileNotFoundException("Failed to load common/font/gfdata.gfd from the game data.");
|
|
GfdFile = file.Data;
|
|
}
|
|
return new GfdFileView(GfdFile);
|
|
}
|
|
}
|
|
|
|
public static byte[] ImageToRaw(this Image<Rgba32> image)
|
|
{
|
|
var data = new byte[4 * image.Width * image.Height];
|
|
image.CopyPixelDataTo(data);
|
|
return data;
|
|
}
|
|
}
|