// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Tools.DotNETCommon; using System.IO; namespace UnrealBuildTool { /// /// Types of config file hierarchy /// public enum ConfigHierarchyType { /// /// BaseGame.ini, DefaultGame.ini, etc... /// Game, /// /// BaseEngine.ini, DefaultEngine.ini, etc... /// Engine, /// /// BaseEditorPerProjectUserSettings.ini, DefaultEditorPerProjectUserSettings.ini, etc.. /// EditorPerProjectUserSettings, /// /// BaseEncryption.ini, DefaultEncryption.ini, etc.. /// Encryption, /// /// BaseCrypto.ini, DefaultCrypto.ini, etc.. /// Crypto, /// /// BaseEditorSettings.ini, DefaultEditorSettings.ini, etc... /// EditorSettings, } /// /// Stores a set of merged key/value pairs for a config section /// public class ConfigHierarchySection { /// /// Map of key names to their values /// Dictionary> KeyToValue = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); /// /// Construct a merged config section from the given per-file config sections /// /// Config sections from individual files internal ConfigHierarchySection(IEnumerable FileSections) { foreach(ConfigFileSection FileSection in FileSections) { foreach(ConfigLine Line in FileSection.Lines) { if (Line.Action == ConfigLineAction.RemoveKey) { KeyToValue.Remove(Line.Key); continue; } // Find or create the values for this key List Values; if(KeyToValue.TryGetValue(Line.Key, out Values)) { // Update the existing list if(Line.Action == ConfigLineAction.Set) { Values.Clear(); Values.Add(Line.Value); } else if(Line.Action == ConfigLineAction.Add) { Values.Add(Line.Value); } else if (Line.Action == ConfigLineAction.RemoveKeyValue) { Values.RemoveAll(x => x.Equals(Line.Value, StringComparison.InvariantCultureIgnoreCase)); } } else { // If it's a set or add action, create and add a new list if(Line.Action == ConfigLineAction.Set || Line.Action == ConfigLineAction.Add) { Values = new List(); Values.Add(Line.Value); KeyToValue.Add(Line.Key, Values); } } } } } /// /// Returns a list of key names /// public IEnumerable KeyNames { get { return KeyToValue.Keys; } } /// /// Tries to find the value for a given key /// /// The key name to search for /// On success, receives the corresponding value /// True if the key was found, false otherwise public bool TryGetValue(string KeyName, out string Value) { List ValuesList; if(KeyToValue.TryGetValue(KeyName, out ValuesList) && ValuesList.Count > 0) { Value = ValuesList[0]; return true; } else { Value = null; return false; } } /// /// Tries to find the values for a given key /// /// The key name to search for /// On success, receives a list of the corresponding values /// True if the key was found, false otherwise public bool TryGetValues(string KeyName, out IEnumerable Values) { List ValuesList; if(KeyToValue.TryGetValue(KeyName, out ValuesList)) { Values = ValuesList; return true; } else { Values = null; return false; } } } /// /// Encapsulates a hierarchy of config files, merging sections from them together on request /// public class ConfigHierarchy { /// /// Array of /// ConfigFile[] Files; /// /// Cache of requested config sections /// Dictionary NameToSection = new Dictionary(StringComparer.InvariantCultureIgnoreCase); /// /// Lock for NameToSection /// System.Threading.ReaderWriterLockSlim NameToSectionLock = new System.Threading.ReaderWriterLockSlim(); /// /// Construct a config hierarchy from the given files /// /// Set of files to include (in order) public ConfigHierarchy(IEnumerable Files) { this.Files = Files.ToArray(); } /// /// Names of all sections in all config files /// /// public HashSet SectionNames { get { HashSet Result = new HashSet(); foreach (ConfigFile File in Files) { foreach (string SectionName in File.SectionNames) { if ( !Result.Contains(SectionName) ) { Result.Add(SectionName); } } } return Result; } } /// /// Finds a config section with the given name /// /// Name of the section to look for /// The merged config section public ConfigHierarchySection FindSection(string SectionName) { ConfigHierarchySection Section; try { // Acquire a read lock and do a quick check for the config section NameToSectionLock.EnterUpgradeableReadLock(); if (!NameToSection.TryGetValue(SectionName, out Section)) { try { // Acquire a write lock and add the config section if another thread didn't just complete it NameToSectionLock.EnterWriteLock(); if (!NameToSection.TryGetValue(SectionName, out Section)) { // Find all the raw sections from the file hierarchy List RawSections = new List(); foreach (ConfigFile File in Files) { ConfigFileSection RawSection; if (File.TryGetSection(SectionName, out RawSection)) { RawSections.Add(RawSection); } } // Merge them together and add it to the cache Section = new ConfigHierarchySection(RawSections); NameToSection.Add(SectionName, Section); } } finally { NameToSectionLock.ExitWriteLock(); } } } finally { NameToSectionLock.ExitUpgradeableReadLock(); } return Section; } /// /// Legacy function for ease of transition from ConfigCacheIni to ConfigHierarchy. Gets a bool with the given key name. /// /// Section name /// Key name /// Value associated with the specified key. If the key has more than one value, only the first one is returned /// True if the key exists public bool GetBool(string SectionName, string KeyName, out bool Value) { return TryGetValue(SectionName, KeyName, out Value); } /// /// Legacy function for ease of transition from ConfigCacheIni to ConfigHierarchy. Gets a string with the given key name, returning an empty string on failure. /// /// Section name /// Key name /// Value associated with the specified key. If the key has more than one value, only the first one is returned /// True if the key exists public bool GetArray(string SectionName, string KeyName, out List Values) { IEnumerable ValuesEnumerable; if(TryGetValues(SectionName, KeyName, out ValuesEnumerable)) { Values = ValuesEnumerable.ToList(); return true; } else { Values = null; return false; } } /// /// Legacy function for ease of transition from ConfigCacheIni to ConfigHierarchy. Gets a string with the given key name, returning an empty string on failure. /// /// Section name /// Key name /// Value associated with the specified key. If the key has more than one value, only the first one is returned /// True if the key exists public bool GetString(string SectionName, string KeyName, out string Value) { if(TryGetValue(SectionName, KeyName, out Value)) { return true; } else { Value = ""; return false; } } /// /// Legacy function for ease of transition from ConfigCacheIni to ConfigHierarchy. Gets an int with the given key name. /// /// Section name /// Key name /// Value associated with the specified key. If the key has more than one value, only the first one is returned /// True if the key exists public bool GetInt32(string SectionName, string KeyName, out int Value) { return TryGetValue(SectionName, KeyName, out Value); } /// /// Gets a single string value associated with the specified key. /// /// Section name /// Key name /// Value associated with the specified key. If the key has more than one value, only the first one is returned /// True if the key exists public bool TryGetValue(string SectionName, string KeyName, out string Value) { return FindSection(SectionName).TryGetValue(KeyName, out Value); } /// /// Gets a single bool value associated with the specified key. /// /// Section name /// Key name /// Value associated with the specified key. If the key has more than one value, only the first one is returned /// True if the key exists public bool TryGetValue(string SectionName, string KeyName, out bool Value) { string Text; if(!TryGetValue(SectionName, KeyName, out Text)) { Value = false; return false; } return TryParse(Text, out Value); } /// /// Gets a single Int32 value associated with the specified key. /// /// Section name /// Key name /// Value associated with the specified key. If the key has more than one value, only the first one is returned /// True if the key exists public bool TryGetValue(string SectionName, string KeyName, out int Value) { string Text; if(!TryGetValue(SectionName, KeyName, out Text)) { Value = 0; return false; } return TryParse(Text, out Value); } /// /// Gets a single GUID value associated with the specified key. /// /// Section name /// Key name /// Value associated with the specified key. If the key has more than one value, only the first one is returned /// True if the key exists public bool TryGetValue(string SectionName, string KeyName, out Guid Value) { string Text; if(!TryGetValue(SectionName, KeyName, out Text)) { Value = Guid.Empty; return false; } return TryParse(Text, out Value); } /// /// Gets a single-precision floating point value associated with the specified key. /// /// Section name /// Key name /// Value associated with the specified key. If the key has more than one value, only the first one is returned /// True if the key exists public bool TryGetValue(string SectionName, string KeyName, out float Value) { string Text; if(!TryGetValue(SectionName, KeyName, out Text)) { Value = 0; return false; } return TryParse(Text, out Value); } /// /// Gets a double-precision floating point value associated with the specified key. /// /// Section name /// Key name /// Value associated with the specified key. If the key has more than one value, only the first one is returned /// True if the key exists public bool TryGetValue(string SectionName, string KeyName, out double Value) { string Text; if(!TryGetValue(SectionName, KeyName, out Text)) { Value = 0; return false; } return TryParse(Text, out Value); } /// /// Gets all values associated with the specified key /// /// Section where the key is located /// Key name /// Copy of the list containing all values associated with the specified key /// True if the key exists public bool TryGetValues(string SectionName, string KeyName, out IEnumerable Values) { return FindSection(SectionName).TryGetValues(KeyName, out Values); } /// /// Parse a string as a boolean value /// /// The text to parse /// The parsed value, if successful /// True if the text was parsed, false otherwise static public bool TryParse(string Text, out bool Value) { // C# Boolean type expects "False" or "True" but since we're not case sensitive, we need to suppor that manually if (Text == "1" || Text.Equals("true", StringComparison.InvariantCultureIgnoreCase)) { Value = true; return true; } else if (Text == "0" || Text.Equals("false", StringComparison.InvariantCultureIgnoreCase)) { Value = false; return true; } else { Value = false; return false; } } /// /// Parse a string as an integer value /// /// The text to parse /// The parsed value, if successful /// True if the text was parsed, false otherwise static public bool TryParse(string Text, out int Value) { return Int32.TryParse(Text, out Value); } /// /// Parse a string as a GUID value /// /// The text to parse /// The parsed value, if successful /// True if the text was parsed, false otherwise public static bool TryParse(string Text, out Guid Value) { if (Text.Contains("A=") && Text.Contains("B=") && Text.Contains("C=") && Text.Contains("D=")) { char[] Separators = new char[] { '(', ')', '=', ',', ' ', 'A', 'B', 'C', 'D' }; string[] ComponentValues = Text.Split(Separators, StringSplitOptions.RemoveEmptyEntries); if (ComponentValues.Length == 4) { StringBuilder HexString = new StringBuilder(); for (int ComponentIndex = 0; ComponentIndex < 4; ComponentIndex++) { int IntegerValue; if(!Int32.TryParse(ComponentValues[ComponentIndex], out IntegerValue)) { Value = Guid.Empty; return false; } HexString.Append(IntegerValue.ToString("X8")); } Text = HexString.ToString(); } } return Guid.TryParseExact(Text, "N", out Value); } /// /// Parse a string as a single-precision floating point value /// /// The text to parse /// The parsed value, if successful /// True if the text was parsed, false otherwise public static bool TryParse(string Text, out float Value) { if(Text.EndsWith("f")) { return Single.TryParse(Text.Substring(0, Text.Length - 1), out Value); } else { return Single.TryParse(Text, out Value); } } /// /// Parse a string as a double-precision floating point value /// /// The text to parse /// The parsed value, if successful /// True if the text was parsed, false otherwise public static bool TryParse(string Text, out double Value) { if(Text.EndsWith("f")) { return Double.TryParse(Text.Substring(0, Text.Length - 1), out Value); } else { return Double.TryParse(Text, out Value); } } /// /// Attempts to parse the given line as a UE4 config object (eg. (Name="Foo",Number=1234)). /// /// Line of text to parse /// Receives key/value pairs for the config object /// True if an object was parsed, false otherwise public static bool TryParse(string Line, out Dictionary Properties) { // Convert the string to a zero-terminated array, to make parsing easier. char[] Chars = new char[Line.Length + 1]; Line.CopyTo(0, Chars, 0, Line.Length); // Get the opening paren int Idx = 0; while(Char.IsWhiteSpace(Chars[Idx])) { Idx++; } if(Chars[Idx] != '(') { Properties = null; return false; } // Read to the next token Idx++; while(Char.IsWhiteSpace(Chars[Idx])) { Idx++; } // Create the dictionary to receive the new properties Dictionary NewProperties = new Dictionary(); // Read a sequence of key/value pairs StringBuilder Value = new StringBuilder(); if(Chars[Idx] != ')') { for (;;) { // Find the end of the name int NameIdx = Idx; while(Char.IsLetterOrDigit(Chars[Idx]) || Chars[Idx] == '_') { Idx++; } if(Idx == NameIdx) { Properties = null; return false; } // Extract the key string, and make sure it hasn't already been added string Key = new string(Chars, NameIdx, Idx - NameIdx); if(NewProperties.ContainsKey(Key)) { Properties = null; return false; } // Consume the equals character while(Char.IsWhiteSpace(Chars[Idx])) { Idx++; } if(Chars[Idx] != '=') { Properties = null; return false; } // Move to the value Idx++; while (Char.IsWhiteSpace(Chars[Idx])) { Idx++; } // Parse the value Value.Clear(); if (Char.IsLetterOrDigit(Chars[Idx]) || Chars[Idx] == '_') { while (Char.IsLetterOrDigit(Chars[Idx]) || Chars[Idx] == '_' || Chars[Idx] == '.') { Value.Append(Chars[Idx]); Idx++; } } else if (Chars[Idx] == '\"') { Idx++; for(; Chars[Idx] != '\"'; Idx++) { if (Chars[Idx] == '\0') { Properties = null; return false; } else { Value.Append(Chars[Idx]); } } Idx++; } else if (Chars[Idx] == '(') { Value.Append(Chars[Idx++]); bool bInQuotes = false; for (int Nesting = 1; Nesting > 0; Idx++) { if (Chars[Idx] == '\0') { Properties = null; return false; } else if (Chars[Idx] == '(' && !bInQuotes) { Nesting++; } else if (Chars[Idx] == ')' && !bInQuotes) { Nesting--; } else if (Chars[Idx] == '\"' || Chars[Idx] == '\'') { bInQuotes ^= true; } Value.Append(Chars[Idx]); } } else { Properties = null; return false; } // Extract the value string NewProperties[Key] = Value.ToString(); // Move to the separator while(Char.IsWhiteSpace(Chars[Idx])) { Idx++; } if(Chars[Idx] == ')') { break; } if(Chars[Idx] != ',') { Properties = null; return false; } // Move to the next field Idx++; while (Char.IsWhiteSpace(Chars[Idx])) { Idx++; } } } // Make sure we're at the end of the string Idx++; while(Char.IsWhiteSpace(Chars[Idx])) { Idx++; } if(Chars[Idx] != '\0') { Properties = null; return false; } Properties = NewProperties; return true; } /// /// Returns a list of INI filenames for the given project /// public static IEnumerable EnumerateConfigFileLocations(ConfigHierarchyType Type, DirectoryReference ProjectDir, UnrealTargetPlatform Platform) { string BaseIniName = Enum.GetName(typeof(ConfigHierarchyType), Type); string PlatformName = GetIniPlatformName(Platform); string PlatformParentName = GetIniPlatformParentName(PlatformName); // Engine/Config/Base.ini (included in every ini type, required) yield return FileReference.Combine(UnrealBuildTool.EngineDirectory, "Config", "Base.ini"); // Engine/Config/Base* ini yield return FileReference.Combine(UnrealBuildTool.EngineDirectory, "Config", "Base" + BaseIniName + ".ini"); if (PlatformParentName != "") { // Engine/Config/Platform/BasePlatform* ini yield return FileReference.Combine(UnrealBuildTool.EngineDirectory, "Config", PlatformParentName, "Base" + PlatformParentName + BaseIniName + ".ini"); } if (Platform != UnrealTargetPlatform.Unknown) { // Engine/Config/Platform/BasePlatform* ini yield return FileReference.Combine(UnrealBuildTool.EngineDirectory, "Config", PlatformName, "Base" + PlatformName + BaseIniName + ".ini"); } // Engine/Config/NotForLicensees/Base* ini yield return FileReference.Combine(UnrealBuildTool.EngineDirectory, "Config", "NotForLicensees", "Base" + BaseIniName + ".ini"); // NOTE: 4.7: See comment in GetSourceIniHierarchyFilenames() // Engine/Config/NoRedist/Base* ini // yield return Path.Combine(EngineDirectory, "Config", "NoRedist", "Base" + BaseIniName + ".ini"); if (ProjectDir != null) { // Game/Config/Default* ini yield return FileReference.Combine(ProjectDir, "Config", "Default" + BaseIniName + ".ini"); // Game/Config/NotForLicensees/Default* ini yield return FileReference.Combine(ProjectDir, "Config", "NotForLicensees", "Default" + BaseIniName + ".ini"); // Game/Config/NoRedist/Default* ini yield return FileReference.Combine(ProjectDir, "Config", "NoRedist", "Default" + BaseIniName + ".ini"); } if (PlatformParentName != "") { // Engine/Config/Platform/Platform* ini yield return FileReference.Combine(UnrealBuildTool.EngineDirectory, "Config", PlatformParentName, PlatformParentName + BaseIniName + ".ini"); // Engine/Config/NotForLicensees/Platform/Platform* ini yield return FileReference.Combine(UnrealBuildTool.EngineDirectory, "Config", "NotForLicensees", PlatformParentName, PlatformParentName + BaseIniName + ".ini"); // Engine/Config/NoRedist/Platform/Platform* ini yield return FileReference.Combine(UnrealBuildTool.EngineDirectory, "Config", "NoRedist", PlatformParentName, PlatformParentName + BaseIniName + ".ini"); if (ProjectDir != null) { // Game/Config/Platform/Platform* ini yield return FileReference.Combine(ProjectDir, "Config", PlatformParentName, PlatformParentName + BaseIniName + ".ini"); // Engine/Config/NotForLicensees/Platform/Platform* ini yield return FileReference.Combine(ProjectDir, "Config", "NotForLicensees", PlatformParentName, PlatformParentName + BaseIniName + ".ini"); // Engine/Config/NoRedist/Platform/Platform* ini yield return FileReference.Combine(ProjectDir, "Config", "NoRedist", PlatformParentName, PlatformParentName + BaseIniName + ".ini"); } } if (Platform != UnrealTargetPlatform.Unknown) { // Engine/Config/Platform/Platform* ini yield return FileReference.Combine(UnrealBuildTool.EngineDirectory, "Config", PlatformName, PlatformName + BaseIniName + ".ini"); // Engine/Config/NotForLicensees/Platform/Platform* ini yield return FileReference.Combine(UnrealBuildTool.EngineDirectory, "Config", "NotForLicensees", PlatformName, PlatformName + BaseIniName + ".ini"); // Engine/Config/NoRedist/Platform/Platform* ini yield return FileReference.Combine(UnrealBuildTool.EngineDirectory, "Config", "NoRedist", PlatformName, PlatformName + BaseIniName + ".ini"); if (ProjectDir != null) { // Game/Config/Platform/Platform* ini yield return FileReference.Combine(ProjectDir, "Config", PlatformName, PlatformName + BaseIniName + ".ini"); // Engine/Config/NotForLicensees/Platform/Platform* ini yield return FileReference.Combine(ProjectDir, "Config", "NotForLicensees", PlatformName, PlatformName + BaseIniName + ".ini"); // Engine/Config/NoRedist/Platform/Platform* ini yield return FileReference.Combine(ProjectDir, "Config", "NoRedist", PlatformName, PlatformName + BaseIniName + ".ini"); } } DirectoryReference UserSettingsFolder = Utils.GetUserSettingDirectory(); // Match FPlatformProcess::UserSettingsDir() if(UserSettingsFolder != null) { // /UE4/EngineConfig/User* ini yield return FileReference.Combine(UserSettingsFolder, "Unreal Engine", "Engine", "Config", "User" + BaseIniName + ".ini"); } // Some user accounts (eg. SYSTEM on Windows) don't have a home directory. Ignore them if Environment.GetFolderPath() returns an empty string. string PersonalFolder = Environment.GetFolderPath(Environment.SpecialFolder.Personal); if (!String.IsNullOrEmpty(PersonalFolder)) { DirectoryReference PersonalConfigFolder; // Match FPlatformProcess::UserDir() if (BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Mac) { PersonalConfigFolder = DirectoryReference.Combine(new DirectoryReference(PersonalFolder), "Documents"); } else if (Environment.OSVersion.Platform == PlatformID.Unix) { PersonalConfigFolder = DirectoryReference.Combine(new DirectoryReference(PersonalFolder), "Documents"); } else { PersonalConfigFolder = new DirectoryReference(PersonalFolder); } // /UE4/EngineConfig/User* ini yield return FileReference.Combine(PersonalConfigFolder, "Unreal Engine", "Engine", "Config", "User" + BaseIniName + ".ini"); } // Game/Config/User* ini if (ProjectDir != null) { yield return FileReference.Combine(ProjectDir, "Config", "User" + BaseIniName + ".ini"); } } /// /// Returns the platform name to use as part of platform-specific config files /// public static string GetIniPlatformName(UnrealTargetPlatform TargetPlatform) { if (TargetPlatform == UnrealTargetPlatform.Win32 || TargetPlatform == UnrealTargetPlatform.Win64) { return "Windows"; } else { return Enum.GetName(typeof(UnrealTargetPlatform), TargetPlatform); } } /// /// Returns the ConfigDataDrivenPlatformInfo for the given platform, or null if nothing was found /// public static string GetIniPlatformParentName(string IniPlatformName) { DataDrivenPlatformInfo.ConfigDataDrivenPlatformInfo Info = DataDrivenPlatformInfo.GetDataDrivenInfoForPlatform(IniPlatformName); return (Info == null) ? "" : Info.IniParent; } } }