up
This commit is contained in:
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user