diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a7b78cc --- /dev/null +++ b/.github/workflows/release.yml @@ -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 \ No newline at end of file diff --git a/README.md b/README.md index 6ee788a..35b99a0 100644 --- a/README.md +++ b/README.md @@ -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_name] [options] +Usage: protodec(.exe) [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 ------- diff --git a/src/LibProtodec/AssemblyInspector.cs b/src/LibProtodec/AssemblyInspector.cs index 56c4530..c9ebcbe 100644 --- a/src/LibProtodec/AssemblyInspector.cs +++ b/src/LibProtodec/AssemblyInspector.cs @@ -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 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 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(); + + /// + /// 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. + /// + private sealed class PermissiveAssemblyResolver(IEnumerable assemblyPaths) : MetadataAssemblyResolver + { + public readonly IReadOnlyDictionary AssemblyPathLookup = + assemblyPaths.ToDictionary( + path => Path.GetFileNameWithoutExtension(path), + StringComparer.OrdinalIgnoreCase); + + /// + public override Assembly? Resolve(MetadataLoadContext mlc, AssemblyName assemblyName) => + AssemblyPathLookup.TryGetValue(assemblyName.Name!, out string? assemblyPath) + ? mlc.LoadFromAssemblyPath(assemblyPath) + : null; + } } \ No newline at end of file diff --git a/src/LibProtodec/Enum.cs b/src/LibProtodec/Enum.cs index 36b1610..9cfdb78 100644 --- a/src/LibProtodec/Enum.cs +++ b/src/LibProtodec/Enum.cs @@ -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; diff --git a/src/LibProtodec/Extensions.cs b/src/LibProtodec/Extensions.cs deleted file mode 100644 index 0c89b44..0000000 --- a/src/LibProtodec/Extensions.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; - -namespace LibProtodec; - -public static class Extensions -{ - public static void Add(this ICollection> keyValuePairs, TKey key, TValue value) => - keyValuePairs.Add(new KeyValuePair(key, value)); - - public static bool ContainsDuplicateKey( - this IEnumerable> keyValuePairs, - IEqualityComparer? comparer = null) - { - HashSet set = new(5, comparer); - - foreach (KeyValuePair 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; - } -} \ No newline at end of file diff --git a/src/LibProtodec/FieldTypeName.cs b/src/LibProtodec/FieldTypeName.cs new file mode 100644 index 0000000..9feb99a --- /dev/null +++ b/src/LibProtodec/FieldTypeName.cs @@ -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"; +} \ No newline at end of file diff --git a/src/LibProtodec/LibProtodec.csproj b/src/LibProtodec/LibProtodec.csproj index bdc59be..8120bb0 100644 --- a/src/LibProtodec/LibProtodec.csproj +++ b/src/LibProtodec/LibProtodec.csproj @@ -3,8 +3,8 @@ Xpl0itR $(SolutionDir)bin/$(MSBuildProjectName) - Copyright © 2023 Xpl0itR - A library to decompile protobuf parser/serializer classes compiled by protoc, from dotnet assemblies back into .proto definitions + Copyright © 2023-2024 Xpl0itR + A library to decompile protobuf classes compiled by protoc, from CIL assemblies back into .proto definitions true true Latest @@ -18,9 +18,9 @@ - + \ No newline at end of file diff --git a/src/LibProtodec/Message.cs b/src/LibProtodec/Message.cs index 975d803..4fb5c71 100644 --- a/src/LibProtodec/Message.cs +++ b/src/LibProtodec/Message.cs @@ -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; diff --git a/src/LibProtodec/Protobuf.cs b/src/LibProtodec/Protobuf.cs index 364fe14..b0911e0 100644 --- a/src/LibProtodec/Protobuf.cs +++ b/src/LibProtodec/Protobuf.cs @@ -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; diff --git a/src/LibProtodec/Protodec.cs b/src/LibProtodec/ProtodecContext.cs similarity index 83% rename from src/LibProtodec/Protodec.cs rename to src/LibProtodec/ProtodecContext.cs index 43e2d50..d98585e 100644 --- a/src/LibProtodec/Protodec.cs +++ b/src/LibProtodec/ProtodecContext.cs @@ -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 _protobufs = []; private readonly HashSet _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"); } \ No newline at end of file diff --git a/src/protodec/Program.cs b/src/protodec/Program.cs index 7e9f79b..fc9d631 100644 --- a/src/protodec/Program.cs +++ b/src/protodec/Program.cs @@ -6,11 +6,10 @@ using LibProtodec; const string indent = " "; const string help = """ - Usage: protodec(.exe) [target_assembly_name] [options] + Usage: protodec(.exe) [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); } \ No newline at end of file diff --git a/src/protodec/protodec.csproj b/src/protodec/protodec.csproj index 36814e3..998145b 100644 --- a/src/protodec/protodec.csproj +++ b/src/protodec/protodec.csproj @@ -5,12 +5,13 @@ Latest enable Exe + true win-x64;linux-x64 net8.0 - + \ No newline at end of file