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