Use ConsoleAppFramework for CLI handling

also move Get*Types to CilAssemblyLoader
This commit is contained in:
Xpl0itR 2024-07-26 03:04:44 +01:00
parent e94fecce66
commit 2126f61cbf
Signed by: Xpl0itR
GPG Key ID: 91798184109676AD
7 changed files with 148 additions and 142 deletions

View File

@ -5,17 +5,19 @@ A tool to decompile protobuf classes compiled by [protoc](https://github.com/pro
Usage Usage
----- -----
``` ```
Usage: protodec(.exe) <target_assembly_path> <out_path> [options] Usage: [arguments...] [options...] [-h|--help] [--version]
Arguments: Arguments:
target_assembly_path Either the path to the target assembly or a directory of assemblies, all of which be parsed. [0] <string> 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. [1] <string> An existing directory to output into individual files, otherwise output to a single file.
Options: Options:
--debug Drops the minimum log level to Debug. --skip-enums Skip parsing enums and replace references to them with int32. (Optional)
--parse_service_servers Parses gRPC service definitions from server classes. --include-properties-without-non-user-code-attribute Includes properties that aren't decorated with `DebuggerNonUserCode` when parsing. (Optional)
--parse_service_clients Parses gRPC service definitions from client classes. --include-service-methods-without-generated-code-attribute Includes methods that aren't decorated with `GeneratedCode("grpc_csharp_plugin")` when parsing gRPC services. (Optional)
--skip_enums Skip parsing enums and replace references to them with int32. --parse-service-servers Parses gRPC service definitions from server classes. (Optional)
--include_properties_without_non_user_code_attribute Includes properties that aren't decorated with `DebuggerNonUserCode` when parsing. --parse-service-clients Parses gRPC service definitions from client classes. (Optional)
--include_service_methods_without_generated_code_attribute Includes methods that aren't decorated with `GeneratedCode("grpc_csharp_plugin")` when parsing gRPC services. --log-level <LogLevel> Logging severity level. (Default: Information)
``` ```
Limitations Limitations

View File

@ -18,9 +18,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0-preview.5.24306.7" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0-preview.6.24327.7" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" /> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
<PackageReference Include="System.Reflection.MetadataLoadContext" Version="9.0.0-preview.5.24306.7" /> <PackageReference Include="System.Reflection.MetadataLoadContext" Version="9.0.0-preview.6.24327.7" />
<PackageReference Include="Xpl0itR.SystemEx" Version="1.2.0" /> <PackageReference Include="Xpl0itR.SystemEx" Version="1.2.0" />
</ItemGroup> </ItemGroup>

View File

@ -6,28 +6,43 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using LibProtodec.Models.Cil; using LibProtodec.Models.Cil;
namespace LibProtodec.Loaders; namespace LibProtodec.Loaders;
public abstract class CilAssemblyLoader : IDisposable 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<ICilType> LoadedTypes { get; protected init; } public IReadOnlyList<ICilType> LoadedTypes { get; protected init; }
public IEnumerable<ICilType> 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<ICilType> 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<ICilType> 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() { } public virtual void Dispose() { }
protected abstract ICilType FindType(string typeFullName, string assemblySimpleName); protected abstract ICilType FindType(string typeFullName, string assemblySimpleName);

View File

@ -20,7 +20,7 @@ public sealed class ClrAssemblyLoader : CilAssemblyLoader
{ {
public readonly MetadataLoadContext LoadContext; public readonly MetadataLoadContext LoadContext;
public ClrAssemblyLoader(string assemblyPath, ILogger? logger = null) public ClrAssemblyLoader(string assemblyPath, ILogger<ClrAssemblyLoader>? logger = null)
{ {
bool isFile = File.Exists(assemblyPath); bool isFile = File.Exists(assemblyPath);
string assemblyDir = isFile string assemblyDir = isFile

View File

@ -23,14 +23,14 @@ namespace LibProtodec;
public delegate bool NameLookupFunc(string name, [MaybeNullWhen(false)] out string translatedName); 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 public class ProtodecContext
{ {
private readonly Dictionary<string, TopLevel> _parsed = []; private readonly Dictionary<string, TopLevel> _parsed = [];
public readonly List<Protobuf> Protobufs = []; public readonly List<Protobuf> Protobufs = [];
public ILogger? Logger { get; set; } public ILogger<ProtodecContext>? Logger { get; set; }
public NameLookupFunc? NameLookup { get; set; } public NameLookupFunc? NameLookup { get; set; }

View File

@ -4,137 +4,125 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this // 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/. // file, You can obtain one at http://mozilla.org/MPL/2.0/.
using System;
using System.CodeDom.Compiler; using System.CodeDom.Compiler;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using ConsoleAppFramework;
using LibProtodec; using LibProtodec;
using LibProtodec.Loaders; using LibProtodec.Loaders;
using LibProtodec.Models.Cil; using LibProtodec.Models.Cil;
using LibProtodec.Models.Protobuf; using LibProtodec.Models.Protobuf;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
const string indent = " "; ConsoleApp.ConsoleAppBuilder app = ConsoleApp.Create();
const string help = """ app.Add<Commands>();
Usage: protodec(.exe) <target_assembly_path> <out_path> [options] app.Run(args);
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.
""";
if (args.Length < 2) internal sealed class Commands
{ {
Console.WriteLine(help); /// <summary>
return; /// A tool to decompile protobuf classes compiled by protoc, from CIL assemblies back into .proto definitions.
} /// </summary>
/// <param name="targetPath">Either the path to the target assembly or a directory of assemblies, all of which be parsed.</param>
string assembly = args[0]; /// <param name="outPath">An existing directory to output into individual files, otherwise output to a single file.</param>
string outPath = Path.GetFullPath(args[1]); /// <param name="logLevel">Logging severity level.</param>
ParserOptions options = ParserOptions.None; /// <param name="parseServiceServers">Parses gRPC service definitions from server classes.</param>
LogLevel logLevel = args.Contains("--debug") /// <param name="parseServiceClients">Parses gRPC service definitions from client classes.</param>
? LogLevel.Debug /// <param name="skipEnums">Skip parsing enums and replace references to them with int32.</param>
: LogLevel.Information; /// <param name="includePropertiesWithoutNonUserCodeAttribute">Includes properties that aren't decorated with `DebuggerNonUserCode` when parsing.</param>
/// <param name="includeServiceMethodsWithoutGeneratedCodeAttribute">Includes methods that aren't decorated with `GeneratedCode("grpc_csharp_plugin")` when parsing gRPC services.</param>
if (args.Contains("--skip_enums")) [Command("")]
options |= ParserOptions.SkipEnums; public void Root(
[Argument] string targetPath,
if (args.Contains("--include_properties_without_non_user_code_attribute")) [Argument] string outPath,
options |= ParserOptions.IncludePropertiesWithoutNonUserCodeAttribute; bool skipEnums,
bool includePropertiesWithoutNonUserCodeAttribute,
if (args.Contains("--include_service_methods_without_generated_code_attribute")) bool includeServiceMethodsWithoutGeneratedCodeAttribute,
options |= ParserOptions.IncludeServiceMethodsWithoutGeneratedCodeAttribute; bool parseServiceServers,
bool parseServiceClients,
using ILoggerFactory loggerFactory = LoggerFactory.Create( LogLevel logLevel = LogLevel.Information)
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<ClrAssemblyLoader>());
ProtodecContext ctx = new()
{
Logger = loggerFactory.CreateLogger<ProtodecContext>()
};
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())
{ {
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")) using ILoggerFactory loggerFactory = LoggerFactory.Create(
{ builder => builder.AddSimpleConsole(static console => console.IncludeScopes = true)
logger.LogInformation("Parsing Protobuf service client types..."); .SetMinimumLevel(logLevel));
foreach (ICilType service in GetProtobufServiceClientTypes())
{
ctx.ParseService(service, options);
}
}
if (Directory.Exists(outPath)) ILogger logger = loggerFactory.CreateLogger("protodec");
{ ConsoleApp.LogError = msg => logger.LogError(msg);
logger.LogInformation("Writing {count} Protobuf files to \"{path}\"...", ctx.Protobufs.Count, outPath);
HashSet<string> writtenFiles = []; logger.LogInformation("Loading target assemblies...");
foreach (Protobuf protobuf in ctx.Protobufs) using CilAssemblyLoader loader = new ClrAssemblyLoader(
{ targetPath,
// This workaround stops files from being overwritten in the case of a naming conflict, loggerFactory.CreateLogger<ClrAssemblyLoader>());
// however the actual conflict will still have to be resolved manually
string fileName = protobuf.FileName; ProtodecContext ctx = new()
while (!writtenFiles.Add(fileName))
{ {
fileName = '_' + fileName; Logger = loggerFactory.CreateLogger<ProtodecContext>()
};
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); if (parseServiceClients)
using IndentedTextWriter indentWriter = new(streamWriter, indent); {
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<string> 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<ICilType> GetProtobufMessageTypes() =>
loader.LoadedTypes.Where(
type => type is { IsNested: false, IsSealed: true }
&& type.Namespace?.StartsWith("Google.Protobuf", StringComparison.Ordinal) != true
&& type.IsAssignableTo(loader.IMessage));
IEnumerable<ICilType> GetProtobufServiceClientTypes() =>
loader.LoadedTypes.Where(
type => type is { IsNested: true, IsAbstract: false }
&& type.IsAssignableTo(loader.ClientBase));
IEnumerable<ICilType> 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));

View File

@ -12,7 +12,8 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="$(SolutionDir)src\LibProtodec\LibProtodec.csproj" /> <ProjectReference Include="$(SolutionDir)src\LibProtodec\LibProtodec.csproj" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0-preview.5.24306.7" /> <PackageReference Include="ConsoleAppFramework" Version="5.2.2" PrivateAssets="All" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0-preview.6.24327.7" />
</ItemGroup> </ItemGroup>
</Project> </Project>