fix(privacy): the screenshot guard was reading a field that gets wiped on purpose

This morning's fix hung on TellTarget, and TellTarget is routing state that the
codebase clears deliberately. StripTellBindingOnPromote sets IsTempTab false,
empties TellTarget, and keeps the name -- so a promoted tell tab is called
"Player@World" permanently while carrying neither marker, and falls through both
possible checks. That state survives restarts. A pinned tab whose binding did not
survive a save is the same hole with a different cause; the auto-tell service
logs that case as expected and repairs around it.

The flag is set where the name is built from a partner and is not cleared by
promotion. Renaming clears it, because at that point the user typed it.

Config v26 carries it backwards for tabs that already exist: anything still
holding a tell binding or the temp flag got its name from a partner. Tabs
promoted before this version cannot be recovered -- nothing in the stored data
says where their name came from -- and renaming one has the same effect anyway.

Two more things the header was giving away. Its icon for an auto-tell tab is
derived from the partner and stable across sessions, which is three bits of
linkable information on a picture meant to be shareable; the message path
re-salts its name hashes on every load precisely to avoid that, so screenshot
mode now falls back to a plain envelope. And a world name that is not ASCII --
the CN and KR clients have those, and we ship translations for both -- was being
drawn in the meta face, which carries ASCII and a middle dot. It would have come
out as question marks, the same defect the split was built to prevent.

Plus two that are not privacy: the header had no FontsReady gate, alone among
the drawing components, so its band height and baseline offset were wrong in
exactly the frames this cycle made more common. And a long tab name ran past the
band and got cut mid-glyph at the window edge; it fits now, the way the honorific
header already did it.
This commit is contained in:
2026-08-19 12:00:16 +02:00
parent 0399b68d8c
commit 8fea9113b9
8 changed files with 138 additions and 31 deletions
+36 -8
View File
@@ -322,13 +322,41 @@ public sealed class Plugin : IAsyncDalamudPlugin
);
}
// v25 carries no migration step. The schema gate above only refuses
// anything under 16, and Json.NET drops keys it does not recognise on
// load, so the fields v1.12.0 deleted simply stop being written on the
// next save. The bump is documentation, and it has to be consistent:
// the constant and this stamp are two separate places, and changing
// only one gives a config that re-stamps itself on every start.
Config.Version = 25;
// v25 carried no migration step; the bump was documentation.
//
// v26 does. NameCameFromPartner is what screenshot mode reads to decide
// whether a tab name is a person, and a config written before it existed
// has it false on every tab -- including pinned tell tabs, which survive
// reloads and are named "Player@World". Anything still carrying a tell
// binding or the temp flag got its name from a partner, so the flag is
// set from those two.
//
// Tabs promoted before this version are past saving: promotion clears
// both markers and keeps the name, so nothing in the stored data says
// where that name came from. Renaming one clears the flag anyway, which
// is the same outcome the user gets by editing it.
if (Config.Version < 26)
{
var carried = 0;
foreach (var tab in Config.Tabs)
{
if (tab.NameCameFromPartner || (!tab.IsTempTab && tab.TellTarget?.IsSet() != true))
continue;
tab.NameCameFromPartner = true;
carried++;
}
if (carried > 0)
{
Log.Information(
$"Marked {carried} tab(s) as partner-named during the v26 migration, so "
+ "screenshot mode hides them in the channel header."
);
}
}
Config.Version = 26;
// Unpinned TempTabs are session-only and dropped on every load. Pinned
// TempTabs survive reload — Jin's tester feedback (v1.4.7).
@@ -491,7 +519,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
new SelfTests.SettingsWindowOpenStep(this),
new SelfTests.OnOpenMainUiRoutesMainWindowStep(this),
new SelfTests.TypingIpcStateStep(this),
new SelfTests.ConfigMigrationV25Step(this),
new SelfTests.ConfigMigrationV26Step(this),
new SelfTests.DbGateWiringStep(this),
new SelfTests.ChannelPopoutBindStep(this),
new SelfTests.HoverStateFootprintStep(),