From d43deef033ea39db6f6cada6a142e1291a43e77d Mon Sep 17 00:00:00 2001 From: Xpl0itR Date: Fri, 7 Jun 2024 16:01:59 +0100 Subject: [PATCH] Even more improvements including but not limited to: - detect protobuf well-known types - detect NonUserCodeAttribute - detect ObsoleteAttribute to mark protobufs/fields as deprecated - parse generated client/server types for gRPC services - fix "optional" parsing - fix obfuscated name translation - fix foreign nested protobufs - support protobuf editions and corresponding options - rewrite protobuf models to better reflect the specification, including decoupling toplevels and files - rewrite a bunch of things to make more sense --- README.md | 8 +- protodec.sln | 5 + src/LibProtodec/AssemblyInspector.cs | 40 +- src/LibProtodec/Enum.cs | 46 -- src/LibProtodec/FieldTypeName.cs | 26 - src/LibProtodec/Message.cs | 98 --- src/LibProtodec/Models/Fields/EnumField.cs | 29 + src/LibProtodec/Models/Fields/MessageField.cs | 42 ++ .../Models/Fields/ServiceMethod.cs | 60 ++ src/LibProtodec/Models/Protobuf.cs | 105 +++ src/LibProtodec/Models/TopLevels/Enum.cs | 67 ++ src/LibProtodec/Models/TopLevels/Message.cs | 69 ++ src/LibProtodec/Models/TopLevels/Service.cs | 37 ++ src/LibProtodec/Models/TopLevels/TopLevel.cs | 35 + src/LibProtodec/Models/Types/INestableType.cs | 14 + src/LibProtodec/Models/Types/IType.cs | 18 + src/LibProtodec/Models/Types/Map.cs | 13 + src/LibProtodec/Models/Types/Repeated.cs | 13 + src/LibProtodec/Models/Types/Scalar.cs | 27 + src/LibProtodec/Models/Types/WellKnown.cs | 39 ++ src/LibProtodec/ParserOptions.cs | 16 + src/LibProtodec/Protobuf.cs | 47 -- src/LibProtodec/ProtodecContext.cs | 625 ++++++++++++++---- src/protodec/Program.cs | 63 +- 24 files changed, 1173 insertions(+), 369 deletions(-) delete mode 100644 src/LibProtodec/Enum.cs delete mode 100644 src/LibProtodec/FieldTypeName.cs delete mode 100644 src/LibProtodec/Message.cs create mode 100644 src/LibProtodec/Models/Fields/EnumField.cs create mode 100644 src/LibProtodec/Models/Fields/MessageField.cs create mode 100644 src/LibProtodec/Models/Fields/ServiceMethod.cs create mode 100644 src/LibProtodec/Models/Protobuf.cs create mode 100644 src/LibProtodec/Models/TopLevels/Enum.cs create mode 100644 src/LibProtodec/Models/TopLevels/Message.cs create mode 100644 src/LibProtodec/Models/TopLevels/Service.cs create mode 100644 src/LibProtodec/Models/TopLevels/TopLevel.cs create mode 100644 src/LibProtodec/Models/Types/INestableType.cs create mode 100644 src/LibProtodec/Models/Types/IType.cs create mode 100644 src/LibProtodec/Models/Types/Map.cs create mode 100644 src/LibProtodec/Models/Types/Repeated.cs create mode 100644 src/LibProtodec/Models/Types/Scalar.cs create mode 100644 src/LibProtodec/Models/Types/WellKnown.cs create mode 100644 src/LibProtodec/ParserOptions.cs delete mode 100644 src/LibProtodec/Protobuf.cs diff --git a/README.md b/README.md index 35b99a0..283af37 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,17 @@ Arguments: target_assembly_path Either the path to the target assembly or a directory of assemblies, all of which be parsed. out_path An existing directory to output into individual files, otherwise output to a single file. Options: - --skip_enums Skip parsing enums and replace references to them with int32. - --skip_properties_without_protoc_attribute Skip properties that aren't decorated with `GeneratedCode("protoc")` when parsing + --parse_service_servers Parses gRPC service definitions from server classes. + --parse_service_clients Parses gRPC service definitions from client classes. + --skip_enums Skip parsing enums and replace references to them with int32. + --include_properties_without_non_user_code_attribute Includes properties that aren't decorated with `DebuggerNonUserCode` when parsing. + --include_service_methods_without_generated_code_attribute Includes methods that aren't decorated with `GeneratedCode("grpc_csharp_plugin")` when parsing gRPC services. ``` Limitations ----------- - Integers are assumed to be (u)int32/64 as CIL doesn't differentiate between them and sint32/64 and (s)fixed32/64. +- Package names are not preserved in protobuf compilation so naturally we cannot recover them during decompilation, which may result in naming conflicts. - When decompiling from [Il2CppDumper](https://github.com/Perfare/Il2CppDumper) DummyDLLs - The `Name` parameter of `OriginalNameAttribute` is not dumped. In this case, the CIL enum field names are used after conforming them to protobuf conventions diff --git a/protodec.sln b/protodec.sln index 06ab2b8..d47bba3 100644 --- a/protodec.sln +++ b/protodec.sln @@ -7,6 +7,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "protodec", "src\protodec\pr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibProtodec", "src\LibProtodec\LibProtodec.csproj", "{5F6DAD82-D9AD-4CE5-86E6-D20C9F059A4D}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{22F217C3-0FC2-4D06-B5F3-AA1E3AFC402E}" + ProjectSection(SolutionItems) = preProject + README.md = README.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/src/LibProtodec/AssemblyInspector.cs b/src/LibProtodec/AssemblyInspector.cs index cfd2878..fbeb64c 100644 --- a/src/LibProtodec/AssemblyInspector.cs +++ b/src/LibProtodec/AssemblyInspector.cs @@ -35,16 +35,36 @@ public AssemblyInspector(string assemblyPath) public IEnumerable GetProtobufMessageTypes() { - Type? googleProtobufIMessage = LoadedTypes.SingleOrDefault(static type => type?.FullName == "Google.Protobuf.IMessage", null) - ?? AssemblyContext.LoadFromAssemblyName("Google.Protobuf") - .GetType("Google.Protobuf.IMessage"); - return from type - in LoadedTypes - where !type.IsNested - && type.IsSealed - && type.Namespace?.StartsWith("Google.Protobuf", StringComparison.Ordinal) != true - && type.IsAssignableTo(googleProtobufIMessage) - select type; + Type? iMessage = LoadedTypes.SingleOrDefault(static type => type?.FullName == "Google.Protobuf.IMessage", null) + ?? AssemblyContext.LoadFromAssemblyName("Google.Protobuf") + .GetType("Google.Protobuf.IMessage"); + + return LoadedTypes.Where( + type => type is { IsNested: false, IsSealed: true } + && type.Namespace?.StartsWith("Google.Protobuf", StringComparison.Ordinal) != true + && type.IsAssignableTo(iMessage)); + } + + public IEnumerable GetProtobufServiceClientTypes() + { + Type? clientBase = LoadedTypes.SingleOrDefault(static type => type?.FullName == "Grpc.Core.ClientBase", null) + ?? AssemblyContext.LoadFromAssemblyName("Grpc.Core.Api") + .GetType("Grpc.Core.ClientBase"); + + return LoadedTypes.Where( + type => type is { IsNested: true, IsAbstract: false } + && type.IsAssignableTo(clientBase)); + } + + public IEnumerable GetProtobufServiceServerTypes() + { + Type? bindServiceMethodAttribute = LoadedTypes.SingleOrDefault(static type => type?.FullName == "Grpc.Core.BindServiceMethodAttribute", null) + ?? AssemblyContext.LoadFromAssemblyName("Grpc.Core.Api") + .GetType("Grpc.Core.BindServiceMethodAttribute"); + + return LoadedTypes.Where( + type => type is { IsNested: true, IsAbstract: true, DeclaringType: { IsNested: false, IsSealed: true, IsAbstract: true } } + && type.GetCustomAttributesData().Any(attribute => attribute.AttributeType == bindServiceMethodAttribute)); } public void Dispose() => diff --git a/src/LibProtodec/Enum.cs b/src/LibProtodec/Enum.cs deleted file mode 100644 index 9cfdb78..0000000 --- a/src/LibProtodec/Enum.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright © 2023-2024 Xpl0itR -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -using System.CodeDom.Compiler; -using System.Collections.Generic; -using SystemEx.Collections; - -namespace LibProtodec; - -public sealed class Enum : Protobuf -{ - public readonly List> Fields = []; - - public override void WriteFileTo(IndentedTextWriter writer) - { - this.WritePreambleTo(writer); - WriteTo(writer); - } - - public override void WriteTo(IndentedTextWriter writer) - { - writer.Write("enum "); - writer.Write(this.Name); - writer.WriteLine(" {"); - writer.Indent++; - - if (Fields.ContainsDuplicateKey()) - { - writer.WriteLine("option allow_alias = true;"); - } - - foreach ((int id, string name) in Fields) - { - writer.Write(name); - writer.Write(" = "); - writer.Write(id); - writer.WriteLine(';'); - } - - writer.Indent--; - writer.Write('}'); - } -} \ No newline at end of file diff --git a/src/LibProtodec/FieldTypeName.cs b/src/LibProtodec/FieldTypeName.cs deleted file mode 100644 index 9feb99a..0000000 --- a/src/LibProtodec/FieldTypeName.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright © 2024 Xpl0itR -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -namespace LibProtodec; - -public static class FieldTypeName -{ - public const string Double = "double"; - public const string Float = "float"; - public const string Int32 = "int32"; - public const string Int64 = "int64"; - public const string UInt32 = "uint32"; - public const string UInt64 = "uint64"; - public const string SInt32 = "sint32"; - public const string SInt64 = "sint64"; - public const string Fixed32 = "fixed32"; - public const string Fixed64 = "fixed64"; - public const string SFixed32 = "sfixed32"; - public const string SFixed64 = "sfixed64"; - public const string Bool = "bool"; - public const string String = "string"; - public const string Bytes = "bytes"; -} \ No newline at end of file diff --git a/src/LibProtodec/Message.cs b/src/LibProtodec/Message.cs deleted file mode 100644 index 75b58ae..0000000 --- a/src/LibProtodec/Message.cs +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright © 2023-2024 Xpl0itR -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -using System.CodeDom.Compiler; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace LibProtodec; - -public sealed class Message : Protobuf -{ - public readonly HashSet Imports = []; - public readonly Dictionary OneOfs = []; - public readonly Dictionary Fields = []; - public readonly Dictionary Nested = []; - - public override void WriteFileTo(IndentedTextWriter writer) - { - this.WritePreambleTo(writer); - - if (Imports.Count > 0) - { - foreach (string import in Imports) - { - writer.Write("import \""); - writer.Write(import); - writer.WriteLine(".proto\";"); - } - - writer.WriteLine(); - } - - WriteTo(writer); - } - - public override void WriteTo(IndentedTextWriter writer) - { - writer.Write("message "); - writer.Write(this.Name); - writer.WriteLine(" {"); - writer.Indent++; - - int[] oneOfs = OneOfs.SelectMany(static oneOf => oneOf.Value).ToArray(); - - foreach ((int fieldId, (bool, string, string) field) in Fields) - { - if (oneOfs.Contains(fieldId)) - continue; - - WriteField(writer, fieldId, field); - } - - foreach ((string name, int[] fieldIds) in OneOfs) - { - // ReSharper disable once StringLiteralTypo - writer.Write("oneof "); - writer.Write(name); - writer.WriteLine(" {"); - writer.Indent++; - - foreach (int fieldId in fieldIds) - { - WriteField(writer, fieldId, Fields[fieldId]); - } - - writer.Indent--; - writer.WriteLine('}'); - } - - foreach (Protobuf nested in Nested.Values) - { - nested.WriteTo(writer); - writer.WriteLine(); - } - - writer.Indent--; - writer.Write('}'); - } - - private static void WriteField(TextWriter writer, int fieldId, (bool IsOptional, string Type, string Name) field) - { - if (field.IsOptional) - { - writer.Write("optional "); - } - - writer.Write(field.Type); - writer.Write(' '); - writer.Write(field.Name); - writer.Write(" = "); - writer.Write(fieldId); - writer.WriteLine(';'); - } -} \ No newline at end of file diff --git a/src/LibProtodec/Models/Fields/EnumField.cs b/src/LibProtodec/Models/Fields/EnumField.cs new file mode 100644 index 0000000..6e3fa13 --- /dev/null +++ b/src/LibProtodec/Models/Fields/EnumField.cs @@ -0,0 +1,29 @@ +// Copyright © 2024 Xpl0itR +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +namespace LibProtodec.Models.Fields; + +public sealed class EnumField +{ + public required string Name { get; init; } + public required int Id { get; init; } + + public bool IsObsolete { get; init; } + + public void WriteTo(System.IO.TextWriter writer) + { + writer.Write(Name); + writer.Write(" = "); + writer.Write(Id); + + if (IsObsolete) + { + writer.Write(" [deprecated = true]"); + } + + writer.WriteLine(';'); + } +} \ No newline at end of file diff --git a/src/LibProtodec/Models/Fields/MessageField.cs b/src/LibProtodec/Models/Fields/MessageField.cs new file mode 100644 index 0000000..33e7adb --- /dev/null +++ b/src/LibProtodec/Models/Fields/MessageField.cs @@ -0,0 +1,42 @@ +// Copyright © 2024 Xpl0itR +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +using System.IO; +using LibProtodec.Models.TopLevels; +using LibProtodec.Models.Types; + +namespace LibProtodec.Models.Fields; + +public sealed class MessageField +{ + public required IType Type { get; init; } + public required string Name { get; init; } + public required int Id { get; init; } + + public bool IsObsolete { get; init; } + public bool HasHasProp { get; init; } + + public void WriteTo(TextWriter writer, TopLevel topLevel, bool isOneOf) + { + if (HasHasProp && !isOneOf && Type is not Repeated) + { + writer.Write("optional "); + } + + Protobuf.WriteTypeNameTo(writer, Type, topLevel); + writer.Write(' '); + writer.Write(Name); + writer.Write(" = "); + writer.Write(Id); + + if (IsObsolete) + { + writer.Write(" [deprecated = true]"); + } + + writer.WriteLine(';'); + } +} \ No newline at end of file diff --git a/src/LibProtodec/Models/Fields/ServiceMethod.cs b/src/LibProtodec/Models/Fields/ServiceMethod.cs new file mode 100644 index 0000000..f1c10e1 --- /dev/null +++ b/src/LibProtodec/Models/Fields/ServiceMethod.cs @@ -0,0 +1,60 @@ +// Copyright © 2024 Xpl0itR +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +using System.CodeDom.Compiler; +using LibProtodec.Models.TopLevels; +using LibProtodec.Models.Types; + +namespace LibProtodec.Models.Fields; + +public sealed class ServiceMethod +{ + public required string Name { get; init; } + public required IType RequestType { get; init; } + public required IType ResponseType { get; init; } + + public bool IsRequestStreamed { get; init; } + public bool IsResponseStreamed { get; init; } + public bool IsObsolete { get; init; } + + public void WriteTo(IndentedTextWriter writer, TopLevel topLevel) + { + writer.Write("rpc "); + writer.Write(Name); + writer.Write(" ("); + + if (IsRequestStreamed) + { + writer.Write("stream "); + } + + Protobuf.WriteTypeNameTo(writer, RequestType, topLevel); + writer.Write(") returns ("); + + if (IsResponseStreamed) + { + writer.Write("stream "); + } + + Protobuf.WriteTypeNameTo(writer, ResponseType, topLevel); + writer.Write(')'); + + if (IsObsolete) + { + writer.WriteLine(" {"); + writer.Indent++; + + Protobuf.WriteOptionTo(writer, "deprecated", "true"); + + writer.Indent--; + writer.WriteLine('}'); + } + else + { + writer.WriteLine(';'); + } + } +} \ No newline at end of file diff --git a/src/LibProtodec/Models/Protobuf.cs b/src/LibProtodec/Models/Protobuf.cs new file mode 100644 index 0000000..1f75aa4 --- /dev/null +++ b/src/LibProtodec/Models/Protobuf.cs @@ -0,0 +1,105 @@ +// Copyright © 2024 Xpl0itR +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +using System.CodeDom.Compiler; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using LibProtodec.Models.TopLevels; +using LibProtodec.Models.Types; + +namespace LibProtodec.Models; + +public sealed class Protobuf +{ + private HashSet? _imports; + public HashSet Imports => + _imports ??= []; + + public readonly List TopLevels = []; + + public string? Edition { get; set; } + public string? AssemblyName { get; init; } + public string? Namespace { get; init; } + + public string FileName => + $"{TopLevels.FirstOrDefault()?.Name}.proto"; + + public void WriteTo(IndentedTextWriter writer) + { + writer.WriteLine("// Decompiled with protodec"); + + if (AssemblyName is not null) + { + writer.Write("// Assembly: "); + writer.WriteLine(AssemblyName); + } + + writer.WriteLine(); + writer.WriteLine( + Edition is null + ? """syntax = "proto3";""" + : $"""edition = "{Edition}";"""); + + if (_imports is not null) + { + writer.WriteLine(); + + foreach (string import in _imports) + { + writer.Write("import \""); + writer.Write(import); + writer.WriteLine("\";"); + } + } + + if (Namespace is not null) + { + writer.WriteLine(); + WriteOptionTo(writer, "csharp_namespace", Namespace, true); + } + + foreach (TopLevel topLevel in TopLevels) + { + writer.WriteLine(); + topLevel.WriteTo(writer); + } + } + + public static void WriteOptionTo(TextWriter writer, string name, string value, bool quoteValue = false) + { + writer.Write("option "); + writer.Write(name); + writer.Write(" = "); + + if (quoteValue) + { + writer.Write('\"'); + } + + writer.Write(value); + + if (quoteValue) + { + writer.Write('\"'); + } + + writer.WriteLine(';'); + } + + public static void WriteTypeNameTo(TextWriter writer, IType type, TopLevel topLevel) + { + if (type is TopLevel { Parent: not null } typeTopLevel && typeTopLevel.Parent != topLevel) + { + writer.Write( + typeTopLevel.QualifyName(topLevel)); + } + else + { + writer.Write(type.Name); + } + } +} \ No newline at end of file diff --git a/src/LibProtodec/Models/TopLevels/Enum.cs b/src/LibProtodec/Models/TopLevels/Enum.cs new file mode 100644 index 0000000..b1c1404 --- /dev/null +++ b/src/LibProtodec/Models/TopLevels/Enum.cs @@ -0,0 +1,67 @@ +// Copyright © 2024 Xpl0itR +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +global using Enum = LibProtodec.Models.TopLevels.Enum; +using System.CodeDom.Compiler; +using System.Collections.Generic; +using LibProtodec.Models.Fields; +using LibProtodec.Models.Types; + +namespace LibProtodec.Models.TopLevels; + +public sealed class Enum : TopLevel, INestableType +{ + public readonly List Fields = []; + + public override void WriteTo(IndentedTextWriter writer) + { + writer.Write("enum "); + writer.Write(this.Name); + writer.WriteLine(" {"); + writer.Indent++; + + if (ContainsDuplicateField) + { + Protobuf.WriteOptionTo(writer, "allow_alias", "true"); + } + + if (this.IsObsolete) + { + Protobuf.WriteOptionTo(writer, "deprecated", "true"); + } + + if (IsClosed) + { + Protobuf.WriteOptionTo(writer, "features.enum_type", "CLOSED"); + } + + foreach (EnumField field in Fields) + { + field.WriteTo(writer); + } + + writer.Indent--; + writer.Write('}'); + } + + public bool IsClosed { get; set; } + + private bool ContainsDuplicateField + { + get + { + if (Fields.Count < 2) + return false; + + HashSet set = []; + foreach (EnumField field in Fields) + if (!set.Add(field.Id)) + return true; + + return false; + } + } +} \ No newline at end of file diff --git a/src/LibProtodec/Models/TopLevels/Message.cs b/src/LibProtodec/Models/TopLevels/Message.cs new file mode 100644 index 0000000..ae8430f --- /dev/null +++ b/src/LibProtodec/Models/TopLevels/Message.cs @@ -0,0 +1,69 @@ +// Copyright © 2024 Xpl0itR +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +using System.CodeDom.Compiler; +using System.Collections.Generic; +using System.Linq; +using LibProtodec.Models.Fields; +using LibProtodec.Models.Types; + +namespace LibProtodec.Models.TopLevels; + +public sealed class Message : TopLevel, INestableType +{ + public readonly Dictionary OneOfs = []; + public readonly Dictionary Fields = []; + public readonly Dictionary Nested = []; + + public override void WriteTo(IndentedTextWriter writer) + { + writer.Write("message "); + writer.Write(this.Name); + writer.WriteLine(" {"); + writer.Indent++; + + if (this.IsObsolete) + { + Protobuf.WriteOptionTo(writer, "deprecated", "true"); + } + + int[] oneOfs = OneOfs.SelectMany(static oneOf => oneOf.Value).ToArray(); + + foreach (MessageField field in Fields.Values) + { + if (oneOfs.Contains(field.Id)) + continue; + + field.WriteTo(writer, this, isOneOf: false); + } + + foreach ((string name, int[] fieldIds) in OneOfs) + { + // ReSharper disable once StringLiteralTypo + writer.Write("oneof "); + writer.Write(name); + writer.WriteLine(" {"); + writer.Indent++; + + foreach (int fieldId in fieldIds) + { + Fields[fieldId].WriteTo(writer, this, isOneOf: true); + } + + writer.Indent--; + writer.WriteLine('}'); + } + + foreach (INestableType nested in Nested.Values) + { + nested.WriteTo(writer); + writer.WriteLine(); + } + + writer.Indent--; + writer.Write('}'); + } +} \ No newline at end of file diff --git a/src/LibProtodec/Models/TopLevels/Service.cs b/src/LibProtodec/Models/TopLevels/Service.cs new file mode 100644 index 0000000..fc46c5a --- /dev/null +++ b/src/LibProtodec/Models/TopLevels/Service.cs @@ -0,0 +1,37 @@ +// Copyright © 2024 Xpl0itR +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +using System.CodeDom.Compiler; +using System.Collections.Generic; +using LibProtodec.Models.Fields; + +namespace LibProtodec.Models.TopLevels; + +public sealed class Service : TopLevel +{ + public readonly List Methods = []; + + public override void WriteTo(IndentedTextWriter writer) + { + writer.Write("service "); + writer.Write(this.Name); + writer.WriteLine(" {"); + writer.Indent++; + + if (this.IsObsolete) + { + Protobuf.WriteOptionTo(writer, "deprecated", "true"); + } + + foreach (ServiceMethod method in Methods) + { + method.WriteTo(writer, this); + } + + writer.Indent--; + writer.Write('}'); + } +} \ No newline at end of file diff --git a/src/LibProtodec/Models/TopLevels/TopLevel.cs b/src/LibProtodec/Models/TopLevels/TopLevel.cs new file mode 100644 index 0000000..f3815d5 --- /dev/null +++ b/src/LibProtodec/Models/TopLevels/TopLevel.cs @@ -0,0 +1,35 @@ +// Copyright © 2024 Xpl0itR +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +using System.CodeDom.Compiler; +using System.Collections.Generic; + +namespace LibProtodec.Models.TopLevels; + +public abstract class TopLevel +{ + public required string Name { get; init; } + public bool IsObsolete { get; init; } + public Protobuf? Protobuf { get; set; } + public TopLevel? Parent { get; set; } + + public string QualifyName(TopLevel topLevel) + { + List names = [Name]; + + TopLevel? parent = Parent; + while (parent is not null && parent != topLevel) + { + names.Add(parent.Name); + parent = parent.Parent; + } + + names.Reverse(); + return string.Join('.', names); + } + + public abstract void WriteTo(IndentedTextWriter writer); +} \ No newline at end of file diff --git a/src/LibProtodec/Models/Types/INestableType.cs b/src/LibProtodec/Models/Types/INestableType.cs new file mode 100644 index 0000000..8977a3f --- /dev/null +++ b/src/LibProtodec/Models/Types/INestableType.cs @@ -0,0 +1,14 @@ +// Copyright © 2024 Xpl0itR +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +namespace LibProtodec.Models.Types; + +public interface INestableType : IType +{ + Protobuf? Protobuf { get; } + + void WriteTo(System.CodeDom.Compiler.IndentedTextWriter writer); +} \ No newline at end of file diff --git a/src/LibProtodec/Models/Types/IType.cs b/src/LibProtodec/Models/Types/IType.cs new file mode 100644 index 0000000..7a180ac --- /dev/null +++ b/src/LibProtodec/Models/Types/IType.cs @@ -0,0 +1,18 @@ +// Copyright © 2024 Xpl0itR +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +namespace LibProtodec.Models.Types; + +public interface IType +{ + string Name { get; } +} + +public sealed class External(string typeName) : IType +{ + public string Name => + typeName; +} \ No newline at end of file diff --git a/src/LibProtodec/Models/Types/Map.cs b/src/LibProtodec/Models/Types/Map.cs new file mode 100644 index 0000000..ba6b31d --- /dev/null +++ b/src/LibProtodec/Models/Types/Map.cs @@ -0,0 +1,13 @@ +// Copyright © 2024 Xpl0itR +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +namespace LibProtodec.Models.Types; + +public sealed class Map(IType typeKey, IType typeVal) : IType +{ + public string Name => + $"map<{typeKey.Name}, {typeVal.Name}>"; +} \ No newline at end of file diff --git a/src/LibProtodec/Models/Types/Repeated.cs b/src/LibProtodec/Models/Types/Repeated.cs new file mode 100644 index 0000000..ac02576 --- /dev/null +++ b/src/LibProtodec/Models/Types/Repeated.cs @@ -0,0 +1,13 @@ +// Copyright © 2024 Xpl0itR +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +namespace LibProtodec.Models.Types; + +public sealed class Repeated(IType type) : IType +{ + public string Name => + $"repeated {type.Name}"; +} \ No newline at end of file diff --git a/src/LibProtodec/Models/Types/Scalar.cs b/src/LibProtodec/Models/Types/Scalar.cs new file mode 100644 index 0000000..510ec94 --- /dev/null +++ b/src/LibProtodec/Models/Types/Scalar.cs @@ -0,0 +1,27 @@ +// Copyright © 2024 Xpl0itR +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +namespace LibProtodec.Models.Types; + +// ReSharper disable StringLiteralTypo +public static class Scalar +{ + public static readonly IType Bool = new External("bool"); + public static readonly IType Bytes = new External("bytes"); + public static readonly IType Double = new External("double"); + public static readonly IType Fixed32 = new External("fixed32"); + public static readonly IType Fixed64 = new External("fixed64"); + public static readonly IType Float = new External("float"); + public static readonly IType Int32 = new External("int32"); + public static readonly IType Int64 = new External("int64"); + public static readonly IType SFixed32 = new External("sfixed32"); + public static readonly IType SFixed64 = new External("sfixed64"); + public static readonly IType SInt32 = new External("sint32"); + public static readonly IType SInt64 = new External("sint64"); + public static readonly IType String = new External("string"); + public static readonly IType UInt32 = new External("uint32"); + public static readonly IType UInt64 = new External("uint64"); +} \ No newline at end of file diff --git a/src/LibProtodec/Models/Types/WellKnown.cs b/src/LibProtodec/Models/Types/WellKnown.cs new file mode 100644 index 0000000..4e4e8c9 --- /dev/null +++ b/src/LibProtodec/Models/Types/WellKnown.cs @@ -0,0 +1,39 @@ +// Copyright © 2024 Xpl0itR +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +namespace LibProtodec.Models.Types; + +public static class WellKnown +{ + public static readonly IType Any = new External("google.protobuf.Any"); + public static readonly IType Api = new External("google.protobuf.Api"); + public static readonly IType BoolValue = new External("google.protobuf.BoolValue"); + public static readonly IType BytesValue = new External("google.protobuf.BytesValue"); + public static readonly IType DoubleValue = new External("google.protobuf.DoubleValue"); + public static readonly IType Duration = new External("google.protobuf.Duration"); + public static readonly IType Empty = new External("google.protobuf.Empty"); + public static readonly IType Enum = new External("google.protobuf.Enum"); + public static readonly IType EnumValue = new External("google.protobuf.EnumValue"); + public static readonly IType Field = new External("google.protobuf.Field"); + public static readonly IType FieldMask = new External("google.protobuf.FieldMask"); + public static readonly IType FloatValue = new External("google.protobuf.FloatValue"); + public static readonly IType Int32Value = new External("google.protobuf.Int32Value"); + public static readonly IType Int64Value = new External("google.protobuf.Int64Value"); + public static readonly IType ListValue = new External("google.protobuf.ListValue"); + public static readonly IType Method = new External("google.protobuf.Method"); + public static readonly IType Mixin = new External("google.protobuf.Mixin"); + public static readonly IType NullValue = new External("google.protobuf.NullValue"); + public static readonly IType Option = new External("google.protobuf.Option"); + public static readonly IType SourceContext = new External("google.protobuf.SourceContext"); + public static readonly IType StringValue = new External("google.protobuf.StringValue"); + public static readonly IType Struct = new External("google.protobuf.Struct"); + public static readonly IType Syntax = new External("google.protobuf.Syntax"); + public static readonly IType Timestamp = new External("google.protobuf.Timestamp"); + public static readonly IType Type = new External("google.protobuf.Type"); + public static readonly IType UInt32Value = new External("google.protobuf.UInt32Value"); + public static readonly IType UInt64Value = new External("google.protobuf.UInt64Value"); + public static readonly IType Value = new External("google.protobuf.Value"); +} \ No newline at end of file diff --git a/src/LibProtodec/ParserOptions.cs b/src/LibProtodec/ParserOptions.cs new file mode 100644 index 0000000..a6970cf --- /dev/null +++ b/src/LibProtodec/ParserOptions.cs @@ -0,0 +1,16 @@ +// Copyright © 2024 Xpl0itR +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +namespace LibProtodec; + +[System.Flags] +public enum ParserOptions +{ + None = 0, + SkipEnums = 1, + IncludePropertiesWithoutNonUserCodeAttribute = 2, + IncludeServiceMethodsWithoutGeneratedCodeAttribute = 4, +} \ No newline at end of file diff --git a/src/LibProtodec/Protobuf.cs b/src/LibProtodec/Protobuf.cs deleted file mode 100644 index b0911e0..0000000 --- a/src/LibProtodec/Protobuf.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright © 2023-2024 Xpl0itR -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -using System.CodeDom.Compiler; -using System.IO; - -namespace LibProtodec; - -public abstract class Protobuf -{ - public required string Name { get; init; } - - public string? AssemblyName { get; init; } - public string? Namespace { get; init; } - - public abstract void WriteFileTo(IndentedTextWriter writer); - - public abstract void WriteTo(IndentedTextWriter writer); - - protected void WritePreambleTo(TextWriter writer) => - WritePreambleTo(writer, AssemblyName, Namespace); - - // ReSharper disable once MethodOverloadWithOptionalParameter - public static void WritePreambleTo(TextWriter writer, string? assemblyName = null, string? @namespace = null) - { - writer.WriteLine("// Decompiled with protodec"); - - if (assemblyName is not null) - { - writer.Write("// Assembly: "); - writer.WriteLine(assemblyName); - } - - writer.WriteLine(); - writer.WriteLine("""syntax = "proto3";"""); - writer.WriteLine(); - - if (@namespace is not null) - { - writer.WriteLine($"""option csharp_namespace = "{@namespace}";"""); - writer.WriteLine(); - } - } -} \ No newline at end of file diff --git a/src/LibProtodec/ProtodecContext.cs b/src/LibProtodec/ProtodecContext.cs index c754c86..23c83f0 100644 --- a/src/LibProtodec/ProtodecContext.cs +++ b/src/LibProtodec/ProtodecContext.cs @@ -1,4 +1,4 @@ -// Copyright � 2023-2024 Xpl0itR +// Copyright © 2023-2024 Xpl0itR // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -7,89 +7,78 @@ using System; using System.CodeDom.Compiler; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using SystemEx; -using SystemEx.Collections; using CommunityToolkit.Diagnostics; +using LibProtodec.Models; +using LibProtodec.Models.Fields; +using LibProtodec.Models.TopLevels; +using LibProtodec.Models.Types; namespace LibProtodec; -public delegate bool LookupFunc(string key, [MaybeNullWhen(false)] out string value); +public delegate bool TypeLookupFunc(Type type, [NotNullWhen(true)] out IType? fieldType, out string? import); +public delegate bool NameLookupFunc(string name, [MaybeNullWhen(false)] out string translatedName); public sealed class ProtodecContext { - private readonly Dictionary _protobufs = []; - private readonly HashSet _currentDescent = []; + private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static; + private const BindingFlags PublicInstanceDeclared = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; - public LookupFunc? CustomTypeLookup { get; init; } + private readonly Dictionary _parsed = []; - public LookupFunc? CustomNameLookup { get; init; } + public readonly List Protobufs = []; - public IReadOnlyDictionary Protobufs => - _protobufs; + public NameLookupFunc? NameLookup { get; set; } + + public TypeLookupFunc TypeLookup { get; set; } = + LookupScalarAndWellKnownTypes; public void WriteAllTo(IndentedTextWriter writer) { - Protobuf.WritePreambleTo(writer); + writer.WriteLine("// Decompiled with protodec"); + writer.WriteLine(); + writer.WriteLine("""syntax = "proto3";"""); + writer.WriteLine(); - foreach (Protobuf proto in _protobufs.Values) + foreach (TopLevel topLevel in Protobufs.SelectMany(static proto => proto.TopLevels)) { - proto.WriteTo(writer); + topLevel.WriteTo(writer); writer.WriteLine(); writer.WriteLine(); } } - public void ParseMessage(Type type, bool skipEnums = false, bool skipPropertiesWithoutProtocAttribute = false) - { - Guard.IsTrue(type.IsClass); - - ParseMessageInternal(type, skipEnums, skipPropertiesWithoutProtocAttribute, null); - _currentDescent.Clear(); - } - - public void ParseEnum(Type type) - { - Guard.IsTrue(type.IsEnum); - - ParseEnumInternal(type, null); - _currentDescent.Clear(); - } - - private bool IsParsed(Type type, Message? parentMessage, out Dictionary protobufs) + public Message ParseMessage(Type messageClass, ParserOptions options = ParserOptions.None) { - protobufs = parentMessage is not null && type.IsNested - ? parentMessage.Nested - : _protobufs; - - return protobufs.ContainsKey(type.Name) - || !_currentDescent.Add(type.Name); - } + Guard.IsTrue(messageClass is { IsClass: true, IsSealed: true }); - private void ParseMessageInternal(Type messageClass, bool skipEnums, bool skipPropertiesWithoutProtocAttribute, Message? parentMessage) - { - if (IsParsed(messageClass, parentMessage, out Dictionary protobufs)) + if (_parsed.TryGetValue(messageClass.FullName ?? messageClass.Name, out TopLevel? parsedMessage)) { - return; + return (Message)parsedMessage; } Message message = new() { - Name = TranslateProtobufName(messageClass.Name), - AssemblyName = messageClass.Assembly.FullName, - Namespace = messageClass.Namespace + Name = TranslateTypeName(messageClass), + IsObsolete = HasObsoleteAttribute(messageClass.GetCustomAttributesData()) }; + _parsed.Add(messageClass.FullName ?? messageClass.Name, message); + + Protobuf protobuf = GetProtobuf(messageClass, message, options); - FieldInfo[] idFields = messageClass.GetFields(BindingFlags.Public | BindingFlags.Static); - PropertyInfo[] properties = messageClass.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + FieldInfo[] idFields = messageClass.GetFields(PublicStatic); + PropertyInfo[] properties = messageClass.GetProperties(PublicInstanceDeclared); for (int pi = 0, fi = 0; pi < properties.Length; pi++, fi++) { - PropertyInfo property = properties[pi]; + PropertyInfo property = properties[pi]; + IList attributes = property.GetCustomAttributesData(); - if ((skipPropertiesWithoutProtocAttribute && !HasProtocAttribute(property)) + if (((options & ParserOptions.IncludePropertiesWithoutNonUserCodeAttribute) == 0 && !HasNonUserCodeAttribute(attributes)) || property.GetMethod?.IsVirtual != false) { fi--; @@ -99,11 +88,10 @@ private void ParseMessageInternal(Type messageClass, bool skipEnums, bool skipPr Type propertyType = property.PropertyType; // only OneOf enums are defined nested directly in the message class - if (propertyType.IsEnum - && propertyType.DeclaringType?.Name == messageClass.Name) + if (propertyType.IsEnum && propertyType.DeclaringType?.Name == messageClass.Name) { - string oneOfName = TranslateOneOfName(property.Name); - int[] oneOfProtoFieldIds = propertyType.GetFields(BindingFlags.Public | BindingFlags.Static) + string oneOfName = TranslateOneOfPropName(property.Name); + int[] oneOfProtoFieldIds = propertyType.GetFields(PublicStatic) .Select(static field => (int)field.GetRawConstantValue()!) .Where(static id => id > 0) .ToArray(); @@ -118,126 +106,320 @@ private void ParseMessageInternal(Type messageClass, bool skipEnums, bool skipPr Guard.IsTrue(idField.IsLiteral); Guard.IsEqualTo(idField.FieldType.Name, nameof(Int32)); - int msgFieldId = (int)idField.GetRawConstantValue()!; - bool msgFieldIsOptional = false; - string msgFieldType = ParseFieldType(propertyType, skipEnums, skipPropertiesWithoutProtocAttribute, message); - string msgFieldName = TranslateMessageFieldName(property.Name); - - // optional protobuf fields will generate an additional "Has" get-only boolean property immediately after the real property + bool msgFieldHasHasProp = false; // some field properties are immediately followed by an additional "Has" get-only boolean property if (properties.Length > pi + 1 && properties[pi + 1].PropertyType.Name == nameof(Boolean) && !properties[pi + 1].CanWrite) { - msgFieldIsOptional = true; + msgFieldHasHasProp = true; pi++; } - message.Fields.Add(msgFieldId, (msgFieldIsOptional, msgFieldType, msgFieldName)); + MessageField field = new() + { + Type = ParseFieldType(propertyType, options, protobuf), + Name = TranslateMessageFieldName(property.Name), + Id = (int)idField.GetRawConstantValue()!, + IsObsolete = HasObsoleteAttribute(attributes), + HasHasProp = msgFieldHasHasProp + }; + + message.Fields.Add(field.Id, field); } - protobufs.Add(message.Name, message); + return message; } - private void ParseEnumInternal(Type enumEnum, Message? parentMessage) + public Enum ParseEnum(Type enumEnum, ParserOptions options = ParserOptions.None) { - if (IsParsed(enumEnum, parentMessage, out Dictionary protobufs)) + Guard.IsTrue(enumEnum.IsEnum); + + if (_parsed.TryGetValue(enumEnum.FullName ?? enumEnum.Name, out TopLevel? parsedEnum)) { - return; + return (Enum)parsedEnum; } Enum @enum = new() { - Name = TranslateProtobufName(enumEnum.Name), - AssemblyName = enumEnum.Assembly.FullName, - Namespace = enumEnum.Namespace + Name = TranslateTypeName(enumEnum), + IsObsolete = HasObsoleteAttribute(enumEnum.GetCustomAttributesData()) }; + _parsed.Add(enumEnum.FullName ?? enumEnum.Name, @enum); - foreach (FieldInfo field in enumEnum.GetFields(BindingFlags.Public | BindingFlags.Static)) + Protobuf protobuf = GetProtobuf(enumEnum, @enum, options); + + foreach (FieldInfo field in enumEnum.GetFields(PublicStatic)) { - int enumFieldId = (int)field.GetRawConstantValue()!; - string enumFieldName = TranslateEnumFieldName(field, @enum.Name); + @enum.Fields.Add( + new EnumField + { + Id = (int)field.GetRawConstantValue()!, + Name = TranslateEnumFieldName(field, @enum.Name), + IsObsolete = HasObsoleteAttribute(field.GetCustomAttributesData()) + }); + } - @enum.Fields.Add(enumFieldId, enumFieldName); + if (@enum.Fields.All(static field => field.Id != 0)) + { + protobuf.Edition = "2023"; + @enum.IsClosed = true; } - protobufs.Add(@enum.Name, @enum); + return @enum; } - private string ParseFieldType(Type type, bool skipEnums, bool skipPropertiesWithoutProtocAttribute, Message message) + public Service ParseService(Type serviceClass, ParserOptions options = ParserOptions.None) { - switch (type.Name) + Guard.IsTrue(serviceClass.IsClass); + + bool? isClientClass = null; + if (serviceClass.IsAbstract) + { + if (serviceClass is { IsSealed: true, IsNested: false }) + { + Type[] nested = serviceClass.GetNestedTypes(); + serviceClass = nested.SingleOrDefault(static nested => nested is { IsAbstract: true, IsSealed: false }) + ?? nested.Single(static nested => nested is { IsClass: true, IsAbstract: false }); + } + + if (serviceClass is { IsNested: true, IsAbstract: true, IsSealed: false }) + { + isClientClass = false; + } + } + + if (serviceClass is { IsAbstract: false, IsNested: true, DeclaringType: not null }) { - case "ByteString": - return FieldTypeName.Bytes; - case nameof(String): - return FieldTypeName.String; - case nameof(Boolean): - return FieldTypeName.Bool; - case nameof(Double): - return FieldTypeName.Double; - case nameof(UInt32): - return FieldTypeName.UInt32; - case nameof(UInt64): - return FieldTypeName.UInt64; - case nameof(Int32): - return FieldTypeName.Int32; - case nameof(Int64): - return FieldTypeName.Int64; - case nameof(Single): - return FieldTypeName.Float; + isClientClass = true; } + Guard.IsNotNull(isClientClass); + + if (_parsed.TryGetValue(serviceClass.DeclaringType!.FullName ?? serviceClass.DeclaringType!.Name, out TopLevel? parsedService)) + { + return (Service)parsedService; + } + + Service service = new() + { + Name = TranslateTypeName(serviceClass.DeclaringType), + IsObsolete = HasObsoleteAttribute(serviceClass.GetCustomAttributesData()) + }; + _parsed.Add(serviceClass.DeclaringType!.FullName ?? serviceClass.DeclaringType.Name, service); + + Protobuf protobuf = NewProtobuf(serviceClass, service); + + foreach (MethodInfo method in serviceClass.GetMethods(PublicInstanceDeclared)) + { + IList attributes = method.GetCustomAttributesData(); + if ((options & ParserOptions.IncludeServiceMethodsWithoutGeneratedCodeAttribute) == 0 + && !HasGeneratedCodeAttribute(attributes, "grpc_csharp_plugin")) + { + continue; + } + + Type requestType, responseType, returnType = method.ReturnType; + bool streamReq, streamRes; + + if (isClientClass.Value) + { + string returnTypeName = TranslateTypeName(returnType); + if (returnTypeName == "AsyncUnaryCall`1") + { + continue; + } + + ParameterInfo[] parameters = method.GetParameters(); + if (parameters.Length > 2) + { + continue; + } + + Type firstParamType = parameters[0].ParameterType; + switch (returnType.GenericTypeArguments.Length) + { + case 2: + requestType = returnType.GenericTypeArguments[0]; + responseType = returnType.GenericTypeArguments[1]; + streamReq = true; + streamRes = returnTypeName == "AsyncDuplexStreamingCall`2"; + break; + case 1: + requestType = firstParamType; + responseType = returnType.GenericTypeArguments[0]; + streamReq = false; + streamRes = true; + break; + default: + requestType = firstParamType; + responseType = returnType; + streamReq = false; + streamRes = false; + break; + } + } + else + { + ParameterInfo[] parameters = method.GetParameters(); + Type firstParamType = parameters[0].ParameterType; + + if (firstParamType.GenericTypeArguments.Length == 1) + { + streamReq = true; + requestType = firstParamType.GenericTypeArguments[0]; + } + else + { + streamReq = false; + requestType = firstParamType; + } + + if (returnType.GenericTypeArguments.Length == 1) + { + streamRes = false; + responseType = returnType.GenericTypeArguments[0]; + } + else + { + streamRes = true; + responseType = parameters[1].ParameterType.GenericTypeArguments[0]; + } + } + + service.Methods.Add( + new ServiceMethod + { + Name = TranslateMethodName(method.Name), + IsObsolete = HasObsoleteAttribute(attributes), + RequestType = ParseFieldType(requestType, options, protobuf), + ResponseType = ParseFieldType(responseType, options, protobuf), + IsRequestStreamed = streamReq, + IsResponseStreamed = streamRes + }); + } + + return service; + } + + private IType ParseFieldType(Type type, ParserOptions options, Protobuf referencingProtobuf) + { switch (type.GenericTypeArguments.Length) { case 1: - string t = ParseFieldType(type.GenericTypeArguments[0], skipEnums, skipPropertiesWithoutProtocAttribute, message); - return "repeated " + t; + return new Repeated( + ParseFieldType(type.GenericTypeArguments[0], options, referencingProtobuf)); case 2: - string t1 = ParseFieldType(type.GenericTypeArguments[0], skipEnums, skipPropertiesWithoutProtocAttribute, message); - string t2 = ParseFieldType(type.GenericTypeArguments[1], skipEnums, skipPropertiesWithoutProtocAttribute, message); - return $"map<{t1}, {t2}>"; + return new Map( + ParseFieldType(type.GenericTypeArguments[0], options, referencingProtobuf), + ParseFieldType(type.GenericTypeArguments[1], options, referencingProtobuf)); } - if (CustomTypeLookup?.Invoke(type.Name, out string? fieldType) == true) + if (TypeLookup(type, out IType? fieldType, out string? import)) { + if (import is not null) + { + referencingProtobuf.Imports.Add(import); + } + return fieldType; } if (type.IsEnum) { - if (skipEnums) + if ((options & ParserOptions.SkipEnums) > 0) { - return FieldTypeName.Int32; + return Scalar.Int32; } - ParseEnumInternal(type, message); + fieldType = ParseEnum(type, options); } else { - ParseMessageInternal(type, skipEnums, skipPropertiesWithoutProtocAttribute, message); + fieldType = ParseMessage(type, options); } - if (!type.IsNested) + Protobuf protobuf = ((INestableType)fieldType).Protobuf!; + if (referencingProtobuf != protobuf) { - message.Imports.Add(type.Name); + referencingProtobuf.Imports.Add(protobuf.FileName); } - return type.Name; + return fieldType; } - private string TranslateProtobufName(string name) => - CustomNameLookup?.Invoke(name, out string? translatedName) == true - ? translatedName - : name; + private Protobuf NewProtobuf(Type topLevelType, TopLevel topLevel) + { + Protobuf protobuf = new() + { + AssemblyName = topLevelType.Assembly.FullName, + Namespace = topLevelType.Namespace + }; + + topLevel.Protobuf = protobuf; + protobuf.TopLevels.Add(topLevel); + Protobufs.Add(protobuf); + + return protobuf; + } - private string TranslateOneOfName(string oneOfEnumName) => - TranslateName(oneOfEnumName, out string translatedName) - ? translatedName.TrimEnd("Case") - : oneOfEnumName.TrimEnd("Case") - .ToSnakeCaseLower(); + private Protobuf GetProtobuf(Type topLevelType, T topLevel, ParserOptions options) + where T : TopLevel, INestableType + { + Protobuf protobuf; + if (topLevelType.IsNested) + { + Type parent = topLevelType.DeclaringType!.DeclaringType!; + if (!_parsed.TryGetValue(parent.FullName ?? parent.Name, out TopLevel? parentTopLevel)) + { + parentTopLevel = ParseMessage(parent, options); + } + + protobuf = parentTopLevel.Protobuf!; + topLevel.Protobuf = protobuf; + topLevel.Parent = parentTopLevel; - private string TranslateMessageFieldName(string fieldName) => - TranslateName(fieldName, out string translatedName) + ((Message)parentTopLevel).Nested.Add(topLevelType.Name, topLevel); + } + else + { + protobuf = NewProtobuf(topLevelType, topLevel); + } + + return protobuf; + } + + private string TranslateMethodName(string methodName) => + NameLookup?.Invoke(methodName, out string? translatedName) == true ? translatedName - : fieldName.ToSnakeCaseLower(); + : methodName; + + private string TranslateOneOfPropName(string oneOfPropName) + { + if (NameLookup?.Invoke(oneOfPropName, out string? translatedName) != true) + { + if (IsBeebyted(oneOfPropName)) + { + return oneOfPropName; + } + + translatedName = oneOfPropName; + } + + return translatedName!.TrimEnd("Case").ToSnakeCaseLower(); + } + + private string TranslateMessageFieldName(string fieldName) + { + if (NameLookup?.Invoke(fieldName, out string? translatedName) != true) + { + if (IsBeebyted(fieldName)) + { + return fieldName; + } + + translatedName = fieldName; + } + + return translatedName!.ToSnakeCaseLower(); + } private string TranslateEnumFieldName(FieldInfo field, string enumName) { @@ -250,9 +432,14 @@ private string TranslateEnumFieldName(FieldInfo field, string enumName) return originalName; } - if (TranslateName(field.Name, out string translatedName)) + if (NameLookup?.Invoke(field.Name, out string? fieldName) != true) + { + fieldName = field.Name; + } + + if (!IsBeebyted(fieldName!)) { - return translatedName; + fieldName = fieldName!.ToSnakeCaseUpper(); } if (!IsBeebyted(enumName)) @@ -260,26 +447,208 @@ private string TranslateEnumFieldName(FieldInfo field, string enumName) enumName = enumName.ToSnakeCaseUpper(); } - return enumName + '_' + field.Name.ToSnakeCaseUpper(); + return enumName + '_' + fieldName; } - private bool TranslateName(string name, out string translatedName) + private string TranslateTypeName(Type type) { - if (CustomNameLookup?.Invoke(name, out translatedName!) == true) + if (NameLookup is null) + return type.Name; + + string? fullName = type.FullName; + Guard.IsNotNull(fullName); + + int genericArgs = fullName.IndexOf('['); + if (genericArgs != -1) + fullName = fullName[..genericArgs]; + + if (!NameLookup(fullName, out string? translatedName)) { - return true; + return type.Name; } - translatedName = name; - return IsBeebyted(name); + int lastSlash = translatedName.LastIndexOf('/'); + if (lastSlash != -1) + translatedName = translatedName[lastSlash..]; + + int lastDot = translatedName.LastIndexOf('.'); + if (lastDot != -1) + translatedName = translatedName[lastDot..]; + + return translatedName; + } + + public static bool LookupScalarAndWellKnownTypes(Type type, [NotNullWhen(true)] out IType? fieldType, out string? import) + { + switch (type.FullName) + { + case "System.String": + import = null; + fieldType = Scalar.String; + return true; + case "System.Boolean": + import = null; + fieldType = Scalar.Bool; + return true; + case "System.Double": + import = null; + fieldType = Scalar.Double; + return true; + case "System.UInt32": + import = null; + fieldType = Scalar.UInt32; + return true; + case "System.UInt64": + import = null; + fieldType = Scalar.UInt64; + return true; + case "System.Int32": + import = null; + fieldType = Scalar.Int32; + return true; + case "System.Int64": + import = null; + fieldType = Scalar.Int64; + return true; + case "System.Single": + import = null; + fieldType = Scalar.Float; + return true; + case "Google.Protobuf.ByteString": + import = null; + fieldType = Scalar.Bytes; + return true; + case "Google.Protobuf.WellKnownTypes.Any": + import = "google/protobuf/any.proto"; + fieldType = WellKnown.Any; + return true; + case "Google.Protobuf.WellKnownTypes.Api": + import = "google/protobuf/api.proto"; + fieldType = WellKnown.Api; + return true; + case "Google.Protobuf.WellKnownTypes.BoolValue": + import = "google/protobuf/wrappers.proto"; + fieldType = WellKnown.BoolValue; + return true; + case "Google.Protobuf.WellKnownTypes.BytesValue": + import = "google/protobuf/wrappers.proto"; + fieldType = WellKnown.BytesValue; + return true; + case "Google.Protobuf.WellKnownTypes.DoubleValue": + import = "google/protobuf/wrappers.proto"; + fieldType = WellKnown.DoubleValue; + return true; + case "Google.Protobuf.WellKnownTypes.Duration": + import = "google/protobuf/duration.proto"; + fieldType = WellKnown.Duration; + return true; + case "Google.Protobuf.WellKnownTypes.Empty": + import = "google/protobuf/empty.proto"; + fieldType = WellKnown.Empty; + return true; + case "Google.Protobuf.WellKnownTypes.Enum": + import = "google/protobuf/type.proto"; + fieldType = WellKnown.Enum; + return true; + case "Google.Protobuf.WellKnownTypes.EnumValue": + import = "google/protobuf/type.proto"; + fieldType = WellKnown.EnumValue; + return true; + case "Google.Protobuf.WellKnownTypes.Field": + import = "google/protobuf/type.proto"; + fieldType = WellKnown.Field; + return true; + case "Google.Protobuf.WellKnownTypes.FieldMask": + import = "google/protobuf/field_mask.proto"; + fieldType = WellKnown.FieldMask; + return true; + case "Google.Protobuf.WellKnownTypes.FloatValue": + import = "google/protobuf/wrappers.proto"; + fieldType = WellKnown.FloatValue; + return true; + case "Google.Protobuf.WellKnownTypes.Int32Value": + import = "google/protobuf/wrappers.proto"; + fieldType = WellKnown.Int32Value; + return true; + case "Google.Protobuf.WellKnownTypes.Int64Value": + import = "google/protobuf/wrappers.proto"; + fieldType = WellKnown.Int64Value; + return true; + case "Google.Protobuf.WellKnownTypes.ListValue": + import = "google/protobuf/struct.proto"; + fieldType = WellKnown.ListValue; + return true; + case "Google.Protobuf.WellKnownTypes.Method": + import = "google/protobuf/api.proto"; + fieldType = WellKnown.Method; + return true; + case "Google.Protobuf.WellKnownTypes.Mixin": + import = "google/protobuf/api.proto"; + fieldType = WellKnown.Mixin; + return true; + case "Google.Protobuf.WellKnownTypes.NullValue": + import = "google/protobuf/struct.proto"; + fieldType = WellKnown.NullValue; + return true; + case "Google.Protobuf.WellKnownTypes.Option": + import = "google/protobuf/type.proto"; + fieldType = WellKnown.Option; + return true; + case "Google.Protobuf.WellKnownTypes.SourceContext": + import = "google/protobuf/source_context.proto"; + fieldType = WellKnown.SourceContext; + return true; + case "Google.Protobuf.WellKnownTypes.StringValue": + import = "google/protobuf/wrappers.proto"; + fieldType = WellKnown.StringValue; + return true; + case "Google.Protobuf.WellKnownTypes.Struct": + import = "google/protobuf/struct.proto"; + fieldType = WellKnown.Struct; + return true; + case "Google.Protobuf.WellKnownTypes.Syntax": + import = "google/protobuf/type.proto"; + fieldType = WellKnown.Syntax; + return true; + case "Google.Protobuf.WellKnownTypes.Timestamp": + import = "google/protobuf/timestamp.proto"; + fieldType = WellKnown.Timestamp; + return true; + case "Google.Protobuf.WellKnownTypes.Type": + import = "google/protobuf/type.proto"; + fieldType = WellKnown.Type; + return true; + case "Google.Protobuf.WellKnownTypes.UInt32Value": + import = "google/protobuf/wrappers.proto"; + fieldType = WellKnown.UInt32Value; + return true; + case "Google.Protobuf.WellKnownTypes.UInt64Value": + import = "google/protobuf/wrappers.proto"; + fieldType = WellKnown.UInt64Value; + return true; + case "Google.Protobuf.WellKnownTypes.Value": + import = "google/protobuf/struct.proto"; + fieldType = WellKnown.Value; + return true; + + default: + import = null; + fieldType = null; + return false; + } } // ReSharper disable once IdentifierTypo private static bool IsBeebyted(string name) => name.Length == 11 && name.CountUpper() == 11; - private static bool HasProtocAttribute(MemberInfo member) => - member.GetCustomAttributesData() - .Any(static attr => attr.AttributeType.Name == nameof(GeneratedCodeAttribute) - && attr.ConstructorArguments[0].Value as string == "protoc"); + private static bool HasGeneratedCodeAttribute(IEnumerable attributes, string tool) => + attributes.Any(attr => attr.AttributeType.Name == nameof(GeneratedCodeAttribute) + && attr.ConstructorArguments[0].Value as string == tool); + + private static bool HasNonUserCodeAttribute(IEnumerable attributes) => + attributes.Any(static attr => attr.AttributeType.Name == nameof(DebuggerNonUserCodeAttribute)); + + private static bool HasObsoleteAttribute(IEnumerable attributes) => + attributes.Any(static attr => attr.AttributeType.Name == nameof(ObsoleteAttribute)); } \ No newline at end of file diff --git a/src/protodec/Program.cs b/src/protodec/Program.cs index fc9d631..e8aad25 100644 --- a/src/protodec/Program.cs +++ b/src/protodec/Program.cs @@ -1,8 +1,10 @@ using System; using System.CodeDom.Compiler; +using System.Collections.Generic; using System.IO; using System.Linq; using LibProtodec; +using LibProtodec.Models; const string indent = " "; const string help = """ @@ -11,8 +13,11 @@ target_assembly_path Either the path to the target assembly or a directory of assemblies, all of which be parsed. out_path An existing directory to output into individual files, otherwise output to a single file. Options: - --skip_enums Skip parsing enums and replace references to them with int32. - --skip_properties_without_protoc_attribute Skip properties that aren't decorated with `GeneratedCode("protoc")` when parsing + --parse_service_servers Parses gRPC service definitions from server classes. + --parse_service_clients Parses gRPC service definitions from client classes. + --skip_enums Skip parsing enums and replace references to them with int32. + --include_properties_without_non_user_code_attribute Includes properties that aren't decorated with `DebuggerNonUserCode` when parsing. + --include_service_methods_without_generated_code_attribute Includes methods that aren't decorated with `GeneratedCode("grpc_csharp_plugin")` when parsing gRPC services. """; if (args.Length < 2) @@ -21,29 +26,63 @@ return; } -string assemblyPath = args[0]; -string outPath = Path.GetFullPath(args[1]); -bool skipEnums = args.Contains("--skip_enums"); -bool skipPropertiesWithoutProtocAttribute = args.Contains("--skip_properties_without_protoc_attribute"); +string assembly = args[0]; +string outPath = Path.GetFullPath(args[1]); +ParserOptions options = ParserOptions.None; -using AssemblyInspector inspector = new(assemblyPath); +if (args.Contains("--skip_enums")) + options |= ParserOptions.SkipEnums; + +if (args.Contains("--include_properties_without_non_user_code_attribute")) + options |= ParserOptions.IncludePropertiesWithoutNonUserCodeAttribute; + +if (args.Contains("--include_service_methods_without_generated_code_attribute")) + options |= ParserOptions.IncludeServiceMethodsWithoutGeneratedCodeAttribute; + +using AssemblyInspector inspector = new(assembly); ProtodecContext ctx = new(); foreach (Type message in inspector.GetProtobufMessageTypes()) { - ctx.ParseMessage(message, skipEnums, skipPropertiesWithoutProtocAttribute); + ctx.ParseMessage(message, options); +} + +if (args.Contains("--parse_service_servers")) +{ + foreach (Type service in inspector.GetProtobufServiceServerTypes()) + { + ctx.ParseService(service, options); + } +} + +if (args.Contains("--parse_service_clients")) +{ + foreach (Type service in inspector.GetProtobufServiceClientTypes()) + { + ctx.ParseService(service, options); + } } if (Directory.Exists(outPath)) { - foreach (Protobuf proto in ctx.Protobufs.Values) + HashSet writtenFiles = []; + + foreach (Protobuf protobuf in ctx.Protobufs) { - string protoPath = Path.Join(outPath, proto.Name + ".proto"); + // This workaround stops files from being overwritten in the case of a naming conflict, + // however the actual conflict will still have to be resolved manually + string fileName = protobuf.FileName; + while (!writtenFiles.Add(fileName)) + { + fileName = '_' + fileName; + } + + string protobufPath = Path.Join(outPath, fileName); - using StreamWriter streamWriter = new(protoPath); + using StreamWriter streamWriter = new(protobufPath); using IndentedTextWriter indentWriter = new(streamWriter, indent); - proto.WriteFileTo(indentWriter); + protobuf.WriteTo(indentWriter); } } else