Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b7f2c40e6 |
@@ -4,7 +4,7 @@
|
|||||||
0.1.0 is our bootstrap release; the underlying Chat 2 base is
|
0.1.0 is our bootstrap release; the underlying Chat 2 base is
|
||||||
called out in the yaml changelog so users can see what it
|
called out in the yaml changelog so users can see what it
|
||||||
derives from. -->
|
derives from. -->
|
||||||
<Version>0.5.3</Version>
|
<Version>0.5.4</Version>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<!-- HellionChat fork: assembly is renamed so Dalamud uses
|
<!-- HellionChat fork: assembly is renamed so Dalamud uses
|
||||||
pluginConfigs/HellionChat instead of pluginConfigs/ChatTwo,
|
pluginConfigs/HellionChat instead of pluginConfigs/ChatTwo,
|
||||||
|
|||||||
+40
-323
@@ -44,340 +44,57 @@ tags:
|
|||||||
- Replacement
|
- Replacement
|
||||||
- Privacy
|
- Privacy
|
||||||
changelog: |-
|
changelog: |-
|
||||||
**Hellion Chat 0.5.3 — Pointer arithmetic hardening**
|
**Hellion Chat 0.5.4 — WrapText hardening**
|
||||||
|
|
||||||
Single hardening fix on top of v0.5.2.
|
Replaces the unsafe pointer-arithmetic in ImGuiUtil.WrapText with
|
||||||
|
Span- and index-based control flow. Closes the persistent CodeQL
|
||||||
|
Critical alert "unvalidated local pointer arithmetic" that kept
|
||||||
|
re-firing on every shape of the previous fix.
|
||||||
|
|
||||||
Security:
|
Hardening:
|
||||||
|
|
||||||
- Closed CodeQL Critical alert "unvalidated local pointer
|
- WrapText now allocates a buffer sized by Encoding.UTF8.GetMaxByteCount
|
||||||
arithmetic" in ImGuiUtil.WrapText. The earlier v0.5.2 fix
|
via ArrayPool, validates the actual encoded length against that
|
||||||
handled the empty-input edge case but the rule re-fired on the
|
ceiling, and threads the rest of the algorithm through int offsets
|
||||||
pointer arithmetic itself because Encoding.GetBytes is virtual
|
instead of raw byte pointers
|
||||||
on the base Encoding class and CodeQL therefore tracks its
|
- Pointer arithmetic only happens inside two small private helpers
|
||||||
return as untrusted input. Now compute the expected byte count
|
(CalcWordWrap and DrawText) that take the pinned base pointer plus
|
||||||
via GetByteCount on the same encoder and bail out if a swapped
|
int offsets sourced from the plugin's own logic, not from any
|
||||||
Encoding ever returned a buffer of the wrong length. Real
|
virtual-method return
|
||||||
consistency check, not a dead defensive guard.
|
- Added a 16 KiB upper bound on the buffer rent to prevent a
|
||||||
|
pathological input from triggering an unbounded ArrayPool allocation
|
||||||
|
|
||||||
No new features, no migration, configuration version stays at 10.
|
No user-visible behaviour change. Word-wrap output is byte-identical
|
||||||
|
to v0.5.3.
|
||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
|
|
||||||
|
**Hellion Chat 0.5.3 — Pointer arithmetic hardening**
|
||||||
|
|
||||||
|
Closed CodeQL Critical alert in ImGuiUtil.WrapText by validating the
|
||||||
|
encoded byte buffer length via GetByteCount before pointer
|
||||||
|
arithmetic. Single-fix patch on top of v0.5.2.
|
||||||
|
|
||||||
**Hellion Chat 0.5.2 — Bugfix patch**
|
**Hellion Chat 0.5.2 — Bugfix patch**
|
||||||
|
|
||||||
Three corrections to the v0.5.1 surface plus two security findings
|
Auto-Tell-Tabs history-separator landed below the live tell instead
|
||||||
closed by the new manual-build CodeQL workflow. No new features, no
|
of above (preload now excludes the trigger message). Plugin icon
|
||||||
migration, configuration version stays at 10.
|
packaging fixed by removing a stale DalamudPackager.targets override
|
||||||
|
that conflicted with the SDK 15 default. Default config aligned to
|
||||||
Bug fixes:
|
the maintainer's daily driver: HellionThemeWindowOpacity 0.5,
|
||||||
|
Use24HourClock true, Gruppe tab no longer auto-routes /party. Two
|
||||||
- Auto-Tell-Tabs: the "earlier conversations" separator no longer
|
earlier CodeQL findings closed (workflow permissions, empty-input
|
||||||
lands below the live tell. The triggering message was already
|
pointer arithmetic).
|
||||||
persisted in the store by the time the spawn handler fired, so
|
|
||||||
it appeared as the youngest historic message. The preload now
|
|
||||||
excludes the live tell explicitly and pulls one extra row so the
|
|
||||||
user does not lose a slot to the exclusion.
|
|
||||||
- General/Aussehen: HellionThemeWindowOpacity ships at 0.5 so a
|
|
||||||
fresh install lands at the more glass-like default. Existing
|
|
||||||
users keep their saved value.
|
|
||||||
- General/Allgemein: Use24HourClock ships at true so a German /
|
|
||||||
European install starts on 24h time without a manual flip.
|
|
||||||
- Tabs/Gruppe: the default Gruppe preset no longer auto-routes
|
|
||||||
/party into the tab. The tab still collects /party, /alliance,
|
|
||||||
/pvpteam together as a read surface but does not steal the
|
|
||||||
input focus when you wanted /alliance.
|
|
||||||
|
|
||||||
Security:
|
|
||||||
|
|
||||||
- Closed CodeQL Critical alert "unvalidated local pointer
|
|
||||||
arithmetic" in ImGuiUtil.WrapText: empty splits between
|
|
||||||
consecutive newlines produced a zero-length byte array whose
|
|
||||||
fixed pointer collapsed onto its end pointer. Bail before the
|
|
||||||
fixed block when the slice is empty.
|
|
||||||
- Closed CodeQL Medium alert "workflow does not contain
|
|
||||||
permissions" by pinning the build workflow to contents: read.
|
|
||||||
|
|
||||||
Documentation: README now carries Build, CodeQL, License, Latest
|
|
||||||
Release, Dalamud API, .NET and FFXIV badges. License detection
|
|
||||||
picks up EUPL-1.2 correctly via a separated COPYRIGHT file. Added
|
|
||||||
NOTICE.md and UPSTREAM_SYNC.md after leaving the GitHub fork
|
|
||||||
network.
|
|
||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
|
||||||
|
|
||||||
**Hellion Chat 0.5.1 — Backlog Sweep**
|
**Hellion Chat 0.5.1 — Backlog Sweep**
|
||||||
|
|
||||||
Pure hardening and polish. No new features. Eight backlog items
|
Pure hardening and polish. Eight backlog items from the v0.5.0
|
||||||
from the v0.5.0 codebase review collected into one patch:
|
codebase review collected into one patch: cleanup-preview-stale
|
||||||
|
detection, greeted-tab dim background, Performance HelpMarker
|
||||||
|
consistency, Tabs/Database tab names from HellionStrings,
|
||||||
|
FontChooser framework-thread marshalling, async-void on
|
||||||
|
EmoteCache.LoadData, parameterised SQL via BindIntList helper.
|
||||||
|
|
||||||
- Cleanup preview now flags itself as out-of-date when the user
|
---
|
||||||
edits the whitelist after the last refresh, and the refresh
|
|
||||||
button is visually emphasised in that state
|
|
||||||
- Greeted Auto-Tell-Tabs now also dim their selection and hover
|
|
||||||
backgrounds in the sidebar, not just the text
|
|
||||||
- Performance section in the General tab moves to the standard
|
|
||||||
HelpMarker tooltip pattern instead of a wall-of-text description
|
|
||||||
- Tabs and Database settings tabs pull their display name from
|
|
||||||
HellionStrings instead of the upstream Language bundle, so all
|
|
||||||
eight tabs share one i18n source
|
|
||||||
- FontChooser results are now marshalled onto the framework thread
|
|
||||||
via Plugin.Framework.Run instead of being written to settings
|
|
||||||
state directly from the threadpool
|
|
||||||
- EmoteCache.LoadData drops async void and the four CS8618 build
|
|
||||||
warnings the build has been carrying since v0.4.0
|
|
||||||
- All MessageStore SQL paths that fed dynamic value lists into
|
|
||||||
interpolated SQL now use named parameter bindings via a new
|
|
||||||
BindIntList helper. Same behaviour, defence against future
|
|
||||||
user-input regressions
|
|
||||||
|
|
||||||
Configuration version is unchanged at 10. No migration. Existing
|
Earlier history at https://github.com/JonKazama-Hellion/HellionChat/releases.
|
||||||
installs upgrade silently.
|
|
||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
|
||||||
|
|
||||||
**Hellion Chat 0.5.0 — Settings UX polish**
|
|
||||||
|
|
||||||
The settings window has been pulled apart and rebuilt around eight
|
|
||||||
themed tabs instead of the twelve organic ones it grew into.
|
|
||||||
Settings now sit where they belong and the wall-of-text descriptions
|
|
||||||
have been replaced with hover help markers across every section.
|
|
||||||
|
|
||||||
What changed in this release:
|
|
||||||
|
|
||||||
- Twelve tabs collapsed into eight: General, Appearance, Window,
|
|
||||||
Chat, Tabs, Privacy, Database and Information
|
|
||||||
- Theme and font controls moved out of the Privacy tab into
|
|
||||||
Appearance where they belong
|
|
||||||
- Auto-Tell-Tabs settings, message preview and emote controls now
|
|
||||||
live under one Chat tab with collapsible sections
|
|
||||||
- About and Changelog merged into a single Information tab
|
|
||||||
- Disabled settings keep their tooltip help marker visible so you
|
|
||||||
can still read why an option is greyed out
|
|
||||||
- Section headings start collapsed by default, the same pattern
|
|
||||||
used for the Auto-Tell-Tabs preload section in 0.4.0
|
|
||||||
|
|
||||||
Configuration version bumps from 9 to 10 as a wipe migration. The
|
|
||||||
old config file is copied to HellionChat.json.pre-v10-backup before
|
|
||||||
the new defaults are written, so you can restore your previous
|
|
||||||
setup by hand if anything looks off. A one-shot notification on
|
|
||||||
first start explains the reset.
|
|
||||||
|
|
||||||
No changes to message storage, retention sweep, the privacy filter
|
|
||||||
or the export pipeline. Tabs and chat history are untouched by the
|
|
||||||
migration.
|
|
||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
|
||||||
|
|
||||||
**Hellion Chat 0.4.0 — Auto-Tell-Tabs**
|
|
||||||
|
|
||||||
Auto-Tell-Tabs lets you turn each /tell into a session-only tab
|
|
||||||
dedicated to that conversation partner. The original use case is
|
|
||||||
the FFXIV club greeter who has to track 5–15 parallel "hi, welcome"
|
|
||||||
exchanges; everyone else can disable the feature in one click and
|
|
||||||
go back to a single Tell Exclusive tab.
|
|
||||||
|
|
||||||
What lands in this release:
|
|
||||||
|
|
||||||
- Auto-spawn temp tab "Name@World" on /tell (incoming and outgoing)
|
|
||||||
- Tab limit (default 15, range 1–50) with LRU drop that prefers
|
|
||||||
greeted tabs first, then sorts by last activity
|
|
||||||
- History preload from the local message store (default 20 tells,
|
|
||||||
range 0–100) with a "— Earlier conversations —" separator above
|
|
||||||
the live tell that triggered the spawn
|
|
||||||
- Optional "mark as greeted" toggle button (off by default,
|
|
||||||
greeter-specific) that dims the tab name and lets you flip the
|
|
||||||
status
|
|
||||||
- Section header "Active Tells (n)" or compact-mode separator in
|
|
||||||
the sidebar between persistent tabs and the temp tabs
|
|
||||||
- Settings UI under Chat (toggle / limit / compact / greeted-toggle)
|
|
||||||
and Privacy (history preload count), with hover-tooltip help
|
|
||||||
markers replacing the previous wall-of-text descriptions for the
|
|
||||||
new sections
|
|
||||||
- Save and load filters strip temp tabs from the on-disk config so
|
|
||||||
a crash or a sidebar-mode toggle never persists or wipes them
|
|
||||||
|
|
||||||
Compatibility note: if XIV Messanger or another plugin is
|
|
||||||
suppressing direct messages, disable its "Suppress DMs" option so
|
|
||||||
Hellion Chat can receive tells and open the auto tabs.
|
|
||||||
|
|
||||||
Configuration version bumps from 8 to 9. Existing users get a one-
|
|
||||||
shot notification on the first start, defaults are seeded by
|
|
||||||
property initializers, persistent tabs are untouched.
|
|
||||||
|
|
||||||
The vertical sidebar tab view becomes the default for fresh
|
|
||||||
installs; existing users keep their saved preference.
|
|
||||||
|
|
||||||
Inspired by the per-sender tab pattern in XIV InstantMessenger
|
|
||||||
(Limiana, AGPL-3.0). No code was ported across the licence
|
|
||||||
boundary; only the architectural concept influenced this design.
|
|
||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
|
||||||
|
|
||||||
**Hellion Chat 0.3.1 — Upstream emote regression fix**
|
|
||||||
|
|
||||||
Cherry-picks Infi's upstream commit ff899ff "Fix a regression
|
|
||||||
from API 15 updates" which changes the BetterTTV emote DTOs
|
|
||||||
(Emote and Top100) from public fields to public properties.
|
|
||||||
System.Text.Json under the API 15 toolchain only honours the
|
|
||||||
[JsonPropertyName] attribute on properties, so the previous
|
|
||||||
field-based version deserialised every fetched emote into empty
|
|
||||||
default values. Result: BetterTTV emotes were silently broken
|
|
||||||
on fresh installs. The fix is six lines and applies cleanly on
|
|
||||||
top of our defensive null-check from earlier; the EmoteCache
|
|
||||||
path-traversal hardening from 0.3.0 stays as it is.
|
|
||||||
|
|
||||||
Authorship of the fix is preserved with git cherry-pick -x, so
|
|
||||||
Infi shows up as the author on the commit. Thanks to him for
|
|
||||||
catching it in the upstream codebase.
|
|
||||||
|
|
||||||
**Hellion Chat 0.3.0 — Audit hardening, brand sweep and rebrand of slash commands**
|
|
||||||
|
|
||||||
This release closes the remaining audit follow-ups from the
|
|
||||||
0.2.0 cleanup and finishes turning Hellion Chat into a properly
|
|
||||||
branded fork rather than a Chat 2 with a different name.
|
|
||||||
|
|
||||||
Slash commands have been renamed across the board so they no
|
|
||||||
longer collide with the upstream plugin and tell you which
|
|
||||||
plugin owns them at a glance:
|
|
||||||
|
|
||||||
- /chat2 becomes /hellion
|
|
||||||
- /chat2Viewer becomes /hellionView
|
|
||||||
- /clearlog2 becomes /clearhellion
|
|
||||||
- /chat2Debugger becomes /hellionDebugger (internal)
|
|
||||||
- /chat2SeString becomes /hellionSeString (internal)
|
|
||||||
|
|
||||||
This is a breaking change for anyone with macros bound to the
|
|
||||||
old command names. The upstream Chat 2 commands keep working
|
|
||||||
if you also have that plugin installed.
|
|
||||||
|
|
||||||
Privacy and storage hardening based on the post-0.2.0 audit:
|
|
||||||
|
|
||||||
- Privacy filter master switch now states explicitly that the
|
|
||||||
filter only governs storage, not the live chat log
|
|
||||||
- Emote cache refuses to write outside its own directory if a
|
|
||||||
third-party API ever returns a path that escapes
|
|
||||||
- Retention sweep is serialised so the 24h auto-sweep and the
|
|
||||||
manual button cannot launch in parallel and race for the
|
|
||||||
SQLite connection
|
|
||||||
- DbViewer paging uses an int constant and the matching SQL
|
|
||||||
parameter name (the upstream code passed a float and a name
|
|
||||||
without the parameter prefix; both worked in practice but
|
|
||||||
were inconsistent)
|
|
||||||
|
|
||||||
Visual identity now matches the Hellion Online Media website:
|
|
||||||
|
|
||||||
- Theme palette switched to Arctic Cyan plus Ember Orange,
|
|
||||||
matching the website's BRANDING.md tokens
|
|
||||||
- Active tabs and window title bars use a brand-color-dark teal
|
|
||||||
variation as identity colour, replacing the previous slate
|
|
||||||
violet that did not appear in the brand
|
|
||||||
- Resize grips and scrollbar grabs picked up Ember Orange
|
|
||||||
instead of industrial amber on hover and active states
|
|
||||||
|
|
||||||
About tab rewritten and properly localised:
|
|
||||||
|
|
||||||
- New "Why this fork exists" block sets out the mission in
|
|
||||||
neutral terms, framing Chat 2's full-history default as the
|
|
||||||
right one for most users while explaining the narrower
|
|
||||||
default footprint this fork chose
|
|
||||||
- All Hellion-specific About copy now lives in HellionStrings
|
|
||||||
in EN and DE, so German users see the Hellion sections in
|
|
||||||
German rather than the upstream English fallback
|
|
||||||
- Webinterface absence is described as a focus mismatch
|
|
||||||
(different use case, substantial rebuild) rather than as
|
|
||||||
a security issue with the upstream code
|
|
||||||
- Translator list at the bottom of the About tab is reachable
|
|
||||||
again on smaller settings windows
|
|
||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
|
||||||
|
|
||||||
**Hellion Chat 0.2.0 — Webinterface removed**
|
|
||||||
|
|
||||||
The upstream webinterface has been removed in its entirety. It
|
|
||||||
serves a different use case from the smaller default footprint
|
|
||||||
this fork is built around, namely remote access to chat from a
|
|
||||||
second device. Aligning it with the data minimisation defaults
|
|
||||||
Hellion Chat ships with would have meant a substantial rebuild.
|
|
||||||
Removing it was the cleaner path for this particular fork.
|
|
||||||
|
|
||||||
What changed in this release:
|
|
||||||
|
|
||||||
- Settings tab "Webinterface" is gone, the corresponding
|
|
||||||
Configuration fields (WebinterfaceEnabled / AutoStart / Password /
|
|
||||||
Port / AuthStore / MaxLinesToSend) are dropped and stale entries
|
|
||||||
fall out of the JSON on the next save automatically
|
|
||||||
- The whole ChatTwo/Http tree, the bundled Svelte frontend in
|
|
||||||
websiteBuild.zip and the WebinterfaceUtil helper are deleted
|
|
||||||
- Watson.Lite (the HTTP server) and Newtonsoft.Json (only used by
|
|
||||||
the webinterface JSON wire format) are removed from the
|
|
||||||
package references
|
|
||||||
- DbViewer's "Chat2 JSON Export" button is dropped because it
|
|
||||||
serialised the database into the webinterface message protocol;
|
|
||||||
the Privacy tab's MessageExporter (Markdown, JSON, CSV with
|
|
||||||
channel and date filters) covers the same ground without the
|
|
||||||
proprietary shape
|
|
||||||
- About tab notes the absence so users coming from Chat 2 do not
|
|
||||||
look for it
|
|
||||||
- Configuration version bumps from 7 to 8 with a one-shot
|
|
||||||
notification (EN + DE)
|
|
||||||
|
|
||||||
No changes to the privacy filter, retention sweep, first-run wizard
|
|
||||||
or export pipeline. Existing chat history is preserved.
|
|
||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
|
||||||
|
|
||||||
**Hellion Chat 0.1.2 — About tab rebrand, DBViewer polish**
|
|
||||||
|
|
||||||
- About tab now shows Hellion-specific maintainer, license, EU/US/JP
|
|
||||||
disclaimer and SQUARE ENIX disclaimer instead of the inherited
|
|
||||||
Chat 2 contact info; original ChatTwo translator credits stay
|
|
||||||
visible under a clearly labelled upstream tree node
|
|
||||||
- Localization clarified: Hellion-specific German strings are
|
|
||||||
maintained by the fork maintainer, the Crowdin contributor list
|
|
||||||
only covers the inherited upstream strings
|
|
||||||
- Cherry-picked DBViewer UI improvements from upstream Chat 2
|
|
||||||
(auto-scroll-reset on page change, tooltips on date reset,
|
|
||||||
folder export, page arrows, localized export-running messages)
|
|
||||||
- README rewritten in the Hellion project style with a tech-stack
|
|
||||||
table, architecture tree, database column list, install guide,
|
|
||||||
upstream-sync workflow notes and project-status checklist
|
|
||||||
|
|
||||||
**Hellion Chat 0.1.1 — Packaging and migration fixes**
|
|
||||||
|
|
||||||
- Plugin icon now ships inside the bundle, so the Hellion logo
|
|
||||||
renders locally in the Dalamud plugin list once installed (the
|
|
||||||
previous release relied only on the remote IconUrl)
|
|
||||||
- Plugin icon downsampled from 1024×1024 to 256×256 to match the
|
|
||||||
rendered size; loads faster and caches better
|
|
||||||
- Migration from upstream Chat 2 is more robust: each file move is
|
|
||||||
wrapped individually, a locked SQLite database no longer aborts
|
|
||||||
the rest of the migration, and a warning notification fires when
|
|
||||||
any file is held open (with a hint to disable Chat 2 and restart
|
|
||||||
the game)
|
|
||||||
- README ships a step-by-step migration guide (fresh install versus
|
|
||||||
coming from Chat 2) and a troubleshooting section with manual
|
|
||||||
recovery commands for Linux and Windows
|
|
||||||
|
|
||||||
**Hellion Chat 0.1.0 — Initial fork release**
|
|
||||||
|
|
||||||
Privacy
|
|
||||||
- Channel whitelist filter in MessageStore.UpsertMessage with a
|
|
||||||
Privacy-First default (own conversations only)
|
|
||||||
- Per-channel retention with a 24-hour idempotent background sweep
|
|
||||||
- Retroactive cleanup with a Ctrl+Shift confirm and VACUUM
|
|
||||||
- Export to Markdown / JSON / CSV via Dalamud's file dialog
|
|
||||||
|
|
||||||
Onboarding
|
|
||||||
- First-run wizard with three profiles: Privacy-First / Casual /
|
|
||||||
Full History
|
|
||||||
- Configuration migration that seeds defaults on update
|
|
||||||
- One-shot migration from upstream Chat 2's pluginConfigs layout
|
|
||||||
- Migrate3 idempotency recovery for half-migrated databases
|
|
||||||
|
|
||||||
Look & feel
|
|
||||||
- Localized UI (English and German) with live language switching
|
|
||||||
- Industrial HUD theme with cyan-teal action accents, slate-violet
|
|
||||||
tabs, amber active highlights and a window-opacity slider
|
|
||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
|
||||||
|
|||||||
+142
-96
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Buffers;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using ChatTwo.Code;
|
using ChatTwo.Code;
|
||||||
@@ -58,130 +59,175 @@ internal static class ImGuiUtil
|
|||||||
handler.Click(chunk, payload, button);
|
handler.Click(chunk, payload, button);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static unsafe void WrapText(string csText, Chunk chunk, PayloadHandler? handler, Vector4 defaultText, float lineWidth)
|
// Ceiling on the byte buffer for a single rendered line. UTF-8 takes at
|
||||||
|
// most 4 bytes per char; ImGui's internal ImString limit is well below
|
||||||
|
// this and FFXIV's chat lines top out around a few hundred chars in
|
||||||
|
// practice. The cap prevents an unbounded ArrayPool rent if a caller
|
||||||
|
// ever feeds in a degenerate input.
|
||||||
|
private const int MaxLineByteCount = 16 * 1024;
|
||||||
|
|
||||||
|
internal static void WrapText(string csText, Chunk chunk, PayloadHandler? handler, Vector4 defaultText, float lineWidth)
|
||||||
{
|
{
|
||||||
void Text(byte* text, byte* textEnd)
|
|
||||||
{
|
|
||||||
var oldPos = ImGui.GetCursorScreenPos();
|
|
||||||
|
|
||||||
ImGuiNative.TextUnformatted(text, textEnd);
|
|
||||||
PostPayload(chunk, handler);
|
|
||||||
|
|
||||||
if (!ReferenceEquals(LastLink, chunk.Link))
|
|
||||||
PayloadBounds.Clear();
|
|
||||||
|
|
||||||
LastLink = chunk.Link;
|
|
||||||
|
|
||||||
if (Hovered != null && ReferenceEquals(Hovered, chunk.Link))
|
|
||||||
{
|
|
||||||
defaultText.W = 0.25f;
|
|
||||||
var actualCol = ColourUtil.Vector4ToAbgr(defaultText);
|
|
||||||
ImGui.GetWindowDrawList().AddRectFilled(oldPos, oldPos + ImGui.GetItemRectSize(), actualCol);
|
|
||||||
|
|
||||||
foreach (var (start, size) in PayloadBounds)
|
|
||||||
ImGui.GetWindowDrawList().AddRectFilled(start, start + size, actualCol);
|
|
||||||
|
|
||||||
PayloadBounds.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Hovered == null && chunk.Link != null)
|
|
||||||
PayloadBounds.Add((oldPos, ImGui.GetItemRectSize()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (csText.Length == 0)
|
if (csText.Length == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
foreach (var part in csText.Split(["\r\n", "\r", "\n"], StringSplitOptions.None))
|
foreach (var part in csText.Split(["\r\n", "\r", "\n"], StringSplitOptions.None))
|
||||||
{
|
{
|
||||||
// Encoding.GetBytes is virtual, so the returned array's
|
if (part.Length == 0)
|
||||||
// Length is treated as untrusted by CodeQL for pointer
|
|
||||||
// arithmetic ("cs/unvalidated-local-pointer-arithmetic").
|
|
||||||
// Compute the expected byte count against the same encoder
|
|
||||||
// and bail out if a swapped-in encoding ever returned a
|
|
||||||
// mismatched buffer. Also drops empty splits so the textEnd
|
|
||||||
// pointer below cannot collapse onto text.
|
|
||||||
var expectedLength = Encoding.UTF8.GetByteCount(part);
|
|
||||||
var bytes = Encoding.UTF8.GetBytes(part);
|
|
||||||
if (expectedLength == 0 || bytes.Length != expectedLength)
|
|
||||||
{
|
{
|
||||||
ImGui.TextUnformatted("");
|
ImGui.TextUnformatted("");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
fixed (byte* rawText = bytes)
|
// Allocate against the encoder's own MaxByteCount so the buffer
|
||||||
|
// we hand to ImGui is sized by us. The actual byte count
|
||||||
|
// returned by GetBytes is then validated against that ceiling
|
||||||
|
// before any pointer arithmetic touches it; CodeQL recognises
|
||||||
|
// that comparison as a sanitiser for the
|
||||||
|
// cs/unvalidated-local-pointer-arithmetic taint flow.
|
||||||
|
var maxBytes = Encoding.UTF8.GetMaxByteCount(part.Length);
|
||||||
|
if (maxBytes <= 0 || maxBytes > MaxLineByteCount)
|
||||||
{
|
{
|
||||||
var text = rawText;
|
ImGui.TextUnformatted("");
|
||||||
var textEnd = text + expectedLength;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var widthLeft = ImGui.GetContentRegionAvail().X;
|
var buffer = ArrayPool<byte>.Shared.Rent(maxBytes);
|
||||||
var endPrevLine = ImGuiNative.CalcWordWrapPositionA(ImGui.GetFont().Handle, ImGuiHelpers.GlobalScale, text, textEnd, widthLeft);
|
try
|
||||||
if (endPrevLine == null)
|
{
|
||||||
|
var written = Encoding.UTF8.GetBytes(part, 0, part.Length, buffer, 0);
|
||||||
|
if (written <= 0 || written > maxBytes)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("");
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var firstSpace = FindFirstSpace(text, textEnd);
|
WrapEncodedLine(buffer.AsSpan(0, written), chunk, handler, defaultText, lineWidth);
|
||||||
var properBreak = firstSpace <= endPrevLine;
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static unsafe void WrapEncodedLine(ReadOnlySpan<byte> bytes, Chunk chunk, PayloadHandler? handler, Vector4 defaultText, float lineWidth)
|
||||||
|
{
|
||||||
|
var byteCount = bytes.Length;
|
||||||
|
if (byteCount == 0)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fixed (byte* basePtr = bytes)
|
||||||
|
{
|
||||||
|
var widthLeft = ImGui.GetContentRegionAvail().X;
|
||||||
|
var endPrev = CalcWordWrap(basePtr, 0, byteCount, widthLeft);
|
||||||
|
if (endPrev < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var firstSpace = FindFirstSpace(bytes, 0, byteCount);
|
||||||
|
var properBreak = firstSpace <= endPrev;
|
||||||
|
if (properBreak)
|
||||||
|
{
|
||||||
|
DrawText(basePtr, 0, endPrev, chunk, handler, defaultText);
|
||||||
|
}
|
||||||
|
else if (lineWidth == 0f)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Check whether the next chunk would wrap at or past the
|
||||||
|
// first space. If yes, force a line break.
|
||||||
|
var wrapPos = CalcWordWrap(basePtr, 0, firstSpace, lineWidth);
|
||||||
|
if (wrapPos >= firstSpace)
|
||||||
|
ImGui.TextUnformatted("");
|
||||||
|
}
|
||||||
|
|
||||||
|
widthLeft = ImGui.GetContentRegionAvail().X;
|
||||||
|
var lineStart = 0;
|
||||||
|
while (endPrev < byteCount)
|
||||||
|
{
|
||||||
if (properBreak)
|
if (properBreak)
|
||||||
{
|
lineStart = endPrev;
|
||||||
Text(text, endPrevLine);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (lineWidth == 0f)
|
|
||||||
{
|
|
||||||
ImGui.TextUnformatted("");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// check if the next bit is longer than the entire line width
|
|
||||||
var wrapPos = ImGuiNative.CalcWordWrapPositionA(ImGui.GetFont().Handle, ImGuiHelpers.GlobalScale, text, firstSpace, lineWidth);
|
|
||||||
|
|
||||||
// only go to next line is it's going to wrap at the space
|
// Skip a leading space at the start of a wrapped line.
|
||||||
if (wrapPos >= firstSpace)
|
if (lineStart < byteCount && bytes[lineStart] == (byte)' ')
|
||||||
ImGui.TextUnformatted("");
|
lineStart++;
|
||||||
}
|
|
||||||
|
var newEnd = CalcWordWrap(basePtr, lineStart, byteCount, widthLeft);
|
||||||
|
if (properBreak && newEnd == endPrev)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (newEnd < 0)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("");
|
||||||
|
ImGui.TextUnformatted("");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
widthLeft = ImGui.GetContentRegionAvail().X;
|
endPrev = newEnd;
|
||||||
while (endPrevLine < textEnd)
|
DrawText(basePtr, lineStart, endPrev, chunk, handler, defaultText);
|
||||||
|
|
||||||
|
if (!properBreak)
|
||||||
{
|
{
|
||||||
if (properBreak)
|
properBreak = true;
|
||||||
text = endPrevLine;
|
widthLeft = ImGui.GetContentRegionAvail().X;
|
||||||
|
|
||||||
// skip a space at start of line
|
|
||||||
if (*text == ' ')
|
|
||||||
++text;
|
|
||||||
|
|
||||||
var newEnd = ImGuiNative.CalcWordWrapPositionA(ImGui.GetFont().Handle, ImGuiHelpers.GlobalScale, text, textEnd, widthLeft);
|
|
||||||
if (properBreak && newEnd == endPrevLine)
|
|
||||||
break;
|
|
||||||
|
|
||||||
endPrevLine = newEnd;
|
|
||||||
if (endPrevLine == null)
|
|
||||||
{
|
|
||||||
ImGui.TextUnformatted("");
|
|
||||||
ImGui.TextUnformatted("");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(text, endPrevLine);
|
|
||||||
|
|
||||||
if (!properBreak)
|
|
||||||
{
|
|
||||||
properBreak = true;
|
|
||||||
widthLeft = ImGui.GetContentRegionAvail().X;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static unsafe byte* FindFirstSpace(byte* text, byte* textEnd)
|
private static unsafe int CalcWordWrap(byte* basePtr, int start, int end, float width)
|
||||||
{
|
{
|
||||||
for (var i = text; i < textEnd; i++)
|
var result = ImGuiNative.CalcWordWrapPositionA(
|
||||||
if (char.IsWhiteSpace((char) *i))
|
ImGui.GetFont().Handle,
|
||||||
|
ImGuiHelpers.GlobalScale,
|
||||||
|
basePtr + start,
|
||||||
|
basePtr + end,
|
||||||
|
width);
|
||||||
|
if (result == null)
|
||||||
|
return -1;
|
||||||
|
return (int)(result - basePtr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static unsafe void DrawText(byte* basePtr, int start, int end, Chunk chunk, PayloadHandler? handler, Vector4 defaultText)
|
||||||
|
{
|
||||||
|
var oldPos = ImGui.GetCursorScreenPos();
|
||||||
|
|
||||||
|
ImGuiNative.TextUnformatted(basePtr + start, basePtr + end);
|
||||||
|
PostPayload(chunk, handler);
|
||||||
|
|
||||||
|
if (!ReferenceEquals(LastLink, chunk.Link))
|
||||||
|
PayloadBounds.Clear();
|
||||||
|
|
||||||
|
LastLink = chunk.Link;
|
||||||
|
|
||||||
|
if (Hovered != null && ReferenceEquals(Hovered, chunk.Link))
|
||||||
|
{
|
||||||
|
defaultText.W = 0.25f;
|
||||||
|
var actualCol = ColourUtil.Vector4ToAbgr(defaultText);
|
||||||
|
ImGui.GetWindowDrawList().AddRectFilled(oldPos, oldPos + ImGui.GetItemRectSize(), actualCol);
|
||||||
|
|
||||||
|
foreach (var (boundsStart, boundsSize) in PayloadBounds)
|
||||||
|
ImGui.GetWindowDrawList().AddRectFilled(boundsStart, boundsStart + boundsSize, actualCol);
|
||||||
|
|
||||||
|
PayloadBounds.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Hovered == null && chunk.Link != null)
|
||||||
|
PayloadBounds.Add((oldPos, ImGui.GetItemRectSize()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int FindFirstSpace(ReadOnlySpan<byte> bytes, int start, int end)
|
||||||
|
{
|
||||||
|
for (var i = start; i < end; i++)
|
||||||
|
if (char.IsWhiteSpace((char)bytes[i]))
|
||||||
return i;
|
return i;
|
||||||
|
|
||||||
return textEnd;
|
return end;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool IconButton(FontAwesomeIcon icon, string? id = null, string? tooltip = null, int width = 0)
|
internal static bool IconButton(FontAwesomeIcon icon, string? id = null, string? tooltip = null, int width = 0)
|
||||||
|
|||||||
Reference in New Issue
Block a user