using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Manifest; /// /// Loads buildx plug-in manifests from the restart-time plug-in directory. /// public sealed class BuildxPluginManifestLoader { public const string DefaultSearchPattern = "*.manifest.json"; private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web) { AllowTrailingCommas = true, ReadCommentHandling = JsonCommentHandling.Skip, PropertyNameCaseInsensitive = true }; private readonly string manifestDirectory; private readonly string searchPattern; public BuildxPluginManifestLoader(string manifestDirectory, string? searchPattern = null) { if (string.IsNullOrWhiteSpace(manifestDirectory)) { throw new ArgumentException("Manifest directory is required.", nameof(manifestDirectory)); } this.manifestDirectory = Path.GetFullPath(manifestDirectory); this.searchPattern = string.IsNullOrWhiteSpace(searchPattern) ? DefaultSearchPattern : searchPattern; } /// /// Loads all manifests in the configured directory. /// public async Task> LoadAsync(CancellationToken cancellationToken) { if (!Directory.Exists(manifestDirectory)) { return Array.Empty(); } var manifests = new List(); foreach (var file in Directory.EnumerateFiles(manifestDirectory, searchPattern, SearchOption.TopDirectoryOnly)) { if (IsHiddenPath(file)) { continue; } var manifest = await DeserializeManifestAsync(file, cancellationToken).ConfigureAwait(false); manifests.Add(manifest); } return manifests .OrderBy(static m => m.Id, StringComparer.OrdinalIgnoreCase) .ThenBy(static m => m.Version, StringComparer.OrdinalIgnoreCase) .ToArray(); } /// /// Loads the manifest with the specified identifier. /// public async Task LoadByIdAsync(string manifestId, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(manifestId)) { throw new ArgumentException("Manifest identifier is required.", nameof(manifestId)); } var manifests = await LoadAsync(cancellationToken).ConfigureAwait(false); var manifest = manifests.FirstOrDefault(m => string.Equals(m.Id, manifestId, StringComparison.OrdinalIgnoreCase)); if (manifest is null) { throw new BuildxPluginException($"Buildx plug-in manifest '{manifestId}' was not found in '{manifestDirectory}'."); } return manifest; } /// /// Loads the first available manifest. /// public async Task LoadDefaultAsync(CancellationToken cancellationToken) { var manifests = await LoadAsync(cancellationToken).ConfigureAwait(false); if (manifests.Count == 0) { throw new BuildxPluginException($"No buildx plug-in manifests were discovered under '{manifestDirectory}'."); } return manifests[0]; } private static bool IsHiddenPath(string path) { var directory = Path.GetDirectoryName(path); while (!string.IsNullOrEmpty(directory)) { var segment = Path.GetFileName(directory); if (segment.StartsWith(".", StringComparison.Ordinal)) { return true; } directory = Path.GetDirectoryName(directory); } return false; } private static async Task DeserializeManifestAsync(string file, CancellationToken cancellationToken) { await using var stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.Asynchronous); BuildxPluginManifest? manifest; try { manifest = await JsonSerializer.DeserializeAsync(stream, SerializerOptions, cancellationToken) .ConfigureAwait(false); } catch (JsonException ex) { throw new BuildxPluginException($"Failed to parse manifest '{file}'.", ex); } if (manifest is null) { throw new BuildxPluginException($"Manifest '{file}' is empty or invalid."); } ValidateManifest(manifest, file); var directory = Path.GetDirectoryName(file); return manifest with { SourcePath = file, SourceDirectory = directory }; } private static void ValidateManifest(BuildxPluginManifest manifest, string file) { if (!string.Equals(manifest.SchemaVersion, BuildxPluginManifest.CurrentSchemaVersion, StringComparison.OrdinalIgnoreCase)) { throw new BuildxPluginException( $"Manifest '{file}' uses unsupported schema version '{manifest.SchemaVersion}'. Expected '{BuildxPluginManifest.CurrentSchemaVersion}'."); } if (string.IsNullOrWhiteSpace(manifest.Id)) { throw new BuildxPluginException($"Manifest '{file}' must specify a non-empty 'id'."); } if (manifest.EntryPoint is null) { throw new BuildxPluginException($"Manifest '{file}' must specify an 'entryPoint'."); } if (string.IsNullOrWhiteSpace(manifest.EntryPoint.Executable)) { throw new BuildxPluginException($"Manifest '{file}' must specify an executable entry point."); } if (!manifest.RequiresRestart) { throw new BuildxPluginException($"Manifest '{file}' must enforce restart-required activation."); } if (manifest.Cas is null) { throw new BuildxPluginException($"Manifest '{file}' must define CAS defaults."); } if (string.IsNullOrWhiteSpace(manifest.Cas.DefaultRoot)) { throw new BuildxPluginException($"Manifest '{file}' must specify a CAS default root directory."); } } }