From 2126f61cbf6ae18fcb8de26b80973b830f960f18 Mon Sep 17 00:00:00 2001 From: Xpl0itR Date: Fri, 26 Jul 2024 03:04:44 +0100 Subject: [PATCH] Use ConsoleAppFramework for CLI handling also move Get*Types to CilAssemblyLoader --- README.md | 20 +- src/LibProtodec/LibProtodec.csproj | 4 +- src/LibProtodec/Loaders/CilAssemblyLoader.cs | 43 ++-- src/LibProtodec/Loaders/ClrAssemblyLoader.cs | 2 +- src/LibProtodec/ProtodecContext.cs | 4 +- src/protodec/Program.cs | 214 +++++++++---------- src/protodec/protodec.csproj | 3 +- 7 files changed, 148 insertions(+), 142 deletions(-) diff --git a/README.md b/README.md index fb55d3a..b350840 100644 --- a/README.md +++ b/README.md @@ -5,17 +5,19 @@ A tool to decompile protobuf classes compiled by [protoc](https://github.com/pro Usage ----- ``` -Usage: protodec(.exe) [options] +Usage: [arguments...] [options...] [-h|--help] [--version] + 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. + [0] Either the path to the target assembly or a directory of assemblies, all of which be parsed. + [1] An existing directory to output into individual files, otherwise output to a single file. + Options: - --debug Drops the minimum log level to Debug. - --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. + --skip-enums Skip parsing enums and replace references to them with int32. (Optional) + --include-properties-without-non-user-code-attribute Includes properties that aren't decorated with `DebuggerNonUserCode` when parsing. (Optional) + --include-service-methods-without-generated-code-attribute Includes methods that aren't decorated with `GeneratedCode("grpc_csharp_plugin")` when parsing gRPC services. (Optional) + --parse-service-servers Parses gRPC service definitions from server classes. (Optional) + --parse-service-clients Parses gRPC service definitions from client classes. (Optional) + --log-level Logging severity level. (Default: Information) ``` Limitations diff --git a/src/LibProtodec/LibProtodec.csproj b/src/LibProtodec/LibProtodec.csproj index 87a204e..2bf839e 100644 --- a/src/LibProtodec/LibProtodec.csproj +++ b/src/LibProtodec/LibProtodec.csproj @@ -18,9 +18,9 @@ - + - + diff --git a/src/LibProtodec/Loaders/CilAssemblyLoader.cs b/src/LibProtodec/Loaders/CilAssemblyLoader.cs index ab803b2..da01b6c 100644 --- a/src/LibProtodec/Loaders/CilAssemblyLoader.cs +++ b/src/LibProtodec/Loaders/CilAssemblyLoader.cs @@ -6,28 +6,43 @@ using System; using System.Collections.Generic; +using System.Linq; using LibProtodec.Models.Cil; namespace LibProtodec.Loaders; public abstract class CilAssemblyLoader : IDisposable { - private ICilType? _iMessage; - private ICilType? _clientBase; - private ICilType? _bindServiceMethodAttribute; - - // ReSharper disable once InconsistentNaming - public ICilType IMessage => - _iMessage ??= FindType("Google.Protobuf.IMessage", "Google.Protobuf"); - - public ICilType ClientBase => - _clientBase ??= FindType("Grpc.Core.ClientBase", "Grpc.Core.Api"); - - public ICilType BindServiceMethodAttribute => - _bindServiceMethodAttribute ??= FindType("Grpc.Core.BindServiceMethodAttribute", "Grpc.Core.Api"); - public IReadOnlyList LoadedTypes { get; protected init; } + public IEnumerable GetProtobufMessageTypes() + { + ICilType iMessage = FindType("Google.Protobuf.IMessage", "Google.Protobuf"); + + return LoadedTypes.Where( + type => type is { IsNested: false, IsSealed: true } + && type.Namespace?.StartsWith("Google.Protobuf", StringComparison.Ordinal) != true + && type.IsAssignableTo(iMessage)); + } + + public IEnumerable GetProtobufServiceClientTypes() + { + ICilType clientBase = FindType("Grpc.Core.ClientBase", "Grpc.Core.Api"); + + return LoadedTypes.Where( + type => type is { IsNested: true, IsAbstract: false } + && type.IsAssignableTo(clientBase)); + } + + public IEnumerable GetProtobufServiceServerTypes() + { + ICilType bindServiceMethodAttribute = FindType("Grpc.Core.BindServiceMethodAttribute", "Grpc.Core.Api"); + + return LoadedTypes.Where( + type => type is { IsNested: true, IsAbstract: true, DeclaringType: { IsNested: false, IsSealed: true, IsAbstract: true } } + && type.CustomAttributes.Any(attribute => attribute.Type == bindServiceMethodAttribute)); + } + public virtual void Dispose() { } protected abstract ICilType FindType(string typeFullName, string assemblySimpleName); diff --git a/src/LibProtodec/Loaders/ClrAssemblyLoader.cs b/src/LibProtodec/Loaders/ClrAssemblyLoader.cs index 2aaf143..0aa0f7f 100644 --- a/src/LibProtodec/Loaders/ClrAssemblyLoader.cs +++ b/src/LibProtodec/Loaders/ClrAssemblyLoader.cs @@ -20,7 +20,7 @@ public sealed class ClrAssemblyLoader : CilAssemblyLoader { public readonly MetadataLoadContext LoadContext; - public ClrAssemblyLoader(string assemblyPath, ILogger? logger = null) + public ClrAssemblyLoader(string assemblyPath, ILogger? logger = null) { bool isFile = File.Exists(assemblyPath); string assemblyDir = isFile diff --git a/src/LibProtodec/ProtodecContext.cs b/src/LibProtodec/ProtodecContext.cs index 5aa905e..ef0d09b 100644 --- a/src/LibProtodec/ProtodecContext.cs +++ b/src/LibProtodec/ProtodecContext.cs @@ -23,14 +23,14 @@ namespace LibProtodec; public delegate bool NameLookupFunc(string name, [MaybeNullWhen(false)] out string translatedName); -// ReSharper disable ClassWithVirtualMembersNeverInherited.Global, MemberCanBePrivate.Global, MemberCanBeProtected.Global +// ReSharper disable ClassWithVirtualMembersNeverInherited.Global, MemberCanBePrivate.Global, MemberCanBeProtected.Global, PropertyCanBeMadeInitOnly.Global public class ProtodecContext { private readonly Dictionary _parsed = []; public readonly List Protobufs = []; - public ILogger? Logger { get; set; } + public ILogger? Logger { get; set; } public NameLookupFunc? NameLookup { get; set; } diff --git a/src/protodec/Program.cs b/src/protodec/Program.cs index cd6040a..9e46bb0 100644 --- a/src/protodec/Program.cs +++ b/src/protodec/Program.cs @@ -4,137 +4,125 @@ // 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; using System.CodeDom.Compiler; using System.Collections.Generic; using System.IO; -using System.Linq; +using ConsoleAppFramework; using LibProtodec; using LibProtodec.Loaders; using LibProtodec.Models.Cil; using LibProtodec.Models.Protobuf; using Microsoft.Extensions.Logging; -const string indent = " "; -const string help = """ - Usage: protodec(.exe) [options] - 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: - --debug Drops the minimum log level to Debug. - --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. - """; +ConsoleApp.ConsoleAppBuilder app = ConsoleApp.Create(); +app.Add(); +app.Run(args); -if (args.Length < 2) +internal sealed class Commands { - Console.WriteLine(help); - return; -} - -string assembly = args[0]; -string outPath = Path.GetFullPath(args[1]); -ParserOptions options = ParserOptions.None; -LogLevel logLevel = args.Contains("--debug") - ? LogLevel.Debug - : LogLevel.Information; - -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 ILoggerFactory loggerFactory = LoggerFactory.Create( - builder => builder.AddSimpleConsole(static console => console.IncludeScopes = true) - .SetMinimumLevel(logLevel)); -ILogger logger = loggerFactory.CreateLogger("protodec"); - -logger.LogInformation("Loading target assemblies..."); -using CilAssemblyLoader loader = new ClrAssemblyLoader( - assembly, loggerFactory.CreateLogger()); - -ProtodecContext ctx = new() -{ - Logger = loggerFactory.CreateLogger() -}; - -logger.LogInformation("Parsing Protobuf message types..."); -foreach (ICilType message in GetProtobufMessageTypes()) -{ - ctx.ParseMessage(message, options); -} - -if (args.Contains("--parse_service_servers")) -{ - logger.LogInformation("Parsing Protobuf service server types..."); - foreach (ICilType service in GetProtobufServiceServerTypes()) + /// + /// A tool to decompile protobuf classes compiled by protoc, from CIL assemblies back into .proto definitions. + /// + /// Either the path to the target assembly or a directory of assemblies, all of which be parsed. + /// An existing directory to output into individual files, otherwise output to a single file. + /// Logging severity level. + /// Parses gRPC service definitions from server classes. + /// Parses gRPC service definitions from client classes. + /// Skip parsing enums and replace references to them with int32. + /// Includes properties that aren't decorated with `DebuggerNonUserCode` when parsing. + /// Includes methods that aren't decorated with `GeneratedCode("grpc_csharp_plugin")` when parsing gRPC services. + [Command("")] + public void Root( + [Argument] string targetPath, + [Argument] string outPath, + bool skipEnums, + bool includePropertiesWithoutNonUserCodeAttribute, + bool includeServiceMethodsWithoutGeneratedCodeAttribute, + bool parseServiceServers, + bool parseServiceClients, + LogLevel logLevel = LogLevel.Information) { - ctx.ParseService(service, options); - } -} + ParserOptions options = ParserOptions.None; + if (skipEnums) + options |= ParserOptions.SkipEnums; + if (includePropertiesWithoutNonUserCodeAttribute) + options |= ParserOptions.IncludePropertiesWithoutNonUserCodeAttribute; + if (includeServiceMethodsWithoutGeneratedCodeAttribute) + options |= ParserOptions.IncludeServiceMethodsWithoutGeneratedCodeAttribute; -if (args.Contains("--parse_service_clients")) -{ - logger.LogInformation("Parsing Protobuf service client types..."); - foreach (ICilType service in GetProtobufServiceClientTypes()) - { - ctx.ParseService(service, options); - } -} + using ILoggerFactory loggerFactory = LoggerFactory.Create( + builder => builder.AddSimpleConsole(static console => console.IncludeScopes = true) + .SetMinimumLevel(logLevel)); -if (Directory.Exists(outPath)) -{ - logger.LogInformation("Writing {count} Protobuf files to \"{path}\"...", ctx.Protobufs.Count, outPath); + ILogger logger = loggerFactory.CreateLogger("protodec"); + ConsoleApp.LogError = msg => logger.LogError(msg); - HashSet writtenFiles = []; - foreach (Protobuf protobuf in ctx.Protobufs) - { - // 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)) + logger.LogInformation("Loading target assemblies..."); + using CilAssemblyLoader loader = new ClrAssemblyLoader( + targetPath, + loggerFactory.CreateLogger()); + + ProtodecContext ctx = new() { - fileName = '_' + fileName; + Logger = loggerFactory.CreateLogger() + }; + + logger.LogInformation("Parsing Protobuf message types..."); + foreach (ICilType message in loader.GetProtobufMessageTypes()) + { + ctx.ParseMessage(message, options); } - string protobufPath = Path.Join(outPath, fileName); + if (parseServiceServers) + { + logger.LogInformation("Parsing Protobuf service server types..."); + foreach (ICilType service in loader.GetProtobufServiceServerTypes()) + { + ctx.ParseService(service, options); + } + } - using StreamWriter streamWriter = new(protobufPath); - using IndentedTextWriter indentWriter = new(streamWriter, indent); + if (parseServiceClients) + { + logger.LogInformation("Parsing Protobuf service client types..."); + foreach (ICilType service in loader.GetProtobufServiceClientTypes()) + { + ctx.ParseService(service, options); + } + } - protobuf.WriteTo(indentWriter); + const string indent = " "; + if (Directory.Exists(outPath)) + { + logger.LogInformation("Writing {count} Protobuf files to \"{path}\"...", ctx.Protobufs.Count, outPath); + + HashSet writtenFiles = []; + foreach (Protobuf protobuf in ctx.Protobufs) + { + // 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(protobufPath); + using IndentedTextWriter indentWriter = new(streamWriter, indent); + + protobuf.WriteTo(indentWriter); + } + } + else + { + logger.LogInformation("Writing Protobufs as a single file to \"{path}\"...", outPath); + + using StreamWriter streamWriter = new(outPath); + using IndentedTextWriter indentWriter = new(streamWriter, indent); + + ctx.WriteAllTo(indentWriter); + } } -} -else -{ - logger.LogInformation("Writing Protobufs as a single file to \"{path}\"...", outPath); - - using StreamWriter streamWriter = new(outPath); - using IndentedTextWriter indentWriter = new(streamWriter, indent); - - ctx.WriteAllTo(indentWriter); -} - -IEnumerable GetProtobufMessageTypes() => - loader.LoadedTypes.Where( - type => type is { IsNested: false, IsSealed: true } - && type.Namespace?.StartsWith("Google.Protobuf", StringComparison.Ordinal) != true - && type.IsAssignableTo(loader.IMessage)); - -IEnumerable GetProtobufServiceClientTypes() => - loader.LoadedTypes.Where( - type => type is { IsNested: true, IsAbstract: false } - && type.IsAssignableTo(loader.ClientBase)); - -IEnumerable GetProtobufServiceServerTypes() => - loader.LoadedTypes.Where( - type => type is { IsNested: true, IsAbstract: true, DeclaringType: { IsNested: false, IsSealed: true, IsAbstract: true } } - && type.CustomAttributes.Any(attribute => attribute.Type == loader.BindServiceMethodAttribute)); \ No newline at end of file +} \ No newline at end of file diff --git a/src/protodec/protodec.csproj b/src/protodec/protodec.csproj index 9a5da0a..1ba4246 100644 --- a/src/protodec/protodec.csproj +++ b/src/protodec/protodec.csproj @@ -12,7 +12,8 @@ - + + \ No newline at end of file