// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using EpicGames.Core; using UnrealBuildTool; namespace UnrealBuildTool { /// /// Stores parsed values from XML config files which can be applied to a configurable type. Can be serialized to disk in binary form as a cache. /// class XmlConfigData { /// /// The current cache serialization version /// const int SerializationVersion = 2; /// /// List of input files. Stored to allow checking cache validity. /// public FileReference[] InputFiles; /// /// Abstract description of a target data member. /// public abstract class TargetMember { /// /// Returns Reflection.MemberInfo describing the target class member. /// public abstract MemberInfo MemberInfo { get; } /// /// Returns Reflection.Type of the target class member. /// public abstract Type Type { get; } /// /// Indicates whether the target class member is static or not. /// public abstract bool IsStatic { get; } /// /// Returns the value setter of the target class member. /// public abstract Action SetValue { get; } /// /// Returns the value getter of the target class member. /// public abstract Func GetValue { get; } } /// /// Description of a field member. /// public class TargetField : TargetMember { public override MemberInfo MemberInfo => FieldInfo; public override Type Type => FieldInfo.FieldType; public override bool IsStatic => FieldInfo.IsStatic; public override Action SetValue => FieldInfo.SetValue; public override Func GetValue => FieldInfo.GetValue; private FieldInfo FieldInfo; public TargetField(FieldInfo FieldInfo) { this.FieldInfo = FieldInfo; } } /// /// Description of a property member. /// public class TargetProperty : TargetMember { public override MemberInfo MemberInfo => PropertyInfo; public override Type Type => PropertyInfo.PropertyType; public override bool IsStatic => PropertyInfo.GetGetMethod()!.IsStatic; public override Action SetValue => PropertyInfo.SetValue; public override Func GetValue => PropertyInfo.GetValue; private PropertyInfo PropertyInfo; public TargetProperty(PropertyInfo PropertyInfo) { this.PropertyInfo = PropertyInfo; } } public class ValueInfo { public TargetMember Target; public object Value; public FileReference SourceFile; public XmlConfigFileAttribute XmlConfigAttribute; public ValueInfo(FieldInfo FieldInfo, object Value, FileReference SourceFile, XmlConfigFileAttribute XmlConfigAttribute) { this.Target = new TargetField(FieldInfo); this.Value = Value; this.SourceFile = SourceFile; this.XmlConfigAttribute = XmlConfigAttribute; } public ValueInfo(PropertyInfo PropertyInfo, object Value, FileReference SourceFile, XmlConfigFileAttribute XmlConfigAttribute) { this.Target = new TargetProperty(PropertyInfo); this.Value = Value; this.SourceFile = SourceFile; this.XmlConfigAttribute = XmlConfigAttribute; } public ValueInfo(TargetMember Target, object Value, FileReference SourceFile, XmlConfigFileAttribute XmlConfigAttribute) { this.Target = Target; this.Value = Value; this.SourceFile = SourceFile; this.XmlConfigAttribute = XmlConfigAttribute; } } /// /// Stores a mapping from type -> member -> value, with all the config values for configurable fields. /// public Dictionary TypeToValues; /// /// Constructor /// /// /// public XmlConfigData(FileReference[] InputFiles, Dictionary TypeToValues) { this.InputFiles = InputFiles; this.TypeToValues = TypeToValues; } /// /// Attempts to read a previous block of config values from disk /// /// The file to read from /// Array of valid types. Used to resolve serialized type names to concrete types. /// On success, receives the parsed data /// True if the data was read and is valid public static bool TryRead(FileReference Location, IEnumerable Types, [NotNullWhen(true)] out XmlConfigData? Data) { // Check the file exists first if (!FileReference.Exists(Location)) { Data = null; return false; } // Read the cache from disk using (BinaryReader Reader = new BinaryReader(File.Open(Location.FullName, FileMode.Open, FileAccess.Read, FileShare.Read))) { // Check the serialization version matches if(Reader.ReadInt32() != SerializationVersion) { Data = null; return false; } // Read the input files FileReference[] InputFiles = Reader.ReadArray(() => Reader.ReadFileReference())!; // Read the types int NumTypes = Reader.ReadInt32(); Dictionary TypeToValues = new Dictionary(NumTypes); for(int TypeIdx = 0; TypeIdx < NumTypes; TypeIdx++) { // Read the type name string TypeName = Reader.ReadString(); // Try to find it in the list of configurable types Type? Type = Types.FirstOrDefault(x => x.Name == TypeName); if(Type == null) { Data = null; return false; } // Read all the values ValueInfo[] Values = new ValueInfo[Reader.ReadInt32()]; for (int ValueIdx = 0; ValueIdx < Values.Length; ValueIdx++) { string MemberName = Reader.ReadString(); XmlConfigData.TargetMember? TargetMember = GetTargetMemberWithAttribute(Type, MemberName); if (TargetMember != null) { // If TargetMember is not null, we know it has our attribute. XmlConfigFileAttribute XmlConfigAttribute = TargetMember!.MemberInfo.GetCustomAttribute()!; // Try to parse the value and add it to the output array object Value = Reader.ReadObject(TargetMember.Type)!; // Read the path of the config file that provided this setting FileReference SourceFile = Reader.ReadFileReference(); Values[ValueIdx] = new ValueInfo(TargetMember, Value, SourceFile, XmlConfigAttribute); } else { Data = null; return false; } } // Add it to the type map TypeToValues.Add(Type, Values); } // Return the parsed data Data = new XmlConfigData(InputFiles.ToArray(), TypeToValues); return true; } } /// /// Find a data member (field or property) with the given name and attribute and returns TargetMember wrapper created for it. /// /// Attribute a member has to have to be considered. /// Type which members are to be searched /// Name of a member (field or property) to find. /// TargetMember wrapper or null if no member has been found. private static XmlConfigData.TargetMember? GetTargetMemberWithAttribute(Type Type, string MemberName) where T : System.Attribute { T? XmlConfigAttribute = null; FieldInfo? Field = Type.GetField(MemberName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); XmlConfigAttribute = Field?.GetCustomAttribute(); if (Field != null && XmlConfigAttribute != null) { return new XmlConfigData.TargetField(Field); } PropertyInfo? Property = Type.GetProperty(MemberName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); XmlConfigAttribute = Property?.GetCustomAttribute(); if (Property != null && XmlConfigAttribute != null) { return new XmlConfigData.TargetProperty(Property); } return null; } /// /// Writes the coalesced config hierarchy to disk /// /// File to write to public void Write(FileReference Location) { DirectoryReference.CreateDirectory(Location.Directory); using (BinaryWriter Writer = new BinaryWriter(File.Open(Location.FullName, FileMode.Create, FileAccess.Write, FileShare.Read))) { Writer.Write(SerializationVersion); // Save all the input files. The cache will not be valid if these change. Writer.Write(InputFiles, Item => Writer.Write(Item)); // Write all the categories Writer.Write(TypeToValues.Count); foreach(KeyValuePair TypePair in TypeToValues) { Writer.Write(TypePair.Key.Name); Writer.Write(TypePair.Value.Length); foreach(ValueInfo MemberPair in TypePair.Value) { Writer.Write(MemberPair.Target.MemberInfo.Name); Writer.Write(MemberPair.Target.Type, MemberPair.Value); Writer.Write(MemberPair.SourceFile); } } } } } }