using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace AIStudio.Assistants.VisualBriefing;
///
/// Centralizes canonical JSON, structural signatures, and SHA-256 hashes for visual briefings.
///
internal static class VisualBriefingHashing
{
///
/// Computes a lowercase SHA-256 hash for UTF-8 text.
///
/// The text to hash.
/// The lowercase hexadecimal hash.
internal static string Compute(string value) => Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(value)));
///
/// Computes a hash over unambiguously separated text sections.
///
/// The ordered text sections.
/// The lowercase hexadecimal hash.
internal static string ComputeSections(params string?[] values) => Compute(string.Join('\u001e', values.Select(value => value ?? string.Empty)));
///
/// Computes a lowercase SHA-256 hash for a file without loading it fully into memory.
///
/// The file path.
/// The cancellation token.
/// The lowercase hexadecimal hash.
internal static async Task ComputeFileAsync(string path, CancellationToken token)
{
await using var stream = new FileStream(
path,
FileMode.Open,
FileAccess.Read,
FileShare.Read,
65_536,
FileOptions.Asynchronous | FileOptions.SequentialScan);
return Convert.ToHexStringLower(await SHA256.HashDataAsync(stream, token));
}
///
/// Returns canonical JSON for one value, with ordinally sorted object properties.
///
///
/// Hashed values go through here instead of being serialized directly. Plain serialization writes
/// properties in declaration order, which would tie every stored hash to the order in which the
/// members happen to appear in the C# file: reordering two properties would invalidate every
/// briefing already on disk, without any visible change to the data.
///
/// The type of the value to canonicalize.
/// The value to canonicalize.
/// Compact canonical JSON.
internal static string CanonicalJson(T value) => CanonicalJson(JsonSerializer.SerializeToElement(value, VisualBriefingJson.Canonical));
///
/// Returns canonical JSON with ordinally sorted object properties.
///
/// The JSON value to canonicalize.
/// Compact canonical JSON.
internal static string CanonicalJson(JsonElement value)
{
using var stream = new MemoryStream();
using (var writer = new Utf8JsonWriter(stream))
WriteCanonical(writer, value);
return Encoding.UTF8.GetString(stream.ToArray());
}
///
/// Computes the structural signature of canonical business data.
///
/// The JSON value to inspect.
/// A stable hash of its property and collection shape.
internal static string StructuralSignature(JsonElement value)
{
var builder = new StringBuilder();
AppendStructuralSignature(builder, value);
return Compute(builder.ToString());
}
///
/// Writes one JSON value in canonical order.
///
/// The JSON writer.
/// The value to write.
private static void WriteCanonical(Utf8JsonWriter writer, JsonElement value)
{
switch (value.ValueKind)
{
case JsonValueKind.Object:
writer.WriteStartObject();
foreach (var property in value.EnumerateObject().OrderBy(property => property.Name, StringComparer.Ordinal))
{
writer.WritePropertyName(property.Name);
WriteCanonical(writer, property.Value);
}
writer.WriteEndObject();
break;
case JsonValueKind.Array:
writer.WriteStartArray();
foreach (var item in value.EnumerateArray())
WriteCanonical(writer, item);
writer.WriteEndArray();
break;
default:
value.WriteTo(writer);
break;
}
}
///
/// Appends type and property shape without business values.
///
/// The signature builder.
/// The value to inspect.
private static void AppendStructuralSignature(StringBuilder builder, JsonElement value)
{
builder.Append(value.ValueKind);
switch (value.ValueKind)
{
case JsonValueKind.Object:
builder.Append('{');
foreach (var property in value.EnumerateObject().OrderBy(property => property.Name, StringComparer.Ordinal))
{
builder.Append(property.Name).Append(':');
AppendStructuralSignature(builder, property.Value);
}
builder.Append('}');
break;
case JsonValueKind.Array:
builder.Append('[');
var first = value.EnumerateArray().FirstOrDefault();
if (first.ValueKind is not JsonValueKind.Undefined)
AppendStructuralSignature(builder, first);
builder.Append(']');
break;
}
}
}