diff --git a/Launcher.Next.Application/LegacyConfigurationAdapters.cs b/Launcher.Next.Application/LegacyConfigurationAdapters.cs new file mode 100644 index 0000000..4119c8d --- /dev/null +++ b/Launcher.Next.Application/LegacyConfigurationAdapters.cs @@ -0,0 +1,61 @@ +using System.Xml; +using System.Xml.Linq; +using Launcher.Next.Core; + +namespace Launcher.Next.Application; + +public sealed record LegacyPluginDefinition(string Id, string Renewal, string Catalog, string Install, int DefinitionOrder); +public sealed record LegacyProductPattern(string Name, string Filter, string Roster, IReadOnlyList Plugins); +public sealed record LegacyLauncherSettings(string Help, string CareMarker, IReadOnlyList Patterns); +public sealed record LegacyDownloadSelection(IReadOnlyList PluginIds); +public sealed record LegacyCompatibilityMapping(string LegacyId, string PackageId, string Version, string Channel); +public sealed record LegacyCompatibilityOverlay(string MappingId, string Product, int ProductYear, string Architecture, bool AllowOfflineFallback, IReadOnlyList Plugins); + +public sealed class LegacyConfigurationAdapter +{ + private static readonly XmlReaderSettings XmlSettings = new() { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null, MaxCharactersInDocument = 1_000_000 }; + + public LegacyLauncherSettings ParseSettings(string xml) + { + using var reader = XmlReader.Create(new StringReader(xml), XmlSettings); var root = XDocument.Load(reader).Root ?? throw new InvalidOperationException("LEGACY_SETTINGS_INVALID"); + var path = root.Element("path") ?? throw new InvalidOperationException("LEGACY_SETTINGS_PATH_MISSING"); + var patterns = root.Elements("data").Select(data => + { + var pattern = data.Element("pattern") ?? throw new InvalidOperationException("LEGACY_PATTERN_MISSING"); + string Value(string name) => pattern.Element(name)?.Value ?? throw new InvalidOperationException("LEGACY_PATTERN_FIELD_MISSING:" + name); + var plugins = data.Elements("plugin").Select((plugin, index) => new LegacyPluginDefinition( + plugin.Element("id")?.Value ?? throw new InvalidOperationException("LEGACY_PLUGIN_ID_MISSING"), + plugin.Element("renewal")?.Value ?? throw new InvalidOperationException("LEGACY_RENEWAL_MISSING"), + plugin.Element("catalog")?.Value ?? throw new InvalidOperationException("LEGACY_CATALOG_MISSING"), + plugin.Element("install")?.Value ?? throw new InvalidOperationException("LEGACY_INSTALL_MISSING"), index)).ToArray(); + return new LegacyProductPattern(Value("name"), Value("filter"), Value("roster"), plugins); + }).ToArray(); + return new(path.Element("help")?.Value ?? throw new InvalidOperationException("LEGACY_HELP_MISSING"), path.Element("care")?.Value ?? throw new InvalidOperationException("LEGACY_CARE_MISSING"), patterns); + } + + public LegacyDownloadSelection ParseDownloadList(string xml) + { + using var reader = XmlReader.Create(new StringReader(xml), XmlSettings); var root = XDocument.Load(reader).Root; + if (root?.Name != "root") throw new InvalidOperationException("LEGACY_DOWNLOAD_LIST_INVALID"); + var ids = root.Elements("id").Select(x => x.Value.Trim()).ToArray(); + if (ids.Length == 0 || ids.Any(string.IsNullOrWhiteSpace) || ids.GroupBy(x => x, StringComparer.OrdinalIgnoreCase).Any(x => x.Count() > 1)) + throw new InvalidOperationException("LEGACY_DOWNLOAD_SELECTION_INVALID"); + return new(ids); + } + + public IReadOnlyList Join(LegacyProductPattern pattern, LegacyDownloadSelection selection, LegacyCompatibilityOverlay overlay) + { + var result = new List(); + foreach (var id in selection.PluginIds) + { + var definition = pattern.Plugins.Where(x => string.Equals(x.Id, id, StringComparison.OrdinalIgnoreCase)).ToArray(); + if (definition.Length == 0) throw new InvalidOperationException("LEGACY_PLUGIN_ID_NOT_DEFINED"); + if (definition.Length > 1) throw new InvalidOperationException("AMBIGUOUS_LEGACY_PLUGIN_DEFINITION"); + var mapping = overlay.Plugins.Where(x => string.Equals(x.LegacyId, id, StringComparison.OrdinalIgnoreCase)).ToArray(); + if (mapping.Length == 0) throw new InvalidOperationException("LEGACY_MAPPING_NOT_FOUND"); + if (mapping.Length > 1) throw new InvalidOperationException("LEGACY_MAPPING_DUPLICATE"); + result.Add(new(mapping[0].PackageId, overlay.Product, overlay.ProductYear, overlay.Architecture, mapping[0].Channel, mapping[0].Version, overlay.MappingId, overlay.AllowOfflineFallback)); + } + return result; + } +} diff --git a/tests/Launcher.Next.Tests/RosterAdaptersTests.cs b/tests/Launcher.Next.Tests/RosterAdaptersTests.cs index 129e760..5be904b 100644 --- a/tests/Launcher.Next.Tests/RosterAdaptersTests.cs +++ b/tests/Launcher.Next.Tests/RosterAdaptersTests.cs @@ -31,6 +31,24 @@ public sealed class RosterAdaptersTests Assert.False(selected.Roster.Packages[1].Enabled); } + [Fact] + public void LegacySettingsAndDownloadList_JoinOnlyThroughExplicitOverlay() + { + const string settings = "hcPlant*.dwgrlegacy-axcatload"; + const string selection = "legacy-a"; + var adapter = new LegacyConfigurationAdapter(); + var parsed = adapter.ParseSettings(settings); + var requirements = adapter.Join(parsed.Patterns[0], adapter.ParseDownloadList(selection), new("map-a", "Plant3D", 2026, "x64", false, [new("legacy-a", "PHIM.A", "1.0.0", "pilot")])); + Assert.Single(requirements); Assert.Equal("PHIM.A", requirements[0].PackageId); + } + + [Fact] + public void LegacyDownloadList_DoesNotInventPluginMetadata() + { + var selection = new LegacyConfigurationAdapter().ParseDownloadList("legacy-a"); + Assert.Equal(["legacy-a"], selection.PluginIds); + } + [Fact] public void ProjectRoster_HasPriorityOverLegacy() {