// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Buffers.Text;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Reflection.Metadata.Ecma335;
using System.Text;
using System.Text.RegularExpressions;
using EpicGames.Core;
namespace EpicGames.Perforce
{
///
/// String constants for perforce values
///
static class StringConstants
{
public static readonly ReadOnlyUtf8String True = new ReadOnlyUtf8String("true");
public static readonly ReadOnlyUtf8String New = new ReadOnlyUtf8String("new");
public static readonly ReadOnlyUtf8String None = new ReadOnlyUtf8String("none");
public static readonly ReadOnlyUtf8String Default = new ReadOnlyUtf8String("default");
}
///
/// Stores cached information about a field with a P4Tag attribute
///
class CachedTagInfo
{
///
/// Name of the tag. Specified in the attribute or inferred from the field name.
///
public ReadOnlyUtf8String Name;
///
/// Whether this tag is optional or not.
///
public bool Optional;
///
/// The field containing the value of this data.
///
public FieldInfo Field;
///
/// Parser for this field type
///
public Action SetFromInteger;
///
/// Parser for this field type
///
public Action SetFromString;
///
/// Index into the bitmask of required types
///
public ulong RequiredTagBitMask;
///
/// Constructor
///
///
///
///
///
public CachedTagInfo(ReadOnlyUtf8String Name, bool Optional, FieldInfo Field, ulong RequiredTagBitMask)
{
this.Name = Name;
this.Optional = Optional;
this.Field = Field;
this.RequiredTagBitMask = RequiredTagBitMask;
this.SetFromInteger = (Obj, Value) => throw new PerforceException($"Field {Name} was not expecting an integer value.");
this.SetFromString = (Obj, String) => throw new PerforceException($"Field {Name} was not expecting a string value.");
}
}
///
/// Stores cached information about a record
///
class CachedRecordInfo
{
///
/// Delegate type for creating a record instance
///
/// New instance
public delegate object CreateRecordDelegate();
///
/// Type of the record
///
public Type Type;
///
/// Method to construct this record
///
public CreateRecordDelegate CreateInstance;
///
/// List of fields in the record. These should be ordered to match P4 output for maximum efficiency.
///
public List Fields = new List();
///
/// Map of name to tag info
///
public Dictionary NameToInfo = new Dictionary();
///
/// Bitmask of all the required tags. Formed by bitwise-or'ing the RequiredTagBitMask fields for each required CachedTagInfo.
///
public ulong RequiredTagsBitMask;
///
/// The type of records to create for subelements
///
public Type? SubElementType;
///
/// The cached record info for the subelement type
///
public CachedRecordInfo? SubElementRecordInfo;
///
/// Field containing subelements
///
public FieldInfo? SubElementField;
///
/// Constructor
///
/// The record type
public CachedRecordInfo(Type Type)
{
this.Type = Type;
ConstructorInfo? Constructor = Type.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null);
if(Constructor == null)
{
throw new PerforceException($"Unable to find default constructor for {Type}");
}
DynamicMethod DynamicMethod = new DynamicMethod("_", Type, null);
ILGenerator Generator = DynamicMethod.GetILGenerator();
Generator.Emit(OpCodes.Newobj, Constructor);
Generator.Emit(OpCodes.Ret);
CreateInstance = (CreateRecordDelegate)DynamicMethod.CreateDelegate(typeof(CreateRecordDelegate));
}
}
///
/// Information about an enum
///
class CachedEnumInfo
{
///
/// The enum type
///
public Type EnumType;
///
/// Whether the enum has the [Flags] attribute
///
public bool bHasFlagsAttribute;
///
/// Map of name to value
///
public Dictionary NameToValue = new Dictionary();
///
/// List of name/value pairs
///
public List> NameValuePairs = new List>();
///
/// Constructor
///
/// The type to construct from
public CachedEnumInfo(Type EnumType)
{
this.EnumType = EnumType;
bHasFlagsAttribute = EnumType.GetCustomAttribute() != null;
FieldInfo[] Fields = EnumType.GetFields(BindingFlags.Public | BindingFlags.Static);
foreach (FieldInfo Field in Fields)
{
PerforceEnumAttribute? Attribute = Field.GetCustomAttribute();
if (Attribute != null)
{
object? Value = Field.GetValue(null);
if (Value != null)
{
ReadOnlyUtf8String Name = new ReadOnlyUtf8String(Attribute.Name);
NameToValue[Name] = (int)Value;
NameValuePairs.Add(new KeyValuePair(Attribute.Name, (int)Value));
}
}
}
}
///
/// Parses the given integer as an enum
///
/// The value to convert to an enum
/// The enum value corresponding to the given value
public object ParseInteger(int Value)
{
return Enum.ToObject(EnumType, Value);
}
///
/// Parses the given text as an enum
///
/// The text to parse
/// The enum value corresponding to the given text
public object ParseString(ReadOnlyUtf8String Text)
{
return Enum.ToObject(EnumType, ParseToInteger(Text));
}
///
/// Parses the given text as an enum
///
///
///
public int ParseToInteger(ReadOnlyUtf8String Name)
{
if (bHasFlagsAttribute)
{
int Result = 0;
for (int Offset = 0; Offset < Name.Length;)
{
if (Name.Span[Offset] == (byte)' ')
{
Offset++;
}
else
{
// Find the end of this name
int StartOffset = ++Offset;
while (Offset < Name.Length && Name.Span[Offset] != (byte)' ')
{
Offset++;
}
// Take the subset
ReadOnlyUtf8String Item = Name.Slice(StartOffset, Offset - StartOffset);
// Get the value
int ItemValue;
if (NameToValue.TryGetValue(Item, out ItemValue))
{
Result |= ItemValue;
}
}
}
return Result;
}
else
{
int Result;
NameToValue.TryGetValue(Name, out Result);
return Result;
}
}
///
/// Parses an enum value, using PerforceEnumAttribute markup for names.
///
/// Value of the enum.
/// Text for the enum.
public string GetEnumText(int Value)
{
if (bHasFlagsAttribute)
{
List Names = new List();
int CombinedIntegerValue = 0;
foreach (KeyValuePair Pair in NameValuePairs)
{
if ((Value & Pair.Value) != 0)
{
Names.Add(Pair.Key);
CombinedIntegerValue |= Pair.Value;
}
}
if (CombinedIntegerValue != Value)
{
throw new ArgumentException($"Invalid enum value {Value}");
}
return String.Join(" ", Names);
}
else
{
string? Name = null;
foreach (KeyValuePair Pair in NameValuePairs)
{
if (Value == Pair.Value)
{
Name = Pair.Key;
break;
}
}
if (Name == null)
{
throw new ArgumentException($"Invalid enum value {Value}");
}
return Name;
}
}
}
///
/// Utility methods for converting to/from native types
///
static class PerforceReflection
{
///
/// Unix epoch; used for converting times back into C# datetime objects
///
public static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
///
/// Constant for the default changelist, where valid.
///
public const int DefaultChange = -2;
///
/// Cached map of enum types to a lookup mapping from p4 strings to enum values.
///
static ConcurrentDictionary EnumTypeToInfo = new ConcurrentDictionary();
///
/// Cached set of record
///
static ConcurrentDictionary RecordTypeToInfo = new ConcurrentDictionary();
///
/// Default type for info
///
public static CachedRecordInfo InfoRecordInfo = GetCachedRecordInfo(typeof(PerforceInfo));
///
/// Default type for errors
///
public static CachedRecordInfo ErrorRecordInfo = GetCachedRecordInfo(typeof(PerforceError));
///
/// Gets a mapping of flags to enum values for the given type
///
/// The enum type to retrieve flags for
/// Map of name to enum value
static CachedEnumInfo GetCachedEnumInfo(Type EnumType)
{
CachedEnumInfo? EnumInfo;
if (!EnumTypeToInfo.TryGetValue(EnumType, out EnumInfo))
{
EnumInfo = new CachedEnumInfo(EnumType);
if (!EnumTypeToInfo.TryAdd(EnumType, EnumInfo))
{
EnumInfo = EnumTypeToInfo[EnumType];
}
}
return EnumInfo;
}
///
/// Parses an enum value, using PerforceEnumAttribute markup for names.
///
/// Type of the enum to parse.
/// Value of the enum.
/// Text for the enum.
public static string GetEnumText(Type EnumType, object Value)
{
return GetCachedEnumInfo(EnumType).GetEnumText((int)Value);
}
///
/// Gets reflection data for the given record type
///
/// The type to retrieve record info for
/// The cached reflection information for the given type
public static CachedRecordInfo GetCachedRecordInfo(Type RecordType)
{
CachedRecordInfo? Record;
if (!RecordTypeToInfo.TryGetValue(RecordType, out Record))
{
Record = new CachedRecordInfo(RecordType);
// Get all the fields for this type
FieldInfo[] Fields = RecordType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
// Build the map of all tags for this record
foreach (FieldInfo Field in Fields)
{
PerforceTagAttribute? TagAttribute = Field.GetCustomAttribute();
if (TagAttribute != null)
{
string TagName = TagAttribute.Name ?? Field.Name;
ulong RequiredTagBitMask = 0;
if (!TagAttribute.Optional)
{
RequiredTagBitMask = Record.RequiredTagsBitMask + 1;
if (RequiredTagBitMask == 0)
{
throw new PerforceException("Too many required tags in {0}; max is {1}", RecordType.Name, sizeof(ulong) * 8);
}
Record.RequiredTagsBitMask |= RequiredTagBitMask;
}
CachedTagInfo TagInfo = new CachedTagInfo(new ReadOnlyUtf8String(TagName), TagAttribute.Optional, Field, RequiredTagBitMask);
Type FieldType = Field.FieldType;
FieldInfo FieldCopy = Field;
if (FieldType == typeof(DateTime))
{
TagInfo.SetFromString = (Obj, String) => FieldCopy.SetValue(Obj, ParseStringAsDateTime(String));
}
else if (FieldType == typeof(bool))
{
TagInfo.SetFromString = (Obj, String) => FieldCopy.SetValue(Obj, ParseStringAsBool(String));
}
else if (FieldType == typeof(Nullable))
{
TagInfo.SetFromString = (Obj, String) => FieldCopy.SetValue(Obj, ParseStringAsNullableBool(String));
}
else if (FieldType == typeof(int))
{
TagInfo.SetFromInteger = (Obj, Int) => FieldCopy.SetValue(Obj, Int);
TagInfo.SetFromString = (Obj, String) => FieldCopy.SetValue(Obj, ParseStringAsInt(String));
}
else if (FieldType == typeof(long))
{
TagInfo.SetFromString = (Obj, String) => FieldCopy.SetValue(Obj, ParseStringAsLong(String));
}
else if (FieldType == typeof(string))
{
TagInfo.SetFromString = (Obj, String) => FieldCopy.SetValue(Obj, ParseString(String));
}
else if (FieldType.IsEnum)
{
CachedEnumInfo EnumInfo = GetCachedEnumInfo(FieldType);
TagInfo.SetFromInteger = (Obj, Int) => FieldCopy.SetValue(Obj, EnumInfo.ParseInteger(Int));
TagInfo.SetFromString = (Obj, String) => FieldCopy.SetValue(Obj, EnumInfo.ParseString(String));
}
else if (FieldType == typeof(DateTimeOffset?))
{
TagInfo.SetFromString = (Obj, String) => FieldCopy.SetValue(Obj, ParseStringAsNullableDateTimeOffset(String));
}
else if (FieldType == typeof(List))
{
TagInfo.SetFromString = (Obj, String) => ((List)FieldCopy.GetValue(Obj)!).Add(String.ToString());
}
else
{
throw new PerforceException("Unsupported type of {0}.{1} for tag '{2}'", RecordType.Name, FieldType.Name, TagName);
}
Record.Fields.Add(TagInfo);
}
Record.NameToInfo = Record.Fields.ToDictionary(x => x.Name, x => x);
PerforceRecordListAttribute? SubElementAttribute = Field.GetCustomAttribute();
if (SubElementAttribute != null)
{
Record.SubElementField = Field;
Record.SubElementType = Field.FieldType.GenericTypeArguments[0];
Record.SubElementRecordInfo = GetCachedRecordInfo(Record.SubElementType);
}
}
// Try to save the record info, or get the version that's already in the cache
if (!RecordTypeToInfo.TryAdd(RecordType, Record))
{
Record = RecordTypeToInfo[RecordType];
}
}
return Record;
}
static object ParseString(ReadOnlyUtf8String String)
{
return String.ToString();
}
static object ParseStringAsDateTime(ReadOnlyUtf8String String)
{
string Text = String.ToString();
DateTime Time;
if (DateTime.TryParse(Text, out Time))
{
return Time;
}
else
{
return PerforceReflection.UnixEpoch + TimeSpan.FromSeconds(long.Parse(Text));
}
}
static object ParseStringAsBool(ReadOnlyUtf8String String)
{
return String.Length == 0 || String == StringConstants.True;
}
static object ParseStringAsNullableBool(ReadOnlyUtf8String String)
{
return String == StringConstants.True;
}
static object ParseStringAsInt(ReadOnlyUtf8String String)
{
int Value;
int BytesConsumed;
if (Utf8Parser.TryParse(String.Span, out Value, out BytesConsumed) && BytesConsumed == String.Length)
{
return Value;
}
else if(String == StringConstants.New || String == StringConstants.None)
{
return -1;
}
else if(String.Length > 0 && String[0] == '#')
{
return ParseStringAsInt(String.Slice(1));
}
else if(String == StringConstants.Default)
{
return DefaultChange;
}
else
{
throw new PerforceException($"Unable to parse {String} as an integer");
}
}
static object ParseStringAsLong(ReadOnlyUtf8String String)
{
long Value;
int BytesConsumed;
if (!Utf8Parser.TryParse(String.Span, out Value, out BytesConsumed) || BytesConsumed != String.Length)
{
throw new PerforceException($"Unable to parse {String} as a long value");
}
return Value;
}
static object ParseStringAsNullableDateTimeOffset(ReadOnlyUtf8String String)
{
string Text = String.ToString();
return DateTimeOffset.Parse(Regex.Replace(Text, "[a-zA-Z ]*$", "")); // Strip timezone name (eg. "EST")
}
}
}