// 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 EpicGames.Core; 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 { get; set; } /// /// 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 readonly FieldInfo _fieldInfo; public TargetField(FieldInfo fieldInfo) { _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 readonly PropertyInfo _propertyInfo; public TargetProperty(PropertyInfo propertyInfo) { _propertyInfo = propertyInfo; } } public class ValueInfo { public TargetMember Target { get; init; } public object Value { get; init; } public FileReference SourceFile { get; init; } public XmlConfigFileAttribute XmlConfigAttribute { get; init; } public ValueInfo(FieldInfo fieldInfo, object value, FileReference sourceFile, XmlConfigFileAttribute xmlConfigAttribute) : this(new TargetField(fieldInfo), value, sourceFile, xmlConfigAttribute) { } public ValueInfo(PropertyInfo propertyInfo, object value, FileReference sourceFile, XmlConfigFileAttribute xmlConfigAttribute) : this(new TargetProperty(propertyInfo), value, sourceFile, xmlConfigAttribute) { } public ValueInfo(TargetMember target, object value, FileReference sourceFile, XmlConfigFileAttribute xmlConfigAttribute) { Target = target; Value = value; SourceFile = sourceFile; XmlConfigAttribute = xmlConfigAttribute; } } /// /// Stores a mapping from type -> member -> value, with all the config values for configurable fields. /// public Dictionary TypeToValues { get; init; } /// /// Constructor /// /// /// public XmlConfigData(FileReference[] inputFiles, Dictionary typeToValues) { InputFiles = inputFiles; 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(); 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 TargetMember? GetTargetMemberWithAttribute(Type type, string memberName) where T : Attribute { FieldInfo? field = type.GetField(memberName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); T? xmlConfigAttribute = field?.GetCustomAttribute(); if (field != null && xmlConfigAttribute != null) { return new 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 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); } } } } } }