See More

using BytecodeApi.Extensions; using System.Collections.ObjectModel; using System.Text; namespace BytecodeApi.IniParser; ///

/// Represents an INI file. /// public class IniFile { /// /// Gets the containing all properties prior to the first section declaration associated with this INI file. /// public IniSection GlobalProperties { get; } /// /// Gets the collection of INI sections associated with this INI file. /// public IniSectionCollection Sections { get; } /// /// Gets the collection of lines that could not be parsed. This collection popuplated, if is set to . /// public ReadOnlyCollection ErrorLines { get; private set; } /// /// Initializes a new instance of the class. /// public IniFile() { GlobalProperties = new(null); Sections = []; ErrorLines = new([]); } /// /// Creates an object from the specified file. /// /// A representing the path to an INI file. /// /// The this method creates. /// public static IniFile FromFile(string path) { return FromFile(path, Encoding.UTF8); } /// /// Creates an object from the specified file. /// /// A representing the path to an INI file. /// The encoding to use to read the file. /// /// The this method creates. /// public static IniFile FromFile(string path, Encoding? encoding) { return FromFile(path, encoding, null); } /// /// Creates an object from the specified file. /// /// A representing the path to an INI file. /// The encoding to use to read the file. /// A object with format specifications for INI parsing, or to use default parsing options. /// /// The this method creates. /// public static IniFile FromFile(string path, Encoding? encoding, IniFileParsingOptions? parsingOptions) { Check.ArgumentNull(path); Check.FileNotFound(path); using FileStream file = File.OpenRead(path); return FromStream(file, encoding, parsingOptions); } /// /// Creates an object from the specified [] that represents an INI file. /// /// The [] that represents an INI file to parse. /// /// The this method creates. /// public static IniFile FromBinary(byte[] file) { return FromBinary(file, Encoding.UTF8); } /// /// Creates an object from the specified [] that represents an INI file. /// /// The [] that represents an INI file to parse. /// The encoding to use to read the file. /// /// The this method creates. /// public static IniFile FromBinary(byte[] file, Encoding? encoding) { return FromBinary(file, encoding, null); } /// /// Creates an object from the specified [] that represents an INI file. /// /// The [] that represents an INI file to parse. /// The encoding to use to read the file. /// A object with format specifications for INI parsing, or to use default parsing options. /// /// The this method creates. /// public static IniFile FromBinary(byte[] file, Encoding? encoding, IniFileParsingOptions? parsingOptions) { Check.ArgumentNull(file); using MemoryStream memoryStream = new(file); return FromStream(memoryStream, encoding, parsingOptions); } /// /// Creates an object from the specified . /// /// The from which to parse the INI file from. /// /// The this method creates. /// public static IniFile FromStream(Stream stream) { return FromStream(stream, Encoding.UTF8); } /// /// Creates an object from the specified . /// /// The from which to parse the INI file from. /// The encoding to use to read the file. /// /// The this method creates. /// public static IniFile FromStream(Stream stream, Encoding? encoding) { return FromStream(stream, encoding, null); } /// /// Creates an object from the specified . /// /// The from which to parse the INI file from. /// The encoding to use to read the file. /// A object with format specifications for INI parsing, or to use default parsing options. /// /// The this method creates. /// public static IniFile FromStream(Stream stream, Encoding? encoding, IniFileParsingOptions? parsingOptions) { return FromStream(stream, encoding, parsingOptions, false); } /// /// Creates an object from the specified . /// /// The from which to parse the INI file from. /// The encoding to use to read the file. /// A object with format specifications for INI parsing, or to use default parsing options. /// A value indicating whether to leave open. /// /// The this method creates. /// public static IniFile FromStream(Stream stream, Encoding? encoding, IniFileParsingOptions? parsingOptions, bool leaveOpen) { Check.ArgumentNull(stream); Check.ArgumentNull(encoding); parsingOptions ??= new(); IniFile ini = new(); IniSection section = ini.GlobalProperties; bool ignoreSection = false; List errorLines = []; using StreamReader reader = new(stream, encoding, true, -1, leaveOpen); for (int lineNumber = 1; !reader.EndOfStream; lineNumber++) { string line = reader.ReadLine() ?? ""; string trimmedLine = line.Trim(); if (trimmedLine.StartsWith(';') && parsingOptions.AllowSemicolonComments || trimmedLine.StartsWith('#') && parsingOptions.AllowNumberSignComments) { } else if (trimmedLine == "") { AbortIf(!parsingOptions.AllowEmptyLines); } else if (trimmedLine.StartsWith('[') && trimmedLine.EndsWith(']')) { string newSection = trimmedLine[1..^1]; if (parsingOptions.TrimSectionNames) { newSection = newSection.Trim(); } AbortIf(!parsingOptions.AllowSectionNameClosingBracket && newSection.Contains(']')); IniSection? duplicate = ini.Sections.FirstOrDefault(sect => string.Equals(sect.Name, newSection, parsingOptions.DuplicateSectionNameIgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal)); bool create = false; switch (parsingOptions.DuplicateSectionNameBehavior) { case IniDuplicateSectionNameBehavior.Abort: AbortIf(duplicate != null); create = true; break; case IniDuplicateSectionNameBehavior.Ignore: if (duplicate == null) { create = true; } else { ignoreSection = true; } break; case IniDuplicateSectionNameBehavior.Merge: if (duplicate == null) { create = true; } else { section = duplicate; } break; case IniDuplicateSectionNameBehavior.Duplicate: create = true; break; default: throw Throw.InvalidEnumArgument(nameof(parsingOptions.DuplicatePropertyNameBehavior), parsingOptions.DuplicatePropertyNameBehavior); } if (create) { section = new(newSection); ini.Sections.Add(section); } } else if (parsingOptions.PropertyDelimiter == IniPropertyDelimiter.EqualSign && line.Contains('=')) { ParseProperty("="); } else if (parsingOptions.PropertyDelimiter == IniPropertyDelimiter.Colon && line.Contains(':')) { ParseProperty(":"); } else { Abort(); } void Abort() { if (parsingOptions.IgnoreErrors) { errorLines.Add(new(lineNumber, line)); } else { throw new IniParsingException(lineNumber, line); } } void AbortIf(bool condition) { if (condition) { Abort(); } } void ParseProperty(string delimiter) { if (!ignoreSection) { AbortIf(!parsingOptions.AllowGlobalProperties && section == ini.GlobalProperties); IniProperty property = new(line.SubstringUntil(delimiter), line.SubstringFrom(delimiter)); if (parsingOptions.TrimPropertyNames) { property.Name = property.Name.Trim(); } if (parsingOptions.TrimPropertyValues) { property.Value = property.Value.Trim(); } if (section.Properties.FirstOrDefault(prop => prop.Name.Equals(property.Name, parsingOptions.DuplicatePropertyNameIgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal)) is IniProperty duplicate) { switch (parsingOptions.DuplicatePropertyNameBehavior) { case IniDuplicatePropertyNameBehavior.Abort: Abort(); break; case IniDuplicatePropertyNameBehavior.Ignore: break; case IniDuplicatePropertyNameBehavior.Overwrite: duplicate.Value = property.Value; break; case IniDuplicatePropertyNameBehavior.Duplicate: section.Properties.Add(property); break; default: throw Throw.InvalidEnumArgument(nameof(parsingOptions.DuplicatePropertyNameBehavior), parsingOptions.DuplicatePropertyNameBehavior); } } else { section.Properties.Add(property); } } } } ini.ErrorLines = errorLines.AsReadOnly(); return ini; } /// /// Retrieves an with the specified name. /// /// A specifying the name of the . /// /// The with the specified name, or if no matching section was found. /// public IniSection? Section(string name) { return Section(name, false); } /// /// Retrieves an with the specified name. /// /// A specifying the name of the . /// to ignore character casing during name comparison. /// /// The with the specified name, or if no matching section was found. /// public IniSection? Section(string name, bool ignoreCase) { Check.ArgumentNull(name); return Sections.FirstOrDefault(section => string.Equals(section.Name, name, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal)); } /// /// Writes the contents of this INI file to a file. /// /// A specifying the path of a file to which this INI file is written to. public void Save(string path) { Save(path, Encoding.UTF8); } /// /// Writes the contents of this INI file to a file. /// /// A specifying the path of a file to which this INI file is written to. /// The encoding to use to write to the file. public void Save(string path, Encoding encoding) { Save(path, encoding, null); } /// /// Writes the contents of this INI file to a file. /// /// A specifying the path of a file to which this INI file is written to. /// The encoding to use to write to the file. /// An object specifying how to format the INI file. public void Save(string path, Encoding encoding, IniFileFormattingOptions? formattingOptions) { Check.ArgumentNull(path); Check.ArgumentNull(encoding); using FileStream file = File.Create(path); Save(file, encoding, formattingOptions, false); } /// /// Writes the contents of this INI file to a . /// /// The to which this INI file is written to. public void Save(Stream stream) { Save(stream, Encoding.UTF8); } /// /// Writes the contents of this INI file to a . /// /// The to which this INI file is written to. /// The encoding to use to write to the file. public void Save(Stream stream, Encoding encoding) { Save(stream, encoding, null); } /// /// Writes the contents of this INI file to a . /// /// The to which this INI file is written to. /// The encoding to use to write to the file. /// An object specifying how to format the INI file. public void Save(Stream stream, Encoding encoding, IniFileFormattingOptions? formattingOptions) { Save(stream, encoding, formattingOptions, false); } /// /// Writes the contents of this INI file to a . /// /// The to which this INI file is written to. /// The encoding to use to write to the file. /// An object specifying how to format the INI file. /// A value indicating whether to leave open. public void Save(Stream stream, Encoding encoding, IniFileFormattingOptions? formattingOptions, bool leaveOpen) { Check.ArgumentNull(stream); Check.ArgumentNull(encoding); formattingOptions ??= new(); string delimiter = (formattingOptions.UseDelimiterSpaceBefore ? " " : null) + formattingOptions.PropertyDelimiter.GetDescription() + (formattingOptions.UseDelimiterSpaceAfter ? " " : null); using StreamWriter streamWriter = new(stream, encoding, -1, leaveOpen); if (GlobalProperties.Properties.Count > 0) { WriteSection(GlobalProperties); if (formattingOptions.UseNewLineBetweenSections) { streamWriter.WriteLine(); } } for (int i = 0; i < Sections.Count; i++) { WriteSection(Sections[i]); if (i < Sections.Count - 1 && formattingOptions.UseNewLineBetweenSections) { streamWriter.WriteLine(); } } void WriteSection(IniSection section) { if (section.Name != null) { streamWriter.WriteLine($"[{section.Name}]"); } foreach (IniProperty property in section.Properties) { streamWriter.WriteLine(property.Name + delimiter + property.Value); } } } }