nearly done

This commit is contained in:
Xpl0itR 2024-10-08 04:46:49 +01:00
parent 41db9569dc
commit b3ff1fe608
8 changed files with 281 additions and 43 deletions

View File

@ -15,6 +15,9 @@ internal static partial class LoggerMessages
[LoggerMessage(Level = LogLevel.Warning, Message = "Failed to locate corresponding id field; likely stripped or otherwise obfuscated.")]
internal static partial void LogFailedToLocateIdField(this ILogger logger);
[LoggerMessage(Level = LogLevel.Warning, Message = "{msg} is not yet implemented. Open an issue if this is something you need.")]
internal static partial void LogNotImplemented(this ILogger logger, string msg);
[LoggerMessage(Level = LogLevel.Information, Message = "Loaded {typeCount} types from {assemblyCount} assemblies for parsing.")]
internal static partial void LogLoadedTypeAndAssemblyCount(this ILogger logger, int typeCount, int assemblyCount);

View File

@ -4,17 +4,21 @@
// 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;
namespace LibProtodec.Models.Protobuf.Fields;
public sealed class EnumField
{
public required string Name { get; init; }
public required int Id { get; init; }
public string? Name { get; set; }
public int Id { get; set; }
public bool IsObsolete { get; init; }
public void WriteTo(System.IO.TextWriter writer)
{
Guard.IsNotNull(Name);
writer.Write(Name);
writer.Write(" = ");
writer.Write(Id);

View File

@ -5,29 +5,43 @@
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
using System.IO;
using CommunityToolkit.Diagnostics;
using LibProtodec.Models.Protobuf.TopLevels;
using LibProtodec.Models.Protobuf.Types;
namespace LibProtodec.Models.Protobuf.Fields;
public sealed class MessageField(Message declaringMessage)
public sealed class MessageField
{
public required IProtobufType Type { get; init; }
public required string Name { get; init; }
public required int Id { get; init; }
public Message? DeclaringMessage { get; set; }
public string? Name { get; set; }
public int Id { get; set; }
public bool IsOptional { get; set; }
public bool IsRequired { get; set; }
public bool IsObsolete { get; init; }
public bool HasHasProp { get; init; }
public void WriteTo(TextWriter writer, bool isOneOf)
{
if (HasHasProp && !isOneOf && Type is not Repeated)
Guard.IsNotNull(Type);
Guard.IsNotNull(Name);
Guard.IsNotNull(DeclaringMessage);
if (IsOptional || (HasHasProp && !isOneOf && Type is not Repeated))
{
writer.Write("optional ");
}
if (IsRequired)
{
writer.Write("required ");
}
writer.Write(
declaringMessage.QualifyTypeName(Type));
DeclaringMessage.QualifyTypeName(Type));
writer.Write(' ');
writer.Write(Name);
writer.Write(" = ");

View File

@ -8,20 +8,23 @@ using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using CommunityToolkit.Diagnostics;
using LibProtodec.Models.Protobuf.TopLevels;
namespace LibProtodec.Models.Protobuf;
public sealed class Protobuf
{
private int _version = 3;
private string? _fileName;
private HashSet<string>? _imports;
public readonly List<TopLevel> TopLevels = [];
public string? Edition { get; set; }
public string? AssemblyName { get; init; }
public string? Namespace { get; init; }
public string? SourceName { get; init; }
public string? Edition { get; set; }
public string? CilNamespace { get; init; }
public string FileName
{
@ -29,6 +32,16 @@ public sealed class Protobuf
set => _fileName = value;
}
public int Version
{
get => _version;
set
{
Guard.IsBetweenOrEqualTo(value, 2, 3);
_version = value;
}
}
public HashSet<string> Imports =>
_imports ??= [];
@ -42,10 +55,16 @@ public sealed class Protobuf
writer.WriteLine(AssemblyName);
}
if (SourceName is not null)
{
writer.Write("// Source: ");
writer.WriteLine(SourceName);
}
writer.WriteLine();
writer.WriteLine(
Edition is null
? """syntax = "proto3";"""
? $"""syntax = "proto{Version}";"""
: $"""edition = "{Edition}";""");
if (_imports is not null)
@ -60,10 +79,10 @@ public sealed class Protobuf
}
}
if (Namespace is not null)
if (CilNamespace is not null)
{
writer.WriteLine();
WriteOptionTo(writer, "csharp_namespace", Namespace, true);
WriteOptionTo(writer, "csharp_namespace", CilNamespace, true);
}
foreach (TopLevel topLevel in TopLevels)

View File

@ -12,7 +12,7 @@ namespace LibProtodec.Models.Protobuf.TopLevels;
public abstract class TopLevel
{
public required string Name { get; init; }
public required string Name { get; set; }
public bool IsObsolete { get; init; }
public Protobuf? Protobuf { get; set; }

View File

@ -0,0 +1,54 @@
// 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<IProtobufType>("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<IProtobufType>()
};
if (IsRepeated)
{
type = new Repeated(type);
}
return type;
}
}
}

View File

@ -4,28 +4,34 @@
// 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.Collections.Generic;
using System.IO;
using System.Linq;
using CommunityToolkit.Diagnostics;
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;
namespace LibProtodec;
partial class ProtodecContext
{
public void ParseLuaSyntaxTree(SyntaxTree ast)
public Protobuf ParseLuaSyntaxTree(SyntaxTree ast)
{
CompilationUnitSyntax root = (CompilationUnitSyntax)ast.GetRoot();
SyntaxList<StatementSyntax> statements = root.Statements.Statements;
LocalVariableDeclarationStatementSyntax pbTableDeclaration =
(LocalVariableDeclarationStatementSyntax)statements[0];
string pbTableName = pbTableDeclaration.Names[0].Name;
Guard.IsTrue(
pbTableName.EndsWith("_pbTable"));
bool importedProtobufLib = false;
string? returns = null;
Dictionary<string, object> pbTable = [];
Protobuf protobuf = new()
{
Version = 2,
SourceName = ast.FilePath
};
foreach (StatementSyntax statement in statements)
{
@ -33,7 +39,6 @@ partial class ProtodecContext
{
case LocalVariableDeclarationStatementSyntax
{
Names: [{ Name: { } varName }],
EqualsValues.Values: [FunctionCallExpressionSyntax { Expression: IdentifierNameSyntax { Name: "require" } } call]
}:
switch (call.Argument)
@ -42,39 +47,179 @@ partial class ProtodecContext
importedProtobufLib = true;
break;
case ExpressionListFunctionArgumentSyntax { Expressions: [LiteralExpressionSyntax { Token.ValueText: {} import }] }:
// TODO: handle imported protos
import = Path.GetFileNameWithoutExtension(import).TrimEnd("_pb"); //todo: handle imports properly
protobuf.Imports.Add($"{import}.proto");
break;
}
break;
case ExpressionStatementSyntax
case AssignmentStatementSyntax
{
Expression: FunctionCallExpressionSyntax
Variables: [MemberAccessExpressionSyntax
{
Expression: IdentifierNameSyntax { Name: "module" },
Argument: ExpressionListFunctionArgumentSyntax
{
Expressions: [LiteralExpressionSyntax literal]
}
}
Expression: IdentifierNameSyntax,
MemberName.ValueText: {} tableKey
}],
EqualsValues.Values: [FunctionCallExpressionSyntax { Expression: MemberAccessExpressionSyntax { MemberName.ValueText: {} factory } }]
}:
string moduleName = literal.Token.ValueText;
switch (factory)
{
case "Descriptor":
Message message = new()
{
Name = tableKey,
Protobuf = protobuf
};
protobuf.TopLevels.Add(message);
pbTable.Add(tableKey, message);
break;
case "EnumDescriptor":
Enum @enum = new()
{
Name = tableKey,
Protobuf = protobuf
};
protobuf.TopLevels.Add(@enum);
pbTable.Add(tableKey, @enum);
break;
case "FieldDescriptor":
pbTable.Add(tableKey, new MessageField { Type = new Descriptor() });
break;
case "EnumValueDescriptor":
pbTable.Add(tableKey, new EnumField());
break;
}
break;
case AssignmentStatementSyntax
{
Variables: [MemberAccessExpressionSyntax varExpr],
EqualsValues.Values: [{} valueExpr]
Variables: [MemberAccessExpressionSyntax
{
Expression: MemberAccessExpressionSyntax { MemberName.ValueText: {} tableKey },
MemberName.ValueText: { } memberName
}],
EqualsValues.Values: [{ } valueExpr]
}:
// TODO: build protos
var valueTableElements =
(valueExpr as TableConstructorExpressionSyntax)?.Fields
.Cast<UnkeyedTableFieldSyntax>()
.Select(static element => element.Value);
object? valueLiteral = (valueExpr as LiteralExpressionSyntax)?.Token.Value;
switch (pbTable[tableKey])
{
case Message message:
switch (memberName)
{
case "name":
message.Name = (string)valueLiteral!;
break;
case "fields":
foreach (MessageField messageField in valueTableElements!
.Cast<MemberAccessExpressionSyntax>()
.Select(x => pbTable[x.MemberName.ValueText])
.Cast<MessageField>())
{
messageField.DeclaringMessage = message;
message.Fields.Add(messageField.Id, messageField);
}
break;
case "nested_types":
if (valueTableElements!.Any())
Logger?.LogNotImplemented("Parsing nested messages from lua");
break;
case "enum_types":
if (valueTableElements!.Any())
Logger?.LogNotImplemented("Parsing nested enums from lua");
break;
case "is_extendable":
if ((bool)valueLiteral!)
Logger?.LogNotImplemented("Parsing message extensions from lua");
break;
}
break;
case Enum @enum:
switch (memberName)
{
case "name":
@enum.Name = (string)valueLiteral!;
break;
case "values":
@enum.Fields.AddRange(
valueTableElements!
.Cast<MemberAccessExpressionSyntax>()
.Select(x => pbTable[x.MemberName.ValueText])
.Cast<EnumField>());
break;
}
break;
case MessageField messageField:
Descriptor descriptor = (Descriptor)messageField.Type;
switch (memberName)
{
case "name":
messageField.Name = (string)valueLiteral!;
break;
case "number":
messageField.Id = (int)(double)valueLiteral!;
break;
case "label":
switch ((int)(double)valueLiteral!)
{
case 1:
messageField.IsOptional = true;
break;
case 2:
messageField.IsRequired = true;
break;
case 3:
descriptor.IsRepeated = true;
break;
}
break;
case "type":
descriptor.TypeIndex = (int)(double)valueLiteral!;
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
break;
case "has_default_value":
if ((bool)valueLiteral!)
Logger?.LogNotImplemented("Parsing default field values from lua");
break;
}
break;
case EnumField enumField:
switch (memberName)
{
case "name":
enumField.Name = (string)valueLiteral!;
break;
case "number" when valueExpr is UnaryExpressionSyntax { Operand: LiteralExpressionSyntax { Token.Value: { } negativeNumber } }:
enumField.Id = -(int)(double)negativeNumber;
break;
case "number":
enumField.Id = (int)(double)valueLiteral!;
break;
}
break;
}
break;
case ReturnStatementSyntax { Expressions: [IdentifierNameSyntax identifier] }:
returns = identifier.Name;
protobuf.FileName = $"{identifier.Name.TrimEnd("_pbTable")}.proto";
break;
}
}
if (!importedProtobufLib || returns != pbTableName)
if (!importedProtobufLib)
{
ThrowHelper.ThrowInvalidDataException();
}
this.Protobufs.Add(protobuf);
return protobuf;
}
}

View File

@ -38,8 +38,6 @@ public partial class ProtodecContext
{
writer.WriteLine("// Decompiled with protodec");
writer.WriteLine();
writer.WriteLine("""syntax = "proto3";""");
writer.WriteLine();
foreach (TopLevel topLevel in Protobufs.SelectMany(static proto => proto.TopLevels))
{
@ -119,13 +117,14 @@ public partial class ProtodecContext
continue;
}
MessageField field = new(message)
MessageField field = new()
{
Type = ParseFieldType(propertyType, options, protobuf),
Name = TranslateMessageFieldName(property.Name),
Id = (int)idFields[fi].ConstantValue!,
IsObsolete = HasObsoleteAttribute(property.CustomAttributes),
HasHasProp = msgFieldHasHasProp
HasHasProp = msgFieldHasHasProp,
DeclaringMessage = message
};
Logger?.LogParsedField(field.Name, field.Id, field.Type.Name);
@ -499,7 +498,7 @@ public partial class ProtodecContext
Protobuf protobuf = new()
{
AssemblyName = topLevelType.DeclaringAssemblyName,
Namespace = topLevelType.Namespace
CilNamespace = topLevelType.Namespace
};
topLevel.Protobuf = protobuf;