You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

3365 lines
113 KiB
C#

// <copyright file="SharpConfig.cs" company="jindongfang">
// Copyright (c) jindongfang. All rights reserved.
// </copyright>
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Text;
/* Configuration.cs */
namespace SharpConfig
{
/// <summary>
/// Represents a configuration.
/// Configurations contain one or multiple sections
/// that in turn can contain one or multiple settings.
/// The <see cref="Configuration"/> class is designed
/// to work with classic configuration formats such as
/// .ini and .cfg, but is not limited to these.
/// </summary>
public partial class Configuration : IEnumerable<Section>
{
#region Fields
private static CultureInfo mCultureInfo;
private static char mPreferredCommentChar;
private static char mArrayElementSeparator;
private static Dictionary<Type, ITypeStringConverter> mTypeStringConverters;
internal readonly List<Section> mSections;
#endregion
#region Construction
static Configuration()
{
// For now, clone the invariant culture so that the
// deprecated DateTimeFormat/NumberFormat properties still work,
// but without modifying the real invariant culture instance.
mCultureInfo = (CultureInfo)CultureInfo.InvariantCulture.Clone();
ValidCommentChars = new[] { '#', ';' };
mPreferredCommentChar = '#';
mArrayElementSeparator = ',';
FallbackConverter = new FallbackStringConverter();
// Add all stock converters.
mTypeStringConverters = new Dictionary<Type, ITypeStringConverter>()
{
{ typeof(bool), new BoolStringConverter() },
{ typeof(byte), new ByteStringConverter() },
{ typeof(char), new CharStringConverter() },
{ typeof(DateTime), new DateTimeStringConverter() },
{ typeof(decimal), new DecimalStringConverter() },
{ typeof(double), new DoubleStringConverter() },
{ typeof(Enum), new EnumStringConverter() },
{ typeof(short), new Int16StringConverter() },
{ typeof(int), new Int32StringConverter() },
{ typeof(long), new Int64StringConverter() },
{ typeof(sbyte), new SByteStringConverter() },
{ typeof(float), new SingleStringConverter() },
{ typeof(string), new StringStringConverter() },
{ typeof(ushort), new UInt16StringConverter() },
{ typeof(uint), new UInt32StringConverter() },
{ typeof(ulong), new UInt64StringConverter() },
};
IgnoreInlineComments = false;
IgnorePreComments = false;
SpaceBetweenEquals = false;
OutputRawStringValues = false;
}
/// <summary>
/// Initializes a new instance of the <see cref="Configuration"/> class.
/// </summary>
public Configuration()
{
this.mSections = new List<Section>();
}
#endregion
#region Public Methods
/// <summary>
/// Gets an enumerator that iterates through the configuration.
/// </summary>
/// <returns>enumerator</returns>
public IEnumerator<Section> GetEnumerator()
=> this.mSections.GetEnumerator();
/// <summary>
/// Gets an enumerator that iterates through the configuration.
/// </summary>
/// <returns>enumerator</returns>
IEnumerator IEnumerable.GetEnumerator()
=> this.GetEnumerator();
/// <summary>
/// Adds a section to the configuration.
/// </summary>
/// <param name="section">The section to add.</param>
/// <exception cref="ArgumentNullException">When <paramref name="section"/> is null.</exception>
/// <exception cref="ArgumentException">When the section already exists in the configuration.</exception>
public void Add(Section section)
{
if (section == null)
{
throw new ArgumentNullException("section");
}
if (this.Contains(section))
{
throw new ArgumentException("The specified section already exists in the configuration.");
}
this.mSections.Add(section);
}
/// <summary>
/// Adds a section with a specific name to the configuration.
/// </summary>
/// <param name="sectionName">The name of the section to add.</param>
/// <returns>The added section.</returns>
/// <exception cref="ArgumentNullException">When <paramref name="sectionName"/> is null or empty.</exception>
public Section Add(string sectionName)
{
var section = new Section(sectionName);
this.Add(section);
return section;
}
/// <summary>
/// Removes a section from the configuration by its name.
/// If there are multiple sections with the same name, only the first section is removed.
/// To remove all sections that have the name name, use the RemoveAllNamed() method instead.
/// </summary>
/// <param name="sectionName">The case-sensitive name of the section to remove.</param>
/// <returns>True if a section with the specified name was removed; false otherwise.</returns>
///
/// <exception cref="ArgumentNullException">When <paramref name="sectionName"/> is null or empty.</exception>
public bool Remove(string sectionName)
{
if (string.IsNullOrEmpty(sectionName))
{
throw new ArgumentNullException("sectionName");
}
return this.Remove(this.FindSection(sectionName));
}
/// <summary>
/// Removes a section from the configuration.
/// </summary>
/// <param name="section">The section to remove.</param>
/// <returns>True if the section was removed; false otherwise.</returns>
public bool Remove(Section section)
=> this.mSections.Remove(section);
/// <summary>
/// Removes all sections that have a specific name.
/// </summary>
/// <param name="sectionName">The case-sensitive name of the sections to remove.</param>
///
/// <exception cref="ArgumentNullException">When <paramref name="sectionName"/> is null or empty.</exception>
public void RemoveAllNamed(string sectionName)
{
if (string.IsNullOrEmpty(sectionName))
{
throw new ArgumentNullException("sectionName");
}
while (this.Remove(sectionName))
{
;
}
}
/// <summary>
/// Clears the configuration of all sections.
/// </summary>
public void Clear()
=> this.mSections.Clear();
/// <summary>
/// Determines whether a specified section is contained in the configuration.
/// </summary>
/// <param name="section">The section to check for containment.</param>
/// <returns>True if the section is contained in the configuration; false otherwise.</returns>
public bool Contains(Section section)
=> this.mSections.Contains(section);
/// <summary>
/// Determines whether a specifically named section is contained in the configuration.
/// </summary>
/// <param name="sectionName">The name of the section.</param>
/// <returns>True if the section is contained in the configuration; false otherwise.</returns>
///
/// <exception cref="ArgumentNullException">When <paramref name="sectionName"/> is null or empty.</exception>
public bool Contains(string sectionName)
{
if (string.IsNullOrEmpty(sectionName))
{
throw new ArgumentNullException("sectionName");
}
return this.FindSection(sectionName) != null;
}
/// <summary>
/// Determines whether a specifically named section is contained in the configuration,
/// and whether that section in turn contains a specifically named setting.
/// </summary>
/// <param name="sectionName">The name of the section.</param>
/// <param name="settingName">The name of the setting.</param>
/// <returns>True if the section and the respective setting was found; false otherwise.</returns>
///
/// <exception cref="ArgumentNullException">When <paramref name="sectionName"/> or <paramref name="settingName"/> is null or empty.</exception>
public bool Contains(string sectionName, string settingName)
{
if (string.IsNullOrEmpty(sectionName))
{
throw new ArgumentNullException("sectionName");
}
if (string.IsNullOrEmpty(settingName))
{
throw new ArgumentNullException("settingName");
}
Section section = this.FindSection(sectionName);
return section != null && section.Contains(settingName);
}
/// <summary>
/// Gets the string representation of the configuration. It represents the same contents
/// as if the configuration was saved to a file or stream.
/// </summary>
public string StringRepresentation
{
get
{
var sb = new StringBuilder();
// Write all sections.
bool isFirstSection = true;
void WriteSection(Section section)
{
if (!isFirstSection)
{
sb.AppendLine();
}
// Leave some space between this section and the element that is above,
// if this section has pre-comments and isn't the first section in the configuration.
if (!isFirstSection && section.PreComment != null)
{
sb.AppendLine();
}
if (section.Name != Section.DefaultSectionName)
{
sb.AppendLine(section.ToString());
}
// Write all settings.
foreach (var setting in section)
{
sb.AppendLine(setting.ToString());
}
if (section.Name != Section.DefaultSectionName || section.SettingCount > 0)
{
isFirstSection = false;
}
}
// Write the default section first.
var defaultSection = this.DefaultSection;
if (defaultSection.SettingCount > 0)
{
WriteSection(this.DefaultSection);
}
// Now the rest.
foreach (var section in this.mSections)
{
if (section != defaultSection)
{
WriteSection(section);
}
}
return sb.ToString();
}
}
/// <summary>
/// Registers a type converter to be used for setting value conversions.
/// </summary>
/// <param name="converter">The converter to register.</param>
///
/// <exception cref="ArgumentNullException">When <paramref name="converter"/> is null.</exception>
/// <exception cref="InvalidOperationException">When a converter for the converter's type is already registered.</exception>
public static void RegisterTypeStringConverter(ITypeStringConverter converter)
{
if (converter == null)
{
throw new ArgumentNullException("converter");
}
var type = converter.ConvertibleType;
if (mTypeStringConverters.ContainsKey(type))
{
throw new InvalidOperationException($"A converter for type '{type.FullName}' is already registered.");
}
mTypeStringConverters.Add(type, converter);
}
/// <summary>
/// Deregisters a type converter from setting value conversion.
/// </summary>
/// <param name="type">The type whose associated converter to deregister.</param>
///
/// <exception cref="ArgumentNullException">When <paramref name="type"/> is null.</exception>
/// <exception cref="InvalidOperationException">When no converter is registered for <paramref name="type"/>.</exception>
public static void DeregisterTypeStringConverter(Type type)
{
if (type == null)
{
throw new ArgumentNullException("type");
}
if (!mTypeStringConverters.ContainsKey(type))
{
throw new InvalidOperationException($"No converter is registered for type '{type.FullName}'.");
}
mTypeStringConverters.Remove(type);
}
internal static ITypeStringConverter FindTypeStringConverter(Type type)
{
if (type.IsEnum)
{
type = typeof(Enum);
}
if (!mTypeStringConverters.TryGetValue(type, out ITypeStringConverter converter))
{
converter = FallbackConverter;
}
return converter;
}
internal static ITypeStringConverter FallbackConverter { get; private set; }
#endregion
#region Load
/// <summary>
/// Loads a configuration from a file auto-detecting the encoding and
/// using the default parsing settings.
/// </summary>
///
/// <param name="filename">The location of the configuration file.</param>
///
/// <returns>
/// The loaded <see cref="Configuration"/> object.
/// </returns>
public static Configuration LoadFromFile(string filename)
{
return LoadFromFile(filename, null);
}
/// <summary>
/// Loads a configuration from a file.
/// </summary>
///
/// <param name="filename">The location of the configuration file.</param>
/// <param name="encoding">The encoding applied to the contents of the file. Specify null to auto-detect the encoding.</param>
///
/// <returns>
/// The loaded <see cref="Configuration"/> object.
/// </returns>
///
/// <exception cref="ArgumentNullException">When <paramref name="filename"/> is null or empty.</exception>
/// <exception cref="FileNotFoundException">When the specified configuration file is not found.</exception>
public static Configuration LoadFromFile(string filename, Encoding encoding)
{
if (string.IsNullOrEmpty(filename))
{
throw new ArgumentNullException("filename");
}
if (!File.Exists(filename))
{
throw new FileNotFoundException("Configuration file not found.", filename);
}
return (encoding == null) ?
LoadFromString(File.ReadAllText(filename)) :
LoadFromString(File.ReadAllText(filename, encoding));
}
/// <summary>
/// Loads a configuration from a text stream auto-detecting the encoding and
/// using the default parsing settings.
/// </summary>
///
/// <param name="stream">The text stream to load the configuration from.</param>
///
/// <returns>
/// The loaded <see cref="Configuration"/> object.
/// </returns>
public static Configuration LoadFromStream(Stream stream)
{
return LoadFromStream(stream, null);
}
/// <summary>
/// Loads a configuration from a text stream.
/// </summary>
///
/// <param name="stream"> The text stream to load the configuration from.</param>
/// <param name="encoding"> The encoding applied to the contents of the stream. Specify null to auto-detect the encoding.</param>
///
/// <returns>
/// The loaded <see cref="Configuration"/> object.
/// </returns>
///
/// <exception cref="ArgumentNullException">When <paramref name="stream"/> is null.</exception>
public static Configuration LoadFromStream(Stream stream, Encoding encoding)
{
if (stream == null)
{
throw new ArgumentNullException("stream");
}
string source = null;
var reader = (encoding == null) ?
new StreamReader(stream) :
new StreamReader(stream, encoding);
using (reader)
{
source = reader.ReadToEnd();
}
return LoadFromString(source);
}
/// <summary>
/// Loads a configuration from text (source code).
/// </summary>
///
/// <param name="source">The text (source code) of the configuration.</param>
///
/// <returns>
/// The loaded <see cref="Configuration"/> object.
/// </returns>
///
/// <exception cref="ArgumentNullException">When <paramref name="source"/> is null.</exception>
public static Configuration LoadFromString(string source)
{
if (source == null)
{
throw new ArgumentNullException("source");
}
return ConfigurationReader.ReadFromString(source);
}
#endregion
#region LoadBinary
/// <summary>
/// Loads a configuration from a binary file using the <b>default</b> <see cref="BinaryReader"/>.
/// </summary>
///
/// <param name="filename">The location of the configuration file.</param>
///
/// <returns>
/// The loaded configuration.
/// </returns>
///
/// <exception cref="ArgumentNullException">When <paramref name="filename"/> is null or empty.</exception>
public static Configuration LoadFromBinaryFile(string filename)
{
return LoadFromBinaryFile(filename, null);
}
/// <summary>
/// Loads a configuration from a binary file using a specific <see cref="BinaryReader"/>.
/// </summary>
///
/// <param name="filename">The location of the configuration file.</param>
/// <param name="reader"> The reader to use. Specify null to use the default <see cref="BinaryReader"/>.</param>
///
/// <returns>
/// The loaded configuration.
/// </returns>
///
/// <exception cref="ArgumentNullException">When <paramref name="filename"/> is null or empty.</exception>
public static Configuration LoadFromBinaryFile(string filename, BinaryReader reader)
{
if (string.IsNullOrEmpty(filename))
{
throw new ArgumentNullException("filename");
}
using (var stream = File.OpenRead(filename))
{
return LoadFromBinaryStream(stream, reader);
}
}
/// <summary>
/// Loads a configuration from a binary stream, using the <b>default</b> <see cref="BinaryReader"/>.
/// </summary>
///
/// <param name="stream">The stream to load the configuration from.</param>
///
/// <returns>
/// The loaded configuration.
/// </returns>
///
/// <exception cref="ArgumentNullException">When <paramref name="stream"/> is null.</exception>
public static Configuration LoadFromBinaryStream(Stream stream)
{
return LoadFromBinaryStream(stream, null);
}
/// <summary>
/// Loads a configuration from a binary stream, using a specific <see cref="BinaryReader"/>.
/// </summary>
///
/// <param name="stream">The stream to load the configuration from.</param>
/// <param name="reader">The reader to use. Specify null to use the default <see cref="BinaryReader"/>.</param>
///
/// <returns>
/// The loaded configuration.
/// </returns>
///
/// <exception cref="ArgumentNullException">When <paramref name="stream"/> is null.</exception>
public static Configuration LoadFromBinaryStream(Stream stream, BinaryReader reader)
{
if (stream == null)
{
throw new ArgumentNullException("stream");
}
return ConfigurationReader.ReadFromBinaryStream(stream, reader);
}
#endregion
#region Save
/// <summary>
/// Saves the configuration to a file using the default character encoding, which is UTF8.
/// </summary>
///
/// <param name="filename">The location of the configuration file.</param>
public void SaveToFile(string filename)
=> this.SaveToFile(filename, null);
/// <summary>
/// Saves the configuration to a file.
/// </summary>
///
/// <param name="filename">The location of the configuration file.</param>
/// <param name="encoding">The character encoding to use. Specify null to use the default encoding, which is UTF8.</param>
///
/// <exception cref="ArgumentNullException">When <paramref name="filename"/> is null or empty.</exception>
public void SaveToFile(string filename, Encoding encoding)
{
if (string.IsNullOrEmpty(filename))
{
throw new ArgumentNullException("filename");
}
using (var stream = new FileStream(filename, FileMode.Create, FileAccess.Write))
{
this.SaveToStream(stream, encoding);
}
}
/// <summary>
/// Saves the configuration to a stream using the default character encoding, which is UTF8.
/// </summary>
///
/// <param name="stream">The stream to save the configuration to.</param>
public void SaveToStream(Stream stream)
=> this.SaveToStream(stream, null);
/// <summary>
/// Saves the configuration to a stream.
/// </summary>
///
/// <param name="stream">The stream to save the configuration to.</param>
/// <param name="encoding">The character encoding to use. Specify null to use the default encoding, which is UTF8.</param>
///
/// <exception cref="ArgumentNullException">When <paramref name="stream"/> is null.</exception>
public void SaveToStream(Stream stream, Encoding encoding)
{
if (stream == null)
{
throw new ArgumentNullException("stream");
}
ConfigurationWriter.WriteToStreamTextual(this, stream, encoding);
}
#endregion
#region SaveBinary
/// <summary>
/// Saves the configuration to a binary file, using the default <see cref="BinaryWriter"/>.
/// </summary>
///
/// <param name="filename">The location of the configuration file.</param>
public void SaveToBinaryFile(string filename)
=> this.SaveToBinaryFile(filename, null);
/// <summary>
/// Saves the configuration to a binary file, using a specific <see cref="BinaryWriter"/>.
/// </summary>
///
/// <param name="filename">The location of the configuration file.</param>
/// <param name="writer"> The writer to use. Specify null to use the default writer.</param>
///
/// <exception cref="ArgumentNullException">When <paramref name="filename"/> is null or empty.</exception>
public void SaveToBinaryFile(string filename, BinaryWriter writer)
{
if (string.IsNullOrEmpty(filename))
{
throw new ArgumentNullException("filename");
}
using (var stream = new FileStream(filename, FileMode.Create, FileAccess.Write))
{
this.SaveToBinaryStream(stream, writer);
}
}
/// <summary>
/// Saves the configuration to a binary stream, using the default <see cref="BinaryWriter"/>.
/// </summary>
///
/// <param name="stream">The stream to save the configuration to.</param>
public void SaveToBinaryStream(Stream stream)
=> this.SaveToBinaryStream(stream, null);
/// <summary>
/// Saves the configuration to a binary file, using a specific <see cref="BinaryWriter"/>.
/// </summary>
///
/// <param name="stream">The stream to save the configuration to.</param>
/// <param name="writer">The writer to use. Specify null to use the default writer.</param>
///
/// <exception cref="ArgumentNullException">When <paramref name="stream"/> is null.</exception>
public void SaveToBinaryStream(Stream stream, BinaryWriter writer)
{
if (stream == null)
{
throw new ArgumentNullException("stream");
}
ConfigurationWriter.WriteToStreamBinary(this, stream, writer);
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the CultureInfo that is used for value conversion in SharpConfig.
/// The default value is CultureInfo.InvariantCulture.
/// </summary>
///
/// <exception cref="ArgumentNullException">When a null reference is set.</exception>
public static CultureInfo CultureInfo
{
get => mCultureInfo;
set => mCultureInfo = value ?? throw new ArgumentNullException("value");
}
/// <summary>
/// Gets the array that contains all valid comment delimiting characters.
/// The current value is { '#', ';' }.
/// </summary>
public static char[] ValidCommentChars { get; private set; }
/// <summary>
/// Gets or sets the preferred comment char when saving configurations.
/// The default value is '#'.
/// </summary>
///
/// <exception cref="ArgumentException">When an invalid character is set.</exception>
public static char PreferredCommentChar
{
get => mPreferredCommentChar;
set
{
if (!Array.Exists(ValidCommentChars, c => c == value))
{
throw new ArgumentException("The specified char '" + value + "' is not allowed as a comment char.");
}
mPreferredCommentChar = value;
}
}
/// <summary>
/// Gets or sets the array element separator character for settings.
/// The default value is ','.
/// NOTE: remember that after you change this value while <see cref="Setting"/> instances exist,
/// to expect their ArraySize and other array-related values to return different values.
/// </summary>
///
/// <exception cref="ArgumentException">When a zero-character ('\0') is set.</exception>
public static char ArrayElementSeparator
{
get
{
return mArrayElementSeparator;
}
set
{
if (value == '\0')
{
throw new ArgumentException("Zero-character is not allowed.");
}
mArrayElementSeparator = value;
}
}
/// <summary>
/// Gets or sets a value indicating whether string values are written
/// without quotes, but including everything in between.
/// Example:
/// The following setting value
/// MySetting=" Example value"
/// is written to a file in the following manner
/// MySetting= Example value
/// </summary>
public static bool OutputRawStringValues { get; set; }
/// <summary>
/// Gets or sets a value indicating whether inline-comments
/// should be ignored when parsing a configuration.
/// </summary>
public static bool IgnoreInlineComments { get; set; }
/// <summary>
/// Gets or sets a value indicating whether pre-comments
/// should be ignored when parsing a configuration.
/// </summary>
public static bool IgnorePreComments { get; set; }
/// <summary>
/// Gets or sets a value indicating whether space between
/// equals should be added when creating a configuration.
/// </summary>
public static bool SpaceBetweenEquals { get; set; }
/// <summary>
/// Gets the number of sections that are in the configuration.
/// </summary>
public int SectionCount => this.mSections.Count;
/// <summary>
/// Gets or sets a section by index.
/// </summary>
/// <param name="index">The index of the section in the configuration.</param>
///
/// <returns>
/// The section at the specified index.
/// Note: no section is created when using this accessor.
/// </returns>
///
/// <exception cref="ArgumentOutOfRangeException">When the index is out of range.</exception>
public Section this[int index]
{
get
{
if (index < 0 || index >= this.mSections.Count)
{
throw new ArgumentOutOfRangeException("index");
}
return this.mSections[index];
}
}
/// <summary>
/// Gets or sets a section by its name.
/// If there are multiple sections with the same name, the first section is returned.
/// If you want to obtain all sections that have the same name, use the GetSectionsNamed() method instead.
/// </summary>
///
/// <param name="name">The case-sensitive name of the section.</param>
///
/// <returns>
/// The section if found, otherwise a new section with
/// the specified name is created, added to the configuration and returned.
/// </returns>
public Section this[string name]
{
get
{
var section = this.FindSection(name);
if (section == null)
{
section = new Section(name);
this.Add(section);
}
return section;
}
}
/// <summary>
/// Gets the default, hidden section.
/// </summary>
public Section DefaultSection
=> this[Section.DefaultSectionName];
/// <summary>
/// Gets all sections that have a specific name.
/// </summary>
/// <param name="name">The case-sensitive name of the sections.</param>
/// <returns>
/// The found sections.
/// </returns>
public IEnumerable<Section> GetSectionsNamed(string name)
{
var sections = new List<Section>();
foreach (var section in this.mSections)
{
if (string.Equals(section.Name, name, StringComparison.OrdinalIgnoreCase))
{
sections.Add(section);
}
}
return sections;
}
// Finds a section by its name.
private Section FindSection(string name)
{
foreach (var section in this.mSections)
{
if (string.Equals(section.Name, name, StringComparison.OrdinalIgnoreCase))
{
return section;
}
}
return null;
}
#endregion
}
}
/* ConfigurationElement.cs */
namespace SharpConfig
{
/// <summary>
/// Represents the base class of all elements
/// that exist in a <see cref="Configuration"/>,
/// such as sections and settings.
/// </summary>
public abstract class ConfigurationElement
{
internal ConfigurationElement(string name)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentNullException("name");
}
this.Name = name;
}
/// <summary>
/// Gets the name of this element.
/// </summary>
public string Name { get; }
/// <summary>
/// Gets or sets the comment of this element.
/// </summary>
public string Comment { get; set; }
/// <summary>
/// Gets the comment above this element.
/// </summary>
public string PreComment { get; set; }
/// <summary>
/// Gets the string representation of the element.
/// </summary>
///
public override string ToString()
{
string stringExpr = this.GetStringExpression();
if (this.Comment != null && this.PreComment != null &&
!Configuration.IgnoreInlineComments && !Configuration.IgnorePreComments)
{
// Include inline comment and pre-comments.
return $"{this.GetFormattedPreComment()}{Environment.NewLine}{stringExpr} {this.GetFormattedComment()}";
}
else if (this.Comment != null && !Configuration.IgnoreInlineComments)
{
// Include only the inline comment.
return $"{stringExpr} {this.GetFormattedComment()}";
}
else if (this.PreComment != null && !Configuration.IgnorePreComments)
{
// Include only the pre-comments.
return $"{this.GetFormattedPreComment()}{Environment.NewLine}{stringExpr}";
}
else
{
// In every other case, just return the expression.
return stringExpr;
}
}
// Gets a formatted comment string that is ready to be written to a config file.
private string GetFormattedComment()
{
// Only get the first line of the inline comment.
string comment = this.Comment;
int iNewLine = this.Comment.IndexOfAny(Environment.NewLine.ToCharArray());
if (iNewLine >= 0)
{
comment = comment.Substring(0, iNewLine);
}
return (Configuration.PreferredCommentChar + " " + comment);
}
// Gets a formatted pre-comment string that is ready
// to be written to a config file.
private string GetFormattedPreComment()
{
string[] lines = this.PreComment.Split(
new[] { "\r\n", "\n" },
StringSplitOptions.None);
return string.Join(
Environment.NewLine,
Array.ConvertAll(lines, s => Configuration.PreferredCommentChar + " " + s));
}
/// <summary>
/// Gets the element's expression as a string.
/// An example for a section would be "[Section]".
/// </summary>
/// <returns>The element's expression as a string.</returns>
protected abstract string GetStringExpression();
}
}
/* ConfigurationReader.cs */
namespace SharpConfig
{
internal static class ConfigurationReader
{
internal static Configuration ReadFromString(string source)
{
var config = new Configuration();
using (var reader = new StringReader(source))
{
Parse(reader, config);
}
return config;
}
private static void Parse(StringReader reader, Configuration config)
{
var currentSection = new Section(Section.DefaultSectionName);
var preCommentBuilder = new StringBuilder();
int lineNumber = 0;
string line;
// Read until EOF.
while ((line = reader.ReadLine()) != null)
{
lineNumber++;
// Remove all leading/trailing white-spaces.
line = line.Trim();
// Do not process empty lines.
if (string.IsNullOrEmpty(line))
{
continue;
}
var comment = ParseComment(line, out int commentIndex);
if (commentIndex == 0)
{
// pre-comment
if (!Configuration.IgnorePreComments)
{
preCommentBuilder.AppendLine(comment);
}
continue;
}
string lineWithoutComment = line;
if (commentIndex > 0)
{
lineWithoutComment = line.Remove(commentIndex).Trim(); // remove inline comment
}
// Section
if (lineWithoutComment.StartsWith("["))
{
// If the first section has been found but settings already exist, add them to the default section.
if (currentSection.Name == Section.DefaultSectionName && currentSection.SettingCount > 0)
{
config.mSections.Add(currentSection);
}
currentSection = ParseSection(lineWithoutComment, lineNumber);
if (!Configuration.IgnoreInlineComments)
{
currentSection.Comment = comment;
}
if (!Configuration.IgnorePreComments && preCommentBuilder.Length > 0)
{
// Set the current section's pre-comment, removing the last newline character.
currentSection.PreComment = preCommentBuilder.ToString().TrimEnd(Environment.NewLine.ToCharArray());
preCommentBuilder.Length = 0; // Clear the SB - With .NET >= 4.0: preCommentBuilder.Clear()
}
config.mSections.Add(currentSection);
}
else
{ // Setting
var setting = ParseSetting(Configuration.IgnoreInlineComments ? line : lineWithoutComment, lineNumber);
if (!Configuration.IgnoreInlineComments)
{
setting.Comment = comment;
}
if (!Configuration.IgnorePreComments && preCommentBuilder.Length > 0)
{
// Set the setting's pre-comment, removing the last newline character.
setting.PreComment = preCommentBuilder.ToString().TrimEnd(Environment.NewLine.ToCharArray());
preCommentBuilder.Length = 0; // Clear the SB - With .NET >= 4.0: preCommentBuilder.Clear()
}
currentSection.Add(setting);
}
}
}
private static string ParseComment(string line, out int commentCharIndex)
{
// A comment starts with a valid comment character that:
// 1. is not within a quote (eg. "this is # not a comment"), and
// 2. is not escaped (eg. this is \# not a comment either).
//
// A quote has two quotation marks, neither of which is escaped.
// For example: "this is a quote \" with an escaped quotation mark inside of it"
string comment = null;
commentCharIndex = -1;
var index = 0;
var quoteCount = 0;
while (line.Length > index) // traverse line from left to right
{
var isValidCommentChar = Array.IndexOf(Configuration.ValidCommentChars, line[index]) > -1;
var isQuotationMark = line[index] == '\"';
var isCharWithinQuotes = quoteCount % 2 == 1;
var isCharEscaped = index > 0 && line[index - 1] == '\\';
if (isValidCommentChar && !isCharWithinQuotes && !isCharEscaped)
{
break; // a comment has started
}
if (isQuotationMark && !isCharEscaped)
{
quoteCount++; // a non-escaped quotation mark has been found
}
index++;
}
if (index < line.Length)
{
// The end of the string has not been reached => index points to a valid comment character.
commentCharIndex = index;
comment = line.Substring(index + 1).TrimStart();
}
return comment;
}
private static Section ParseSection(string line, int lineNumber)
{
// Format(s) of a section:
// 1) [<name>]
// <name> may contain any char, including '[', ']', and a valid comment delimiter character
int closingBracketIndex = line.LastIndexOf(']');
if (closingBracketIndex < 0)
{
throw new ParserException("closing bracket missing.", lineNumber);
}
string sectionName = line.Substring(1, closingBracketIndex - 1).Trim();
// Anything after the (last) closing bracket must be whitespace.
if (line.Length > closingBracketIndex + 1)
{
var endPart = line.Substring(closingBracketIndex + 1).Trim();
if (endPart.Length > 0)
{
throw new ParserException($"unexpected token: '{endPart}'", lineNumber);
}
}
return new Section(sectionName);
}
private static Setting ParseSetting(string line, int lineNumber)
{
// Format(s) of a setting:
// 1) <name> = <value>
// <name> may not contain a '='
// 2) "<name>" = <value>
// <name> may contain any char, including '='
string settingName = null;
int equalSignIndex;
// Parse the name first.
bool isQuotedName = line.StartsWith("\"");
if (isQuotedName)
{
// Format 2
int closingQuoteIndex = 0;
do
{
closingQuoteIndex = line.IndexOf('\"', closingQuoteIndex + 1);
}
while (closingQuoteIndex > 0 && line[closingQuoteIndex - 1] == '\\');
if (closingQuoteIndex < 0)
{
throw new ParserException("closing quote mark expected.", lineNumber);
}
// Don't trim the name. Quoted names should be taken verbatim.
settingName = line.Substring(1, closingQuoteIndex - 1);
equalSignIndex = line.IndexOf('=', closingQuoteIndex + 1);
}
else
{
// Format 1
equalSignIndex = line.IndexOf('=');
}
if (equalSignIndex < 0)
{
throw new ParserException("setting assignment expected.", lineNumber);
}
if (!isQuotedName)
{
settingName = line.Substring(0, equalSignIndex).Trim();
}
if (string.IsNullOrEmpty(settingName))
{
throw new ParserException("setting name expected.", lineNumber);
}
var settingValue = line.Substring(equalSignIndex + 1).Trim();
return new Setting(settingName, settingValue);
}
internal static Configuration ReadFromBinaryStream(Stream stream, BinaryReader reader)
{
if (stream == null)
{
throw new ArgumentNullException("stream");
}
if (reader == null)
{
reader = new BinaryReader(stream);
}
var config = new Configuration();
int sectionCount = reader.ReadInt32();
for (int i = 0; i < sectionCount; ++i)
{
string sectionName = reader.ReadString();
int settingCount = reader.ReadInt32();
var section = new Section(sectionName);
ReadCommentsBinary(reader, section);
for (int j = 0; j < settingCount; j++)
{
var setting = new Setting(reader.ReadString())
{
RawValue = reader.ReadString(),
};
ReadCommentsBinary(reader, setting);
section.Add(setting);
}
config.Add(section);
}
return config;
}
private static void ReadCommentsBinary(BinaryReader reader, ConfigurationElement element)
{
bool hasComment = reader.ReadBoolean();
if (hasComment)
{
// Read the comment char, but don't do anything with it.
// This is just for backwards-compatibility.
reader.ReadChar();
element.Comment = reader.ReadString();
}
bool hasPreComment = reader.ReadBoolean();
if (hasPreComment)
{
// Same as above.
reader.ReadChar();
element.PreComment = reader.ReadString();
}
}
}
}
/* ConfigurationWriter.cs */
namespace SharpConfig
{
internal static class ConfigurationWriter
{
// We need this, as we never want to close the stream the user has given us.
// But we also want to call the specified writer's Dispose() method.
// We wouldn't need this if we were targeting .NET 4+, because BinaryWriter
// gives us the option to leave the stream open after Dispose(), but not
// on .NET lower than 4.0.
// To circumvent this, we just define our own writer that does not close
// the underlying stream in Dispose().
private class NonClosingBinaryWriter : BinaryWriter
{
public NonClosingBinaryWriter(Stream stream)
: base(stream)
{ }
protected override void Dispose(bool disposing)
{ }
}
internal static void WriteToStreamTextual(Configuration cfg, Stream stream, Encoding encoding)
{
Debug.Assert(cfg != null);
if (stream == null)
{
throw new ArgumentNullException("stream");
}
if (encoding == null)
{
encoding = Encoding.UTF8;
}
var str = cfg.StringRepresentation;
// Encode & write the string.
var byteBuffer = new byte[encoding.GetByteCount(str)];
int byteCount = encoding.GetBytes(str, 0, str.Length, byteBuffer, 0);
stream.Write(byteBuffer, 0, byteCount);
stream.Flush();
}
internal static void WriteToStreamBinary(Configuration cfg, Stream stream, BinaryWriter writer)
{
Debug.Assert(cfg != null);
if (stream == null)
{
throw new ArgumentNullException("stream");
}
if (writer == null)
{
writer = new NonClosingBinaryWriter(stream);
}
writer.Write(cfg.SectionCount);
foreach (var section in cfg)
{
writer.Write(section.Name);
writer.Write(section.SettingCount);
WriteCommentsBinary(writer, section);
// Write the section's settings.
foreach (var setting in section)
{
writer.Write(setting.Name);
writer.Write(setting.RawValue);
WriteCommentsBinary(writer, setting);
}
}
writer.Close();
}
private static void WriteCommentsBinary(BinaryWriter writer, ConfigurationElement element)
{
writer.Write(element.Comment != null);
if (element.Comment != null)
{
// SharpConfig <3.0 wrote the comment char here.
// We'll just write a single char for backwards-compatibility.
writer.Write(' ');
writer.Write(element.Comment);
}
writer.Write(element.PreComment != null);
if (element.PreComment != null)
{
// Same as with inline comments above.
writer.Write(' ');
writer.Write(element.PreComment);
}
}
}
}
/* IgnoreAttribute.cs */
namespace SharpConfig
{
/// <summary>
/// Represents an attribute that tells SharpConfig to
/// ignore the subject this attribute is applied to.
/// For example, if this attribute is applied to a property
/// of a type, that property will be ignored when creating
/// sections from objects and vice versa.
/// </summary>
public sealed class IgnoreAttribute : Attribute
{ }
}
/* ITypeStringConverter.cs */
namespace SharpConfig
{
/// <summary>
/// Defines a type-to-string and string-to-type converter
/// that is used for the conversion of setting values.
/// </summary>
public interface ITypeStringConverter
{
/// <summary>
/// Converts an object to its string representation.
/// </summary>
/// <param name="value">The value to convert.</param>
/// <returns>The object's string representation.</returns>
string ConvertToString(object value);
/// <summary>
/// The type that this converter is able to convert to and from a string.
/// </summary>
Type ConvertibleType { get; }
/// <summary>
/// Tries to convert a string value to an object of this converter's type.
/// </summary>
/// <param name="value"></param>
/// <param name="hint">
/// A type hint. This is used rarely, such as in the enum converter.
/// The enum converter's official type is Enum, whereas the type hint
/// represents the underlying enum type.
/// This parameter can be safely ignored for custom converters.
/// </param>
/// <returns>The converted object, or null if conversion is not possible.</returns>
object TryConvertFromString(string value, Type hint);
}
/// <summary>
/// Represents a type-to-string and string-to-type converter
/// that is used for the conversion of setting values.
/// </summary>
/// <typeparam name="T">The type that this converter is able to convert.</typeparam>
public abstract class TypeStringConverter<T> : ITypeStringConverter
{
/// <summary>
/// Converts an object to its string representation.
/// </summary>
/// <param name="value">The value to convert.</param>
/// <returns>The object's string representation.</returns>
public abstract string ConvertToString(object value);
/// <summary>
/// The type that this converter is able to convert to and from a string.
/// </summary>
public Type ConvertibleType => typeof(T);
/// <summary>
/// Tries to convert a string value to an object of this converter's type.
/// </summary>
/// <param name="value"></param>
/// <param name="hint">
/// A type hint. This is used rarely, such as in the enum converter.
/// The enum converter's official type is Enum, whereas the type hint
/// represents the underlying enum type.
/// This parameter can be safely ignored for custom converters.
/// </param>
/// <returns>The converted object, or null if conversion is not possible.</returns>
public abstract object TryConvertFromString(string value, Type hint);
}
}
/* ParserException.cs */
namespace SharpConfig
{
/// <summary>
/// Represents an error that occurred during
/// the configuration parsing stage.
/// </summary>
[Serializable]
public sealed class ParserException : Exception
{
internal ParserException(string message, int line)
: base($"Line {line}: {message}")
{
this.Line = line;
}
/// <summary>
/// Gets the line in the configuration that caused the exception.
/// </summary>
public int Line { get; private set; }
}
}
/* Section.cs */
namespace SharpConfig
{
/// <summary>
/// Represents a group of <see cref="Setting"/> objects.
/// </summary>
public sealed class Section : ConfigurationElement, IEnumerable<Setting>
{
/// <summary>
/// The name of the default, hidden section.
/// </summary>
public const string DefaultSectionName = "$SharpConfigDefaultSection";
private readonly List<Setting> mSettings;
/// <summary>
/// Initializes a new instance of the <see cref="Section"/> class.
/// </summary>
///
/// <param name="name">The name of the section.</param>
public Section(string name)
: base(name)
{
this.mSettings = new List<Setting>();
}
/// <summary>
/// Creates a new instance of the <see cref="Section"/> class that is
/// based on an existing object.
/// Important: the section is built only from the public getter properties
/// and fields of its type.
/// When this method is called, all of those properties will be called
/// and fields accessed once to obtain their values.
/// Properties and fields that are marked with the <see cref="IgnoreAttribute"/> attribute
/// or are of a type that is marked with that attribute, are ignored.
/// </summary>
/// <param name="name">The name of the section.</param>
/// <param name="obj"></param>
/// <returns>The newly created section.</returns>
///
/// <exception cref="ArgumentException">When <paramref name="name"/> is null or empty.</exception>
/// <exception cref="ArgumentNullException">When <paramref name="obj"/> is null.</exception>
public static Section FromObject(string name, object obj)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("The section name must not be null or empty.", "name");
}
if (obj == null)
{
throw new ArgumentNullException("obj");
}
var section = new Section(name);
var type = obj.GetType();
foreach (var prop in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
if (!prop.CanRead || ShouldIgnoreMappingFor(prop))
{
// Skip this property, as it can't be read from.
continue;
}
var setting = new Setting(prop.Name, prop.GetValue(obj, null));
section.mSettings.Add(setting);
}
// Repeat for each public field.
foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public))
{
if (ShouldIgnoreMappingFor(field))
{
// Skip this field.
continue;
}
var setting = new Setting(field.Name, field.GetValue(obj));
section.mSettings.Add(setting);
}
return section;
}
/// <summary>
/// Creates an object of a specific type, and maps the settings
/// in this section to the public properties and writable fields of the object.
/// Properties and fields that are marked with the <see cref="IgnoreAttribute"/> attribute
/// or are of a type that is marked with that attribute, are ignored.
/// </summary>
///
/// <typeparam name="T">
/// The type of object to create.
/// Note: the type must be default-constructible, meaning it has a public default constructor.
/// </typeparam>
///
/// <returns>The created object.</returns>
///
/// <remarks>
/// The specified type must have a public default constructor
/// in order to be created.
/// </remarks>
public T ToObject<T>() where T : new()
{
var obj = Activator.CreateInstance<T>();
this.SetValuesTo(obj);
return obj;
}
/// <summary>
/// Creates an object of a specific type, and maps the settings
/// in this section to the public properties and writable fields of the object.
/// Properties and fields that are marked with the <see cref="IgnoreAttribute"/> attribute
/// or are of a type that is marked with that attribute, are ignored.
/// </summary>
///
/// <param name="type">
/// The type of object to create.
/// Note: the type must be default-constructible, meaning it has a public default constructor.
/// </param>
///
/// <returns>The created object.</returns>
///
/// <remarks>
/// The specified type must have a public default constructor
/// in order to be created.
/// </remarks>
///
/// <exception cref="ArgumentNullException">When <paramref name="type"/> is null.</exception>
public object ToObject(Type type)
{
if (type == null)
{
throw new ArgumentNullException(type.Name);
}
var obj = Activator.CreateInstance(type);
this.SetValuesTo(obj);
return obj;
}
/// <summary>
/// Assigns the values of an object's public properties and fields to the corresponding
/// <b>already existing</b> settings in this section.
/// Properties and fields that are marked with the <see cref="IgnoreAttribute"/> attribute
/// or are of a type that is marked with that attribute, are ignored.
/// </summary>
///
/// <param name="obj">The object from which the values are obtained.</param>
///
/// <exception cref="ArgumentNullException">When <paramref name="obj"/> is null.</exception>
public void GetValuesFrom(object obj)
{
if (obj == null)
{
throw new ArgumentNullException("obj");
}
var type = obj.GetType();
// Scan the type's properties.
foreach (var prop in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
if (!prop.CanRead)
{
continue;
}
this.SetSettingValueFromMemberInfo(prop, obj);
}
// Scan the type's fields.
foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public))
{
this.SetSettingValueFromMemberInfo(field, obj);
}
}
private void SetSettingValueFromMemberInfo(MemberInfo info, object instance)
{
if (ShouldIgnoreMappingFor(info))
{
return;
}
var setting = this.FindSetting(info.Name);
if (setting != null)
{
object value = null;
if (info is FieldInfo)
{
value = ((FieldInfo)info).GetValue(instance);
}
else if (info is PropertyInfo)
{
value = ((PropertyInfo)info).GetValue(instance, null);
}
setting.SetValue(value);
}
}
/// <summary>
/// Assigns the values of this section to an object's public properties and fields.
/// Properties and fields that are marked with the <see cref="IgnoreAttribute"/> attribute
/// or are of a type that is marked with that attribute, are ignored.
/// </summary>
///
/// <param name="obj">The object that is modified based on the section.</param>
///
/// <exception cref="ArgumentNullException">When <paramref name="obj"/> is null.</exception>
public void SetValuesTo(object obj)
{
if (obj == null)
{
throw new ArgumentNullException("obj");
}
var type = obj.GetType();
// Scan the type's properties.
foreach (var prop in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
if (!prop.CanWrite || ShouldIgnoreMappingFor(prop))
{
continue;
}
var setting = this.FindSetting(prop.Name);
if (setting == null)
{
continue;
}
object value = prop.PropertyType.IsArray ?
setting.GetValueArray(prop.PropertyType.GetElementType()) :
setting.GetValue(prop.PropertyType);
if (prop.PropertyType.IsArray)
{
var settingArray = value as Array;
var propArray = prop.GetValue(obj, null) as Array;
if (settingArray != null && (propArray == null || propArray.Length != settingArray.Length))
{
// (Re)create the property's array.
propArray = Array.CreateInstance(prop.PropertyType.GetElementType(), length: settingArray.Length);
}
for (int i = 0; i < settingArray.Length; i++)
{
propArray?.SetValue(settingArray.GetValue(i), i);
}
prop.SetValue(obj, propArray, null);
}
else
{
prop.SetValue(obj, value, null);
}
}
// Scan the type's fields.
foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public))
{
// Skip readonly fields.
if (field.IsInitOnly || ShouldIgnoreMappingFor(field))
{
continue;
}
var setting = this.FindSetting(field.Name);
if (setting == null)
{
continue;
}
object value = field.FieldType.IsArray ?
setting.GetValueArray(field.FieldType.GetElementType()) :
setting.GetValue(field.FieldType);
if (field.FieldType.IsArray)
{
var settingArray = value as Array;
var fieldArray = field.GetValue(obj) as Array;
if (settingArray != null && (fieldArray == null || fieldArray.Length != settingArray.Length))
{
// (Re)create the field's array.
fieldArray = Array.CreateInstance(field.FieldType.GetElementType(), settingArray.Length);
}
for (int i = 0; i < settingArray.Length; i++)
{
fieldArray.SetValue(settingArray.GetValue(i), i);
}
field.SetValue(obj, fieldArray);
}
else
{
field.SetValue(obj, value);
}
}
}
// Determines whether a member should be ignored.
private static bool ShouldIgnoreMappingFor(MemberInfo member)
{
if (member.GetCustomAttributes(typeof(IgnoreAttribute), false).Length > 0)
{
return true;
}
else
{
if (member as PropertyInfo != null)
{
return (member as PropertyInfo).PropertyType.GetCustomAttributes(typeof(IgnoreAttribute), false).Length > 0;
}
if (member as FieldInfo != null)
{
return (member as FieldInfo).FieldType.GetCustomAttributes(typeof(IgnoreAttribute), false).Length > 0;
}
}
return false;
}
/// <summary>
/// Gets an enumerator that iterates through the section.
/// </summary>
public IEnumerator<Setting> GetEnumerator()
=> this.mSettings.GetEnumerator();
/// <summary>
/// Gets an enumerator that iterates through the section.
/// </summary>
IEnumerator IEnumerable.GetEnumerator()
=> this.GetEnumerator();
/// <summary>
/// Adds a setting to the section.
/// </summary>
/// <param name="setting">The setting to add.</param>
///
/// <exception cref="ArgumentNullException">When <paramref name="setting"/> is null.</exception>
/// <exception cref="ArgumentException">When the specified setting already exists in the section.</exception>
public void Add(Setting setting)
{
if (setting == null)
{
throw new ArgumentNullException("setting");
}
if (this.Contains(setting))
{
throw new ArgumentException("The specified setting already exists in the section.");
}
this.mSettings.Add(setting);
}
/// <summary>
/// Adds a setting with a specific name and empty value to the section.
/// </summary>
/// <param name="settingName">The name of the setting to add.</param>
/// <returns>The added setting.</returns>
/// <exception cref="ArgumentNullException">When <paramref name="settingName"/> is null or empty.</exception>
public Setting Add(string settingName)
=> this.Add(settingName, string.Empty);
/// <summary>
/// Adds a setting with a specific name and value to the section.
/// </summary>
/// <param name="settingName">The name of the setting to add.</param>
/// <param name="settingValue">The initial value of the setting to add.</param>
/// <returns>The added setting.</returns>
/// <exception cref="ArgumentNullException">When <paramref name="settingName"/> is null or empty.</exception>
public Setting Add(string settingName, object settingValue)
{
var setting = new Setting(settingName, settingValue);
this.Add(setting);
return setting;
}
/// <summary>
/// Removes a setting from the section by its name.
/// If there are multiple settings with the same name, only the first setting is removed.
/// To remove all settings that have the name name, use the RemoveAllNamed() method instead.
/// </summary>
/// <param name="settingName">The case-sensitive name of the setting to remove.</param>
/// <returns>True if a setting with the specified name was removed; false otherwise.</returns>
///
/// <exception cref="ArgumentNullException">When <paramref name="settingName"/> is null or empty.</exception>
public bool Remove(string settingName)
{
if (string.IsNullOrEmpty(settingName))
{
throw new ArgumentNullException("settingName");
}
return this.Remove(this.FindSetting(settingName));
}
/// <summary>
/// Removes a setting from the section.
/// </summary>
/// <param name="setting">The setting to remove.</param>
/// <returns>True if the setting was removed; false otherwise.</returns>
public bool Remove(Setting setting)
=> this.mSettings.Remove(setting);
/// <summary>
/// Removes all settings that have a specific name.
/// </summary>
/// <param name="settingName">The case-sensitive name of the settings to remove.</param>
///
/// <exception cref="ArgumentNullException">When <paramref name="settingName"/> is null or empty.</exception>
public void RemoveAllNamed(string settingName)
{
if (string.IsNullOrEmpty(settingName))
{
throw new ArgumentNullException("settingName");
}
while (this.Remove(settingName))
{
;
}
}
/// <summary>
/// Clears the section of all settings.
/// </summary>
public void Clear()
=> this.mSettings.Clear();
/// <summary>
/// Determines whether a specified setting is contained in the section.
/// </summary>
/// <param name="setting">The setting to check for containment.</param>
/// <returns>True if the setting is contained in the section; false otherwise.</returns>
public bool Contains(Setting setting)
=> this.mSettings.Contains(setting);
/// <summary>
/// Determines whether a specifically named setting is contained in the section.
/// </summary>
/// <param name="settingName">The case-sensitive name of the setting.</param>
/// <returns>True if the setting is contained in the section; false otherwise.</returns>
///
/// <exception cref="ArgumentNullException">When <paramref name="settingName"/> is null or empty.</exception>
public bool Contains(string settingName)
{
if (string.IsNullOrEmpty(settingName))
{
throw new ArgumentNullException("settingName");
}
return this.FindSetting(settingName) != null;
}
/// <summary>
/// Gets the number of settings that are in the section.
/// </summary>
public int SettingCount
=> this.mSettings.Count;
/// <summary>
/// Gets or sets a setting by index.
/// </summary>
/// <param name="index">The index of the setting in the section.</param>
///
/// <returns>
/// The setting at the specified index.
/// Note: no setting is created when using this accessor.
/// </returns>
///
/// <exception cref="ArgumentOutOfRangeException">When <paramref name="index"/> is out of range.</exception>
public Setting this[int index]
{
get
{
if (index < 0 || index >= this.mSettings.Count)
{
throw new ArgumentOutOfRangeException("index");
}
return this.mSettings[index];
}
}
/// <summary>
/// Gets or sets a setting by its name.
/// If there are multiple settings with the same name, the first setting is returned.
/// If you want to obtain all settings that have the same name, use the GetSettingsNamed() method instead.
/// </summary>
///
/// <param name="name">The case-sensitive name of the setting.</param>
///
/// <returns>
/// The setting if found, otherwise a new setting with
/// the specified name is created, added to the section and returned.
/// </returns>
public Setting this[string name]
{
get
{
var setting = this.FindSetting(name);
if (setting == null)
{
setting = new Setting(name);
this.mSettings.Add(setting);
}
return setting;
}
}
/// <summary>
/// Gets all settings that have a specific name.
/// </summary>
/// <param name="name">The case-sensitive name of the settings.</param>
/// <returns>
/// The found settings.
/// </returns>
public IEnumerable<Setting> GetSettingsNamed(string name)
{
var settings = new List<Setting>();
foreach (var setting in this.mSettings)
{
if (string.Equals(setting.Name, name, StringComparison.OrdinalIgnoreCase))
{
settings.Add(setting);
}
}
return settings;
}
// Finds a setting by its name.
private Setting FindSetting(string name)
{
foreach (var setting in this.mSettings)
{
if (string.Equals(setting.Name, name, StringComparison.OrdinalIgnoreCase))
{
return setting;
}
}
return null;
}
/// <summary>
/// Gets the element's expression as a string.
/// An example for a section would be "[Section]".
/// </summary>
/// <returns>The element's expression as a string.</returns>
protected override string GetStringExpression()
=> $"[{this.Name}]";
}
}
/* Setting.cs */
namespace SharpConfig
{
/// <summary>
/// Represents a setting in a <see cref="Configuration"/>.
/// Settings are always stored in a <see cref="Section"/>.
/// </summary>
public sealed class Setting : ConfigurationElement
{
#region Fields
private int mCachedArraySize;
private bool mShouldCalculateArraySize;
private char mCachedArrayElementSeparator;
#endregion
#region Construction
/// <summary>
/// Initializes a new instance of the <see cref="Setting"/> class.
/// </summary>
public Setting(string name)
: this(name, string.Empty)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="Setting"/> class.
/// </summary>
///
/// <param name="name"> The name of the setting.</param>
/// <param name="value">The value of the setting.</param>
public Setting(string name, object value)
: base(name)
{
this.SetValue(value);
this.mCachedArrayElementSeparator = Configuration.ArrayElementSeparator;
}
#endregion
#region Properties
/// <summary>
/// Gets a value indicating whether this setting's value is empty.
/// </summary>
public bool IsEmpty
=> string.IsNullOrEmpty(this.RawValue);
/// <summary>
/// Gets the value of this setting as a <see cref="string"/>, with quotes removed if present.
/// </summary>
[Obsolete("Use StringValue instead")]
public string StringValueTrimmed
{
get
{
string value = this.StringValue;
if (value[0] == '\"')
{
value = value.Trim('\"');
}
return value;
}
}
/// <summary>
/// Gets or sets the raw value of this setting.
/// </summary>
public string RawValue { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the value of this setting as a <see cref="string"/>.
/// Note: this is a shortcut to GetValue and SetValue.
/// </summary>
public string StringValue
{
get => this.GetValue<string>().Trim('\"');
set => this.SetValue(value.Trim('\"'));
}
/// <summary>
/// Gets or sets the value of this setting as a <see cref="string"/> array.
/// Note: this is a shortcut to GetValueArray and SetValue.
/// </summary>
public string[] StringValueArray
{
get => this.GetValueArray<string>();
set => this.SetValue(value);
}
/// <summary>
/// Gets or sets the value of this setting as an <see cref="int"/>.
/// Note: this is a shortcut to GetValue and SetValue.
/// </summary>
public int IntValue
{
get => this.GetValue<int>();
set => this.SetValue(value);
}
/// <summary>
/// Gets or sets the value of this setting as an <see cref="int"/> array.
/// Note: this is a shortcut to GetValueArray and SetValue.
/// </summary>
public int[] IntValueArray
{
get => this.GetValueArray<int>();
set => this.SetValue(value);
}
/// <summary>
/// Gets or sets the value of this setting as a <see cref="float"/>.
/// Note: this is a shortcut to GetValue and SetValue.
/// </summary>
public float FloatValue
{
get => this.GetValue<float>();
set => this.SetValue(value);
}
/// <summary>
/// Gets or sets the value of this setting as a <see cref="float"/> array.
/// Note: this is a shortcut to GetValueArray and SetValue.
/// </summary>
public float[] FloatValueArray
{
get => this.GetValueArray<float>();
set => this.SetValue(value);
}
/// <summary>
/// Gets or sets the value of this setting as a <see cref="double"/>.
/// Note: this is a shortcut to GetValue and SetValue.
/// </summary>
public double DoubleValue
{
get => this.GetValue<double>();
set => this.SetValue(value);
}
/// <summary>
/// Gets or sets the value of this setting as a <see cref="double"/> array.
/// Note: this is a shortcut to GetValueArray and SetValue.
/// </summary>
public double[] DoubleValueArray
{
get => this.GetValueArray<double>();
set => this.SetValue(value);
}
/// <summary>
/// Gets or sets the value of this setting as a <see cref="decimal"/>.
/// Note: this is a shortcut to GetValue and SetValue.
/// </summary>
public decimal DecimalValue
{
get => this.GetValue<decimal>();
set => this.SetValue(value);
}
/// <summary>
/// Gets or sets the value of this setting as a <see cref="decimal"/> array.
/// Note: this is a shortcut to GetValueArray and SetValue.
/// </summary>
public decimal[] DecimalValueArray
{
get => this.GetValueArray<decimal>();
set => this.SetValue(value);
}
/// <summary>
/// Gets or sets the value of this setting as a <see cref="bool"/>.
/// Note: this is a shortcut to GetValue and SetValue.
/// </summary>
public bool BoolValue
{
get => this.GetValue<bool>();
set => this.SetValue(value);
}
/// <summary>
/// Gets or sets the value of this setting as a <see cref="bool"/> array.
/// Note: this is a shortcut to GetValueArray and SetValue.
/// </summary>
public bool[] BoolValueArray
{
get => this.GetValueArray<bool>();
set => this.SetValue(value);
}
/// <summary>
/// Gets or sets the value of this settings as a <see cref="DateTime"/>.
/// Note: this is a shortcut to GetValue and SetValue.
/// </summary>
public DateTime DateTimeValue
{
get => this.GetValue<DateTime>();
set => this.SetValue(value);
}
/// <summary>
/// Gets or sets the value of this setting as a <see cref="DateTime"/> array.
/// Note: this is a shortcut to GetValueArray and SetValue.
/// </summary>
public DateTime[] DateTimeValueArray
{
get => this.GetValueArray<DateTime>();
set => this.SetValue(value);
}
/// <summary>
/// Gets or sets the value of this setting as a <see cref="byte"/>.
/// Note: this is a shortcut to GetValueArray and SetValue.
/// </summary>
public byte ByteValue
{
get => this.GetValue<byte>();
set => this.SetValue(value);
}
/// <summary>
/// Gets or sets the value of this setting as a <see cref="byte"/> array.
/// Note: this is a shortcut to GetValueArray and SetValue.
/// </summary>
public byte[] ByteValueArray
{
get => this.GetValueArray<byte>();
set => this.SetValue(value);
}
/// <summary>
/// Gets or sets the value of this setting as a <see cref="sbyte"/>.
/// Note: this is a shortcut to GetValueArray and SetValue.
/// </summary>
public sbyte SByteValue
{
get => this.GetValue<sbyte>();
set => this.SetValue(value);
}
/// <summary>
/// Gets or sets the value of this setting as a <see cref="sbyte"/> array.
/// Note: this is a shortcut to GetValueArray and SetValue.
/// </summary>
public sbyte[] SByteValueArray
{
get => this.GetValueArray<sbyte>();
set => this.SetValue(value);
}
/// <summary>
/// Gets or sets the value of this setting as a <see cref="char"/>.
/// Note: this is a shortcut to GetValueArray and SetValue.
/// </summary>
public char CharValue
{
get => this.GetValue<char>();
set => this.SetValue(value);
}
/// <summary>
/// Gets or sets the value of this setting as a <see cref="char"/> array.
/// Note: this is a shortcut to GetValueArray and SetValue.
/// </summary>
public char[] CharValueArray
{
get
{
// Decode the bytes back to chars.
var bytes = this.ByteValueArray;
if (bytes != null)
{
return Encoding.UTF8.GetChars(this.ByteValueArray);
}
else
{
return null;
}
}
set
{
if (value != null)
{
// Encode the chars to bytes, because writing raw chars such as
// '\0' can mess up the configuration file and the parser.
this.ByteValueArray = Encoding.UTF8.GetBytes(value);
}
else
this.SetEmptyValue();
}
}
/// <summary>
/// Gets a value indicating whether this setting is an array.
/// </summary>
public bool IsArray
=> (this.ArraySize >= 0);
/// <summary>
/// Gets the size of the array that this setting represents.
/// If this setting is not an array, -1 is returned.
/// </summary>
public int ArraySize
{
get
{
// If the user changed the array element separator during the lifetime
// of this setting, we have to recalculate the array size.
if (this.mCachedArrayElementSeparator != Configuration.ArrayElementSeparator)
{
this.mCachedArrayElementSeparator = Configuration.ArrayElementSeparator;
this.mShouldCalculateArraySize = true;
}
if (this.mShouldCalculateArraySize)
{
this.mCachedArraySize = this.CalculateArraySize();
this.mShouldCalculateArraySize = false;
}
return this.mCachedArraySize;
}
}
private int CalculateArraySize()
{
int size = 0;
var enumerator = new SettingArrayEnumerator(this.RawValue, false);
while (enumerator.Next())
{
++size;
}
return (enumerator.IsValid ? size : -1);
}
#endregion
#region GetValue
/// <summary>
/// Gets this setting's value as a specific type.
/// </summary>
///
/// <param name="type">The type of the object to retrieve.</param>
///
/// <exception cref="ArgumentNullException">When <paramref name="type"/> is null.</exception>
/// <exception cref="InvalidOperationException">When <paramref name="type"/> is an array type.</exception>
/// <exception cref="InvalidOperationException">When the setting represents an array.</exception>
public object GetValue(Type type)
{
if (type == null)
{
throw new ArgumentNullException("type");
}
if (type.IsArray)
{
throw new InvalidOperationException("To obtain an array value, use GetValueArray() instead of GetValue().");
}
if (this.IsArray)
{
throw new InvalidOperationException("The setting represents an array. Use GetValueArray() to obtain its value.");
}
return CreateObjectFromString(this.RawValue, type);
}
/// <summary>
/// Gets this setting's value as an array of a specific type.
/// Note: this only works if the setting represents an array. If it is not, then null is returned.
/// </summary>
/// <param name="elementType">
/// The type of elements in the array. All values in the array are going to be converted to objects of this type.
/// If the conversion of an element fails, an exception is thrown.
/// </param>
/// <returns>The values of this setting as an array.</returns>
public object[] GetValueArray(Type elementType)
{
if (elementType.IsArray)
{
throw CreateJaggedArraysNotSupportedEx(elementType);
}
int myArraySize = this.ArraySize;
if (this.ArraySize < 0)
{
return null;
}
var values = new object[myArraySize];
if (myArraySize > 0)
{
var enumerator = new SettingArrayEnumerator(this.RawValue, true);
int iElem = 0;
while (enumerator.Next())
{
values[iElem] = CreateObjectFromString(enumerator.Current, elementType);
++iElem;
}
}
return values;
}
/// <summary>
/// Gets this setting's value as a specific type.
/// </summary>
///
/// <typeparam name="T">The type of the object to retrieve.</typeparam>
///
/// <exception cref="InvalidOperationException">When <typeparamref name="T"/> is an array type.</exception>
/// <exception cref="InvalidOperationException">When the setting represents an array.</exception>
public T GetValue<T>()
{
var type = typeof(T);
if (type.IsArray)
{
throw new InvalidOperationException("To obtain an array value, use GetValueArray() instead of GetValue().");
}
if (this.IsArray)
{
throw new InvalidOperationException("The setting represents an array. Use GetValueArray() to obtain its value.");
}
return (T)CreateObjectFromString(this.RawValue, type);
}
/// <summary>
/// Gets this setting's value as an array of a specific type.
/// Note: this only works if the setting represents an array. If it is not, then null is returned.
/// </summary>
/// <typeparam name="T">
/// The type of elements in the array. All values in the array are going to be converted to objects of this type.
/// If the conversion of an element fails, an exception is thrown.
/// </typeparam>
/// <returns>The values of this setting as an array.</returns>
public T[] GetValueArray<T>()
{
var type = typeof(T);
if (type.IsArray)
{
throw CreateJaggedArraysNotSupportedEx(type);
}
int myArraySize = this.ArraySize;
if (myArraySize < 0)
{
return null;
}
var values = new T[myArraySize];
if (myArraySize > 0)
{
var enumerator = new SettingArrayEnumerator(this.RawValue, true);
int iElem = 0;
while (enumerator.Next())
{
values[iElem] = (T)CreateObjectFromString(enumerator.Current, type);
++iElem;
}
}
return values;
}
/// <summary>
/// Gets this setting's value as a specific type, or a specified default value
/// if casting the setting to the type fails.
/// </summary>
/// <param name="defaultValue">
/// Default value if casting the setting to the specified type fails.
/// </param>
/// <param name="setDefault">
/// If true, and casting the setting to the specified type fails, <paramref name="defaultValue"/> is set
/// as this setting's new value.
/// </param>
/// <typeparam name="T">The type of the object to retrieve.</typeparam>
public T GetValueOrDefault<T>(T defaultValue, bool setDefault = false)
{
var type = typeof(T);
if (type.IsArray)
{
throw new InvalidOperationException("GetValueOrDefault<T> cannot be used with arrays.");
}
if (this.IsArray)
{
throw new InvalidOperationException("The setting represents an array. Use GetValueArray() to obtain its value.");
}
var result = CreateObjectFromString(this.RawValue, type, true);
if (result != null)
{
return (T)result;
}
if (setDefault)
{
this.SetValue(defaultValue);
}
return defaultValue;
}
// Converts the value of a single element to a desired type.
private static object CreateObjectFromString(string value, Type dstType, bool tryConvert = false)
{
var underlyingType = Nullable.GetUnderlyingType(dstType);
if (underlyingType != null)
{
if (string.IsNullOrEmpty(value))
{
return null; // Returns Nullable<T>().
}
// Otherwise, continue with our conversion using
// the underlying type of the nullable.
dstType = underlyingType;
}
var converter = Configuration.FindTypeStringConverter(dstType);
var obj = converter.TryConvertFromString(value, dstType);
if (obj == null && !tryConvert)
{
throw SettingValueCastException.Create(value, dstType, null);
}
return obj;
}
#endregion
#region SetValue
/// <summary>
/// Sets the value of this setting via an object.
/// </summary>
///
/// <param name="value">The value to set.</param>
public void SetValue(object value)
{
if (value == null)
{
this.SetEmptyValue();
return;
}
var type = value.GetType();
if (type.IsArray)
{
var elementType = type.GetElementType();
if (elementType != null && elementType.IsArray)
{
throw CreateJaggedArraysNotSupportedEx(type.GetElementType());
}
var values = value as Array;
if (values != null)
{
var strings = new string[values.Length];
for (int i = 0; i < values.Length; i++)
{
object elemValue = values.GetValue(i);
var converter = Configuration.FindTypeStringConverter(elemValue.GetType());
strings[i] = GetValueForOutput(converter.ConvertToString(elemValue));
}
this.RawValue = $"{{{string.Join(Configuration.ArrayElementSeparator.ToString(), strings)}}}";
}
if (values != null)
{
this.mCachedArraySize = values.Length;
}
this.mShouldCalculateArraySize = false;
}
else
{
var converter = Configuration.FindTypeStringConverter(type);
this.RawValue = converter.ConvertToString(value);
this.mShouldCalculateArraySize = true;
}
}
private void SetEmptyValue()
{
this.RawValue = string.Empty;
this.mCachedArraySize = -1;
this.mShouldCalculateArraySize = false;
}
#endregion
private static string GetValueForOutput(string rawValue)
{
if (Configuration.OutputRawStringValues)
{
return rawValue;
}
if (rawValue.StartsWith("{") && rawValue.EndsWith("}"))
{
return rawValue;
}
if (rawValue.StartsWith("\"") && rawValue.EndsWith("\""))
{
return rawValue;
}
if (
rawValue.IndexOf(" ", StringComparison.Ordinal) >= 0 || (
rawValue.IndexOfAny(Configuration.ValidCommentChars) >= 0 &&
!Configuration.IgnoreInlineComments))
{
rawValue = "\"" + rawValue + "\"";
}
return rawValue;
}
/// <summary>
/// Gets the element's expression as a string.
/// An example for a section would be "[Section]".
/// </summary>
/// <returns>The element's expression as a string.</returns>
protected override string GetStringExpression()
{
if (Configuration.SpaceBetweenEquals)
{
return $"{this.Name} = {GetValueForOutput(this.RawValue)}";
}
else
{
return $"{this.Name}={GetValueForOutput(this.RawValue)}";
}
}
private static ArgumentException CreateJaggedArraysNotSupportedEx(Type type)
{
// Determine the underlying element type.
Type elementType = type.GetElementType();
while (elementType != null && elementType.IsArray)
{
elementType = elementType.GetElementType();
}
throw new ArgumentException(
$"Jagged arrays are not supported. The type you have specified is '{type.Name}', but '{elementType?.Name}' was expected.");
}
}
}
/* SettingArrayEnumerator.cs */
// Copyright (c) 2013-2018 Cemalettin Dervis, MIT License.
// https://github.com/cemdervis/SharpConfig
namespace SharpConfig
{
// Enumerates the elements of a Setting that represents an array.
internal sealed class SettingArrayEnumerator
{
private readonly string mStringValue;
private readonly bool mShouldCalcElemString;
private int mIdxInString;
private readonly int mLastRBraceIdx;
private int mPrevElemIdxInString;
private int mBraceBalance;
private bool mIsInQuotes;
private bool mIsDone;
public SettingArrayEnumerator(string value, bool shouldCalcElemString)
{
this.mStringValue = value;
this.mIdxInString = -1;
this.mLastRBraceIdx = -1;
this.mShouldCalcElemString = shouldCalcElemString;
this.IsValid = true;
this.mIsDone = false;
for (int i = 0; i < value.Length; ++i)
{
char ch = value[i];
if (ch != ' ' && ch != '{')
{
break;
}
if (ch == '{')
{
this.mIdxInString = i + 1;
this.mBraceBalance = 1;
this.mPrevElemIdxInString = i + 1;
break;
}
}
// Abort if no valid '{' occurred.
if (this.mIdxInString < 0)
{
this.IsValid = false;
this.mIsDone = true;
return;
}
// See where the last valid '}' is.
for (int i = value.Length - 1; i >= 0; --i)
{
char ch = value[i];
if (ch != ' ' && ch != '}')
{
break;
}
if (ch == '}')
{
this.mLastRBraceIdx = i;
break;
}
}
// Abort if no valid '}' occurred.
if (this.mLastRBraceIdx < 0)
{
this.IsValid = false;
this.mIsDone = true;
return;
}
// See if this is an empty array such as "{ }" or "{}".
// If so, this is a valid array, but with size 0.
if (this.mIdxInString == this.mLastRBraceIdx ||
!IsNonEmptyValue(this.mStringValue, this.mIdxInString, this.mLastRBraceIdx))
{
this.IsValid = true;
this.mIsDone = true;
return;
}
}
private void UpdateElementString(int idx)
{
this.Current = this.mStringValue.Substring(
this.mPrevElemIdxInString,
idx - this.mPrevElemIdxInString);
this.Current = this.Current.Trim(' '); // trim spaces first
// Now trim the quotes, but only the first and last, because
// the setting value itself can contain quotes.
if (this.Current[this.Current.Length - 1] == '\"')
{
this.Current = this.Current.Remove(this.Current.Length - 1, 1);
}
if (this.Current[0] == '\"')
{
this.Current = this.Current.Remove(0, 1);
}
}
public bool Next()
{
if (this.mIsDone)
{
return false;
}
int idx = this.mIdxInString;
while (idx <= this.mLastRBraceIdx)
{
char ch = this.mStringValue[idx];
if (ch == '{' && !this.mIsInQuotes)
{
++this.mBraceBalance;
}
else if (ch == '}' && !this.mIsInQuotes)
{
--this.mBraceBalance;
if (idx == this.mLastRBraceIdx)
{
// This is the last element.
if (!IsNonEmptyValue(this.mStringValue, this.mPrevElemIdxInString, idx))
{
// Empty array element; invalid array.
this.IsValid = false;
}
else if (this.mShouldCalcElemString)
{
this.UpdateElementString(idx);
}
this.mIsDone = true;
break;
}
}
else if (ch == '\"')
{
int iNextQuoteMark = this.mStringValue.IndexOf('\"', idx + 1);
if (iNextQuoteMark > 0 && this.mStringValue[iNextQuoteMark - 1] != '\\')
{
idx = iNextQuoteMark;
this.mIsInQuotes = false;
}
else
{
this.mIsInQuotes = true;
}
}
else if (ch == Configuration.ArrayElementSeparator && this.mBraceBalance == 1 && !this.mIsInQuotes)
{
if (!IsNonEmptyValue(this.mStringValue, this.mPrevElemIdxInString, idx))
{
// Empty value in-between commas; this is an invalid array.
this.IsValid = false;
}
else if (this.mShouldCalcElemString)
{
this.UpdateElementString(idx);
}
this.mPrevElemIdxInString = idx + 1;
// Yield.
++idx;
break;
}
++idx;
}
this.mIdxInString = idx;
if (this.mIsInQuotes)
{
this.IsValid = false;
}
return this.IsValid;
}
private static bool IsNonEmptyValue(string s, int begin, int end)
{
for (; begin < end; ++begin)
{
if (s[begin] != ' ')
{
return true;
}
}
return false;
}
public string Current { get; private set; }
public bool IsValid { get; private set; }
}
}
/* SettingValueCastException.cs */
namespace SharpConfig
{
/// <summary>
/// Represents an error that occurs when a string value could not be converted to a specific instance.
/// </summary>
[Serializable]
public sealed class SettingValueCastException : Exception
{
private SettingValueCastException(string message, Exception innerException)
: base(message, innerException)
{ }
/// <summary>
/// Creates the specified string value.
/// </summary>
/// <param name="stringValue">The string value.</param>
/// <param name="dstType">Type of the DST.</param>
/// <param name="innerException">The inner exception.</param>
/// <returns></returns>
internal static SettingValueCastException Create(string stringValue, Type dstType, Exception innerException)
{
string msg = $"Failed to convert value '{stringValue}' to type {dstType.FullName}.";
return new SettingValueCastException(msg, innerException);
}
}
}
/* StockStringConverters.cs */
namespace SharpConfig
{
internal sealed class FallbackStringConverter : ITypeStringConverter
{
public string ConvertToString(object value)
{
try
{
var converter = TypeDescriptor.GetConverter(value);
return converter.ConvertToString(null, Configuration.CultureInfo, value);
}
catch (Exception ex)
{
throw SettingValueCastException.Create(value.ToString(), value.GetType(), ex);
}
}
public object ConvertFromString(string value, Type hint)
{
try
{
var converter = TypeDescriptor.GetConverter(hint);
return converter.ConvertFrom(null, Configuration.CultureInfo, value);
}
catch (Exception ex)
{
throw SettingValueCastException.Create(value, hint, ex);
}
}
public Type ConvertibleType => null;
public object TryConvertFromString(string value, Type hint)
{
// Just call ConvertFromString since implementation is already in a try-catch block.
return this.ConvertFromString(value, hint);
}
}
internal sealed class BoolStringConverter : TypeStringConverter<bool>
{
public override string ConvertToString(object value)
=> value.ToString();
public override object TryConvertFromString(string value, Type hint)
{
switch (value.ToLowerInvariant())
{
case "":
case "false":
case "off":
case "no":
case "n":
case "0":
return false;
case "true":
case "on":
case "yes":
case "y":
case "1":
return true;
default:
return null;
}
}
}
internal sealed class ByteStringConverter : TypeStringConverter<byte>
{
public override string ConvertToString(object value)
=> value.ToString();
public override object TryConvertFromString(string value, Type hint)
{
if (value == string.Empty)
{
return default(byte);
}
if (!byte.TryParse(value, NumberStyles.Integer, Configuration.CultureInfo.NumberFormat, out var result))
{
return null;
}
return result;
}
}
internal sealed class CharStringConverter : TypeStringConverter<char>
{
public override string ConvertToString(object value)
=> value.ToString();
public override object TryConvertFromString(string value, Type hint)
{
if (value == string.Empty)
{
return default(char);
}
if (!char.TryParse(value, out var result))
{
return null;
}
return result;
}
}
internal sealed class DateTimeStringConverter : TypeStringConverter<DateTime>
{
public override string ConvertToString(object value)
=> ((DateTime)value).ToString(Configuration.CultureInfo.DateTimeFormat);
public override object TryConvertFromString(string value, Type hint)
{
if (value == string.Empty)
{
return default(DateTime);
}
if (!DateTime.TryParse(value, Configuration.CultureInfo.DateTimeFormat, DateTimeStyles.None, out var result))
{
return null;
}
return result;
}
}
internal sealed class DecimalStringConverter : TypeStringConverter<decimal>
{
public override string ConvertToString(object value)
=> ((decimal)value).ToString(Configuration.CultureInfo.NumberFormat);
public override object TryConvertFromString(string value, Type hint)
{
if (value == string.Empty)
{
return default(decimal);
}
if (!decimal.TryParse(value, NumberStyles.Number, Configuration.CultureInfo.NumberFormat, out var result))
{
return null;
}
return result;
}
}
internal sealed class DoubleStringConverter : TypeStringConverter<double>
{
public override string ConvertToString(object value)
=> ((double)value).ToString(Configuration.CultureInfo.NumberFormat);
public override object TryConvertFromString(string value, Type hint)
{
if (value == string.Empty)
{
return default(double);
}
if (!double.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, Configuration.CultureInfo.NumberFormat, out var result))
{
return null;
}
return result;
}
}
internal sealed class EnumStringConverter : TypeStringConverter<Enum>
{
public override string ConvertToString(object value)
=> value.ToString();
public override object TryConvertFromString(string value, Type hint)
{
if (value == string.Empty)
{
return default(double);
}
value = RemoveTypeNames(value);
return Enum.IsDefined(hint, value) ? Enum.Parse(hint, value) : null;
}
/// <summary>
/// Removes possible type names from a string value.
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
/// <remarks>
/// It's possible that the value is something like:
/// UriFormat.Unescaped
/// We, and especially Enum.Parse do not want this format. Instead, it wants the clean name like:
/// Unescaped
/// </remarks>
private static string RemoveTypeNames(string value)
{
var indexOfLastDot = value.LastIndexOf('.');
if (indexOfLastDot >= 0)
{
value = value.Substring(indexOfLastDot + 1, value.Length - indexOfLastDot - 1).Trim();
}
return value;
}
}
internal sealed class Int16StringConverter : TypeStringConverter<short>
{
public override string ConvertToString(object value)
=> ((short)value).ToString(Configuration.CultureInfo.NumberFormat);
public override object TryConvertFromString(string value, Type hint)
{
if (value == string.Empty)
{
return default(short);
}
if (!short.TryParse(value, NumberStyles.Integer, Configuration.CultureInfo.NumberFormat, out var result))
{
return null;
}
return result;
}
}
internal sealed class Int32StringConverter : TypeStringConverter<int>
{
public override string ConvertToString(object value)
=> ((int)value).ToString(Configuration.CultureInfo.NumberFormat);
public override object TryConvertFromString(string value, Type hint)
{
if (value == string.Empty)
{
return default(int);
}
if (!int.TryParse(value, NumberStyles.Integer, Configuration.CultureInfo.NumberFormat, out var result))
{
return null;
}
return result;
}
}
internal sealed class Int64StringConverter : TypeStringConverter<long>
{
public override string ConvertToString(object value)
=> ((long)value).ToString(Configuration.CultureInfo.NumberFormat);
public override object TryConvertFromString(string value, Type hint)
{
if (value == string.Empty)
{
return default(long);
}
if (!long.TryParse(value, NumberStyles.Integer, Configuration.CultureInfo.NumberFormat, out var result))
{
return null;
}
return result;
}
}
internal sealed class SByteStringConverter : TypeStringConverter<sbyte>
{
public override string ConvertToString(object value)
=> ((sbyte)value).ToString(Configuration.CultureInfo.NumberFormat);
public override object TryConvertFromString(string value, Type hint)
{
if (value == string.Empty)
{
return default(sbyte);
}
if (!sbyte.TryParse(value, NumberStyles.Integer, Configuration.CultureInfo.NumberFormat, out var result))
{
return null;
}
return result;
}
}
internal sealed class SingleStringConverter : TypeStringConverter<float>
{
public override string ConvertToString(object value)
=> ((float)value).ToString(Configuration.CultureInfo.NumberFormat);
public override object TryConvertFromString(string value, Type hint)
{
if (value == string.Empty)
{
return default(float);
}
if (!float.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, Configuration.CultureInfo.NumberFormat, out var result))
{
return null;
}
return result;
}
}
internal sealed class StringStringConverter : TypeStringConverter<string>
{
public override string ConvertToString(object value)
=> value.ToString().Trim('\"');
public override object TryConvertFromString(string value, Type hint)
=> value.Trim('\"');
}
internal sealed class UInt16StringConverter : TypeStringConverter<ushort>
{
public override string ConvertToString(object value)
=> ((ushort)value).ToString(Configuration.CultureInfo.NumberFormat);
public override object TryConvertFromString(string value, Type hint)
{
if (value == string.Empty)
{
return default(ushort);
}
if (!ushort.TryParse(value, NumberStyles.Integer, Configuration.CultureInfo.NumberFormat, out var result))
{
return null;
}
return result;
}
}
internal sealed class UInt32StringConverter : TypeStringConverter<uint>
{
public override string ConvertToString(object value)
=> ((uint)value).ToString(Configuration.CultureInfo.NumberFormat);
public override object TryConvertFromString(string value, Type hint)
{
if (value == string.Empty)
{
return default(uint);
}
if (!uint.TryParse(value, NumberStyles.Integer, Configuration.CultureInfo.NumberFormat, out var result))
{
return null;
}
return result;
}
}
internal sealed class UInt64StringConverter : TypeStringConverter<ulong>
{
public override string ConvertToString(object value)
=> ((ulong)value).ToString(Configuration.CultureInfo.NumberFormat);
public override object TryConvertFromString(string value, Type hint)
{
if (value == string.Empty)
{
return default(ulong);
}
if (!ulong.TryParse(value, NumberStyles.Integer, Configuration.CultureInfo.NumberFormat, out var result))
{
return null;
}
return result;
}
}
}