-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[cdac] Implement a JSON contract reader (#100966)
Implement a parser for the "compact" JSON contract descriptor format specified in [data_contracts.md](https://github.com/dotnet/runtime/blob/main/docs/design/datacontracts/data_descriptor.md) The data model is a WIP - it's likely we will want something a bit more abstract (and less mutable). This is not wired up to consume a real contract descriptor from target process memory at the moment. It's only exercised by unit tests for now. --- * compact descriptor format json parser * suggestions from reviews; remove FieldDescriptor wrong conversion we incorrectly allowed `[number]` as a field descriptor conversion. that's not allowed. removed it. * Make test project like the nativeoat+linker tests Dont' use libraries test infrastructure. Just normal arcade xunit support. * add tools.cdacreadertests subset; add to CLR_Tools_Tests test leg * no duplicate fields/sizes in types * Make all cdacreader.csproj ProjectReferences use the same AdditionalProperties Since we set Configuration and RuntimeIdentifier, if we don't pass the same AdditionalProperties in all ProjectReferences, we bad project.assets.json files * Don't share the native compilation AdditionalProperties --------- Co-authored-by: Aaron Robinson <arobins@microsoft.com> Co-authored-by: Adeel Mujahid <3840695+am11@users.noreply.github.com> Co-authored-by: Elinor Fung <elfung@microsoft.com>
- Loading branch information
1 parent
a344abd
commit 43b22a8
Showing
8 changed files
with
584 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
327 changes: 327 additions & 0 deletions
327
src/native/managed/cdacreader/src/ContractDescriptorParser.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,327 @@ | ||
// 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.Contracts; | ||
using System.Text.Json; | ||
using System.Text.Json.Serialization; | ||
|
||
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 = "!"; | ||
|
||
/// <summary> | ||
/// Parses the "compact" representation of a contract descriptor. | ||
/// </summary> | ||
public static ContractDescriptor? ParseCompact(ReadOnlySpan<byte> json) | ||
{ | ||
return JsonSerializer.Deserialize(json, ContractDescriptorContext.Default.ContractDescriptor); | ||
} | ||
|
||
[JsonSerializable(typeof(ContractDescriptor))] | ||
[JsonSerializable(typeof(int))] | ||
[JsonSerializable(typeof(string))] | ||
[JsonSerializable(typeof(Dictionary<string, int>))] | ||
[JsonSerializable(typeof(Dictionary<string, TypeDescriptor>))] | ||
[JsonSerializable(typeof(Dictionary<string, FieldDescriptor>))] | ||
[JsonSerializable(typeof(Dictionary<string, GlobalDescriptor>))] | ||
[JsonSerializable(typeof(TypeDescriptor))] | ||
[JsonSerializable(typeof(FieldDescriptor))] | ||
[JsonSerializable(typeof(GlobalDescriptor))] | ||
[JsonSourceGenerationOptions(AllowTrailingCommas = true, | ||
DictionaryKeyPolicy = JsonKnownNamingPolicy.Unspecified, // contracts, types and globals are case sensitive | ||
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, | ||
NumberHandling = JsonNumberHandling.AllowReadingFromString, | ||
ReadCommentHandling = JsonCommentHandling.Skip)] | ||
internal sealed partial class ContractDescriptorContext : JsonSerializerContext | ||
{ | ||
} | ||
|
||
public class ContractDescriptor | ||
{ | ||
public int? Version { get; set; } | ||
public string? Baseline { get; set; } | ||
public Dictionary<string, int>? Contracts { get; set; } | ||
|
||
public Dictionary<string, TypeDescriptor>? Types { get; set; } | ||
|
||
public Dictionary<string, GlobalDescriptor>? Globals { get; set; } | ||
|
||
[JsonExtensionData] | ||
public Dictionary<string, object?>? Extras { get; set; } | ||
} | ||
|
||
[JsonConverter(typeof(TypeDescriptorConverter))] | ||
public class TypeDescriptor | ||
{ | ||
public uint? Size { get; set; } | ||
public Dictionary<string, FieldDescriptor>? 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 | ||
{ | ||
public string? Type { get; set; } | ||
public ulong Value { get; set; } | ||
public bool Indirect { get; set; } | ||
} | ||
|
||
internal sealed class TypeDescriptorConverter : JsonConverter<TypeDescriptor> | ||
{ | ||
// 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<string, FieldDescriptor>? 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(); | ||
} | ||
} | ||
throw new JsonException(); | ||
} | ||
|
||
public override void Write(Utf8JsonWriter writer, TypeDescriptor value, JsonSerializerOptions options) | ||
{ | ||
throw new NotImplementedException(); | ||
} | ||
} | ||
|
||
internal sealed class FieldDescriptorConverter : JsonConverter<FieldDescriptor> | ||
{ | ||
// 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 (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)) | ||
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(); | ||
} | ||
} | ||
|
||
internal sealed class GlobalDescriptorConverter : JsonConverter<GlobalDescriptor> | ||
{ | ||
public override GlobalDescriptor Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) | ||
{ | ||
// four cases: | ||
// 1. number - no type, direct value, given value | ||
// 2. [number] - no type, indirect value, given aux data ptr | ||
// 3. [number, string] - type, direct value, given value | ||
// 4. [[number], string] - type, indirect value, given aux data ptr | ||
|
||
// Case 1: number | ||
if (TryGetUInt64FromToken(ref reader, out ulong valueCase1)) | ||
return new GlobalDescriptor { Value = valueCase1 }; | ||
if (reader.TokenType != JsonTokenType.StartArray) | ||
throw new JsonException(); | ||
reader.Read(); | ||
// we're in case 2 or 3 or 4: | ||
// case 2: [number] | ||
// ^ we're here | ||
// case 3: [number, string] | ||
// ^ we're here | ||
// case 4: [[number], string] | ||
// ^ we're here | ||
if (TryGetUInt64FromToken(ref reader, out ulong valueCase2or3)) | ||
{ | ||
// case 2 or 3 | ||
// case 2: [number] | ||
// ^ we're here | ||
// case 3: [number, string] | ||
// ^ we're here | ||
reader.Read(); // end of array (case 2) or string (case 3) | ||
if (reader.TokenType == JsonTokenType.EndArray) // it was case 2 | ||
{ | ||
return new GlobalDescriptor { Value = valueCase2or3, 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, Value = valueCase2or3 }; | ||
} | ||
throw new JsonException(); | ||
} | ||
if (reader.TokenType == JsonTokenType.StartArray) | ||
{ | ||
// case 4: [[number], string] | ||
// ^ we're here | ||
reader.Read(); // number | ||
if (!TryGetUInt64FromToken(ref reader, out ulong value)) | ||
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(); | ||
return new GlobalDescriptor { Type = type, Value = value, Indirect = true }; | ||
} | ||
throw new JsonException(); | ||
} | ||
|
||
public override void Write(Utf8JsonWriter writer, GlobalDescriptor value, JsonSerializerOptions options) | ||
{ | ||
throw new JsonException(); | ||
} | ||
} | ||
|
||
// Somewhat flexible parsing of numbers, allowing json number tokens or strings as decimal or hex, possibly negatated. | ||
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) | ||
{ | ||
var 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 either 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) | ||
{ | ||
var 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; | ||
} | ||
} |
Oops, something went wrong.