235 lines
8.1 KiB
C#
235 lines
8.1 KiB
C#
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<string> 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 <built-pack-directory>");
|
|
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<string>(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<Type> GetLoadableTypes(Assembly assembly)
|
|
{
|
|
try
|
|
{
|
|
return assembly.GetTypes();
|
|
}
|
|
catch (ReflectionTypeLoadException exception)
|
|
{
|
|
return exception.Types.OfType<Type>();
|
|
}
|
|
}
|
|
|
|
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<CapturedRenderPack> _entries = [];
|
|
private bool _disposed;
|
|
|
|
internal IReadOnlyList<CapturedRenderPack> 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<CapturedRenderPack> entries,
|
|
CapturedRenderPack entry) : IDisposable
|
|
{
|
|
private List<CapturedRenderPack>? _entries = entries;
|
|
|
|
public void Dispose() =>
|
|
Interlocked.Exchange(ref _entries, null)?.Remove(entry);
|
|
}
|
|
}
|
|
|
|
internal sealed record CapturedRenderPack(
|
|
RenderPackDescriptor Descriptor,
|
|
IRenderPackAssets Assets);
|