up
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Loads buildx plug-in manifests from the restart-time plug-in directory.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads all manifests in the configured directory.
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<BuildxPluginManifest>> LoadAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!Directory.Exists(manifestDirectory))
|
||||
{
|
||||
return Array.Empty<BuildxPluginManifest>();
|
||||
}
|
||||
|
||||
var manifests = new List<BuildxPluginManifest>();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the manifest with the specified identifier.
|
||||
/// </summary>
|
||||
public async Task<BuildxPluginManifest> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the first available manifest.
|
||||
/// </summary>
|
||||
public async Task<BuildxPluginManifest> 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<BuildxPluginManifest> 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<BuildxPluginManifest>(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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user