Refactor code structure for improved readability and maintainability; optimize performance in key functions.
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using StellaOps.AirGap.Bundle.Models;
|
||||
using StellaOps.AirGap.Bundle.Serialization;
|
||||
|
||||
namespace StellaOps.AirGap.Bundle.Validation;
|
||||
|
||||
public sealed class BundleValidator : IBundleValidator
|
||||
{
|
||||
public async Task<BundleValidationResult> ValidateAsync(
|
||||
BundleManifest manifest,
|
||||
string bundlePath,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var errors = new List<BundleValidationError>();
|
||||
var warnings = new List<BundleValidationWarning>();
|
||||
|
||||
if (manifest.Feeds.Length == 0)
|
||||
{
|
||||
errors.Add(new BundleValidationError("Feeds", "At least one feed required"));
|
||||
}
|
||||
|
||||
if (manifest.CryptoMaterials.Length == 0)
|
||||
{
|
||||
errors.Add(new BundleValidationError("CryptoMaterials", "Trust roots required"));
|
||||
}
|
||||
|
||||
foreach (var feed in manifest.Feeds)
|
||||
{
|
||||
var filePath = Path.Combine(bundlePath, feed.RelativePath);
|
||||
var result = await VerifyFileDigestAsync(filePath, feed.Digest, ct).ConfigureAwait(false);
|
||||
if (!result.IsValid)
|
||||
{
|
||||
errors.Add(new BundleValidationError("Feeds",
|
||||
$"Feed {feed.FeedId} digest mismatch: expected {feed.Digest}, got {result.ActualDigest}"));
|
||||
}
|
||||
}
|
||||
|
||||
if (manifest.ExpiresAt.HasValue && manifest.ExpiresAt.Value < DateTimeOffset.UtcNow)
|
||||
{
|
||||
warnings.Add(new BundleValidationWarning("ExpiresAt", "Bundle has expired"));
|
||||
}
|
||||
|
||||
foreach (var feed in manifest.Feeds)
|
||||
{
|
||||
var age = DateTimeOffset.UtcNow - feed.SnapshotAt;
|
||||
if (age.TotalDays > 7)
|
||||
{
|
||||
warnings.Add(new BundleValidationWarning("Feeds",
|
||||
$"Feed {feed.FeedId} is {age.TotalDays:F0} days old"));
|
||||
}
|
||||
}
|
||||
|
||||
if (manifest.BundleDigest is not null)
|
||||
{
|
||||
var computed = ComputeBundleDigest(manifest);
|
||||
if (!string.Equals(computed, manifest.BundleDigest, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
errors.Add(new BundleValidationError("BundleDigest", "Bundle digest mismatch"));
|
||||
}
|
||||
}
|
||||
|
||||
return new BundleValidationResult(
|
||||
errors.Count == 0,
|
||||
errors,
|
||||
warnings,
|
||||
manifest.TotalSizeBytes);
|
||||
}
|
||||
|
||||
private static async Task<(bool IsValid, string ActualDigest)> VerifyFileDigestAsync(
|
||||
string filePath, string expectedDigest, CancellationToken ct)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
return (false, "FILE_NOT_FOUND");
|
||||
}
|
||||
|
||||
await using var stream = File.OpenRead(filePath);
|
||||
var hash = await SHA256.HashDataAsync(stream, ct).ConfigureAwait(false);
|
||||
var actualDigest = Convert.ToHexString(hash).ToLowerInvariant();
|
||||
return (string.Equals(actualDigest, expectedDigest, StringComparison.OrdinalIgnoreCase), actualDigest);
|
||||
}
|
||||
|
||||
private static string ComputeBundleDigest(BundleManifest manifest)
|
||||
{
|
||||
var withoutDigest = manifest with { BundleDigest = null };
|
||||
var json = BundleManifestSerializer.Serialize(withoutDigest);
|
||||
return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(json))).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
public interface IBundleValidator
|
||||
{
|
||||
Task<BundleValidationResult> ValidateAsync(BundleManifest manifest, string bundlePath, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public sealed record BundleValidationResult(
|
||||
bool IsValid,
|
||||
IReadOnlyList<BundleValidationError> Errors,
|
||||
IReadOnlyList<BundleValidationWarning> Warnings,
|
||||
long TotalSizeBytes);
|
||||
|
||||
public sealed record BundleValidationError(string Component, string Message);
|
||||
public sealed record BundleValidationWarning(string Component, string Message);
|
||||
Reference in New Issue
Block a user