// // Copyright (c) jindongfang. All rights reserved. // 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 { /// /// Represents a configuration. /// Configurations contain one or multiple sections /// that in turn can contain one or multiple settings. /// The class is designed /// to work with classic configuration formats such as /// .ini and .cfg, but is not limited to these. /// public partial class Configuration : IEnumerable
{ #region Fields private static CultureInfo mCultureInfo; private static char mPreferredCommentChar; private static char mArrayElementSeparator; private static Dictionary mTypeStringConverters; internal readonly List
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() { { 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; } /// /// Initializes a new instance of the class. /// public Configuration() { this.mSections = new List
(); } #endregion #region Public Methods /// /// Gets an enumerator that iterates through the configuration. /// /// enumerator public IEnumerator
GetEnumerator() => this.mSections.GetEnumerator(); /// /// Gets an enumerator that iterates through the configuration. /// /// enumerator IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); /// /// Adds a section to the configuration. /// /// The section to add. /// When is null. /// When the section already exists in the configuration. 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); } /// /// Adds a section with a specific name to the configuration. /// /// The name of the section to add. /// The added section. /// When is null or empty. public Section Add(string sectionName) { var section = new Section(sectionName); this.Add(section); return section; } /// /// 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. /// /// The case-sensitive name of the section to remove. /// True if a section with the specified name was removed; false otherwise. /// /// When is null or empty. public bool Remove(string sectionName) { if (string.IsNullOrEmpty(sectionName)) { throw new ArgumentNullException("sectionName"); } return this.Remove(this.FindSection(sectionName)); } /// /// Removes a section from the configuration. /// /// The section to remove. /// True if the section was removed; false otherwise. public bool Remove(Section section) => this.mSections.Remove(section); /// /// Removes all sections that have a specific name. /// /// The case-sensitive name of the sections to remove. /// /// When is null or empty. public void RemoveAllNamed(string sectionName) { if (string.IsNullOrEmpty(sectionName)) { throw new ArgumentNullException("sectionName"); } while (this.Remove(sectionName)) { ; } } /// /// Clears the configuration of all sections. /// public void Clear() => this.mSections.Clear(); /// /// Determines whether a specified section is contained in the configuration. /// /// The section to check for containment. /// True if the section is contained in the configuration; false otherwise. public bool Contains(Section section) => this.mSections.Contains(section); /// /// Determines whether a specifically named section is contained in the configuration. /// /// The name of the section. /// True if the section is contained in the configuration; false otherwise. /// /// When is null or empty. public bool Contains(string sectionName) { if (string.IsNullOrEmpty(sectionName)) { throw new ArgumentNullException("sectionName"); } return this.FindSection(sectionName) != null; } /// /// Determines whether a specifically named section is contained in the configuration, /// and whether that section in turn contains a specifically named setting. /// /// The name of the section. /// The name of the setting. /// True if the section and the respective setting was found; false otherwise. /// /// When or is null or empty. 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); } /// /// Gets the string representation of the configuration. It represents the same contents /// as if the configuration was saved to a file or stream. /// 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(); } } /// /// Registers a type converter to be used for setting value conversions. /// /// The converter to register. /// /// When is null. /// When a converter for the converter's type is already registered. 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); } /// /// Deregisters a type converter from setting value conversion. /// /// The type whose associated converter to deregister. /// /// When is null. /// When no converter is registered for . 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 /// /// Loads a configuration from a file auto-detecting the encoding and /// using the default parsing settings. /// /// /// The location of the configuration file. /// /// /// The loaded object. /// public static Configuration LoadFromFile(string filename) { return LoadFromFile(filename, null); } /// /// Loads a configuration from a file. /// /// /// The location of the configuration file. /// The encoding applied to the contents of the file. Specify null to auto-detect the encoding. /// /// /// The loaded object. /// /// /// When is null or empty. /// When the specified configuration file is not found. 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)); } /// /// Loads a configuration from a text stream auto-detecting the encoding and /// using the default parsing settings. /// /// /// The text stream to load the configuration from. /// /// /// The loaded object. /// public static Configuration LoadFromStream(Stream stream) { return LoadFromStream(stream, null); } /// /// Loads a configuration from a text stream. /// /// /// The text stream to load the configuration from. /// The encoding applied to the contents of the stream. Specify null to auto-detect the encoding. /// /// /// The loaded object. /// /// /// When is null. 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); } /// /// Loads a configuration from text (source code). /// /// /// The text (source code) of the configuration. /// /// /// The loaded object. /// /// /// When is null. public static Configuration LoadFromString(string source) { if (source == null) { throw new ArgumentNullException("source"); } return ConfigurationReader.ReadFromString(source); } #endregion #region LoadBinary /// /// Loads a configuration from a binary file using the default . /// /// /// The location of the configuration file. /// /// /// The loaded configuration. /// /// /// When is null or empty. public static Configuration LoadFromBinaryFile(string filename) { return LoadFromBinaryFile(filename, null); } /// /// Loads a configuration from a binary file using a specific . /// /// /// The location of the configuration file. /// The reader to use. Specify null to use the default . /// /// /// The loaded configuration. /// /// /// When is null or empty. 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); } } /// /// Loads a configuration from a binary stream, using the default . /// /// /// The stream to load the configuration from. /// /// /// The loaded configuration. /// /// /// When is null. public static Configuration LoadFromBinaryStream(Stream stream) { return LoadFromBinaryStream(stream, null); } /// /// Loads a configuration from a binary stream, using a specific . /// /// /// The stream to load the configuration from. /// The reader to use. Specify null to use the default . /// /// /// The loaded configuration. /// /// /// When is null. public static Configuration LoadFromBinaryStream(Stream stream, BinaryReader reader) { if (stream == null) { throw new ArgumentNullException("stream"); } return ConfigurationReader.ReadFromBinaryStream(stream, reader); } #endregion #region Save /// /// Saves the configuration to a file using the default character encoding, which is UTF8. /// /// /// The location of the configuration file. public void SaveToFile(string filename) => this.SaveToFile(filename, null); /// /// Saves the configuration to a file. /// /// /// The location of the configuration file. /// The character encoding to use. Specify null to use the default encoding, which is UTF8. /// /// When is null or empty. 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); } } /// /// Saves the configuration to a stream using the default character encoding, which is UTF8. /// /// /// The stream to save the configuration to. public void SaveToStream(Stream stream) => this.SaveToStream(stream, null); /// /// Saves the configuration to a stream. /// /// /// The stream to save the configuration to. /// The character encoding to use. Specify null to use the default encoding, which is UTF8. /// /// When is null. public void SaveToStream(Stream stream, Encoding encoding) { if (stream == null) { throw new ArgumentNullException("stream"); } ConfigurationWriter.WriteToStreamTextual(this, stream, encoding); } #endregion #region SaveBinary /// /// Saves the configuration to a binary file, using the default . /// /// /// The location of the configuration file. public void SaveToBinaryFile(string filename) => this.SaveToBinaryFile(filename, null); /// /// Saves the configuration to a binary file, using a specific . /// /// /// The location of the configuration file. /// The writer to use. Specify null to use the default writer. /// /// When is null or empty. 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); } } /// /// Saves the configuration to a binary stream, using the default . /// /// /// The stream to save the configuration to. public void SaveToBinaryStream(Stream stream) => this.SaveToBinaryStream(stream, null); /// /// Saves the configuration to a binary file, using a specific . /// /// /// The stream to save the configuration to. /// The writer to use. Specify null to use the default writer. /// /// When is null. public void SaveToBinaryStream(Stream stream, BinaryWriter writer) { if (stream == null) { throw new ArgumentNullException("stream"); } ConfigurationWriter.WriteToStreamBinary(this, stream, writer); } #endregion #region Properties /// /// Gets or sets the CultureInfo that is used for value conversion in SharpConfig. /// The default value is CultureInfo.InvariantCulture. /// /// /// When a null reference is set. public static CultureInfo CultureInfo { get => mCultureInfo; set => mCultureInfo = value ?? throw new ArgumentNullException("value"); } /// /// Gets the array that contains all valid comment delimiting characters. /// The current value is { '#', ';' }. /// public static char[] ValidCommentChars { get; private set; } /// /// Gets or sets the preferred comment char when saving configurations. /// The default value is '#'. /// /// /// When an invalid character is set. 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; } } /// /// Gets or sets the array element separator character for settings. /// The default value is ','. /// NOTE: remember that after you change this value while instances exist, /// to expect their ArraySize and other array-related values to return different values. /// /// /// When a zero-character ('\0') is set. public static char ArrayElementSeparator { get { return mArrayElementSeparator; } set { if (value == '\0') { throw new ArgumentException("Zero-character is not allowed."); } mArrayElementSeparator = value; } } /// /// 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 /// public static bool OutputRawStringValues { get; set; } /// /// Gets or sets a value indicating whether inline-comments /// should be ignored when parsing a configuration. /// public static bool IgnoreInlineComments { get; set; } /// /// Gets or sets a value indicating whether pre-comments /// should be ignored when parsing a configuration. /// public static bool IgnorePreComments { get; set; } /// /// Gets or sets a value indicating whether space between /// equals should be added when creating a configuration. /// public static bool SpaceBetweenEquals { get; set; } /// /// Gets the number of sections that are in the configuration. /// public int SectionCount => this.mSections.Count; /// /// Gets or sets a section by index. /// /// The index of the section in the configuration. /// /// /// The section at the specified index. /// Note: no section is created when using this accessor. /// /// /// When the index is out of range. public Section this[int index] { get { if (index < 0 || index >= this.mSections.Count) { throw new ArgumentOutOfRangeException("index"); } return this.mSections[index]; } } /// /// 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. /// /// /// The case-sensitive name of the section. /// /// /// The section if found, otherwise a new section with /// the specified name is created, added to the configuration and returned. /// public Section this[string name] { get { var section = this.FindSection(name); if (section == null) { section = new Section(name); this.Add(section); } return section; } } /// /// Gets the default, hidden section. /// public Section DefaultSection => this[Section.DefaultSectionName]; /// /// Gets all sections that have a specific name. /// /// The case-sensitive name of the sections. /// /// The found sections. /// public IEnumerable
GetSectionsNamed(string name) { var sections = new List
(); 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 { /// /// Represents the base class of all elements /// that exist in a , /// such as sections and settings. /// public abstract class ConfigurationElement { internal ConfigurationElement(string name) { if (string.IsNullOrEmpty(name)) { throw new ArgumentNullException("name"); } this.Name = name; } /// /// Gets the name of this element. /// public string Name { get; } /// /// Gets or sets the comment of this element. /// public string Comment { get; set; } /// /// Gets the comment above this element. /// public string PreComment { get; set; } /// /// Gets the string representation of the element. /// /// 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)); } /// /// Gets the element's expression as a string. /// An example for a section would be "[Section]". /// /// The element's expression as a string. 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) [] // 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) = // may not contain a '=' // 2) "" = // 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 { /// /// 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. /// public sealed class IgnoreAttribute : Attribute { } } /* ITypeStringConverter.cs */ namespace SharpConfig { /// /// Defines a type-to-string and string-to-type converter /// that is used for the conversion of setting values. /// public interface ITypeStringConverter { /// /// Converts an object to its string representation. /// /// The value to convert. /// The object's string representation. string ConvertToString(object value); /// /// The type that this converter is able to convert to and from a string. /// Type ConvertibleType { get; } /// /// Tries to convert a string value to an object of this converter's type. /// /// /// /// 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. /// /// The converted object, or null if conversion is not possible. object TryConvertFromString(string value, Type hint); } /// /// Represents a type-to-string and string-to-type converter /// that is used for the conversion of setting values. /// /// The type that this converter is able to convert. public abstract class TypeStringConverter : ITypeStringConverter { /// /// Converts an object to its string representation. /// /// The value to convert. /// The object's string representation. public abstract string ConvertToString(object value); /// /// The type that this converter is able to convert to and from a string. /// public Type ConvertibleType => typeof(T); /// /// Tries to convert a string value to an object of this converter's type. /// /// /// /// 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. /// /// The converted object, or null if conversion is not possible. public abstract object TryConvertFromString(string value, Type hint); } } /* ParserException.cs */ namespace SharpConfig { /// /// Represents an error that occurred during /// the configuration parsing stage. /// [Serializable] public sealed class ParserException : Exception { internal ParserException(string message, int line) : base($"Line {line}: {message}") { this.Line = line; } /// /// Gets the line in the configuration that caused the exception. /// public int Line { get; private set; } } } /* Section.cs */ namespace SharpConfig { /// /// Represents a group of objects. /// public sealed class Section : ConfigurationElement, IEnumerable { /// /// The name of the default, hidden section. /// public const string DefaultSectionName = "$SharpConfigDefaultSection"; private readonly List mSettings; /// /// Initializes a new instance of the class. /// /// /// The name of the section. public Section(string name) : base(name) { this.mSettings = new List(); } /// /// Creates a new instance of the 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 attribute /// or are of a type that is marked with that attribute, are ignored. /// /// The name of the section. /// /// The newly created section. /// /// When is null or empty. /// When is null. 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; } /// /// 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 attribute /// or are of a type that is marked with that attribute, are ignored. /// /// /// /// The type of object to create. /// Note: the type must be default-constructible, meaning it has a public default constructor. /// /// /// The created object. /// /// /// The specified type must have a public default constructor /// in order to be created. /// public T ToObject() where T : new() { var obj = Activator.CreateInstance(); this.SetValuesTo(obj); return obj; } /// /// 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 attribute /// or are of a type that is marked with that attribute, are ignored. /// /// /// /// The type of object to create. /// Note: the type must be default-constructible, meaning it has a public default constructor. /// /// /// The created object. /// /// /// The specified type must have a public default constructor /// in order to be created. /// /// /// When is null. public object ToObject(Type type) { if (type == null) { throw new ArgumentNullException(type.Name); } var obj = Activator.CreateInstance(type); this.SetValuesTo(obj); return obj; } /// /// Assigns the values of an object's public properties and fields to the corresponding /// already existing settings in this section. /// Properties and fields that are marked with the attribute /// or are of a type that is marked with that attribute, are ignored. /// /// /// The object from which the values are obtained. /// /// When is null. 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); } } /// /// Assigns the values of this section to an object's public properties and fields. /// Properties and fields that are marked with the attribute /// or are of a type that is marked with that attribute, are ignored. /// /// /// The object that is modified based on the section. /// /// When is null. 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; } /// /// Gets an enumerator that iterates through the section. /// public IEnumerator GetEnumerator() => this.mSettings.GetEnumerator(); /// /// Gets an enumerator that iterates through the section. /// IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); /// /// Adds a setting to the section. /// /// The setting to add. /// /// When is null. /// When the specified setting already exists in the section. 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); } /// /// Adds a setting with a specific name and empty value to the section. /// /// The name of the setting to add. /// The added setting. /// When is null or empty. public Setting Add(string settingName) => this.Add(settingName, string.Empty); /// /// Adds a setting with a specific name and value to the section. /// /// The name of the setting to add. /// The initial value of the setting to add. /// The added setting. /// When is null or empty. public Setting Add(string settingName, object settingValue) { var setting = new Setting(settingName, settingValue); this.Add(setting); return setting; } /// /// 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. /// /// The case-sensitive name of the setting to remove. /// True if a setting with the specified name was removed; false otherwise. /// /// When is null or empty. public bool Remove(string settingName) { if (string.IsNullOrEmpty(settingName)) { throw new ArgumentNullException("settingName"); } return this.Remove(this.FindSetting(settingName)); } /// /// Removes a setting from the section. /// /// The setting to remove. /// True if the setting was removed; false otherwise. public bool Remove(Setting setting) => this.mSettings.Remove(setting); /// /// Removes all settings that have a specific name. /// /// The case-sensitive name of the settings to remove. /// /// When is null or empty. public void RemoveAllNamed(string settingName) { if (string.IsNullOrEmpty(settingName)) { throw new ArgumentNullException("settingName"); } while (this.Remove(settingName)) { ; } } /// /// Clears the section of all settings. /// public void Clear() => this.mSettings.Clear(); /// /// Determines whether a specified setting is contained in the section. /// /// The setting to check for containment. /// True if the setting is contained in the section; false otherwise. public bool Contains(Setting setting) => this.mSettings.Contains(setting); /// /// Determines whether a specifically named setting is contained in the section. /// /// The case-sensitive name of the setting. /// True if the setting is contained in the section; false otherwise. /// /// When is null or empty. public bool Contains(string settingName) { if (string.IsNullOrEmpty(settingName)) { throw new ArgumentNullException("settingName"); } return this.FindSetting(settingName) != null; } /// /// Gets the number of settings that are in the section. /// public int SettingCount => this.mSettings.Count; /// /// Gets or sets a setting by index. /// /// The index of the setting in the section. /// /// /// The setting at the specified index. /// Note: no setting is created when using this accessor. /// /// /// When is out of range. public Setting this[int index] { get { if (index < 0 || index >= this.mSettings.Count) { throw new ArgumentOutOfRangeException("index"); } return this.mSettings[index]; } } /// /// 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. /// /// /// The case-sensitive name of the setting. /// /// /// The setting if found, otherwise a new setting with /// the specified name is created, added to the section and returned. /// public Setting this[string name] { get { var setting = this.FindSetting(name); if (setting == null) { setting = new Setting(name); this.mSettings.Add(setting); } return setting; } } /// /// Gets all settings that have a specific name. /// /// The case-sensitive name of the settings. /// /// The found settings. /// public IEnumerable GetSettingsNamed(string name) { var settings = new List(); 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; } /// /// Gets the element's expression as a string. /// An example for a section would be "[Section]". /// /// The element's expression as a string. protected override string GetStringExpression() => $"[{this.Name}]"; } } /* Setting.cs */ namespace SharpConfig { /// /// Represents a setting in a . /// Settings are always stored in a . /// public sealed class Setting : ConfigurationElement { #region Fields private int mCachedArraySize; private bool mShouldCalculateArraySize; private char mCachedArrayElementSeparator; #endregion #region Construction /// /// Initializes a new instance of the class. /// public Setting(string name) : this(name, string.Empty) { } /// /// Initializes a new instance of the class. /// /// /// The name of the setting. /// The value of the setting. public Setting(string name, object value) : base(name) { this.SetValue(value); this.mCachedArrayElementSeparator = Configuration.ArrayElementSeparator; } #endregion #region Properties /// /// Gets a value indicating whether this setting's value is empty. /// public bool IsEmpty => string.IsNullOrEmpty(this.RawValue); /// /// Gets the value of this setting as a , with quotes removed if present. /// [Obsolete("Use StringValue instead")] public string StringValueTrimmed { get { string value = this.StringValue; if (value[0] == '\"') { value = value.Trim('\"'); } return value; } } /// /// Gets or sets the raw value of this setting. /// public string RawValue { get; set; } = string.Empty; /// /// Gets or sets the value of this setting as a . /// Note: this is a shortcut to GetValue and SetValue. /// public string StringValue { get => this.GetValue().Trim('\"'); set => this.SetValue(value.Trim('\"')); } /// /// Gets or sets the value of this setting as a array. /// Note: this is a shortcut to GetValueArray and SetValue. /// public string[] StringValueArray { get => this.GetValueArray(); set => this.SetValue(value); } /// /// Gets or sets the value of this setting as an . /// Note: this is a shortcut to GetValue and SetValue. /// public int IntValue { get => this.GetValue(); set => this.SetValue(value); } /// /// Gets or sets the value of this setting as an array. /// Note: this is a shortcut to GetValueArray and SetValue. /// public int[] IntValueArray { get => this.GetValueArray(); set => this.SetValue(value); } /// /// Gets or sets the value of this setting as a . /// Note: this is a shortcut to GetValue and SetValue. /// public float FloatValue { get => this.GetValue(); set => this.SetValue(value); } /// /// Gets or sets the value of this setting as a array. /// Note: this is a shortcut to GetValueArray and SetValue. /// public float[] FloatValueArray { get => this.GetValueArray(); set => this.SetValue(value); } /// /// Gets or sets the value of this setting as a . /// Note: this is a shortcut to GetValue and SetValue. /// public double DoubleValue { get => this.GetValue(); set => this.SetValue(value); } /// /// Gets or sets the value of this setting as a array. /// Note: this is a shortcut to GetValueArray and SetValue. /// public double[] DoubleValueArray { get => this.GetValueArray(); set => this.SetValue(value); } /// /// Gets or sets the value of this setting as a . /// Note: this is a shortcut to GetValue and SetValue. /// public decimal DecimalValue { get => this.GetValue(); set => this.SetValue(value); } /// /// Gets or sets the value of this setting as a array. /// Note: this is a shortcut to GetValueArray and SetValue. /// public decimal[] DecimalValueArray { get => this.GetValueArray(); set => this.SetValue(value); } /// /// Gets or sets the value of this setting as a . /// Note: this is a shortcut to GetValue and SetValue. /// public bool BoolValue { get => this.GetValue(); set => this.SetValue(value); } /// /// Gets or sets the value of this setting as a array. /// Note: this is a shortcut to GetValueArray and SetValue. /// public bool[] BoolValueArray { get => this.GetValueArray(); set => this.SetValue(value); } /// /// Gets or sets the value of this settings as a . /// Note: this is a shortcut to GetValue and SetValue. /// public DateTime DateTimeValue { get => this.GetValue(); set => this.SetValue(value); } /// /// Gets or sets the value of this setting as a array. /// Note: this is a shortcut to GetValueArray and SetValue. /// public DateTime[] DateTimeValueArray { get => this.GetValueArray(); set => this.SetValue(value); } /// /// Gets or sets the value of this setting as a . /// Note: this is a shortcut to GetValueArray and SetValue. /// public byte ByteValue { get => this.GetValue(); set => this.SetValue(value); } /// /// Gets or sets the value of this setting as a array. /// Note: this is a shortcut to GetValueArray and SetValue. /// public byte[] ByteValueArray { get => this.GetValueArray(); set => this.SetValue(value); } /// /// Gets or sets the value of this setting as a . /// Note: this is a shortcut to GetValueArray and SetValue. /// public sbyte SByteValue { get => this.GetValue(); set => this.SetValue(value); } /// /// Gets or sets the value of this setting as a array. /// Note: this is a shortcut to GetValueArray and SetValue. /// public sbyte[] SByteValueArray { get => this.GetValueArray(); set => this.SetValue(value); } /// /// Gets or sets the value of this setting as a . /// Note: this is a shortcut to GetValueArray and SetValue. /// public char CharValue { get => this.GetValue(); set => this.SetValue(value); } /// /// Gets or sets the value of this setting as a array. /// Note: this is a shortcut to GetValueArray and SetValue. /// 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(); } } /// /// Gets a value indicating whether this setting is an array. /// public bool IsArray => (this.ArraySize >= 0); /// /// Gets the size of the array that this setting represents. /// If this setting is not an array, -1 is returned. /// 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 /// /// Gets this setting's value as a specific type. /// /// /// The type of the object to retrieve. /// /// When is null. /// When is an array type. /// When the setting represents an array. 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); } /// /// 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. /// /// /// 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. /// /// The values of this setting as an array. 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; } /// /// Gets this setting's value as a specific type. /// /// /// The type of the object to retrieve. /// /// When is an array type. /// When the setting represents an array. public T GetValue() { 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); } /// /// 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. /// /// /// 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. /// /// The values of this setting as an array. public T[] GetValueArray() { 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; } /// /// Gets this setting's value as a specific type, or a specified default value /// if casting the setting to the type fails. /// /// /// Default value if casting the setting to the specified type fails. /// /// /// If true, and casting the setting to the specified type fails, is set /// as this setting's new value. /// /// The type of the object to retrieve. public T GetValueOrDefault(T defaultValue, bool setDefault = false) { var type = typeof(T); if (type.IsArray) { throw new InvalidOperationException("GetValueOrDefault 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(). } // 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 /// /// Sets the value of this setting via an object. /// /// /// The value to set. 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; } /// /// Gets the element's expression as a string. /// An example for a section would be "[Section]". /// /// The element's expression as a string. 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 { /// /// Represents an error that occurs when a string value could not be converted to a specific instance. /// [Serializable] public sealed class SettingValueCastException : Exception { private SettingValueCastException(string message, Exception innerException) : base(message, innerException) { } /// /// Creates the specified string value. /// /// The string value. /// Type of the DST. /// The inner exception. /// 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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; } /// /// Removes possible type names from a string value. /// /// /// /// /// 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 /// 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 { 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 { 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 { 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 { 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 { 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 { public override string ConvertToString(object value) => value.ToString().Trim('\"'); public override object TryConvertFromString(string value, Type hint) => value.Trim('\"'); } internal sealed class UInt16StringConverter : TypeStringConverter { 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 { 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 { 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; } } }