This commit is contained in:
StellaOps Bot
2025-12-14 23:20:14 +02:00
parent 3411e825cd
commit b058dbe031
356 changed files with 68310 additions and 1108 deletions

View File

@@ -0,0 +1,209 @@
using System;
using System.Reflection;
namespace StellaOps.Plugin.Versioning;
/// <summary>
/// Provides plugin version compatibility checking against the host application.
/// </summary>
public static class PluginCompatibilityChecker
{
/// <summary>
/// Checks if a plugin assembly is compatible with the specified host version.
/// Uses lenient defaults (does not require version attribute, no strict major version check).
/// For production use, prefer the overload that accepts <see cref="CompatibilityCheckOptions"/>.
/// </summary>
/// <param name="pluginAssembly">The plugin assembly to check.</param>
/// <param name="hostVersion">The host application version.</param>
/// <returns>A result indicating compatibility status.</returns>
public static PluginCompatibilityResult CheckCompatibility(Assembly pluginAssembly, Version hostVersion)
{
return CheckCompatibility(pluginAssembly, hostVersion, CompatibilityCheckOptions.Lenient);
}
/// <summary>
/// Checks if a plugin assembly is compatible with the specified host version using the provided options.
/// </summary>
/// <param name="pluginAssembly">The plugin assembly to check.</param>
/// <param name="hostVersion">The host application version.</param>
/// <param name="options">Options controlling compatibility checking behavior.</param>
/// <returns>A result indicating compatibility status.</returns>
public static PluginCompatibilityResult CheckCompatibility(
Assembly pluginAssembly,
Version hostVersion,
CompatibilityCheckOptions options)
{
if (pluginAssembly == null)
{
throw new ArgumentNullException(nameof(pluginAssembly));
}
if (hostVersion == null)
{
throw new ArgumentNullException(nameof(hostVersion));
}
options ??= CompatibilityCheckOptions.Default;
var attribute = pluginAssembly.GetCustomAttribute<StellaPluginVersionAttribute>();
if (attribute == null)
{
// No version attribute - fail if required
if (options.RequireVersionAttribute)
{
return new PluginCompatibilityResult(
IsCompatible: false,
PluginVersion: null,
MinimumHostVersion: null,
MaximumHostVersion: null,
RequiresSignature: true,
FailureReason: "Plugin does not declare a [StellaPluginVersion] attribute. All plugins must specify version compatibility.",
HasVersionAttribute: false);
}
return new PluginCompatibilityResult(
IsCompatible: true,
PluginVersion: null,
MinimumHostVersion: null,
MaximumHostVersion: null,
RequiresSignature: true,
FailureReason: null,
HasVersionAttribute: false);
}
var minVersion = attribute.GetMinimumHostVersion();
var maxVersion = attribute.GetMaximumHostVersion();
// Check minimum version
if (minVersion != null && hostVersion < minVersion)
{
return new PluginCompatibilityResult(
IsCompatible: false,
PluginVersion: attribute.PluginVersion,
MinimumHostVersion: minVersion,
MaximumHostVersion: maxVersion,
RequiresSignature: attribute.RequiresSignature,
FailureReason: $"Host version {hostVersion} is below minimum required version {minVersion}.",
HasVersionAttribute: true);
}
// Check maximum version (explicit)
if (maxVersion != null && hostVersion > maxVersion)
{
return new PluginCompatibilityResult(
IsCompatible: false,
PluginVersion: attribute.PluginVersion,
MinimumHostVersion: minVersion,
MaximumHostVersion: maxVersion,
RequiresSignature: attribute.RequiresSignature,
FailureReason: $"Host version {hostVersion} exceeds maximum supported version {maxVersion}.",
HasVersionAttribute: true);
}
// Strict major version check: when MaximumHostVersion is not specified,
// assume the plugin only supports the same major version range as its declared MinimumHostVersion
// This prevents loading a plugin designed for host 1.x on host 2.x without explicit declaration.
if (options.StrictMajorVersionCheck && maxVersion == null && minVersion != null)
{
var referenceMajor = minVersion.Major;
if (hostVersion.Major > referenceMajor)
{
return new PluginCompatibilityResult(
IsCompatible: false,
PluginVersion: attribute.PluginVersion,
MinimumHostVersion: minVersion,
MaximumHostVersion: maxVersion,
RequiresSignature: attribute.RequiresSignature,
FailureReason: $"Host major version {hostVersion.Major} exceeds plugin's declared compatibility range (major version {referenceMajor}). " +
$"Plugin must explicitly declare MaximumHostVersion to support host {hostVersion}.",
HasVersionAttribute: true);
}
}
// Strict major version check when NO MinimumHostVersion is specified but we have a plugin version
// Use the plugin's own major version as the reference
if (options.StrictMajorVersionCheck && maxVersion == null && minVersion == null)
{
var pluginMajor = attribute.PluginVersion.Major;
if (hostVersion.Major > pluginMajor)
{
return new PluginCompatibilityResult(
IsCompatible: false,
PluginVersion: attribute.PluginVersion,
MinimumHostVersion: minVersion,
MaximumHostVersion: maxVersion,
RequiresSignature: attribute.RequiresSignature,
FailureReason: $"Host major version {hostVersion.Major} exceeds plugin version {attribute.PluginVersion} with no explicit compatibility declaration. " +
$"Plugin must declare MinimumHostVersion and/or MaximumHostVersion to support host {hostVersion}.",
HasVersionAttribute: true);
}
}
return new PluginCompatibilityResult(
IsCompatible: true,
PluginVersion: attribute.PluginVersion,
MinimumHostVersion: minVersion,
MaximumHostVersion: maxVersion,
RequiresSignature: attribute.RequiresSignature,
FailureReason: null,
HasVersionAttribute: true);
}
}
/// <summary>
/// Options for controlling plugin compatibility checking behavior.
/// </summary>
public sealed class CompatibilityCheckOptions
{
/// <summary>
/// Default options for production use. Requires version attribute and enforces strict major version checking.
/// </summary>
public static CompatibilityCheckOptions Default { get; } = new()
{
RequireVersionAttribute = true,
StrictMajorVersionCheck = true
};
/// <summary>
/// Lenient options for backward compatibility. Does not require version attribute, no strict major version check.
/// </summary>
public static CompatibilityCheckOptions Lenient { get; } = new()
{
RequireVersionAttribute = false,
StrictMajorVersionCheck = false
};
/// <summary>
/// Whether plugins must have a <see cref="StellaPluginVersionAttribute"/>.
/// When true, plugins without the attribute will be rejected.
/// Default: true.
/// </summary>
public bool RequireVersionAttribute { get; init; } = true;
/// <summary>
/// Whether to enforce strict major version boundary checking.
/// When true and MaximumHostVersion is not specified, the checker assumes
/// the plugin only supports host versions with the same major version.
/// Default: true.
/// </summary>
public bool StrictMajorVersionCheck { get; init; } = true;
}
/// <summary>
/// Represents the result of a plugin compatibility check.
/// </summary>
/// <param name="IsCompatible">True if the plugin is compatible with the host.</param>
/// <param name="PluginVersion">The plugin's declared version, if available.</param>
/// <param name="MinimumHostVersion">The minimum host version required, if specified.</param>
/// <param name="MaximumHostVersion">The maximum host version supported, if specified.</param>
/// <param name="RequiresSignature">Whether the plugin requires signature verification.</param>
/// <param name="FailureReason">Description of why compatibility check failed, if applicable.</param>
/// <param name="HasVersionAttribute">True if the plugin has a StellaPluginVersion attribute.</param>
public sealed record PluginCompatibilityResult(
bool IsCompatible,
Version? PluginVersion,
Version? MinimumHostVersion,
Version? MaximumHostVersion,
bool RequiresSignature,
string? FailureReason,
bool HasVersionAttribute);

View File

@@ -0,0 +1,76 @@
using System;
namespace StellaOps.Plugin.Versioning;
/// <summary>
/// Declares the minimum host version required by this plugin assembly.
/// The plugin host uses this attribute to verify compatibility before loading.
/// </summary>
/// <remarks>
/// Apply this attribute at the assembly level in your plugin project:
/// <code>
/// [assembly: StellaPluginVersion("1.0.0", MinimumHostVersion = "1.0.0")]
/// </code>
/// </remarks>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)]
public sealed class StellaPluginVersionAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="StellaPluginVersionAttribute"/> class.
/// </summary>
/// <param name="pluginVersion">The semantic version of this plugin (e.g., "1.2.3").</param>
public StellaPluginVersionAttribute(string pluginVersion)
{
PluginVersion = ParseVersion(pluginVersion, nameof(pluginVersion));
}
/// <summary>
/// Gets the semantic version of this plugin.
/// </summary>
public Version PluginVersion { get; }
/// <summary>
/// Gets or sets the minimum host version required for this plugin to function.
/// When specified, the plugin host will reject loading if the host version is below this value.
/// </summary>
public string? MinimumHostVersion { get; set; }
/// <summary>
/// Gets or sets the maximum host version supported by this plugin.
/// When specified, the plugin host will reject loading if the host version exceeds this value.
/// </summary>
public string? MaximumHostVersion { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this plugin requires signature verification.
/// Defaults to true. Set to false only for development/testing scenarios.
/// </summary>
public bool RequiresSignature { get; set; } = true;
/// <summary>
/// Gets the parsed minimum host version, or null if not specified.
/// </summary>
public Version? GetMinimumHostVersion() =>
string.IsNullOrWhiteSpace(MinimumHostVersion) ? null : ParseVersion(MinimumHostVersion, nameof(MinimumHostVersion));
/// <summary>
/// Gets the parsed maximum host version, or null if not specified.
/// </summary>
public Version? GetMaximumHostVersion() =>
string.IsNullOrWhiteSpace(MaximumHostVersion) ? null : ParseVersion(MaximumHostVersion, nameof(MaximumHostVersion));
private static Version ParseVersion(string version, string parameterName)
{
if (string.IsNullOrWhiteSpace(version))
{
throw new ArgumentException("Version string cannot be null or empty.", parameterName);
}
if (!Version.TryParse(version, out var parsed))
{
throw new ArgumentException($"Invalid version format: '{version}'. Expected format: Major.Minor[.Build[.Revision]]", parameterName);
}
return parsed;
}
}