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);
}
}
}
}