diff --git a/README.md b/README.md index b7f032a..7e3e44f 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Options: Commands: il2cpp Use LibCpp2IL backend to directly load Il2Cpp compiled game assembly. EXPERIMENTAL. - lua Use Lua AST backend to load Lua source files. + lua Use Loretta backend to load Lua source files. ``` See per-command help message for more info. diff --git a/src/LibProtodec/LibProtodec.csproj b/src/LibProtodec/LibProtodec.csproj index cabbf8f..e637b0c 100644 --- a/src/LibProtodec/LibProtodec.csproj +++ b/src/LibProtodec/LibProtodec.csproj @@ -18,12 +18,13 @@ + - + \ No newline at end of file diff --git a/src/LibProtodec/Loaders/LuaSourceLoader.cs b/src/LibProtodec/Loaders/LuaSourceLoader.cs index 2d15ff8..1076c93 100644 --- a/src/LibProtodec/Loaders/LuaSourceLoader.cs +++ b/src/LibProtodec/Loaders/LuaSourceLoader.cs @@ -16,19 +16,27 @@ namespace LibProtodec.Loaders; public sealed class LuaSourceLoader { - public IReadOnlyList LoadedSyntaxTrees { get; } + public IReadOnlyDictionary LoadedSyntaxTrees { get; } public LuaSourceLoader(string sourcePath, ILogger? logger = null) { LoadedSyntaxTrees = File.Exists(sourcePath) - ? [LoadSyntaxTreeFromSourceFile(sourcePath)] + ? new Dictionary { { Path.GetFileNameWithoutExtension(sourcePath) , LoadSyntaxTreeFromSourceFile(sourcePath) } } : Directory.EnumerateFiles(sourcePath, searchPattern: "*.lua") - .Select(LoadSyntaxTreeFromSourceFile) - .ToList(); + .Select(static sourcePath => + (Path.GetFileNameWithoutExtension(sourcePath), LoadSyntaxTreeFromSourceFile(sourcePath))) + .ToDictionary(); logger?.LogLoadedLuaSyntaxTrees(LoadedSyntaxTrees.Count); } + public SyntaxTree ResolveImport(string import) + { + import = Path.GetFileNameWithoutExtension(import); + + return LoadedSyntaxTrees[import]; + } + private static SyntaxTree LoadSyntaxTreeFromSourceFile(string filePath) { using FileStream fileStream = File.OpenRead(filePath); diff --git a/src/LibProtodec/Models/Protobuf/Fields/MessageField.cs b/src/LibProtodec/Models/Protobuf/Fields/MessageField.cs index 2940931..0a7cc29 100644 --- a/src/LibProtodec/Models/Protobuf/Fields/MessageField.cs +++ b/src/LibProtodec/Models/Protobuf/Fields/MessageField.cs @@ -13,7 +13,9 @@ namespace LibProtodec.Models.Protobuf.Fields; public sealed class MessageField { - public required IProtobufType Type { get; init; } + private bool? _isRepeated; + + public IProtobufType? Type { get; set; } public Message? DeclaringMessage { get; set; } public string? Name { get; set; } @@ -23,6 +25,11 @@ public sealed class MessageField public bool IsRequired { get; set; } public bool IsObsolete { get; init; } public bool HasHasProp { get; init; } + public bool IsRepeated + { + get => _isRepeated ??= Type is Repeated; + set => _isRepeated = value; + } public void WriteTo(TextWriter writer, bool isOneOf) { @@ -30,7 +37,7 @@ public sealed class MessageField Guard.IsNotNull(Name); Guard.IsNotNull(DeclaringMessage); - if (IsOptional || (HasHasProp && !isOneOf && Type is not Repeated)) + if (IsOptional || (HasHasProp && !isOneOf && !IsRepeated)) { writer.Write("optional "); } diff --git a/src/LibProtodec/Models/Protobuf/Fields/ServiceMethod.cs b/src/LibProtodec/Models/Protobuf/Fields/ServiceMethod.cs index 1f436ad..473bbbc 100644 --- a/src/LibProtodec/Models/Protobuf/Fields/ServiceMethod.cs +++ b/src/LibProtodec/Models/Protobuf/Fields/ServiceMethod.cs @@ -50,6 +50,7 @@ public sealed class ServiceMethod(Service declaringService) writer.Indent++; Protobuf.WriteOptionTo(writer, "deprecated", "true"); + writer.WriteLine(); writer.Indent--; writer.WriteLine('}'); diff --git a/src/LibProtodec/Models/Protobuf/Protobuf.cs b/src/LibProtodec/Models/Protobuf/Protobuf.cs index 1e3543a..b4526df 100644 --- a/src/LibProtodec/Models/Protobuf/Protobuf.cs +++ b/src/LibProtodec/Models/Protobuf/Protobuf.cs @@ -69,24 +69,25 @@ public sealed class Protobuf if (_imports is not null) { - writer.WriteLine(); - foreach (string import in _imports) { + writer.WriteLine(); writer.Write("import \""); writer.Write(import); - writer.WriteLine("\";"); + writer.Write("\";"); } } if (CilNamespace is not null) { + writer.WriteLine(); writer.WriteLine(); WriteOptionTo(writer, "csharp_namespace", CilNamespace, true); } foreach (TopLevel topLevel in TopLevels) { + writer.WriteLine(); writer.WriteLine(); topLevel.WriteTo(writer); } @@ -110,6 +111,6 @@ public sealed class Protobuf writer.Write('\"'); } - writer.WriteLine(';'); + writer.Write(';'); } } \ No newline at end of file diff --git a/src/LibProtodec/Models/Protobuf/TopLevels/Enum.cs b/src/LibProtodec/Models/Protobuf/TopLevels/Enum.cs index d538bc7..b25240c 100644 --- a/src/LibProtodec/Models/Protobuf/TopLevels/Enum.cs +++ b/src/LibProtodec/Models/Protobuf/TopLevels/Enum.cs @@ -26,16 +26,19 @@ public sealed class Enum : TopLevel, INestableType if (ContainsDuplicateFieldId) { Protobuf.WriteOptionTo(writer, "allow_alias", "true"); + writer.WriteLine(); } if (this.IsObsolete) { Protobuf.WriteOptionTo(writer, "deprecated", "true"); + writer.WriteLine(); } if (IsClosed) { Protobuf.WriteOptionTo(writer, "features.enum_type", "CLOSED"); + writer.WriteLine(); } foreach (EnumField field in Fields) diff --git a/src/LibProtodec/Models/Protobuf/TopLevels/Message.cs b/src/LibProtodec/Models/Protobuf/TopLevels/Message.cs index 458e780..9c55142 100644 --- a/src/LibProtodec/Models/Protobuf/TopLevels/Message.cs +++ b/src/LibProtodec/Models/Protobuf/TopLevels/Message.cs @@ -28,6 +28,7 @@ public sealed class Message : TopLevel, INestableType if (this.IsObsolete) { Protobuf.WriteOptionTo(writer, "deprecated", "true"); + writer.WriteLine(); } int[] oneOfs = OneOfs.SelectMany(static oneOf => oneOf.Value).ToArray(); diff --git a/src/LibProtodec/Models/Protobuf/TopLevels/Service.cs b/src/LibProtodec/Models/Protobuf/TopLevels/Service.cs index 42fa5bc..941a275 100644 --- a/src/LibProtodec/Models/Protobuf/TopLevels/Service.cs +++ b/src/LibProtodec/Models/Protobuf/TopLevels/Service.cs @@ -24,6 +24,7 @@ public sealed class Service : TopLevel if (this.IsObsolete) { Protobuf.WriteOptionTo(writer, "deprecated", "true"); + writer.WriteLine(); } foreach (ServiceMethod method in Methods) diff --git a/src/LibProtodec/Models/Protobuf/Types/Descriptor.cs b/src/LibProtodec/Models/Protobuf/Types/Descriptor.cs deleted file mode 100644 index 974bf53..0000000 --- a/src/LibProtodec/Models/Protobuf/Types/Descriptor.cs +++ /dev/null @@ -1,54 +0,0 @@ -// 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/. - -using CommunityToolkit.Diagnostics; -using LibProtodec.Models.Protobuf.TopLevels; - -namespace LibProtodec.Models.Protobuf.Types; - -public sealed class Descriptor : IProtobufType -{ - public int TypeIndex { get; set; } - public bool IsRepeated { get; set; } - public /*TopLevel*/IProtobufType? TopLevelType { get; set; } - - public string Name => - Type.Name; - - public IProtobufType Type - { - get - { - IProtobufType type = TopLevelType as IProtobufType ?? TypeIndex switch - { - 1 => Scalar.Double, - 2 => Scalar.Float, - 3 => Scalar.Int64, - 4 => Scalar.UInt64, - 5 => Scalar.Int32, - 6 => Scalar.Fixed64, - 7 => Scalar.Fixed32, - 8 => Scalar.Bool, - 9 => Scalar.String, - 10 => ThrowHelper.ThrowNotSupportedException("Parsing proto2 groups are not supported. Open an issue if you need this."), - 12 => Scalar.Bytes, - 13 => Scalar.UInt32, - 15 => Scalar.SFixed32, - 16 => Scalar.SFixed64, - 17 => Scalar.SInt32, - 18 => Scalar.SInt64, - _ => ThrowHelper.ThrowArgumentOutOfRangeException() - }; - - if (IsRepeated) - { - type = new Repeated(type); - } - - return type; - } - } -} \ No newline at end of file diff --git a/src/LibProtodec/ProtodecContext.Lua.cs b/src/LibProtodec/ProtodecContext.Lua.cs index 9f2d330..f5ec624 100644 --- a/src/LibProtodec/ProtodecContext.Lua.cs +++ b/src/LibProtodec/ProtodecContext.Lua.cs @@ -5,28 +5,39 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. using System.Collections.Generic; -using System.IO; using System.Linq; +using SystemEx; using CommunityToolkit.Diagnostics; +using LibProtodec.Loaders; using LibProtodec.Models.Protobuf; using LibProtodec.Models.Protobuf.Fields; using LibProtodec.Models.Protobuf.TopLevels; using LibProtodec.Models.Protobuf.Types; using Loretta.CodeAnalysis; using Loretta.CodeAnalysis.Lua.Syntax; -using SystemEx; +using FdpTypes = Google.Protobuf.Reflection.FieldDescriptorProto.Types; namespace LibProtodec; +// TODO: add debug logging partial class ProtodecContext { - public Protobuf ParseLuaSyntaxTree(SyntaxTree ast) + private readonly Dictionary> _parsedPbTables = []; + private readonly Dictionary _parsedProtobufs = []; + + public Protobuf ParseLuaSyntaxTree(LuaSourceLoader loader, SyntaxTree ast, ParserOptions options = ParserOptions.None) { + if (_parsedProtobufs.TryGetValue(ast.FilePath, out Protobuf? parsedProto)) + { + return parsedProto; + } + CompilationUnitSyntax root = (CompilationUnitSyntax)ast.GetRoot(); SyntaxList statements = root.Statements.Statements; bool importedProtobufLib = false; - Dictionary pbTable = []; + Dictionary pbTable = _parsedPbTables[ast.FilePath] = []; + Dictionary> imports = []; Protobuf protobuf = new() { Version = 2, @@ -39,6 +50,7 @@ partial class ProtodecContext { case LocalVariableDeclarationStatementSyntax { + Names: [{ IdentifierName.Name: { } importKey }], EqualsValues.Values: [FunctionCallExpressionSyntax { Expression: IdentifierNameSyntax { Name: "require" } } call] }: switch (call.Argument) @@ -46,9 +58,11 @@ partial class ProtodecContext case StringFunctionArgumentSyntax { Expression.Token.ValueText: "protobuf/protobuf" }: importedProtobufLib = true; break; - case ExpressionListFunctionArgumentSyntax { Expressions: [LiteralExpressionSyntax { Token.ValueText: {} import }] }: - import = Path.GetFileNameWithoutExtension(import).TrimEnd("_pb"); //todo: handle imports properly - protobuf.Imports.Add($"{import}.proto"); + case ExpressionListFunctionArgumentSyntax { Expressions: [LiteralExpressionSyntax { Token.ValueText: { } import }] }: + SyntaxTree importedAst = loader.ResolveImport(import); + Protobuf importedProto = ParseLuaSyntaxTree(loader, importedAst); + imports.Add(importKey, _parsedPbTables[importedAst.FilePath]); + protobuf.Imports.Add(importedProto.FileName); break; } break; @@ -57,9 +71,9 @@ partial class ProtodecContext Variables: [MemberAccessExpressionSyntax { Expression: IdentifierNameSyntax, - MemberName.ValueText: {} tableKey + MemberName.ValueText: { } tableKey }], - EqualsValues.Values: [FunctionCallExpressionSyntax { Expression: MemberAccessExpressionSyntax { MemberName.ValueText: {} factory } }] + EqualsValues.Values: [FunctionCallExpressionSyntax { Expression: MemberAccessExpressionSyntax { MemberName.ValueText: { } factory } }] }: switch (factory) { @@ -82,7 +96,7 @@ partial class ProtodecContext pbTable.Add(tableKey, @enum); break; case "FieldDescriptor": - pbTable.Add(tableKey, new MessageField { Type = new Descriptor() }); + pbTable.Add(tableKey, new MessageField()); break; case "EnumValueDescriptor": pbTable.Add(tableKey, new EnumField()); @@ -93,7 +107,7 @@ partial class ProtodecContext { Variables: [MemberAccessExpressionSyntax { - Expression: MemberAccessExpressionSyntax { MemberName.ValueText: {} tableKey }, + Expression: MemberAccessExpressionSyntax { MemberName.ValueText: { } tableKey }, MemberName.ValueText: { } memberName }], EqualsValues.Values: [{ } valueExpr] @@ -103,7 +117,6 @@ partial class ProtodecContext .Cast() .Select(static element => element.Value); object? valueLiteral = (valueExpr as LiteralExpressionSyntax)?.Token.Value; - switch (pbTable[tableKey]) { case Message message: @@ -152,7 +165,6 @@ partial class ProtodecContext } break; case MessageField messageField: - Descriptor descriptor = (Descriptor)messageField.Type; switch (memberName) { case "name": @@ -162,29 +174,36 @@ partial class ProtodecContext messageField.Id = (int)(double)valueLiteral!; break; case "label": - switch ((int)(double)valueLiteral!) + switch ((FdpTypes.Label)(double)valueLiteral!) { - case 1: + case FdpTypes.Label.Optional: messageField.IsOptional = true; break; - case 2: + case FdpTypes.Label.Required: messageField.IsRequired = true; break; - case 3: - descriptor.IsRepeated = true; + case FdpTypes.Label.Repeated: + messageField.IsRepeated = true; break; } break; - case "type": - descriptor.TypeIndex = (int)(double)valueLiteral!; + case "enum_type" when (options & ParserOptions.SkipEnums) > 0: + messageField.Type = Scalar.Int32; break; - case "message_type": case "enum_type": - string typeTableKey = ((MemberAccessExpressionSyntax)valueExpr).MemberName.ValueText; - if (pbTable.TryGetValue(typeTableKey, out object? topLevel)) //TODO: if this is false, then the top level is from an import, we will need to handle this - descriptor.TopLevelType = (IProtobufType)topLevel; - else - descriptor.TopLevelType = new Scalar(typeTableKey); // temporary hack + case "message_type": + MemberAccessExpressionSyntax memberAccessExpr = (MemberAccessExpressionSyntax)valueExpr; + string importKey = ((IdentifierNameSyntax)memberAccessExpr.Expression).Name; + Dictionary table = imports.GetValueOrDefault(importKey, pbTable); + string typeTableKey = memberAccessExpr.MemberName.ValueText; + IProtobufType scalar = (IProtobufType)table[typeTableKey]; + messageField.Type = messageField.IsRepeated + ? new Repeated(scalar) + : scalar; + break; + case "type": + messageField.Type ??= ParseFieldType( + (FdpTypes.Type)(double)valueLiteral!, messageField.IsRepeated); break; case "has_default_value": if ((bool)valueLiteral!) @@ -220,6 +239,35 @@ partial class ProtodecContext } this.Protobufs.Add(protobuf); + _parsedProtobufs.Add(ast.FilePath, protobuf); return protobuf; } + + protected static IProtobufType ParseFieldType(FdpTypes.Type type, bool isRepeated) + { + IProtobufType scalar = type switch + { + FdpTypes.Type.Double => Scalar.Double, + FdpTypes.Type.Float => Scalar.Float, + FdpTypes.Type.Int64 => Scalar.Int64, + FdpTypes.Type.Uint64 => Scalar.UInt64, + FdpTypes.Type.Int32 => Scalar.Int32, + FdpTypes.Type.Fixed64 => Scalar.Fixed64, + FdpTypes.Type.Fixed32 => Scalar.Fixed32, + FdpTypes.Type.Bool => Scalar.Bool, + FdpTypes.Type.String => Scalar.String, + FdpTypes.Type.Bytes => Scalar.Bytes, + FdpTypes.Type.Uint32 => Scalar.UInt32, + FdpTypes.Type.Sfixed32 => Scalar.SFixed32, + FdpTypes.Type.Sfixed64 => Scalar.SFixed64, + FdpTypes.Type.Sint32 => Scalar.SInt32, + FdpTypes.Type.Sint64 => Scalar.SInt64, + FdpTypes.Type.Group => ThrowHelper.ThrowNotSupportedException( + "Parsing proto2 groups are not supported. Open an issue if you need this."), + }; + + return isRepeated + ? new Repeated(scalar) + : scalar; + } } \ No newline at end of file diff --git a/src/LibProtodec/ProtodecContext.cs b/src/LibProtodec/ProtodecContext.cs index ff4ab85..a2e7843 100644 --- a/src/LibProtodec/ProtodecContext.cs +++ b/src/LibProtodec/ProtodecContext.cs @@ -36,14 +36,13 @@ public partial class ProtodecContext public void WriteAllTo(IndentedTextWriter writer) { - writer.WriteLine("// Decompiled with protodec"); - writer.WriteLine(); + writer.Write("// Decompiled with protodec"); foreach (TopLevel topLevel in Protobufs.SelectMany(static proto => proto.TopLevels)) { + writer.WriteLine(); + writer.WriteLine(); topLevel.WriteTo(writer); - writer.WriteLine(); - writer.WriteLine(); } } diff --git a/src/protodec/Program.cs b/src/protodec/Program.cs index 7e4a40c..8e961a6 100644 --- a/src/protodec/Program.cs +++ b/src/protodec/Program.cs @@ -117,17 +117,23 @@ internal sealed class Commands } /// - /// Use Lua AST backend to load Lua source files. + /// Use Loretta backend to load Lua source files. /// /// Either the path to the target lua file or a directory of lua files, all of which be parsed. /// An existing directory to output into individual files, otherwise output to a single file. + /// Skip parsing enums and replace references to them with int32. /// Logging severity level. [Command("lua")] public void Lua( [Argument] string targetPath, [Argument] string outPath, + bool skipEnums, LogLevel logLevel = LogLevel.Information) { + ParserOptions options = ParserOptions.None; + if (skipEnums) + options |= ParserOptions.SkipEnums; + using ILoggerFactory loggerFactory = CreateLoggerFactory(logLevel); ILogger logger = CreateProtodecLogger(loggerFactory); @@ -140,9 +146,9 @@ internal sealed class Commands }; logger.LogInformation("Parsing Lua syntax trees..."); - foreach (SyntaxTree ast in loader.LoadedSyntaxTrees) + foreach (SyntaxTree ast in loader.LoadedSyntaxTrees.Values) { - ctx.ParseLuaSyntaxTree(ast); + ctx.ParseLuaSyntaxTree(loader, ast, options); } // NOTE: I'm duplicating this code rather than refactoring because I have a lot of uncommited changes on another computer,