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: protodec(.exe) <target_assembly_path> <out_path> [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] <string> Either the path to the target assembly or a directory of assemblies, all of which be parsed.
[1] <string> 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 <LogLevel> Logging severity level. (Default: Information)
```
Limitations

View File

@ -18,9 +18,9 @@
</PropertyGroup>
<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="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" />
</ItemGroup>

View File

@ -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<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() { }
protected abstract ICilType FindType(string typeFullName, string assemblySimpleName);

View File

@ -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<ClrAssemblyLoader>? logger = null)
{
bool isFile = File.Exists(assemblyPath);
string assemblyDir = isFile

View File

@ -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<string, TopLevel> _parsed = [];
public readonly List<Protobuf> Protobufs = [];
public ILogger? Logger { get; set; }
public ILogger<ProtodecContext>? Logger { 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
// 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) <target_assembly_path> <out_path> [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<Commands>();
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<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())
/// <summary>
/// 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>
/// <param name="outPath">An existing directory to output into individual files, otherwise output to a single file.</param>
/// <param name="logLevel">Logging severity level.</param>
/// <param name="parseServiceServers">Parses gRPC service definitions from server classes.</param>
/// <param name="parseServiceClients">Parses gRPC service definitions from client classes.</param>
/// <param name="skipEnums">Skip parsing enums and replace references to them with int32.</param>
/// <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>
[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<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))
logger.LogInformation("Loading target assemblies...");
using CilAssemblyLoader loader = new ClrAssemblyLoader(
targetPath,
loggerFactory.CreateLogger<ClrAssemblyLoader>());
ProtodecContext ctx = new()
{
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);
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<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>
<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>
</Project>