diff --git a/Sledge.Formats.Tests/TestStringExtensions.cs b/Sledge.Formats.Tests/TestStringExtensions.cs index 48bfa72..cd4c73e 100644 --- a/Sledge.Formats.Tests/TestStringExtensions.cs +++ b/Sledge.Formats.Tests/TestStringExtensions.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Sledge.Formats.Tests @@ -37,5 +39,34 @@ public void TestSplitWithQuotes() "\"a b\"\t\"c\"".SplitWithQuotes() ); } + + [TestMethod] + public void TestSplitWithQuotes_EscapedQuotes() + { + CollectionAssert.AreEqual( + new[] { "Simple" }, + "Simple".SplitWithQuotes() + ); + CollectionAssert.AreEqual( + new[] { "No", "Quotes" }, + "No Quotes".SplitWithQuotes() + ); + CollectionAssert.AreEqual( + new[] { "With", "Quotes" }, + @"""With"" ""Quotes""".SplitWithQuotes() + ); + CollectionAssert.AreEqual( + new[] { "Empty", "" }, + @"""Empty"" """"".SplitWithQuotes() + ); + CollectionAssert.AreEqual( + new[] { "With", @"""Escaped"" Quotes" }, + @"""With"" ""\""Escaped\"" Quotes""".SplitWithQuotes() + ); + CollectionAssert.AreEqual( + new[] { "Json", @"{ ""Key"": ""Value"" }" }, + @"""Json"" ""{ \""Key\"": \""Value\"" }""".SplitWithQuotes() + ); + } } } \ No newline at end of file diff --git a/Sledge.Formats.Tests/Valve/TestSerialisedObject.cs b/Sledge.Formats.Tests/Valve/TestSerialisedObject.cs new file mode 100644 index 0000000..c88923e --- /dev/null +++ b/Sledge.Formats.Tests/Valve/TestSerialisedObject.cs @@ -0,0 +1,145 @@ +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Sledge.Formats.Valve; + +namespace Sledge.Formats.Tests.Valve +{ + [TestClass] + public class TestSerialisedObject + { + private static Stream Streamify(string s) + { + var ms = new MemoryStream(Encoding.UTF8.GetBytes(s)); + ms.Seek(0, SeekOrigin.Begin); + return ms; + } + + private const string Q = "\""; + + [TestMethod] + public void TestLoadingSimple() + { + var fmt = new SerialisedObjectFormatter(); + using var input = Streamify($@"Test +{{ + {Q}Key1{Q} {Q}Value1{Q} + {Q}Key2{Q} {Q}Value2{Q} +}} +"); + var output = fmt.Deserialize(input).ToList(); + Assert.AreEqual(1, output.Count); + Assert.AreEqual("Test", output[0].Name); + Assert.AreEqual(0, output[0].Children.Count); + Assert.AreEqual(2, output[0].Properties.Count); + Assert.AreEqual("Key1", output[0].Properties[0].Key); + Assert.AreEqual("Key2", output[0].Properties[1].Key); + Assert.AreEqual("Value1", output[0].Properties[0].Value); + Assert.AreEqual("Value2", output[0].Properties[1].Value); + } + + [TestMethod] + public void TestLoadingKeyOrder() + { + var fmt = new SerialisedObjectFormatter(); + using var input = Streamify($@"Test +{{ + {Q}Key{Q} {Q}1{Q} + {Q}Key{Q} {Q}3{Q} + {Q}Key{Q} {Q}2{Q} +}} +"); + var output = fmt.Deserialize(input).ToList(); + Assert.AreEqual(1, output.Count); + Assert.AreEqual("Test", output[0].Name); + Assert.AreEqual(0, output[0].Children.Count); + Assert.AreEqual(3, output[0].Properties.Count); + Assert.AreEqual("Key", output[0].Properties[0].Key); + Assert.AreEqual("Key", output[0].Properties[1].Key); + Assert.AreEqual("Key", output[0].Properties[2].Key); + Assert.AreEqual("1", output[0].Properties[0].Value); + Assert.AreEqual("3", output[0].Properties[1].Value); + Assert.AreEqual("2", output[0].Properties[2].Value); + } + + [TestMethod] + public void TestLoadingChildren() + { + var fmt = new SerialisedObjectFormatter(); + using var input = Streamify($@"Test1 +{{ + {Q}A{Q} {Q}1{Q} + Test2 + {{ + {Q}B{Q} {Q}2{Q} + Test3 + {{ + {Q}C{Q} {Q}3{Q} + }} + }} + Test2 + {{ + }} + Test2 + {{ + {Q}D{Q} {Q}4{Q} + }} +}} +Test4 +{{ + {Q}E{Q} {Q}5{Q} +}} +"); + var output = fmt.Deserialize(input).ToList(); + Assert.AreEqual(2, output.Count); + + Assert.AreEqual("Test1", output[0].Name); + Assert.AreEqual(3, output[0].Children.Count); + Assert.AreEqual("A", output[0].Properties[0].Key); + + Assert.AreEqual("Test2", output[0].Children[0].Name); + Assert.AreEqual(1, output[0].Children[0].Children.Count); + Assert.AreEqual(1, output[0].Children[0].Properties.Count); + Assert.AreEqual("B", output[0].Children[0].Properties[0].Key); + Assert.AreEqual("2", output[0].Children[0].Properties[0].Value); + + Assert.AreEqual("Test3", output[0].Children[0].Children[0].Name); + Assert.AreEqual(0, output[0].Children[0].Children[0].Children.Count); + Assert.AreEqual(1, output[0].Children[0].Children[0].Properties.Count); + Assert.AreEqual("C", output[0].Children[0].Children[0].Properties[0].Key); + Assert.AreEqual("3", output[0].Children[0].Children[0].Properties[0].Value); + + Assert.AreEqual("Test2", output[0].Children[1].Name); + Assert.AreEqual(0, output[0].Children[1].Children.Count); + Assert.AreEqual(0, output[0].Children[1].Properties.Count); + + Assert.AreEqual("Test2", output[0].Children[2].Name); + Assert.AreEqual("D", output[0].Children[2].Properties[0].Key); + Assert.AreEqual("4", output[0].Children[2].Properties[0].Value); + + Assert.AreEqual("Test4", output[1].Name); + Assert.AreEqual(0, output[1].Children.Count); + Assert.AreEqual("E", output[1].Properties[0].Key); + Assert.AreEqual("5", output[1].Properties[0].Value); + } + + [TestMethod] + public void TestEscapedQuotes() + { + var fmt = new SerialisedObjectFormatter(); + using var input = Streamify($@"Test +{{ + {Q}Key\{Q}With\{Q}Quotes{Q} {Q}Quoted\{Q}Value{Q} +}} +"); + var output = fmt.Deserialize(input).ToList(); + Assert.AreEqual(1, output.Count); + Assert.AreEqual("Test", output[0].Name); + Assert.AreEqual(0, output[0].Children.Count); + Assert.AreEqual(1, output[0].Properties.Count); + Assert.AreEqual("Key\"With\"Quotes", output[0].Properties[0].Key); + Assert.AreEqual("Quoted\"Value", output[0].Properties[0].Value); + } + } +} diff --git a/Sledge.Formats/Sledge.Formats.csproj b/Sledge.Formats/Sledge.Formats.csproj index 9ea8326..01e6977 100644 --- a/Sledge.Formats/Sledge.Formats.csproj +++ b/Sledge.Formats/Sledge.Formats.csproj @@ -16,7 +16,7 @@ Included XML documentation MIT - 1.0.1 + 1.0.2 diff --git a/Sledge.Formats/StringExtensions.cs b/Sledge.Formats/StringExtensions.cs index 5bce54c..9d6391d 100644 --- a/Sledge.Formats/StringExtensions.cs +++ b/Sledge.Formats/StringExtensions.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Sledge.Formats { @@ -13,37 +14,59 @@ public static class StringExtensions /// The string to split /// The characters to split by. Defaults to space and tab characters if not specified. /// The character which indicates the start or end of a quote + /// The character which indicates that the next character should be escaped /// The split result, with split characters removed - public static string[] SplitWithQuotes(this string line, char[] splitCharacters = null, char quoteChar = '"') + public static string[] SplitWithQuotes(this string line, char[] splitCharacters = null, char quoteChar = '"', + char escapeChar = '\\') { - if (splitCharacters == null) splitCharacters = new[] { ' ', '\t' }; + if (splitCharacters == null) splitCharacters = new[] {' ', '\t'}; var result = new List(); - int i; - for (i = 0; i < line.Length; i++) + char[] builder = new char[line.Length]; + int b = 0; + var inQuote = false; + for (var i = 0; i < line.Length; i++) { - var split = line.IndexOfAny(splitCharacters, i); - var quote = line.IndexOf(quoteChar, i); - - if (split < 0) split = line.Length; - if (quote < 0) quote = line.Length; - - if (quote < split) + var c = line[i]; + if (c == escapeChar) + { + // Escape character, skip the next character + i++; + if (line.Length == i) throw new InvalidOperationException("Unexpected escape character at end of string"); + builder[b++] = line[i]; + } + else if (c == quoteChar && !inQuote) + { + // Quote character to begin a token + if (b != 0) throw new InvalidOperationException("Unexpected quote - quotes must be at the beginning of a token"); + inQuote = true; + } + else if (c == quoteChar) + { + // Quote character to end a token + inQuote = false; + result.Add(new string(builder, 0, b)); + b = 0; + i++; + if (line.Length < i && Array.IndexOf(splitCharacters, line[i]) < 0) throw new InvalidOperationException("Missing split character - closing quotes must complete a token"); + } + else if (!inQuote && Array.IndexOf(splitCharacters, c) >= 0) { - if (quote > i) result.Add(line.Substring(i, quote)); - var nextQuote = line.IndexOf(quoteChar, quote + 1); - if (nextQuote < 0) nextQuote = line.Length; - result.Add(line.Substring(quote + 1, nextQuote - quote - 1)); - i = nextQuote; + // Split character outside of quotes, split here + if (b > 0) result.Add(new string(builder, 0, b)); + b = 0; } else { - if (split > i) result.Add(line.Substring(i, split - i)); - i = split; + builder[b++] = line[i]; } } + + if (inQuote) throw new InvalidOperationException("Unclosed quote at end of string"); + if (b > 0) result.Add(new string(builder, 0, b)); + return result.ToArray(); } } -} +} \ No newline at end of file diff --git a/Sledge.Formats/Valve/SerialisedObjectFormatter.cs b/Sledge.Formats/Valve/SerialisedObjectFormatter.cs index 0d693b5..432bb57 100644 --- a/Sledge.Formats/Valve/SerialisedObjectFormatter.cs +++ b/Sledge.Formats/Valve/SerialisedObjectFormatter.cs @@ -46,7 +46,7 @@ public IEnumerable Deserialize(Stream serializationStream) { using (var reader = new StreamReader(serializationStream, Encoding.UTF8, true, 1024, true)) { - return Parse(reader); + return Parse(reader).ToList(); } }