From 871c4877fa608e5ce1b71ed2c6b7bb5bd12ec9c8 Mon Sep 17 00:00:00 2001 From: "Brett V. Forsgren" Date: Tue, 6 Aug 2024 16:05:53 -0600 Subject: [PATCH] only operate on strings --- src/IxMilia.Config.Test/ConfigEscapeTests.cs | 19 ++ src/IxMilia.Config.Test/ConfigParseTests.cs | 104 +------ src/IxMilia.Config.Test/ConfigReaderTests.cs | 8 +- .../ConfigSerializeTests.cs | 74 ----- .../ConfigToStringTests.cs | 45 --- src/IxMilia.Config.Test/ConfigWriterTests.cs | 8 +- src/IxMilia.Config.Test/Enums.cs | 22 -- src/IxMilia.Config/ConfigExtensions.cs | 294 +----------------- src/IxMilia.Config/IxMilia.Config.csproj | 4 + 9 files changed, 57 insertions(+), 521 deletions(-) create mode 100644 src/IxMilia.Config.Test/ConfigEscapeTests.cs delete mode 100644 src/IxMilia.Config.Test/ConfigSerializeTests.cs delete mode 100644 src/IxMilia.Config.Test/ConfigToStringTests.cs delete mode 100644 src/IxMilia.Config.Test/Enums.cs diff --git a/src/IxMilia.Config.Test/ConfigEscapeTests.cs b/src/IxMilia.Config.Test/ConfigEscapeTests.cs new file mode 100644 index 0000000..e6a7600 --- /dev/null +++ b/src/IxMilia.Config.Test/ConfigEscapeTests.cs @@ -0,0 +1,19 @@ +// Copyright (c) IxMilia. All Rights Reserved. + +using Xunit; + +namespace IxMilia.Config.Test +{ + public class ConfigEscapeTests + { + [Theory] + [InlineData("abcd", "abcd")] + [InlineData("ab cd", "ab cd")] + [InlineData("ab\"cd", "\"ab\\\"cd\"")] + public void VerifySerialize(string value, string expected) + { + var actual = ConfigExtensions.EscapeString(value); + Assert.Equal(expected, actual); + } + } +} diff --git a/src/IxMilia.Config.Test/ConfigParseTests.cs b/src/IxMilia.Config.Test/ConfigParseTests.cs index 46c81c7..b382bcc 100644 --- a/src/IxMilia.Config.Test/ConfigParseTests.cs +++ b/src/IxMilia.Config.Test/ConfigParseTests.cs @@ -1,106 +1,20 @@ // Copyright (c) IxMilia. All Rights Reserved. -using System.Collections.Generic; using Xunit; namespace IxMilia.Config.Test { public class ConfigParseTests { - private void VerifyParse(T expected, string value) - { - var dict = new Dictionary(); - dict["key"] = value; - T result; - Assert.True(dict.TryParseValue("key", out result)); - Assert.Equal(expected, result); - } - - private void VerifyParseFail(string value) - { - var dict = new Dictionary(); - dict["key"] = value; - T result; - Assert.False(dict.TryParseValue("key", out result)); - } - - [Fact] - public void ParseDoubleTest() - { - VerifyParse(3.14, "3.14"); - } - - [Fact] - public void ParseAssignDoubleTest() - { - var dbl = 1.0; - "2.0".TryParseAssign(ref dbl); - Assert.Equal(2.0, dbl); - } - - [Fact] - public void AssignDoubleFromDictionaryTest() - { - var dict = new Dictionary(); - dict["key"] = "2.0"; - var dbl = 1.0; - dict.TryParseAssign("key", ref dbl); - Assert.Equal(2.0, dbl); - } - - [Fact] - public void ParseStringNotQuotedTest() - { - VerifyParse("some string", "some string"); - } - - [Fact] - public void ParseStringNotQuotedSameStartAndEndCharacterTest() - { - VerifyParse("abba", "abba"); - } - - [Fact] - public void ParseQuotedStringTest() - { - VerifyParse("final\nvalue", @"""final\nvalue"""); - } - - [Fact] - public void ParseDoubleFailTest() - { - VerifyParseFail("three"); - } - - [Fact] - public void NoParserTest() - { - // System.Object has no Parse() method - VerifyParseFail("foo"); - } - - [Fact] - public void ParseEnumTest() - { - VerifyParse(Numeros.Dos, "Dos"); - } - - [Fact] - public void ParseEnumFlagsTest() - { - VerifyParse(Flags.IsAlpha | Flags.IsBeta, "IsAlpha|IsBeta"); - } - - [Fact] - public void ParseEnumFailTest() - { - VerifyParseFail("Cinco"); - } - - [Fact] - public void ParseArrayTest() - { - VerifyParse(new[] { 1.0, 2.0 }, "1.0;2.0"); + [Theory] + [InlineData("some string", "some string")] // regular string + [InlineData("abba", "abba")] // first and last characters identical, but not quotes + [InlineData(@"'final\nvalue'", "final\nvalue")] // single-quoted string + [InlineData(@"""final\nvalue""", "final\nvalue")] // double-quoted string + public void VerifyParse(string value, string expected) + { + var actual = ConfigExtensions.ParseString(value); + Assert.Equal(expected, actual); } } } diff --git a/src/IxMilia.Config.Test/ConfigReaderTests.cs b/src/IxMilia.Config.Test/ConfigReaderTests.cs index c909663..3238a77 100644 --- a/src/IxMilia.Config.Test/ConfigReaderTests.cs +++ b/src/IxMilia.Config.Test/ConfigReaderTests.cs @@ -19,7 +19,7 @@ private IDictionary Parse(string data) [Fact] public void SimpleParseTest() { - var dict = Parse(@" + var dict = Parse(""" ; comment rootValue = true @@ -28,12 +28,12 @@ public void SimpleParseTest() key = value [section.deeperSection] -key = value2 -"); +key = "quoted\nstring" +"""); Assert.Equal(3, dict.Keys.Count); Assert.Equal("true", dict["rootValue"]); Assert.Equal("value", dict["section.key"]); - Assert.Equal("value2", dict["section.deeperSection.key"]); + Assert.Equal("quoted\nstring", dict["section.deeperSection.key"]); } } } diff --git a/src/IxMilia.Config.Test/ConfigSerializeTests.cs b/src/IxMilia.Config.Test/ConfigSerializeTests.cs deleted file mode 100644 index b458813..0000000 --- a/src/IxMilia.Config.Test/ConfigSerializeTests.cs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) IxMilia. All Rights Reserved. - -using System.Collections.Generic; -using System.Linq; -using Xunit; - -namespace IxMilia.Config.Test -{ - public class ConfigSerializeTests - { - private void VerifySerialize(string expected, TestClass tc) - { - string actual = tc.SerializeConfig(); - Assert.Equal(expected.Replace("\r", "").Trim(), actual.Replace("\r", "").Trim()); - } - - [Fact] - public void SimpleSerializeTest() - { - var tc = new TestClass() - { - DoubleValue = 2.0, - EnumValue = Numeros.Quatro, - Integers = new[] { 1, 2, 3 }, - }; - VerifySerialize(@" -DoubleValue = 2 -Integers = 1;2;3 - -[enums] -numeros = Quatro -", tc); - } - - [Fact] - public void SimpleDeserializeTest() - { - var str = @" -DoubleValue = 2 -Integers = 1;2;3 - -[enums] -numeros = Quatro -"; - var tc = new TestClass(); - tc.DeserializeConfig(str.Split('\n').Select(line => line.TrimEnd('\r')).ToArray()); - Assert.Equal(2.0, tc.DoubleValue); - Assert.Equal(new[] { 1, 2, 3 }, tc.Integers); - Assert.Equal(Numeros.Quatro, tc.EnumValue); - } - - [Fact] - public void DeserializePropertyTest() - { - var tc = new TestClass(); - Assert.Equal(0.0, tc.DoubleValue); - Assert.Equal((Numeros)0, tc.EnumValue); - tc.DeserializeProperty("DoubleValue", "2.0"); - tc.DeserializeProperty("enums.numeros", "Quatro"); - Assert.Equal(2.0, tc.DoubleValue); - Assert.Equal(Numeros.Quatro, tc.EnumValue); - } - - private class TestClass - { - public double DoubleValue { get; set; } - - [ConfigPath("enums.numeros")] - public Numeros EnumValue { get; set; } - - public int[] Integers { get; set; } - } - } -} diff --git a/src/IxMilia.Config.Test/ConfigToStringTests.cs b/src/IxMilia.Config.Test/ConfigToStringTests.cs deleted file mode 100644 index 88aed24..0000000 --- a/src/IxMilia.Config.Test/ConfigToStringTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) IxMilia. All Rights Reserved. - -using Xunit; - -namespace IxMilia.Config.Test -{ - public class ConfigToStringTests - { - private void Verify(string expected, T value) - { - var actual = value.ToConfigString(); - Assert.Equal(expected, actual); - } - - [Fact] - public void DoubleToConfigStringTest() - { - Verify("2", 2.0); - } - - [Fact] - public void StringToConfigStringTest() - { - Verify(@"\""final\nvalue\""", "\"final\nvalue\""); - } - - [Fact] - public void EnumToConfigStringTest() - { - Verify("Dos", Numeros.Dos); - } - - [Fact] - public void FlagsEnumToConfigStringTest() - { - Verify("IsAlpha|IsBeta", Flags.IsAlpha | Flags.IsBeta); - } - - [Fact] - public void ArrayToConfigStringTest() - { - Verify("1;2", new[] { 1.0, 2.0 }); - } - } -} diff --git a/src/IxMilia.Config.Test/ConfigWriterTests.cs b/src/IxMilia.Config.Test/ConfigWriterTests.cs index b7f4b3c..4cd222e 100644 --- a/src/IxMilia.Config.Test/ConfigWriterTests.cs +++ b/src/IxMilia.Config.Test/ConfigWriterTests.cs @@ -22,21 +22,21 @@ public void WriteToEmptyFileTest() var dict = new Dictionary() { { "rootValue", "true" }, - { "section.key2", "value2" }, + { "section.key2", "quoted\nstring" }, { "section.key1", "value1" }, { "section.deeperSection.key", "valueDeeper" }, }; - AssertWritten(dict, @" + AssertWritten(dict, """ rootValue = true [section] key1 = value1 -key2 = value2 +key2 = "quoted\nstring" [section.deeperSection] key = valueDeeper -"); +"""); } [Fact] diff --git a/src/IxMilia.Config.Test/Enums.cs b/src/IxMilia.Config.Test/Enums.cs deleted file mode 100644 index 01c6636..0000000 --- a/src/IxMilia.Config.Test/Enums.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) IxMilia. All Rights Reserved. - -using System; - -namespace IxMilia.Config.Test -{ - internal enum Numeros - { - Uno, - Dos, - Tres, - Quatro, - } - - [Flags] - internal enum Flags - { - IsAlpha = 1, - IsBeta = 2, - IsGamma = 4, - } -} diff --git a/src/IxMilia.Config/ConfigExtensions.cs b/src/IxMilia.Config/ConfigExtensions.cs index 95742a0..64361b1 100644 --- a/src/IxMilia.Config/ConfigExtensions.cs +++ b/src/IxMilia.Config/ConfigExtensions.cs @@ -2,24 +2,11 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; -using System.Reflection; using System.Text; namespace IxMilia.Config { - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] - public class ConfigPathAttribute : Attribute - { - public string Path { get; } - - public ConfigPathAttribute(string path) - { - Path = path; - } - } - public static class ConfigExtensions { private static char[] Separator = new[] { '=' }; @@ -177,265 +164,6 @@ public static string WriteConfig(this IDictionary dictionary, pa return result; } - public static bool TryParseValue(this string str, out T result) - { - return str.TryParseValue(GetParseFunction(), out result); - } - - public static bool TryParseValue(this string str, Func parser, out T result) - { - result = default(T); - if (parser == null) - { - return false; - } - - try - { - result = parser(str); - } - catch - { - return false; - } - - return true; - } - - public static void TryParseAssign(this string str, ref T target) - { - str.TryParseAssign(GetParseFunction(), ref target); - } - - public static void TryParseAssign(this string str, Func parser, ref T target) - { - T result; - if (str.TryParseValue(parser, out result)) - { - target = result; - } - } - - public static bool TryParseValue(this IDictionary dictionary, string key, out T result) - { - return dictionary.TryParseValue(key, GetParseFunction(), out result); - } - - public static bool TryParseValue(this IDictionary dictionary, string key, Func parser, out T result) - { - string value; - if (dictionary.TryGetValue(key, out value)) - { - return value.TryParseValue(parser, out result); - } - else - { - result = default(T); - return false; - } - } - - public static void TryParseAssign(this IDictionary dictionary, string key, ref T target) - { - dictionary.TryParseAssign(key, GetParseFunction(), ref target); - } - - public static void TryParseAssign(this IDictionary dictionary, string key, Func parser, ref T target) - { - T result; - if (dictionary.TryParseValue(key, parser, out result)) - { - target = result; - } - } - - public static string ToConfigString(this T value) - { - if ((object)value is null) - { - return null; - } - - var toString = GetToStringFunction(value.GetType()); - return toString?.Invoke(value); - } - - public static void InsertConfigValue(this IDictionary dictionary, string key, T value) - { - dictionary[key] = value.ToConfigString(); - } - - public static void DeserializeConfig(this T value, params string[] lines) - { - var dictionary = new Dictionary(); - dictionary.ParseConfig(lines); - foreach (var key in dictionary.Keys) - { - value.DeserializeProperty(key, dictionary[key]); - } - } - - public static string SerializeConfig(this T value, params string[] existingLines) - { - var dict = new Dictionary(); - foreach (var property in typeof(T).GetRuntimeProperties()) - { - var configPath = property.GetCustomAttribute(); - var key = configPath?.Path ?? property.Name; - dict[key] = property.GetValue(value).ToConfigString(); - } - - return dict.WriteConfig(existingLines); - } - - public static void DeserializeProperty(this T parentObject, string key, string value) - { - var property = (from prop in typeof(T).GetRuntimeProperties() - let configPath = prop.GetCustomAttribute() - let path = configPath?.Path ?? prop.Name - where path == key - select prop).FirstOrDefault(); - if (property != null) - { - // a terrible hack to get the appropriate generic method - var getParseFunction = typeof(ConfigExtensions).GetRuntimeMethods().Single(m => m.Name == nameof(GetParseFunction)); - getParseFunction = getParseFunction.MakeGenericMethod(property.PropertyType); - var parser = getParseFunction.Invoke(null, new object[0]); - var parseInvoke = parser.GetType().GetRuntimeMethod("Invoke", new[] { typeof(string) }); - try - { - var result = parseInvoke.Invoke(parser, new object[] { value }); - property.SetValue(parentObject, result); - } - catch - { - } - } - } - - private static Func GetParseFunction() - { - var parser = GetParseFunctionSimple(typeof(T)); - if (typeof(T).IsArray) - { - // when creating an array, each element must be manually copied over - return str => - { - var items = (object[])parser(str); - var elementType = typeof(T).GetElementType(); - var array = Array.CreateInstance(elementType, items.Length); - Array.Copy(items, array, items.Length); - return (T)((object)array); - }; - } - - return x => (T)parser(x); - } - - private static Func GetParseFunctionSimple(Type type) - { - // try to find a Parse() method to use - Func parser = null; - if (type == typeof(string)) - { - parser = value => ParseString(value); - } - else if (type.GetTypeInfo().IsEnum) - { - parser = value => value.Split('|').Select(v => (int)Enum.Parse(type, v.Trim())).Aggregate((a, b) => a | b); - } - else if (type.IsArray) - { - var elementType = type.GetElementType(); - var elementParser = GetParseFunctionSimple(elementType); - parser = str => str.Split(';').Select(s => elementParser(s)).ToArray(); - } - else - { - // use reflection to find a Parse() method, first trying for one that also takes an IFormatProvider - var parseMethod = type.GetRuntimeMethod("Parse", new[] { typeof(string), typeof(IFormatProvider) }); - if (parseMethod != null && parseMethod.IsStatic) - { - parser = value => parseMethod.Invoke(null, new object[] { value, CultureInfo.InvariantCulture }); - } - - if (parser == null) - { - // otherwise look for the string-only version - parseMethod = type.GetRuntimeMethod("Parse", new[] { typeof(string) }); - if (parseMethod != null && parseMethod.IsStatic) - { - parser = value => parseMethod.Invoke(null, new object[] { value }); - } - } - } - - return parser; - } - - private static Func GetToStringFunction(Type type) - { - Func toString = null; - if (type == typeof(string)) - { - toString = value => EscapeString((string)value); - } - else if (type.GetTypeInfo().IsEnum) - { - toString = value => - { - var enm = (Enum)value; - - // first try a simple `.ToString()` call - var simple = enm.ToString(); - if (Enum.GetNames(type).Contains(simple)) - { - return simple; - } - - // otherwise construct it from the flags - var flags = new List(); - foreach (Enum flag in Enum.GetValues(type)) - { - if (enm.HasFlag(flag)) - { - flags.Add(Enum.GetName(type, flag)); - } - } - - return String.Join("|", flags); - }; - } - else if (type.IsArray) - { - var elementType = type.GetElementType(); - var elementToString = GetToStringFunction(elementType); - toString = value => - { - var array = (Array)value; - var values = Enumerable.Range(0, array.Length).Select(i => elementToString(array.GetValue(i))); - return string.Join(";", values); - }; - } - else - { - // try to find a `.ToString(IFormatProvider)` method - var toStringMethod = type.GetRuntimeMethod("ToString", new[] { typeof(IFormatProvider) }); - if (toStringMethod != null && !toStringMethod.IsStatic) - { - toString = value => (string)toStringMethod.Invoke(value, new[] { CultureInfo.InvariantCulture }); - } - - if (toString == null) - { - // fall back to `object.ToString()` - toString = value => value.ToString(); - } - } - - return toString; - } - private static bool IsLineIgnorable(string line) { return string.IsNullOrWhiteSpace(line) || line.StartsWith(";") || line.StartsWith("#"); @@ -468,7 +196,8 @@ private static KeyValuePair GetKeyValuePair(string line) var parts = line.Split(Separator, 2); var key = parts[0].Trim(); var value = parts.Length == 2 ? parts[1].Trim() : null; - return new KeyValuePair(key, value); + var parsedValue = ParseString(value); + return new KeyValuePair(key, parsedValue); } private static string MakeFullKey(Tuple key) @@ -485,10 +214,11 @@ private static string MakeFullKey(string prefix, string shortKey) private static string MakeLine(string key, string value) { - return string.Concat(key, " = ", value); + var escapedValue = EscapeString(value); + return string.Concat(key, " = ", escapedValue); } - private static string ParseString(string value) + internal static string ParseString(string value) { if (value == null || value.Length == 1) { @@ -554,37 +284,46 @@ private static string ParseString(string value) return sb.ToString(); } - private static string EscapeString(string value) + internal static string EscapeString(string value) { if (value == null) { return null; } + var neededEscaping = false; var sb = new StringBuilder(); + sb.Append('"'); foreach (var c in value) { switch (c) { case '\f': + neededEscaping = true; sb.Append("\\f"); break; case '\n': + neededEscaping = true; sb.Append("\\n"); break; case '\r': + neededEscaping = true; sb.Append("\\r"); break; case '\t': + neededEscaping = true; sb.Append("\\t"); break; case '\v': + neededEscaping = true; sb.Append("\\v"); break; case '\\': + neededEscaping = true; sb.Append("\\\\"); break; case '"': + neededEscaping = true; sb.Append("\\\""); break; default: @@ -593,7 +332,8 @@ private static string EscapeString(string value) } } - return sb.ToString(); + sb.Append('"'); + return neededEscaping ? sb.ToString() : value; } private class KeyPrefixComparer : IComparer> diff --git a/src/IxMilia.Config/IxMilia.Config.csproj b/src/IxMilia.Config/IxMilia.Config.csproj index 326e6d8..e77abf6 100644 --- a/src/IxMilia.Config/IxMilia.Config.csproj +++ b/src/IxMilia.Config/IxMilia.Config.csproj @@ -18,6 +18,10 @@ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + + + +