Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
// Licensed to StellaOps under the AGPL-3.0-or-later license.
|
||||
|
||||
using Blake3;
|
||||
using StellaOps.ReachGraph.Schema;
|
||||
using StellaOps.ReachGraph.Serialization;
|
||||
|
||||
namespace StellaOps.ReachGraph.Hashing;
|
||||
|
||||
/// <summary>
|
||||
/// Computes BLAKE3-256 digests for reachability graphs using canonical serialization.
|
||||
/// </summary>
|
||||
public sealed class ReachGraphDigestComputer
|
||||
{
|
||||
private readonly CanonicalReachGraphSerializer _serializer;
|
||||
|
||||
public ReachGraphDigestComputer()
|
||||
: this(new CanonicalReachGraphSerializer())
|
||||
{
|
||||
}
|
||||
|
||||
public ReachGraphDigestComputer(CanonicalReachGraphSerializer serializer)
|
||||
{
|
||||
_serializer = serializer ?? throw new ArgumentNullException(nameof(serializer));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compute BLAKE3-256 digest of canonical JSON (excluding signatures).
|
||||
/// </summary>
|
||||
/// <param name="graph">The reachability graph to hash.</param>
|
||||
/// <returns>Digest in format "blake3:{hex}".</returns>
|
||||
public string ComputeDigest(ReachGraphMinimal graph)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(graph);
|
||||
|
||||
// Remove signatures before hashing (avoid circular dependency)
|
||||
var unsigned = graph with { Signatures = null };
|
||||
var canonical = _serializer.SerializeMinimal(unsigned);
|
||||
|
||||
using var hasher = Hasher.New();
|
||||
hasher.Update(canonical);
|
||||
var hash = hasher.Finalize();
|
||||
|
||||
return $"blake3:{Convert.ToHexString(hash.AsSpan()).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compute BLAKE3-256 digest from raw canonical JSON bytes.
|
||||
/// </summary>
|
||||
/// <param name="canonicalJson">The canonical JSON bytes to hash.</param>
|
||||
/// <returns>Digest in format "blake3:{hex}".</returns>
|
||||
public static string ComputeDigest(ReadOnlySpan<byte> canonicalJson)
|
||||
{
|
||||
using var hasher = Hasher.New();
|
||||
hasher.Update(canonicalJson);
|
||||
var hash = hasher.Finalize();
|
||||
|
||||
return $"blake3:{Convert.ToHexString(hash.AsSpan()).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify digest matches graph content.
|
||||
/// </summary>
|
||||
/// <param name="graph">The reachability graph to verify.</param>
|
||||
/// <param name="expectedDigest">The expected digest.</param>
|
||||
/// <returns>True if digest matches, false otherwise.</returns>
|
||||
public bool VerifyDigest(ReachGraphMinimal graph, string expectedDigest)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(graph);
|
||||
ArgumentException.ThrowIfNullOrEmpty(expectedDigest);
|
||||
|
||||
var computed = ComputeDigest(graph);
|
||||
return string.Equals(computed, expectedDigest, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a digest string into its algorithm and hash components.
|
||||
/// </summary>
|
||||
/// <param name="digest">The digest string (e.g., "blake3:abc123...").</param>
|
||||
/// <returns>Tuple of (algorithm, hash) or null if invalid format.</returns>
|
||||
public static (string Algorithm, string Hash)? ParseDigest(string digest)
|
||||
{
|
||||
if (string.IsNullOrEmpty(digest))
|
||||
return null;
|
||||
|
||||
var colonIndex = digest.IndexOf(':');
|
||||
if (colonIndex <= 0 || colonIndex >= digest.Length - 1)
|
||||
return null;
|
||||
|
||||
var algorithm = digest[..colonIndex];
|
||||
var hash = digest[(colonIndex + 1)..];
|
||||
|
||||
return (algorithm, hash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate that a digest string has the correct format for BLAKE3.
|
||||
/// </summary>
|
||||
/// <param name="digest">The digest string to validate.</param>
|
||||
/// <returns>True if valid BLAKE3 digest format, false otherwise.</returns>
|
||||
public static bool IsValidBlake3Digest(string digest)
|
||||
{
|
||||
var parsed = ParseDigest(digest);
|
||||
if (parsed is null)
|
||||
return false;
|
||||
|
||||
var (algorithm, hash) = parsed.Value;
|
||||
|
||||
// BLAKE3-256 produces 64 hex characters (32 bytes)
|
||||
return string.Equals(algorithm, "blake3", StringComparison.OrdinalIgnoreCase) &&
|
||||
hash.Length == 64 &&
|
||||
hash.All(c => char.IsAsciiHexDigit(c));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user