using System.Reflection; using System.Runtime.Loader; using AcDream.Plugin.Abstractions.Rendering; namespace AcDream.Tools.RenderPackValidator; internal static class RenderPackValidatorCommand { internal static int Run( IReadOnlyList args, TextWriter output, TextWriter error) { ArgumentNullException.ThrowIfNull(args); ArgumentNullException.ThrowIfNull(output); ArgumentNullException.ThrowIfNull(error); if (args.Count != 1 || args[0] is "-h" or "--help") { TextWriter target = args.Count == 1 ? output : error; target.WriteLine("usage: RenderPackValidator "); target.WriteLine("The directory must contain plugin.json and its built entry DLL."); return args.Count == 1 ? 0 : 2; } string packDirectory; try { packDirectory = Path.GetFullPath(args[0]); } catch (Exception exception) when (exception is ArgumentException or NotSupportedException or PathTooLongException) { error.WriteLine($"FAIL: invalid pack directory: {exception.Message}"); return 2; } string manifestPath = Path.Combine(packDirectory, "plugin.json"); if (!File.Exists(manifestPath)) { error.WriteLine($"FAIL: plugin.json was not found in '{packDirectory}'."); return 1; } string manifestJson; try { manifestJson = File.ReadAllText(manifestPath); } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) { error.WriteLine($"FAIL: plugin.json could not be read: {exception.Message}"); return 1; } ValidationOutcome parsed = PackManifest.Parse(manifestJson); if (!parsed.Success) { error.WriteLine($"FAIL: {parsed.Reason}"); return 1; } PackManifest manifest = parsed.Manifest!; string entryPath = ResolveInside(packDirectory, manifest.EntryDll); if (!File.Exists(entryPath)) { error.WriteLine($"FAIL: entry DLL '{manifest.EntryDll}' does not exist."); return 1; } PackLoadContext? loadContext = null; try { loadContext = new PackLoadContext(entryPath); Assembly assembly = loadContext.LoadFromAssemblyPath(entryPath); Type[] entryPoints = GetLoadableTypes(assembly) .Where(static type => !type.IsAbstract && !type.IsInterface && typeof(IRenderPackPlugin).IsAssignableFrom(type)) .ToArray(); if (entryPoints.Length != 1) { error.WriteLine( $"FAIL: entry DLL must contain exactly one public constructible " + $"IRenderPackPlugin; found {entryPoints.Length}."); return 1; } if (!entryPoints[0].IsVisible || entryPoints[0].GetConstructor(Type.EmptyTypes) is null) { error.WriteLine( $"FAIL: render-pack entry point '{entryPoints[0].FullName}' " + "has no public parameterless constructor."); return 1; } var plugin = (IRenderPackPlugin)Activator.CreateInstance(entryPoints[0])!; using var registrations = new CapturingRenderPackRegistry(); plugin.Register(registrations); if (registrations.Entries.Count == 0) { error.WriteLine("FAIL: render-pack entry point registered no packs."); return 1; } var seen = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (CapturedRenderPack entry in registrations.Entries) { RenderPackSdkValidationResult descriptor = RenderPackSdkValidator.ValidateDescriptor(entry.Descriptor); if (!descriptor.Success) { error.WriteLine($"FAIL: {descriptor.Reason}"); return 1; } if (!seen.Add(entry.Descriptor.Id)) { error.WriteLine( $"FAIL: render-pack id '{entry.Descriptor.Id}' was registered more than once."); return 1; } RenderPackSdkValidationResult assets = RenderPackSdkValidator.ValidateAssets(entry.Descriptor, entry.Assets); if (!assets.Success) { error.WriteLine($"FAIL: {assets.Reason}"); return 1; } output.WriteLine( $"OK: {entry.Descriptor.Id} {entry.Descriptor.PackVersion} " + $"(API {entry.Descriptor.PackApiVersion}, " + $"{entry.Descriptor.QualityPresets.Count} preset(s))."); } output.WriteLine( $"Validated {registrations.Entries.Count} render pack(s) from '{manifest.Id}'."); return 0; } catch (Exception exception) { error.WriteLine( $"FAIL: pack entry point could not be inspected: " + exception.GetBaseException().Message); return 1; } finally { loadContext?.Unload(); } } private static string ResolveInside(string root, string relativePath) { string resolved = Path.GetFullPath(Path.Combine(root, relativePath)); string prefix = Path.TrimEndingDirectorySeparator(root) + Path.DirectorySeparatorChar; StringComparison comparison = OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; if (!resolved.StartsWith(prefix, comparison)) throw new UnauthorizedAccessException("entryDll escapes the pack directory."); return resolved; } private static IEnumerable GetLoadableTypes(Assembly assembly) { try { return assembly.GetTypes(); } catch (ReflectionTypeLoadException exception) { return exception.Types.OfType(); } } private sealed class PackLoadContext(string entryPath) : AssemblyLoadContext("render-pack-sdk-validator", isCollectible: true) { private const string AbstractionsAssemblyName = "AcDream.Plugin.Abstractions"; private readonly AssemblyDependencyResolver _resolver = new(entryPath); protected override Assembly? Load(AssemblyName assemblyName) { if (assemblyName.Name == AbstractionsAssemblyName) return null; string? path = _resolver.ResolveAssemblyToPath(assemblyName); return path is null ? null : LoadFromAssemblyPath(path); } } } internal sealed class CapturingRenderPackRegistry : IRenderPackRegistry, IDisposable { private readonly List _entries = []; private bool _disposed; internal IReadOnlyList Entries => _entries; public IDisposable Register(RenderPackDescriptor descriptor, IRenderPackAssets assets) { ObjectDisposedException.ThrowIf(_disposed, this); ArgumentNullException.ThrowIfNull(descriptor); ArgumentNullException.ThrowIfNull(assets); var entry = new CapturedRenderPack(descriptor, assets); _entries.Add(entry); return new Registration(_entries, entry); } public void Dispose() { _disposed = true; _entries.Clear(); } private sealed class Registration( List entries, CapturedRenderPack entry) : IDisposable { private List? _entries = entries; public void Dispose() => Interlocked.Exchange(ref _entries, null)?.Remove(entry); } } internal sealed record CapturedRenderPack( RenderPackDescriptor Descriptor, IRenderPackAssets Assets);