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