stabilizaiton work - projects rework for maintenanceability and ui livening
This commit is contained in:
@@ -9,7 +9,7 @@ Own deterministic canonical JSON serialization and hashing for content-addressed
|
||||
- Surface open work on `TASKS.md`; update statuses (TODO/DOING/DONE/BLOCKED/REVIEW).
|
||||
|
||||
## Key Paths
|
||||
- `CanonJson.cs`
|
||||
- `CanonJson*.cs`
|
||||
- `CanonVersion.cs`
|
||||
- `README.md`
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Canonical.Json;
|
||||
|
||||
public static partial class CanonJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Canonicalizes an object to a deterministic byte array.
|
||||
/// Object keys are recursively sorted using Ordinal comparison.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to serialize.</typeparam>
|
||||
/// <param name="obj">The object to canonicalize.</param>
|
||||
/// <returns>UTF-8 encoded canonical JSON bytes.</returns>
|
||||
public static byte[] Canonicalize<T>(T obj)
|
||||
{
|
||||
var json = JsonSerializer.SerializeToUtf8Bytes(obj, _defaultSerializerOptions);
|
||||
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
using var ms = new MemoryStream();
|
||||
using var writer = new Utf8JsonWriter(ms, _defaultWriterOptions);
|
||||
|
||||
WriteElementSorted(doc.RootElement, writer);
|
||||
writer.Flush();
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Canonicalizes an object using custom serializer options.
|
||||
/// Object keys are recursively sorted using Ordinal comparison.
|
||||
/// </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>
|
||||
/// <returns>UTF-8 encoded canonical JSON bytes.</returns>
|
||||
public static byte[] Canonicalize<T>(T obj, JsonSerializerOptions options)
|
||||
{
|
||||
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));
|
||||
|
||||
WriteElementSorted(doc.RootElement, writer);
|
||||
writer.Flush();
|
||||
return ms.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Canonical.Json;
|
||||
|
||||
public static partial class CanonJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Canonicalizes raw JSON bytes.
|
||||
/// </summary>
|
||||
/// <param name="jsonBytes">UTF-8 encoded JSON bytes.</param>
|
||||
/// <returns>UTF-8 encoded canonical JSON bytes.</returns>
|
||||
public static byte[] CanonicalizeParsedJson(ReadOnlySpan<byte> jsonBytes)
|
||||
{
|
||||
var reader = new Utf8JsonReader(jsonBytes, isFinalBlock: true, state: default);
|
||||
using var doc = JsonDocument.ParseValue(ref reader);
|
||||
using var ms = new MemoryStream();
|
||||
using var writer = new Utf8JsonWriter(ms, _defaultWriterOptions);
|
||||
|
||||
WriteElementSorted(doc.RootElement, writer);
|
||||
writer.Flush();
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Canonicalizes raw JSON bytes using a custom encoder for output.
|
||||
/// </summary>
|
||||
/// <param name="jsonBytes">UTF-8 encoded JSON bytes.</param>
|
||||
/// <param name="encoder">Encoder to use for output escaping.</param>
|
||||
/// <returns>UTF-8 encoded canonical JSON bytes.</returns>
|
||||
public static byte[] CanonicalizeParsedJson(ReadOnlySpan<byte> jsonBytes, JavaScriptEncoder encoder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(encoder);
|
||||
|
||||
var reader = new Utf8JsonReader(jsonBytes, isFinalBlock: true, state: default);
|
||||
using var doc = JsonDocument.ParseValue(ref reader);
|
||||
using var ms = new MemoryStream();
|
||||
using var writer = new Utf8JsonWriter(ms, new JsonWriterOptions
|
||||
{
|
||||
Indented = false,
|
||||
Encoder = encoder
|
||||
});
|
||||
|
||||
WriteElementSorted(doc.RootElement, writer);
|
||||
writer.Flush();
|
||||
return ms.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace StellaOps.Canonical.Json;
|
||||
|
||||
public static partial class CanonJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes SHA-256 hash of bytes, returns lowercase hex string.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The bytes to hash.</param>
|
||||
/// <returns>64-character lowercase hex string.</returns>
|
||||
public static string Sha256Hex(ReadOnlySpan<byte> bytes)
|
||||
=> Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();
|
||||
|
||||
/// <summary>
|
||||
/// Computes SHA-256 hash of bytes, returns prefixed hash string.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The bytes to hash.</param>
|
||||
/// <returns>Hash string with "sha256:" prefix.</returns>
|
||||
public static string Sha256Prefixed(ReadOnlySpan<byte> bytes)
|
||||
=> "sha256:" + Sha256Hex(bytes);
|
||||
|
||||
/// <summary>
|
||||
/// Canonicalizes an object and computes its SHA-256 hash.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to serialize.</typeparam>
|
||||
/// <param name="obj">The object to hash.</param>
|
||||
/// <returns>64-character lowercase hex string.</returns>
|
||||
public static string Hash<T>(T obj)
|
||||
{
|
||||
var canonical = Canonicalize(obj);
|
||||
return Sha256Hex(canonical);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Canonicalizes an object and computes its prefixed SHA-256 hash.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to serialize.</typeparam>
|
||||
/// <param name="obj">The object to hash.</param>
|
||||
/// <returns>Hash string with "sha256:" prefix.</returns>
|
||||
public static string HashPrefixed<T>(T obj)
|
||||
{
|
||||
var canonical = Canonicalize(obj);
|
||||
return Sha256Prefixed(canonical);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes SHA-256 hash of versioned canonical representation.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to serialize.</typeparam>
|
||||
/// <param name="obj">The object to hash.</param>
|
||||
/// <param name="version">Canonicalization version (default: Current).</param>
|
||||
/// <returns>64-character lowercase hex string.</returns>
|
||||
public static string HashVersioned<T>(T obj, string version = CanonVersion.Current)
|
||||
{
|
||||
var canonical = CanonicalizeVersioned(obj, version);
|
||||
return Sha256Hex(canonical);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes prefixed SHA-256 hash of versioned canonical representation.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to serialize.</typeparam>
|
||||
/// <param name="obj">The object to hash.</param>
|
||||
/// <param name="version">Canonicalization version (default: Current).</param>
|
||||
/// <returns>Hash string with "sha256:" prefix.</returns>
|
||||
public static string HashVersionedPrefixed<T>(T obj, string version = CanonVersion.Current)
|
||||
{
|
||||
var canonical = CanonicalizeVersioned(obj, version);
|
||||
return Sha256Prefixed(canonical);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Canonical.Json;
|
||||
|
||||
public static partial class CanonJson
|
||||
{
|
||||
private static readonly JsonSerializerOptions _defaultSerializerOptions = new()
|
||||
{
|
||||
WriteIndented = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
|
||||
private static readonly JsonWriterOptions _defaultWriterOptions = new()
|
||||
{
|
||||
Indented = false,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
|
||||
private static JsonWriterOptions CreateWriterOptions(JsonSerializerOptions? options)
|
||||
{
|
||||
var encoder = options?.Encoder ?? _defaultWriterOptions.Encoder;
|
||||
return new JsonWriterOptions
|
||||
{
|
||||
Indented = false,
|
||||
Encoder = encoder
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
76
src/__Libraries/StellaOps.Canonical.Json/CanonJson.Writer.cs
Normal file
76
src/__Libraries/StellaOps.Canonical.Json/CanonJson.Writer.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Canonical.Json;
|
||||
|
||||
public static partial class CanonJson
|
||||
{
|
||||
private static void WriteElementSorted(JsonElement el, Utf8JsonWriter w)
|
||||
{
|
||||
switch (el.ValueKind)
|
||||
{
|
||||
case JsonValueKind.Object:
|
||||
w.WriteStartObject();
|
||||
foreach (var prop in el.EnumerateObject()
|
||||
.OrderBy(p => p.Name.IsNormalized(NormalizationForm.FormC) ? p.Name : p.Name.Normalize(NormalizationForm.FormC),
|
||||
StringComparer.Ordinal))
|
||||
{
|
||||
var propName = prop.Name;
|
||||
if (!propName.IsNormalized(NormalizationForm.FormC))
|
||||
{
|
||||
propName = propName.Normalize(NormalizationForm.FormC);
|
||||
}
|
||||
w.WritePropertyName(propName);
|
||||
WriteElementSorted(prop.Value, w);
|
||||
}
|
||||
w.WriteEndObject();
|
||||
break;
|
||||
|
||||
case JsonValueKind.Array:
|
||||
w.WriteStartArray();
|
||||
foreach (var item in el.EnumerateArray())
|
||||
{
|
||||
WriteElementSorted(item, w);
|
||||
}
|
||||
w.WriteEndArray();
|
||||
break;
|
||||
|
||||
case JsonValueKind.Number:
|
||||
if (TryWriteNormalizedNumber(el, w))
|
||||
{
|
||||
break;
|
||||
}
|
||||
el.WriteTo(w);
|
||||
break;
|
||||
|
||||
case JsonValueKind.String:
|
||||
var str = el.GetString()!;
|
||||
if (!str.IsNormalized(NormalizationForm.FormC))
|
||||
{
|
||||
str = str.Normalize(NormalizationForm.FormC);
|
||||
}
|
||||
w.WriteStringValue(str);
|
||||
break;
|
||||
|
||||
default:
|
||||
el.WriteTo(w);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryWriteNormalizedNumber(JsonElement element, Utf8JsonWriter writer)
|
||||
{
|
||||
if (element.TryGetDouble(out var doubleValue))
|
||||
{
|
||||
var bits = BitConverter.DoubleToInt64Bits(doubleValue);
|
||||
if (bits == unchecked((long)0x8000000000000000))
|
||||
{
|
||||
writer.WriteNumberValue(0);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Canonical.Json;
|
||||
@@ -20,21 +16,8 @@ namespace StellaOps.Canonical.Json;
|
||||
/// <item>UTF-8 encoding without BOM</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public static class CanonJson
|
||||
public static partial class CanonJson
|
||||
{
|
||||
private static readonly JsonSerializerOptions DefaultSerializerOptions = new()
|
||||
{
|
||||
WriteIndented = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
|
||||
private static readonly JsonWriterOptions DefaultWriterOptions = new()
|
||||
{
|
||||
Indented = false,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Serializes an object to a canonical JSON string.
|
||||
/// Object keys are recursively sorted using Ordinal comparison.
|
||||
@@ -61,310 +44,4 @@ public static class CanonJson
|
||||
var bytes = Canonicalize(obj, options);
|
||||
return Encoding.UTF8.GetString(bytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Canonicalizes an object to a deterministic byte array.
|
||||
/// Object keys are recursively sorted using Ordinal comparison.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to serialize.</typeparam>
|
||||
/// <param name="obj">The object to canonicalize.</param>
|
||||
/// <returns>UTF-8 encoded canonical JSON bytes.</returns>
|
||||
public static byte[] Canonicalize<T>(T obj)
|
||||
{
|
||||
var json = JsonSerializer.SerializeToUtf8Bytes(obj, DefaultSerializerOptions);
|
||||
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
using var ms = new MemoryStream();
|
||||
using var writer = new Utf8JsonWriter(ms, DefaultWriterOptions);
|
||||
|
||||
WriteElementSorted(doc.RootElement, writer);
|
||||
writer.Flush();
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Canonicalizes an object using custom serializer options.
|
||||
/// Object keys are recursively sorted using Ordinal comparison.
|
||||
/// </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>
|
||||
/// <returns>UTF-8 encoded canonical JSON bytes.</returns>
|
||||
public static byte[] Canonicalize<T>(T obj, JsonSerializerOptions options)
|
||||
{
|
||||
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));
|
||||
|
||||
WriteElementSorted(doc.RootElement, writer);
|
||||
writer.Flush();
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Canonicalizes raw JSON bytes by parsing and re-sorting keys.
|
||||
/// Use this when you have existing JSON that needs to be canonicalized.
|
||||
/// </summary>
|
||||
/// <param name="jsonBytes">UTF-8 encoded JSON bytes.</param>
|
||||
/// <returns>UTF-8 encoded canonical JSON bytes.</returns>
|
||||
public static byte[] CanonicalizeParsedJson(ReadOnlySpan<byte> jsonBytes)
|
||||
{
|
||||
var reader = new Utf8JsonReader(jsonBytes, isFinalBlock: true, state: default);
|
||||
using var doc = JsonDocument.ParseValue(ref reader);
|
||||
using var ms = new MemoryStream();
|
||||
using var writer = new Utf8JsonWriter(ms, DefaultWriterOptions);
|
||||
|
||||
WriteElementSorted(doc.RootElement, writer);
|
||||
writer.Flush();
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Canonicalizes raw JSON bytes using a custom encoder for output.
|
||||
/// </summary>
|
||||
/// <param name="jsonBytes">UTF-8 encoded JSON bytes.</param>
|
||||
/// <param name="encoder">Encoder to use for output escaping.</param>
|
||||
/// <returns>UTF-8 encoded canonical JSON bytes.</returns>
|
||||
public static byte[] CanonicalizeParsedJson(ReadOnlySpan<byte> jsonBytes, JavaScriptEncoder encoder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(encoder);
|
||||
|
||||
var reader = new Utf8JsonReader(jsonBytes, isFinalBlock: true, state: default);
|
||||
using var doc = JsonDocument.ParseValue(ref reader);
|
||||
using var ms = new MemoryStream();
|
||||
using var writer = new Utf8JsonWriter(ms, new JsonWriterOptions
|
||||
{
|
||||
Indented = false,
|
||||
Encoder = encoder
|
||||
});
|
||||
|
||||
WriteElementSorted(doc.RootElement, writer);
|
||||
writer.Flush();
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
private static JsonWriterOptions CreateWriterOptions(JsonSerializerOptions? options)
|
||||
{
|
||||
var encoder = options?.Encoder ?? DefaultWriterOptions.Encoder;
|
||||
return new JsonWriterOptions
|
||||
{
|
||||
Indented = false,
|
||||
Encoder = encoder
|
||||
};
|
||||
}
|
||||
|
||||
private static void WriteElementSorted(JsonElement el, Utf8JsonWriter w)
|
||||
{
|
||||
switch (el.ValueKind)
|
||||
{
|
||||
case JsonValueKind.Object:
|
||||
w.WriteStartObject();
|
||||
foreach (var prop in el.EnumerateObject()
|
||||
.OrderBy(p => p.Name.IsNormalized(NormalizationForm.FormC) ? p.Name : p.Name.Normalize(NormalizationForm.FormC), StringComparer.Ordinal))
|
||||
{
|
||||
var propName = prop.Name;
|
||||
if (!propName.IsNormalized(NormalizationForm.FormC))
|
||||
{
|
||||
propName = propName.Normalize(NormalizationForm.FormC);
|
||||
}
|
||||
w.WritePropertyName(propName);
|
||||
WriteElementSorted(prop.Value, w);
|
||||
}
|
||||
w.WriteEndObject();
|
||||
break;
|
||||
|
||||
case JsonValueKind.Array:
|
||||
w.WriteStartArray();
|
||||
foreach (var item in el.EnumerateArray())
|
||||
{
|
||||
WriteElementSorted(item, w);
|
||||
}
|
||||
w.WriteEndArray();
|
||||
break;
|
||||
|
||||
case JsonValueKind.Number:
|
||||
if (TryWriteNormalizedNumber(el, w))
|
||||
{
|
||||
break;
|
||||
}
|
||||
el.WriteTo(w);
|
||||
break;
|
||||
|
||||
case JsonValueKind.String:
|
||||
var str = el.GetString()!;
|
||||
if (!str.IsNormalized(NormalizationForm.FormC))
|
||||
{
|
||||
str = str.Normalize(NormalizationForm.FormC);
|
||||
}
|
||||
w.WriteStringValue(str);
|
||||
break;
|
||||
|
||||
default:
|
||||
el.WriteTo(w);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryWriteNormalizedNumber(JsonElement element, Utf8JsonWriter writer)
|
||||
{
|
||||
if (element.TryGetDouble(out var doubleValue))
|
||||
{
|
||||
var bits = BitConverter.DoubleToInt64Bits(doubleValue);
|
||||
if (bits == unchecked((long)0x8000000000000000))
|
||||
{
|
||||
writer.WriteNumberValue(0);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes SHA-256 hash of bytes, returns lowercase hex string.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The bytes to hash.</param>
|
||||
/// <returns>64-character lowercase hex string.</returns>
|
||||
public static string Sha256Hex(ReadOnlySpan<byte> bytes)
|
||||
=> Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();
|
||||
|
||||
/// <summary>
|
||||
/// Computes SHA-256 hash of bytes, returns prefixed hash string.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The bytes to hash.</param>
|
||||
/// <returns>Hash string with "sha256:" prefix.</returns>
|
||||
public static string Sha256Prefixed(ReadOnlySpan<byte> bytes)
|
||||
=> "sha256:" + Sha256Hex(bytes);
|
||||
|
||||
/// <summary>
|
||||
/// Canonicalizes an object and computes its SHA-256 hash.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to serialize.</typeparam>
|
||||
/// <param name="obj">The object to hash.</param>
|
||||
/// <returns>64-character lowercase hex string.</returns>
|
||||
public static string Hash<T>(T obj)
|
||||
{
|
||||
var canonical = Canonicalize(obj);
|
||||
return Sha256Hex(canonical);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Canonicalizes an object and computes its prefixed SHA-256 hash.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to serialize.</typeparam>
|
||||
/// <param name="obj">The object to hash.</param>
|
||||
/// <returns>Hash string with "sha256:" prefix.</returns>
|
||||
public static string HashPrefixed<T>(T obj)
|
||||
{
|
||||
var canonical = Canonicalize(obj);
|
||||
return Sha256Prefixed(canonical);
|
||||
}
|
||||
|
||||
/// <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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes SHA-256 hash of versioned canonical representation.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to serialize.</typeparam>
|
||||
/// <param name="obj">The object to hash.</param>
|
||||
/// <param name="version">Canonicalization version (default: Current).</param>
|
||||
/// <returns>64-character lowercase hex string.</returns>
|
||||
public static string HashVersioned<T>(T obj, string version = CanonVersion.Current)
|
||||
{
|
||||
var canonical = CanonicalizeVersioned(obj, version);
|
||||
return Sha256Hex(canonical);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes prefixed SHA-256 hash of versioned canonical representation.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to serialize.</typeparam>
|
||||
/// <param name="obj">The object to hash.</param>
|
||||
/// <param name="version">Canonicalization version (default: Current).</param>
|
||||
/// <returns>Hash string with "sha256:" prefix.</returns>
|
||||
public static string HashVersionedPrefixed<T>(T obj, string version = CanonVersion.Current)
|
||||
{
|
||||
var canonical = CanonicalizeVersioned(obj, version);
|
||||
return Sha256Prefixed(canonical);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Canonical Json Task Board
|
||||
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
|
||||
Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_solid_review.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
@@ -9,3 +9,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| AUDIT-0047-T | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0047-A | TODO | Requires MAINT/TEST + approval. |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
| REMED-2026-02-03 | DONE | Split CanonJson into partials <=100 lines; usings moved outside namespace; private fields renamed; `dotnet test src/__Libraries/StellaOps.Canonical.Json.Tests/StellaOps.Canonical.Json.Tests.csproj` passed (61 tests). |
|
||||
|
||||
Reference in New Issue
Block a user