using System.Collections.Generic; using System.Formats.Cbor; using System.Text; using System.Text.Json; namespace StellaOps.Scanner.WebService.Serialization; internal static class DeterministicCborSerializer { private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web) { WriteIndented = false }; public static byte[] Serialize(T value) { using var document = JsonSerializer.SerializeToDocument(value, JsonOptions); var writer = new CborWriter(CborConformanceMode.Canonical); WriteElement(writer, document.RootElement); return writer.Encode(); } private static void WriteElement(CborWriter writer, JsonElement element) { switch (element.ValueKind) { case JsonValueKind.Object: WriteObject(writer, element); return; case JsonValueKind.Array: writer.WriteStartArray(element.GetArrayLength()); foreach (var item in element.EnumerateArray()) { WriteElement(writer, item); } writer.WriteEndArray(); return; case JsonValueKind.String: writer.WriteTextString(element.GetString() ?? string.Empty); return; case JsonValueKind.Number: if (element.TryGetInt64(out var int64)) { writer.WriteInt64(int64); return; } writer.WriteDouble(element.GetDouble()); return; case JsonValueKind.True: writer.WriteBoolean(true); return; case JsonValueKind.False: writer.WriteBoolean(false); return; case JsonValueKind.Null: case JsonValueKind.Undefined: writer.WriteNull(); return; default: writer.WriteNull(); return; } } private static void WriteObject(CborWriter writer, JsonElement element) { var properties = new List<(byte[] KeyBytes, string Key, JsonElement Value)>(); foreach (var property in element.EnumerateObject()) { var keyBytes = Encoding.UTF8.GetBytes(property.Name); properties.Add((keyBytes, property.Name, property.Value)); } properties.Sort(static (left, right) => { var lengthCompare = left.KeyBytes.Length.CompareTo(right.KeyBytes.Length); if (lengthCompare != 0) { return lengthCompare; } for (var i = 0; i < left.KeyBytes.Length; i++) { var byteCompare = left.KeyBytes[i].CompareTo(right.KeyBytes[i]); if (byteCompare != 0) { return byteCompare; } } return 0; }); writer.WriteStartMap(properties.Count); foreach (var (_, key, value) in properties) { writer.WriteTextString(key); WriteElement(writer, value); } writer.WriteEndMap(); } }