V18 / 8d896b8a86e5
ScenarioFileService.cs
Lilith/Core/Scenarios/ScenarioFileService.cs
Changed in this versionCS
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);
}
}