// Copyright Epic Games, Inc. All Rights Reserved. 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 EpicGames.Core; using Microsoft.Extensions.Logging; namespace UnrealGameSync { [JsonConverter(typeof(ConfigObjectJsonConverter))] public class ConfigObject { const string ConfigSeparatorCharacters = "(),= \t\""; public List> Pairs { get; } 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.Contains(text[idx], StringComparison.Ordinal)) { return text[idx++].ToString(); } else { int startIdx = idx; while (idx < text.Length && !ConfigSeparatorCharacters.Contains(text[idx], StringComparison.Ordinal)) { 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; } public IEnumerable GetKeys() { for (int idx = 0; idx < Pairs.Count; idx++) { yield return Pairs[idx].Key; } } [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.OrdinalIgnoreCase)) { 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 (Int32.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 (Boolean.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 => 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 obj, ConfigObject? defaultObject) { foreach (KeyValuePair pair in obj.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("\\", "\\\\", StringComparison.Ordinal).Replace("\"", "\\\"", StringComparison.Ordinal)); } } } 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 obj = new ConfigObject(); if (reader.TokenType == JsonTokenType.StartObject) { while (reader.Read() && reader.TokenType == JsonTokenType.PropertyName) { string name = reader.GetString()!; reader.Read(); string value = reader.GetString()!; obj.Pairs.Add(KeyValuePair.Create(name, value)); } } else { reader.Skip(); } return obj; } 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 { get; set; } public Dictionary Pairs { get; } = 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, long 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) => GetOptionalIntValue(key, defaultValue) ?? defaultValue; public int? GetOptionalIntValue(string key, int? defaultValue) { string? valueString = GetValue(key); if (valueString != null) { int value; if (Int32.TryParse(valueString, out value)) { return value; } } return defaultValue; } public long GetValue(string key, long defaultValue) { string? valueString = GetValue(key); if (valueString != null) { long value; if (Int64.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; } public TEnum GetEnumValue(string key, TEnum defaultValue) where TEnum : struct { string? str = GetValue(key, null); if (str != null && Enum.TryParse(str, true, out TEnum value)) { return value; } return defaultValue; } [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 { readonly List _sections = new List(); public ConfigFile() { } static 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(";", StringComparison.Ordinal)) { if (trimLine.StartsWith("[", StringComparison.Ordinal) && trimLine.EndsWith("]", StringComparison.Ordinal)) { string sectionName = trimLine.Substring(1, trimLine.Length - 2).Trim(); currentSection = FindOrAddSection(sectionName); } else if (currentSection != null) { int equalsIdx = trimLine.IndexOf('=', StringComparison.Ordinal); if (equalsIdx != -1) { string value = line.Substring(equalsIdx + 1).TrimStart(); if (trimLine.StartsWith("+", StringComparison.Ordinal)) { 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', StringComparison.Ordinal)) { 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.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); } public ConfigSection FindOrAddSection(string name) { ConfigSection? section = FindSection(name); if (section == null) { section = new ConfigSection(name); _sections.Add(section); } return section; } public void RemoveSection(string name) { ConfigSection? section = FindSection(name); if (section != null) { _sections.Remove(section); } } public void SetValue(string key, int value) { int dotIdx = key.IndexOf('.', StringComparison.Ordinal); 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('.', StringComparison.Ordinal); 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('.', StringComparison.Ordinal); 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('.', StringComparison.Ordinal); 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('.', StringComparison.Ordinal); 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('.', StringComparison.Ordinal); ConfigSection? section = FindSection(key.Substring(0, dotIdx)); return (section == null) ? defaultValue : section.GetValue(key.Substring(dotIdx + 1), defaultValue); } public int? GetOptionalIntValue(string key, int? defaultValue) { int dotIdx = key.IndexOf('.', StringComparison.Ordinal); ConfigSection? section = FindSection(key.Substring(0, dotIdx)); return (section == null) ? defaultValue : section.GetOptionalIntValue(key.Substring(dotIdx + 1), defaultValue); } [return: NotNullIfNotNull("defaultValue")] public string? GetValue(string key, string? defaultValue) { int dotIdx = key.IndexOf('.', StringComparison.Ordinal); ConfigSection? section = FindSection(key.Substring(0, dotIdx)); return (section == null) ? defaultValue : section.GetValue(key.Substring(dotIdx + 1), defaultValue); } public TEnum GetEnumValue(string key, TEnum defaultValue) where TEnum : struct { int dotIdx = key.IndexOf('.', StringComparison.Ordinal); ConfigSection? section = FindSection(key.Substring(0, dotIdx)); return (section == null) ? defaultValue : section.GetEnumValue(key.Substring(dotIdx + 1), defaultValue); } [return: NotNullIfNotNull("defaultValue")] public string[]? GetValues(string key, string[]? defaultValue) { int dotIdx = key.IndexOf('.', StringComparison.Ordinal); 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('.', StringComparison.Ordinal); ConfigSection? section = FindSection(key.Substring(0, dotIdx)); return (section == null) ? defaultValue : section.GetValues(key.Substring(dotIdx + 1), defaultValue); } } }