using System.Security.Cryptography; using System.Text.Json; namespace Launcher.Next.Core; public enum StartupRosterCandidateStatus { CandidateOnly, LegacyCandidatePresent, ConflictingCandidatePresent, UnexpectedAutoloadRegistration, UnresolvedStartupEntry } public sealed record StartupRosterEntry(string Source, string? Registration, string? PackageId, string? Version, string? EntryAssembly, string? EntryPath, string? EntrySha256, bool Eligible, string LoadTiming); public sealed record StartupRosterReport(StartupRosterCandidateStatus Status, string RequestedPackageId, string RequestedVersion, string? RequestedEntrySha256, IReadOnlyList Entries, DateTimeOffset CreatedUtc) { public bool IsSafe => Status == StartupRosterCandidateStatus.CandidateOnly; } /// Read-only, bounded discovery of P_Rec_Annot startup candidates in controlled local roots. public static class StartupRosterPreflight { public static StartupRosterReport Inspect(string requestedPackageRoot, PackageManifest requested, IEnumerable? additionalRoots = null) { var roots = new HashSet(StringComparer.OrdinalIgnoreCase) { Path.GetFullPath(requestedPackageRoot) }; foreach (var root in additionalRoots ?? DiscoverKnownRoots()) if (!string.IsNullOrWhiteSpace(root) && Directory.Exists(root)) roots.Add(Path.GetFullPath(root)); var entries = new List(); var requestedPath = Path.Combine(Path.GetFullPath(requestedPackageRoot), requested.EntryAssembly); var requestedHash = requested.Files.FirstOrDefault(f => f.Path.Equals(requested.EntryAssembly, StringComparison.OrdinalIgnoreCase))?.Sha256; if (File.Exists(requestedPath)) entries.Add(new("RequestedPackage", requested.Startup.Type + ":" + requested.Startup.Command, requested.PackageId, requested.Version, requested.EntryAssembly, requestedPath, Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(requestedPath))), true, "before-netload")); else entries.Add(new("RequestedPackage", requested.Startup.Type + ":" + requested.Startup.Command, requested.PackageId, requested.Version, requested.EntryAssembly, null, requestedHash, false, "before-netload")); foreach (var root in roots) DiscoverRoot(root, requested, entries); var eligible = entries.Where(e => e.Eligible).ToArray(); StartupRosterCandidateStatus status; if (entries.Any(e => e.EntryPath is null)) status = StartupRosterCandidateStatus.UnresolvedStartupEntry; else if (eligible.Length == 0) status = StartupRosterCandidateStatus.UnresolvedStartupEntry; else if (eligible.Length == 1 && string.Equals(eligible[0].EntrySha256, requestedHash, StringComparison.OrdinalIgnoreCase) && string.Equals(eligible[0].PackageId, requested.PackageId, StringComparison.OrdinalIgnoreCase) && string.Equals(eligible[0].Version, requested.Version, StringComparison.OrdinalIgnoreCase)) status = StartupRosterCandidateStatus.CandidateOnly; else if (eligible.Any(e => string.Equals(e.PackageId, requested.PackageId, StringComparison.OrdinalIgnoreCase) && !string.Equals(e.Version, requested.Version, StringComparison.OrdinalIgnoreCase))) status = StartupRosterCandidateStatus.ConflictingCandidatePresent; else if (eligible.Any(e => !string.Equals(e.EntrySha256, requestedHash, StringComparison.OrdinalIgnoreCase))) status = StartupRosterCandidateStatus.LegacyCandidatePresent; else status = StartupRosterCandidateStatus.UnexpectedAutoloadRegistration; return new(status, requested.PackageId, requested.Version, requestedHash, entries, DateTimeOffset.UtcNow); } public static void Write(string path, StartupRosterReport report) => File.WriteAllText(path, JsonSerializer.Serialize(report, new JsonSerializerOptions { WriteIndented = true })); private static IEnumerable DiscoverKnownRoots() { var temp = Path.GetTempPath(); if (Directory.Exists(temp)) foreach (var root in Directory.EnumerateDirectories(temp, "local-parity-launch-*", SearchOption.TopDirectoryOnly)) yield return Path.Combine(root, "Applications"); foreach (var root in new[] { Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "\\Autodesk\\ApplicationPlugins", Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) + "\\Autodesk\\ApplicationPlugins" }) yield return root; } private static void DiscoverRoot(string root, PackageManifest requested, List entries) { if (!Directory.Exists(root)) return; foreach (var dir in Directory.EnumerateDirectories(root, "P_Rec_Annot", SearchOption.TopDirectoryOnly)) { var current = Path.Combine(dir, "current.json"); if (File.Exists(current)) { using var doc = JsonDocument.Parse(File.ReadAllText(current)); var version = doc.RootElement.TryGetProperty("Current", out var c) ? c.GetString() : null; var packageRoot = version is null ? null : Path.Combine(dir, "versions", version); AddManifest(packageRoot, "LauncherCurrent", "netload", requested, entries); } else AddManifest(dir, "ApplicationPlugins", "autoload", requested, entries); } foreach (var bundle in Directory.EnumerateDirectories(root, "P_Rec_Annot*.bundle", SearchOption.TopDirectoryOnly)) AddManifest(bundle, "PackageContents", "autoload", requested, entries); } private static void AddManifest(string? packageRoot, string source, string registration, PackageManifest requested, List entries) { if (packageRoot is null) { entries.Add(new(source, registration, "P_Rec_Annot", null, null, null, null, false, "before-netload")); return; } var manifestPath = Path.Combine(packageRoot, "package.manifest.json"); if (!File.Exists(manifestPath)) { entries.Add(new(source, registration, "P_Rec_Annot", Path.GetFileName(packageRoot), null, null, null, false, "before-netload")); return; } using var doc = JsonDocument.Parse(File.ReadAllText(manifestPath)); var root = doc.RootElement; string? S(string n) => root.TryGetProperty(n, out var p) ? p.GetString() : null; var id = S("PackageId") ?? "P_Rec_Annot"; var version = S("Version"); var asm = S("EntryAssembly") ?? requested.EntryAssembly; var path = Path.Combine(packageRoot, asm); string? hash = File.Exists(path) ? Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path))) : null; var eligible = File.Exists(path); entries.Add(new(source, registration, id, version, asm, eligible ? path : null, hash, eligible, "before-netload")); } }