fix(vt): round 3 items 3-4 — hidden-prefix-first nav naming + disjoint pickers
Item 3: MossTankRouteProfileStore's per-character auto route file was named
nav_--Name_Server.af (NavPrefix + AutoCharacterFileName) — the "nav_" marker
came BEFORE the "--" hidden prefix, so the whole filename does not start
with "--" and defeats every StartsWith("--") hidden-file check in
VtankProfileDirectory, leaking another character's private route binding
into both the nav and meta pickers. Chose hidden-prefix-first naming
(--nav_Name_Server.af, matching VTank's own "--" convention with the nav_
kind marker second) via a new AutoCharacterFileName(name, server, ext,
marker) overload; applied only to the per-character auto file — named
routes keep their existing nav_Name.af (shared/visible) shape.
Item 4: ListNavigationProfiles and ListMetaProfiles shared the same flat
.af directory with no marker check at all, so each picker returned the
other's files too (a Meta profile appeared in the nav picker and vice
versa). ListNavigationProfiles now requires the nav_ marker;
ListMetaProfiles now excludes it.
Mutation: reverted VtankProfileDirectory.cs and MossTankRouteProfileStore.cs
to HEAD (keeping only the new/changed tests) — the build failed outright
(VtankProfileDirectory has no NavMarker/marker-overload for the new tests to
call), and the two pre-existing tests this round updated
(ListNavigationProfilesFiltersBothReservedPrefixes,
RouteStoreLeavesLegacyJsonUntouchedWhenAfCounterpartExists) independently
failed at runtime against their OLD un-marked/mis-ordered fixtures once
this round's marker/ordering requirement was pinned, confirming both are
exercising real, fixed behavior.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
145bd62503
commit
0398913538
4 changed files with 124 additions and 12 deletions
|
|
@ -375,8 +375,15 @@ internal sealed class MossTankRouteProfileStore
|
|||
// File naming, storage plumbing.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
// Round 3 item 3: the hidden "--" prefix MUST come before the "nav_"
|
||||
// kind marker (--nav_Name_Server.af) — putting the marker first
|
||||
// (nav_--Name_Server.af, the pre-fix shape) means the filename does not
|
||||
// start with "--" at all, defeating the StartsWith("--") hidden-file
|
||||
// check both ListNavigationProfiles and ListMetaProfiles rely on and
|
||||
// leaking this character's private per-character route to every other
|
||||
// character's picker.
|
||||
private string CurrentFileName() => _selected.Equals(ByCharacter, StringComparison.OrdinalIgnoreCase)
|
||||
? NavPrefix + VtankProfileDirectory.AutoCharacterFileName(_characterName, Server, "af")
|
||||
? VtankProfileDirectory.AutoCharacterFileName(_characterName, Server, "af", NavPrefix)
|
||||
: _selected;
|
||||
|
||||
private static string ToFileName(string bareName) =>
|
||||
|
|
|
|||
|
|
@ -35,6 +35,22 @@ internal static class VtankProfileDirectory
|
|||
internal const string DefaultLabel = "[Default]";
|
||||
internal const string NoneLabel = "[None]";
|
||||
|
||||
/// <summary>
|
||||
/// The <c>nav_</c> marker (round 3 items 3/4) that distinguishes a
|
||||
/// stand-alone route <c>.af</c> from a Meta profile sharing the same
|
||||
/// flat <see cref="IPluginHost.VtankProfiles"/> directory and extension
|
||||
/// — metaf's own observed convention for a nav-only <c>.af</c> (see the
|
||||
/// committed <c>nav_*.af</c> fixtures). MUST be checked AFTER stripping
|
||||
/// <see cref="HiddenPrefix"/> when the marker is combined with it (see
|
||||
/// <see cref="AutoCharacterFileName(string,string,string,string)"/>'s
|
||||
/// <c>marker</c> parameter): a hidden per-character route file is named
|
||||
/// <c>--nav_Name_Server.af</c> — hidden prefix FIRST, marker SECOND —
|
||||
/// never <c>nav_--Name_Server.af</c>, which does not start with
|
||||
/// <see cref="HiddenPrefix"/> at all and so defeats every
|
||||
/// <c>StartsWith("--")</c> hidden-file check in this class.
|
||||
/// </summary>
|
||||
internal const string NavMarker = "nav_";
|
||||
|
||||
/// <summary>
|
||||
/// VTank's single per-character default filename
|
||||
/// (<c>uTank2/PluginCore.cs:3863-3865</c>): <c>--Name_Server.ext</c>.
|
||||
|
|
@ -45,6 +61,21 @@ internal static class VtankProfileDirectory
|
|||
string extension) =>
|
||||
$"{HiddenPrefix}{characterName}_{server}.{extension.TrimStart('.')}";
|
||||
|
||||
/// <summary>
|
||||
/// Overload for a per-character auto file that ALSO carries a kind
|
||||
/// marker (currently only <see cref="NavMarker"/>, for
|
||||
/// <see cref="MossTankRouteProfileStore"/>'s route <c>.af</c>): the
|
||||
/// hidden prefix always comes first so the file stays hidden from every
|
||||
/// other character's picker exactly like the unmarked overload above —
|
||||
/// <c>--{marker}Name_Server.ext</c>, e.g. <c>--nav_Name_Server.af</c>.
|
||||
/// </summary>
|
||||
public static string AutoCharacterFileName(
|
||||
string characterName,
|
||||
string server,
|
||||
string extension,
|
||||
string marker) =>
|
||||
$"{HiddenPrefix}{marker}{characterName}_{server}.{extension.TrimStart('.')}";
|
||||
|
||||
/// <summary>
|
||||
/// The longer, trailing-underscore prefix
|
||||
/// (<c>uTank2/PluginCore.cs:933,3866</c>, field <c>dw</c>) that marks a
|
||||
|
|
@ -155,7 +186,10 @@ internal static class VtankProfileDirectory
|
|||
/// VTank's navigation-profile list (<c>l()</c>,
|
||||
/// <c>uTank2/PluginCore.cs:7156-7185</c>): seeds
|
||||
/// <see cref="NoneLabel"/>/<see cref="ByCharacterLabel"/>, then every
|
||||
/// <c>.af</c> file that starts with neither <c>--</c> nor <c>~~</c>.
|
||||
/// <see cref="NavMarker"/>-marked <c>.af</c> file that starts with
|
||||
/// neither <c>--</c> nor <c>~~</c>. Round 3 item 4: a Meta profile and a
|
||||
/// route share this same flat, single-extension directory — without
|
||||
/// the marker check this returned every Meta <c>.af</c> too.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<ProfileEntry> ListNavigationProfiles(IPluginStorage storage)
|
||||
{
|
||||
|
|
@ -171,6 +205,8 @@ internal static class VtankProfileDirectory
|
|||
{
|
||||
continue;
|
||||
}
|
||||
if (!fileName.StartsWith(NavMarker, StringComparison.Ordinal))
|
||||
continue;
|
||||
entries.Add(new ProfileEntry(fileName, fileName));
|
||||
}
|
||||
return entries;
|
||||
|
|
@ -180,11 +216,12 @@ internal static class VtankProfileDirectory
|
|||
/// VTank's meta-profile list (<c>ac()</c>,
|
||||
/// <c>uTank2/PluginCore.cs:7187+</c>): seeds
|
||||
/// <see cref="NoneLabel"/>/<see cref="ByCharacterLabel"/>, then every
|
||||
/// non-<c>--</c> file (a meta and a nav profile share the same
|
||||
/// directory and extension here — <c>.af</c> — so callers pass a
|
||||
/// distinguishing sub-extension convention if they need one; VTank
|
||||
/// itself distinguished by the separate <c>.met</c>/<c>.nav</c>
|
||||
/// extensions).
|
||||
/// non-<c>--</c>, non-<see cref="NavMarker"/>-marked file (a meta and a
|
||||
/// nav profile share the same directory and extension here —
|
||||
/// <c>.af</c> — so the marker is what VTank's own separate
|
||||
/// <c>.met</c>/<c>.nav</c> extensions used to provide; round 3 item 4:
|
||||
/// without excluding <see cref="NavMarker"/> files this returned every
|
||||
/// route <c>.af</c> too).
|
||||
/// </summary>
|
||||
public static IReadOnlyList<ProfileEntry> ListMetaProfiles(IPluginStorage storage)
|
||||
{
|
||||
|
|
@ -197,6 +234,8 @@ internal static class VtankProfileDirectory
|
|||
{
|
||||
if (fileName.StartsWith(HiddenPrefix, StringComparison.Ordinal))
|
||||
continue;
|
||||
if (fileName.StartsWith(NavMarker, StringComparison.Ordinal))
|
||||
continue;
|
||||
entries.Add(new ProfileEntry(fileName, fileName));
|
||||
}
|
||||
return entries;
|
||||
|
|
|
|||
|
|
@ -514,8 +514,9 @@ public sealed class NavigationTests
|
|||
var storage = new MemoryStorage();
|
||||
string legacyKey = LegacyRouteByCharacterKey("Barris");
|
||||
storage.Text[legacyKey] = """{ "Mode": 1, "Waypoints": [] }""";
|
||||
string realKey = "nav_" + VtankProfileDirectory.AutoCharacterFileName(
|
||||
"Barris", string.Empty, "af");
|
||||
// Round 3 item 3: hidden prefix first, nav_ marker second.
|
||||
string realKey = VtankProfileDirectory.AutoCharacterFileName(
|
||||
"Barris", string.Empty, "af", VtankProfileDirectory.NavMarker);
|
||||
var real = new NavigationSettings { Mode = RouteMode.Circular };
|
||||
real.Waypoints.Add(new RouteWaypoint
|
||||
{
|
||||
|
|
|
|||
|
|
@ -74,8 +74,10 @@ public sealed class VtankProfileDirectoryTests
|
|||
public void ListNavigationProfilesFiltersBothReservedPrefixes()
|
||||
{
|
||||
var storage = new MemoryStorage();
|
||||
storage.WriteText("Hunt.af", "1\r\n");
|
||||
storage.WriteText("--Barris_Coldeve.af", "1\r\n");
|
||||
// Round 3 item 4: a route .af must carry the nav_ marker to be
|
||||
// listed at all — an un-marked file is a Meta profile, not a route.
|
||||
storage.WriteText("nav_Hunt.af", "1\r\n");
|
||||
storage.WriteText("--nav_Barris_Coldeve.af", "1\r\n");
|
||||
storage.WriteText("~~backup.af", "1\r\n");
|
||||
|
||||
IReadOnlyList<VtankProfileDirectory.ProfileEntry> entries =
|
||||
|
|
@ -83,11 +85,74 @@ public sealed class VtankProfileDirectoryTests
|
|||
|
||||
Assert.Equal(VtankProfileDirectory.NoneLabel, entries[0].DisplayName);
|
||||
Assert.Equal(VtankProfileDirectory.ByCharacterLabel, entries[1].DisplayName);
|
||||
Assert.Contains(entries, static e => e.DisplayName == "Hunt.af");
|
||||
Assert.Contains(entries, static e => e.DisplayName == "nav_Hunt.af");
|
||||
Assert.DoesNotContain(entries, static e => e.FileName.StartsWith("--", StringComparison.Ordinal));
|
||||
Assert.DoesNotContain(entries, static e => e.FileName.StartsWith("~~", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Round 3 items 3/4: a Meta profile and a route share the exact same
|
||||
/// flat directory and <c>.af</c> extension, so the nav_ marker (and the
|
||||
/// hidden "--" prefix coming BEFORE it on a per-character auto file) is
|
||||
/// the only thing that keeps the two pickers from showing each other's
|
||||
/// files. This is the disambiguation test both pickers must pass
|
||||
/// together against one mixed directory.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void NavigationAndMetaPickersPartitionTheSameMixedDirectory()
|
||||
{
|
||||
var storage = new MemoryStorage();
|
||||
storage.WriteText("nav_Hunt.af", "1\r\n");
|
||||
storage.WriteText("MyMeta.af", "1\r\n");
|
||||
// Barris's own hidden per-character route: hidden prefix FIRST,
|
||||
// marker SECOND (item 3's fix) — must be invisible to BOTH pickers,
|
||||
// not just the nav one.
|
||||
storage.WriteText("--nav_Barris_Coldeve.af", "1\r\n");
|
||||
// Barris's own hidden per-character Meta file (unmarked, as
|
||||
// MossTankMetaProfileStore has always named it) — must likewise be
|
||||
// invisible to both.
|
||||
storage.WriteText("--Barris_Coldeve.af", "1\r\n");
|
||||
|
||||
IReadOnlyList<VtankProfileDirectory.ProfileEntry> navEntries =
|
||||
VtankProfileDirectory.ListNavigationProfiles(storage);
|
||||
IReadOnlyList<VtankProfileDirectory.ProfileEntry> metaEntries =
|
||||
VtankProfileDirectory.ListMetaProfiles(storage);
|
||||
|
||||
Assert.Contains(navEntries, static e => e.FileName == "nav_Hunt.af");
|
||||
Assert.DoesNotContain(navEntries, static e => e.FileName == "MyMeta.af");
|
||||
Assert.DoesNotContain(navEntries, static e => e.FileName.StartsWith("--", StringComparison.Ordinal));
|
||||
|
||||
Assert.Contains(metaEntries, static e => e.FileName == "MyMeta.af");
|
||||
Assert.DoesNotContain(metaEntries, static e => e.FileName == "nav_Hunt.af");
|
||||
Assert.DoesNotContain(metaEntries, static e => e.FileName.StartsWith("--", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Round 3 item 3 (BLOCKER-adjacent leak): before the fix, a hidden
|
||||
/// per-character route file was named <c>nav_--Name_Server.af</c>
|
||||
/// (marker first) — which does NOT start with "--" and so defeated the
|
||||
/// hidden-file check in BOTH pickers, leaking another character's
|
||||
/// private route/Meta binding.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AnotherCharactersAutoRouteIsHiddenFromBothPickers()
|
||||
{
|
||||
var storage = new MemoryStorage();
|
||||
// The CORRECT (post-fix) shape: hidden prefix first, marker second.
|
||||
storage.WriteText(
|
||||
VtankProfileDirectory.AutoCharacterFileName(
|
||||
"Someone", "Coldeve", "af", VtankProfileDirectory.NavMarker),
|
||||
"1\r\n");
|
||||
|
||||
IReadOnlyList<VtankProfileDirectory.ProfileEntry> navEntries =
|
||||
VtankProfileDirectory.ListNavigationProfiles(storage);
|
||||
IReadOnlyList<VtankProfileDirectory.ProfileEntry> metaEntries =
|
||||
VtankProfileDirectory.ListMetaProfiles(storage);
|
||||
|
||||
Assert.DoesNotContain(navEntries, static e => e.FileName.Contains("Someone", StringComparison.Ordinal));
|
||||
Assert.DoesNotContain(metaEntries, static e => e.FileName.Contains("Someone", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ListingsOnUnavailableStorageOnlySeedTheBuiltInEntries()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue