// 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") } } }