// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Buffers.Binary;
using System.Buffers.Text;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Text.RegularExpressions;
using EpicGames.Core;
namespace EpicGames.Perforce
{
///
/// String constants for perforce values
///
static class StringConstants
{
public static readonly Utf8String True = new Utf8String("true");
public static readonly Utf8String False = new Utf8String("false");
public static readonly Utf8String New = new Utf8String("new");
public static readonly Utf8String None = new Utf8String("none");
public static readonly Utf8String Default = new Utf8String("default");
}
///
/// Delegate type for creating a record instance
///
/// New instance
public delegate object CreateRecordDelegate();
///
/// Information about a nested record
///
record struct NestedRecordInfo(PropertyInfo PropertyInfo, CreateRecordDelegate CreateInstance);
///
/// Name and rank of a tagged property within a record
///
record struct TaggedPropertyNameAndRank(Utf8String Tag, int Rank);
///
/// Stores cached information about a property with a attribute.
///
class TaggedPropertyInfo
{
///
/// Name of the tag. Specified in the attribute or inferred from the field name.
///
public Utf8String Tag { get; }
///
/// Whether this tag is optional or not.
///
public bool Optional { get; }
///
/// List of nested records before getting to the target object
///
public NestedRecordInfo[] ParentRecords { get; }
///
/// The property containing the value of this data.
///
public PropertyInfo PropertyInfo { get; }
///
/// Writes an instance of this field from an object
///
public Action Write { get; set; }
///
/// Parser for this field type
///
public Action ReadFromInteger { get; set; }
///
/// Parser for this field type
///
public Action ReadFromString { get; set; }
///
/// Index into the bitmask of required types
///
public ulong RequiredTagBitMask { get; }
///
/// Constructor
///
///
///
///
///
///
public TaggedPropertyInfo(Utf8String name, bool optional, NestedRecordInfo[] parentRecords, PropertyInfo propertyInfo, ulong requiredTagBitMask)
{
Tag = name;
Optional = optional;
ParentRecords = parentRecords;
PropertyInfo = propertyInfo;
RequiredTagBitMask = requiredTagBitMask;
Write = (obj, writer) => throw new PerforceException($"Field {name} does not have a serializer.");
ReadFromInteger = (obj, value) => throw new PerforceException($"Field {name} was not expecting an integer value.");
ReadFromString = (obj, str) => throw new PerforceException($"Field {name} was not expecting a string value.");
}
///
public override string ToString()
{
StringBuilder fullName = new StringBuilder(Tag.ToString());
if (ParentRecords.Length > 0)
{
fullName.Append('0');
for (int idx = 1; idx < ParentRecords.Length; idx++)
{
fullName.Append(",0");
}
}
return fullName.ToString();
}
}
///
/// Stores cached information about a record
///
class CachedRecordInfo
{
///
/// Type of the record
///
public Type Type { get; }
///
/// Method to construct this record
///
public CreateRecordDelegate CreateInstance { get; }
///
/// List of fields in the record. These should be ordered to match P4 output for maximum efficiency.
///
public List Properties { get; } = new List();
///
/// Map of name to tag info
///
public Dictionary NameAndRankToInfo { get; set; } = new Dictionary();
///
/// Bitmask of all the required tags. Formed by bitwise-or'ing the RequiredTagBitMask fields for each required CachedTagInfo.
///
public ulong RequiredTagsBitMask { get; set; }
///
/// Constructor
///
/// The record type
/// Method used to create an instance of this record
public CachedRecordInfo(Type type, CreateRecordDelegate createInstance)
{
Type = type;
CreateInstance = createInstance;
}
}
///
/// 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();
///
/// Map of value to name
///
public Dictionary _valueToName = new Dictionary();
///
/// List of name/value pairs
///
public List> _nameValuePairs = new List>();
///
/// Constructor
///
/// The type to construct from
public CachedEnumInfo(Type enumType)
{
_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)
{
Utf8String name = new Utf8String(attribute.Name);
_nameToValue[name] = (int)value;
_valueToName[(int)value] = name;
_nameValuePairs.Add(new KeyValuePair(attribute.Name, (int)value));
}
}
}
}
///
/// Gets the name of a particular enum value
///
///
///
public Utf8String GetName(int value) => _valueToName[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(Utf8String text)
{
return Enum.ToObject(_enumType, ParseToInteger(text));
}
///
/// Parses the given text as an enum
///
///
///
public int ParseToInteger(Utf8String 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
Utf8String 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 readonly ConcurrentDictionary s_enumTypeToInfo = new ConcurrentDictionary();
///
/// Cached set of record
///
static readonly ConcurrentDictionary s_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));
///
/// Default type for errors
///
public static CachedRecordInfo IoRecordInfo = GetCachedRecordInfo(typeof(PerforceIo));
///
/// Serializes a sequence of objects to a stream
///
/// Object to serialize
/// Writer for output data
public static void Serialize(object obj, IMemoryWriter writer)
{
CachedRecordInfo recordInfo = GetCachedRecordInfo(obj.GetType());
foreach (TaggedPropertyInfo tagInfo in recordInfo.Properties)
{
object? value = tagInfo.PropertyInfo.GetValue(obj);
if (value != null)
{
WriteUtf8StringWithTag(writer, tagInfo.Tag);
tagInfo.Write(writer, value!);
}
}
}
static void WriteIntegerWithTag(IMemoryWriter writer, int value)
{
Span span = writer.GetSpanAndAdvance(5);
span[0] = (byte)'i';
BinaryPrimitives.WriteInt32LittleEndian(span.Slice(1, 4), value);
}
static void WriteStringWithTag(IMemoryWriter writer, string str)
{
int length = Encoding.UTF8.GetByteCount(str);
Span span = writer.GetSpanAndAdvance(1 + length + 4);
span[0] = (byte)'s';
BinaryPrimitives.WriteInt32LittleEndian(span.Slice(1, 4), length);
Encoding.UTF8.GetBytes(str, span.Slice(5));
}
static void WriteUtf8StringWithTag(IMemoryWriter writer, Utf8String str)
{
Span span = writer.GetSpanAndAdvance(1 + str.Length + 4);
span[0] = (byte)'s';
BinaryPrimitives.WriteInt32LittleEndian(span.Slice(1, 4), str.Length);
str.Span.CopyTo(span.Slice(5));
}
///
/// 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 (!s_enumTypeToInfo.TryGetValue(enumType, out enumInfo))
{
enumInfo = new CachedEnumInfo(enumType);
if (!s_enumTypeToInfo.TryAdd(enumType, enumInfo))
{
enumInfo = s_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 (!s_recordTypeToInfo.TryGetValue(recordType, out record))
{
record = new CachedRecordInfo(recordType, GetCreateRecordDelegate(recordType));
// Find all the properties in the record
AddRecordProperties(recordType, record, Array.Empty());
record.NameAndRankToInfo = record.Properties.ToDictionary(x => new TaggedPropertyNameAndRank(x.Tag, x.ParentRecords.Length), x => x);
// Try to save the record info, or get the version that's already in the cache
if (!s_recordTypeToInfo.TryAdd(recordType, record))
{
record = s_recordTypeToInfo[recordType];
}
}
return record;
}
static void AddRecordProperties(Type recordType, CachedRecordInfo rootRecord, NestedRecordInfo[] parentRecords)
{
// Get all the fields for this type
PropertyInfo[] properties = recordType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
// Build the map of all tags for this record
foreach (PropertyInfo property in properties)
{
PerforceTagAttribute? tagAttribute = property.GetCustomAttribute();
if (tagAttribute != null)
{
string tagName = tagAttribute.Name ?? property.Name;
ulong requiredTagBitMask = 0;
if (!tagAttribute.Optional && parentRecords.Length == 0)
{
requiredTagBitMask = rootRecord.RequiredTagsBitMask + 1;
if (requiredTagBitMask == 0)
{
throw new PerforceException("Too many required tags in {0}; max is {1}", recordType.Name, sizeof(ulong) * 8);
}
rootRecord.RequiredTagsBitMask |= requiredTagBitMask;
}
TaggedPropertyInfo tagInfo = new TaggedPropertyInfo(new Utf8String(tagName), tagAttribute.Optional, parentRecords, property, requiredTagBitMask);
Type fieldType = property.PropertyType;
PropertyInfo propertyCopy = property;
if (fieldType == typeof(DateTime))
{
tagInfo.Write = (writer, value) => WriteUtf8StringWithTag(writer, new Utf8String(((long)((DateTime)value - PerforceReflection.UnixEpoch).TotalSeconds).ToString()));
tagInfo.ReadFromString = (obj, value) => propertyCopy.SetValue(obj, ParseStringAsDateTime(value));
}
else if (fieldType == typeof(bool))
{
tagInfo.Write = (writer, value) => WriteUtf8StringWithTag(writer, ((bool)value) ? StringConstants.True : StringConstants.False);
tagInfo.ReadFromString = (obj, value) => propertyCopy.SetValue(obj, ParseStringAsBool(value));
}
else if (fieldType == typeof(Nullable))
{
tagInfo.ReadFromString = (obj, value) => propertyCopy.SetValue(obj, ParseStringAsNullableBool(value));
}
else if (fieldType == typeof(int))
{
tagInfo.Write = (writer, value) => WriteIntegerWithTag(writer, (int)value);
tagInfo.ReadFromInteger = (obj, value) => propertyCopy.SetValue(obj, value);
tagInfo.ReadFromString = (obj, value) => propertyCopy.SetValue(obj, ParseStringAsInt(value));
}
else if (fieldType == typeof(long))
{
tagInfo.Write = (writer, value) => WriteUtf8StringWithTag(writer, new Utf8String(((long)value).ToString()));
tagInfo.ReadFromString = (obj, value) => propertyCopy.SetValue(obj, ParseStringAsLong(value));
}
else if (fieldType == typeof(string))
{
tagInfo.Write = (writer, value) => WriteStringWithTag(writer, (string)value);
tagInfo.ReadFromString = (obj, value) => propertyCopy.SetValue(obj, ParseString(value));
}
else if (fieldType == typeof(Utf8String))
{
tagInfo.Write = (writer, value) => WriteUtf8StringWithTag(writer, (Utf8String)value);
tagInfo.ReadFromString = (obj, str) => propertyCopy.SetValue(obj, str.Clone());
}
else if (fieldType.IsEnum)
{
CachedEnumInfo enumInfo = GetCachedEnumInfo(fieldType);
tagInfo.Write = (writer, value) => WriteUtf8StringWithTag(writer, enumInfo.GetName((int)value));
tagInfo.ReadFromInteger = (obj, value) => propertyCopy.SetValue(obj, enumInfo.ParseInteger(value));
tagInfo.ReadFromString = (obj, value) => propertyCopy.SetValue(obj, enumInfo.ParseString(value));
}
else if (fieldType == typeof(DateTimeOffset?))
{
tagInfo.ReadFromString = (obj, value) => propertyCopy.SetValue(obj, ParseStringAsNullableDateTimeOffset(value));
}
else if (fieldType == typeof(List))
{
tagInfo.ReadFromString = (obj, value) => ((List)propertyCopy.GetValue(obj)!).Add(value.ToString());
}
else if (fieldType == typeof(ReadOnlyMemory))
{
tagInfo.ReadFromString = (obj, value) => propertyCopy.SetValue(obj, value.Memory);
}
else
{
throw new PerforceException("Unsupported type of {0}.{1} for tag '{2}'", recordType.Name, fieldType.Name, tagName);
}
rootRecord.Properties.Add(tagInfo);
}
PerforceRecordListAttribute? subElementAttribute = property.GetCustomAttribute();
if (subElementAttribute != null)
{
Type newRecordType = property.PropertyType.GenericTypeArguments[0];
NestedRecordInfo newParentRecord = new NestedRecordInfo(property, GetCreateRecordDelegate(newRecordType));
AddRecordProperties(newRecordType, rootRecord, parentRecords.Append(newParentRecord).ToArray());
}
}
}
static CreateRecordDelegate GetCreateRecordDelegate(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);
return (CreateRecordDelegate)dynamicMethod.CreateDelegate(typeof(CreateRecordDelegate));
}
static object ParseString(Utf8String str)
{
return str.ToString();
}
static object ParseStringAsDateTime(Utf8String str)
{
string text = str.ToString();
DateTime time;
if (DateTime.TryParse(text, out time))
{
return time;
}
else
{
return PerforceReflection.UnixEpoch + TimeSpan.FromSeconds(Int64.Parse(text));
}
}
static object ParseStringAsBool(Utf8String str)
{
return str.Length == 0 || str == StringConstants.True;
}
static object ParseStringAsNullableBool(Utf8String str)
{
return str == StringConstants.True;
}
static object ParseStringAsInt(Utf8String str)
{
int value;
int bytesConsumed;
if (Utf8Parser.TryParse(str.Span, out value, out bytesConsumed) && bytesConsumed == str.Length)
{
return value;
}
else if (str == StringConstants.New || str == StringConstants.None)
{
return -1;
}
else if (str.Length > 0 && str[0] == '#')
{
return ParseStringAsInt(str.Slice(1));
}
else if (str == StringConstants.Default)
{
return DefaultChange;
}
else
{
throw new PerforceException($"Unable to parse {str} as an integer");
}
}
static object ParseStringAsLong(Utf8String str)
{
long value;
int bytesConsumed;
if (!Utf8Parser.TryParse(str.Span, out value, out bytesConsumed) || bytesConsumed != str.Length)
{
throw new PerforceException($"Unable to parse {str} as a long value");
}
return value;
}
static object ParseStringAsNullableDateTimeOffset(Utf8String str)
{
string text = str.ToString();
return DateTimeOffset.Parse(Regex.Replace(text, "[^0-9]*$", "")); // Strip timezone name (eg. "EST")
}
}
}