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
========
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: protodec(.exe) <target_assembly_dir> <out_path> [target_assembly_name] [options]
Usage: protodec(.exe) <target_assembly_path> <out_path> [options]
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.
target_assembly_name The name of an assembly to parse. If omitted, all assemblies in the target_assembly_dir will be parsed.
Options:
--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
@ -17,13 +16,9 @@ Options:
Limitations
-----------
- Integers are assumed to be (u)int32/64 as C# doesn't differentiate between them and sint32/64 and (s)fixed32/64.
### 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
- 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.
- Integers are assumed to be (u)int32/64 as CIL doesn't differentiate between them and sint32/64 and (s)fixed32/64.
- When decompiling from [Il2CppDumper](https://github.com/Perfare/Il2CppDumper) DummyDLLs
- The `Name` parameter of `OriginalNameAttribute` is not dumped. In this case, the CIL enum field names are used after conforming them to protobuf conventions
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.IO;
using System.Linq;
@ -11,33 +17,54 @@ public sealed class AssemblyInspector : IDisposable
public readonly MetadataLoadContext AssemblyContext;
public readonly IReadOnlyList<Type> LoadedTypes;
public AssemblyInspector(string assemblyDir, string? assemblyName = null)
public AssemblyInspector(string assemblyPath)
{
string[] assemblyPaths = Directory.EnumerateFiles(assemblyDir, searchPattern: "*.dll")
.ToArray();
bool isFile = File.Exists(assemblyPath);
string assemblyDir = isFile
? Path.GetDirectoryName(assemblyPath)!
: assemblyPath;
AssemblyContext = new MetadataLoadContext(
new PathAssemblyResolver(assemblyPaths));
PermissiveAssemblyResolver assemblyResolver = new(
Directory.EnumerateFiles(assemblyDir, searchPattern: "*.dll"));
LoadedTypes = assemblyName is null
? assemblyPaths.SelectMany(path => AssemblyContext.LoadFromAssemblyPath(path).GetTypes()).ToList()
: AssemblyContext.LoadFromAssemblyName(assemblyName).GetTypes();
AssemblyContext = new MetadataLoadContext(assemblyResolver);
LoadedTypes = isFile
? AssemblyContext.LoadFromAssemblyPath(assemblyPath).GetTypes()
: assemblyResolver.AssemblyPathLookup.Values.SelectMany(path => AssemblyContext.LoadFromAssemblyPath(path).GetTypes()).ToList();
}
public IEnumerable<Type> GetProtobufMessageTypes()
{
Type googleProtobufIMessage = AssemblyContext.LoadFromAssemblyName("Google.Protobuf")
.GetType("Google.Protobuf.IMessage")!;
Type? googleProtobufIMessage = AssemblyContext.LoadFromAssemblyName("Google.Protobuf")
.GetType("Google.Protobuf.IMessage");
return from type
in LoadedTypes
where !type.IsNested
&& type.IsSealed
&& type.Namespace != "Google.Protobuf.Reflection"
&& type.Namespace != "Google.Protobuf.WellKnownTypes"
&& type.Namespace?.StartsWith("Google.Protobuf", StringComparison.Ordinal) != true
&& type.IsAssignableTo(googleProtobufIMessage)
select type;
}
public void 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 SystemEx.Collections;
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>
<Authors>Xpl0itR</Authors>
<BaseOutputPath>$(SolutionDir)bin/$(MSBuildProjectName)</BaseOutputPath>
<Copyright>Copyright © 2023 Xpl0itR</Copyright>
<Description>A library to decompile protobuf parser/serializer classes compiled by protoc, from dotnet assemblies back into .proto definitions</Description>
<Copyright>Copyright © 2023-2024 Xpl0itR</Copyright>
<Description>A library to decompile protobuf classes compiled by protoc, from CIL assemblies back into .proto definitions</Description>
<IncludeSymbols>true</IncludeSymbols>
<IsPackable>true</IsPackable>
<LangVersion>Latest</LangVersion>
@ -18,9 +18,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Diagnostics" Version="8.2.2" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
<PackageReference Include="System.Reflection.MetadataLoadContext" Version="8.0.0" />
<PackageReference Include="Xpl0itR.SystemEx" Version="1.1.0" />
</ItemGroup>
</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.IO;
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;
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.CodeDom.Compiler;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using SystemEx;
using SystemEx.Collections;
using CommunityToolkit.Diagnostics;
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 HashSet<string> _currentDescent = [];
@ -81,9 +89,8 @@ public sealed class Protodec
{
PropertyInfo property = properties[pi];
if (property.GetMethod is null
|| property.GetMethod.IsVirtual
|| (skipPropertiesWithoutProtocAttribute && !HasProtocAttribute(property)))
if ((skipPropertiesWithoutProtocAttribute && !HasProtocAttribute(property))
|| property.GetMethod?.IsVirtual != false)
{
fi--;
continue;
@ -92,7 +99,8 @@ public sealed class Protodec
Type propertyType = property.PropertyType;
// 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);
int[] oneOfProtoFieldIds = propertyType.GetFields(BindingFlags.Public | BindingFlags.Static)
@ -158,23 +166,23 @@ public sealed class Protodec
switch (type.Name)
{
case "ByteString":
return "bytes";
return FieldTypeName.Bytes;
case nameof(String):
return "string";
return FieldTypeName.String;
case nameof(Boolean):
return "bool";
return FieldTypeName.Bool;
case nameof(Double):
return "double";
return FieldTypeName.Double;
case nameof(UInt32):
return "uint32";
return FieldTypeName.UInt32;
case nameof(UInt64):
return "uint64";
return FieldTypeName.UInt64;
case nameof(Int32):
return "int32";
return FieldTypeName.Int32;
case nameof(Int64):
return "int64";
return FieldTypeName.Int64;
case nameof(Single):
return "float";
return FieldTypeName.Float;
}
switch (type.GenericTypeArguments.Length)
@ -197,7 +205,7 @@ public sealed class Protodec
{
if (skipEnums)
{
return "int32";
return FieldTypeName.Int32;
}
ParseEnumInternal(type, message);
@ -215,12 +223,6 @@ public sealed class Protodec
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) =>
CustomNameLookup?.Invoke(name, out string? translatedName) == true
? translatedName
@ -253,7 +255,7 @@ public sealed class Protodec
return translatedName;
}
if (!enumName.IsBeebyted())
if (!IsBeebyted(enumName))
{
enumName = enumName.ToSnakeCaseUpper();
}
@ -269,6 +271,15 @@ public sealed class Protodec
}
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 help = """
Usage: protodec(.exe) <target_assembly_dir> <out_path> [target_assembly_name] [options]
Usage: protodec(.exe) <target_assembly_path> <out_path> [options]
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.
target_assembly_name The name of an assembly to parse. If omitted, all assemblies in the target_assembly_dir will be parsed.
Options:
--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
@ -22,28 +21,22 @@ if (args.Length < 2)
return;
}
string? assemblyName = null;
if (args.Length > 2 && !args[2].StartsWith('-'))
{
assemblyName = args[2];
}
string assemblyDir = args[0];
string outPath = Path.GetFullPath(args[1]);
bool skipEnums = args.Contains("--skip_enums");
string assemblyPath = args[0];
string outPath = Path.GetFullPath(args[1]);
bool skipEnums = args.Contains("--skip_enums");
bool skipPropertiesWithoutProtocAttribute = args.Contains("--skip_properties_without_protoc_attribute");
using AssemblyInspector inspector = new(assemblyDir, assemblyName);
Protodec protodec = new();
using AssemblyInspector inspector = new(assemblyPath);
ProtodecContext ctx = new();
foreach (Type message in inspector.GetProtobufMessageTypes())
{
protodec.ParseMessage(message, skipEnums, skipPropertiesWithoutProtocAttribute);
ctx.ParseMessage(message, skipEnums, skipPropertiesWithoutProtocAttribute);
}
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");
@ -58,5 +51,5 @@ else
using StreamWriter streamWriter = new(outPath);
using IndentedTextWriter indentWriter = new(streamWriter, indent);
protodec.WriteAllTo(indentWriter);
ctx.WriteAllTo(indentWriter);
}

View File

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