From 8764348a75b121401ad4fa0e83611f1bb83195c4 Mon Sep 17 00:00:00 2001 From: codex Date: Wed, 5 Aug 2026 16:33:13 +0300 Subject: [PATCH] feat: add strict legacy probe override schema --- .../LegacyProbeOverride.cs | 39 +++++++++++++++++++ .../RosterAdaptersTests.cs | 9 +++++ 2 files changed, 48 insertions(+) create mode 100644 Launcher.Next.Application/LegacyProbeOverride.cs diff --git a/Launcher.Next.Application/LegacyProbeOverride.cs b/Launcher.Next.Application/LegacyProbeOverride.cs new file mode 100644 index 0000000..13062fd --- /dev/null +++ b/Launcher.Next.Application/LegacyProbeOverride.cs @@ -0,0 +1,39 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Launcher.Next.Application; + +public sealed record LegacyProbeOverride( + int SchemaVersion, + string Mode, + string SettingsXml, + string DownloadListXml, + string CompatibilityOverlay, + string SelectedPattern, + string EvidenceOutputRoot); + +public static class LegacyProbeOverrideParser +{ + private static readonly JsonSerializerOptions Options = new() + { + PropertyNameCaseInsensitive = true, + UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow + }; + + public static LegacyProbeOverride Parse(string json) + { + try + { + var value = JsonSerializer.Deserialize(json, Options) + ?? throw new InvalidOperationException("LEGACY_PROBE_OVERRIDE_MISSING"); + if (value.SchemaVersion != 1) throw new InvalidOperationException("LEGACY_PROBE_OVERRIDE_SCHEMA_UNSUPPORTED"); + if (!string.Equals(value.Mode, "LegacyConfigurationProbe", StringComparison.Ordinal)) throw new InvalidOperationException("LEGACY_PROBE_MODE_REQUIRED"); + if (new[] { value.SettingsXml, value.DownloadListXml, value.CompatibilityOverlay, value.SelectedPattern, value.EvidenceOutputRoot }.Any(string.IsNullOrWhiteSpace)) + throw new InvalidOperationException("LEGACY_PROBE_INPUT_MISSING"); + if (new[] { value.SettingsXml, value.DownloadListXml, value.CompatibilityOverlay, value.EvidenceOutputRoot }.Any(x => Uri.TryCreate(x, UriKind.Absolute, out var uri) && !uri.IsFile || x.Contains('*') || x.Contains('?'))) + throw new InvalidOperationException("LEGACY_PROBE_INPUT_PATH_INVALID"); + return value; + } + catch (JsonException) { throw new InvalidOperationException("LEGACY_PROBE_OVERRIDE_UNKNOWN_FIELD"); } + } +} diff --git a/tests/Launcher.Next.Tests/RosterAdaptersTests.cs b/tests/Launcher.Next.Tests/RosterAdaptersTests.cs index 5be904b..587308c 100644 --- a/tests/Launcher.Next.Tests/RosterAdaptersTests.cs +++ b/tests/Launcher.Next.Tests/RosterAdaptersTests.cs @@ -49,6 +49,15 @@ public sealed class RosterAdaptersTests Assert.Equal(["legacy-a"], selection.PluginIds); } + [Fact] + public void LegacyProbeOverride_RequiresExplicitProbeModeAndStrictSchema() + { + const string valid = """{"schemaVersion":1,"mode":"LegacyConfigurationProbe","settingsXml":"C:/Settings.xml","downloadListXml":"C:/download_list.xml","compatibilityOverlay":"C:/overlay.json","selectedPattern":"Plant","evidenceOutputRoot":"C:/evidence"}"""; + Assert.Equal("Plant", LegacyProbeOverrideParser.Parse(valid).SelectedPattern); + Assert.Throws(() => LegacyProbeOverrideParser.Parse(valid.Replace("LegacyConfigurationProbe","Production"))); + Assert.Throws(() => LegacyProbeOverrideParser.Parse(valid[..^1] + ",\"unknown\":true}")); + } + [Fact] public void ProjectRoster_HasPriorityOverLegacy() {