84 lines
2.7 KiB
C#
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) { }
|
|
}
|