// Copyright Epic Games, Inc. All Rights Reserved. using EpicGames.Core; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; namespace UnrealGameSync { [JsonConverter(typeof(ConfigObjectJsonConverter))] public class ConfigObject { const string ConfigSeparatorCharacters = "(),= \t\""; public List> Pairs; public ConfigObject() { Pairs = new List>(); } public ConfigObject(ConfigObject Other) { Pairs = new List>(Other.Pairs); } public ConfigObject(string Text) { Pairs = new List>(); ParseConfigString(Text); } public ConfigObject(ConfigObject BaseObject, string Text) { Pairs = new List>(BaseObject.Pairs); ParseConfigString(Text); } void ParseConfigString(string Text) { int Idx = 0; if(ParseConfigToken(Text, ref Idx) == "(") { while(Idx < Text.Length) { // Read the next key/value pair string? Key = ParseConfigToken(Text, ref Idx); if (Key == null) { return; } if(ParseConfigToken(Text, ref Idx) == "=") { string? Value = ParseConfigValueToken(Text, ref Idx); if (Value == null) { return; } SetValue(Key, Value); } // Check for the end of the list, or a comma before the next pair for(;;) { string? Token = ParseConfigValueToken(Text, ref Idx); if(Token == ",") { break; } if(Token == ")" || Token == null) { return; } } } } } static string? ParseConfigToken(string Text, ref int Idx) { // Skip whitespace while(Idx < Text.Length && Char.IsWhiteSpace(Text[Idx])) { Idx++; } if(Idx == Text.Length) { return null; } // Read the token if (Text[Idx] == '\"') { StringBuilder Token = new StringBuilder(); while (++Idx < Text.Length) { if (Text[Idx] == '\"') { Idx++; break; } if (Text[Idx] == '\\' && Idx + 1 < Text.Length) { Idx++; } Token.Append(Text[Idx]); } return Token.ToString(); } else if (ConfigSeparatorCharacters.IndexOf(Text[Idx]) != -1) { return Text[Idx++].ToString(); } else { int StartIdx = Idx; while (Idx < Text.Length && ConfigSeparatorCharacters.IndexOf(Text[Idx]) == -1) { Idx++; } return Text.Substring(StartIdx, Idx - StartIdx); } } static string? ParseConfigValueToken(string Text, ref int Idx) { string? Token = ParseConfigToken(Text, ref Idx); if (Token == "(") { int StartIdx = Idx - 1; for (; ; ) { string? NextToken = ParseConfigValueToken(Text, ref Idx); if (NextToken == null || NextToken == ")") { break; } } Token = Text.Substring(StartIdx, Idx - StartIdx); } return Token; } [return: NotNullIfNotNull("DefaultValue")] public string? GetValue(string Key, string? DefaultValue = null) { for(int Idx = 0; Idx < Pairs.Count; Idx++) { if(Pairs[Idx].Key.Equals(Key, StringComparison.InvariantCultureIgnoreCase)) { return Pairs[Idx].Value; } } return DefaultValue; } public Guid GetValue(string Key, Guid DefaultValue) { string? StringValue = GetValue(Key); if(StringValue != null) { Guid Value; if(Guid.TryParse(StringValue, out Value)) { return Value; } } return DefaultValue; } public int GetValue(string Key, int DefaultValue) { string? StringValue = GetValue(Key); if(StringValue != null) { int Value; if(int.TryParse(StringValue, out Value)) { return Value; } } return DefaultValue; } public bool GetValue(string Key, bool DefaultValue) { string? StringValue = GetValue(Key); if(StringValue != null) { bool Value; if(bool.TryParse(StringValue, out Value)) { return Value; } } return DefaultValue; } public void SetValue(string Key, string? Value) { if (Value == null) { Pairs.RemoveAll(x => x.Key.Equals(Key, StringComparison.OrdinalIgnoreCase)); } else { for (int Idx = 0; Idx < Pairs.Count; Idx++) { if (Pairs[Idx].Key.Equals(Key, StringComparison.OrdinalIgnoreCase)) { Pairs[Idx] = new KeyValuePair(Key, Value); return; } } Pairs.Add(new KeyValuePair(Key, Value)); } } public void SetValue(string Key, Guid Value) { SetValue(Key, Value.ToString()); } public void SetValue(string Key, int Value) { SetValue(Key, Value.ToString()); } public void SetValue(string Key, bool Value) { SetValue(Key, Value.ToString()); } public string? this[string Key] { get { return GetValue(Key); } set { SetValue(Key, value); } } public void SetDefaults(ConfigObject Other) { foreach(KeyValuePair Pair in Other.Pairs) { if(GetValue(Pair.Key) == null) { SetValue(Pair.Key, Pair.Value); } } } public void AddOverrides(ConfigObject Object, ConfigObject? DefaultObject) { foreach(KeyValuePair Pair in Object.Pairs) { if(DefaultObject == null || DefaultObject.GetValue(Pair.Key) != Pair.Value) { SetValue(Pair.Key, Pair.Value); } } } public string ToString(ConfigObject? BaseObject) { StringBuilder Result = new StringBuilder(); Result.Append("("); foreach(KeyValuePair Pair in Pairs) { if(BaseObject == null || BaseObject.GetValue(Pair.Key) != Pair.Value) { if(Result.Length > 1) { Result.Append(", "); } Result.Append(Pair.Key); Result.Append("="); if(Pair.Value == null) { Result.Append("\"\""); } else { Result.AppendFormat("\"{0}\"", Pair.Value.Replace("\\", "\\\\").Replace("\"", "\\\"")); } } } Result.Append(")"); return Result.ToString(); } public override string ToString() { return ToString(null); } } public class ConfigObjectJsonConverter : JsonConverter { public override ConfigObject Read(ref Utf8JsonReader Reader, Type TypeToConvert, JsonSerializerOptions Options) { ConfigObject Object = new ConfigObject(); if (Reader.TokenType == JsonTokenType.StartObject) { while (Reader.Read() && Reader.TokenType == JsonTokenType.PropertyName) { string Name = Reader.GetString(); Reader.Read(); string Value = Reader.GetString(); Object.Pairs.Add(KeyValuePair.Create(Name, Value)); } } else { Reader.Skip(); } return Object; } public override void Write(Utf8JsonWriter Writer, ConfigObject Value, JsonSerializerOptions Options) { Writer.WriteStartObject(); foreach (KeyValuePair Pair in Value.Pairs) { Writer.WriteString(Pair.Key, Pair.Value); } Writer.WriteEndObject(); } } [DebuggerDisplay("{Name}")] public class ConfigSection { public string Name; public Dictionary Pairs = new Dictionary(); public ConfigSection(string InName) { Name = InName; } public void Clear() { Pairs.Clear(); } public void SetValue(string Key, int Value) { Pairs[Key] = Value.ToString(); } public void SetValue(string Key, bool Value) { Pairs[Key] = Value? "1" : "0"; } public void SetValue(string Key, string Value) { if(Value == null) { RemoveValue(Key); } else { Pairs[Key] = Value; } } public void SetValues(string Key, string[] Values) { if(Values == null || Values.Length == 0) { RemoveValue(Key); } else { Pairs[Key] = String.Join("\n", Values); } } public void SetValues(string Key, Guid[] Values) { if(Values == null) { RemoveValue(Key); } else { Pairs[Key] = String.Join("\n", Values.Select(x => x.ToString())); } } public void AppendValue(string Key, string Value) { string? CurrentValue; if(Pairs.TryGetValue(Key, out CurrentValue)) { Pairs[Key] = CurrentValue + "\n" + Value; } else { Pairs[Key] = Value; } } public void RemoveValue(string Key) { Pairs.Remove(Key); } public int GetValue(string Key, int DefaultValue) { string? ValueString = GetValue(Key); if(ValueString != null) { int Value; if(int.TryParse(ValueString, out Value)) { return Value; } } return DefaultValue; } public bool GetValue(string Key, bool DefaultValue) { return GetValue(Key, DefaultValue? 1 : 0) != 0; } [return: NotNullIfNotNull("DefaultValue")] public string? GetValue(string Key, string? DefaultValue = null) { string? Value; if(!Pairs.TryGetValue(Key, out Value)) { Value = DefaultValue; } return Value; } [return: NotNullIfNotNull("DefaultValue")] public string[]? GetValues(string Key, string[]? DefaultValue = null) { string? Value = GetValue(Key, null); if(Value == null) { return DefaultValue; } else { return Value.Split('\n'); } } [return: NotNullIfNotNull("DefaultValue")] public Guid[]? GetValues(string Key, Guid[]? DefaultValue = null) { string[]? StringValues = GetValues(Key, (string[]?)null); if(StringValues == null) { return DefaultValue; } List GuidValues = new List(); foreach(string StringValue in StringValues) { Guid GuidValue; if(Guid.TryParse(StringValue, out GuidValue)) { GuidValues.Add(GuidValue); } } return GuidValues.ToArray(); } } public class ConfigFile { List Sections = new List(); public ConfigFile() { } FileReference GetTempFileName(FileReference FileName) { return FileName + ".tmp"; } public void Load(FileReference FileName) { Parse(FileReference.ReadAllLines(FileName)); } public bool TryLoad(FileReference FileName, ILogger Logger) { FileInfo FileInfo = FileName.ToFileInfo(); if (FileInfo.Exists) { Logger.LogDebug("Loading config file from {File} ({Size} bytes)", FileInfo.FullName, FileInfo.Length); Load(FileName); return true; } FileReference TempFileName = GetTempFileName(FileName); FileInfo TempFileInfo = TempFileName.ToFileInfo(); if (TempFileInfo.Exists) { Logger.LogDebug("Loading temporary config file from {File} ({Size} bytes)", TempFileInfo.FullName, TempFileInfo.Length); Load(TempFileName); return true; } Logger.LogDebug("No existing config file at {File}", FileName); return false; } public void Parse(string[] Lines) { ConfigSection? CurrentSection = null; foreach(string Line in Lines) { string TrimLine = Line.Trim(); if(!TrimLine.StartsWith(";")) { if(TrimLine.StartsWith("[") && TrimLine.EndsWith("]")) { string SectionName = TrimLine.Substring(1, TrimLine.Length - 2).Trim(); CurrentSection = FindOrAddSection(SectionName); } else if(CurrentSection != null) { int EqualsIdx = TrimLine.IndexOf('='); if(EqualsIdx != -1) { string Value = Line.Substring(EqualsIdx + 1).TrimStart(); if(TrimLine.StartsWith("+")) { CurrentSection.AppendValue(TrimLine.Substring(1, EqualsIdx - 1).Trim(), Value); } else { CurrentSection.SetValue(TrimLine.Substring(0, EqualsIdx).TrimEnd(), Value); } } } } } } public void Save(FileReference FileName) { FileReference TempFileName = GetTempFileName(FileName); FileReference.Delete(TempFileName); using (StreamWriter Writer = new StreamWriter(TempFileName.FullName)) { for(int Idx = 0; Idx < Sections.Count; Idx++) { Writer.WriteLine("[{0}]", Sections[Idx].Name); foreach(KeyValuePair Pair in Sections[Idx].Pairs) { if(Pair.Value.Contains('\n')) { foreach(string Line in Pair.Value.Split('\n')) { Writer.WriteLine("+{0}={1}", Pair.Key, Line); } } else { Writer.WriteLine("{0}={1}", Pair.Key, Pair.Value); } } if(Idx < Sections.Count - 1) { Writer.WriteLine(); } } } FileReference.Delete(FileName); FileReference.Move(TempFileName, FileName); } public ConfigSection FindSection(string Name) { return Sections.FirstOrDefault(x => String.Compare(x.Name, Name, true) == 0); } public ConfigSection FindOrAddSection(string Name) { ConfigSection Section = FindSection(Name); if(Section == null) { Section = new ConfigSection(Name); Sections.Add(Section); } return Section; } public void SetValue(string Key, int Value) { int DotIdx = Key.IndexOf('.'); ConfigSection Section = FindOrAddSection(Key.Substring(0, DotIdx)); Section.SetValue(Key.Substring(DotIdx + 1), Value); } public void SetValue(string Key, bool Value) { int DotIdx = Key.IndexOf('.'); ConfigSection Section = FindOrAddSection(Key.Substring(0, DotIdx)); Section.SetValue(Key.Substring(DotIdx + 1), Value); } public void SetValue(string Key, string Value) { int DotIdx = Key.IndexOf('.'); ConfigSection Section = FindOrAddSection(Key.Substring(0, DotIdx)); Section.SetValue(Key.Substring(DotIdx + 1), Value); } public void SetValues(string Key, string[] Values) { int DotIdx = Key.IndexOf('.'); ConfigSection Section = FindOrAddSection(Key.Substring(0, DotIdx)); Section.SetValues(Key.Substring(DotIdx + 1), Values); } public bool GetValue(string Key, bool DefaultValue) { int DotIdx = Key.IndexOf('.'); ConfigSection Section = FindSection(Key.Substring(0, DotIdx)); return (Section == null)? DefaultValue : Section.GetValue(Key.Substring(DotIdx + 1), DefaultValue); } public int GetValue(string Key, int DefaultValue) { int DotIdx = Key.IndexOf('.'); ConfigSection Section = FindSection(Key.Substring(0, DotIdx)); return (Section == null)? DefaultValue : Section.GetValue(Key.Substring(DotIdx + 1), DefaultValue); } [return: NotNullIfNotNull("DefaultValue")] public string? GetValue(string Key, string? DefaultValue) { int DotIdx = Key.IndexOf('.'); ConfigSection Section = FindSection(Key.Substring(0, DotIdx)); return (Section == null)? DefaultValue : Section.GetValue(Key.Substring(DotIdx + 1), DefaultValue); } [return: NotNullIfNotNull("DefaultValue")] public string[]? GetValues(string Key, string[]? DefaultValue) { int DotIdx = Key.IndexOf('.'); ConfigSection Section = FindSection(Key.Substring(0, DotIdx)); return (Section == null)? DefaultValue : Section.GetValues(Key.Substring(DotIdx + 1), DefaultValue); } [return: NotNullIfNotNull("DefaultValue")] public Guid[]? GetGuidValues(string Key, Guid[]? DefaultValue) { int DotIdx = Key.IndexOf('.'); ConfigSection Section = FindSection(Key.Substring(0, DotIdx)); return (Section == null)? DefaultValue : Section.GetValues(Key.Substring(DotIdx + 1), DefaultValue); } } }