using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Text; namespace SabreTools.Text.INI { /// /// Key-value pair INI file /// public class IniFile : IDictionary { private readonly Dictionary _keyValuePairs = []; public string? this[string? key] { get { key = key?.ToLowerInvariant() ?? string.Empty; if (_keyValuePairs.ContainsKey(key)) return _keyValuePairs[key]; return null; } set { key = key?.ToLowerInvariant() ?? string.Empty; _keyValuePairs[key] = value; } } /// /// Create an empty INI file /// public IniFile() { } /// /// Populate an INI file from path /// /// /// Thrown if is not a valid file. /// public IniFile(string path) { // If we don't have a file, we can't read it if (!File.Exists(path)) throw new FileNotFoundException(nameof(path)); using var fileStream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); Parse(fileStream); } /// /// Populate an INI file from stream /// public IniFile(Stream stream) => Parse(stream); /// /// Add or update a key and value to the INI file /// public void AddOrUpdate(string key, string value) { this[key] = value; } /// /// Remove a key from the INI file /// public bool Remove(string key) { if (_keyValuePairs.ContainsKey(key)) { _keyValuePairs.Remove(key.ToLowerInvariant()); return true; } return false; } /// /// Write an INI file to a path /// public bool Write(string path) { // If we don't have a valid dictionary with values, we can't write out if (_keyValuePairs.Count == 0) return false; using var fileStream = File.OpenWrite(path); return Write(fileStream); } /// /// Write an INI file to a stream /// public bool Write(Stream stream) { // If we don't have a valid dictionary with values, we can't write out if (_keyValuePairs.Count == 0) return false; // If the stream is invalid, we can't output to it if (!stream.CanWrite) return false; try { using Writer writer = new(stream, Encoding.UTF8); // Order the keys to link sections together var orderedKeys = new string[_keyValuePairs.Keys.Count]; _keyValuePairs.Keys.CopyTo(orderedKeys, 0); Array.Sort(orderedKeys); string section = string.Empty; for (int i = 0; i < orderedKeys.Length; i++) { // Retrive the key and value string key = orderedKeys[i]; string? value = _keyValuePairs[key]; // We assume '.' is a section name separator if (key.Contains(".")) { // Split the key by '.' string[] data = key.Split('.'); // If the key contains an '.', we need to put them back in string newSection = data[0].Trim(); string[] keyArr = new string[data.Length - 1]; Array.Copy(data, 1, keyArr, 0, keyArr.Length); key = string.Join(".", keyArr).Trim(); // If we have a new section, write it out if (!string.Equals(newSection, section, StringComparison.OrdinalIgnoreCase)) { writer.WriteSection(newSection); section = newSection; } } // Now write out the key and value in a standardized way writer.WriteKeyValuePair(key, value); } } catch { // We don't care what the error was, just catch and return return false; } return true; } /// /// Read an INI file from a stream /// private bool Parse(Stream? stream) { // If the stream is invalid or unreadable, we can't process it if (stream is null || !stream.CanRead || stream.Position >= stream.Length - 1) return false; // Keys are case-insensitive by default try { // TODO: Can we use the section header in the reader? using var reader = new Reader(stream, Encoding.UTF8); string? section = string.Empty; while (!reader.EndOfStream) { // If we dont have a next line if (!reader.ReadNextLine()) break; // Process the row according to type switch (reader.RowType) { case RowType.SectionHeader: section = reader.Section; break; case RowType.KeyValue: string? key = reader.KeyValuePair?.Key; // Section names are prepended to the key with a '.' separating if (!string.IsNullOrEmpty(section)) key = $"{section}.{key}"; // Set or overwrite keys in the returned dictionary this[key] = reader.KeyValuePair?.Value; break; case RowType.None: case RowType.Comment: case RowType.Invalid: default: // No-op break; } } } catch { // We don't care what the error was, just catch and return return false; } return true; } #region IDictionary Impelementations public ICollection Keys => _keyValuePairs.Keys; public ICollection Values => _keyValuePairs.Values; public int Count => (_keyValuePairs as ICollection>)?.Count ?? 0; public bool IsReadOnly => false; public void Add(string key, string? value) => this[key] = value; bool IDictionary.Remove(string key) => Remove(key); public bool TryGetValue(string key, out string? value) { value = null; return _keyValuePairs?.TryGetValue(key.ToLowerInvariant(), out value) ?? false; } public void Add(KeyValuePair item) => this[item.Key] = item.Value; public void Clear() => _keyValuePairs?.Clear(); public bool Contains(KeyValuePair item) { var newItem = new KeyValuePair(item.Key.ToLowerInvariant(), item.Value); return (_keyValuePairs as ICollection>)?.Contains(newItem) ?? false; } public bool ContainsKey(string? key) => _keyValuePairs?.ContainsKey(key?.ToLowerInvariant() ?? string.Empty) ?? false; public void CopyTo(KeyValuePair[] array, int arrayIndex) { (_keyValuePairs as ICollection>)?.CopyTo(array, arrayIndex); } public bool Remove(KeyValuePair item) { var newItem = new KeyValuePair(item.Key.ToLowerInvariant(), item.Value); return (_keyValuePairs as ICollection>)?.Remove(newItem) ?? false; } public IEnumerator> GetEnumerator() { return (_keyValuePairs as IEnumerable>)!.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return (_keyValuePairs as IEnumerable)!.GetEnumerator(); } #endregion } }