using Microsoft.Extensions.Logging; using StellaOps.Canonicalization.Json; using StellaOps.Replay.Engine; using StellaOps.Testing.Manifests.Models; using System.Security.Cryptography; using System.Text; namespace StellaOps.Replay.Loaders; public sealed class FeedSnapshotLoader : IFeedLoader { private readonly IFeedStorage _storage; private readonly ILogger _logger; public FeedSnapshotLoader(IFeedStorage storage, ILogger logger) { _storage = storage; _logger = logger; } public async Task LoadByDigestAsync(string digest, CancellationToken ct = default) { _logger.LogDebug("Loading feed snapshot with digest {Digest}", digest); var localPath = GetLocalPath(digest); if (File.Exists(localPath)) { var feed = await LoadFromFileAsync(localPath, ct).ConfigureAwait(false); VerifyDigest(feed, digest); return feed; } var storedFeed = await _storage.GetByDigestAsync(digest, ct).ConfigureAwait(false); if (storedFeed is not null) { VerifyDigest(storedFeed, digest); return storedFeed; } throw new FeedNotFoundException($"Feed snapshot not found: {digest}"); } private static void VerifyDigest(FeedSnapshot feed, string expected) { var actual = ComputeDigest(feed); if (!string.Equals(actual, expected, StringComparison.OrdinalIgnoreCase)) { throw new DigestMismatchException($"Feed digest mismatch: expected {expected}, got {actual}"); } } private static string ComputeDigest(FeedSnapshot feed) { var json = CanonicalJsonSerializer.Serialize(feed); return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(json))).ToLowerInvariant(); } private static string GetLocalPath(string digest) => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "stellaops", "feeds", digest[..2], digest); private static async Task LoadFromFileAsync(string path, CancellationToken ct) { var json = await File.ReadAllTextAsync(path, ct).ConfigureAwait(false); return CanonicalJsonSerializer.Deserialize(json); } } public interface IFeedStorage { Task GetByDigestAsync(string digest, CancellationToken ct = default); } public sealed class FeedNotFoundException : Exception { public FeedNotFoundException(string message) : base(message) { } } public sealed class DigestMismatchException : Exception { public DigestMismatchException(string message) : base(message) { } }