Files
git.stella-ops.org/src/__Libraries/StellaOps.Canonical.Json/CanonJson.Versioned.cs

87 lines
3.4 KiB
C#

using System.Text.Json;
namespace StellaOps.Canonical.Json;
public static partial class CanonJson
{
/// <summary>
/// Canonicalizes an object with version marker for content-addressed hashing.
/// The version marker is embedded as the first field in the canonical JSON,
/// ensuring stable hashes even if canonicalization logic evolves.
/// </summary>
/// <typeparam name="T">The type to serialize.</typeparam>
/// <param name="obj">The object to canonicalize.</param>
/// <param name="version">Canonicalization version (default: Current).</param>
/// <returns>UTF-8 encoded canonical JSON bytes with version marker.</returns>
public static byte[] CanonicalizeVersioned<T>(T obj, string version = CanonVersion.Current)
{
ArgumentException.ThrowIfNullOrWhiteSpace(version);
var json = JsonSerializer.SerializeToUtf8Bytes(obj, _defaultSerializerOptions);
using var doc = JsonDocument.Parse(json);
using var ms = new MemoryStream();
using var writer = new Utf8JsonWriter(ms, _defaultWriterOptions);
WriteElementVersioned(doc.RootElement, writer, version);
writer.Flush();
return ms.ToArray();
}
/// <summary>
/// Canonicalizes an object with version marker using custom serializer options.
/// </summary>
/// <typeparam name="T">The type to serialize.</typeparam>
/// <param name="obj">The object to canonicalize.</param>
/// <param name="options">JSON serializer options to use for initial serialization.</param>
/// <param name="version">Canonicalization version (default: Current).</param>
/// <returns>UTF-8 encoded canonical JSON bytes with version marker.</returns>
public static byte[] CanonicalizeVersioned<T>(T obj, JsonSerializerOptions options, string version = CanonVersion.Current)
{
ArgumentException.ThrowIfNullOrWhiteSpace(version);
var json = JsonSerializer.SerializeToUtf8Bytes(obj, options);
using var doc = JsonDocument.Parse(json);
using var ms = new MemoryStream();
using var writer = new Utf8JsonWriter(ms, CreateWriterOptions(options));
WriteElementVersioned(doc.RootElement, writer, version);
writer.Flush();
return ms.ToArray();
}
private static void WriteElementVersioned(JsonElement el, Utf8JsonWriter w, string version)
{
if (el.ValueKind == JsonValueKind.Object)
{
w.WriteStartObject();
// Write version marker first (underscore prefix ensures lexicographic first position)
w.WriteString(CanonVersion.VersionFieldName, version);
// Write remaining properties sorted
foreach (var prop in el.EnumerateObject().OrderBy(p => p.Name, StringComparer.Ordinal))
{
if (string.Equals(prop.Name, CanonVersion.VersionFieldName, StringComparison.Ordinal))
{
continue;
}
w.WritePropertyName(prop.Name);
WriteElementSorted(prop.Value, w);
}
w.WriteEndObject();
}
else
{
// Non-object root: wrap in object with version marker
w.WriteStartObject();
w.WriteString(CanonVersion.VersionFieldName, version);
w.WritePropertyName("_value");
WriteElementSorted(el, w);
w.WriteEndObject();
}
}
}