From 039891353868c2ea51add3e97a3ce1bf6436afcd Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 7 Sep 2026 01:18:54 +0200 Subject: [PATCH] =?UTF-8?q?fix(vt):=20round=203=20items=203-4=20=E2=80=94?= =?UTF-8?q?=20hidden-prefix-first=20nav=20naming=20+=20disjoint=20pickers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../MossTankRouteProfileStore.cs | 9 ++- .../VtankProfileDirectory.cs | 51 +++++++++++-- .../NavigationTests.cs | 5 +- .../VtankProfileDirectoryTests.cs | 71 ++++++++++++++++++- 4 files changed, 124 insertions(+), 12 deletions(-) diff --git a/src/AcDream.Plugins.MossTank/MossTankRouteProfileStore.cs b/src/AcDream.Plugins.MossTank/MossTankRouteProfileStore.cs index 94c3505bd..9eeb4a83e 100644 --- a/src/AcDream.Plugins.MossTank/MossTankRouteProfileStore.cs +++ b/src/AcDream.Plugins.MossTank/MossTankRouteProfileStore.cs @@ -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) => diff --git a/src/AcDream.Plugins.MossTank/VtankProfileDirectory.cs b/src/AcDream.Plugins.MossTank/VtankProfileDirectory.cs index 53d80ff86..b093482da 100644 --- a/src/AcDream.Plugins.MossTank/VtankProfileDirectory.cs +++ b/src/AcDream.Plugins.MossTank/VtankProfileDirectory.cs @@ -35,6 +35,22 @@ internal static class VtankProfileDirectory internal const string DefaultLabel = "[Default]"; internal const string NoneLabel = "[None]"; + /// + /// The nav_ marker (round 3 items 3/4) that distinguishes a + /// stand-alone route .af from a Meta profile sharing the same + /// flat directory and extension + /// — metaf's own observed convention for a nav-only .af (see the + /// committed nav_*.af fixtures). MUST be checked AFTER stripping + /// when the marker is combined with it (see + /// 's + /// marker parameter): a hidden per-character route file is named + /// --nav_Name_Server.af — hidden prefix FIRST, marker SECOND — + /// never nav_--Name_Server.af, which does not start with + /// at all and so defeats every + /// StartsWith("--") hidden-file check in this class. + /// + internal const string NavMarker = "nav_"; + /// /// VTank's single per-character default filename /// (uTank2/PluginCore.cs:3863-3865): --Name_Server.ext. @@ -45,6 +61,21 @@ internal static class VtankProfileDirectory string extension) => $"{HiddenPrefix}{characterName}_{server}.{extension.TrimStart('.')}"; + /// + /// Overload for a per-character auto file that ALSO carries a kind + /// marker (currently only , for + /// 's route .af): the + /// hidden prefix always comes first so the file stays hidden from every + /// other character's picker exactly like the unmarked overload above — + /// --{marker}Name_Server.ext, e.g. --nav_Name_Server.af. + /// + public static string AutoCharacterFileName( + string characterName, + string server, + string extension, + string marker) => + $"{HiddenPrefix}{marker}{characterName}_{server}.{extension.TrimStart('.')}"; + /// /// The longer, trailing-underscore prefix /// (uTank2/PluginCore.cs:933,3866, field dw) that marks a @@ -155,7 +186,10 @@ internal static class VtankProfileDirectory /// VTank's navigation-profile list (l(), /// uTank2/PluginCore.cs:7156-7185): seeds /// /, then every - /// .af file that starts with neither -- nor ~~. + /// -marked .af file that starts with + /// neither -- nor ~~. 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 .af too. /// public static IReadOnlyList 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 (ac(), /// uTank2/PluginCore.cs:7187+): seeds /// /, then every - /// non--- file (a meta and a nav profile share the same - /// directory and extension here — .af — so callers pass a - /// distinguishing sub-extension convention if they need one; VTank - /// itself distinguished by the separate .met/.nav - /// extensions). + /// non---, non--marked file (a meta and a + /// nav profile share the same directory and extension here — + /// .af — so the marker is what VTank's own separate + /// .met/.nav extensions used to provide; round 3 item 4: + /// without excluding files this returned every + /// route .af too). /// public static IReadOnlyList 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; diff --git a/tests/AcDream.Plugins.MossTank.Tests/NavigationTests.cs b/tests/AcDream.Plugins.MossTank.Tests/NavigationTests.cs index e4c71ff64..0aec9e989 100644 --- a/tests/AcDream.Plugins.MossTank.Tests/NavigationTests.cs +++ b/tests/AcDream.Plugins.MossTank.Tests/NavigationTests.cs @@ -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 { diff --git a/tests/AcDream.Plugins.MossTank.Tests/VtankProfileDirectoryTests.cs b/tests/AcDream.Plugins.MossTank.Tests/VtankProfileDirectoryTests.cs index f5907cf38..76862fabe 100644 --- a/tests/AcDream.Plugins.MossTank.Tests/VtankProfileDirectoryTests.cs +++ b/tests/AcDream.Plugins.MossTank.Tests/VtankProfileDirectoryTests.cs @@ -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 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)); } + /// + /// Round 3 items 3/4: a Meta profile and a route share the exact same + /// flat directory and .af 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. + /// + [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 navEntries = + VtankProfileDirectory.ListNavigationProfiles(storage); + IReadOnlyList 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)); + } + + /// + /// Round 3 item 3 (BLOCKER-adjacent leak): before the fix, a hidden + /// per-character route file was named nav_--Name_Server.af + /// (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. + /// + [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 navEntries = + VtankProfileDirectory.ListNavigationProfiles(storage); + IReadOnlyList 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() {