mirror of https://github.com/Xpl0itR/protodec.git
Use ConsoleAppFramework for CLI handling
also move Get*Types to CilAssemblyLoader
This commit is contained in:
parent
e94fecce66
commit
2126f61cbf
20
README.md
20
README.md
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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; }
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
|
@ -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>
|
Loading…
Reference in New Issue