← All source files

V18 / 8d896b8a86e5

ScenarioFileService.cs

Lilith/Core/Scenarios/ScenarioFileService.cs

Changed in this version
CS
namespace Lilith.Core.Scenarios;

/// <summary>Loads trusted local application content, never code supplied by a model tool call.</summary>
public sealed class ScenarioFileService
{
    public string RootDirectory { get; }
    public ScenarioFileService(string? root = null) => RootDirectory = Path.GetFullPath(root ??
        Environment.GetEnvironmentVariable("LILITH_SCENARIOS_ROOT") ?? Path.Combine(AppContext.BaseDirectory, "Scenarios"));

    public string LoadScenarioXml(string scenarioName)
    {
        if (!ScenarioXmlParser.ValidName(scenarioName)) throw new ArgumentException("Invalid scenario name.");
        var rootInfo = new DirectoryInfo(RootDirectory);
        for (DirectoryInfo? directory = rootInfo; directory is not null; directory = directory.Parent)
            if (directory.Exists && directory.Attributes.HasFlag(FileAttributes.ReparsePoint))
                throw new InvalidDataException("Scenario roots may not traverse reparse points.");
        string path = Path.Combine(RootDirectory, scenarioName + ".xml");
        var info = new FileInfo(path);
        if (!info.Exists) throw new FileNotFoundException("Unknown scenario: " + scenarioName);
        if (info.Attributes.HasFlag(FileAttributes.ReparsePoint) || info.Length > 1_000_000)
            throw new InvalidDataException("Invalid scenario file.");
        string xml = File.ReadAllText(path);
        if (ScenarioXmlParser.Parse(xml).Id != scenarioName) throw new InvalidDataException("Scenario ID must match its filename.");
        return xml;
    }

    public IReadOnlyList<string> ListNames() => Directory.Exists(RootDirectory)
        ? Directory.EnumerateFiles(RootDirectory, "*.xml").Select(Path.GetFileNameWithoutExtension).OfType<string>()
            .Where(ScenarioXmlParser.ValidName).OrderBy(n => n, StringComparer.Ordinal).Take(256).ToArray() : [];

    internal string Bind(string name, IReadOnlyDictionary<string, string> values)
        => BindXml(LoadScenarioXml(name), values);

    internal static string BindXml(string xml, IReadOnlyDictionary<string, string> values)
    {
        var definition = ScenarioXmlParser.Parse(xml);
        int bound = (string?)ScenarioXmlParser.Read(xml).Root!.Attribute("schemaVersion") == "3" ? 60000 : 20000;
        if (values.Any(v => !definition.Inputs.Any(i => i.Name == v.Key) || v.Value.Length > bound)) throw new ArgumentException("Unknown or oversized nested scenario input.");
        var document = ScenarioXmlParser.Read(xml);
        foreach (var input in document.Root!.Element("inputs")?.Elements("input") ?? [])
            if (values.TryGetValue((string)input.Attribute("name")!, out var value)) input.SetAttributeValue("value", value);
        var filled = ScenarioXmlParser.Parse(document.ToString());
        if (filled.Inputs.Any(i => i.Required && string.IsNullOrWhiteSpace(i.Value))) throw new ArgumentException("Missing required inputs for nested scenario: " + filled.Id);
        return document.ToString(System.Xml.Linq.SaveOptions.DisableFormatting);
    }
}