Files
UnrealEngineUWP/Engine/Source/Programs/UnrealBuildTool/System/ConfigFile.cs
josh adams e41490b912 - Added a system to remap .ini section and key names. This will convert names on load, so saving out the section will use new names, and should not cause issues (we do not force update because the files are usually checked in, so we warn in logs and in the editor on start to tell folks to update).
- Deprecation warnings will show up in C# log, C++ log and in editor as a load warning
- Next changelist will use the system to make some replacements
#rb david.hibbits
#jira UE-130960
#preflight 631f3c4194758d0bf225bd95

[CL 21974033 by josh adams in ue5-main branch]
2022-09-12 18:15:29 -04:00

488 lines
15 KiB
C#

// 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.Text;
using System.Threading.Tasks;
using EpicGames.Core;
using UnrealBuildTool;
using Microsoft.Extensions.Logging;
namespace UnrealBuildTool
{
/// <summary>
/// Specifies the action to take for a config line, as denoted by its prefix.
/// </summary>
public enum ConfigLineAction
{
/// <summary>
/// Assign the value to the key
/// </summary>
Set,
/// <summary>
/// Add the value to the key (denoted with +X=Y in config files)
/// </summary>
Add,
/// <summary>
/// Remove the key without having to match value (denoted with !X in config files)
/// </summary>
RemoveKey,
/// <summary>
/// Remove the matching key and value (denoted with -X=Y in config files)
/// </summary>
RemoveKeyValue
}
/// <summary>
/// Contains a pre-parsed raw config line, consisting of action, key and value components.
/// </summary>
public class ConfigLine
{
/// <summary>
/// The action to take when merging this key/value pair with an existing value
/// </summary>
public ConfigLineAction Action;
/// <summary>
/// Name of the key to modify
/// </summary>
public string Key;
/// <summary>
/// Value to assign to the key
/// </summary>
public string Value;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="Action">Action to take when merging this key/value pair with an existing value</param>
/// <param name="Key">Name of the key to modify</param>
/// <param name="Value">Value to assign</param>
public ConfigLine(ConfigLineAction Action, string Key, string Value)
{
this.Action = Action;
this.Key = Key;
this.Value = Value;
}
/// <summary>
/// Formats this object for the debugger
/// </summary>
/// <returns>The original config line</returns>
public override string ToString()
{
string Prefix = (Action == ConfigLineAction.Add)? "+" : (Action == ConfigLineAction.RemoveKey)? "!" : (Action == ConfigLineAction.RemoveKeyValue) ? "-" : "";
return String.Format("{0}{1}={2}", Prefix, Key, Value);
}
}
/// <summary>
/// Contains the lines which appeared in a config section, with all comments and whitespace removed
/// </summary>
public class ConfigFileSection
{
/// <summary>
/// Name of the section
/// </summary>
public string Name;
/// <summary>
/// Lines which appeared in the config section
/// </summary>
public List<ConfigLine> Lines = new List<ConfigLine>();
/// <summary>
/// Construct an empty config section with the given name
/// </summary>
/// <param name="Name">Name of the config section</param>
public ConfigFileSection(string Name)
{
this.Name = Name;
}
/// <summary>
/// try to get a line using it's name, if the line doesn't exist returns false
/// </summary>
/// <param name="Name">Name of the line you want to get</param>
/// <param name="OutLine">The result of the operation</param>
/// <returns>return true if the line is retrieved return false and null OutLine if Name isn't found in this section</returns>
public bool TryGetLine(string Name, [NotNullWhen(true)] out ConfigLine? OutLine)
{
foreach ( ConfigLine Line in Lines)
{
if (Line.Key.Equals(Name))
{
OutLine = Line;
return true;
}
}
OutLine = null;
return false;
}
}
/// <summary>
/// Represents a single config file as stored on disk.
/// </summary>
public class ConfigFile
{
/// <summary>
/// Maps names to config sections
/// </summary>
Dictionary<string, ConfigFileSection> Sections = new Dictionary<string, ConfigFileSection>(StringComparer.InvariantCultureIgnoreCase);
// Remap of config names/sections
static private Dictionary<string, string> SectionNameRemap = new();
static private Dictionary<string, Dictionary<string, string>> SectionKeyRemap = new();
static private HashSet<string> WarnedKeys = new(StringComparer.InvariantCultureIgnoreCase);
static private string Remap(Dictionary<string, string>? Remap, string Key, string Context)
{
if (Remap != null)
{
string? Remapped;
if (Remap.TryGetValue(Key, out Remapped))
{
if (!WarnedKeys.Contains(Key))
{
WarnedKeys.Add(Key);
Log.Logger.LogWarning($"DEPRECATION: '{Key}', {Context}, has been deprecated. Using '{Remapped}' instead. It is recommended you update your .ini files as soon as possible, and replace {Key} with {Remapped}");
}
return Remapped;
}
}
return Key;
}
/// <summary>
/// Static (class) constructor which is called before any ConfigFile instance is created
/// </summary>
static ConfigFile()
{
// read the special ConfigRemap.ini file into sections
FileReference ConfigRemapFile = FileReference.Combine(UnrealBuildBase.Unreal.EngineDirectory, "Config", "ConfigRedirects.ini");
Dictionary<string, ConfigFileSection> Sections = new(StringComparer.InvariantCultureIgnoreCase);
ReadIntoSections(ConfigRemapFile, Sections, ConfigLineAction.Set);
// walk over the sections, where all but the special SectionNameRemap section is a section of keys to remap in that same section
foreach (KeyValuePair<string, ConfigFileSection> Pair in Sections)
{
// remember a remap for section names
if (Pair.Key.Equals("SectionNameRemap", StringComparison.InvariantCultureIgnoreCase))
{
foreach (ConfigLine Line in Pair.Value.Lines)
{
SectionNameRemap.Add(Line.Key, Line.Value);
}
}
else
{
// any other section is rmembered by the section name here, and each key/value pair is a remap for the given section
Dictionary<string, string> KeyRemap = new(StringComparer.InvariantCultureIgnoreCase);
SectionKeyRemap.Add(Pair.Key, KeyRemap);
foreach (ConfigLine Line in Pair.Value.Lines)
{
KeyRemap.Add(Line.Key, Line.Value);
}
}
}
}
/// <summary>
/// Constructs a new, empty config file
/// </summary>
/// <param name="DefaultAction">The default action to take when encountering arrays without a '+' prefix</param>
public ConfigFile(ConfigLineAction DefaultAction = ConfigLineAction.Set)
{
}
/// <summary>
/// Reads config data from the given file.
/// </summary>
/// <param name="Location">File to read from</param>
/// <param name="DefaultAction">The default action to take when encountering arrays without a '+' prefix</param>
public ConfigFile(FileReference Location, ConfigLineAction DefaultAction = ConfigLineAction.Set)
{
ReadIntoSections(Location, Sections, DefaultAction);
}
private static void ReadIntoSections(FileReference Location, Dictionary<string, ConfigFileSection> Sections, ConfigLineAction DefaultAction)
{
using (StreamReader Reader = new StreamReader(Location.FullName))
{
ConfigFileSection? CurrentSection = null;
Dictionary<string, string>? CurrentRemap = null;
for (; ;)
{
string? Line = Reader.ReadLine();
if (Line == null)
{
break;
}
// Find the first non-whitespace character
for (int StartIdx = 0; StartIdx < Line.Length; StartIdx++)
{
if (Line[StartIdx] != ' ' && Line[StartIdx] != '\t')
{
// Find the last non-whitespace character. If it's an escaped newline, merge the following line with it.
int EndIdx = Line.Length;
while (EndIdx > StartIdx)
{
if(Line[EndIdx - 1] == '\\')
{
string? NextLine = Reader.ReadLine();
if(NextLine == null)
{
break;
}
Line += NextLine;
EndIdx = Line.Length;
continue;
}
if(Line[EndIdx - 1] != ' ' && Line[EndIdx - 1] != '\t')
{
break;
}
EndIdx--;
}
// Break out if we've got a comment
if(Line[StartIdx] == ';')
{
break;
}
if(Line[StartIdx] == '/' && StartIdx + 1 < Line.Length && Line[StartIdx + 1] == '/')
{
break;
}
// Check if it's the start of a new section
if(Line[StartIdx] == '[')
{
CurrentSection = null;
if(Line[EndIdx - 1] == ']')
{
string SectionName = Line.Substring(StartIdx + 1, EndIdx - StartIdx - 2);
// lookup remaps
SectionName = Remap(SectionNameRemap, SectionName, $"which is a config section in '{Location.FullName}'");
SectionKeyRemap.TryGetValue(SectionName, out CurrentRemap);
if (!Sections.TryGetValue(SectionName, out CurrentSection))
{
CurrentSection = new ConfigFileSection(SectionName);
Sections.Add(SectionName, CurrentSection);
}
}
break;
}
// Otherwise add it to the current section or add a new one
if(CurrentSection != null)
{
TryAddConfigLine(CurrentSection, CurrentRemap, Location.FullName, Line, StartIdx, EndIdx, DefaultAction);
break;
}
// Otherwise just ignore it
break;
}
}
}
}
}
/// <summary>
/// Reads config data from the given string.
/// </summary>
/// <param name="IniText">Single line string of config settings in the format [Section1]:Key1=Value1,[Section2]:Key2=Value2</param>
/// <param name="DefaultAction">The default action to take when encountering arrays without a '+' prefix</param>
public ConfigFile(string IniText, ConfigLineAction DefaultAction = ConfigLineAction.Set)
{
// Break into individual settings of the form [Section]:Key=Value
string[] SettingLines = IniText.Split(new char[] { ',' });
Dictionary<string, string>? CurrentRemap;
foreach (string Setting in SettingLines)
{
// Locate and break off the section name
string SectionName = Setting.Remove(Setting.IndexOf(':')).Trim(new char[] { '[', ']' });
// lookup remaps
Remap(SectionNameRemap, SectionName, $"which is a config section (found in a string in code - search your .ini files for it, the source file is unknown)");
SectionKeyRemap.TryGetValue(SectionName, out CurrentRemap);
if (SectionName.Length > 0)
{
ConfigFileSection? CurrentSection = null;
if (!Sections.TryGetValue(SectionName, out CurrentSection))
{
CurrentSection = new ConfigFileSection(SectionName);
Sections.Add(SectionName, CurrentSection);
}
if (CurrentSection != null)
{
string IniKeyValue = Setting.Substring(Setting.IndexOf(':') + 1);
TryAddConfigLine(CurrentSection, CurrentRemap, "unknown source file", IniKeyValue, 0, IniKeyValue.Length, DefaultAction);
}
}
}
}
/// <summary>
/// Try to parse a key/value pair from the given line, and add it to a config section
/// </summary>
/// <param name="Section">The section to receive the parsed config line</param>
/// <param name="KeyRemap">Optional map to change names of keys on read</param>
/// <param name="Filename">Optional map to change names of keys on read</param>
/// <param name="Line">Text to parse</param>
/// <param name="StartIdx">Index of the first non-whitespace character in this line</param>
/// <param name="EndIdx">Index of the last (exclusive) non-whitespace character in this line</param>
/// <param name="DefaultAction">The default action to take if '+' or '-' is not specified on a config line</param>
/// <returns>True if the line was parsed correctly, false otherwise</returns>
static bool TryAddConfigLine(ConfigFileSection Section, Dictionary<string, string>? KeyRemap, string Filename, string Line, int StartIdx, int EndIdx, ConfigLineAction DefaultAction)
{
// Find the '=' character separating key and value
int EqualsIdx = Line.IndexOf('=', StartIdx, EndIdx - StartIdx);
if(EqualsIdx == -1 && Line[StartIdx] != '!')
{
return false;
}
// Keep track of the start of the key name
int KeyStartIdx = StartIdx;
// Remove the +/-/! prefix, if present
ConfigLineAction Action = DefaultAction;
if(Line[KeyStartIdx] == '+' || Line[KeyStartIdx] == '-' || Line[KeyStartIdx] == '!')
{
Action = (Line[KeyStartIdx] == '+')? ConfigLineAction.Add : (Line[KeyStartIdx] == '!') ? ConfigLineAction.RemoveKey : ConfigLineAction.RemoveKeyValue;
KeyStartIdx++;
while(Line[KeyStartIdx] == ' ' || Line[KeyStartIdx] == '\t')
{
KeyStartIdx++;
}
}
// RemoveKey actions do not require a value
if (Action == ConfigLineAction.RemoveKey && EqualsIdx == -1)
{
Section.Lines.Add(new ConfigLine(Action, Line.Substring(KeyStartIdx).Trim(), ""));
return true;
}
// Remove trailing spaces after the name of the key
int KeyEndIdx = EqualsIdx;
for(; KeyEndIdx > KeyStartIdx; KeyEndIdx--)
{
if(Line[KeyEndIdx - 1] != ' ' && Line[KeyEndIdx - 1] != '\t')
{
break;
}
}
// Make sure there's a non-empty key name
if(KeyStartIdx == EqualsIdx)
{
return false;
}
// Skip whitespace between the '=' and the start of the value
int ValueStartIdx = EqualsIdx + 1;
for(; ValueStartIdx < EndIdx; ValueStartIdx++)
{
if(Line[ValueStartIdx] != ' ' && Line[ValueStartIdx] != '\t')
{
break;
}
}
// Strip quotes around the value if present
int ValueEndIdx = EndIdx;
if(ValueEndIdx >= ValueStartIdx + 2 && Line[ValueStartIdx] == '"' && Line[ValueEndIdx - 1] == '"')
{
ValueStartIdx++;
ValueEndIdx--;
}
// Add it to the config section
string Key = Line.Substring(KeyStartIdx, KeyEndIdx - KeyStartIdx);
string Value = Line.Substring(ValueStartIdx, ValueEndIdx - ValueStartIdx);
// remap the key if needed
Key = Remap(KeyRemap, Key, $"which is a config key in section [{Section.Name}], in '{Filename}'");
Section.Lines.Add(new ConfigLine(Action, Key, Value));
return true;
}
/// <summary>
/// Names of sections in this file
/// </summary>
public IEnumerable<string> SectionNames
{
get { return Sections.Keys; }
}
/// <summary>
/// Tries to get a config section by name, or creates one if it doesn't exist
/// </summary>
/// <param name="SectionName">Name of the section to look for</param>
/// <returns>The config section</returns>
public ConfigFileSection FindOrAddSection(string SectionName)
{
ConfigFileSection? Section;
if(!Sections.TryGetValue(SectionName, out Section))
{
Section = new ConfigFileSection(SectionName);
Sections.Add(SectionName, Section);
}
return Section;
}
/// <summary>
/// Tries to get a config section by name
/// </summary>
/// <param name="SectionName">Name of the section to look for</param>
/// <param name="RawSection">On success, the config section that was found</param>
/// <returns>True if the section was found, false otherwise</returns>
public bool TryGetSection(string SectionName, [NotNullWhen(true)] out ConfigFileSection? RawSection)
{
return Sections.TryGetValue(SectionName, out RawSection);
}
/// <summary>
/// Write the config file out to the given location. Useful for debugging.
/// </summary>
/// <param name="Location">The file to write</param>
public void Write(FileReference Location)
{
using (StreamWriter Writer = new StreamWriter(Location.FullName))
{
foreach (ConfigFileSection Section in Sections.Values)
{
Writer.WriteLine("[{0}]", Section.Name);
foreach (ConfigLine Line in Section.Lines)
{
Writer.WriteLine("{0}", Line.ToString());
}
Writer.WriteLine();
}
}
}
}
}