From 3f236498fc6ebe6e0ec9c724e7b5a18f66a6a82d Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Mon, 24 Aug 2026 14:24:00 -0400 Subject: [PATCH 1/4] Reduce cDAC contract descriptor parser size Replace source-generated JSON serialization with a direct Utf8JsonReader parser while preserving the compact descriptor format and extension data behavior. This reduces the win-x64 universal cDAC binary by about 576 KB (9.5%). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7006a215-ea24-4756-8f27-14cc48b78680 --- .../ContractDescriptorParser.cs | 427 +++++++++++------- .../ContractDescriptor/ParserTests.cs | 45 ++ 2 files changed, 301 insertions(+), 171 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorParser.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorParser.cs index 22561b0d7cba3a..dd11994888718c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorParser.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorParser.cs @@ -3,11 +3,8 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.Unicode; namespace Microsoft.Diagnostics.DataContractReader; @@ -25,36 +22,80 @@ public partial class ContractDescriptorParser /// /// Parses the "compact" representation of a contract descriptor. /// - // Workaround for https://github.com/dotnet/runtime/issues/101205 - [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Root))] public static ContractDescriptor? ParseCompact(ReadOnlySpan json) { - return JsonSerializer.Deserialize(json, ContractDescriptorContext.Default.ContractDescriptor); + try + { + return ParseCompactCore(json); + } + catch (JsonException ex) when (ex.GetType() != typeof(JsonException)) + { + throw new JsonException(ex.Message, ex); + } } - [JsonSerializable(typeof(ContractDescriptor))] - [JsonSerializable(typeof(int?))] - [JsonSerializable(typeof(string))] - [JsonSerializable(typeof(Dictionary))] - [JsonSerializable(typeof(Dictionary))] - [JsonSerializable(typeof(Dictionary))] - [JsonSerializable(typeof(Dictionary))] - [JsonSerializable(typeof(TypeDescriptor))] - [JsonSerializable(typeof(FieldDescriptor))] - [JsonSerializable(typeof(GlobalDescriptor))] - [JsonSerializable(typeof(Dictionary))] - [JsonSourceGenerationOptions(AllowTrailingCommas = true, - DictionaryKeyPolicy = JsonKnownNamingPolicy.Unspecified, // contracts, types and globals are case sensitive - PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, - NumberHandling = JsonNumberHandling.AllowReadingFromString, - ReadCommentHandling = JsonCommentHandling.Skip, - UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, - UnknownTypeHandling = JsonUnknownTypeHandling.JsonElement, - Converters = [typeof(TypeDescriptorConverter), - typeof(FieldDescriptorConverter), - typeof(GlobalDescriptorConverter)])] - internal sealed partial class ContractDescriptorContext : JsonSerializerContext + private static ContractDescriptor? ParseCompactCore(ReadOnlySpan json) { + var reader = new Utf8JsonReader( + json, + new JsonReaderOptions + { + AllowTrailingCommas = true, + CommentHandling = JsonCommentHandling.Skip, + }); + + if (!reader.Read()) + throw new JsonException(); + if (reader.TokenType == JsonTokenType.Null) + return null; + if (reader.TokenType != JsonTokenType.StartObject) + throw new JsonException(); + + var descriptor = new ContractDescriptor(); + 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 = ReadStringDictionary(ref reader); + break; + case "types": + descriptor.Types = ReadTypeDictionary(ref reader); + break; + case "globals": + descriptor.Globals = ReadGlobalDictionary(ref reader); + break; + case "subDescriptors": + descriptor.SubDescriptors = ReadGlobalDictionary(ref reader); + break; + default: + descriptor.Extras ??= []; + using (JsonDocument extra = JsonDocument.ParseValue(ref reader)) + { + descriptor.Extras[propertyName] = extra.RootElement.Clone(); + } + break; + } + } + + throw new JsonException(); } public class ContractDescriptor @@ -69,7 +110,6 @@ public class ContractDescriptor public Dictionary? SubDescriptors { get; set; } - [JsonExtensionData] public Dictionary? Extras { get; set; } public override string ToString() @@ -79,21 +119,18 @@ public override string ToString() } - [JsonConverter(typeof(TypeDescriptorConverter))] public class TypeDescriptor { public uint? Size { get; set; } public Dictionary? Fields { get; set; } } - [JsonConverter(typeof(FieldDescriptorConverter))] public class FieldDescriptor { public string? Type { get; set; } public int Offset { get; set; } } - [JsonConverter(typeof(GlobalDescriptorConverter))] public class GlobalDescriptor { [MemberNotNullWhen(true, nameof(NumericValue))] @@ -105,171 +142,219 @@ public class GlobalDescriptor public string? StringValue { get; set; } } - internal sealed class TypeDescriptorConverter : JsonConverter + private static Dictionary? ReadStringDictionary(ref Utf8JsonReader reader) { - // Almost a normal dictionary converter except: - // 1. looks for a special key "!" to set the Size property - // 2. field names are property names, but treated case-sensitively - public override TypeDescriptor Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.StartObject) - throw new JsonException(); - uint? size = null; - Dictionary? fields = new(); - while (reader.Read()) - { - switch (reader.TokenType) - { - case JsonTokenType.EndObject: - return new TypeDescriptor { Size = size, Fields = fields }; - case JsonTokenType.PropertyName: - string? fieldNameOrSizeSigil = reader.GetString(); - reader.Read(); // read the next value: either a number or a field descriptor - if (fieldNameOrSizeSigil == TypeDescriptorSizeSigil) - { - uint newSize = reader.GetUInt32(); - if (size is not null) - { - throw new JsonException($"Size specified multiple times: {size} and {newSize}"); - } - size = newSize; - } - else - { - string? fieldName = fieldNameOrSizeSigil; - var field = JsonSerializer.Deserialize(ref reader, ContractDescriptorContext.Default.FieldDescriptor); - if (fieldName is null || field is null) - throw new JsonException(); - if (!fields.TryAdd(fieldName, field)) - { - throw new JsonException($"Duplicate field name: {fieldName}"); - } - } - break; - case JsonTokenType.Comment: - // unexpected - we specified to skip comments. but let's ignore anyway - break; - default: - throw new JsonException(); - } - } + if (reader.TokenType == JsonTokenType.Null) + return null; + if (reader.TokenType != JsonTokenType.StartObject) throw new JsonException(); - } - public override void Write(Utf8JsonWriter writer, TypeDescriptor value, JsonSerializerOptions options) + var values = new Dictionary(); + while (reader.Read()) { - throw new NotImplementedException(); + 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); + string value = reader.TokenType == JsonTokenType.String + ? reader.GetString() ?? throw new JsonException() + : throw new JsonException(); + values[name] = value; } + + throw new JsonException(); } - internal sealed class FieldDescriptorConverter : JsonConverter + private static Dictionary? ReadTypeDictionary(ref Utf8JsonReader reader) { - // Compact Field descriptors are either a number or a two element array - // 1. number - no type, offset is given as the number - // 2. [number, string] - offset is given as the number, type name is given as the string - public override FieldDescriptor Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + if (reader.TokenType == JsonTokenType.Null) + return null; + if (reader.TokenType != JsonTokenType.StartObject) + throw new JsonException(); + + var types = new Dictionary(); + while (reader.Read()) { - if (TryGetInt32FromToken(ref reader, out int offset)) - return new FieldDescriptor { Offset = offset }; - if (reader.TokenType != JsonTokenType.StartArray) - throw new JsonException(); - reader.Read(); - // [number, string] - // ^ we're here - if (!TryGetInt32FromToken(ref reader, out offset)) + if (reader.TokenType == JsonTokenType.EndObject) + return types; + if (reader.TokenType != JsonTokenType.PropertyName) throw new JsonException(); - reader.Read(); // string - if (reader.TokenType != JsonTokenType.String) - throw new JsonException(); - string? type = reader.GetString(); - reader.Read(); // end of array - if (reader.TokenType != JsonTokenType.EndArray) - throw new JsonException(); - return new FieldDescriptor { Type = type, Offset = offset }; - } - public override void Write(Utf8JsonWriter writer, FieldDescriptor value, JsonSerializerOptions options) - { - throw new JsonException(); + string name = reader.GetString() ?? throw new JsonException(); + ReadNext(ref reader); + types[name] = ReadTypeDescriptor(ref reader); } + + throw new JsonException(); } - internal sealed class GlobalDescriptorConverter : JsonConverter + private static TypeDescriptor ReadTypeDescriptor(ref Utf8JsonReader reader) { - public override GlobalDescriptor Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + // 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; + var fields = new Dictionary(); + while (reader.Read()) { - // four cases: - // 1. value - no type, direct value, given value - // 2. [number] - no type, indirect value, given aux data ptr - // 3. [value, string] - type, direct value, given value - // 4. [[number], string] - type, indirect value, given aux data ptr - // value can either be a string or a number. if a string is able to be parsed as a number, it is read as both - - // Case 1: value - if (TryGetGlobalValueFromToken(ref reader, out GlobalValue valueCase1)) - return new GlobalDescriptor { NumericValue = valueCase1.NumericValue, StringValue = valueCase1.StringValue, Indirect = false }; - if (reader.TokenType != JsonTokenType.StartArray) + if (reader.TokenType == JsonTokenType.EndObject) + return new TypeDescriptor { Size = size, Fields = fields }; + if (reader.TokenType != JsonTokenType.PropertyName) throw new JsonException(); - reader.Read(); - // we're in case 2, 3, or 4: - // case 2: [number] - // ^ we're here - // case 3: [value, string] - // ^ we're here - // case 4: [[number], string] - // ^ we're here - if (TryGetGlobalValueFromToken(ref reader, out GlobalValue valueCase2or3)) + + string fieldName = reader.GetString() ?? throw new JsonException(); + ReadNext(ref reader); + if (fieldName == TypeDescriptorSizeSigil) { - // case 2 or 3 - // case 2: [number] - // ^ we're here - // case 3: [value, string] - // ^ we're here - reader.Read(); // end of array (case 2) or string (case 3) - if (reader.TokenType == JsonTokenType.EndArray) // it was case 2 - { - if (valueCase2or3.NumericValue is null) - throw new JsonException("Indirect global value could not be converted to a number."); - return new GlobalDescriptor { NumericValue = valueCase2or3.NumericValue, StringValue = valueCase2or3.StringValue, Indirect = true }; - } - if (reader.TokenType == JsonTokenType.String) // it was case 3 - { - string? type = reader.GetString(); - reader.Read(); // end of array for case 3 - if (reader.TokenType != JsonTokenType.EndArray) - throw new JsonException(); - return new GlobalDescriptor { Type = type, NumericValue = valueCase2or3.NumericValue, StringValue = valueCase2or3.StringValue, Indirect = false }; - } - throw new JsonException(); + uint newSize = reader.GetUInt32(); + if (size is not null) + throw new JsonException($"Size specified multiple times: {size} and {newSize}"); + size = newSize; } - if (reader.TokenType == JsonTokenType.StartArray) + else if (!fields.TryAdd(fieldName, ReadFieldDescriptor(ref reader))) { - // case 4: [[number], string] - // ^ we're here - reader.Read(); // number - if (!TryGetGlobalValueFromToken(ref reader, out GlobalValue valueCase4)) - throw new JsonException(); - reader.Read(); // end of inner array - if (reader.TokenType != JsonTokenType.EndArray) - throw new JsonException(); - reader.Read(); // string - if (reader.TokenType != JsonTokenType.String) - throw new JsonException(); - string? type = reader.GetString(); - reader.Read(); // end of outer array - if (reader.TokenType != JsonTokenType.EndArray) - throw new JsonException(); - if (valueCase4.NumericValue is null) - throw new JsonException("Indirect global value could not be converted to a number."); - return new GlobalDescriptor { Type = type, NumericValue = valueCase4.NumericValue, StringValue = valueCase4.StringValue, Indirect = true }; + throw new JsonException($"Duplicate field name: {fieldName}"); } - throw new JsonException(); } - public override void Write(Utf8JsonWriter writer, GlobalDescriptor value, JsonSerializerOptions options) + 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); + if (!TryGetInt32FromToken(ref reader, out offset)) + throw new JsonException(); + ReadNext(ref reader); + 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? ReadGlobalDictionary(ref Utf8JsonReader reader) + { + if (reader.TokenType == JsonTokenType.Null) + return null; + if (reader.TokenType != JsonTokenType.StartObject) + throw new JsonException(); + + var globals = new Dictionary(); + while (reader.Read()) { + if (reader.TokenType == JsonTokenType.EndObject) + return globals; + if (reader.TokenType != JsonTokenType.PropertyName) + throw new JsonException(); + + string name = reader.GetString() ?? throw new JsonException(); + ReadNext(ref reader); + globals[name] = ReadGlobalDescriptor(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); + 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); + if (!TryGetGlobalValueFromToken(ref reader, out value)) + throw new JsonException(); + ReadNext(ref reader); + if (reader.TokenType != JsonTokenType.EndArray) + throw new JsonException(); + ReadNext(ref reader); + 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 diff --git a/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/ParserTests.cs b/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/ParserTests.cs index 42fc7fe311a742..8a8304a3a4cd97 100644 --- a/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/ParserTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/ParserTests.cs @@ -43,6 +43,51 @@ public void ParsesTrivialContract() Assert.Null(descriptor.Extras); } + [Fact] + public void ParsesExtensionData() + { + ReadOnlySpan json = """ + { + "version": "1", + "unknownObject": { "value": 2 }, + "unknownArray": [3, 4], + "unknownString": "value" + } + """u8; + + ContractDescriptorParser.ContractDescriptor descriptor = ContractDescriptorParser.ParseCompact(json); + + Assert.Equal(1, descriptor.Version); + Assert.Equal(2, descriptor.Extras["unknownObject"].GetProperty("value").GetInt32()); + Assert.Equal(2, descriptor.Extras["unknownArray"].GetArrayLength()); + Assert.Equal("value", descriptor.Extras["unknownString"].GetString()); + } + + [Fact] + public void RejectsTrailingJson() + { + byte[] json = "{}{}"u8.ToArray(); + + Assert.Throws(() => ContractDescriptorParser.ParseCompact(json)); + } + + [Fact] + public void RejectsDuplicateFieldNames() + { + byte[] json = """ + { + "types": { + "Type": { + "field": 0, + "field": 8 + } + } + } + """u8.ToArray(); + + Assert.Throws(() => ContractDescriptorParser.ParseCompact(json)); + } + [Fact] public void ParseSizedTypes() { From 576176d369459c91f646b257413d4a5f87760251 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Mon, 24 Aug 2026 14:59:14 -0400 Subject: [PATCH 2/4] Ignore unknown contract descriptor properties Skip unknown top-level values instead of retaining unused JsonElement extension data. This removes JsonDocument support and further reduces the universal cDAC binary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7006a215-ea24-4756-8f27-14cc48b78680 --- .../ContractDescriptorParser.cs | 8 +------- .../tests/UnitTests/ContractDescriptor/ParserTests.cs | 11 +++++------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorParser.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorParser.cs index dd11994888718c..141a5e17b184d8 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorParser.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorParser.cs @@ -86,11 +86,7 @@ public partial class ContractDescriptorParser descriptor.SubDescriptors = ReadGlobalDictionary(ref reader); break; default: - descriptor.Extras ??= []; - using (JsonDocument extra = JsonDocument.ParseValue(ref reader)) - { - descriptor.Extras[propertyName] = extra.RootElement.Clone(); - } + reader.Skip(); break; } } @@ -110,8 +106,6 @@ public class ContractDescriptor public Dictionary? SubDescriptors { get; set; } - public Dictionary? Extras { get; set; } - public override string ToString() { return $"Version: {Version}, Baseline: {Baseline}, Contracts: {Contracts?.Count}, Types: {Types?.Count}, Globals: {Globals?.Count}, SubDescriptors: {SubDescriptors?.Count}"; diff --git a/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/ParserTests.cs b/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/ParserTests.cs index 8a8304a3a4cd97..365d530b1e9cd0 100644 --- a/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/ParserTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/ParserTests.cs @@ -20,7 +20,6 @@ public void ParsesEmptyContract() Assert.Null(descriptor.Baseline); Assert.Null(descriptor.Contracts); Assert.Null(descriptor.Types); - Assert.Null(descriptor.Extras); } [Fact] public void ParsesTrivialContract() @@ -40,11 +39,10 @@ public void ParsesTrivialContract() Assert.Empty(descriptor.Contracts); Assert.Empty(descriptor.Types); Assert.Empty(descriptor.Globals); - Assert.Null(descriptor.Extras); } [Fact] - public void ParsesExtensionData() + public void IgnoresUnknownProperties() { ReadOnlySpan json = """ { @@ -58,9 +56,10 @@ public void ParsesExtensionData() ContractDescriptorParser.ContractDescriptor descriptor = ContractDescriptorParser.ParseCompact(json); Assert.Equal(1, descriptor.Version); - Assert.Equal(2, descriptor.Extras["unknownObject"].GetProperty("value").GetInt32()); - Assert.Equal(2, descriptor.Extras["unknownArray"].GetArrayLength()); - Assert.Equal("value", descriptor.Extras["unknownString"].GetString()); + Assert.Null(descriptor.Baseline); + Assert.Null(descriptor.Contracts); + Assert.Null(descriptor.Types); + Assert.Null(descriptor.Globals); } [Fact] From bdfb6d4d4f872e2fd02e7945bd3837b063a1fa80 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Mon, 24 Aug 2026 15:45:29 -0400 Subject: [PATCH 3/4] Address contract descriptor parser feedback Share dictionary parsing, use explicit product-code types, cache reader options, ignore unknown values, and preserve complete-input validation without normalizing derived JSON exceptions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7006a215-ea24-4756-8f27-14cc48b78680 --- .../ContractDescriptorParser.cs | 106 +++++------------- .../ContractDescriptor/ParserTests.cs | 11 +- .../ContractDescriptor/TargetTests.cs | 2 +- 3 files changed, 39 insertions(+), 80 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorParser.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorParser.cs index 141a5e17b184d8..6632807af14b31 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorParser.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorParser.cs @@ -19,39 +19,33 @@ 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(ref Utf8JsonReader reader); + /// /// Parses the "compact" representation of a contract descriptor. /// public static ContractDescriptor? ParseCompact(ReadOnlySpan json) { - try - { - return ParseCompactCore(json); - } - catch (JsonException ex) when (ex.GetType() != typeof(JsonException)) - { - throw new JsonException(ex.Message, ex); - } - } - - private static ContractDescriptor? ParseCompactCore(ReadOnlySpan json) - { - var reader = new Utf8JsonReader( - json, - new JsonReaderOptions - { - AllowTrailingCommas = true, - CommentHandling = JsonCommentHandling.Skip, - }); + 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(); - var descriptor = new ContractDescriptor(); + ContractDescriptor descriptor = new(); while (reader.Read()) { if (reader.TokenType == JsonTokenType.EndObject) @@ -74,16 +68,16 @@ public partial class ContractDescriptorParser descriptor.Baseline = ReadNullableString(ref reader); break; case "contracts": - descriptor.Contracts = ReadStringDictionary(ref reader); + descriptor.Contracts = ReadDictionary(ref reader, ReadString); break; case "types": - descriptor.Types = ReadTypeDictionary(ref reader); + descriptor.Types = ReadDictionary(ref reader, ReadTypeDescriptor); break; case "globals": - descriptor.Globals = ReadGlobalDictionary(ref reader); + descriptor.Globals = ReadDictionary(ref reader, ReadGlobalDescriptor); break; case "subDescriptors": - descriptor.SubDescriptors = ReadGlobalDictionary(ref reader); + descriptor.SubDescriptors = ReadDictionary(ref reader, ReadGlobalDescriptor); break; default: reader.Skip(); @@ -136,52 +130,10 @@ public class GlobalDescriptor public string? StringValue { get; set; } } - private static Dictionary? ReadStringDictionary(ref Utf8JsonReader reader) + private static string ReadString(ref Utf8JsonReader reader) { - if (reader.TokenType == JsonTokenType.Null) - return null; - if (reader.TokenType != JsonTokenType.StartObject) - throw new JsonException(); - - var values = new Dictionary(); - 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); - string value = reader.TokenType == JsonTokenType.String - ? reader.GetString() ?? throw new JsonException() - : throw new JsonException(); - values[name] = value; - } - - throw new JsonException(); - } - - private static Dictionary? ReadTypeDictionary(ref Utf8JsonReader reader) - { - if (reader.TokenType == JsonTokenType.Null) - return null; - if (reader.TokenType != JsonTokenType.StartObject) - throw new JsonException(); - - var types = new Dictionary(); - while (reader.Read()) - { - if (reader.TokenType == JsonTokenType.EndObject) - return types; - if (reader.TokenType != JsonTokenType.PropertyName) - throw new JsonException(); - - string name = reader.GetString() ?? throw new JsonException(); - ReadNext(ref reader); - types[name] = ReadTypeDescriptor(ref reader); - } - + if (reader.TokenType == JsonTokenType.String) + return reader.GetString() ?? throw new JsonException(); throw new JsonException(); } @@ -194,7 +146,7 @@ private static TypeDescriptor ReadTypeDescriptor(ref Utf8JsonReader reader) throw new JsonException(); uint? size = null; - var fields = new Dictionary(); + Dictionary fields = new(); while (reader.Read()) { if (reader.TokenType == JsonTokenType.EndObject) @@ -243,24 +195,26 @@ private static FieldDescriptor ReadFieldDescriptor(ref Utf8JsonReader reader) return new FieldDescriptor { Type = type, Offset = offset }; } - private static Dictionary? ReadGlobalDictionary(ref Utf8JsonReader reader) + private static Dictionary? ReadDictionary( + ref Utf8JsonReader reader, + ReadValue readValue) { if (reader.TokenType == JsonTokenType.Null) return null; if (reader.TokenType != JsonTokenType.StartObject) throw new JsonException(); - var globals = new Dictionary(); + Dictionary values = []; while (reader.Read()) { if (reader.TokenType == JsonTokenType.EndObject) - return globals; + return values; if (reader.TokenType != JsonTokenType.PropertyName) throw new JsonException(); string name = reader.GetString() ?? throw new JsonException(); ReadNext(ref reader); - globals[name] = ReadGlobalDescriptor(ref reader); + values[name] = readValue(ref reader); } throw new JsonException(); @@ -401,7 +355,7 @@ private static bool TryGetUInt64FromToken(ref Utf8JsonReader reader, out ulong v } if (reader.TokenType == JsonTokenType.String) { - var s = reader.GetString(); + string? s = reader.GetString(); if (s == null) { value = 0u; @@ -440,7 +394,7 @@ private static bool TryGetInt32FromToken(ref Utf8JsonReader reader, out int valu } if (reader.TokenType == JsonTokenType.String) { - var s = reader.GetString(); + string? s = reader.GetString(); if (s == null) { value = 0; diff --git a/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/ParserTests.cs b/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/ParserTests.cs index 365d530b1e9cd0..e05b7cb6ee1ae6 100644 --- a/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/ParserTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/ParserTests.cs @@ -65,9 +65,14 @@ public void IgnoresUnknownProperties() [Fact] public void RejectsTrailingJson() { - byte[] json = "{}{}"u8.ToArray(); - - Assert.Throws(() => ContractDescriptorParser.ParseCompact(json)); + byte[][] invalidJson = + [ + "{}{}"u8.ToArray(), + "null{}"u8.ToArray(), + ]; + + foreach (byte[] json in invalidJson) + Assert.ThrowsAny(() => ContractDescriptorParser.ParseCompact(json)); } [Fact] diff --git a/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.cs b/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.cs index 5aac9733083b99..2035a8076134e4 100644 --- a/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.cs @@ -246,7 +246,7 @@ public void Create_InvalidDescriptorJson_ThrowsFormatException(MockTarget.Archit ContractDescriptorHelpers.Fill(descriptor, targetTestHelpers.Arch, descriptorJson.Length, 0xdddddddd, 0, 0xeeeeeeee); FormatException ex = Assert.Throws(() => builder.CreateTargetFromRawDescriptor(descriptor, descriptorJson, [])); - Assert.IsType(ex.InnerException); + Assert.IsAssignableFrom(ex.InnerException); Assert.Equal(CdacHResults.CDAC_E_DESCRIPTOR_MALFORMED, ex.HResult); } From 842d5eabfde7362d330f9e30351a4844b2f1ce1b Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Mon, 24 Aug 2026 15:58:28 -0400 Subject: [PATCH 4/4] Document compact descriptor parsing positions Restore token-position comments for compact field and global descriptor forms. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7006a215-ea24-4756-8f27-14cc48b78680 --- .../ContractDescriptorParser.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorParser.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorParser.cs index 6632807af14b31..be24395faae224 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorParser.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorParser.cs @@ -183,9 +183,13 @@ private static FieldDescriptor ReadFieldDescriptor(ref Utf8JsonReader reader) 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(); @@ -234,6 +238,14 @@ private static GlobalDescriptor ReadGlobalDescriptor(ref Utf8JsonReader reader) 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); @@ -252,12 +264,18 @@ private static GlobalDescriptor ReadGlobalDescriptor(ref Utf8JsonReader reader) 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();