176 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			176 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
using System.Collections.Generic;
 | 
						|
using System.Linq;
 | 
						|
using System.Text.Encodings.Web;
 | 
						|
using System.Text.Json;
 | 
						|
using System.Text.Json.Serialization;
 | 
						|
using System.Text.Json.Serialization.Metadata;
 | 
						|
 | 
						|
namespace StellaOps.Feedser.Models;
 | 
						|
 | 
						|
/// <summary>
 | 
						|
/// Deterministic JSON serializer tuned for canonical advisory output.
 | 
						|
/// </summary>
 | 
						|
public static class CanonicalJsonSerializer
 | 
						|
{
 | 
						|
    private static readonly JsonSerializerOptions CompactOptions = CreateOptions(writeIndented: false);
 | 
						|
    private static readonly JsonSerializerOptions PrettyOptions = CreateOptions(writeIndented: true);
 | 
						|
 | 
						|
    private static readonly IReadOnlyDictionary<Type, string[]> PropertyOrderOverrides = new Dictionary<Type, string[]>
 | 
						|
    {
 | 
						|
        {
 | 
						|
            typeof(AdvisoryProvenance),
 | 
						|
            new[]
 | 
						|
            {
 | 
						|
                "source",
 | 
						|
                "kind",
 | 
						|
                "value",
 | 
						|
                "decisionReason",
 | 
						|
                "recordedAt",
 | 
						|
                "fieldMask",
 | 
						|
            }
 | 
						|
        },
 | 
						|
        {
 | 
						|
            typeof(AffectedPackage),
 | 
						|
            new[]
 | 
						|
            {
 | 
						|
                "type",
 | 
						|
                "identifier",
 | 
						|
                "platform",
 | 
						|
                "versionRanges",
 | 
						|
                "normalizedVersions",
 | 
						|
                "statuses",
 | 
						|
                "provenance",
 | 
						|
            }
 | 
						|
        },
 | 
						|
        {
 | 
						|
            typeof(AdvisoryCredit),
 | 
						|
            new[]
 | 
						|
            {
 | 
						|
                "displayName",
 | 
						|
                "role",
 | 
						|
                "contacts",
 | 
						|
                "provenance",
 | 
						|
            }
 | 
						|
        },
 | 
						|
        {
 | 
						|
            typeof(NormalizedVersionRule),
 | 
						|
            new[]
 | 
						|
            {
 | 
						|
                "scheme",
 | 
						|
                "type",
 | 
						|
                "min",
 | 
						|
                "minInclusive",
 | 
						|
                "max",
 | 
						|
                "maxInclusive",
 | 
						|
                "value",
 | 
						|
                "notes",
 | 
						|
            }
 | 
						|
        },
 | 
						|
        {
 | 
						|
            typeof(AdvisoryWeakness),
 | 
						|
            new[]
 | 
						|
            {
 | 
						|
                "taxonomy",
 | 
						|
                "identifier",
 | 
						|
                "name",
 | 
						|
                "uri",
 | 
						|
                "provenance",
 | 
						|
            }
 | 
						|
        },
 | 
						|
    };
 | 
						|
 | 
						|
    public static string Serialize<T>(T value)
 | 
						|
        => JsonSerializer.Serialize(value, CompactOptions);
 | 
						|
 | 
						|
    public static string SerializeIndented<T>(T value)
 | 
						|
        => JsonSerializer.Serialize(value, PrettyOptions);
 | 
						|
 | 
						|
    public static Advisory Normalize(Advisory advisory)
 | 
						|
        => new(
 | 
						|
            advisory.AdvisoryKey,
 | 
						|
            advisory.Title,
 | 
						|
            advisory.Summary,
 | 
						|
            advisory.Language,
 | 
						|
            advisory.Published,
 | 
						|
            advisory.Modified,
 | 
						|
            advisory.Severity,
 | 
						|
            advisory.ExploitKnown,
 | 
						|
            advisory.Aliases,
 | 
						|
            advisory.Credits,
 | 
						|
            advisory.References,
 | 
						|
            advisory.AffectedPackages,
 | 
						|
            advisory.CvssMetrics,
 | 
						|
            advisory.Provenance,
 | 
						|
            advisory.Description,
 | 
						|
            advisory.Cwes,
 | 
						|
            advisory.CanonicalMetricId);
 | 
						|
 | 
						|
    public static T Deserialize<T>(string json)
 | 
						|
        => JsonSerializer.Deserialize<T>(json, PrettyOptions)!
 | 
						|
            ?? throw new InvalidOperationException($"Unable to deserialize type {typeof(T).Name}.");
 | 
						|
 | 
						|
    private static JsonSerializerOptions CreateOptions(bool writeIndented)
 | 
						|
    {
 | 
						|
        var options = new JsonSerializerOptions
 | 
						|
        {
 | 
						|
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
 | 
						|
            DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
 | 
						|
            DefaultIgnoreCondition = JsonIgnoreCondition.Never,
 | 
						|
            WriteIndented = writeIndented,
 | 
						|
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
 | 
						|
        };
 | 
						|
 | 
						|
        var baselineResolver = options.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver();
 | 
						|
        options.TypeInfoResolver = new DeterministicTypeInfoResolver(baselineResolver);
 | 
						|
        options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, allowIntegerValues: false));
 | 
						|
        return options;
 | 
						|
    }
 | 
						|
 | 
						|
    private sealed class DeterministicTypeInfoResolver : IJsonTypeInfoResolver
 | 
						|
    {
 | 
						|
        private readonly IJsonTypeInfoResolver _inner;
 | 
						|
 | 
						|
        public DeterministicTypeInfoResolver(IJsonTypeInfoResolver inner)
 | 
						|
        {
 | 
						|
            _inner = inner ?? throw new ArgumentNullException(nameof(inner));
 | 
						|
        }
 | 
						|
 | 
						|
        public JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
 | 
						|
        {
 | 
						|
            var info = _inner.GetTypeInfo(type, options);
 | 
						|
            if (info is null)
 | 
						|
            {
 | 
						|
                throw new InvalidOperationException($"Unable to resolve JsonTypeInfo for '{type}'.");
 | 
						|
            }
 | 
						|
 | 
						|
            if (info.Kind is JsonTypeInfoKind.Object && info.Properties is { Count: > 1 })
 | 
						|
            {
 | 
						|
                var ordered = info.Properties
 | 
						|
                    .OrderBy(property => GetPropertyOrder(type, property.Name))
 | 
						|
                    .ThenBy(property => property.Name, StringComparer.Ordinal)
 | 
						|
                    .ToArray();
 | 
						|
 | 
						|
                info.Properties.Clear();
 | 
						|
                foreach (var property in ordered)
 | 
						|
                {
 | 
						|
                    info.Properties.Add(property);
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            return info;
 | 
						|
        }
 | 
						|
 | 
						|
        private static int GetPropertyOrder(Type type, string propertyName)
 | 
						|
        {
 | 
						|
            if (PropertyOrderOverrides.TryGetValue(type, out var order) &&
 | 
						|
                Array.IndexOf(order, propertyName) is var index &&
 | 
						|
                index >= 0)
 | 
						|
            {
 | 
						|
                return index;
 | 
						|
            }
 | 
						|
 | 
						|
            return int.MaxValue;
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |