using System; using System.CodeDom.Compiler; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using CommunityToolkit.Diagnostics; namespace protodec; public sealed class Protodec { public readonly Dictionary Messages = new(); public readonly Dictionary Enums = new(); private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static; public void ParseMessage(Type messageClass, bool skipEnums = false) { Guard.IsTrue(messageClass.IsClass); if (Messages.ContainsKey(messageClass.Name)) return; ProtobufMessage message = new(messageClass.Name); FieldInfo[] idFields = messageClass.GetFields(PublicStatic); PropertyInfo[] properties = messageClass.GetProperties(BindingFlags.Public | BindingFlags.Instance); for (int i = 0; i < properties.Length; i++) { Type propertyType = properties[i].PropertyType; // only OneOf enums are defined nested directly in the message class if (propertyType.IsEnum && propertyType.DeclaringType?.Name == messageClass.Name) { string oneOfName = TranslateMessageFieldName(properties[i].Name); int[] oneOfProtoFieldIds = propertyType.GetFields(PublicStatic) .Select(field => (int)field.GetRawConstantValue()!) .Where(id => id > 0) .ToArray(); message.OneOfs.Add(oneOfName, oneOfProtoFieldIds); continue; } FieldInfo idField = idFields[i]; Guard.IsTrue(idField.IsLiteral); Guard.IsEqualTo(idField.FieldType.Name, nameof(Int32)); int msgFieldId = (int)idField.GetRawConstantValue()!; string msgFieldType = ParseType(propertyType, skipEnums, message); string msgFieldName = TranslateMessageFieldName(properties[i].Name); message.Fields.Add(msgFieldId, (msgFieldType, msgFieldName)); } Messages.Add(message.Name, message); } private string ParseType(Type type, bool skipEnums, ProtobufMessage message) { switch (type.Name) { case "ByteString": return "bytes"; case nameof(String): return "string"; case nameof(Boolean): return "bool"; case nameof(Double): return "double"; case nameof(UInt32): return "uint32"; case nameof(UInt64): return "uint64"; case nameof(Int32): return "int32"; case nameof(Int64): return "int64"; case nameof(Single): return "float"; case "RepeatedField`1": string typeName = ParseType(type.GenericTypeArguments[0], skipEnums, message); return "repeated " + typeName; case "MapField`2": string t1 = ParseType(type.GenericTypeArguments[0], skipEnums, message); string t2 = ParseType(type.GenericTypeArguments[1], skipEnums, message); return $"map<{t1}, {t2}>"; default: { if (type.IsEnum) { if (skipEnums) return "int32"; ParseEnum(type, message); } else { ParseMessage(type, skipEnums); message.Imports.Add(type.Name); } return type.Name; } } } private void ParseEnum(Type enumEnum, ProtobufMessage message) { if ((enumEnum.IsNested && message.Nested.ContainsKey(enumEnum.Name)) || Enums.ContainsKey(enumEnum.Name)) return; ProtobufEnum protoEnum = new(enumEnum.Name); foreach (FieldInfo field in enumEnum.GetFields(PublicStatic)) { int enumFieldId = (int)field.GetRawConstantValue()!; string enumFieldName = field.GetCustomAttributesData() .SingleOrDefault(attr => attr.AttributeType.Name == "OriginalNameAttribute") ?.ConstructorArguments[0] .Value as string ?? TranslateEnumFieldName(field.Name); protoEnum.Fields.Add(enumFieldId, enumFieldName); } if (enumEnum.IsNested) { message.Nested.Add(protoEnum.Name, protoEnum); } else { message.Imports.Add(protoEnum.Name); Enums.Add(protoEnum.Name, protoEnum); } } public void WriteAllTo(IndentedTextWriter writer) { WritePreambleTo(writer); foreach (IWritable proto in Messages.Values.Concat(Enums.Values)) { proto.WriteTo(writer); writer.WriteLine(); writer.WriteLine(); } } internal static void WritePreambleTo(TextWriter writer) { writer.WriteLine("// Decompiled with protodec"); writer.WriteLine(); writer.WriteLine("""syntax = "proto3";"""); writer.WriteLine(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static string TranslateMessageFieldName(string name) => name.IsBeebyted() ? name : name.ToSnakeCaseLower(); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static string TranslateEnumFieldName(string name) => name.IsBeebyted() ? name : name.ToSnakeCaseUpper(); private bool TryParseWriteToMethod(Type targetClass) { //MethodInfo method = targetClass.GetInterface("Google.Protobuf.IBufferMessage")?.GetMethod("InternalWriteTo", BindingFlags.Public | BindingFlags.Instance)!; byte[] cil = targetClass.GetMethod("WriteTo", BindingFlags.Public | BindingFlags.Instance)! .GetMethodBody()! .GetILAsByteArray()!; if (cil[0] == 0x2A) // ret { return false; } throw new NotImplementedException(); } }