Skip to content

Commit

Permalink
Handle escaped quotes in the string split
Browse files Browse the repository at this point in the history
Fixes #3
  • Loading branch information
LogicAndTrick committed Feb 9, 2021
1 parent 56f12ba commit 72abe60
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 22 deletions.
31 changes: 31 additions & 0 deletions Sledge.Formats.Tests/TestStringExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Sledge.Formats.Tests
Expand Down Expand Up @@ -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()
);
}
}
}
145 changes: 145 additions & 0 deletions Sledge.Formats.Tests/Valve/TestSerialisedObject.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
2 changes: 1 addition & 1 deletion Sledge.Formats/Sledge.Formats.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<PackageReleaseNotes>Included XML documentation</PackageReleaseNotes>
<PackageLicenseFile></PackageLicenseFile>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Version>1.0.1</Version>
<Version>1.0.2</Version>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
Expand Down
63 changes: 43 additions & 20 deletions Sledge.Formats/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;

namespace Sledge.Formats
{
Expand All @@ -13,37 +14,59 @@ public static class StringExtensions
/// <param name="line">The string to split</param>
/// <param name="splitCharacters">The characters to split by. Defaults to space and tab characters if not specified.</param>
/// <param name="quoteChar">The character which indicates the start or end of a quote</param>
/// <param name="escapeChar">The character which indicates that the next character should be escaped</param>
/// <returns>The split result, with split characters removed</returns>
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<string>();

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();
}
}
}
}
2 changes: 1 addition & 1 deletion Sledge.Formats/Valve/SerialisedObjectFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public IEnumerable<SerialisedObject> Deserialize(Stream serializationStream)
{
using (var reader = new StreamReader(serializationStream, Encoding.UTF8, true, 1024, true))
{
return Parse(reader);
return Parse(reader).ToList();
}
}

Expand Down

0 comments on commit 72abe60

Please sign in to comment.