From bdbb5a00856195315de0f2d1e61e7beb2eb8c06b Mon Sep 17 00:00:00 2001 From: ITR Date: Sun, 14 Jan 2024 03:30:06 +0100 Subject: [PATCH 01/16] fix: Serializing dictionary with ', ", . in key. Fixes issue where GetTopLevelAndSubKeys splits a key when serializing. Adds test to handle new cases. Does not test cases with mixed " and ' due to issue explained in #57 --- Tomlet.Tests/SerializeSpecialKeysTests.cs | 197 ++++++++++++++++++++++ Tomlet/Models/TomlTable.cs | 1 + Tomlet/TomlKeyUtils.cs | 14 ++ 3 files changed, 212 insertions(+) create mode 100644 Tomlet.Tests/SerializeSpecialKeysTests.cs diff --git a/Tomlet.Tests/SerializeSpecialKeysTests.cs b/Tomlet.Tests/SerializeSpecialKeysTests.cs new file mode 100644 index 0000000..4baac0a --- /dev/null +++ b/Tomlet.Tests/SerializeSpecialKeysTests.cs @@ -0,0 +1,197 @@ +using System.Collections.Generic; +using Tomlet.Tests.TestModelClasses; +using Xunit; + +namespace Tomlet.Tests +{ + public class SerializeSpecialKeysTests + { + void AssertEqual(Dictionary dictionary, Dictionary other) + { + Assert.Equal(dictionary.Count, other.Count); + foreach (var (key, value) in dictionary) + { + var otherValue = Assert.Contains(key, (IDictionary)other); + Assert.Equal(value, otherValue); + } + } + + + [Fact] + public void NoSpecialKeys() + { + var dict = new Dictionary + { + { "SomeKey", "SomeValue" }, + }; + var tomlString = TomletMain.TomlStringFrom(dict); + var otherDict = TomletMain.To>(tomlString); + AssertEqual(dict, otherDict); + } + + [Fact] + public void DottedKey() + { + var dict = new Dictionary + { + { "Some.Key", "Some.Value" }, + { "Some.", "Some." }, + { ".Key", ".Value" }, + { ".", "." }, + }; + var tomlString = TomletMain.TomlStringFrom(dict); + var otherDict = TomletMain.To>(tomlString); + AssertEqual(dict, otherDict); + } + + [Fact] + public void QuotedKey() + { + var dict = new Dictionary + { + { "'SomeKey'", "'SomeValue'" }, + { "\"SomeKey\"", "\"SomeValue\"" }, + { "\"", "\"" }, + { "'", "'" }, + }; + var tomlString = TomletMain.TomlStringFrom(dict); + var otherDict = TomletMain.To>(tomlString); + AssertEqual(dict, otherDict); + } + + [Fact] + public void QuotedDottedKey() + { + var dict = new Dictionary + { + { "'Some.Key'", "'Some.Value'" }, + { "\"Some.Key\"", "\"Some.Value\"" }, + { "'Some'.Key", "'Some'.Value" }, + { "\"Some\".Key", "\"Some\".Value" }, + { "Some.'Key'", "Some.'Value'" }, + { "Some.\"Key\"", "Some.\"Value\"" }, + }; + var tomlString = TomletMain.TomlStringFrom(dict); + var otherDict = TomletMain.To>(tomlString); + AssertEqual(dict, otherDict); + } + + [Fact] + public void Brackets() + { + var dict = new Dictionary + { + { "[SomeKey]", "[SomeValue]" }, + { "[SomeKey\"", "[SomeValue\"" }, + { "[SomeKey", "[SomeValue" }, + { "SomeKey]", "SomeValue]" }, + { "[", "]" }, + { "]", "]" }, + }; + var tomlString = TomletMain.TomlStringFrom(dict); + var otherDict = TomletMain.To>(tomlString); + AssertEqual(dict, otherDict); + } + + [Fact] + public void NoSpecialKeysWithClass() + { + var dict = new Dictionary + { + { "SomeKey", "SomeValue" }, + }; + var tomlString = TomletMain.TomlStringFrom( + new ClassWithDictionary + { + GenericDictionary = dict, + } + ); + var otherClass = TomletMain.To(tomlString); + AssertEqual(dict, otherClass.GenericDictionary); + } + + [Fact] + public void DottedKeyWithClass() + { + var dict = new Dictionary + { + { "Some.Key", "Some.Value" }, + { "Some.", "Some." }, + { ".Key", ".Value" }, + { ".", "." }, + }; + var tomlString = TomletMain.TomlStringFrom( + new ClassWithDictionary + { + GenericDictionary = dict, + } + ); + var otherClass = TomletMain.To(tomlString); + AssertEqual(dict, otherClass.GenericDictionary); + } + + [Fact] + public void QuotedKeyWithClass() + { + var dict = new Dictionary + { + { "'SomeKey'", "'SomeValue'" }, + { "\"SomeKey\"", "\"SomeValue\"" }, + { "\"", "\"" }, + { "'", "'" }, + }; + var tomlString = TomletMain.TomlStringFrom( + new ClassWithDictionary + { + GenericDictionary = dict, + } + ); + var otherClass = TomletMain.To(tomlString); + AssertEqual(dict, otherClass.GenericDictionary); + } + + [Fact] + public void QuotedDottedKeyWithClass() + { + var dict = new Dictionary + { + { "'Some.Key'", "'Some.Value'" }, + { "\"Some.Key\"", "\"Some.Value\"" }, + { "'Some'.Key", "'Some'.Value" }, + { "\"Some\".Key", "\"Some\".Value" }, + { "Some.'Key'", "Some.'Value'" }, + { "Some.\"Key\"", "Some.\"Value\"" }, + }; + var tomlString = TomletMain.TomlStringFrom( + new ClassWithDictionary + { + GenericDictionary = dict, + } + ); + var otherClass = TomletMain.To(tomlString); + AssertEqual(dict, otherClass.GenericDictionary); + } + + [Fact] + public void BracketsWithClass() + { + var dict = new Dictionary + { + { "[SomeKey]", "[SomeValue]" }, + { "[SomeKey\"", "[SomeValue\"" }, + { "[SomeKey", "[SomeValue" }, + { "SomeKey]", "SomeValue]" }, + { "[", "]" }, + { "]", "]" }, + }; + var tomlString = TomletMain.TomlStringFrom( + new ClassWithDictionary + { + GenericDictionary = dict, + } + ); + var otherClass = TomletMain.To(tomlString); + AssertEqual(dict, otherClass.GenericDictionary); + } + } +} \ No newline at end of file diff --git a/Tomlet/Models/TomlTable.cs b/Tomlet/Models/TomlTable.cs index 280d838..b1ed27b 100644 --- a/Tomlet/Models/TomlTable.cs +++ b/Tomlet/Models/TomlTable.cs @@ -89,6 +89,7 @@ public string SerializeNonInlineTable(string? keyName, bool includeHeader = true private void WriteValueToStringBuilder(string? keyName, string subKey, StringBuilder builder) { + subKey = TomlKeyUtils.FullStringToProperKey(subKey); var value = GetValue(subKey); subKey = EscapeKeyIfNeeded(subKey); diff --git a/Tomlet/TomlKeyUtils.cs b/Tomlet/TomlKeyUtils.cs index c5b228b..ced8714 100644 --- a/Tomlet/TomlKeyUtils.cs +++ b/Tomlet/TomlKeyUtils.cs @@ -39,5 +39,19 @@ internal static void GetTopLevelAndSubKeys(string key, out string ourKeyName, ou ourKeyName = ourKeyName.Trim(); } + + public static string FullStringToProperKey(string key) + { + GetTopLevelAndSubKeys(key, out var a, out var b); + var keyLooksQuoted = key.StartsWith("\"") || key.StartsWith("'"); + var keyLooksDotted = key.Contains("."); + + if (keyLooksQuoted || keyLooksDotted || !string.IsNullOrEmpty(b)) + { + return TomlUtils.AddCorrectQuotes(key); + } + + return key; + } } } \ No newline at end of file From 2f9b83f9f4f633dd90c772f06cdfbd17a938c353 Mon Sep 17 00:00:00 2001 From: ITR Date: Sun, 14 Jan 2024 03:35:07 +0100 Subject: [PATCH 02/16] chore: Accidentally didn't push all changes --- Tomlet/Models/TomlDocument.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Tomlet/Models/TomlDocument.cs b/Tomlet/Models/TomlDocument.cs index cdf8118..c26cc5e 100644 --- a/Tomlet/Models/TomlDocument.cs +++ b/Tomlet/Models/TomlDocument.cs @@ -1,4 +1,5 @@ using System.Text; +using Tomlet.Extensions; namespace Tomlet.Models { @@ -16,9 +17,9 @@ internal TomlDocument() internal TomlDocument(TomlTable from) { - foreach (var key in from.Keys) + foreach (var (key, value) in from) { - PutValue(key, from.GetValue(key)); + PutValue(TomlKeyUtils.FullStringToProperKey(key), value); } } From f02a66297e92ebd98016efd090c8417c33b2afe8 Mon Sep 17 00:00:00 2001 From: ITR Date: Sun, 14 Jan 2024 23:11:30 +0100 Subject: [PATCH 03/16] fix: Valid key-parsing with mixed quotes. Ideally fixes #37, but current implementation has some issues. Will elaborate on issues with this "fix" in a comment. --- Tomlet.Tests/QuotedKeyTests.cs | 97 +++++++++++++ Tomlet/Exceptions/InvalidTomlKeyException.cs | 2 +- Tomlet/TomlKeyUtils.cs | 135 +++++++++++++++---- Tomlet/TomlUtils.cs | 47 +++++-- 4 files changed, 243 insertions(+), 38 deletions(-) create mode 100644 Tomlet.Tests/QuotedKeyTests.cs diff --git a/Tomlet.Tests/QuotedKeyTests.cs b/Tomlet.Tests/QuotedKeyTests.cs new file mode 100644 index 0000000..35acbfe --- /dev/null +++ b/Tomlet.Tests/QuotedKeyTests.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Tomlet.Exceptions; +using Xunit; + +namespace Tomlet.Tests +{ + public class QuotedKeysTests + { + [Theory] + [InlineData("\"a.'b\"", "a.'b")] // a.'b + [InlineData("\"a.\\\"b\"", "a.\"b")] // a."b + [InlineData("\"\"", "")] // + [InlineData("\"\\\"\"", "\"")] // " + [InlineData("\"a.🐱b\"", "a.🐱b")] // a.🐱b + [InlineData("'a.\"b'", "a.\"b")] // a."b + [InlineData("'a.\\\"b'", "a.\\\"b")] // a.\"b + [InlineData("''", "")] // + [InlineData("'\"'", "\"")] // \" + [InlineData("'\\\"'", "\\\"")] // \" + [InlineData("'a.🐱b'", "a.🐱b")] // a.🐱b + [InlineData("\"a.b\\\".c\"", "a.b\".c")] // a.b".c + public void NonDottedKeysWork(string inputKey, string expectedKey) + { + var inputString = $"{inputKey} = \"value\""; + var dict = TomletMain.To>(inputString); + Assert.Contains(expectedKey, (IDictionary)dict); + } + + [Theory] + [InlineData("\"a\"b\"")] + [InlineData("'a'b'")] + [InlineData("'a\\'b'")] + //[InlineData("a\"b")] // Illegal in specs, but no harm in reading it + //[InlineData("a'b")] // Illegal in specs, but no harm in reading it + //[InlineData("a🐱b")] // Illegal in specs, but no harm in reading it + [InlineData("'ab\"")] + public void IllegalNonDottedKeysThrow(string inputKey) + { + var inputString = $"{inputKey} = \"value\""; + Assert.ThrowsAny(() => _ = TomletMain.To>(inputString)); + } + + [Theory] + [InlineData("'a.b'.c", "a.b", "c")] + [InlineData("'a.b'.\"c\"", "a.b", "c")] + [InlineData("a.'b.c'", "a", "b.c")] + [InlineData("\"a\".'b.c'", "a", "b.c")] + [InlineData("\"a\\\".b.c", "a", "b.c")] + [InlineData("'a.\"b'.c", "a.\"b", "c")] + [InlineData("\"a.b\\\"c\".d", "a.b\"c", "d")] + public void DottedKeysWork(string inputKey, string expectedKey, string expectedSubkey) + { + var inputString = $"{inputKey} = \"value\""; + var dict = TomletMain.To>>(inputString); + var subDict = Assert.Contains(expectedKey, (IDictionary>)dict); + Assert.Contains(expectedSubkey, (IDictionary)subDict); + } + + [Theory] + [InlineData("'a.\"b'.c\"")] + [InlineData("\"a.bc\".d\"")] + [InlineData("\"a.b\"c\".d\"")] + [InlineData("\"a.b\"c\".d")] + [InlineData("\"a.b\\\"c\".d\"")] + [InlineData("'a.b'c'.d")] + [InlineData("'a.b\\'c'.d")] + [InlineData("'a.bc'.d'")] + public void IllegalDottedKeysThrow(string inputKey) + { + var inputString = $"{inputKey} = \"value\""; + Assert.ThrowsAny(() => _ = TomletMain.To>(inputString)); + } + + + [Theory] + [InlineData("\"a\"b\"", @"(?:'""a""b""')|(?:""\\""a\\""b\\"""")")] // Simple or Literal + [InlineData("'a'b'", @"""'a'b'""")] // Simple only + [InlineData("'a\\'b'", @"""'a\\'b'""")] // Simple only + [InlineData("a\"b", @"(?:'a""b')|(?:""a\\""b"")")] // Simple or Literal + [InlineData("a'b", @"""a'b""")] // Simple only + [InlineData("a🐱b", @"(?:'a🐱b')|(?:""a🐱b"")")] // Simple or Literal + [InlineData("'ab\"", @"""'ab\\""""")] // Simple only + public void SerializingIllegalKeysWorks(string inputKey, string expectedOutput) + { + var dict = new Dictionary + { + { inputKey, "a" }, + }; + var document = TomletMain.DocumentFrom(dict); + Assert.NotEmpty(document.Keys); + var parsedKey = document.Keys.First(); + Assert.Matches(expectedOutput, parsedKey); + } + } +} \ No newline at end of file diff --git a/Tomlet/Exceptions/InvalidTomlKeyException.cs b/Tomlet/Exceptions/InvalidTomlKeyException.cs index a300c69..d9186f7 100644 --- a/Tomlet/Exceptions/InvalidTomlKeyException.cs +++ b/Tomlet/Exceptions/InvalidTomlKeyException.cs @@ -9,6 +9,6 @@ public InvalidTomlKeyException(string key) _key = key; } - public override string Message => $"The string |{_key}| (between the two bars) contains at least one of both a double quote and a single quote, so it cannot be used for a TOML key."; + public override string Message => $"The string |{_key}| (between the two bars) contains invalid characters, so it cannot be used for a TOML key."; } } \ No newline at end of file diff --git a/Tomlet/TomlKeyUtils.cs b/Tomlet/TomlKeyUtils.cs index ced8714..07a07b9 100644 --- a/Tomlet/TomlKeyUtils.cs +++ b/Tomlet/TomlKeyUtils.cs @@ -1,57 +1,136 @@ using System; +using System.Text.RegularExpressions; +using Tomlet.Exceptions; namespace Tomlet { internal static class TomlKeyUtils { + private static readonly Regex UnquotedKeyRegex = new Regex("^[a-zA-Z0-9-_]+$"); + internal static void GetTopLevelAndSubKeys(string key, out string ourKeyName, out string restOfKey) { - var wholeKeyIsQuoted = key.StartsWith("\"") && key.EndsWith("\"") || key.StartsWith("'") && key.EndsWith("'"); - var firstPartOfKeyIsQuoted = !wholeKeyIsQuoted && (key.StartsWith("\"") || key.StartsWith("'")); + var isBasicString = key.StartsWith("\""); + var isLiteralString = key.StartsWith("'"); - if (!key.Contains(".") || wholeKeyIsQuoted) + if (isLiteralString) { - ourKeyName = key; - restOfKey = ""; + // Literal strings can't be escaped + var literalEnd = key.IndexOf('\'', 1); + if (literalEnd + 1 == key.Length) + { + // Full key, no splitting needed. + ourKeyName = key; + restOfKey = ""; + return; + } + + if (key[literalEnd + 1] != '.') + { + // Literal strings cannot contain ' + // TODO: Find better exception + throw new InvalidTomlKeyException(key); + } + + if (literalEnd + 2 == key.Length) + { + // You cannot have an empty unquoted key + // TODO: Find better exception + throw new InvalidTomlKeyException(key); + } + + ourKeyName = key.Substring(0, literalEnd + 1); + restOfKey = key.Substring(literalEnd + 2); return; } - //Unquoted dotted key means we put this in a sub-table. + if (!isBasicString) + { + var firstDot = key.IndexOf(".", StringComparison.Ordinal); + if (firstDot == -1) + { + // Key is undotted. + // We could make a check for illegal characters here, but there isn't much point to it. + ourKeyName = key; + restOfKey = ""; + return; + } + + if (firstDot + 1 == key.Length) + { + // You cannot have an empty unquoted key + // TODO: Find better exception + throw new InvalidTomlKeyException(key); + } + + ourKeyName = key.Substring(0, firstDot); + restOfKey = key.Substring(firstDot + 1); + return; + } - //First get the name of the key in *this* table. - if (!firstPartOfKeyIsQuoted) + var firstUnquote = FindNextUnescapedQuote(key, 1); + if (firstUnquote == -1) { - var split = key.Split('.'); - ourKeyName = split[0]; + // Quoted string was never closed + // TODO: Find better exception + throw new InvalidTomlKeyException(key); } - else + + if (firstUnquote + 1 == key.Length) { + // Full key, no splitting needed. ourKeyName = key; - var keyNameWithoutOpeningQuote = ourKeyName.Substring(1); - if (ourKeyName.Contains("\"")) - ourKeyName = ourKeyName.Substring(0, 2 + keyNameWithoutOpeningQuote.IndexOf("\"", StringComparison.Ordinal)); - else - ourKeyName = ourKeyName.Substring(0, 2 + keyNameWithoutOpeningQuote.IndexOf("'", StringComparison.Ordinal)); + restOfKey = ""; + return; } - //And get the remainder of the key, relative to the sub-table. - restOfKey = key.Substring(ourKeyName.Length + 1); + if (key[firstUnquote + 1] != '.') + { + // Quoted strings cannot contain unescaped " + // TODO: Find better exception + throw new InvalidTomlKeyException(key); + } - ourKeyName = ourKeyName.Trim(); + if (firstUnquote + 2 == key.Length) + { + // You cannot have an empty unquoted key + // TODO: Find better exception + throw new InvalidTomlKeyException(key); + } + + ourKeyName = key.Substring(0, firstUnquote + 1); + restOfKey = key.Substring(firstUnquote + 2); } - public static string FullStringToProperKey(string key) - { - GetTopLevelAndSubKeys(key, out var a, out var b); - var keyLooksQuoted = key.StartsWith("\"") || key.StartsWith("'"); - var keyLooksDotted = key.Contains("."); - if (keyLooksQuoted || keyLooksDotted || !string.IsNullOrEmpty(b)) + private static int FindNextUnescapedQuote(string input, int startingIndex) + { + var i = startingIndex; + var isEscaped = false; + for (; i < input.Length; i++) { - return TomlUtils.AddCorrectQuotes(key); + if (input[i] == '\\') + { + isEscaped = !isEscaped; + continue; + } + + if (input[i] != '"' || isEscaped) + { + isEscaped = false; + continue; + } + + return i; } - - return key; + + return -1; // Return -1 if no unescaped quote is found + } + + internal static string FullStringToProperKey(string key) + { + var canBeUnquoted = UnquotedKeyRegex.Match(key).Success; + return canBeUnquoted ? key : TomlUtils.AddCorrectQuotes(key); } } } \ No newline at end of file diff --git a/Tomlet/TomlUtils.cs b/Tomlet/TomlUtils.cs index 26c5f28..7453592 100644 --- a/Tomlet/TomlUtils.cs +++ b/Tomlet/TomlUtils.cs @@ -1,26 +1,55 @@ -using Tomlet.Exceptions; +using System.Text.RegularExpressions; +using Tomlet.Exceptions; namespace Tomlet { internal static class TomlUtils { + // Characters that can't be in either literal or quoted strings. *Technically* these can be converted to \u + // characters, but somebody else can implement this functionality. + private static readonly Regex CanBeBasicRegex = + new Regex(@"^[\x08-\x0A\x0C-\x0D\x20-\x7E\x80-\uD7FF\uE000-\uFFFF]+$"); + + // Toml defines non-ascii as %x80-D7FF / %xE000-10FFFF, so this will break hard for UTF16 + private static readonly Regex CanBeLiteralRegex = + new Regex(@"^[\x09\x20-\x26\x28-\x7E\x80-\uD7FF\uE000-\uFFFF]+$"); + public static string EscapeStringValue(string key) { - var escaped = key.Replace(@"\", @"\\") - .Replace("\n", @"\n") - .Replace("\r", ""); - + // Escaped characters allowed in simple strings: + // https://github.com/toml-lang/toml/blob/8eae5e1c005bc5836098505f85a7aa06568999dd/toml.abnf#L74 + var escaped = + key.Replace(@"\", @"\\") + .Replace("\n", @"\n") + .Replace("\t", @"\t") + .Replace("\"", @"\""") + .Replace("\b", @"\b") // Backspace + .Replace("\f", @"\f") // Form Feed + .Replace("\r", @"\r") // Carriage Return + // \uXXXX and \UXXXXXXXX get parsed as unicode, thus we should escape strings that the parser + // would mistake for such an escape value. Since unicode symbols are allowed we don't need to + // escape *actual* unicode characters in the text + .Replace(@"\u", @"\\u") + .Replace(@"\U", @"\\U"); return escaped; } public static string AddCorrectQuotes(string key) { - if (key.Contains("'") && key.Contains("\"")) - throw new InvalidTomlKeyException(key); - - if (key.Contains("\"")) + var literal = CanBeLiteralRegex.Match(key).Success; + if (literal) + { + // Literal strings aren't escaped return $"'{key}'"; + } + + var basic = CanBeBasicRegex.Match(key).Success; + if (!basic) + { + throw new InvalidTomlKeyException(key); + } + key = EscapeStringValue(key); return $"\"{key}\""; } } From 618543cae9ce8b87056d5e92a459fcf60dd33ab9 Mon Sep 17 00:00:00 2001 From: ITR Date: Sat, 20 Jan 2024 02:22:28 +0100 Subject: [PATCH 04/16] Implemented key splitting on parser level --- Tomlet.Tests/TestResources.Designer.cs | 2 +- Tomlet.Tests/TestResources.resx | 2 +- Tomlet.Tests/TomlTableArrayTests.cs | 2 +- Tomlet/Models/TomlDocument.cs | 2 +- Tomlet/Models/TomlTable.cs | 185 +++++---------- Tomlet/TomlKeyUtils.cs | 95 -------- Tomlet/TomlParser.cs | 307 +++++++++++-------------- 7 files changed, 202 insertions(+), 393 deletions(-) diff --git a/Tomlet.Tests/TestResources.Designer.cs b/Tomlet.Tests/TestResources.Designer.cs index 605bc68..7c49902 100644 --- a/Tomlet.Tests/TestResources.Designer.cs +++ b/Tomlet.Tests/TestResources.Designer.cs @@ -198,7 +198,7 @@ internal static string CommentTestInput { ///shape = "round" /// ///[fruits.jam] # second subtable - ///color = "yellow" + ///color = "yellowy" ///feel = "sticky" /// ///[[fruits.varieties]] # nested array of tables diff --git a/Tomlet.Tests/TestResources.resx b/Tomlet.Tests/TestResources.resx index dfab62a..022ca7f 100644 --- a/Tomlet.Tests/TestResources.resx +++ b/Tomlet.Tests/TestResources.resx @@ -328,7 +328,7 @@ color = "red" shape = "round" [fruits.jam] # second subtable -color = "yellow" +color = "yellowy" feel = "sticky" [[fruits.varieties]] # nested array of tables diff --git a/Tomlet.Tests/TomlTableArrayTests.cs b/Tomlet.Tests/TomlTableArrayTests.cs index 4d19557..4be6f3a 100644 --- a/Tomlet.Tests/TomlTableArrayTests.cs +++ b/Tomlet.Tests/TomlTableArrayTests.cs @@ -57,7 +57,7 @@ public void ComplexTableArraysAreSupported() Assert.Equal("red", physical.GetString("color")); Assert.Equal("round", physical.GetString("shape")); - Assert.Equal("yellow", jam.GetString("color")); + Assert.Equal("yellowy", jam.GetString("color")); Assert.Equal("sticky", jam.GetString("feel")); Assert.Equal(2, varieties.Count); diff --git a/Tomlet/Models/TomlDocument.cs b/Tomlet/Models/TomlDocument.cs index c26cc5e..85b0c88 100644 --- a/Tomlet/Models/TomlDocument.cs +++ b/Tomlet/Models/TomlDocument.cs @@ -19,7 +19,7 @@ internal TomlDocument(TomlTable from) { foreach (var (key, value) in from) { - PutValue(TomlKeyUtils.FullStringToProperKey(key), value); + PutValue(key, value); } } diff --git a/Tomlet/Models/TomlTable.cs b/Tomlet/Models/TomlTable.cs index b1ed27b..2a5afed 100644 --- a/Tomlet/Models/TomlTable.cs +++ b/Tomlet/Models/TomlTable.cs @@ -37,7 +37,7 @@ public override string SerializedValue var builder = new StringBuilder("{ "); - builder.Append(string.Join(", ", Entries.Select(o => EscapeKeyIfNeeded(o.Key) + " = " + o.Value.SerializedValue).ToArray())); + builder.Append(string.Join(", ", Entries.Select(o => TomlKeyUtils.FullStringToProperKey(o.Key) + " = " + o.Value.SerializedValue).ToArray())); builder.Append(" }"); @@ -89,13 +89,11 @@ public string SerializeNonInlineTable(string? keyName, bool includeHeader = true private void WriteValueToStringBuilder(string? keyName, string subKey, StringBuilder builder) { - subKey = TomlKeyUtils.FullStringToProperKey(subKey); var value = GetValue(subKey); - - subKey = EscapeKeyIfNeeded(subKey); + subKey = TomlKeyUtils.FullStringToProperKey(subKey); if (keyName != null) - keyName = EscapeKeyIfNeeded(keyName); + keyName = TomlKeyUtils.FullStringToProperKey(keyName); var fullSubKey = keyName == null ? subKey : $"{keyName}.{subKey}"; @@ -137,43 +135,14 @@ private void WriteValueToStringBuilder(string? keyName, string subKey, StringBui //Then append a newline builder.Append('\n'); } - - private static string EscapeKeyIfNeeded(string key) - { - if (key.StartsWith("\"") && key.EndsWith("\"") && key.Count(c => c == '"') == 2) - //Already double quoted - return key; - - if (key.StartsWith("'") && key.EndsWith("'") && key.Count(c => c == '\'') == 2) - //Already single quoted - return key; - - if (IsValidKey(key)) - return key; - - key = TomlUtils.EscapeStringValue(key); - return TomlUtils.AddCorrectQuotes(key); - } - - private static bool IsValidKey(string key) - { - foreach (var c in key) - { - if (!char.IsLetterOrDigit(c) && c != '_' && c != '-') - { - return false; - } - } - - return true; - } - - internal void ParserPutValue(string key, TomlValue value, int lineNumber) + + internal void ParserPutValue(ref List key, TomlValue value, int lineNumber) { + // NB: key is ref to signal that it mutates! if (Locked) - throw new TomlTableLockedException(lineNumber, key); - - InternalPutValue(key, value, lineNumber, true); + throw new TomlTableLockedException(lineNumber, string.Join(".", key.ToArray())); + + InternalPutValue(ref key, value, lineNumber); } public void PutValue(string key, TomlValue value, bool quote = false) @@ -184,9 +153,7 @@ public void PutValue(string key, TomlValue value, bool quote = false) if (value == null) throw new ArgumentNullException(nameof(value)); - if (quote) - key = TomlUtils.AddCorrectQuotes(key); - InternalPutValue(key, value, null, false); + InternalPutValue(key, value, null); } public void Put(string key, T t, bool quote = false) @@ -199,82 +166,68 @@ public void Put(string key, T t, bool quote = false) PutValue(key, tomlValue, quote); } - - public string DeQuoteKey(string key) + + private void InternalPutValue(ref List key, TomlValue value, int? lineNumber) { - var wholeKeyIsQuoted = key.StartsWith("\"") && key.EndsWith("\"") || key.StartsWith("'") && key.EndsWith("'"); - return !wholeKeyIsQuoted ? key : key.Substring(1, key.Length - 2); - } + // NB: key is ref to signal that it mutates! + if (key.Count == 0) + { + // TODO: Check what should be done here + throw new NoTomlKeyException(lineNumber ?? -1); + } + + var ourKeyName = key[0]; + key.RemoveAt(0); - private void InternalPutValue(string key, TomlValue value, int? lineNumber, bool callParserForm) - { - key = key.Trim(); - TomlKeyUtils.GetTopLevelAndSubKeys(key, out var ourKeyName, out var restOfKey); + // Do we have a dotted key? + if (key.Count == 0) + { + // Non-dotted keys land here. + if (Entries.ContainsKey(ourKeyName) && lineNumber.HasValue) + throw new TomlKeyRedefinitionException(lineNumber.Value, ourKeyName); + + Entries[ourKeyName] = value; + return; + } - if (!string.IsNullOrEmpty(restOfKey)) + // Dotted keys land here + if (!Entries.TryGetValue(ourKeyName, out var existingValue)) { - if (!Entries.TryGetValue(DeQuoteKey(ourKeyName), out var existingValue)) - { - //We don't have a sub-table with this name defined. That's fine, make one. - var subtable = new TomlTable(); - if (callParserForm) - ParserPutValue(ourKeyName, subtable, lineNumber!.Value); - else - PutValue(ourKeyName, subtable); - - //And tell it to handle the rest of the key. - if (callParserForm) - subtable.ParserPutValue(restOfKey, value, lineNumber!.Value); - else - subtable.PutValue(restOfKey, value); - return; - } - - //We have a key by this name already. Is it a table? - if (existingValue is not TomlTable existingTable) - { - //No - throw an exception - if (lineNumber.HasValue) - throw new TomlDottedKeyParserException(lineNumber.Value, ourKeyName); - - throw new TomlDottedKeyException(ourKeyName); - } - - //Yes, get the sub-table to handle the rest of the key - if (callParserForm) - existingTable.ParserPutValue(restOfKey, value, lineNumber!.Value); - else - existingTable.PutValue(restOfKey, value); + //We don't have a sub-table with this name defined. That's fine, make one. + var subtable = new TomlTable(); + Entries[ourKeyName] = subtable; + subtable.ParserPutValue(ref key, value, lineNumber!.Value); return; } - //Non-dotted keys land here. - key = DeQuoteKey(key); + //We have a key by this name already. Is it a table? + if (existingValue is not TomlTable existingTable) + { + //No - throw an exception + if (lineNumber.HasValue) throw new TomlDottedKeyParserException(lineNumber.Value, ourKeyName); + + throw new TomlDottedKeyException(ourKeyName); + } + //Yes, get the sub-table to handle the rest of the key + existingTable.ParserPutValue(ref key, value, lineNumber!.Value); + } + + private void InternalPutValue(string key, TomlValue value, int? lineNumber) + { + // Because we have a single key, we know it's not dotted if (Entries.ContainsKey(key) && lineNumber.HasValue) throw new TomlKeyRedefinitionException(lineNumber.Value, key); Entries[key] = value; } - + public bool ContainsKey(string key) { if (key == null) throw new ArgumentNullException("key"); - TomlKeyUtils.GetTopLevelAndSubKeys(key, out var ourKeyName, out var restOfKey); - - if (string.IsNullOrEmpty(restOfKey)) - //Non-dotted key - return Entries.ContainsKey(DeQuoteKey(key)); - - if (!Entries.TryGetValue(ourKeyName, out var existingKey)) - return false; - - if (existingKey is TomlTable table) - return table.ContainsKey(restOfKey); - - throw new TomlContainsDottedKeyNonTableException(key); + return Entries.ContainsKey(key); } #if NET6_0 @@ -304,22 +257,10 @@ public TomlValue GetValue(string key) if (key == null) throw new ArgumentNullException("key"); - if (!ContainsKey(key)) + if (!Entries.ContainsKey(key)) throw new TomlNoSuchValueException(key); - TomlKeyUtils.GetTopLevelAndSubKeys(key, out var ourKeyName, out var restOfKey); - - if (string.IsNullOrEmpty(restOfKey)) - //Non-dotted key - return Entries[DeQuoteKey(key)]; - - if (!Entries.TryGetValue(ourKeyName, out var existingKey)) - throw new TomlNoSuchValueException(key); //Should already be handled by ContainsKey test - - if (existingKey is TomlTable table) - return table.GetValue(restOfKey); - - throw new Exception("Tomlet Internal bug - existing key is not a table in TomlTable GetValue, but we didn't throw in ContainsKey?"); + return Entries[key]; } /// @@ -334,7 +275,7 @@ public string GetString(string key) if (key == null) throw new ArgumentNullException("key"); - var value = GetValue(TomlUtils.AddCorrectQuotes(key)); + var value = GetValue(key); if (value is not TomlString str) throw new TomlTypeMismatchException(typeof(TomlString), value.GetType(), typeof(string)); @@ -354,7 +295,7 @@ public int GetInteger(string key) if (key == null) throw new ArgumentNullException("key"); - var value = GetValue(TomlUtils.AddCorrectQuotes(key)); + var value = GetValue(key); if (value is not TomlLong lng) throw new TomlTypeMismatchException(typeof(TomlLong), value.GetType(), typeof(int)); @@ -374,7 +315,7 @@ public long GetLong(string key) if (key == null) throw new ArgumentNullException("key"); - var value = GetValue(TomlUtils.AddCorrectQuotes(key)); + var value = GetValue(key); if (value is not TomlLong lng) throw new TomlTypeMismatchException(typeof(TomlLong), value.GetType(), typeof(int)); @@ -394,7 +335,7 @@ public float GetFloat(string key) if (key == null) throw new ArgumentNullException("key"); - var value = GetValue(TomlUtils.AddCorrectQuotes(key)); + var value = GetValue(key); if (value is not TomlDouble dbl) throw new TomlTypeMismatchException(typeof(TomlDouble), value.GetType(), typeof(float)); @@ -414,7 +355,7 @@ public bool GetBoolean(string key) if (key == null) throw new ArgumentNullException("key"); - var value = GetValue(TomlUtils.AddCorrectQuotes(key)); + var value = GetValue(key); if (value is not TomlBoolean b) throw new TomlTypeMismatchException(typeof(TomlBoolean), value.GetType(), typeof(bool)); @@ -434,7 +375,7 @@ public TomlArray GetArray(string key) if (key == null) throw new ArgumentNullException("key"); - var value = GetValue(TomlUtils.AddCorrectQuotes(key)); + var value = GetValue(key); if (value is not TomlArray arr) throw new TomlTypeMismatchException(typeof(TomlArray), value.GetType(), typeof(TomlArray)); @@ -454,7 +395,7 @@ public TomlTable GetSubTable(string key) if (key == null) throw new ArgumentNullException("key"); - var value = GetValue(TomlUtils.AddCorrectQuotes(key)); + var value = GetValue(key); if (value is not TomlTable tbl) throw new TomlTypeMismatchException(typeof(TomlTable), value.GetType(), typeof(TomlTable)); diff --git a/Tomlet/TomlKeyUtils.cs b/Tomlet/TomlKeyUtils.cs index 07a07b9..66d173c 100644 --- a/Tomlet/TomlKeyUtils.cs +++ b/Tomlet/TomlKeyUtils.cs @@ -8,101 +8,6 @@ internal static class TomlKeyUtils { private static readonly Regex UnquotedKeyRegex = new Regex("^[a-zA-Z0-9-_]+$"); - internal static void GetTopLevelAndSubKeys(string key, out string ourKeyName, out string restOfKey) - { - var isBasicString = key.StartsWith("\""); - var isLiteralString = key.StartsWith("'"); - - if (isLiteralString) - { - // Literal strings can't be escaped - var literalEnd = key.IndexOf('\'', 1); - if (literalEnd + 1 == key.Length) - { - // Full key, no splitting needed. - ourKeyName = key; - restOfKey = ""; - return; - } - - if (key[literalEnd + 1] != '.') - { - // Literal strings cannot contain ' - // TODO: Find better exception - throw new InvalidTomlKeyException(key); - } - - if (literalEnd + 2 == key.Length) - { - // You cannot have an empty unquoted key - // TODO: Find better exception - throw new InvalidTomlKeyException(key); - } - - ourKeyName = key.Substring(0, literalEnd + 1); - restOfKey = key.Substring(literalEnd + 2); - return; - } - - if (!isBasicString) - { - var firstDot = key.IndexOf(".", StringComparison.Ordinal); - if (firstDot == -1) - { - // Key is undotted. - // We could make a check for illegal characters here, but there isn't much point to it. - ourKeyName = key; - restOfKey = ""; - return; - } - - if (firstDot + 1 == key.Length) - { - // You cannot have an empty unquoted key - // TODO: Find better exception - throw new InvalidTomlKeyException(key); - } - - ourKeyName = key.Substring(0, firstDot); - restOfKey = key.Substring(firstDot + 1); - return; - } - - var firstUnquote = FindNextUnescapedQuote(key, 1); - if (firstUnquote == -1) - { - // Quoted string was never closed - // TODO: Find better exception - throw new InvalidTomlKeyException(key); - } - - if (firstUnquote + 1 == key.Length) - { - // Full key, no splitting needed. - ourKeyName = key; - restOfKey = ""; - return; - } - - if (key[firstUnquote + 1] != '.') - { - // Quoted strings cannot contain unescaped " - // TODO: Find better exception - throw new InvalidTomlKeyException(key); - } - - if (firstUnquote + 2 == key.Length) - { - // You cannot have an empty unquoted key - // TODO: Find better exception - throw new InvalidTomlKeyException(key); - } - - ourKeyName = key.Substring(0, firstUnquote + 1); - restOfKey = key.Substring(firstUnquote + 2); - } - - private static int FindNextUnescapedQuote(string input, int startingIndex) { var i = startingIndex; diff --git a/Tomlet/TomlParser.cs b/Tomlet/TomlParser.cs index 4ec7dfe..2fb4ef5 100644 --- a/Tomlet/TomlParser.cs +++ b/Tomlet/TomlParser.cs @@ -18,7 +18,6 @@ public class TomlParser private int _lineNumber = 1; - private string[] _tableNames = new string[0]; private TomlTable? _currentTable; // ReSharper disable once UnusedMember.Global @@ -77,10 +76,10 @@ public TomlDocument Parse(string input) if (_currentTable != null) //Insert into current table - _currentTable.ParserPutValue(key, value, _lineNumber); + _currentTable.ParserPutValue(ref key, value, _lineNumber); else //Insert into the document - document.ParserPutValue(key, value, _lineNumber); + document.ParserPutValue(ref key, value, _lineNumber); //Read up until the end of the line, ignoring any comments or whitespace reader.SkipWhitespace(); @@ -104,19 +103,19 @@ public TomlDocument Parse(string input) } } - private void ReadKeyValuePair(TomletStringReader reader, out string key, out TomlValue value) + private void ReadKeyValuePair(TomletStringReader reader, out List key, out TomlValue value) { //Read the key key = ReadKey(reader); //Consume the equals sign, potentially with whitespace either side. - reader.SkipWhitespace(); if (!reader.ExpectAndConsume('=')) { - if (reader.TryPeek(out var shouldHaveBeenEquals)) - throw new TomlMissingEqualsException(_lineNumber, (char) shouldHaveBeenEquals); + if (!reader.TryPeek(out var shouldHaveBeenEquals)) + throw new TomlEndOfFileException(_lineNumber); + + throw new TomlMissingEqualsException(_lineNumber, (char) shouldHaveBeenEquals); - throw new TomlEndOfFileException(_lineNumber); } reader.SkipWhitespace(); @@ -125,12 +124,12 @@ private void ReadKeyValuePair(TomletStringReader reader, out string key, out Tom value = ReadValue(reader); } - private string ReadKey(TomletStringReader reader) + private List ReadKey(TomletStringReader reader) { reader.SkipWhitespace(); if (!reader.TryPeek(out var nextChar)) - return ""; + return new List(); if (nextChar.IsEquals()) throw new NoTomlKeyException(_lineNumber); @@ -138,110 +137,84 @@ private string ReadKey(TomletStringReader reader) //Read a key reader.SkipWhitespace(); - string key; + var key = new List { ReadKeyPart(reader) }; + + reader.SkipWhitespace(); + while (reader.TryPeek(out nextChar) && nextChar == '.') + { + reader.ExpectAndConsume('.'); + reader.SkipWhitespace(); + key.Add(ReadKeyPart(reader)); + reader.SkipWhitespace(); + } + + return key; + } + + private string ReadKeyPart(TomletStringReader reader) + { + if (!reader.TryPeek(out var nextChar)) + return ""; + + if(nextChar.IsPeriod()) + throw new TomlDoubleDottedKeyException(_lineNumber); + if (nextChar.IsDoubleQuote()) { //Read double-quoted key reader.Read(); - if (reader.TryPeek(out var maybeSecondDoubleQuote) && maybeSecondDoubleQuote.IsDoubleQuote()) + string basicString; + try { - reader.Read(); //Consume second double quote. - - //Check for third quote => invalid key - //Else => empty key - if (reader.TryPeek(out var maybeThirdDoubleQuote) && maybeThirdDoubleQuote.IsDoubleQuote()) - throw new TomlTripleQuotedKeyException(_lineNumber); - - return string.Empty; + basicString = ReadSingleLineBasicString(reader).StringValue; } - - //We delegate to the dedicated string reading function here because a double-quoted key can contain everything a double-quoted string can. - key = '"' + ReadSingleLineBasicString(reader, false).StringValue + '"'; - - if (!reader.ExpectAndConsume('"')) + catch (UnterminatedTomlStringException) + { throw new UnterminatedTomlKeyException(_lineNumber); + } + + if (reader.TryPeek(out var notQuote) && notQuote.IsDoubleQuote()) + { + throw new TomlTripleQuotedKeyException(_lineNumber); + } + return basicString; } - else if (nextChar.IsSingleQuote()) + + if (nextChar.IsSingleQuote()) { reader.Read(); //Consume opening quote. //Read single-quoted key - key = "'" + ReadSingleLineLiteralString(reader, false).StringValue + "'"; - if (!reader.ExpectAndConsume('\'')) + try + { + return ReadSingleLineLiteralString(reader).StringValue; + } + catch (UnterminatedTomlStringException) + { throw new UnterminatedTomlKeyException(_lineNumber); + } } - else - //Read unquoted key - key = ReadKeyInternal(reader, keyChar => keyChar.IsEquals() || keyChar.IsHashSign()); - - key = key.Replace("\\n", "\n") - .Replace("\\t", "\t"); - return key; + return ReadUnquotedKey(reader); } - private string ReadKeyInternal(TomletStringReader reader, Func charSignalsEndOfKey) + private string ReadUnquotedKey(TomletStringReader reader) { - var parts = new List(); - - //Parts loop + var sb = new StringBuilder(); while (reader.TryPeek(out var nextChar)) { - if (charSignalsEndOfKey(nextChar)) - return string.Join(".", parts.ToArray()); - - if (nextChar.IsPeriod()) - throw new TomlDoubleDottedKeyException(_lineNumber); - - var thisPart = new StringBuilder(); - //Part loop - while (reader.TryPeek(out nextChar)) + nextChar.EnsureLegalChar(_lineNumber); + if (nextChar.IsPeriod() || nextChar.IsWhitespace() || nextChar.IsEquals() || nextChar.IsEndOfArrayChar()) { - nextChar.EnsureLegalChar(_lineNumber); - - var numLeadingWhitespace = reader.SkipWhitespace(); - reader.TryPeek(out var charAfterWhitespace); - if (charAfterWhitespace.IsPeriod()) - { - //Whitespace is permitted in keys only around periods - parts.Add(thisPart.ToString()); //Add this part - - //Consume period and any trailing whitespace - reader.ExpectAndConsume('.'); - reader.SkipWhitespace(); - break; //End of part, move to next - } - - if (numLeadingWhitespace > 0 && charSignalsEndOfKey(charAfterWhitespace)) - { - //Add this part to the list of parts and break out of the loop, without consuming the char (it'll be picked up by the outer loop) - parts.Add(thisPart.ToString()); - break; - } - - //Un-skip the whitespace - reader.Backtrack(numLeadingWhitespace); - - //NextChar is still the whitespace itself - if (charSignalsEndOfKey(nextChar)) - { - //Add this part to the list of parts and break out of the loop, without consuming the char (it'll be picked up by the outer loop) - parts.Add(thisPart.ToString()); - break; - } - - if (numLeadingWhitespace > 0) - //Whitespace is not allowed outside of the area immediately around a period in a dotted key - throw new TomlWhitespaceInKeyException(_lineNumber); - - //Append this char to the part - thisPart.Append((char) reader.Read()); + return sb.ToString(); } + + sb.Append((char) reader.Read()); } throw new TomlEndOfFileException(_lineNumber); } - + private TomlValue ReadValue(TomletStringReader reader) { if (!reader.TryPeek(out var startOfValue)) @@ -788,7 +761,7 @@ private TomlTable ReadInlineTable(TomletStringReader reader) //Read a key-value pair ReadKeyValuePair(reader, out var key, out var value); //Insert into the table - result.ParserPutValue(key, value, _lineNumber); + result.ParserPutValue(ref key, value, _lineNumber); } catch (TomlException ex) when (ex is TomlMissingEqualsException or NoTomlKeyException or TomlWhitespaceInKeyException) { @@ -822,48 +795,38 @@ private TomlTable ReadInlineTable(TomletStringReader reader) private TomlTable ReadTableStatement(TomletStringReader reader, TomlDocument document) { - //Table name - var currentTableKey = reader.ReadWhile(c => !c.IsEndOfArrayChar() && !c.IsNewline()); - - var parent = (TomlTable) document; - var relativeKey = currentTableKey; - FindParentAndRelativeKey(ref parent, ref relativeKey); + var key = ReadKey(reader); + var originalKey = string.Join(".", key.ToArray()); + + var parent = (TomlTable)document; + GetLowestTable(ref parent, ref key, 0, typeof(TomlTable)); - TomlTable table; - try + if (key.Count == 0) { - if (parent.ContainsKey(relativeKey)) + if (parent.Defined) { - try - { - table = (TomlTable) parent.GetValue(relativeKey); + throw new TomlTableRedefinitionException(_lineNumber, originalKey); + } - //The cast succeeded - we are defining an existing table - if (table.Defined) - { - // The table was not one created automatically - throw new TomlTableRedefinitionException(_lineNumber, currentTableKey); - } - } - catch (InvalidCastException) - { - //The cast failed, we are re-defining a non-table. - throw new TomlKeyRedefinitionException(_lineNumber, currentTableKey); - } + parent.Defined = true; + } + + var table = parent; + if (key.Count > 0) + { + table = new TomlTable { Defined = true }; + try + { + parent.ParserPutValue(ref key, table, _lineNumber); } - else + catch (TomlContainsDottedKeyNonTableException e) { - table = new TomlTable {Defined = true}; - parent.ParserPutValue(relativeKey, table, _lineNumber); + //Re-throw with correct line number and exception type. + //To be clear - here we're re-defining a NON-TABLE key as a table, so this is a dotted key exception + //while the one above is a TableRedefinition exception because it's re-defining a key which is already a table. + throw new TomlDottedKeyParserException(_lineNumber, e.Key); } } - catch (TomlContainsDottedKeyNonTableException e) - { - //Re-throw with correct line number and exception type. - //To be clear - here we're re-defining a NON-TABLE key as a table, so this is a dotted key exception - //while the one above is a TableRedefinition exception because it's re-defining a key which is already a table. - throw new TomlDottedKeyParserException(_lineNumber, e.Key); - } if (!reader.TryPeek(out _)) throw new TomlEndOfFileException(_lineNumber); @@ -882,11 +845,45 @@ private TomlTable ReadTableStatement(TomletStringReader reader, TomlDocument doc throw new TomlMissingNewlineException(_lineNumber, (char) shouldBeNewline); _currentTable = table; + return table; + } - //Save table names - _tableNames = currentTableKey.Split('.'); + private void GetLowestTable(ref TomlTable parent, ref List key, int keepSubkeys, Type context) + { + // NB: Mutates key. Variable marked as ref so nobody uses this wrong. - return table; + var usedKeys = new List(); + // Loop through all the subkeys until we have only one key left or have to create a new table + while (key.Count > keepSubkeys) + { + var subkey = key[0]; + usedKeys.Add(subkey); + + if (!parent.Entries.TryGetValue(subkey, out var value)) + { + break; + } + key.RemoveAt(0); + + if (value is TomlTable subTable) + { + parent = subTable; + } + else if (value is TomlArray array) + { + var arrayElement = array.Last(); + if (arrayElement is not TomlTable table) + { + throw new TomlKeyRedefinitionException(_lineNumber, string.Join(".", usedKeys.ToArray())); + } + parent = table; + } + else + { + // Note: Expects either TomlArray or TomlTable + throw new TomlKeyRedefinitionException(_lineNumber, string.Join(".", usedKeys.ToArray())); + } + } } private TomlArray ReadTableArrayStatement(TomletStringReader reader, TomlDocument document) @@ -896,82 +893,48 @@ private TomlArray ReadTableArrayStatement(TomletStringReader reader, TomlDocumen throw new ArgumentException("Internal Tomlet Bug: ReadTableArrayStatement called and first char is not a ["); //Array - var arrayName = reader.ReadWhile(c => !c.IsEndOfArrayChar() && !c.IsNewline()); + var key = ReadKey(reader); if (!reader.ExpectAndConsume(']') || !reader.ExpectAndConsume(']')) throw new UnterminatedTomlTableArrayException(_lineNumber); - TomlTable parentTable = document; - var relativeKey = arrayName; - FindParentAndRelativeKey(ref parentTable, ref relativeKey); + var parent = (TomlTable)document; + GetLowestTable(ref parent, ref key, 1, typeof(TomlArray)); - if (parentTable == document) + if (parent == document && key.Count > 1) { - if (relativeKey.Contains('.')) - throw new MissingIntermediateInTomlTableArraySpecException(_lineNumber, relativeKey); + throw new MissingIntermediateInTomlTableArraySpecException(_lineNumber, string.Join(".", key.ToArray())); } - + var remainingKey = key[0]; + //Find existing array or make new one TomlArray array; - if (parentTable.ContainsKey(relativeKey)) + if (parent.Entries.TryGetValue(remainingKey, out var value)) { - var value = parentTable.GetValue(relativeKey); if (value is TomlArray arr) array = arr; else - throw new TomlTableArrayAlreadyExistsAsNonArrayException(_lineNumber, arrayName); + throw new TomlTableArrayAlreadyExistsAsNonArrayException(_lineNumber, string.Join(".", key.ToArray())); if (!array.IsLockedToBeTableArray) { - throw new TomlNonTableArrayUsedAsTableArrayException(_lineNumber, arrayName); + throw new TomlNonTableArrayUsedAsTableArrayException(_lineNumber, string.Join(".", key.ToArray())); } } else { array = new TomlArray {IsLockedToBeTableArray = true}; //Insert into parent table - parentTable.ParserPutValue(relativeKey, array, _lineNumber); + parent.ParserPutValue(ref key, array, _lineNumber); } // Create new table and add it to the array _currentTable = new TomlTable {Defined = true}; array.ArrayValues.Add(_currentTable); - - //Save table names - _tableNames = arrayName.Split('.'); return array; } - - private void FindParentAndRelativeKey(ref TomlTable parent, ref string relativeName) - { - for (var index = 0; index < _tableNames.Length; index++) - { - var rootTableName = _tableNames[index]; - if (!relativeName.StartsWith(rootTableName + ".")) - { - break; - } - - var value = parent.GetValue(rootTableName); - if (value is TomlTable subTable) - { - parent = subTable; - } - else if (value is TomlArray array) - { - parent = (TomlTable) array.Last(); - } - else - { - // Note: Expects either TomlArray or TomlTable - throw new TomlTypeMismatchException(typeof(TomlArray), value.GetType(), typeof(TomlArray)); - } - - relativeName = relativeName.Substring(rootTableName.Length + 1); - } - } - + private string? ReadAnyPotentialInlineComment(TomletStringReader reader) { if (!reader.ExpectAndConsume('#')) From d55e24639b42af63481269465fb9b66f185d8373 Mon Sep 17 00:00:00 2001 From: ITR Date: Sat, 20 Jan 2024 02:40:39 +0100 Subject: [PATCH 05/16] Fixed key validation for utf16 --- Tomlet/TomlUtils.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Tomlet/TomlUtils.cs b/Tomlet/TomlUtils.cs index 7453592..90b6332 100644 --- a/Tomlet/TomlUtils.cs +++ b/Tomlet/TomlUtils.cs @@ -7,12 +7,11 @@ internal static class TomlUtils { // Characters that can't be in either literal or quoted strings. *Technically* these can be converted to \u // characters, but somebody else can implement this functionality. + // NB: /[\uD800-\uDBFF][\uDC00-\uDFFF]/ finds 2-byte unicode characters private static readonly Regex CanBeBasicRegex = - new Regex(@"^[\x08-\x0A\x0C-\x0D\x20-\x7E\x80-\uD7FF\uE000-\uFFFF]+$"); - - // Toml defines non-ascii as %x80-D7FF / %xE000-10FFFF, so this will break hard for UTF16 + new Regex(@"^(([\uD800-\uDBFF][\uDC00-\uDFFF])|[\x08-\x0A\x0C-\x0D\x20-\x7E\x80-\uD7FF\uE000-\uFFFF])+$"); private static readonly Regex CanBeLiteralRegex = - new Regex(@"^[\x09\x20-\x26\x28-\x7E\x80-\uD7FF\uE000-\uFFFF]+$"); + new Regex(@"^(([\uD800-\uDBFF][\uDC00-\uDFFF])|[\x09\x20-\x26\x28-\x7E\x80-\uD7FF\uE000-\uFFFF])+$"); public static string EscapeStringValue(string key) { From 97fac79572ba629f7701a2bfbe47d1d87ad27034 Mon Sep 17 00:00:00 2001 From: ITR Date: Sat, 20 Jan 2024 03:07:48 +0100 Subject: [PATCH 06/16] Fixed tests that had technically correct output --- Tomlet.Tests/ExceptionTests.cs | 6 +++--- Tomlet.Tests/TableTests.cs | 11 ++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Tomlet.Tests/ExceptionTests.cs b/Tomlet.Tests/ExceptionTests.cs index 1865244..61ff2ce 100644 --- a/Tomlet.Tests/ExceptionTests.cs +++ b/Tomlet.Tests/ExceptionTests.cs @@ -88,7 +88,7 @@ public void DatesWithUnnecessarySeparatorThrow() => [Fact] public void ImplyingAValueIsATableViaDottedKeyInADocumentWhenItIsNotThrows() => - AssertThrows(() => GetDocument(DeliberatelyIncorrectTestResources.TomlBadDottedKeyExample)); + AssertThrows(() => GetDocument(DeliberatelyIncorrectTestResources.TomlBadDottedKeyExample)); [Fact] public void ImplyingAValueIsATableViaDottedKeyWhenItIsNotThrows() @@ -103,7 +103,7 @@ public void BadEnumValueThrows() => [Fact] public void ReDefiningASubTableAsASubTableArrayThrowsAnException() => - AssertThrows(() => GetDocument(DeliberatelyIncorrectTestResources.ReDefiningSubTableAsSubTableArrayTestInput)); + AssertThrows(() => GetDocument(DeliberatelyIncorrectTestResources.ReDefiningSubTableAsSubTableArrayTestInput)); [Fact] public void RedefiningAKeyAsATableNameThrowsAnException() => @@ -143,7 +143,7 @@ public void TripleQuotedKeysThrow() => [Fact] public void WhitespaceInKeyThrows() => - AssertThrows(() => GetDocument(DeliberatelyIncorrectTestResources.TomlWhitespaceInKeyExample)); + AssertThrows(() => GetDocument(DeliberatelyIncorrectTestResources.TomlWhitespaceInKeyExample)); [Fact] public void MissingEqualsSignThrows() => diff --git a/Tomlet.Tests/TableTests.cs b/Tomlet.Tests/TableTests.cs index 4546b33..cdba79b 100644 --- a/Tomlet.Tests/TableTests.cs +++ b/Tomlet.Tests/TableTests.cs @@ -37,6 +37,7 @@ public void TablesCanHaveQuotedKeyNames() { //Ensure we have enough entries to make sure the table is not re-serialized inline var inputString = "[\"Table Name With Spaces\"]\nkey = \"value\"\nkey2 = 1\nkey3 = 2\nkey4 = 3\nkey5 = 4"; + var expectedOutput = "['Table Name With Spaces']\nkey = \"value\"\nkey2 = 1\nkey3 = 2\nkey4 = 3\nkey5 = 4"; var document = GetDocument(inputString); Assert.Single(document.Keys, "Table Name With Spaces"); @@ -44,17 +45,17 @@ public void TablesCanHaveQuotedKeyNames() Assert.Equal("value", document.GetSubTable("Table Name With Spaces").GetString("key")); var tomlString = document.SerializedValue.Trim(); - Assert.Equal(inputString, tomlString); + Assert.Equal(expectedOutput, tomlString); } [Theory] [InlineData("normal-key", "normal-key")] [InlineData("normal_key", "normal_key")] [InlineData("normalkey", "normalkey")] - [InlineData("\"key with spaces\"", "\"key with spaces\"")] - [InlineData("key!with{}(*%&)random[other+symbols", "\"key!with{}(*%&)random[other+symbols\"")] - [InlineData("key/with/slashes", "\"key/with/slashes\"")] - [InlineData("Nam\\e", "\"Nam\\\\e\"")] + [InlineData("\"key with spaces\"", "'key with spaces'")] + [InlineData("key!with{}(*%&)random[other+symbols", "'key!with{}(*%&)random[other+symbols'")] + [InlineData("key/with/slashes", "'key/with/slashes'")] + [InlineData("Nam\\e", "'Nam\\e'")] public void KeysShouldBeSerializedCorrectly(string inputKey, string expectedKey) { var inputString = $""" From 32737d2f29705478eed5f0ef9ecd96a6d95af494 Mon Sep 17 00:00:00 2001 From: ITR Date: Sat, 20 Jan 2024 03:08:02 +0100 Subject: [PATCH 07/16] Fixed some of my own tests with wrong inputs --- Tomlet.Tests/QuotedKeyTests.cs | 35 +++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/Tomlet.Tests/QuotedKeyTests.cs b/Tomlet.Tests/QuotedKeyTests.cs index 35acbfe..fbc49c8 100644 --- a/Tomlet.Tests/QuotedKeyTests.cs +++ b/Tomlet.Tests/QuotedKeyTests.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Tomlet.Exceptions; using Xunit; @@ -47,7 +46,7 @@ public void IllegalNonDottedKeysThrow(string inputKey) [InlineData("'a.b'.\"c\"", "a.b", "c")] [InlineData("a.'b.c'", "a", "b.c")] [InlineData("\"a\".'b.c'", "a", "b.c")] - [InlineData("\"a\\\".b.c", "a", "b.c")] + [InlineData("\"a\".b.c", "a", "b")] [InlineData("'a.\"b'.c", "a.\"b", "c")] [InlineData("\"a.b\\\"c\".d", "a.b\"c", "d")] public void DottedKeysWork(string inputKey, string expectedKey, string expectedSubkey) @@ -59,14 +58,14 @@ public void DottedKeysWork(string inputKey, string expectedKey, string expectedS } [Theory] - [InlineData("'a.\"b'.c\"")] - [InlineData("\"a.bc\".d\"")] + // [InlineData("'a.\"b'.c\"")] // Illegal in specs, but no harm in reading it + // [InlineData("\"a.bc\".d\"")] // Illegal in specs, but no harm in reading it [InlineData("\"a.b\"c\".d\"")] - [InlineData("\"a.b\"c\".d")] - [InlineData("\"a.b\\\"c\".d\"")] + [InlineData("\"a.b\"c\".d")] + //[InlineData("\"a.b\\\"c\".d\"")] // Illegal in specs, but no harm in reading it [InlineData("'a.b'c'.d")] [InlineData("'a.b\\'c'.d")] - [InlineData("'a.bc'.d'")] + //[InlineData("'a.bc'.d'")] // Illegal in specs, but no harm in reading it public void IllegalDottedKeysThrow(string inputKey) { var inputString = $"{inputKey} = \"value\""; @@ -75,23 +74,25 @@ public void IllegalDottedKeysThrow(string inputKey) [Theory] - [InlineData("\"a\"b\"", @"(?:'""a""b""')|(?:""\\""a\\""b\\"""")")] // Simple or Literal - [InlineData("'a'b'", @"""'a'b'""")] // Simple only - [InlineData("'a\\'b'", @"""'a\\'b'""")] // Simple only - [InlineData("a\"b", @"(?:'a""b')|(?:""a\\""b"")")] // Simple or Literal - [InlineData("a'b", @"""a'b""")] // Simple only - [InlineData("a🐱b", @"(?:'a🐱b')|(?:""a🐱b"")")] // Simple or Literal - [InlineData("'ab\"", @"""'ab\\""""")] // Simple only + [InlineData("\"a\"b\"", @"^(?:'""a""b""')|(?:""\\""a\\""b\\"""")")] // Simple or Literal + [InlineData("'a'b'", @"^""'a'b'""")] // Simple only + [InlineData("'a\\'b'", @"^""'a\\\\'b'""")] // Simple only + [InlineData("a\"b", @"^(?:'a""b')|(?:""a\\""b"")")] // Simple or Literal + [InlineData("a'b", @"^""a'b""")] // Simple only + [InlineData("a🐱b", @"^(?:'a🐱b')|(?:""a🐱b"")")] // Simple or Literal + [InlineData("'ab\"", @"^""'ab\\""""")] // Simple only public void SerializingIllegalKeysWorks(string inputKey, string expectedOutput) { var dict = new Dictionary { - { inputKey, "a" }, + { inputKey, "z" }, }; var document = TomletMain.DocumentFrom(dict); Assert.NotEmpty(document.Keys); var parsedKey = document.Keys.First(); - Assert.Matches(expectedOutput, parsedKey); + Assert.Equal(inputKey, parsedKey); + var serializedString = document.SerializedValue; + Assert.Matches(expectedOutput, serializedString); } } } \ No newline at end of file From 37bcfb780b468225cbbf4b0105a0a07cd002a75a Mon Sep 17 00:00:00 2001 From: ITR Date: Sat, 20 Jan 2024 03:12:37 +0100 Subject: [PATCH 08/16] Deleted obsolete tests --- Tomlet.Tests/ExceptionTests.cs | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/Tomlet.Tests/ExceptionTests.cs b/Tomlet.Tests/ExceptionTests.cs index 61ff2ce..6039da2 100644 --- a/Tomlet.Tests/ExceptionTests.cs +++ b/Tomlet.Tests/ExceptionTests.cs @@ -89,13 +89,6 @@ public void DatesWithUnnecessarySeparatorThrow() => [Fact] public void ImplyingAValueIsATableViaDottedKeyInADocumentWhenItIsNotThrows() => AssertThrows(() => GetDocument(DeliberatelyIncorrectTestResources.TomlBadDottedKeyExample)); - - [Fact] - public void ImplyingAValueIsATableViaDottedKeyWhenItIsNotThrows() - { - var doc = GetDocument(TestResources.ArrayOfEmptyStringTestInput); - AssertThrows(() => doc.Put("Array.a", "foo")); - } [Fact] public void BadEnumValueThrows() => @@ -211,20 +204,7 @@ public void GettingAValueWhichDoesntExistThrows() => public void MismatchingTypesInDeserializationThrow() => AssertThrows(() => TomletMain.To("MyFloat = \"hello\"")); - [Fact] - public void AskingATableForTheValueAssociatedWithAnInvalidKeyThrows() => - AssertThrows(() => GetDocument("").GetBoolean("\"I am invalid'")); - [Fact] public void SettingAnInlineCommentToIncludeANewlineThrows() => AssertThrows(() => TomlDocument.CreateEmpty().Comments.InlineComment = "hello\nworld"); - - [Fact] - public void BadKeysThrow() - { - var doc = GetDocument(""); - - //A key with both quotes - AssertThrows(() => doc.GetLong("\"hello'")); - } } \ No newline at end of file From 233dd5bc943ff14670fe911505f8b50022ed0e0f Mon Sep 17 00:00:00 2001 From: ITR Date: Sat, 20 Jan 2024 03:23:16 +0100 Subject: [PATCH 09/16] Automatic formatting --- Tomlet/Models/TomlTable.cs | 12 ++++----- Tomlet/TomlParser.cs | 50 ++++++++++++++++++++------------------ 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/Tomlet/Models/TomlTable.cs b/Tomlet/Models/TomlTable.cs index 2a5afed..32dbe5a 100644 --- a/Tomlet/Models/TomlTable.cs +++ b/Tomlet/Models/TomlTable.cs @@ -135,7 +135,7 @@ private void WriteValueToStringBuilder(string? keyName, string subKey, StringBui //Then append a newline builder.Append('\n'); } - + internal void ParserPutValue(ref List key, TomlValue value, int lineNumber) { // NB: key is ref to signal that it mutates! @@ -163,10 +163,10 @@ public void Put(string key, T t, bool quote = false) if (tomlValue == null) throw new ArgumentException("Value to insert into TOML table serialized to null.", nameof(t)); - + PutValue(key, tomlValue, quote); } - + private void InternalPutValue(ref List key, TomlValue value, int? lineNumber) { // NB: key is ref to signal that it mutates! @@ -175,7 +175,7 @@ private void InternalPutValue(ref List key, TomlValue value, int? lineNu // TODO: Check what should be done here throw new NoTomlKeyException(lineNumber ?? -1); } - + var ourKeyName = key[0]; key.RemoveAt(0); @@ -212,7 +212,7 @@ private void InternalPutValue(ref List key, TomlValue value, int? lineNu //Yes, get the sub-table to handle the rest of the key existingTable.ParserPutValue(ref key, value, lineNumber!.Value); } - + private void InternalPutValue(string key, TomlValue value, int? lineNumber) { // Because we have a single key, we know it's not dotted @@ -221,7 +221,7 @@ private void InternalPutValue(string key, TomlValue value, int? lineNumber) Entries[key] = value; } - + public bool ContainsKey(string key) { if (key == null) diff --git a/Tomlet/TomlParser.cs b/Tomlet/TomlParser.cs index 2fb4ef5..a5de6e1 100644 --- a/Tomlet/TomlParser.cs +++ b/Tomlet/TomlParser.cs @@ -208,13 +208,13 @@ private string ReadUnquotedKey(TomletStringReader reader) { return sb.ToString(); } - - sb.Append((char) reader.Read()); + + sb.Append((char)reader.Read()); } throw new TomlEndOfFileException(_lineNumber); } - + private TomlValue ReadValue(TomletStringReader reader) { if (!reader.TryPeek(out var startOfValue)) @@ -300,7 +300,7 @@ private TomlValue ReadValue(TomletStringReader reader) var charsRead = reader.ReadChars(4); if (!TrueChars.SequenceEqual(charsRead)) - throw new TomlInvalidValueException(_lineNumber, (char) startOfValue); + throw new TomlInvalidValueException(_lineNumber, (char)startOfValue); value = TomlBoolean.True; break; @@ -311,13 +311,13 @@ private TomlValue ReadValue(TomletStringReader reader) var charsRead = reader.ReadChars(5); if (!FalseChars.SequenceEqual(charsRead)) - throw new TomlInvalidValueException(_lineNumber, (char) startOfValue); + throw new TomlInvalidValueException(_lineNumber, (char)startOfValue); value = TomlBoolean.False; break; } default: - throw new TomlInvalidValueException(_lineNumber, (char) startOfValue); + throw new TomlInvalidValueException(_lineNumber, (char)startOfValue); } reader.SkipWhitespace(); @@ -363,7 +363,7 @@ private TomlValue ReadSingleLineBasicString(TomletStringReader reader, bool cons if (fourDigitUnicodeMode || eightDigitUnicodeMode) { //Handle \u1234 and \U12345678 - unicodeStringBuilder.Append((char) nextChar); + unicodeStringBuilder.Append((char)nextChar); if (fourDigitUnicodeMode && unicodeStringBuilder.Length == 4 || eightDigitUnicodeMode && unicodeStringBuilder.Length == 8) { @@ -382,7 +382,7 @@ private TomlValue ReadSingleLineBasicString(TomletStringReader reader, bool cons if (nextChar.IsNewline()) throw new UnterminatedTomlStringException(_lineNumber); - content.Append((char) nextChar); + content.Append((char)nextChar); } if (consumeClosingQuote) @@ -403,7 +403,7 @@ private string DecipherUnicodeEscapeSequence(string unicodeString, bool fourDigi { //16-bit char var decodedChar = short.Parse(unicodeString, NumberStyles.HexNumber); - return ((char) decodedChar).ToString(); + return ((char)decodedChar).ToString(); } //32-bit char @@ -459,7 +459,6 @@ private TomlValue ReadSingleLineLiteralString(TomletStringReader reader, bool co { //Literally (hah) just read until a single-quote var stringContent = reader.ReadWhile(valueChar => !valueChar.IsSingleQuote() && !valueChar.IsNewline()); - foreach (var i in stringContent.Select(c => (int) c)) i.EnsureLegalChar(_lineNumber); @@ -488,7 +487,7 @@ private TomlValue ReadMultiLineLiteralString(TomletStringReader reader) if (!nextChar.IsSingleQuote()) { - content.Append((char) nextChar); + content.Append((char)nextChar); if (nextChar == '\n') _lineNumber++; //We've wrapped to a new line. @@ -600,7 +599,7 @@ private TomlValue ReadMultiLineBasicString(TomletStringReader reader) if (fourDigitUnicodeMode || eightDigitUnicodeMode) { //Handle \u1234 and \U12345678 - unicodeStringBuilder.Append((char) nextChar); + unicodeStringBuilder.Append((char)nextChar); if (fourDigitUnicodeMode && unicodeStringBuilder.Length == 4 || eightDigitUnicodeMode && unicodeStringBuilder.Length == 8) { @@ -621,7 +620,7 @@ private TomlValue ReadMultiLineBasicString(TomletStringReader reader) if (nextChar == '\n') _lineNumber++; - content.Append((char) nextChar); + content.Append((char)nextChar); continue; } @@ -736,7 +735,7 @@ private TomlTable ReadInlineTable(TomletStringReader reader) //Move to the first key _lineNumber += reader.SkipAnyCommentNewlineWhitespaceEtc(); - var result = new TomlTable {Defined = true}; + var result = new TomlTable { Defined = true }; while (reader.TryPeek(out _)) { @@ -784,7 +783,7 @@ private TomlTable ReadInlineTable(TomletStringReader reader) if (postValueChar.IsEndOfInlineObjectChar()) break; //end of table - throw new TomlInlineTableSeparatorException(_lineNumber, (char) postValueChar); + throw new TomlInlineTableSeparatorException(_lineNumber, (char)postValueChar); } reader.ExpectAndConsume('}'); @@ -797,7 +796,7 @@ private TomlTable ReadTableStatement(TomletStringReader reader, TomlDocument doc { var key = ReadKey(reader); var originalKey = string.Join(".", key.ToArray()); - + var parent = (TomlTable)document; GetLowestTable(ref parent, ref key, 0, typeof(TomlTable)); @@ -858,11 +857,12 @@ private void GetLowestTable(ref TomlTable parent, ref List key, int keep { var subkey = key[0]; usedKeys.Add(subkey); - + if (!parent.Entries.TryGetValue(subkey, out var value)) { break; } + key.RemoveAt(0); if (value is TomlTable subTable) @@ -876,6 +876,7 @@ private void GetLowestTable(ref TomlTable parent, ref List key, int keep { throw new TomlKeyRedefinitionException(_lineNumber, string.Join(".", usedKeys.ToArray())); } + parent = table; } else @@ -905,8 +906,9 @@ private TomlArray ReadTableArrayStatement(TomletStringReader reader, TomlDocumen { throw new MissingIntermediateInTomlTableArraySpecException(_lineNumber, string.Join(".", key.ToArray())); } + var remainingKey = key[0]; - + //Find existing array or make new one TomlArray array; if (parent.Entries.TryGetValue(remainingKey, out var value)) @@ -923,23 +925,23 @@ private TomlArray ReadTableArrayStatement(TomletStringReader reader, TomlDocumen } else { - array = new TomlArray {IsLockedToBeTableArray = true}; + array = new TomlArray { IsLockedToBeTableArray = true }; //Insert into parent table parent.ParserPutValue(ref key, array, _lineNumber); } // Create new table and add it to the array - _currentTable = new TomlTable {Defined = true}; + _currentTable = new TomlTable { Defined = true }; array.ArrayValues.Add(_currentTable); - + return array; } - + private string? ReadAnyPotentialInlineComment(TomletStringReader reader) { if (!reader.ExpectAndConsume('#')) return null; //No comment - + var ret = reader.ReadWhile(c => !c.IsNewline()).Trim(); if (ret.Length < 1) @@ -954,7 +956,7 @@ private TomlArray ReadTableArrayStatement(TomletStringReader reader, TomlDocumen return ret; } - + private string? ReadAnyPotentialMultilineComment(TomletStringReader reader) { var ret = new StringBuilder(); From e9e61a93508ede718ca6890c7be218fdd82c1121 Mon Sep 17 00:00:00 2001 From: ITR Date: Sat, 20 Jan 2024 03:34:08 +0100 Subject: [PATCH 10/16] Removed now unused exception --- Tomlet/Exceptions/TomlWhitespaceInKeyException.cs | 10 ---------- Tomlet/TomlParser.cs | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) delete mode 100644 Tomlet/Exceptions/TomlWhitespaceInKeyException.cs diff --git a/Tomlet/Exceptions/TomlWhitespaceInKeyException.cs b/Tomlet/Exceptions/TomlWhitespaceInKeyException.cs deleted file mode 100644 index 68ccd99..0000000 --- a/Tomlet/Exceptions/TomlWhitespaceInKeyException.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Tomlet.Exceptions; - -public class TomlWhitespaceInKeyException : TomlExceptionWithLine -{ - public TomlWhitespaceInKeyException(int lineNumber) : base(lineNumber) - { - } - - public override string Message => "Found whitespace in an unquoted TOML key at line " + LineNumber; -} \ No newline at end of file diff --git a/Tomlet/TomlParser.cs b/Tomlet/TomlParser.cs index a5de6e1..d4442f4 100644 --- a/Tomlet/TomlParser.cs +++ b/Tomlet/TomlParser.cs @@ -762,7 +762,7 @@ private TomlTable ReadInlineTable(TomletStringReader reader) //Insert into the table result.ParserPutValue(ref key, value, _lineNumber); } - catch (TomlException ex) when (ex is TomlMissingEqualsException or NoTomlKeyException or TomlWhitespaceInKeyException) + catch (TomlException ex) when (ex is TomlMissingEqualsException or NoTomlKeyException) { //Wrap missing keys or equals signs in a parent exception. throw new InvalidTomlInlineTableException(_lineNumber, ex); From abfd5e82e0282f570fb6681afc007a51ad557c53 Mon Sep 17 00:00:00 2001 From: ITR Date: Sat, 20 Jan 2024 03:43:45 +0100 Subject: [PATCH 11/16] Increased test coverage. Added test for redefining dotted key. Made InternalPutValue always have lineNumber since it's used by the parser. Deleted now unused exception. --- Tomlet.Tests/ExceptionTests.cs | 13 +++++++++++++ Tomlet/Exceptions/TomlDottedKeyException.cs | 14 -------------- Tomlet/Models/TomlTable.cs | 16 +++++++--------- 3 files changed, 20 insertions(+), 23 deletions(-) delete mode 100644 Tomlet/Exceptions/TomlDottedKeyException.cs diff --git a/Tomlet.Tests/ExceptionTests.cs b/Tomlet.Tests/ExceptionTests.cs index 6039da2..4830870 100644 --- a/Tomlet.Tests/ExceptionTests.cs +++ b/Tomlet.Tests/ExceptionTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Tomlet.Exceptions; using Tomlet.Models; using Tomlet.Tests.TestModelClasses; @@ -207,4 +208,16 @@ public void MismatchingTypesInDeserializationThrow() => [Fact] public void SettingAnInlineCommentToIncludeANewlineThrows() => AssertThrows(() => TomlDocument.CreateEmpty().Comments.InlineComment = "hello\nworld"); + + [Fact] + public void RedefiningDottedKeyThrows() => AssertThrows( + () => + { + var parser = new TomlParser(); + var tomlDocument = parser.Parse(""" + a.b = 2 + a.b.c = 3 + """); + } + ); } \ No newline at end of file diff --git a/Tomlet/Exceptions/TomlDottedKeyException.cs b/Tomlet/Exceptions/TomlDottedKeyException.cs deleted file mode 100644 index 97ffd95..0000000 --- a/Tomlet/Exceptions/TomlDottedKeyException.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Tomlet.Exceptions -{ - public class TomlDottedKeyException : TomlException - { - private readonly string _key; - - public TomlDottedKeyException(string key) - { - _key = key; - } - - public override string Message => $"Tried to redefine key {_key} as a table (by way of a dotted key) when it's already defined as not being a table."; - } -} \ No newline at end of file diff --git a/Tomlet/Models/TomlTable.cs b/Tomlet/Models/TomlTable.cs index 32dbe5a..a2cf52e 100644 --- a/Tomlet/Models/TomlTable.cs +++ b/Tomlet/Models/TomlTable.cs @@ -167,13 +167,13 @@ public void Put(string key, T t, bool quote = false) PutValue(key, tomlValue, quote); } - private void InternalPutValue(ref List key, TomlValue value, int? lineNumber) + private void InternalPutValue(ref List key, TomlValue value, int lineNumber) { // NB: key is ref to signal that it mutates! if (key.Count == 0) { // TODO: Check what should be done here - throw new NoTomlKeyException(lineNumber ?? -1); + throw new NoTomlKeyException(lineNumber); } var ourKeyName = key[0]; @@ -183,8 +183,8 @@ private void InternalPutValue(ref List key, TomlValue value, int? lineNu if (key.Count == 0) { // Non-dotted keys land here. - if (Entries.ContainsKey(ourKeyName) && lineNumber.HasValue) - throw new TomlKeyRedefinitionException(lineNumber.Value, ourKeyName); + if (Entries.ContainsKey(ourKeyName)) + throw new TomlKeyRedefinitionException(lineNumber, ourKeyName); Entries[ourKeyName] = value; return; @@ -196,7 +196,7 @@ private void InternalPutValue(ref List key, TomlValue value, int? lineNu //We don't have a sub-table with this name defined. That's fine, make one. var subtable = new TomlTable(); Entries[ourKeyName] = subtable; - subtable.ParserPutValue(ref key, value, lineNumber!.Value); + subtable.ParserPutValue(ref key, value, lineNumber); return; } @@ -204,13 +204,11 @@ private void InternalPutValue(ref List key, TomlValue value, int? lineNu if (existingValue is not TomlTable existingTable) { //No - throw an exception - if (lineNumber.HasValue) throw new TomlDottedKeyParserException(lineNumber.Value, ourKeyName); - - throw new TomlDottedKeyException(ourKeyName); + throw new TomlDottedKeyParserException(lineNumber, ourKeyName); } //Yes, get the sub-table to handle the rest of the key - existingTable.ParserPutValue(ref key, value, lineNumber!.Value); + existingTable.ParserPutValue(ref key, value, lineNumber); } private void InternalPutValue(string key, TomlValue value, int? lineNumber) From 996406b07b815b5e2f16979611d16d5f9b0122c5 Mon Sep 17 00:00:00 2001 From: ITR Date: Sat, 20 Jan 2024 03:47:12 +0100 Subject: [PATCH 12/16] Deleted now pointless try-catch. The current exception thrown here already shows line-number. --- .../TomlContainsDottedKeyNonTableException.cs | 14 -------------- Tomlet/TomlParser.cs | 12 +----------- 2 files changed, 1 insertion(+), 25 deletions(-) delete mode 100644 Tomlet/Exceptions/TomlContainsDottedKeyNonTableException.cs diff --git a/Tomlet/Exceptions/TomlContainsDottedKeyNonTableException.cs b/Tomlet/Exceptions/TomlContainsDottedKeyNonTableException.cs deleted file mode 100644 index 4676670..0000000 --- a/Tomlet/Exceptions/TomlContainsDottedKeyNonTableException.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Tomlet.Exceptions -{ - public class TomlContainsDottedKeyNonTableException : TomlException - { - internal readonly string Key; - - public TomlContainsDottedKeyNonTableException(string key) - { - Key = key; - } - - public override string Message => $"A call was made on a TOML table which attempted to access a sub-key of {Key}, but the value it refers to is not a table"; - } -} \ No newline at end of file diff --git a/Tomlet/TomlParser.cs b/Tomlet/TomlParser.cs index d4442f4..e780075 100644 --- a/Tomlet/TomlParser.cs +++ b/Tomlet/TomlParser.cs @@ -814,17 +814,7 @@ private TomlTable ReadTableStatement(TomletStringReader reader, TomlDocument doc if (key.Count > 0) { table = new TomlTable { Defined = true }; - try - { - parent.ParserPutValue(ref key, table, _lineNumber); - } - catch (TomlContainsDottedKeyNonTableException e) - { - //Re-throw with correct line number and exception type. - //To be clear - here we're re-defining a NON-TABLE key as a table, so this is a dotted key exception - //while the one above is a TableRedefinition exception because it's re-defining a key which is already a table. - throw new TomlDottedKeyParserException(_lineNumber, e.Key); - } + parent.ParserPutValue(ref key, table, _lineNumber); } if (!reader.TryPeek(out _)) From d36aaa5497381026d8cbd53dd53a3b5052927408 Mon Sep 17 00:00:00 2001 From: ITR Date: Sat, 20 Jan 2024 04:07:58 +0100 Subject: [PATCH 13/16] Added test for invalid characters in key --- Tomlet.Tests/ExceptionTests.cs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Tomlet.Tests/ExceptionTests.cs b/Tomlet.Tests/ExceptionTests.cs index 4830870..e025144 100644 --- a/Tomlet.Tests/ExceptionTests.cs +++ b/Tomlet.Tests/ExceptionTests.cs @@ -220,4 +220,27 @@ public void RedefiningDottedKeyThrows() => AssertThrows( + () => + { + var document = TomletMain.TomlStringFrom(new Dictionary { { text, "a" } }); + } + ); + } } \ No newline at end of file From 97002e3141c653b54bc3dc5df4be18891fe85e86 Mon Sep 17 00:00:00 2001 From: ITR Date: Sat, 20 Jan 2024 04:09:22 +0100 Subject: [PATCH 14/16] Removed unused method --- Tomlet/TomlKeyUtils.cs | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/Tomlet/TomlKeyUtils.cs b/Tomlet/TomlKeyUtils.cs index 66d173c..5663d18 100644 --- a/Tomlet/TomlKeyUtils.cs +++ b/Tomlet/TomlKeyUtils.cs @@ -7,31 +7,6 @@ namespace Tomlet internal static class TomlKeyUtils { private static readonly Regex UnquotedKeyRegex = new Regex("^[a-zA-Z0-9-_]+$"); - - private static int FindNextUnescapedQuote(string input, int startingIndex) - { - var i = startingIndex; - var isEscaped = false; - for (; i < input.Length; i++) - { - if (input[i] == '\\') - { - isEscaped = !isEscaped; - continue; - } - - if (input[i] != '"' || isEscaped) - { - isEscaped = false; - continue; - } - - return i; - } - - return -1; // Return -1 if no unescaped quote is found - } - internal static string FullStringToProperKey(string key) { var canBeUnquoted = UnquotedKeyRegex.Match(key).Success; From 526e7b4906659580fcb5530cd5cb49f6f3e9a077 Mon Sep 17 00:00:00 2001 From: ITR Date: Sat, 20 Jan 2024 04:16:29 +0100 Subject: [PATCH 15/16] Added test for trailing comments --- Tomlet.Tests/CommentSerializationTests.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Tomlet.Tests/CommentSerializationTests.cs b/Tomlet.Tests/CommentSerializationTests.cs index cf733db..1c9e0aa 100644 --- a/Tomlet.Tests/CommentSerializationTests.cs +++ b/Tomlet.Tests/CommentSerializationTests.cs @@ -121,4 +121,17 @@ public void CommentAttributesWork() Assert.Equal("The password you use to access the mailbox", doc.GetValue("password").Comments.InlineComment); Assert.Equal("The rules for the mailbox follow", doc.GetArray("rules").Comments.PrecedingComment); } + + [Fact] + public void TrailingCommentWorks() + { + var document = TomlDocument.CreateEmpty(); + document.TrailingComment = "Hello World!"; + var output = document.SerializedValue; + Assert.Equal("\n# Hello World!", output); + + document.Put("Hello", "World"); + output = document.SerializedValue; + Assert.Equal("Hello = \"World\"\n\n# Hello World!", output); + } } \ No newline at end of file From 9061871a70b934fc3b425100ad899da92af6c48b Mon Sep 17 00:00:00 2001 From: ITR Date: Sat, 20 Jan 2024 04:41:32 +0100 Subject: [PATCH 16/16] Increased test coverage. I think rider had a field-day when getting to generate Designer.rs Added tests for String Reader since Backtrack wasn't covered. Added test for Document.ToString() Added test for more places it's possible to reach the end of the file. Added test for different ways of writing numbers. --- ...beratelyIncorrectTestResources.Designer.cs | 324 ++++++++++++++---- .../DeliberatelyIncorrectTestResources.resx | 6 + Tomlet.Tests/ExceptionTests.cs | 8 + Tomlet.Tests/ObjectToStringTests.cs | 13 + Tomlet.Tests/StringTests.cs | 34 +- Tomlet.Tests/TomlStringReaderTests.cs | 58 ++++ 6 files changed, 358 insertions(+), 85 deletions(-) create mode 100644 Tomlet.Tests/TomlStringReaderTests.cs diff --git a/Tomlet.Tests/DeliberatelyIncorrectTestResources.Designer.cs b/Tomlet.Tests/DeliberatelyIncorrectTestResources.Designer.cs index 8052348..c86c95a 100644 --- a/Tomlet.Tests/DeliberatelyIncorrectTestResources.Designer.cs +++ b/Tomlet.Tests/DeliberatelyIncorrectTestResources.Designer.cs @@ -11,32 +11,46 @@ namespace Tomlet.Tests { using System; - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [System.Diagnostics.DebuggerNonUserCodeAttribute()] - [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class DeliberatelyIncorrectTestResources { - private static System.Resources.ResourceManager resourceMan; + private static global::System.Resources.ResourceManager resourceMan; - private static System.Globalization.CultureInfo resourceCulture; + private static global::System.Globalization.CultureInfo resourceCulture; - [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal DeliberatelyIncorrectTestResources() { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] - internal static System.Resources.ResourceManager ResourceManager { + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { get { - if (object.Equals(null, resourceMan)) { - System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Tomlet.Tests.DeliberatelyIncorrectTestResources", typeof(DeliberatelyIncorrectTestResources).Assembly); + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Tomlet.Tests.DeliberatelyIncorrectTestResources", typeof(DeliberatelyIncorrectTestResources).Assembly); resourceMan = temp; } return resourceMan; } } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] - internal static System.Globalization.CultureInfo Culture { + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -45,207 +59,369 @@ internal static System.Globalization.CultureInfo Culture { } } - internal static string TomlBadInlineTableExample { + /// + /// Looks up a localized string similar to [fruit.physical] # subtable, but to which parent element should it belong? + ///color = "red" + ///shape = "round" + /// + ///[[fruit]] # parser must throw an error upon discovering that "fruit" is + /// # an array rather than a table + ///name = "apple". + /// + internal static string DefiningAsArrayWhenAlreadyTableTestInput { get { - return ResourceManager.GetString("TomlBadInlineTableExample", resourceCulture); + return ResourceManager.GetString("DefiningAsArrayWhenAlreadyTableTestInput", resourceCulture); } } - internal static string TomlBadEscapeExample { + /// + /// Looks up a localized string similar to type = { name = "Nail" } + ///type.edible = false # INVALID. + /// + internal static string InlineTableLockedTestInput { get { - return ResourceManager.GetString("TomlBadEscapeExample", resourceCulture); + return ResourceManager.GetString("InlineTableLockedTestInput", resourceCulture); } } - internal static string TomlBadNumberExample { + /// + /// Looks up a localized string similar to [fruit] + ///apple = "red" + /// + ///[fruit.apple] + ///texture = "smooth". + /// + internal static string KeyRedefinitionViaTableTestInput { get { - return ResourceManager.GetString("TomlBadNumberExample", resourceCulture); + return ResourceManager.GetString("KeyRedefinitionViaTableTestInput", resourceCulture); } } - internal static string TomlBadDateExample { + /// + /// Looks up a localized string similar to # INVALID TOML DOC + ///fruits = [] + /// + ///[[fruits]] # Not allowed. + /// + internal static string ReDefiningAnArrayAsATableArrayIsAnErrorTestInput { get { - return ResourceManager.GetString("TomlBadDateExample", resourceCulture); + return ResourceManager.GetString("ReDefiningAnArrayAsATableArrayIsAnErrorTestInput", resourceCulture); } } - internal static string TomlTruncatedFileExample { + /// + /// Looks up a localized string similar to # INVALID TOML DOC + ///[[fruits]] + ///name = "apple" + /// + ///[[fruits.varieties]] + ///name = "red delicious" + /// + ///# INVALID: This table conflicts with the previous array of tables + ///[fruits.varieties] + ///name = "granny smith" + /// + ///[fruits.physical] + ///color = "red" + ///shape = "round" + /// + ///# INVALID: This array of tables conflicts with the previous table + ///[[fruits.physical]] + ///color = "green". + /// + internal static string ReDefiningSubTableAsSubTableArrayTestInput { get { - return ResourceManager.GetString("TomlTruncatedFileExample", resourceCulture); + return ResourceManager.GetString("ReDefiningSubTableAsSubTableArrayTestInput", resourceCulture); } } - internal static string TomlTableArrayWithMissingIntermediateExample { + /// + /// Looks up a localized string similar to [fruit] + ///apple = "red" + /// + ///[fruit] + ///orange = "orange". + /// + internal static string TableRedefinitionTestInput { get { - return ResourceManager.GetString("TomlTableArrayWithMissingIntermediateExample", resourceCulture); + return ResourceManager.GetString("TableRedefinitionTestInput", resourceCulture); } } - internal static string TomlMissingKeyExample { + /// + /// Looks up a localized string similar to myArr = [1, 2, 3}. + /// + internal static string TomlBadArrayExample { get { - return ResourceManager.GetString("TomlMissingKeyExample", resourceCulture); + return ResourceManager.GetString("TomlBadArrayExample", resourceCulture); } } - internal static string TomlLocalTimeWithOffsetExample { + /// + /// Looks up a localized string similar to date = 2021-13-13T08:30:00. + /// + internal static string TomlBadDateExample { get { - return ResourceManager.GetString("TomlLocalTimeWithOffsetExample", resourceCulture); + return ResourceManager.GetString("TomlBadDateExample", resourceCulture); } } - internal static string TomlBadArrayExample { + /// + /// Looks up a localized string similar to array = [1,2,3] + ///[array.a] + ///value = 1. + /// + internal static string TomlBadDottedKeyExample { get { - return ResourceManager.GetString("TomlBadArrayExample", resourceCulture); + return ResourceManager.GetString("TomlBadDottedKeyExample", resourceCulture); } } - internal static string TomlDateTimeWithNoSeparatorExample { + /// + /// Looks up a localized string similar to Enum = "QUX". + /// + internal static string TomlBadEnumExample { get { - return ResourceManager.GetString("TomlDateTimeWithNoSeparatorExample", resourceCulture); + return ResourceManager.GetString("TomlBadEnumExample", resourceCulture); } } - internal static string TomlUnnecessaryDateTimeSeparatorExample { + /// + /// Looks up a localized string similar to invalid = "hello\z". + /// + internal static string TomlBadEscapeExample { get { - return ResourceManager.GetString("TomlUnnecessaryDateTimeSeparatorExample", resourceCulture); + return ResourceManager.GetString("TomlBadEscapeExample", resourceCulture); } } - internal static string TomlBadDottedKeyExample { + /// + /// Looks up a localized string similar to point = { x = 1, y 2 }. + /// + internal static string TomlBadInlineTableExample { get { - return ResourceManager.GetString("TomlBadDottedKeyExample", resourceCulture); + return ResourceManager.GetString("TomlBadInlineTableExample", resourceCulture); } } - internal static string TomlBadEnumExample { + /// + /// Looks up a localized string similar to number = 1.e3. + /// + internal static string TomlBadNumberExample { get { - return ResourceManager.GetString("TomlBadEnumExample", resourceCulture); + return ResourceManager.GetString("TomlBadNumberExample", resourceCulture); } } - internal static string ReDefiningAnArrayAsATableArrayIsAnErrorTestInput { + /// + /// Looks up a localized string similar to key = ""hello, world"". + /// + internal static string TomlBadStringExample { get { - return ResourceManager.GetString("ReDefiningAnArrayAsATableArrayIsAnErrorTestInput", resourceCulture); + return ResourceManager.GetString("TomlBadStringExample", resourceCulture); } } - internal static string DefiningAsArrayWhenAlreadyTableTestInput { + /// + /// Looks up a localized string similar to badDate = 2021-05-0512:00:00. + /// + internal static string TomlDateTimeWithNoSeparatorExample { get { - return ResourceManager.GetString("DefiningAsArrayWhenAlreadyTableTestInput", resourceCulture); + return ResourceManager.GetString("TomlDateTimeWithNoSeparatorExample", resourceCulture); } } - internal static string KeyRedefinitionViaTableTestInput { + /// + /// Looks up a localized string similar to myTable..badKey = "hello". + /// + internal static string TomlDoubleDottedKeyExample { get { - return ResourceManager.GetString("KeyRedefinitionViaTableTestInput", resourceCulture); + return ResourceManager.GetString("TomlDoubleDottedKeyExample", resourceCulture); } } - internal static string ReDefiningSubTableAsSubTableArrayTestInput { + /// + /// Looks up a localized string similar to key = {a = "one" b = "two"}. + /// + internal static string TomlInlineTableWithMissingSeparatorExample { get { - return ResourceManager.GetString("ReDefiningSubTableAsSubTableArrayTestInput", resourceCulture); + return ResourceManager.GetString("TomlInlineTableWithMissingSeparatorExample", resourceCulture); } } + /// + /// Looks up a localized string similar to key = {a = 1, b = 2, + ///c = 3}. + /// internal static string TomlInlineTableWithNewlineExample { get { return ResourceManager.GetString("TomlInlineTableWithNewlineExample", resourceCulture); } } - internal static string TomlDoubleDottedKeyExample { + /// + /// Looks up a localized string similar to badTime = 07:00:00Z. + /// + internal static string TomlLocalTimeWithOffsetExample { get { - return ResourceManager.GetString("TomlDoubleDottedKeyExample", resourceCulture); + return ResourceManager.GetString("TomlLocalTimeWithOffsetExample", resourceCulture); } } - internal static string TomlInlineTableWithMissingSeparatorExample { + /// + /// Looks up a localized string similar to "key" 1. + /// + internal static string TomlMissingEqualsExample { get { - return ResourceManager.GetString("TomlInlineTableWithMissingSeparatorExample", resourceCulture); + return ResourceManager.GetString("TomlMissingEqualsExample", resourceCulture); } } - internal static string TomlBadStringExample { + /// + /// Looks up a localized string similar to = "a". + /// + internal static string TomlMissingKeyExample { get { - return ResourceManager.GetString("TomlBadStringExample", resourceCulture); + return ResourceManager.GetString("TomlMissingKeyExample", resourceCulture); } } - internal static string TomlTripleQuotedKeyExample { + /// + /// Looks up a localized string similar to . + /// + internal static string TomlNullBytesExample { get { - return ResourceManager.GetString("TomlTripleQuotedKeyExample", resourceCulture); + return ResourceManager.GetString("TomlNullBytesExample", resourceCulture); } } - internal static string TomlWhitespaceInKeyExample { + /// + /// Looks up a localized string similar to [[numbers.one]] + ///value = 1. + /// + internal static string TomlTableArrayWithMissingIntermediateExample { get { - return ResourceManager.GetString("TomlWhitespaceInKeyExample", resourceCulture); + return ResourceManager.GetString("TomlTableArrayWithMissingIntermediateExample", resourceCulture); } } - internal static string TomlMissingEqualsExample { + /// + /// Looks up a localized string similar to key = """literally three quotes: """""". + /// + internal static string TomlTripleDoubleQuoteInStringExample { get { - return ResourceManager.GetString("TomlMissingEqualsExample", resourceCulture); + return ResourceManager.GetString("TomlTripleDoubleQuoteInStringExample", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to """key""" = 1. + /// + internal static string TomlTripleQuotedKeyExample { + get { + return ResourceManager.GetString("TomlTripleQuotedKeyExample", resourceCulture); } } + /// + /// Looks up a localized string similar to key = '''literally three quotes: ''''''. + /// internal static string TomlTripleSingleQuoteInStringExample { get { return ResourceManager.GetString("TomlTripleSingleQuoteInStringExample", resourceCulture); } } - internal static string TomlTripleDoubleQuoteInStringExample { + /// + /// Looks up a localized string similar to string =. + /// + internal static string TomlTruncatedFileExample { get { - return ResourceManager.GetString("TomlTripleDoubleQuoteInStringExample", resourceCulture); + return ResourceManager.GetString("TomlTruncatedFileExample", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [. + /// + internal static string TomlTruncatedFileExample2 { + get { + return ResourceManager.GetString("TomlTruncatedFileExample2", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to string. + /// + internal static string TomlTruncatedFileExample3 { + get { + return ResourceManager.GetString("TomlTruncatedFileExample3", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to badDate = 2021-05-05T. + /// + internal static string TomlUnnecessaryDateTimeSeparatorExample { + get { + return ResourceManager.GetString("TomlUnnecessaryDateTimeSeparatorExample", resourceCulture); } } + /// + /// Looks up a localized string similar to arr = [1, 2 + ///anotherKey = "hello". + /// internal static string TomlUnterminatedArrayExample { get { return ResourceManager.GetString("TomlUnterminatedArrayExample", resourceCulture); } } + /// + /// Looks up a localized string similar to "key = 1. + /// internal static string TomlUnterminatedQuotedKeyExample { get { return ResourceManager.GetString("TomlUnterminatedQuotedKeyExample", resourceCulture); } } + /// + /// Looks up a localized string similar to key = "one. + /// internal static string TomlUnterminatedStringExample { get { return ResourceManager.GetString("TomlUnterminatedStringExample", resourceCulture); } } + /// + /// Looks up a localized string similar to [[array + ///one = 1. + /// internal static string TomlUnterminatedTableArrayExample { get { return ResourceManager.GetString("TomlUnterminatedTableArrayExample", resourceCulture); } } + /// + /// Looks up a localized string similar to [table + ///one = 1. + /// internal static string TomlUnterminatedTableExample { get { return ResourceManager.GetString("TomlUnterminatedTableExample", resourceCulture); } } - internal static string InlineTableLockedTestInput { - get { - return ResourceManager.GetString("InlineTableLockedTestInput", resourceCulture); - } - } - - internal static string TableRedefinitionTestInput { - get { - return ResourceManager.GetString("TableRedefinitionTestInput", resourceCulture); - } - } - - internal static string TomlNullBytesExample { + /// + /// Looks up a localized string similar to key name = 1. + /// + internal static string TomlWhitespaceInKeyExample { get { - return ResourceManager.GetString("TomlNullBytesExample", resourceCulture); + return ResourceManager.GetString("TomlWhitespaceInKeyExample", resourceCulture); } } } diff --git a/Tomlet.Tests/DeliberatelyIncorrectTestResources.resx b/Tomlet.Tests/DeliberatelyIncorrectTestResources.resx index 14993b7..a8030bb 100644 --- a/Tomlet.Tests/DeliberatelyIncorrectTestResources.resx +++ b/Tomlet.Tests/DeliberatelyIncorrectTestResources.resx @@ -33,6 +33,12 @@ string = + + [ + + + string + [[numbers.one]] value = 1 diff --git a/Tomlet.Tests/ExceptionTests.cs b/Tomlet.Tests/ExceptionTests.cs index e025144..42ddc33 100644 --- a/Tomlet.Tests/ExceptionTests.cs +++ b/Tomlet.Tests/ExceptionTests.cs @@ -63,6 +63,14 @@ public void InvalidDatesThrow() => public void TruncatedFilesThrow() => AssertThrows(() => GetDocument(DeliberatelyIncorrectTestResources.TomlTruncatedFileExample)); + [Fact] + public void TruncatedFilesThrow2() => + AssertThrows(() => GetDocument(DeliberatelyIncorrectTestResources.TomlTruncatedFileExample2)); + + [Fact] + public void TruncatedFilesThrow3() => + AssertThrows(() => GetDocument(DeliberatelyIncorrectTestResources.TomlTruncatedFileExample3)); + [Fact] public void UndefinedTableArraysThrow() => AssertThrows(() => GetDocument(DeliberatelyIncorrectTestResources.TomlTableArrayWithMissingIntermediateExample)); diff --git a/Tomlet.Tests/ObjectToStringTests.cs b/Tomlet.Tests/ObjectToStringTests.cs index a7f4939..ea30f38 100644 --- a/Tomlet.Tests/ObjectToStringTests.cs +++ b/Tomlet.Tests/ObjectToStringTests.cs @@ -1,4 +1,5 @@ using System; +using Tomlet.Models; using Tomlet.Tests.TestModelClasses; using Xunit; @@ -111,5 +112,17 @@ public void AttemptingToDirectlySerializeNullThrows() //We need to use a type of T that actually has something to serialize Assert.Throws(() => TomletMain.DocumentFrom(typeof(SimplePrimitiveTestClass), null!, null)); } + + [Fact] + public void DocumentToStringWorks() + { + var document = TomlDocument.CreateEmpty(); + var output = document.ToString(); + Assert.Equal("Toml root document (0 entries)", output); + + document.Put("a", "a"); + output = document.ToString(); + Assert.Equal("Toml root document (1 entries)", output); + } } } \ No newline at end of file diff --git a/Tomlet.Tests/StringTests.cs b/Tomlet.Tests/StringTests.cs index 57d8b68..8ea6714 100644 --- a/Tomlet.Tests/StringTests.cs +++ b/Tomlet.Tests/StringTests.cs @@ -187,17 +187,29 @@ public void SerializingPathStringsPrefersLiterals() } [Fact] - public void StringValueIsSameAsToString() - { - var document = GetDocument(TestResources.StringEqualsToStringInput); - - Assert.Collection(document.Entries.Values, - entry => Assert.Equal(Assert.IsType(entry).StringValue, Assert.IsType(entry).ToString()), - entry => Assert.Equal(Assert.IsType(entry).StringValue, Assert.IsType(entry).ToString()), - entry => Assert.Equal(Assert.IsType(entry).StringValue, Assert.IsType(entry).ToString()), - entry => Assert.Equal(Assert.IsType(entry).StringValue, Assert.IsType(entry).ToString()), - entry => Assert.Equal(Assert.IsType(entry).StringValue, Assert.IsType(entry).ToString()) - ); + public void StringValueIsSameAsToString() + { + var document = GetDocument(TestResources.StringEqualsToStringInput); + + Assert.Collection(document.Entries.Values, + entry => Assert.Equal(Assert.IsType(entry).StringValue, Assert.IsType(entry).ToString()), + entry => Assert.Equal(Assert.IsType(entry).StringValue, Assert.IsType(entry).ToString()), + entry => Assert.Equal(Assert.IsType(entry).StringValue, Assert.IsType(entry).ToString()), + entry => Assert.Equal(Assert.IsType(entry).StringValue, Assert.IsType(entry).ToString()), + entry => Assert.Equal(Assert.IsType(entry).StringValue, Assert.IsType(entry).ToString()) + ); + } + + [Fact] + public void AllNumbersWork() + { + var document = GetDocument("a = [0o12, 0x12, 0b10010110]"); + var values = document + .GetArray("a") + .Select(Assert.IsType) + .Select(value => value.Value) + .ToArray(); + Assert.Equal(new long[] { 10, 0x12, 0b10010110 }, values); } } } \ No newline at end of file diff --git a/Tomlet.Tests/TomlStringReaderTests.cs b/Tomlet.Tests/TomlStringReaderTests.cs new file mode 100644 index 0000000..cb8cdc5 --- /dev/null +++ b/Tomlet.Tests/TomlStringReaderTests.cs @@ -0,0 +1,58 @@ +using System; +using Xunit; + +namespace Tomlet.Tests +{ + public class TomletStringReaderTest + { + [Fact] + public void PeekWhenPositionIsAtEndShouldReturnMinusOne() + { + var reader = new TomletStringReader("t"); + reader.Read(); // Move to the end + var result = reader.Peek(); + Assert.Equal(-1, result); + } + + [Fact] + public void ReadWhenPositionIsAtEndShouldReturnMinusOne() + { + var reader = new TomletStringReader("t"); + reader.Read(); // Move to the end + var result = reader.Read(); + Assert.Equal(-1, result); + } + + [Fact] + public void ReadBlockWhenPositionIsAtEndShouldReturnZero() + { + var reader = new TomletStringReader("test"); + reader.Read(); // Move to the end + reader.Read(); + reader.Read(); + reader.Read(); + var buffer = new char[10]; + var result = reader.ReadBlock(buffer, 0, 5); + Assert.Equal(0, result); + } + + [Fact] + public void BacktrackWhenAmountIsGreaterThanPositionShouldThrowException() + { + var reader = new TomletStringReader("test"); + Assert.Throws(() => reader.Backtrack(10)); + } + + [Fact] + public void ReadBlockShouldReadCorrectly() + { + var reader = new TomletStringReader("test"); + + var buffer = new char[10]; + var result = reader.ReadBlock(buffer, 0, 4); + + Assert.Equal(4, result); + Assert.Equal("test", new string(buffer, 0, result)); + } + } +}