File: ContractDescriptorParser.cs
Web Access
Project: src\runtime\src\native\managed\cdac\Microsoft.Diagnostics.DataContractReader\Microsoft.Diagnostics.DataContractReader.csproj (Microsoft.Diagnostics.DataContractReader)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;

namespace Microsoft.Diagnostics.DataContractReader;

/// <summary>
///   A parser for the JSON representation of a contract descriptor.
/// </summary>
/// <remarks>
/// <see href="https://github.com/dotnet/runtime/blob/main/docs/design/datacontracts/data_descriptor.md">See design doc</see> for the format.
/// </remarks>
public partial class ContractDescriptorParser
{
    // data_descriptor.md uses a distinguished property name to indicate the size of a type
    public const string TypeDescriptorSizeSigil = "!";

    private static readonly JsonReaderOptions s_readerOptions = new()
    {
        AllowTrailingCommas = true,
        CommentHandling = JsonCommentHandling.Skip,
    };

    private delegate TValue ReadValue<TValue>(ref Utf8JsonReader reader);

    /// <summary>
    ///  Parses the "compact" representation of a contract descriptor.
    /// </summary>
    public static ContractDescriptor? ParseCompact(ReadOnlySpan<byte> json)
    {
        Utf8JsonReader reader = new(json, s_readerOptions);

        if (!reader.Read())
            throw new JsonException();
        if (reader.TokenType == JsonTokenType.Null)
        {
            if (reader.Read())
                throw new JsonException();
            return null;
        }
        if (reader.TokenType != JsonTokenType.StartObject)
            throw new JsonException();

        ContractDescriptor descriptor = new();
        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndObject)
            {
                if (reader.Read())
                    throw new JsonException();
                return descriptor;
            }
            if (reader.TokenType != JsonTokenType.PropertyName)
                throw new JsonException();

            string propertyName = reader.GetString() ?? throw new JsonException();
            ReadNext(ref reader);
            switch (propertyName)
            {
                case "version":
                    descriptor.Version = ReadNullableInt32(ref reader);
                    break;
                case "baseline":
                    descriptor.Baseline = ReadNullableString(ref reader);
                    break;
                case "contracts":
                    descriptor.Contracts = ReadDictionary(ref reader, ReadString);
                    break;
                case "types":
                    descriptor.Types = ReadDictionary(ref reader, ReadTypeDescriptor);
                    break;
                case "globals":
                    descriptor.Globals = ReadDictionary(ref reader, ReadGlobalDescriptor);
                    break;
                case "subDescriptors":
                    descriptor.SubDescriptors = ReadDictionary(ref reader, ReadGlobalDescriptor);
                    break;
                default:
                    reader.Skip();
                    break;
            }
        }

        throw new JsonException();
    }

    public class ContractDescriptor
    {
        public int? Version { get; set; }
        public string? Baseline { get; set; }
        public Dictionary<string, string>? Contracts { get; set; }

        public Dictionary<string, TypeDescriptor>? Types { get; set; }

        public Dictionary<string, GlobalDescriptor>? Globals { get; set; }

        public Dictionary<string, GlobalDescriptor>? SubDescriptors { get; set; }

        public override string ToString()
        {
            return $"Version: {Version}, Baseline: {Baseline}, Contracts: {Contracts?.Count}, Types: {Types?.Count}, Globals: {Globals?.Count}, SubDescriptors: {SubDescriptors?.Count}";
        }

    }

    public class TypeDescriptor
    {
        public uint? Size { get; set; }
        public Dictionary<string, FieldDescriptor>? Fields { get; set; }
    }

    public class FieldDescriptor
    {
        public string? Type { get; set; }
        public int Offset { get; set; }
    }

    public class GlobalDescriptor
    {
        [MemberNotNullWhen(true, nameof(NumericValue))]
        public bool Indirect { get; set; }
        public string? Type { get; set; }

        // When the descriptor is indirect, NumericValue must be non-null to point to the actual data
        public ulong? NumericValue { get; set; }
        public string? StringValue { get; set; }
    }

    private static string ReadString(ref Utf8JsonReader reader)
    {
        if (reader.TokenType == JsonTokenType.String)
            return reader.GetString() ?? throw new JsonException();
        throw new JsonException();
    }

    private static TypeDescriptor ReadTypeDescriptor(ref Utf8JsonReader reader)
    {
        // Almost a normal dictionary except:
        //  1. "!" specifies the type size.
        //  2. All other property names are case-sensitive field names.
        if (reader.TokenType != JsonTokenType.StartObject)
            throw new JsonException();

        uint? size = null;
        Dictionary<string, FieldDescriptor> fields = new();
        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndObject)
                return new TypeDescriptor { Size = size, Fields = fields };
            if (reader.TokenType != JsonTokenType.PropertyName)
                throw new JsonException();

            string fieldName = reader.GetString() ?? throw new JsonException();
            ReadNext(ref reader);
            if (fieldName == TypeDescriptorSizeSigil)
            {
                uint newSize = reader.GetUInt32();
                if (size is not null)
                    throw new JsonException($"Size specified multiple times: {size} and {newSize}");
                size = newSize;
            }
            else if (!fields.TryAdd(fieldName, ReadFieldDescriptor(ref reader)))
            {
                throw new JsonException($"Duplicate field name: {fieldName}");
            }
        }

        throw new JsonException();
    }

    private static FieldDescriptor ReadFieldDescriptor(ref Utf8JsonReader reader)
    {
        // Compact field descriptors are either:
        //  1. number - no type, offset is given as the number.
        //  2. [number, string] - offset is the number and type name is the string.
        if (TryGetInt32FromToken(ref reader, out int offset))
            return new FieldDescriptor { Offset = offset };
        if (reader.TokenType != JsonTokenType.StartArray)
            throw new JsonException();

        ReadNext(ref reader);
        //   [number, string]
        //    ^ we're here
        if (!TryGetInt32FromToken(ref reader, out offset))
            throw new JsonException();
        ReadNext(ref reader);
        //   [number, string]
        //            ^ we're here
        if (reader.TokenType != JsonTokenType.String)
            throw new JsonException();
        string type = reader.GetString() ?? throw new JsonException();
        ReadNext(ref reader);
        if (reader.TokenType != JsonTokenType.EndArray)
            throw new JsonException();
        return new FieldDescriptor { Type = type, Offset = offset };
    }

    private static Dictionary<string, TValue>? ReadDictionary<TValue>(
        ref Utf8JsonReader reader,
        ReadValue<TValue> readValue)
    {
        if (reader.TokenType == JsonTokenType.Null)
            return null;
        if (reader.TokenType != JsonTokenType.StartObject)
            throw new JsonException();

        Dictionary<string, TValue> values = [];
        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndObject)
                return values;
            if (reader.TokenType != JsonTokenType.PropertyName)
                throw new JsonException();

            string name = reader.GetString() ?? throw new JsonException();
            ReadNext(ref reader);
            values[name] = readValue(ref reader);
        }

        throw new JsonException();
    }

    private static GlobalDescriptor ReadGlobalDescriptor(ref Utf8JsonReader reader)
    {
        // Compact global descriptors have four forms:
        //  1. value - no type, direct value.
        //  2. [value] - no type, indirect value.
        //  3. [value, string] - typed direct value.
        //  4. [[value], string] - typed indirect value.
        // A value can be a string or number. Numeric strings are retained as strings and parsed as numbers.
        if (TryGetGlobalValueFromToken(ref reader, out GlobalValue directValue))
            return CreateGlobalDescriptor(directValue, indirect: false, type: null);
        if (reader.TokenType != JsonTokenType.StartArray)
            throw new JsonException();

        ReadNext(ref reader);
        // Cases 2 and 3:
        //   [value]
        //    ^ we're here
        //   [value, string]
        //    ^ we're here
        // Case 4:
        //   [[value], string]
        //    ^ we're here
        if (TryGetGlobalValueFromToken(ref reader, out GlobalValue value))
        {
            ReadNext(ref reader);
            if (reader.TokenType == JsonTokenType.EndArray)
                return CreateGlobalDescriptor(value, indirect: true, type: null);
            if (reader.TokenType != JsonTokenType.String)
                throw new JsonException();

            string type = reader.GetString() ?? throw new JsonException();
            ReadNext(ref reader);
            if (reader.TokenType != JsonTokenType.EndArray)
                throw new JsonException();
            return CreateGlobalDescriptor(value, indirect: false, type);
        }

        if (reader.TokenType != JsonTokenType.StartArray)
            throw new JsonException();
        ReadNext(ref reader);
        //   [[value], string]
        //     ^ we're here
        if (!TryGetGlobalValueFromToken(ref reader, out value))
            throw new JsonException();
        ReadNext(ref reader);
        //   [[value], string]
        //           ^ we're here
        if (reader.TokenType != JsonTokenType.EndArray)
            throw new JsonException();
        ReadNext(ref reader);
        //   [[value], string]
        //             ^ we're here
        if (reader.TokenType != JsonTokenType.String)
            throw new JsonException();
        string indirectType = reader.GetString() ?? throw new JsonException();
        ReadNext(ref reader);
        if (reader.TokenType != JsonTokenType.EndArray)
            throw new JsonException();
        return CreateGlobalDescriptor(value, indirect: true, indirectType);
    }

    private static GlobalDescriptor CreateGlobalDescriptor(GlobalValue value, bool indirect, string? type)
    {
        if (indirect && value.NumericValue is null)
            throw new JsonException("Indirect global value could not be converted to a number.");

        return new GlobalDescriptor
        {
            Type = type,
            NumericValue = value.NumericValue,
            StringValue = value.StringValue,
            Indirect = indirect,
        };
    }

    private static int? ReadNullableInt32(ref Utf8JsonReader reader)
    {
        if (reader.TokenType == JsonTokenType.Null)
            return null;
        if (TryGetInt32FromToken(ref reader, out int value))
            return value;
        throw new JsonException();
    }

    private static string? ReadNullableString(ref Utf8JsonReader reader)
    {
        if (reader.TokenType == JsonTokenType.Null)
            return null;
        if (reader.TokenType == JsonTokenType.String)
            return reader.GetString();
        throw new JsonException();
    }

    private static void ReadNext(ref Utf8JsonReader reader)
    {
        if (!reader.Read())
            throw new JsonException();
    }

    private struct GlobalValue
    {
        public ulong? NumericValue;
        public string? StringValue;
    }

    private static bool TryGetGlobalValueFromToken(ref Utf8JsonReader reader, out GlobalValue directGlobalValue)
    {
        bool foundNumeric = TryGetUInt64FromToken(ref reader, out ulong numericValue);
        bool foundString = TryGetStringFromToken(ref reader, out string stringValue);
        if (foundNumeric || foundString)
        {
            // this parsed as a valid direct global value
            directGlobalValue = new GlobalValue
            {
                NumericValue = foundNumeric ? numericValue : null,
                StringValue = foundString ? stringValue : null
            };
            return true;
        }
        directGlobalValue = default;
        return false;
    }

    private static bool TryGetStringFromToken(ref Utf8JsonReader reader, out string value)
    {
        value = string.Empty;
        if (reader.TokenType == JsonTokenType.String && reader.GetString() is string stringValue)
        {
            value = stringValue;
            return true;
        }
        return false;
    }

    // Somewhat flexible parsing of numbers, allowing json number tokens or strings as decimal or hex, possibly negated.
    private static bool TryGetUInt64FromToken(ref Utf8JsonReader reader, out ulong value)
    {
        if (reader.TokenType == JsonTokenType.Number)
        {
            if (reader.TryGetUInt64(out value))
                return true;
            if (reader.TryGetInt64(out long signedValue))
            {
                value = (ulong)signedValue;
                return true;
            }
        }
        if (reader.TokenType == JsonTokenType.String)
        {
            string? s = reader.GetString();
            if (s == null)
            {
                value = 0u;
                return false;
            }
            if (ulong.TryParse(s, out value))
                return true;
            if (long.TryParse(s, out long signedValue))
            {
                value = (ulong)signedValue;
                return true;
            }
            if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase) &&
                ulong.TryParse(s.AsSpan(2), System.Globalization.NumberStyles.HexNumber, null, out value))
            {
                return true;
            }
            if (s.StartsWith("-0x", StringComparison.OrdinalIgnoreCase) &&
                ulong.TryParse(s.AsSpan(3), System.Globalization.NumberStyles.HexNumber, null, out ulong negValue))
            {
                value = ~negValue + 1; // two's complement
                return true;
            }
        }
        value = 0;
        return false;
    }

    // Somewhat flexible parsing of numbers, allowing json number tokens or strings as decimal or hex, possibly negated.
    private static bool TryGetInt32FromToken(ref Utf8JsonReader reader, out int value)
    {
        if (reader.TokenType == JsonTokenType.Number)
        {
            value = reader.GetInt32();
            return true;
        }
        if (reader.TokenType == JsonTokenType.String)
        {
            string? s = reader.GetString();
            if (s == null)
            {
                value = 0;
                return false;
            }
            if (int.TryParse(s, out value))
            {
                return true;
            }
            if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase) &&
                int.TryParse(s.AsSpan(2), System.Globalization.NumberStyles.HexNumber, null, out value))
            {
                return true;
            }
            if (s.StartsWith("-0x", StringComparison.OrdinalIgnoreCase) &&
                int.TryParse(s.AsSpan(3), System.Globalization.NumberStyles.HexNumber, null, out int negValue))
            {
                value = -negValue;
                return true;
            }
        }
        value = 0;
        return false;
    }
}