diff --git a/src/native/managed/cdacreader/src/ContractDescriptorParser.cs b/src/native/managed/cdacreader/src/ContractDescriptorParser.cs index 6e30b7efa664f4..4d26b6dc2965eb 100644 --- a/src/native/managed/cdacreader/src/ContractDescriptorParser.cs +++ b/src/native/managed/cdacreader/src/ContractDescriptorParser.cs @@ -12,19 +12,27 @@ public partial class ContractDescriptorParser { public const string TypeDescriptorSizeSigil = "!"; - public static CompactContractDescriptor? Parse(ReadOnlySpan json) + /// + /// Parses the "compact" representation of a contract descriptor. + /// + /// + /// See data_descriptor.md for the format. + /// + public static ContractDescriptor? ParseCompact(ReadOnlySpan json) { - return JsonSerializer.Deserialize(json, ContractDescriptorContext.Default.CompactContractDescriptor); + return JsonSerializer.Deserialize(json, ContractDescriptorContext.Default.ContractDescriptor); } - [JsonSerializable(typeof(CompactContractDescriptor))] + [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))] [JsonSourceGenerationOptions(AllowTrailingCommas = true, DictionaryKeyPolicy = JsonKnownNamingPolicy.Unspecified, // contracts, types and globals are case sensitive PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, @@ -34,7 +42,7 @@ internal sealed partial class ContractDescriptorContext : JsonSerializerContext { } - public class CompactContractDescriptor + public class ContractDescriptor { public int? Version { get; set; } public string? Baseline { get; set; } @@ -42,7 +50,7 @@ public class CompactContractDescriptor public Dictionary? Types { get; set; } - // TODO: globals + public Dictionary? Globals { get; set; } [JsonExtensionData] public Dictionary? Extras { get; set; } @@ -51,11 +59,10 @@ public class CompactContractDescriptor [JsonConverter(typeof(TypeDescriptorConverter))] public class TypeDescriptor { - public uint Size { get; set; } + public uint? Size { get; set; } public Dictionary? Fields { get; set; } } - // TODO: compact format needs a custom converter [JsonConverter(typeof(FieldDescriptorConverter))] public class FieldDescriptor { @@ -63,13 +70,24 @@ public class FieldDescriptor 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 { + // 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 = 0; + uint? size = null; Dictionary? fields = new(); while (reader.Read()) { @@ -97,6 +115,7 @@ public override TypeDescriptor Read(ref Utf8JsonReader reader, Type typeToConver } break; case JsonTokenType.Comment: + // unexpected - we specified to skip comments. but let's ignore anyway break; default: throw new JsonException(); @@ -113,60 +132,187 @@ public override void Write(Utf8JsonWriter writer, TypeDescriptor value, JsonSeri internal sealed class FieldDescriptorConverter : JsonConverter { + // Compact Field descriptors are either one or two element arrays + // 1. [number] - no type, offset is given as the number + // 2. [number, string] - has a type, offset is given as the number public override FieldDescriptor Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType == JsonTokenType.Number || reader.TokenType == JsonTokenType.String) - return new FieldDescriptor { Offset = reader.GetInt32() }; + if (GetInt32FromToken(ref reader, out int offset)) + return new FieldDescriptor { Offset = offset }; if (reader.TokenType != JsonTokenType.StartArray) throw new JsonException(); - int eltIdx = 0; - string? type = null; - int offset = 0; - while (reader.Read()) + reader.Read(); + // two cases: + // [number] + // ^ we're here + // or + // [number, string] + // ^ we're here + if (!GetInt32FromToken(ref reader, out offset)) + throw new JsonException(); + reader.Read(); // end of array or string + if (reader.TokenType == JsonTokenType.EndArray) + return new FieldDescriptor { Offset = offset }; + 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 NotImplementedException(); + } + } + + internal sealed class GlobalDescriptorConverter : JsonConverter + { + 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 (GetUInt64FromToken(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 (reader.TokenType == JsonTokenType.StartArray) { - switch (reader.TokenType) - { - case JsonTokenType.EndArray: - return new FieldDescriptor { Type = type, Offset = offset }; - case JsonTokenType.Comment: - // don't incrment eltIdx - continue; - default: - break; - } - switch (eltIdx) + // case 4: [[number], string] + // ^ we're here + reader.Read(); // number + if (!GetUInt64FromToken(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 }; + } + else + { + // case 2 or 3 + // case 2: [number] + // ^ we're here + // case 3: [number, string] + // ^ we're here + if (!GetUInt64FromToken(ref reader, out ulong valueCase2or3)) + throw new JsonException(); + 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 }; + else if (reader.TokenType == JsonTokenType.String) // it was case 3 { - case 0: - { - // expect an offset - either a string or a number token - if (reader.TokenType == JsonTokenType.Number || reader.TokenType == JsonTokenType.String) - offset = reader.GetInt32(); - else - throw new JsonException(); - break; - } - case 1: - { - // expect a type - a string token - if (reader.TokenType == JsonTokenType.String) - type = reader.GetString(); - else - throw new JsonException(); - break; - } - default: - // too many elements + 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 }; } - eltIdx++; + else + throw new JsonException(); } - throw new JsonException(); } - public override void Write(Utf8JsonWriter writer, FieldDescriptor value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, GlobalDescriptor value, JsonSerializerOptions options) { throw new NotImplementedException(); } } + // Somewhat flexible parsing of numbers, allowing json number tokens or strings as decimal or hex, possibly negatated. + private static bool GetUInt64FromToken(ref Utf8JsonReader reader, out ulong value) + { + if (reader.TokenType == JsonTokenType.Number) + { + if (reader.TryGetUInt64(out value)) + return true; + else 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") || s.StartsWith("0X")) && + ulong.TryParse(s.AsSpan(2), System.Globalization.NumberStyles.HexNumber, null, out value)) + return true; + if ((s.StartsWith("-0x") || s.StartsWith("-0X")) && + ulong.TryParse(s.AsSpan(3), System.Globalization.NumberStyles.HexNumber, null, out ulong negValue)) + { + value = ~negValue + 1; // twos 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 GetInt32FromToken(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") || s.StartsWith("0X")) && + int.TryParse(s.AsSpan(2), System.Globalization.NumberStyles.HexNumber, null, out value)) + return true; + if ((s.StartsWith("-0x") || s.StartsWith("-0X")) && + int.TryParse(s.AsSpan(3), System.Globalization.NumberStyles.HexNumber, null, out int negValue)) + { + value = -negValue; + return true; + } + } + value = 0; + return false; + } + } diff --git a/src/native/managed/cdacreader/tests/ContractDescriptorParserTests.cs b/src/native/managed/cdacreader/tests/ContractDescriptorParserTests.cs index 997679bbffb989..9feb5c68b5d535 100644 --- a/src/native/managed/cdacreader/tests/ContractDescriptorParserTests.cs +++ b/src/native/managed/cdacreader/tests/ContractDescriptorParserTests.cs @@ -15,7 +15,7 @@ public class ContractDescriptorParserTests public void ParsesEmptyContract() { ReadOnlyMemory json = "{}"u8.ToArray(); - ContractDescriptorParser.CompactContractDescriptor descriptor = ContractDescriptorParser.Parse(json.Span); + ContractDescriptorParser.ContractDescriptor descriptor = ContractDescriptorParser.ParseCompact(json.Span); Assert.Null(descriptor.Version); Assert.Null(descriptor.Baseline); Assert.Null(descriptor.Contracts); @@ -34,12 +34,13 @@ public void ParsesTrivialContract() "globals": {} } """u8.ToArray(); - ContractDescriptorParser.CompactContractDescriptor descriptor = ContractDescriptorParser.Parse(json.Span); + ContractDescriptorParser.ContractDescriptor descriptor = ContractDescriptorParser.ParseCompact(json.Span); Assert.Equal(0, descriptor.Version); Assert.Equal("empty", descriptor.Baseline); Assert.Empty(descriptor.Contracts); Assert.Empty(descriptor.Types); - Assert.NotNull(descriptor.Extras["globals"]); + Assert.Empty(descriptor.Globals); + Assert.Null(descriptor.Extras); } [Fact] @@ -64,13 +65,15 @@ public void ParseSizedTypes() "phi": 8, "rho": 16 } - } + }, + "globals": {} } """u8.ToArray(); - ContractDescriptorParser.CompactContractDescriptor descriptor = ContractDescriptorParser.Parse(json.Span); + ContractDescriptorParser.ContractDescriptor descriptor = ContractDescriptorParser.ParseCompact(json.Span); Assert.Equal(0, descriptor.Version); Assert.Equal("empty", descriptor.Baseline); Assert.Empty(descriptor.Contracts); + Assert.Empty(descriptor.Globals); Assert.Equal(4, descriptor.Types.Count); Assert.Equal(8u, descriptor.Types["pointer"].Size); Assert.Equal(4u, descriptor.Types["int"].Size); @@ -86,7 +89,7 @@ public void ParseSizedTypes() Assert.Equal(16, descriptor.Types["Point3D"].Fields["rho"].Offset); Assert.Equal("double", descriptor.Types["Point3D"].Fields["r"].Type); Assert.Null(descriptor.Types["Point3D"].Fields["phi"].Type); - Assert.Equal(0u, descriptor.Types["Point3D"].Size); + Assert.Null(descriptor.Types["Point3D"].Size); } [Fact] @@ -104,11 +107,109 @@ public void ParseContractsCaseSensitive() "globals": {} } """u8.ToArray(); - ContractDescriptorParser.CompactContractDescriptor descriptor = ContractDescriptorParser.Parse(json.Span); + ContractDescriptorParser.ContractDescriptor descriptor = ContractDescriptorParser.ParseCompact(json.Span); Assert.Equal(0, descriptor.Version); Assert.Equal("empty", descriptor.Baseline); Assert.Equal(2, descriptor.Contracts.Count); Assert.Equal(1, descriptor.Contracts["foo"]); Assert.Equal(2, descriptor.Contracts["Foo"]); } + + [Fact] + public void ParsesGlobals() + { + ReadOnlyMemory json = """ + { + "version": 0, + "baseline": "empty", + "contracts": {}, + "types": {}, + "globals": { + "globalInt": 1, + "globalPtr": [2], + "globalTypedInt": [3, "uint8"], + "globalTypedPtr": [[4], "uintptr"], + "globalHex": "0x1234", + "globalNegative": -2, + "globalStringyInt": "17", + "globalStringyNegative": "-2", + "globalNegativeHex": "-0xff", + "globalBigStringyInt": "0x123456789abcdef", + "globalStringyPtr": ["0x1234"], + "globalTypedStringyInt": ["0x1234", "int"], + "globalTypedStringyPtr": [["0x1234"], "int"] + } + } + """u8.ToArray(); + ContractDescriptorParser.ContractDescriptor descriptor = ContractDescriptorParser.ParseCompact(json.Span); + Assert.Equal(0, descriptor.Version); + Assert.Equal("empty", descriptor.Baseline); + Assert.Empty(descriptor.Contracts); + Assert.Empty(descriptor.Types); + Assert.Equal(13, descriptor.Globals.Count); + Assert.Equal((ulong)1, descriptor.Globals["globalInt"].Value); + Assert.False(descriptor.Globals["globalInt"].Indirect); + Assert.Equal((ulong)2, descriptor.Globals["globalPtr"].Value); + Assert.True(descriptor.Globals["globalPtr"].Indirect); + Assert.Equal((ulong)3, descriptor.Globals["globalTypedInt"].Value); + Assert.False(descriptor.Globals["globalTypedInt"].Indirect); + Assert.Equal("uint8", descriptor.Globals["globalTypedInt"].Type); + Assert.Equal((ulong)4, descriptor.Globals["globalTypedPtr"].Value); + Assert.True(descriptor.Globals["globalTypedPtr"].Indirect); + Assert.Equal("uintptr", descriptor.Globals["globalTypedPtr"].Type); + Assert.Equal((ulong)0x1234, descriptor.Globals["globalHex"].Value); + Assert.False(descriptor.Globals["globalHex"].Indirect); + Assert.Equal((ulong)0xfffffffffffffffe, descriptor.Globals["globalNegative"].Value); + Assert.False(descriptor.Globals["globalNegative"].Indirect); + Assert.Equal((ulong)17, descriptor.Globals["globalStringyInt"].Value); + Assert.False(descriptor.Globals["globalStringyInt"].Indirect); + Assert.Equal((ulong)0xfffffffffffffffe, descriptor.Globals["globalStringyNegative"].Value); + Assert.False(descriptor.Globals["globalStringyNegative"].Indirect); + Assert.Equal((ulong)0xffffffffffffff01, descriptor.Globals["globalNegativeHex"].Value); + Assert.False(descriptor.Globals["globalNegativeHex"].Indirect); + Assert.Equal((ulong)0x123456789abcdef, descriptor.Globals["globalBigStringyInt"].Value); + Assert.False(descriptor.Globals["globalBigStringyInt"].Indirect); + Assert.Equal((ulong)0x1234, descriptor.Globals["globalStringyPtr"].Value); + Assert.True(descriptor.Globals["globalStringyPtr"].Indirect); + Assert.Equal("int", descriptor.Globals["globalTypedStringyInt"].Type); + Assert.Equal((ulong)0x1234, descriptor.Globals["globalTypedStringyInt"].Value); + Assert.False(descriptor.Globals["globalTypedStringyInt"].Indirect); + Assert.Equal("int", descriptor.Globals["globalTypedStringyPtr"].Type); + Assert.Equal((ulong)0x1234, descriptor.Globals["globalTypedStringyPtr"].Value); + Assert.True(descriptor.Globals["globalTypedStringyPtr"].Indirect); + } + + [Fact] + void ParsesExoticOffsets() + { + ReadOnlyMemory json = """ + { + "version": 0, + "baseline": "empty", + "contracts": {}, + "types": { + "OddStruct": { + "a": -12, + "b": "0x12", + "c": "-0x12", + "d": ["0x100", "int"] + } + }, + "globals": { + } + } + """u8.ToArray(); + ContractDescriptorParser.ContractDescriptor descriptor = ContractDescriptorParser.ParseCompact(json.Span); + Assert.Equal(0, descriptor.Version); + Assert.Equal("empty", descriptor.Baseline); + Assert.Empty(descriptor.Contracts); + Assert.Empty(descriptor.Globals); + Assert.Equal(1, descriptor.Types.Count); + Assert.Equal(4, descriptor.Types["OddStruct"].Fields.Count); + Assert.Equal(-12, descriptor.Types["OddStruct"].Fields["a"].Offset); + Assert.Equal(0x12, descriptor.Types["OddStruct"].Fields["b"].Offset); + Assert.Equal(-0x12, descriptor.Types["OddStruct"].Fields["c"].Offset); + Assert.Equal(0x100, descriptor.Types["OddStruct"].Fields["d"].Offset); + Assert.Equal("int", descriptor.Types["OddStruct"].Fields["d"].Type); + } }