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, IReadOnlyList? Machines = null) { public bool IsAllowedOn(string machineName) => Machines is null || Machines.Count == 0 || Machines.Contains(machineName, StringComparer.OrdinalIgnoreCase); } public sealed record LegacyProductPattern(string Name, string Filter, string Roster, IReadOnlyList Plugins, IReadOnlyList? Attributes = null, IReadOnlyList? Sources = null, string? Target = null, string? Loader = null, string? ObjectPath = null); public sealed record LegacyLauncherSettings(string Help, string CareMarker, IReadOnlyList Patterns); public sealed record LegacyDownloadListEntry(int ElementIndex, string RawId, string NormalizedId, int? FirstOccurrenceIndex, string SourceShape); public sealed record LegacyDownloadSelection(IReadOnlyList PluginIds, IReadOnlyList? Entries = null) { public IReadOnlyList EffectivePluginIds => PluginIds.Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } 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, plugin.Elements("machine").Select(x => x.Value.Trim()).Where(x => x.Length > 0).ToArray())).ToArray(); return new LegacyProductPattern(Value("name"), Value("filter"), Value("roster"), plugins, pattern.Elements("attrib").Select(x => x.Value).ToArray(), pattern.Elements("source").Elements("course").Select(x => x.Value).ToArray(), pattern.Element("target")?.Value, pattern.Element("loader")?.Value, pattern.Element("object")?.Value); }).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) { try { using var reader = XmlReader.Create(new StringReader(xml), XmlSettings); var root = XDocument.Load(reader).Root; if (root?.Name != "root") throw new InvalidOperationException("DOWNLOAD_LIST_ROOT_INVALID"); // F2_Xml serialized only root/id and removed only exact empty strings. Real project files // also contain source-grouped entries; preserve their document order as the // modern plan while retaining raw values and duplicate evidence. var direct = root.Elements("id").Select(x => (Element: x, Shape: "Root")); var grouped = root.Elements("area").SelectMany(area => area.Elements("id").Select(x => (Element: x, Shape: "Area"))); var entries = (direct.Any() ? direct : grouped).Select((entry, index) => { var raw = entry.Element.Value; var normalized = raw; // legacy F2_Xml did not Trim or case-normalize IDs. return new LegacyDownloadListEntry(index, raw, normalized, null, entry.Shape); }).ToArray(); var seen = new Dictionary(StringComparer.OrdinalIgnoreCase); var annotated = entries.Select(entry => { if (entry.RawId == "") return entry; if (seen.TryGetValue(entry.NormalizedId, out var first)) return entry with { FirstOccurrenceIndex = first }; seen[entry.NormalizedId] = entry.ElementIndex; return entry; }).ToArray(); return new(annotated.Where(x => x.RawId != "").Select(x => x.RawId).ToArray(), annotated); } catch (XmlException) { throw new InvalidOperationException("DOWNLOAD_LIST_XML_MALFORMED"); } } public IReadOnlyList Join(LegacyProductPattern pattern, LegacyDownloadSelection selection, LegacyCompatibilityOverlay overlay) { var result = new List(); foreach (var id in selection.EffectivePluginIds) { 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; } }