More improvements

including but not limited to:
- Improve assembly loading
- Enable AoT publish
- Add github action for building releases
This commit is contained in:
Xpl0itR 2024-01-22 01:33:05 +00:00
parent e41df439ea
commit c61334d291
Signed by: Xpl0itR
GPG Key ID: 91798184109676AD
12 changed files with 186 additions and 167 deletions

41
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,41 @@
name: Release
on:
release:
types:
- published
jobs:
release:
strategy:
matrix:
include:
- runtime: 'linux-x64'
os: 'ubuntu-latest'
- runtime: 'win-x64'
os: 'windows-latest'
runs-on: ${{ matrix.os }}
steps:
- name: Git checkout
uses: actions/checkout@v4
- name: Setup dotnet
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
- name: Build protodec
run: dotnet publish --configuration Release --runtime ${{ matrix.runtime }} /p:VersionPrefix=${{ github.ref_name }} /p:ContinuousIntegrationBuild=true
- name: Pack LibProtodec
if: matrix.os == 'ubuntu-latest'
run: dotnet pack --configuration Release /p:VersionPrefix=${{ github.ref_name }} /p:ContinuousIntegrationBuild=true
- name: Release
uses: softprops/action-gh-release@v1
with:
files: |
bin/protodec/Release/net8.0/${{ matrix.runtime }}/publish/protodec*
bin/LibProtodec/Release/*nupkg

View File

@ -1,15 +1,14 @@
protodec protodec
======== ========
A tool to decompile protobuf parser/serializer classes compiled by [protoc](https://github.com/protocolbuffers/protobuf), from dotnet assemblies back into .proto definitions. A tool to decompile protobuf classes compiled by [protoc](https://github.com/protocolbuffers/protobuf), from CIL assemblies back into .proto definitions.
Usage Usage
----- -----
``` ```
Usage: protodec(.exe) <target_assembly_dir> <out_path> [target_assembly_name] [options] Usage: protodec(.exe) <target_assembly_path> <out_path> [options]
Arguments: Arguments:
target_assembly_dir A directory of assemblies to be loaded. 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. out_path An existing directory to output into individual files, otherwise output to a single file.
target_assembly_name The name of an assembly to parse. If omitted, all assemblies in the target_assembly_dir will be parsed.
Options: Options:
--skip_enums Skip parsing enums and replace references to them with int32. --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 --skip_properties_without_protoc_attribute Skip properties that aren't decorated with `GeneratedCode("protoc")` when parsing
@ -17,13 +16,9 @@ Options:
Limitations Limitations
----------- -----------
- Integers are assumed to be (u)int32/64 as C# doesn't differentiate between them and sint32/64 and (s)fixed32/64. - Integers are assumed to be (u)int32/64 as CIL doesn't differentiate between them and sint32/64 and (s)fixed32/64.
### Decompiling from [Il2CppDumper](https://github.com/Perfare/Il2CppDumper) DummyDLLs - When decompiling from [Il2CppDumper](https://github.com/Perfare/Il2CppDumper) DummyDLLs
- The `Name` parameter of `OriginalNameAttribute` is not dumped. In this case the C# names are used after conforming them to protobuf conventions - The `Name` parameter of `OriginalNameAttribute` is not dumped. In this case, the CIL enum field names are used after conforming them to protobuf conventions
- Dumped assemblies depend on strong-named core libs, however the ones dumped are not strong-named.
This interferes with loading and can be mitigated by copying the assemblies from your runtime into the target assembly directory.
I recommend using [Cpp2IL](https://github.com/SamboyCoding/Cpp2IL) instead of Il2CppDumper.
License License
------- -------

View File

@ -1,4 +1,10 @@
using System; // 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -11,33 +17,54 @@ public sealed class AssemblyInspector : IDisposable
public readonly MetadataLoadContext AssemblyContext; public readonly MetadataLoadContext AssemblyContext;
public readonly IReadOnlyList<Type> LoadedTypes; public readonly IReadOnlyList<Type> LoadedTypes;
public AssemblyInspector(string assemblyDir, string? assemblyName = null) public AssemblyInspector(string assemblyPath)
{ {
string[] assemblyPaths = Directory.EnumerateFiles(assemblyDir, searchPattern: "*.dll") bool isFile = File.Exists(assemblyPath);
.ToArray(); string assemblyDir = isFile
? Path.GetDirectoryName(assemblyPath)!
: assemblyPath;
AssemblyContext = new MetadataLoadContext( PermissiveAssemblyResolver assemblyResolver = new(
new PathAssemblyResolver(assemblyPaths)); Directory.EnumerateFiles(assemblyDir, searchPattern: "*.dll"));
LoadedTypes = assemblyName is null AssemblyContext = new MetadataLoadContext(assemblyResolver);
? assemblyPaths.SelectMany(path => AssemblyContext.LoadFromAssemblyPath(path).GetTypes()).ToList() LoadedTypes = isFile
: AssemblyContext.LoadFromAssemblyName(assemblyName).GetTypes(); ? AssemblyContext.LoadFromAssemblyPath(assemblyPath).GetTypes()
: assemblyResolver.AssemblyPathLookup.Values.SelectMany(path => AssemblyContext.LoadFromAssemblyPath(path).GetTypes()).ToList();
} }
public IEnumerable<Type> GetProtobufMessageTypes() public IEnumerable<Type> GetProtobufMessageTypes()
{ {
Type googleProtobufIMessage = AssemblyContext.LoadFromAssemblyName("Google.Protobuf") Type? googleProtobufIMessage = AssemblyContext.LoadFromAssemblyName("Google.Protobuf")
.GetType("Google.Protobuf.IMessage")!; .GetType("Google.Protobuf.IMessage");
return from type return from type
in LoadedTypes in LoadedTypes
where !type.IsNested where !type.IsNested
&& type.IsSealed && type.IsSealed
&& type.Namespace != "Google.Protobuf.Reflection" && type.Namespace?.StartsWith("Google.Protobuf", StringComparison.Ordinal) != true
&& type.Namespace != "Google.Protobuf.WellKnownTypes"
&& type.IsAssignableTo(googleProtobufIMessage) && type.IsAssignableTo(googleProtobufIMessage)
select type; select type;
} }
public void Dispose() => public void Dispose() =>
AssemblyContext.Dispose(); AssemblyContext.Dispose();
/// <summary>
/// An assembly resolver that uses paths to every assembly that may be loaded.
/// The file name is expected to be the same as the assembly's simple name (casing ignored).
/// PublicKeyToken, Version and CultureName are ignored.
/// </summary>
private sealed class PermissiveAssemblyResolver(IEnumerable<string> assemblyPaths) : MetadataAssemblyResolver
{
public readonly IReadOnlyDictionary<string, string> AssemblyPathLookup =
assemblyPaths.ToDictionary(
path => Path.GetFileNameWithoutExtension(path),
StringComparer.OrdinalIgnoreCase);
/// <inheritdoc />
public override Assembly? Resolve(MetadataLoadContext mlc, AssemblyName assemblyName) =>
AssemblyPathLookup.TryGetValue(assemblyName.Name!, out string? assemblyPath)
? mlc.LoadFromAssemblyPath(assemblyPath)
: null;
}
} }

View File

@ -1,5 +1,12 @@
using System.CodeDom.Compiler; // 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.Collections.Generic;
using SystemEx.Collections;
namespace LibProtodec; namespace LibProtodec;

View File

@ -1,94 +0,0 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace LibProtodec;
public static class Extensions
{
public static void Add<TKey, TValue>(this ICollection<KeyValuePair<TKey, TValue>> keyValuePairs, TKey key, TValue value) =>
keyValuePairs.Add(new KeyValuePair<TKey, TValue>(key, value));
public static bool ContainsDuplicateKey<TKey, TValue>(
this IEnumerable<KeyValuePair<TKey, TValue>> keyValuePairs,
IEqualityComparer<TKey>? comparer = null)
{
HashSet<TKey> set = new(5, comparer);
foreach (KeyValuePair<TKey, TValue> kvp in keyValuePairs)
{
if (!set.Add(kvp.Key))
{
return true;
}
}
return false;
}
public static string TrimEnd(this string @string, string trimStr) =>
@string.EndsWith(trimStr, StringComparison.Ordinal)
? @string[..^trimStr.Length]
: @string;
public static string ToSnakeCaseLower(this string str) =>
string.Create(str.Length + CountUpper(str, 1), str, (newString, oldString) =>
{
newString[0] = char.ToLowerInvariant(oldString[0]);
char chr;
for (int i = 1, j = 1; i < oldString.Length; i++, j++)
{
chr = oldString[i];
if (char.IsAsciiLetterUpper(chr))
{
newString[j++] = '_';
newString[j] = char.ToLowerInvariant(chr);
}
else
{
newString[j] = chr;
}
}
});
public static string ToSnakeCaseUpper(this string str) =>
string.Create(str.Length + CountUpper(str, 1), str, (newString, oldString) =>
{
newString[0] = char.ToUpperInvariant(oldString[0]);
char chr;
for (int i = 1, j = 1; i < oldString.Length; i++, j++)
{
chr = oldString[i];
if (char.IsAsciiLetterUpper(chr))
{
newString[j++] = '_';
newString[j] = chr;
}
else
{
newString[j] = char.ToUpperInvariant(chr);
}
}
});
[MethodImpl(MethodImplOptions.AggressiveInlining)]
// ReSharper disable once IdentifierTypo
public static bool IsBeebyted(this string name) =>
name.Length == 11 && CountUpper(name) == 11;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int CountUpper(this string str, int i = 0)
{
int upper = 0;
for (; i < str.Length; i++)
if (char.IsAsciiLetterUpper(str[i]))
upper++;
return upper;
}
}

View File

@ -0,0 +1,26 @@
// 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";
}

View File

@ -3,8 +3,8 @@
<PropertyGroup> <PropertyGroup>
<Authors>Xpl0itR</Authors> <Authors>Xpl0itR</Authors>
<BaseOutputPath>$(SolutionDir)bin/$(MSBuildProjectName)</BaseOutputPath> <BaseOutputPath>$(SolutionDir)bin/$(MSBuildProjectName)</BaseOutputPath>
<Copyright>Copyright © 2023 Xpl0itR</Copyright> <Copyright>Copyright © 2023-2024 Xpl0itR</Copyright>
<Description>A library to decompile protobuf parser/serializer classes compiled by protoc, from dotnet assemblies back into .proto definitions</Description> <Description>A library to decompile protobuf classes compiled by protoc, from CIL assemblies back into .proto definitions</Description>
<IncludeSymbols>true</IncludeSymbols> <IncludeSymbols>true</IncludeSymbols>
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
<LangVersion>Latest</LangVersion> <LangVersion>Latest</LangVersion>
@ -18,9 +18,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CommunityToolkit.Diagnostics" Version="8.2.2" />
<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="8.0.0" /> <PackageReference Include="System.Reflection.MetadataLoadContext" Version="8.0.0" />
<PackageReference Include="Xpl0itR.SystemEx" Version="1.1.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,4 +1,10 @@
using System.CodeDom.Compiler; // 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.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;

View File

@ -1,4 +1,10 @@
using System.CodeDom.Compiler; // 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; using System.IO;
namespace LibProtodec; namespace LibProtodec;

View File

@ -1,17 +1,25 @@
// 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; using System;
using System.CodeDom.Compiler; using System.CodeDom.Compiler;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using SystemEx;
using SystemEx.Collections;
using CommunityToolkit.Diagnostics; using CommunityToolkit.Diagnostics;
namespace LibProtodec; namespace LibProtodec;
public sealed class Protodec public delegate bool LookupFunc(string key, [MaybeNullWhen(false)] out string value);
{
public delegate bool LookupFunc(string key, [MaybeNullWhen(false)] out string value);
public sealed class ProtodecContext
{
private readonly Dictionary<string, Protobuf> _protobufs = []; private readonly Dictionary<string, Protobuf> _protobufs = [];
private readonly HashSet<string> _currentDescent = []; private readonly HashSet<string> _currentDescent = [];
@ -81,9 +89,8 @@ public sealed class Protodec
{ {
PropertyInfo property = properties[pi]; PropertyInfo property = properties[pi];
if (property.GetMethod is null if ((skipPropertiesWithoutProtocAttribute && !HasProtocAttribute(property))
|| property.GetMethod.IsVirtual || property.GetMethod?.IsVirtual != false)
|| (skipPropertiesWithoutProtocAttribute && !HasProtocAttribute(property)))
{ {
fi--; fi--;
continue; continue;
@ -92,7 +99,8 @@ public sealed class Protodec
Type propertyType = property.PropertyType; Type propertyType = property.PropertyType;
// only OneOf enums are defined nested directly in the message class // only OneOf enums are defined nested directly in the message class
if (propertyType.IsEnum && propertyType.DeclaringType?.Name == message.Name) if (propertyType.IsEnum
&& propertyType.DeclaringType?.Name == messageClass.Name)
{ {
string oneOfName = TranslateOneOfName(property.Name); string oneOfName = TranslateOneOfName(property.Name);
int[] oneOfProtoFieldIds = propertyType.GetFields(BindingFlags.Public | BindingFlags.Static) int[] oneOfProtoFieldIds = propertyType.GetFields(BindingFlags.Public | BindingFlags.Static)
@ -158,23 +166,23 @@ public sealed class Protodec
switch (type.Name) switch (type.Name)
{ {
case "ByteString": case "ByteString":
return "bytes"; return FieldTypeName.Bytes;
case nameof(String): case nameof(String):
return "string"; return FieldTypeName.String;
case nameof(Boolean): case nameof(Boolean):
return "bool"; return FieldTypeName.Bool;
case nameof(Double): case nameof(Double):
return "double"; return FieldTypeName.Double;
case nameof(UInt32): case nameof(UInt32):
return "uint32"; return FieldTypeName.UInt32;
case nameof(UInt64): case nameof(UInt64):
return "uint64"; return FieldTypeName.UInt64;
case nameof(Int32): case nameof(Int32):
return "int32"; return FieldTypeName.Int32;
case nameof(Int64): case nameof(Int64):
return "int64"; return FieldTypeName.Int64;
case nameof(Single): case nameof(Single):
return "float"; return FieldTypeName.Float;
} }
switch (type.GenericTypeArguments.Length) switch (type.GenericTypeArguments.Length)
@ -197,7 +205,7 @@ public sealed class Protodec
{ {
if (skipEnums) if (skipEnums)
{ {
return "int32"; return FieldTypeName.Int32;
} }
ParseEnumInternal(type, message); ParseEnumInternal(type, message);
@ -215,12 +223,6 @@ public sealed class Protodec
return type.Name; return type.Name;
} }
private static bool HasProtocAttribute(PropertyInfo property) =>
property.GetCustomAttributesData()
.Any(attr =>
attr.AttributeType.Name == "GeneratedCodeAttribute"
&& attr.ConstructorArguments[0].Value as string == "protoc");
private string TranslateProtobufName(string name) => private string TranslateProtobufName(string name) =>
CustomNameLookup?.Invoke(name, out string? translatedName) == true CustomNameLookup?.Invoke(name, out string? translatedName) == true
? translatedName ? translatedName
@ -253,7 +255,7 @@ public sealed class Protodec
return translatedName; return translatedName;
} }
if (!enumName.IsBeebyted()) if (!IsBeebyted(enumName))
{ {
enumName = enumName.ToSnakeCaseUpper(); enumName = enumName.ToSnakeCaseUpper();
} }
@ -269,6 +271,15 @@ public sealed class Protodec
} }
translatedName = name; translatedName = name;
return name.IsBeebyted(); return IsBeebyted(name);
} }
// 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(attr => attr.AttributeType.Name == nameof(GeneratedCodeAttribute)
&& attr.ConstructorArguments[0].Value as string == "protoc");
} }

View File

@ -6,11 +6,10 @@ using LibProtodec;
const string indent = " "; const string indent = " ";
const string help = """ const string help = """
Usage: protodec(.exe) <target_assembly_dir> <out_path> [target_assembly_name] [options] Usage: protodec(.exe) <target_assembly_path> <out_path> [options]
Arguments: Arguments:
target_assembly_dir A directory of assemblies to be loaded. 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. out_path An existing directory to output into individual files, otherwise output to a single file.
target_assembly_name The name of an assembly to parse. If omitted, all assemblies in the target_assembly_dir will be parsed.
Options: Options:
--skip_enums Skip parsing enums and replace references to them with int32. --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 --skip_properties_without_protoc_attribute Skip properties that aren't decorated with `GeneratedCode("protoc")` when parsing
@ -22,28 +21,22 @@ if (args.Length < 2)
return; return;
} }
string? assemblyName = null; string assemblyPath = args[0];
if (args.Length > 2 && !args[2].StartsWith('-')) string outPath = Path.GetFullPath(args[1]);
{ bool skipEnums = args.Contains("--skip_enums");
assemblyName = args[2];
}
string assemblyDir = args[0];
string outPath = Path.GetFullPath(args[1]);
bool skipEnums = args.Contains("--skip_enums");
bool skipPropertiesWithoutProtocAttribute = args.Contains("--skip_properties_without_protoc_attribute"); bool skipPropertiesWithoutProtocAttribute = args.Contains("--skip_properties_without_protoc_attribute");
using AssemblyInspector inspector = new(assemblyDir, assemblyName); using AssemblyInspector inspector = new(assemblyPath);
Protodec protodec = new(); ProtodecContext ctx = new();
foreach (Type message in inspector.GetProtobufMessageTypes()) foreach (Type message in inspector.GetProtobufMessageTypes())
{ {
protodec.ParseMessage(message, skipEnums, skipPropertiesWithoutProtocAttribute); ctx.ParseMessage(message, skipEnums, skipPropertiesWithoutProtocAttribute);
} }
if (Directory.Exists(outPath)) if (Directory.Exists(outPath))
{ {
foreach (Protobuf proto in protodec.Protobufs.Values) foreach (Protobuf proto in ctx.Protobufs.Values)
{ {
string protoPath = Path.Join(outPath, proto.Name + ".proto"); string protoPath = Path.Join(outPath, proto.Name + ".proto");
@ -58,5 +51,5 @@ else
using StreamWriter streamWriter = new(outPath); using StreamWriter streamWriter = new(outPath);
using IndentedTextWriter indentWriter = new(streamWriter, indent); using IndentedTextWriter indentWriter = new(streamWriter, indent);
protodec.WriteAllTo(indentWriter); ctx.WriteAllTo(indentWriter);
} }

View File

@ -5,12 +5,13 @@
<LangVersion>Latest</LangVersion> <LangVersion>Latest</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<PublishAot>true</PublishAot>
<RuntimeIdentifiers>win-x64;linux-x64</RuntimeIdentifiers> <RuntimeIdentifiers>win-x64;linux-x64</RuntimeIdentifiers>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\LibProtodec\LibProtodec.csproj" /> <ProjectReference Include="$(SolutionDir)src\LibProtodec\LibProtodec.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>