Files
git.stella-ops.org/src/__Libraries/StellaOps.Replay/Loaders/FeedSnapshotLoader.cs
2026-02-01 21:37:40 +02:00

84 lines
2.7 KiB
C#

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<FeedSnapshotLoader> _logger;
public FeedSnapshotLoader(IFeedStorage storage, ILogger<FeedSnapshotLoader> logger)
{
_storage = storage;
_logger = logger;
}
public async Task<FeedSnapshot> 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<FeedSnapshot> LoadFromFileAsync(string path, CancellationToken ct)
{
var json = await File.ReadAllTextAsync(path, ct).ConfigureAwait(false);
return CanonicalJsonSerializer.Deserialize<FeedSnapshot>(json);
}
}
public interface IFeedStorage
{
Task<FeedSnapshot?> 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) { }
}