diff --git a/extensions/Sisk.IniConfiguration/IniConfigurationPipeline.cs b/extensions/Sisk.IniConfiguration/IniConfigurationPipeline.cs
new file mode 100644
index 0000000..06ecd57
--- /dev/null
+++ b/extensions/Sisk.IniConfiguration/IniConfigurationPipeline.cs
@@ -0,0 +1,111 @@
+using Sisk.Core.Http;
+using Sisk.Core.Http.Hosting;
+using Sisk.IniConfiguration.Parser;
+using System.Text;
+
+namespace Sisk.IniConfiguration;
+
+///
+/// Provides an INI-Document based configuration-reader pipeline.
+///
+public sealed class IniConfigurationPipeline : IConfigurationReader
+{
+ ///
+ public void ReadConfiguration(ConfigurationContext context)
+ {
+ IniDocument document = IniDocument.FromFile(context.ConfigurationFile);
+
+ string parsingNode = "";
+ try
+ {
+ var serverSection = document.GetSection("Server");
+ if (serverSection is not null)
+ {
+ parsingNode = "Server.Listen";
+ string[] listeningPorts = serverSection.GetMany("Listen");
+ context.TargetListeningHost.Ports = listeningPorts.Select(n => ListeningPort.Parse(n, null)).ToArray();
+
+ parsingNode = "Server.Encoding";
+ if (serverSection.GetOne("Encoding") is { } encoding)
+ context.Host.ServerConfiguration.DefaultEncoding = Encoding.GetEncoding(encoding);
+
+ parsingNode = "Server.MaximumContentLength";
+ if (serverSection.GetOne("MaximumContentLength") is { } MaximumContentLength)
+ context.Host.ServerConfiguration.MaximumContentLength = Int32.Parse(MaximumContentLength);
+
+ parsingNode = "Server.IncludeRequestIdHeader";
+ if (serverSection.GetOne("IncludeRequestIdHeader") is { } IncludeRequestIdHeader)
+ context.Host.ServerConfiguration.IncludeRequestIdHeader = IniParser.IniNamingComparer.Compare(IncludeRequestIdHeader, "true") == 0;
+
+ parsingNode = "Server.ThrowExceptions";
+ if (serverSection.GetOne("ThrowExceptions") is { } ThrowExceptions)
+ context.Host.ServerConfiguration.ThrowExceptions = IniParser.IniNamingComparer.Compare(ThrowExceptions, "true") == 0;
+
+ parsingNode = "Server.AccessLogsStream";
+ if (serverSection.GetOne("AccessLogsStream") is { } AccessLogsStream)
+ context.Host.ServerConfiguration.AccessLogsStream = string.Compare(AccessLogsStream, "console", true) == 0 ?
+ LogStream.ConsoleOutput : new LogStream(AccessLogsStream);
+
+ parsingNode = "Server.ErrorsLogsStream";
+ if (serverSection.GetOne("ErrorsLogsStream") is { } ErrorsLogsStream)
+ context.Host.ServerConfiguration.ErrorsLogsStream = string.Compare(ErrorsLogsStream, "console", true) == 0 ?
+ LogStream.ConsoleOutput : new LogStream(ErrorsLogsStream);
+ }
+
+ var paramsSection = document.GetSection("Parameters");
+ if (paramsSection is not null)
+ {
+ foreach (string key in paramsSection.Keys)
+ {
+ string? value = paramsSection.GetOne(key);
+ if (value is not null)
+ context.Parameters.Add(key, value);
+ }
+ }
+
+ parsingNode = "Cors";
+ var corsSection = document.GetSection("CORS");
+ if (corsSection is not null)
+ {
+ parsingNode = "Cors.AllowMethods";
+ if (corsSection.GetOne("AllowMethods") is { } AllowMethods)
+ context.TargetListeningHost.CrossOriginResourceSharingPolicy.AllowMethods
+ = AllowMethods.Split(",", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
+
+ parsingNode = "Cors.AllowHeaders";
+ if (corsSection.GetOne("AllowHeaders") is { } AllowHeaders)
+ context.TargetListeningHost.CrossOriginResourceSharingPolicy.AllowHeaders
+ = AllowHeaders.Split(",", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
+
+ parsingNode = "Cors.AllowOrigins";
+ if (corsSection.GetOne("AllowOrigins") is { } AllowOrigins)
+ context.TargetListeningHost.CrossOriginResourceSharingPolicy.AllowOrigins
+ = AllowOrigins.Split(",", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
+
+ parsingNode = "Cors.AllowOrigin";
+ if (corsSection.GetOne("AllowOrigin") is { } AllowOrigin)
+ context.TargetListeningHost.CrossOriginResourceSharingPolicy.AllowOrigin
+ = AllowOrigin;
+
+ parsingNode = "Cors.ExposeHeaders";
+ if (corsSection.GetOne("ExposeHeaders") is { } ExposeHeaders)
+ context.TargetListeningHost.CrossOriginResourceSharingPolicy.ExposeHeaders
+ = ExposeHeaders.Split(",", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
+
+ parsingNode = "Cors.AllowCredentials";
+ if (corsSection.GetOne("AllowCredentials") is { } AllowCredentials)
+ context.TargetListeningHost.CrossOriginResourceSharingPolicy.AllowCredentials
+ = string.Compare(AllowCredentials, "True", true) == 0;
+
+ parsingNode = "Cors.MaxAge";
+ if (corsSection.GetOne("MaxAge") is { } MaxAge)
+ context.TargetListeningHost.CrossOriginResourceSharingPolicy.MaxAge
+ = TimeSpan.FromSeconds(int.Parse(MaxAge));
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Caught exception while trying to read the property {parsingNode}: {ex.Message}", ex);
+ }
+ }
+}
diff --git a/extensions/Sisk.IniConfiguration/IniDocument.cs b/extensions/Sisk.IniConfiguration/IniDocument.cs
new file mode 100644
index 0000000..bc8aef4
--- /dev/null
+++ b/extensions/Sisk.IniConfiguration/IniDocument.cs
@@ -0,0 +1,90 @@
+using Sisk.IniConfiguration.Parser;
+using System.Text;
+
+namespace Sisk.IniConfiguration;
+
+///
+/// Represents an INI configuration document.
+///
+public sealed class IniDocument
+{
+ ///
+ /// Gets all INI sections defined in this INI document.
+ ///
+ public IniSection[] Sections { get; }
+
+ ///
+ /// Gets the global INI section, which is the primary section in the document.
+ ///
+ public IniSection Global { get => Sections[0]; }
+
+ ///
+ /// Creates an new document from the specified
+ /// string, reading it as an UTF-8 string.
+ ///
+ /// The UTF-8 string.
+ public static IniDocument FromString(string iniConfiguration)
+ {
+ using TextReader reader = new StringReader(iniConfiguration);
+ using IniParser parser = new IniParser(reader);
+ return parser.Parse();
+ }
+
+ ///
+ /// Creates an new document from the specified
+ /// file using the specified encoding.
+ ///
+ /// The absolute or relative file path to the INI document.
+ /// Optional. The encoding used to read the file. Defaults to UTF-8.
+ public static IniDocument FromFile(string filePath, Encoding? encoding = null)
+ {
+ using TextReader reader = new StreamReader(filePath, encoding ?? Encoding.UTF8);
+ using IniParser parser = new IniParser(reader);
+ return parser.Parse();
+ }
+
+ ///
+ /// Creates an new document from the specified
+ /// stream using the specified encoding.
+ ///
+ /// The input stream where the INI document is.
+ /// Optional. The encoding used to read the stream. Defaults to UTF-8.
+ public static IniDocument FromStream(Stream stream, Encoding? encoding = null)
+ {
+ using TextReader reader = new StreamReader(stream, encoding ?? Encoding.UTF8);
+ using IniParser parser = new IniParser(reader);
+ return parser.Parse();
+ }
+
+ ///
+ /// Creates an new document from the specified
+ /// .
+ ///
+ /// The instance.
+ public static IniDocument FromStream(TextReader reader)
+ {
+ using IniParser parser = new IniParser(reader);
+ return parser.Parse();
+ }
+
+ internal IniDocument(IniSection[] sections)
+ {
+ Sections = sections;
+ }
+
+ ///
+ /// Gets an defined INI section from this document. The search is case-insensitive.
+ ///
+ /// The section name.
+ /// The object if found, or null if not defined.
+ public IniSection? GetSection(string sectionName)
+ {
+ for (int i = 0; i < Sections.Length; i++)
+ {
+ IniSection section = Sections[i];
+ if (IniParser.IniNamingComparer.Compare(section.Name, sectionName) == 0)
+ return section;
+ }
+ return null;
+ }
+}
diff --git a/extensions/Sisk.IniConfiguration/IniSection.cs b/extensions/Sisk.IniConfiguration/IniSection.cs
new file mode 100644
index 0000000..a4069d5
--- /dev/null
+++ b/extensions/Sisk.IniConfiguration/IniSection.cs
@@ -0,0 +1,141 @@
+using Sisk.IniConfiguration.Parser;
+using System.Collections;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Sisk.IniConfiguration;
+
+///
+/// Represents an INI section, which contains it's own properties.
+///
+public sealed class IniSection : IReadOnlyDictionary
+{
+ internal (string, string)[] items;
+
+ ///
+ /// Gets the INI section name.
+ ///
+ public string Name { get; }
+
+ internal IniSection(string name, (string, string)[] items)
+ {
+ this.items = items;
+ Name = name;
+ }
+
+ ///
+ /// Gets all values associated with the specified property name, performing an case-insensitive search.
+ ///
+ /// The property name.
+ public string[] this[string key]
+ {
+ get
+ {
+ return items
+ .Where(k => IniParser.IniNamingComparer.Compare(key, k.Item1) == 0)
+ .Select(k => k.Item2)
+ .ToArray();
+ }
+ }
+
+ ///
+ /// Gets all keys defined in this INI section, without duplicates.
+ ///
+ public IEnumerable Keys
+ {
+ get
+ {
+ return items.Select(i => i.Item1).Distinct().ToArray();
+ }
+ }
+
+ ///
+ /// Gets all values defined in this INI section.
+ ///
+ public IEnumerable Values
+ {
+ get
+ {
+ using (var e = GetEnumerator())
+ {
+ while (e.MoveNext())
+ {
+ yield return e.Current.Value;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Gets the number of properties in this INI section.
+ ///
+ public int Count => items.Length;
+
+ ///
+ /// Gets the last value defined in this INI section by their property name.
+ ///
+ /// The property name.
+ /// The last value associated with the specified property name, or null if nothing is found.
+ public string? GetOne(string key)
+ {
+ return items
+ .Where(k => IniParser.IniNamingComparer.Compare(key, k.Item1) == 0)
+ .Select(k => k.Item2)
+ .LastOrDefault();
+ }
+
+ ///
+ /// Gets all values defined in this INI section by their property name.
+ ///
+ /// The property name.
+ /// All values associated with the specified property name.
+ public string[] GetMany(string key)
+ {
+ return this[key];
+ }
+
+ ///
+ /// Gets an boolean indicating if the specified key/property name is
+ /// defined in this .
+ ///
+ /// The property name.
+ /// An indicating if the specified property name is defined or not.
+ public bool ContainsKey(string key)
+ {
+ for (int i = 0; i < items.Length; i++)
+ {
+ (string, string?) item = items[i];
+
+ if (IniParser.IniNamingComparer.Compare(item.Item1, key) == 0)
+ return true;
+ }
+ return false;
+ }
+
+ ///
+ public IEnumerator> GetEnumerator()
+ {
+ string[] keysDistinct = items.Select(i => i.Item1).Distinct().ToArray();
+
+ foreach (string key in keysDistinct)
+ {
+ string[] valuesByKey = items
+ .Where(i => i.Item1 == key)
+ .Select(i => i.Item2)
+ .ToArray();
+
+ yield return new KeyValuePair(key, valuesByKey);
+ }
+ }
+
+ ///
+ public bool TryGetValue(string key, [MaybeNullWhen(false)] out string[] value)
+ {
+ value = this[key];
+ return value.Length > 0;
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+}
diff --git a/extensions/Sisk.IniConfiguration/Parser/IniParser.cs b/extensions/Sisk.IniConfiguration/Parser/IniParser.cs
new file mode 100644
index 0000000..6ccc934
--- /dev/null
+++ b/extensions/Sisk.IniConfiguration/Parser/IniParser.cs
@@ -0,0 +1,212 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Sisk.IniConfiguration.Parser;
+
+///
+/// Provides an INI-document parser.
+///
+public sealed class IniParser : IDisposable
+{
+ internal static readonly StringComparer IniNamingComparer = StringComparer.InvariantCultureIgnoreCase;
+
+ private TextReader reader;
+ private bool disposedValue;
+
+ ///
+ /// Creates an new with the specified text reader.
+ ///
+ /// The instace to read the INI document.
+ public IniParser(TextReader reader)
+ {
+ this.reader = reader;
+ }
+
+ const char SECTION_START = '[';
+ const char SECTION_END = ']';
+ const char COMMENT_1 = '#';
+ const char COMMENT_2 = ';';
+ const char STRING_QUOTE_1 = '\'';
+ const char STRING_QUOTE_2 = '\"';
+ const char PROPERTY_DELIMITER = '=';
+ const char NEW_LINE = '\n';
+
+ ///
+ /// Reads the INI document from the input stream.
+ ///
+ /// An file containing all properties and data from the input stream.
+ public IniDocument Parse()
+ {
+ ThrowIfDisposed();
+
+ string lastSectionName = "__GLOBAL__";
+ List<(string, string)> items = new List<(string, string)>();
+ List creatingSections = new List();
+
+ int read = 0;
+ while ((read = reader.Peek()) >= 0)
+ {
+ char c = (char)read;
+
+ if (c == SECTION_START)
+ {
+ reader.Read();
+ string? sectionName = ReadUntil(new char[] { SECTION_END })?.Trim();
+
+ if (sectionName is null)
+ break;
+
+ var closingSection = new IniSection(lastSectionName, items.ToArray());
+ creatingSections.Add(closingSection);
+
+ items.Clear();
+ lastSectionName = sectionName;
+
+ SkipWhiteSpace();
+ }
+ else if (c == COMMENT_1 || c == COMMENT_2)
+ {
+ SkipUntilNewLine();
+ }
+ else
+ {
+ string? propertyName = ReadUntil(new char[] { PROPERTY_DELIMITER }, true);
+ if (propertyName is null)
+ break;
+
+ string? propertyValue = ReadValue();
+ if (propertyValue is null)
+ break;
+
+ if ((string.IsNullOrWhiteSpace(propertyName) && string.IsNullOrWhiteSpace(propertyValue)) == false)
+ items.Add((propertyName.Trim(), propertyValue));
+
+ SkipWhiteSpace();
+ }
+ }
+
+ if (items.Count > 0)
+ {
+ var closingSection = new IniSection(lastSectionName, items.ToArray());
+ creatingSections.Add(closingSection);
+ }
+
+ return new IniDocument(creatingSections.ToArray());
+ }
+
+ void SkipUntilNewLine()
+ {
+ ReadUntil(new char[] { NEW_LINE }, false);
+ }
+
+ void SkipWhiteSpace()
+ {
+ int read = 0;
+ while ((read = reader.Peek()) >= 0)
+ {
+ char c = (char)read;
+ if (!char.IsWhiteSpace(c))
+ {
+ break;
+ }
+ else reader.Read();
+ }
+ }
+
+ string? ReadValue()
+ {
+ readNext:
+ int read = reader.Read();
+ if (read < 0)
+ {
+ return "";
+ }
+ else
+ {
+ char c = (char)read;
+
+ if (c == ' ' || c == '\t')
+ {
+ goto readNext;
+ }
+ else if (c == '\r' || c == '\n')
+ {
+ return "";
+ }
+ if (c == STRING_QUOTE_1)
+ {
+ return ReadUntil(new char[] { STRING_QUOTE_1 }, false);
+ }
+ else if (c == STRING_QUOTE_2)
+ {
+ return ReadUntil(new char[] { STRING_QUOTE_2 }, false);
+ }
+ else
+ {
+ return (c + ReadUntil(new char[] { NEW_LINE }, true, true)).Trim();
+ }
+ }
+ }
+
+ string? ReadUntil(in char[] until, bool canExplode = false, bool breakOnComment = false)
+ {
+ StringBuilder sb = new StringBuilder();
+ int read = 0;
+ while ((read = reader.Read()) >= 0)
+ {
+ char c = (char)read;
+
+ if (until.Contains(c))
+ {
+ return sb.ToString();
+ }
+ else if (breakOnComment && (c == COMMENT_1 || c == COMMENT_2))
+ {
+ var s = sb.ToString();
+ SkipUntilNewLine();
+ return s;
+ }
+ else
+ {
+ sb.Append(c);
+ }
+ }
+
+ if (canExplode)
+ {
+ return sb.ToString();
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ void ThrowIfDisposed()
+ {
+ if (disposedValue)
+ throw new ObjectDisposedException(nameof(IniParser));
+ }
+
+ void Dispose(bool disposing)
+ {
+ if (!disposedValue)
+ {
+ if (disposing)
+ {
+ reader.Dispose();
+ }
+
+ disposedValue = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/extensions/Sisk.IniConfiguration/Sisk.IniConfiguration.csproj b/extensions/Sisk.IniConfiguration/Sisk.IniConfiguration.csproj
new file mode 100644
index 0000000..c6e9b8a
--- /dev/null
+++ b/extensions/Sisk.IniConfiguration/Sisk.IniConfiguration.csproj
@@ -0,0 +1,59 @@
+
+
+
+ net6.0
+ enable
+ enable
+ True
+ True
+
+ Sisk.IniConfiguration
+ Sisk.IniConfiguration
+
+ CypherPotato
+ Project Principium
+ Sisk.IniConfiguration
+ This package provides an INI configuration parser for the Sisk Framework and other .NET projects.
+ https://sisk.proj.pw/
+ Icon.png
+ README.md
+ https://github.com/sisk-http/core
+ http-server,http,web framework
+ git
+
+ 1.0.0.0
+ 1.0.0.0
+ 1.0.0.0
+
+ en
+ LICENSE.txt
+ True
+
+
+
+ true
+
+
+ $(NoWarn);SYSLIB0020
+
+
+
+
+ True
+ \
+
+
+ True
+ \
+
+
+ True
+ \
+
+
+
+
+
+
+
+
\ No newline at end of file