2024-01-22 01:33:05 +00:00
|
|
|
|
// Copyright <20> 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/.
|
|
|
|
|
|
2023-10-31 02:30:03 +00:00
|
|
|
|
using System;
|
|
|
|
|
using System.CodeDom.Compiler;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Diagnostics.CodeAnalysis;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Reflection;
|
2024-01-22 01:33:05 +00:00
|
|
|
|
using SystemEx;
|
|
|
|
|
using SystemEx.Collections;
|
2023-10-31 02:30:03 +00:00
|
|
|
|
using CommunityToolkit.Diagnostics;
|
|
|
|
|
|
|
|
|
|
namespace LibProtodec;
|
|
|
|
|
|
2024-01-22 01:33:05 +00:00
|
|
|
|
public delegate bool LookupFunc(string key, [MaybeNullWhen(false)] out string value);
|
2023-10-31 02:30:03 +00:00
|
|
|
|
|
2024-01-22 01:33:05 +00:00
|
|
|
|
public sealed class ProtodecContext
|
|
|
|
|
{
|
2023-12-25 01:58:12 +00:00
|
|
|
|
private readonly Dictionary<string, Protobuf> _protobufs = [];
|
|
|
|
|
private readonly HashSet<string> _currentDescent = [];
|
2023-10-31 02:30:03 +00:00
|
|
|
|
|
|
|
|
|
public LookupFunc? CustomTypeLookup { get; init; }
|
|
|
|
|
|
|
|
|
|
public LookupFunc? CustomNameLookup { get; init; }
|
|
|
|
|
|
|
|
|
|
public IReadOnlyDictionary<string, Protobuf> Protobufs =>
|
|
|
|
|
_protobufs;
|
|
|
|
|
|
|
|
|
|
public void WriteAllTo(IndentedTextWriter writer)
|
|
|
|
|
{
|
|
|
|
|
Protobuf.WritePreambleTo(writer);
|
|
|
|
|
|
|
|
|
|
foreach (Protobuf proto in _protobufs.Values)
|
|
|
|
|
{
|
|
|
|
|
proto.WriteTo(writer);
|
|
|
|
|
writer.WriteLine();
|
|
|
|
|
writer.WriteLine();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-25 01:58:12 +00:00
|
|
|
|
public void ParseMessage(Type type, bool skipEnums = false, bool skipPropertiesWithoutProtocAttribute = false)
|
2023-10-31 02:30:03 +00:00
|
|
|
|
{
|
|
|
|
|
Guard.IsTrue(type.IsClass);
|
|
|
|
|
|
2023-12-25 01:58:12 +00:00
|
|
|
|
ParseMessageInternal(type, skipEnums, skipPropertiesWithoutProtocAttribute, null);
|
2023-10-31 02:30:03 +00:00
|
|
|
|
_currentDescent.Clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void ParseEnum(Type type)
|
|
|
|
|
{
|
|
|
|
|
Guard.IsTrue(type.IsEnum);
|
|
|
|
|
|
|
|
|
|
ParseEnumInternal(type, null);
|
|
|
|
|
_currentDescent.Clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool IsParsed(Type type, Message? parentMessage, out Dictionary<string, Protobuf> protobufs)
|
|
|
|
|
{
|
|
|
|
|
protobufs = parentMessage is not null && type.IsNested
|
|
|
|
|
? parentMessage.Nested
|
|
|
|
|
: _protobufs;
|
|
|
|
|
|
|
|
|
|
return protobufs.ContainsKey(type.Name)
|
|
|
|
|
|| !_currentDescent.Add(type.Name);
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-25 01:58:12 +00:00
|
|
|
|
private void ParseMessageInternal(Type messageClass, bool skipEnums, bool skipPropertiesWithoutProtocAttribute, Message? parentMessage)
|
2023-10-31 02:30:03 +00:00
|
|
|
|
{
|
|
|
|
|
if (IsParsed(messageClass, parentMessage, out Dictionary<string, Protobuf> protobufs))
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Message message = new()
|
|
|
|
|
{
|
|
|
|
|
Name = TranslateProtobufName(messageClass.Name),
|
|
|
|
|
AssemblyName = messageClass.Assembly.FullName,
|
|
|
|
|
Namespace = messageClass.Namespace
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
FieldInfo[] idFields = messageClass.GetFields(BindingFlags.Public | BindingFlags.Static);
|
|
|
|
|
PropertyInfo[] properties = messageClass.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
|
|
|
|
|
|
|
|
|
|
for (int pi = 0, fi = 0; pi < properties.Length; pi++, fi++)
|
|
|
|
|
{
|
|
|
|
|
PropertyInfo property = properties[pi];
|
|
|
|
|
|
2024-01-22 01:33:05 +00:00
|
|
|
|
if ((skipPropertiesWithoutProtocAttribute && !HasProtocAttribute(property))
|
|
|
|
|
|| property.GetMethod?.IsVirtual != false)
|
2023-10-31 02:30:03 +00:00
|
|
|
|
{
|
|
|
|
|
fi--;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Type propertyType = property.PropertyType;
|
|
|
|
|
|
|
|
|
|
// only OneOf enums are defined nested directly in the message class
|
2024-01-22 01:33:05 +00:00
|
|
|
|
if (propertyType.IsEnum
|
|
|
|
|
&& propertyType.DeclaringType?.Name == messageClass.Name)
|
2023-10-31 02:30:03 +00:00
|
|
|
|
{
|
|
|
|
|
string oneOfName = TranslateOneOfName(property.Name);
|
|
|
|
|
int[] oneOfProtoFieldIds = propertyType.GetFields(BindingFlags.Public | BindingFlags.Static)
|
2024-03-19 00:11:12 +00:00
|
|
|
|
.Select(static field => (int)field.GetRawConstantValue()!)
|
|
|
|
|
.Where(static id => id > 0)
|
2023-10-31 02:30:03 +00:00
|
|
|
|
.ToArray();
|
|
|
|
|
|
|
|
|
|
message.OneOfs.Add(oneOfName, oneOfProtoFieldIds);
|
|
|
|
|
|
|
|
|
|
fi--;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FieldInfo idField = idFields[fi];
|
|
|
|
|
Guard.IsTrue(idField.IsLiteral);
|
|
|
|
|
Guard.IsEqualTo(idField.FieldType.Name, nameof(Int32));
|
|
|
|
|
|
|
|
|
|
int msgFieldId = (int)idField.GetRawConstantValue()!;
|
|
|
|
|
bool msgFieldIsOptional = false;
|
2023-12-25 01:58:12 +00:00
|
|
|
|
string msgFieldType = ParseFieldType(propertyType, skipEnums, skipPropertiesWithoutProtocAttribute, message);
|
2023-10-31 02:30:03 +00:00
|
|
|
|
string msgFieldName = TranslateMessageFieldName(property.Name);
|
|
|
|
|
|
|
|
|
|
// optional protobuf fields will generate an additional "Has" get-only boolean property immediately after the real property
|
|
|
|
|
if (properties.Length > pi + 1 && properties[pi + 1].PropertyType.Name == nameof(Boolean) && !properties[pi + 1].CanWrite)
|
|
|
|
|
{
|
|
|
|
|
msgFieldIsOptional = true;
|
|
|
|
|
pi++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
message.Fields.Add(msgFieldId, (msgFieldIsOptional, msgFieldType, msgFieldName));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protobufs.Add(message.Name, message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ParseEnumInternal(Type enumEnum, Message? parentMessage)
|
|
|
|
|
{
|
|
|
|
|
if (IsParsed(enumEnum, parentMessage, out Dictionary<string, Protobuf> protobufs))
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Enum @enum = new()
|
|
|
|
|
{
|
|
|
|
|
Name = TranslateProtobufName(enumEnum.Name),
|
|
|
|
|
AssemblyName = enumEnum.Assembly.FullName,
|
|
|
|
|
Namespace = enumEnum.Namespace
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
foreach (FieldInfo field in enumEnum.GetFields(BindingFlags.Public | BindingFlags.Static))
|
|
|
|
|
{
|
|
|
|
|
int enumFieldId = (int)field.GetRawConstantValue()!;
|
|
|
|
|
string enumFieldName = TranslateEnumFieldName(field, @enum.Name);
|
|
|
|
|
|
|
|
|
|
@enum.Fields.Add(enumFieldId, enumFieldName);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protobufs.Add(@enum.Name, @enum);
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-25 01:58:12 +00:00
|
|
|
|
private string ParseFieldType(Type type, bool skipEnums, bool skipPropertiesWithoutProtocAttribute, Message message)
|
2023-10-31 02:30:03 +00:00
|
|
|
|
{
|
|
|
|
|
switch (type.Name)
|
|
|
|
|
{
|
|
|
|
|
case "ByteString":
|
2024-01-22 01:33:05 +00:00
|
|
|
|
return FieldTypeName.Bytes;
|
2023-10-31 02:30:03 +00:00
|
|
|
|
case nameof(String):
|
2024-01-22 01:33:05 +00:00
|
|
|
|
return FieldTypeName.String;
|
2023-10-31 02:30:03 +00:00
|
|
|
|
case nameof(Boolean):
|
2024-01-22 01:33:05 +00:00
|
|
|
|
return FieldTypeName.Bool;
|
2023-10-31 02:30:03 +00:00
|
|
|
|
case nameof(Double):
|
2024-01-22 01:33:05 +00:00
|
|
|
|
return FieldTypeName.Double;
|
2023-10-31 02:30:03 +00:00
|
|
|
|
case nameof(UInt32):
|
2024-01-22 01:33:05 +00:00
|
|
|
|
return FieldTypeName.UInt32;
|
2023-10-31 02:30:03 +00:00
|
|
|
|
case nameof(UInt64):
|
2024-01-22 01:33:05 +00:00
|
|
|
|
return FieldTypeName.UInt64;
|
2023-10-31 02:30:03 +00:00
|
|
|
|
case nameof(Int32):
|
2024-01-22 01:33:05 +00:00
|
|
|
|
return FieldTypeName.Int32;
|
2023-10-31 02:30:03 +00:00
|
|
|
|
case nameof(Int64):
|
2024-01-22 01:33:05 +00:00
|
|
|
|
return FieldTypeName.Int64;
|
2023-10-31 02:30:03 +00:00
|
|
|
|
case nameof(Single):
|
2024-01-22 01:33:05 +00:00
|
|
|
|
return FieldTypeName.Float;
|
2023-10-31 02:30:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (type.GenericTypeArguments.Length)
|
|
|
|
|
{
|
|
|
|
|
case 1:
|
2023-12-25 01:58:12 +00:00
|
|
|
|
string t = ParseFieldType(type.GenericTypeArguments[0], skipEnums, skipPropertiesWithoutProtocAttribute, message);
|
2023-10-31 02:30:03 +00:00
|
|
|
|
return "repeated " + t;
|
|
|
|
|
case 2:
|
2023-12-25 01:58:12 +00:00
|
|
|
|
string t1 = ParseFieldType(type.GenericTypeArguments[0], skipEnums, skipPropertiesWithoutProtocAttribute, message);
|
|
|
|
|
string t2 = ParseFieldType(type.GenericTypeArguments[1], skipEnums, skipPropertiesWithoutProtocAttribute, message);
|
2023-10-31 02:30:03 +00:00
|
|
|
|
return $"map<{t1}, {t2}>";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (CustomTypeLookup?.Invoke(type.Name, out string? fieldType) == true)
|
|
|
|
|
{
|
|
|
|
|
return fieldType;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (type.IsEnum)
|
|
|
|
|
{
|
|
|
|
|
if (skipEnums)
|
|
|
|
|
{
|
2024-01-22 01:33:05 +00:00
|
|
|
|
return FieldTypeName.Int32;
|
2023-10-31 02:30:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ParseEnumInternal(type, message);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2023-12-25 01:58:12 +00:00
|
|
|
|
ParseMessageInternal(type, skipEnums, skipPropertiesWithoutProtocAttribute, message);
|
2023-10-31 02:30:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!type.IsNested)
|
|
|
|
|
{
|
|
|
|
|
message.Imports.Add(type.Name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return type.Name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string TranslateProtobufName(string name) =>
|
|
|
|
|
CustomNameLookup?.Invoke(name, out string? translatedName) == true
|
|
|
|
|
? translatedName
|
|
|
|
|
: name;
|
|
|
|
|
|
|
|
|
|
private string TranslateOneOfName(string oneOfEnumName) =>
|
|
|
|
|
TranslateName(oneOfEnumName, out string translatedName)
|
|
|
|
|
? translatedName.TrimEnd("Case")
|
|
|
|
|
: oneOfEnumName.TrimEnd("Case")
|
|
|
|
|
.ToSnakeCaseLower();
|
|
|
|
|
|
|
|
|
|
private string TranslateMessageFieldName(string fieldName) =>
|
|
|
|
|
TranslateName(fieldName, out string translatedName)
|
|
|
|
|
? translatedName
|
|
|
|
|
: fieldName.ToSnakeCaseLower();
|
|
|
|
|
|
|
|
|
|
private string TranslateEnumFieldName(FieldInfo field, string enumName)
|
|
|
|
|
{
|
|
|
|
|
if (field.GetCustomAttributesData()
|
2024-03-19 00:11:12 +00:00
|
|
|
|
.SingleOrDefault(static attr => attr.AttributeType.Name == "OriginalNameAttribute")
|
2023-10-31 02:30:03 +00:00
|
|
|
|
?.ConstructorArguments[0]
|
|
|
|
|
.Value
|
|
|
|
|
is string originalName)
|
|
|
|
|
{
|
|
|
|
|
return originalName;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (TranslateName(field.Name, out string translatedName))
|
|
|
|
|
{
|
|
|
|
|
return translatedName;
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-22 01:33:05 +00:00
|
|
|
|
if (!IsBeebyted(enumName))
|
2023-10-31 02:30:03 +00:00
|
|
|
|
{
|
|
|
|
|
enumName = enumName.ToSnakeCaseUpper();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return enumName + '_' + field.Name.ToSnakeCaseUpper();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool TranslateName(string name, out string translatedName)
|
|
|
|
|
{
|
|
|
|
|
if (CustomNameLookup?.Invoke(name, out translatedName!) == true)
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
translatedName = name;
|
2024-01-22 01:33:05 +00:00
|
|
|
|
return IsBeebyted(name);
|
2023-10-31 02:30:03 +00:00
|
|
|
|
}
|
2024-01-22 01:33:05 +00:00
|
|
|
|
|
|
|
|
|
// ReSharper disable once IdentifierTypo
|
|
|
|
|
private static bool IsBeebyted(string name) =>
|
|
|
|
|
name.Length == 11 && name.CountUpper() == 11;
|
|
|
|
|
|
|
|
|
|
private static bool HasProtocAttribute(MemberInfo member) =>
|
|
|
|
|
member.GetCustomAttributesData()
|
2024-03-19 00:11:12 +00:00
|
|
|
|
.Any(static attr => attr.AttributeType.Name == nameof(GeneratedCodeAttribute)
|
|
|
|
|
&& attr.ConstructorArguments[0].Value as string == "protoc");
|
2023-10-31 02:30:03 +00:00
|
|
|
|
}
|