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();
}
}