using System.Security.Cryptography; using System.Text.Json; namespace PRecAnnot.Plant3D2026; internal sealed record CompositeAssemblyManifest(string Name, string Sha256, bool Required); internal sealed record CompositeManifest(int SchemaVersion, string Package, string Version, IReadOnlyList Modules, IReadOnlyList Assemblies); internal static class CompositeModuleManifest { public static CompositeManifest Validate() { var assemblyPath = typeof(EntryPoint).Assembly.Location; var path = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(assemblyPath)!, "..", "module-manifest.json")); if (!File.Exists(path)) throw new InvalidOperationException("COMPOSITE_MODULE_MANIFEST_MISSING"); var manifest = JsonSerializer.Deserialize(File.ReadAllText(path), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? throw new InvalidOperationException("COMPOSITE_MODULE_MANIFEST_INVALID"); if (manifest.SchemaVersion != 1 || !string.Equals(manifest.Package, "P_Rec_Annot", StringComparison.Ordinal) || manifest.Modules.Count == 0) throw new InvalidOperationException("COMPOSITE_MODULE_MANIFEST_INVALID"); foreach (var item in manifest.Assemblies.Where(value => value.Required)) { var file = Path.Combine(Path.GetDirectoryName(assemblyPath)!, item.Name); if (!File.Exists(file)) throw new InvalidOperationException($"COMPOSITE_MODULE_ASSEMBLY_MISSING:{item.Name}"); var hash = Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(file))).ToLowerInvariant(); if (!string.Equals(hash, item.Sha256, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException($"COMPOSITE_MODULE_HASH_MISMATCH:{item.Name}"); } return manifest; } }