diff --git a/ValveKeyValue/ValveKeyValue.Console/ValveKeyValue.Console.csproj b/ValveKeyValue/ValveKeyValue.Console/ValveKeyValue.Console.csproj index 90627608..7ae41713 100644 --- a/ValveKeyValue/ValveKeyValue.Console/ValveKeyValue.Console.csproj +++ b/ValveKeyValue/ValveKeyValue.Console/ValveKeyValue.Console.csproj @@ -1,7 +1,7 @@ Exe - net9.0 + net10.0 LatestMajor enable enable diff --git a/ValveKeyValue/ValveKeyValue.Test/ApiSurfaceTestCase.cs b/ValveKeyValue/ValveKeyValue.Test/ApiSurfaceTestCase.cs index 0b2ef39c..2ef91cab 100644 --- a/ValveKeyValue/ValveKeyValue.Test/ApiSurfaceTestCase.cs +++ b/ValveKeyValue/ValveKeyValue.Test/ApiSurfaceTestCase.cs @@ -13,6 +13,7 @@ public void ApiSurfaceIsWellKnown() var expected = TestDataHelper.ReadTextResource("apisurface.txt"); var actual = GenerateApiSurface(typeof(KVObject).GetTypeInfo().Assembly); + File.WriteAllText(Path.Combine(TestContext.CurrentContext.TestDirectory, "apisurface.txt"), actual); Assert.That(actual, Is.EqualTo(expected), "This may indicate a breaking change."); } diff --git a/ValveKeyValue/ValveKeyValue.Test/KVBasicObjectIndexerTestCase.cs b/ValveKeyValue/ValveKeyValue.Test/KVBasicObjectIndexerTestCase.cs index 25133f74..a83a1fa7 100644 --- a/ValveKeyValue/ValveKeyValue.Test/KVBasicObjectIndexerTestCase.cs +++ b/ValveKeyValue/ValveKeyValue.Test/KVBasicObjectIndexerTestCase.cs @@ -3,12 +3,9 @@ namespace ValveKeyValue.Test class KVBasicObjectIndexerTestCase { [Test] - public void IndexerOnValueNodeThrowsException() + public void IndexerOnValueNodeReturnsNull() { - Assert.That( - () => data["baz"], - Throws.Exception.InstanceOf() - .With.Message.EqualTo("This operation on a KVObject can only be used when the value has children.")); + Assert.That(data["baz"], Is.Null); } KVObject data; diff --git a/ValveKeyValue/ValveKeyValue.Test/KVValueToStringTestCase.cs b/ValveKeyValue/ValveKeyValue.Test/KVValueToStringTestCase.cs index 8fab8835..d10398ba 100644 --- a/ValveKeyValue/ValveKeyValue.Test/KVValueToStringTestCase.cs +++ b/ValveKeyValue/ValveKeyValue.Test/KVValueToStringTestCase.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Linq; namespace ValveKeyValue.Test { @@ -13,7 +14,7 @@ public static IEnumerable ToStringTestCases { yield return new TestCaseData(new KVObject("a", "blah").Value).Returns("blah"); yield return new TestCaseData(new KVObject("a", "yay").Value).Returns("yay"); - yield return new TestCaseData(new KVObject("a", []).Value).Returns("[Collection]").SetName("{m} - Empty Collection"); + yield return new TestCaseData(new KVObject("a", Enumerable.Empty()).Value).Returns("[Collection]").SetName("{m} - Empty Collection"); yield return new TestCaseData(new KVObject("a", [new KVObject("boo", "aah")]).Value).Returns("[Collection]").SetName("{m} - Collection With Value"); } } diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/.gitattributes b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/.gitattributes new file mode 100644 index 00000000..317b9fcd --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/.gitattributes @@ -0,0 +1,3 @@ +# Keep intended line endings to test parser +*.kv3 eol=lf +*_crlf.kv3 eol=crlf diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/array.kv3 b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/array.kv3 new file mode 100644 index 00000000..cefef2aa --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/array.kv3 @@ -0,0 +1,33 @@ + +{ + arrayValue = + [ + "a", + "b", + ] + arrayOnSingleLine = [ 16.7551, 20.3763, 19.6448 ] + arrayNoSpace=[1.3763,19.6448] + arrayMixedTypes = + [ + "a", + 1, + true, + false, + null, + { + foo = "bar" + }, + [ + 1, 3, 3, 7 + ], + #[ + 11 FF + ], + resource:"hello.world", + """ +multiline +string +""", + -69.420 + ] +} diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/array_from_kv1.kv3 b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/array_from_kv1.kv3 new file mode 100644 index 00000000..39ce3a62 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/array_from_kv1.kv3 @@ -0,0 +1,41 @@ + +{ + arrayValue = { + "0" = "a" + "1" = "b" + } + arrayOnSingleLine = { + "0" = 16.755100 + "1" = 20.376300 + "2" = 19.644800 + } + arrayNoSpace = { + "0" = 1.376300 + "1" = 19.644800 + } + arrayMixedTypes = { + "0" = "a" + "1" = 1 + "2" = 1 + "3" = 0 + "4" = "" + "5" = { + foo = "bar" + } + "6" = { + "0" = 1 + "1" = 3 + "2" = 3 + "3" = 7 + } + "7" = "11 FF" + "8" = "hello.world" + "9" = """ +multiline +string +""" + "10" = -69.420000 + } + test = "success" + test = "success" +} diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/array_kv1.vdf b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/array_kv1.vdf new file mode 100644 index 00000000..7dc6cbc6 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/array_kv1.vdf @@ -0,0 +1,44 @@ +"" +{ + "arrayValue" + { + "0" "a" + "1" "b" + } + "arrayOnSingleLine" + { + "0" "16.7551" + "1" "20.3763" + "2" "19.6448" + } + "arrayNoSpace" + { + "0" "1.3763" + "1" "19.6448" + } + "arrayMixedTypes" + { + "0" "a" + "1" "1" + "2" "1" + "3" "0" + "4" "" + "5" + { + "foo" "bar" + } + "6" + { + "0" "1" + "1" "3" + "2" "3" + "3" "7" + } + "7" "11 FF" + "8" "hello.world" + "9" "multiline +string" + "10" "-69.42" + } + "test" "success" +} diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/array_nested.kv3 b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/array_nested.kv3 new file mode 100644 index 00000000..643a1517 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/array_nested.kv3 @@ -0,0 +1,40 @@ + +{ + array = [ + 1, + 2, + 3, + { + array2 = [ + 4, + 5, + 6, + { + something = "something" + array3 = [ + 7, + 8, + 9, + ] + test = "abc" + }, + 10, + ] + test2 = "def" + }, + "string", + 11, + 12, + [ + 13, + 14, + 15, + [ + 16, + 17, + 18, + ], + ], + 19, + ] +} diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/array_serialized.kv3 b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/array_serialized.kv3 new file mode 100644 index 00000000..d8943ea2 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/array_serialized.kv3 @@ -0,0 +1,42 @@ + +{ + arrayValue = [ + "a", + "b", + ] + arrayOnSingleLine = [ + 16.755100, + 20.376300, + 19.644800, + ] + arrayNoSpace = [ + 1.376300, + 19.644800, + ] + arrayMixedTypes = [ + "a", + 1, + true, + false, + null, + { + foo = "bar" + }, + [ + 1, + 3, + 3, + 7, + ], + #[ + 11 FF + ], + resource:"hello.world", + """ +multiline +string +""", + -69.420000, + ] + test = "success" +} diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/basic.kv3 b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/basic.kv3 new file mode 100644 index 00000000..fcabd606 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/basic.kv3 @@ -0,0 +1,4 @@ + +{ + foo = "bar" +} diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/binary_blob.kv3 b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/binary_blob.kv3 new file mode 100644 index 00000000..4323149a --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/binary_blob.kv3 @@ -0,0 +1,8 @@ + +{ + array = + #[ + 00 11 22 33 44 55 66 77 88 99 + AA BB CC DD FF + ] +} diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/comments.kv3 b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/comments.kv3 new file mode 100644 index 00000000..9d773e33 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/comments.kv3 @@ -0,0 +1,10 @@ + +{ + // single line comment + /* multi + line + comment */ + foo = "bar" // comment after + one = /* comment in between */ /* another comment in between */ "1" /* comment after */ + two /* comment in between */ = "2" /* comment after */ +} diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/entity_name.kv3 b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/entity_name.kv3 new file mode 100644 index 00000000..00a70aef --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/entity_name.kv3 @@ -0,0 +1,4 @@ + +{ + name = entity_name:"some_entity" +} diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/escape_sequences.kv3 b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/escape_sequences.kv3 new file mode 100644 index 00000000..cf99164d --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/escape_sequences.kv3 @@ -0,0 +1,8 @@ + +{ + newline = "hello\nworld" + tab = "hello\tworld" + backslash = "hello\\world" + quote = "hello\"world" + combined = "line1\nline2\ttab\\slash\"quote" +} diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/flagged_value.kv3 b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/flagged_value.kv3 new file mode 100644 index 00000000..7c625688 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/flagged_value.kv3 @@ -0,0 +1,19 @@ + +{ + foo = resource:"bar" + bar = resource|"foo" + uppercase = RESOURCE:"foo" + flaggedNumber = panorama:-1234 + multipleFlags = resource:resource_name|subclass:"cool value" + soundEvent = soundEvent:"event sound" + noFlags = 5 + + flaggedObject = panorama:{ + 1 = soundEvent:"test1" + 2 = "test2" + 3 = subclass:[ + "test3" + ] + 4 = resource_name:"test4" + } +} diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/flagged_value_from_kv1.kv3 b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/flagged_value_from_kv1.kv3 new file mode 100644 index 00000000..de2c7daf --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/flagged_value_from_kv1.kv3 @@ -0,0 +1,20 @@ + +{ + foo = "bar" + bar = "foo" + uppercase = "foo" + flaggedNumber = -1234 + multipleFlags = "cool value" + soundEvent = "event sound" + noFlags = 5 + flaggedObject = { + "1" = "test1" + "2" = "test2" + "3" = { + "0" = "test3" + } + "4" = "test4" + } + test = "success" + test = "success" +} diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/flagged_value_kv1.vdf b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/flagged_value_kv1.vdf new file mode 100644 index 00000000..dde518a0 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/flagged_value_kv1.vdf @@ -0,0 +1,21 @@ +"" +{ + "foo" "bar" + "bar" "foo" + "uppercase" "foo" + "flaggedNumber" "-1234" + "multipleFlags" "cool value" + "soundEvent" "event sound" + "noFlags" "5" + "flaggedObject" + { + "1" "test1" + "2" "test2" + "3" + { + "0" "test3" + } + "4" "test4" + } + "test" "success" +} diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/flagged_value_serialized.kv3 b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/flagged_value_serialized.kv3 new file mode 100644 index 00000000..f09a5dfb --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/flagged_value_serialized.kv3 @@ -0,0 +1,19 @@ + +{ + foo = resource:"bar" + bar = resource:"foo" + uppercase = resource:"foo" + flaggedNumber = panorama:-1234 + multipleFlags = subclass:"cool value" + soundEvent = soundevent:"event sound" + noFlags = 5 + flaggedObject = panorama:{ + "1" = soundevent:"test1" + "2" = "test2" + "3" = subclass:[ + "test3", + ] + "4" = resource_name:"test4" + } + test = "success" +} diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/multiline.kv3 b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/multiline.kv3 new file mode 100644 index 00000000..09fc4c67 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/multiline.kv3 @@ -0,0 +1,13 @@ + +{ + multiLineStringValue = """ +First line of a multi-line string literal. +Second line of a multi-line string literal. +""" + multiLineWithQuotesInside = """ +hmm this \"""is awkward +\""" yes +""" + singleQuotesButWithNewLineAnyway = "hello +valve" +} diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/multiline_crlf.kv3 b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/multiline_crlf.kv3 new file mode 100644 index 00000000..73ba27d3 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/multiline_crlf.kv3 @@ -0,0 +1,7 @@ + +{ + multiLineStringValue = """ +First line of a multi-line string literal. +Second line of a multi-line string literal. +""" +} diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/object.kv3 b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/object.kv3 new file mode 100644 index 00000000..06d3a2e5 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/object.kv3 @@ -0,0 +1,9 @@ + +{ + a = { + foo = "bar" + b = { + c = "d" + } + } +} diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_array.kv3 b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_array.kv3 new file mode 100644 index 00000000..23180206 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_array.kv3 @@ -0,0 +1,23 @@ + +[ + "a", + 1, + true, + false, + null, + { + foo = "bar" + }, + [ + 1, 3, 3, 7 + ], + #[ + 11 FF + ], + resource:"hello.world", + """ +multiline +string +""", + -69.420 +] diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_binary_blob.kv3 b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_binary_blob.kv3 new file mode 100644 index 00000000..576d37da --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_binary_blob.kv3 @@ -0,0 +1,5 @@ + +#[ + 00 11 22 33 44 55 66 77 88 99 + AA BB CC DD FF +] diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_flagged_object.kv3 b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_flagged_object.kv3 new file mode 100644 index 00000000..b225c276 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_flagged_object.kv3 @@ -0,0 +1,4 @@ + +panorama:{ + foo = resource:"bar" +} diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_flagged_string.kv3 b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_flagged_string.kv3 new file mode 100644 index 00000000..3c25bf65 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_flagged_string.kv3 @@ -0,0 +1,2 @@ + +resource:"cool_resource.txt" diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_float.kv3 b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_float.kv3 new file mode 100644 index 00000000..e980b0c1 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_float.kv3 @@ -0,0 +1,2 @@ + +-1337.401 diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_multiline.kv3 b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_multiline.kv3 new file mode 100644 index 00000000..a79c6405 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_multiline.kv3 @@ -0,0 +1,5 @@ + +""" +First line of a multi-line string literal. +Second line of a multi-line string literal. +""" diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_null.kv3 b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_null.kv3 new file mode 100644 index 00000000..e00a7e2f --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_null.kv3 @@ -0,0 +1,2 @@ + +null \ No newline at end of file diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_number.kv3 b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_number.kv3 new file mode 100644 index 00000000..97480416 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_number.kv3 @@ -0,0 +1,2 @@ + +1234567890 \ No newline at end of file diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_number_negative.kv3 b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_number_negative.kv3 new file mode 100644 index 00000000..b9b5ff00 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_number_negative.kv3 @@ -0,0 +1,2 @@ + +-1234567890 \ No newline at end of file diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_string.kv3 b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_string.kv3 new file mode 100644 index 00000000..e627908c --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/root_string.kv3 @@ -0,0 +1,2 @@ + +"cool 123 string" diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/types.kv3 b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/types.kv3 new file mode 100644 index 00000000..22b2563d --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/types.kv3 @@ -0,0 +1,35 @@ + +{ + boolFalseValue = false + boolTrueValue = true + nullValue = null + intValue = 128 + doubleValue = 64.123 + negativeIntValue = -1337 + negativeDoubleValue = -0.1337 + plusIntValue = +1337 + plusDoubleValue = +0.1337 + stringValue = "hello world" + negativeMaxInt = -9223372036854775807 + positiveMaxInt = 18446744073709551615 + doubleMaxValue = 62147483647.1337 + doubleNegativeMaxValue = -62147483647.1337 + doubleExponent = 1.23456E+2 + intWithStringSuffix = 123foobar + singleQuotes = 'string' + singleQuotesWithQuotesInside = 'string is "pretty" cool' + key_with._various.separators = "test" + "quoted key with : {} terminators" = "test quoted key" + """ +this is a multi +line +key +""" = "multi line key parsed" + empty.string = "" + 1 = "one" + a = "alpha" + 22 = "two" + a3 = "three" + bb = "bravo" + "" = "empty key" +} diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/types_serialized.kv3 b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/types_serialized.kv3 new file mode 100644 index 00000000..2477dcad --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/types_serialized.kv3 @@ -0,0 +1,35 @@ + +{ + boolFalseValue = false + boolTrueValue = true + nullValue = null + intValue = 128 + doubleValue = 64.123000 + negativeIntValue = -1337 + negativeDoubleValue = -0.133700 + plusIntValue = 1337 + plusDoubleValue = 0.133700 + stringValue = "hello world" + negativeMaxInt = -9223372036854775807 + positiveMaxInt = 18446744073709551615 + doubleMaxValue = 62147483647.133700 + doubleNegativeMaxValue = -62147483647.133700 + doubleExponent = 123.456000 + intWithStringSuffix = "123foobar" + singleQuotes = "string" + singleQuotesWithQuotesInside = "string is \"pretty\" cool" + key_with._various.separators = "test" + "quoted key with : {} terminators" = "test quoted key" + "this is a multi\nline\nkey" = "multi line key parsed" + empty.string = "" + "1" = "one" + a = "alpha" + "22" = "two" + a3 = "three" + bb = "bravo" + "" = "empty key" + multiLineString = """ +hello +world +""" +} diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/apisurface.txt b/ValveKeyValue/ValveKeyValue.Test/Test Data/apisurface.txt index 69930fbc..d862b758 100644 --- a/ValveKeyValue/ValveKeyValue.Test/Test Data/apisurface.txt +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/apisurface.txt @@ -43,6 +43,7 @@ public class ValveKeyValue.KVArrayValue public bool Equals(object obj); protected void Finalize(); public int get_Count(); + public ValveKeyValue.KVFlag get_Flag(); public bool get_IsReadOnly(); public ValveKeyValue.KVValue get_Item(int key); public ValveKeyValue.KVValue get_Item(string key); @@ -56,6 +57,7 @@ public class ValveKeyValue.KVArrayValue protected object MemberwiseClone(); public bool Remove(ValveKeyValue.KVValue item); public void RemoveAt(int index); + public void set_Flag(ValveKeyValue.KVFlag value); public void set_Item(int key, ValveKeyValue.KVValue value); public bool ToBoolean(IFormatProvider provider); public byte ToByte(IFormatProvider provider); @@ -83,12 +85,52 @@ public class ValveKeyValue.KVBinaryBlob public bool Equals(object obj); protected void Finalize(); public Memory`1[[byte]] get_Bytes(); + public ValveKeyValue.KVFlag get_Flag(); public ValveKeyValue.KVValue get_Item(string key); public ValveKeyValue.KVValueType get_ValueType(); public int GetHashCode(); public Type GetType(); public TypeCode GetTypeCode(); protected object MemberwiseClone(); + public void set_Flag(ValveKeyValue.KVFlag value); + public bool ToBoolean(IFormatProvider provider); + public byte ToByte(IFormatProvider provider); + public char ToChar(IFormatProvider provider); + public DateTime ToDateTime(IFormatProvider provider); + public decimal ToDecimal(IFormatProvider provider); + public double ToDouble(IFormatProvider provider); + public short ToInt16(IFormatProvider provider); + public int ToInt32(IFormatProvider provider); + public long ToInt64(IFormatProvider provider); + public sbyte ToSByte(IFormatProvider provider); + public float ToSingle(IFormatProvider provider); + public string ToString(); + public string ToString(IFormatProvider provider); + public object ToType(Type conversionType, IFormatProvider provider); + public ushort ToUInt16(IFormatProvider provider); + public uint ToUInt32(IFormatProvider provider); + public ulong ToUInt64(IFormatProvider provider); +} + +public class ValveKeyValue.KVCollectionValue +{ + public .ctor(); + public void Add(ValveKeyValue.KVObject value); + public void AddRange(System.Collections.Generic.IEnumerable`1[[ValveKeyValue.KVObject]] values); + public bool Equals(object obj); + protected void Finalize(); + public ValveKeyValue.KVObject Get(string name); + public int get_Count(); + public ValveKeyValue.KVFlag get_Flag(); + public ValveKeyValue.KVValue get_Item(string key); + public ValveKeyValue.KVValueType get_ValueType(); + public System.Collections.Generic.IEnumerator`1[[ValveKeyValue.KVObject]] GetEnumerator(); + public int GetHashCode(); + public Type GetType(); + public TypeCode GetTypeCode(); + protected object MemberwiseClone(); + public void Set(string name, ValveKeyValue.KVValue value); + public void set_Flag(ValveKeyValue.KVFlag value); public bool ToBoolean(IFormatProvider provider); public byte ToByte(IFormatProvider provider); public char ToChar(IFormatProvider provider); @@ -110,14 +152,23 @@ public class ValveKeyValue.KVBinaryBlob public class ValveKeyValue.KVDocument { - public .ctor(string name, ValveKeyValue.KVValue value); + public .ctor(ValveKeyValue.KVHeader header, string name, ValveKeyValue.KVValue value); public void Add(ValveKeyValue.KVObject value); + public void Add(ValveKeyValue.KVValue value); + public void AddProperty(string name, ValveKeyValue.KVValue value); + public bool ContainsKey(string name); public bool Equals(object obj); protected void Finalize(); public System.Collections.Generic.IEnumerable`1[[ValveKeyValue.KVObject]] get_Children(); + public System.Collections.Generic.IEnumerable`1[[ValveKeyValue.KVValue]] get_ChildrenValues(); + public int get_Count(); + public ValveKeyValue.KVHeader get_Header(); + public bool get_IsArray(); + public ValveKeyValue.KVValue get_Item(int index); public ValveKeyValue.KVValue get_Item(string key); public string get_Name(); public ValveKeyValue.KVValue get_Value(); + public ValveKeyValue.KVObject GetChild(string name); public System.Collections.Generic.IEnumerator`1[[ValveKeyValue.KVObject]] GetEnumerator(); public int GetHashCode(); public Type GetType(); @@ -126,6 +177,45 @@ public class ValveKeyValue.KVDocument public string ToString(); } +public sealed enum ValveKeyValue.KVFlag +{ + None = 0; + Resource = 1; + ResourceName = 2; + Panorama = 3; + SoundEvent = 4; + SubClass = 5; + EntityName = 6; + + public int CompareTo(object target); + public bool Equals(object obj); + protected void Finalize(); + public int GetHashCode(); + public Type GetType(); + public TypeCode GetTypeCode(); + public bool HasFlag(Enum flag); + protected object MemberwiseClone(); + public string ToString(); + public string ToString(IFormatProvider provider); + public string ToString(string format); + public string ToString(string format, IFormatProvider provider); +} + +public class ValveKeyValue.KVHeader +{ + public .ctor(); + public bool Equals(object obj); + protected void Finalize(); + public ValveKeyValue.KeyValues3.KV3ID get_Encoding(); + public ValveKeyValue.KeyValues3.KV3ID get_Format(); + public int GetHashCode(); + public Type GetType(); + protected object MemberwiseClone(); + public void set_Encoding(ValveKeyValue.KeyValues3.KV3ID value); + public void set_Format(ValveKeyValue.KeyValues3.KV3ID value); + public string ToString(); +} + public sealed class ValveKeyValue.KVIgnoreAttribute { public .ctor(); @@ -140,17 +230,59 @@ public sealed class ValveKeyValue.KVIgnoreAttribute public string ToString(); } +public class ValveKeyValue.KVNullValue +{ + public .ctor(); + public bool Equals(object obj); + protected void Finalize(); + public ValveKeyValue.KVFlag get_Flag(); + public ValveKeyValue.KVValue get_Item(string key); + public ValveKeyValue.KVValueType get_ValueType(); + public int GetHashCode(); + public Type GetType(); + public TypeCode GetTypeCode(); + protected object MemberwiseClone(); + public void set_Flag(ValveKeyValue.KVFlag value); + public bool ToBoolean(IFormatProvider provider); + public byte ToByte(IFormatProvider provider); + public char ToChar(IFormatProvider provider); + public DateTime ToDateTime(IFormatProvider provider); + public decimal ToDecimal(IFormatProvider provider); + public double ToDouble(IFormatProvider provider); + public short ToInt16(IFormatProvider provider); + public int ToInt32(IFormatProvider provider); + public long ToInt64(IFormatProvider provider); + public sbyte ToSByte(IFormatProvider provider); + public float ToSingle(IFormatProvider provider); + public string ToString(); + public string ToString(IFormatProvider provider); + public object ToType(Type conversionType, IFormatProvider provider); + public ushort ToUInt16(IFormatProvider provider); + public uint ToUInt32(IFormatProvider provider); + public ulong ToUInt64(IFormatProvider provider); +} + public class ValveKeyValue.KVObject { + public .ctor(string name); public .ctor(string name, System.Collections.Generic.IEnumerable`1[[ValveKeyValue.KVObject]] items); + public .ctor(string name, System.Collections.Generic.IEnumerable`1[[ValveKeyValue.KVValue]] items); public .ctor(string name, ValveKeyValue.KVValue value); public void Add(ValveKeyValue.KVObject value); + public void Add(ValveKeyValue.KVValue value); + public void AddProperty(string name, ValveKeyValue.KVValue value); + public bool ContainsKey(string name); public bool Equals(object obj); protected void Finalize(); public System.Collections.Generic.IEnumerable`1[[ValveKeyValue.KVObject]] get_Children(); + public System.Collections.Generic.IEnumerable`1[[ValveKeyValue.KVValue]] get_ChildrenValues(); + public int get_Count(); + public bool get_IsArray(); + public ValveKeyValue.KVValue get_Item(int index); public ValveKeyValue.KVValue get_Item(string key); public string get_Name(); public ValveKeyValue.KVValue get_Value(); + public ValveKeyValue.KVObject GetChild(string name); public System.Collections.Generic.IEnumerator`1[[ValveKeyValue.KVObject]] GetEnumerator(); public int GetHashCode(); public Type GetType(); @@ -178,6 +310,7 @@ public sealed enum ValveKeyValue.KVSerializationFormat { KeyValues1Text = 0; KeyValues1Binary = 1; + KeyValues3Text = 2; public int CompareTo(object target); public bool Equals(object obj); @@ -235,6 +368,7 @@ public class ValveKeyValue.KVValue protected .ctor(); public bool Equals(object obj); protected void Finalize(); + public ValveKeyValue.KVFlag get_Flag(); public ValveKeyValue.KVValue get_Item(string key); public ValveKeyValue.KVValueType get_ValueType(); public int GetHashCode(); @@ -257,12 +391,17 @@ public class ValveKeyValue.KVValue public static ushort op_Explicit(ValveKeyValue.KVValue value); public static IntPtr op_Explicit(ValveKeyValue.KVValue value); public static ValveKeyValue.KVValue op_Implicit(bool value); + public static ValveKeyValue.KVValue op_Implicit(double value); public static ValveKeyValue.KVValue op_Implicit(float value); public static ValveKeyValue.KVValue op_Implicit(int value); public static ValveKeyValue.KVValue op_Implicit(IntPtr value); public static ValveKeyValue.KVValue op_Implicit(long value); + public static ValveKeyValue.KVValue op_Implicit(short value); public static ValveKeyValue.KVValue op_Implicit(string value); + public static ValveKeyValue.KVValue op_Implicit(uint value); public static ValveKeyValue.KVValue op_Implicit(ulong value); + public static ValveKeyValue.KVValue op_Implicit(ushort value); + public void set_Flag(ValveKeyValue.KVFlag value); public bool ToBoolean(IFormatProvider provider); public byte ToByte(IFormatProvider provider); public char ToChar(IFormatProvider provider); @@ -331,3 +470,51 @@ public sealed class ValveKeyValue.StringTable public string ToString(); } +public class ValveKeyValue.KeyValues3.Encoding +{ + public .ctor(); + public bool Equals(object obj); + protected void Finalize(); + public static Guid get_Binary(); + public static Guid get_BinaryAuto(); + public static Guid get_BinaryBlockCompressed(); + public static Guid get_BinaryLZ4(); + public static Guid get_BinaryZstd(); + public static Guid get_Text(); + public int GetHashCode(); + public Type GetType(); + protected object MemberwiseClone(); + public string ToString(); +} + +public class ValveKeyValue.KeyValues3.Format +{ + public .ctor(); + public bool Equals(object obj); + protected void Finalize(); + public static Guid get_Generic(); + public int GetHashCode(); + public Type GetType(); + protected object MemberwiseClone(); + public string ToString(); +} + +public sealed struct ValveKeyValue.KeyValues3.KV3ID +{ + public .ctor(string Name, Guid Id); + public void Deconstruct(out String& Name, out Guid& Id); + public bool Equals(object obj); + public bool Equals(ValveKeyValue.KeyValues3.KV3ID other); + protected void Finalize(); + public Guid get_Id(); + public string get_Name(); + public int GetHashCode(); + public Type GetType(); + protected object MemberwiseClone(); + public static bool op_Equality(ValveKeyValue.KeyValues3.KV3ID left, ValveKeyValue.KeyValues3.KV3ID right); + public static bool op_Inequality(ValveKeyValue.KeyValues3.KV3ID left, ValveKeyValue.KeyValues3.KV3ID right); + public void set_Id(Guid value); + public void set_Name(string value); + public string ToString(); +} + diff --git a/ValveKeyValue/ValveKeyValue.Test/TextKV3/BasicKV3TestCases.cs b/ValveKeyValue/ValveKeyValue.Test/TextKV3/BasicKV3TestCases.cs new file mode 100644 index 00000000..df993ef9 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/TextKV3/BasicKV3TestCases.cs @@ -0,0 +1,233 @@ +namespace ValveKeyValue.Test.TextKV3 +{ + class BasicKV3TestCases + { + [Test] + public void DeserializesHeaderAndValue() + { + using var stream = TestDataHelper.OpenResource("TextKV3.basic.kv3"); + var data = KVSerializer.Create(KVSerializationFormat.KeyValues3Text).Deserialize(stream); + + Assert.That((string)data["foo"], Is.EqualTo("bar")); + } + + [Test] + public void DeserializesFlaggedValues() + { + using var stream = TestDataHelper.OpenResource("TextKV3.flagged_value.kv3"); + var data = KVSerializer.Create(KVSerializationFormat.KeyValues3Text).Deserialize(stream); + + Assert.Multiple(() => + { + Assert.That(data["foo"].Flag, Is.EqualTo(KVFlag.Resource)); + Assert.That((string)data["foo"], Is.EqualTo("bar")); + + Assert.That(data["bar"].Flag, Is.EqualTo(KVFlag.Resource)); + Assert.That((string)data["bar"], Is.EqualTo("foo")); + + Assert.That(data["multipleFlags"].Flag, Is.EqualTo(KVFlag.SubClass)); + Assert.That((string)data["multipleFlags"], Is.EqualTo("cool value")); + + Assert.That(data["flaggedNumber"].Flag, Is.EqualTo(KVFlag.Panorama)); + Assert.That((long)data["flaggedNumber"], Is.EqualTo(-1234)); + + Assert.That(data["soundEvent"].Flag, Is.EqualTo(KVFlag.SoundEvent)); + Assert.That((string)data["soundEvent"], Is.EqualTo("event sound")); + + Assert.That(data["noFlags"].Flag, Is.EqualTo(KVFlag.None)); + Assert.That((long)data["noFlags"], Is.EqualTo(5)); + + Assert.That(data["flaggedObject"].Flag, Is.EqualTo(KVFlag.Panorama)); + Assert.That(data["flaggedObject"]["1"].Flag, Is.EqualTo(KVFlag.SoundEvent)); + Assert.That(data["flaggedObject"]["2"].Flag, Is.EqualTo(KVFlag.None)); + Assert.That(data["flaggedObject"]["3"].Flag, Is.EqualTo(KVFlag.SubClass)); + Assert.That(data["flaggedObject"]["4"].Flag, Is.EqualTo(KVFlag.ResourceName)); + }); + } + + [Test] + public void DeserializesMultilineStrings() + { + using var stream = TestDataHelper.OpenResource("TextKV3.multiline.kv3"); + var data = KVSerializer.Create(KVSerializationFormat.KeyValues3Text).Deserialize(stream); + + Assert.Multiple(() => + { + Assert.That((string)data["multiLineStringValue"], Is.EqualTo("First line of a multi-line string literal.\nSecond line of a multi-line string literal.")); + Assert.That((string)data["multiLineWithQuotesInside"], Is.EqualTo("hmm this \\\"\"\"is awkward\n\\\"\"\" yes")); + Assert.That((string)data["singleQuotesButWithNewLineAnyway"], Is.EqualTo("hello\nvalve")); + }); + } + + [Test] + public void DeserializesMultilineStringsCRLF() + { + using var stream = TestDataHelper.OpenResource("TextKV3.multiline_crlf.kv3"); + var data = KVSerializer.Create(KVSerializationFormat.KeyValues3Text).Deserialize(stream); + + Assert.That((string)data["multiLineStringValue"], Is.EqualTo("First line of a multi-line string literal.\r\nSecond line of a multi-line string literal.")); + } + + [Test] + public void DeserializesComments() + { + using var stream = TestDataHelper.OpenResource("TextKV3.comments.kv3"); + var data = KVSerializer.Create(KVSerializationFormat.KeyValues3Text).Deserialize(stream); + + Assert.Multiple(() => + { + Assert.That((string)data["foo"], Is.EqualTo("bar")); + Assert.That((string)data["one"], Is.EqualTo("1")); + Assert.That((string)data["two"], Is.EqualTo("2")); + }); + } + + [Test] + public void DeserializesArray() + { + using var stream = TestDataHelper.OpenResource("TextKV3.array.kv3"); + var data = KVSerializer.Create(KVSerializationFormat.KeyValues3Text).Deserialize(stream); + + Assert.That(data["arrayValue"].ValueType, Is.EqualTo(KVValueType.Array)); + Assert.That(data["arrayOnSingleLine"].ValueType, Is.EqualTo(KVValueType.Array)); + Assert.That(data["arrayNoSpace"].ValueType, Is.EqualTo(KVValueType.Array)); + Assert.That(data["arrayMixedTypes"].ValueType, Is.EqualTo(KVValueType.Array)); + + var arrayValue = (KVArrayValue)data["arrayValue"]; + + Assert.That(arrayValue, Has.Count.EqualTo(2)); + Assert.That(arrayValue[0].ToString(), Is.EqualTo("a")); + Assert.That(arrayValue[1].ToString(), Is.EqualTo("b")); + + // TODO: Test all the children values + } + + [Test] + public void DeserializesBinaryBlob() + { + using var stream = TestDataHelper.OpenResource("TextKV3.binary_blob.kv3"); + var data = KVSerializer.Create(KVSerializationFormat.KeyValues3Text).Deserialize(stream); + + Assert.That(data["array"].ValueType, Is.EqualTo(KVValueType.BinaryBlob)); + Assert.That(((KVBinaryBlob)data["array"]).Bytes.ToArray(), Is.EqualTo(new byte[] + { + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xFF + })); + } + + [Test] + public void DeserializesNestedObject() + { + using var stream = TestDataHelper.OpenResource("TextKV3.object.kv3"); + var data = KVSerializer.Create(KVSerializationFormat.KeyValues3Text).Deserialize(stream); + + Assert.That((string)data["a"]["b"]["c"], Is.EqualTo("d")); + } + + [Test] + public void DeserializesEntityNameFlag() + { + using var stream = TestDataHelper.OpenResource("TextKV3.entity_name.kv3"); + var data = KVSerializer.Create(KVSerializationFormat.KeyValues3Text).Deserialize(stream); + + Assert.Multiple(() => + { + Assert.That(data["name"].Flag, Is.EqualTo(KVFlag.EntityName)); + Assert.That((string)data["name"], Is.EqualTo("some_entity")); + }); + } + + [Test] + public void DeserializesEscapeSequences() + { + using var stream = TestDataHelper.OpenResource("TextKV3.escape_sequences.kv3"); + var data = KVSerializer.Create(KVSerializationFormat.KeyValues3Text).Deserialize(stream); + + Assert.Multiple(() => + { + Assert.That((string)data["newline"], Is.EqualTo("hello\nworld")); + Assert.That((string)data["tab"], Is.EqualTo("hello\tworld")); + Assert.That((string)data["backslash"], Is.EqualTo("hello\\world")); + Assert.That((string)data["quote"], Is.EqualTo("hello\"world")); + Assert.That((string)data["combined"], Is.EqualTo("line1\nline2\ttab\\slash\"quote")); + }); + } + + [Test] + public void DeserializesBasicTypes() + { + using var stream = TestDataHelper.OpenResource("TextKV3.types.kv3"); + var data = KVSerializer.Create(KVSerializationFormat.KeyValues3Text).Deserialize(stream); + Assert.Multiple(() => + { + Assert.That(data["boolFalseValue"].ValueType, Is.EqualTo(KVValueType.Boolean)); + Assert.That((bool)data["boolFalseValue"], Is.EqualTo(false)); + + Assert.That(data["boolTrueValue"].ValueType, Is.EqualTo(KVValueType.Boolean)); + Assert.That((bool)data["boolTrueValue"], Is.EqualTo(true)); + + Assert.That(data["nullValue"].ValueType, Is.EqualTo(KVValueType.Null)); + //Assert.That(data["nullValue"], Is.EqualTo(null)); + + Assert.That(data["intValue"].ValueType, Is.EqualTo(KVValueType.UInt64)); + Assert.That((int)data["intValue"], Is.EqualTo(128)); + + Assert.That(data["doubleValue"].ValueType, Is.EqualTo(KVValueType.FloatingPoint64)); + Assert.That((double)data["doubleValue"], Is.EqualTo(64.123)); + + Assert.That(data["negativeIntValue"].ValueType, Is.EqualTo(KVValueType.Int64)); + Assert.That((long)data["negativeIntValue"], Is.EqualTo(-1337)); + + Assert.That(data["negativeDoubleValue"].ValueType, Is.EqualTo(KVValueType.FloatingPoint64)); + Assert.That((double)data["negativeDoubleValue"], Is.EqualTo(-0.1337)); + + Assert.That(data["plusIntValue"].ValueType, Is.EqualTo(KVValueType.UInt64)); + Assert.That((ulong)data["plusIntValue"], Is.EqualTo(+1337)); + + Assert.That(data["plusDoubleValue"].ValueType, Is.EqualTo(KVValueType.FloatingPoint64)); + Assert.That((double)data["plusDoubleValue"], Is.EqualTo(+0.1337)); + + Assert.That(data["stringValue"].ValueType, Is.EqualTo(KVValueType.String)); + Assert.That((string)data["stringValue"], Is.EqualTo("hello world")); + + Assert.That(data["negativeMaxInt"].ValueType, Is.EqualTo(KVValueType.Int64)); + Assert.That((long)data["negativeMaxInt"], Is.EqualTo(-9223372036854775807)); + + Assert.That(data["positiveMaxInt"].ValueType, Is.EqualTo(KVValueType.UInt64)); + Assert.That((ulong)data["positiveMaxInt"], Is.EqualTo(18446744073709551615)); + + Assert.That(data["doubleMaxValue"].ValueType, Is.EqualTo(KVValueType.FloatingPoint64)); + Assert.That((double)data["doubleMaxValue"], Is.EqualTo(62147483647.1337)); + + Assert.That(data["doubleNegativeMaxValue"].ValueType, Is.EqualTo(KVValueType.FloatingPoint64)); + Assert.That((double)data["doubleNegativeMaxValue"], Is.EqualTo(-62147483647.1337)); + + Assert.That(data["doubleExponent"].ValueType, Is.EqualTo(KVValueType.FloatingPoint64)); + Assert.That((double)data["doubleExponent"], Is.EqualTo(123.456)); + + // TODO: Should this throw instead because strings need to be quoted? Or should it parse until it hits a non number like 123? + Assert.That(data["intWithStringSuffix"].ValueType, Is.EqualTo(KVValueType.String)); + Assert.That((string)data["intWithStringSuffix"], Is.EqualTo("123foobar")); + + Assert.That(data["singleQuotes"].ValueType, Is.EqualTo(KVValueType.String)); + Assert.That((string)data["singleQuotes"], Is.EqualTo("string")); + + Assert.That(data["singleQuotesWithQuotesInside"].ValueType, Is.EqualTo(KVValueType.String)); + Assert.That((string)data["singleQuotesWithQuotesInside"], Is.EqualTo("string is \"pretty\" cool")); + + Assert.That(data["key_with._various.separators"].ValueType, Is.EqualTo(KVValueType.String)); + Assert.That((string)data["key_with._various.separators"], Is.EqualTo("test")); + + Assert.That(data["quoted key with : {} terminators"].ValueType, Is.EqualTo(KVValueType.String)); + Assert.That((string)data["quoted key with : {} terminators"], Is.EqualTo("test quoted key")); + + Assert.That(data["this is a multi\nline\nkey"].ValueType, Is.EqualTo(KVValueType.String)); + Assert.That((string)data["this is a multi\nline\nkey"], Is.EqualTo("multi line key parsed")); + + Assert.That(data["empty.string"].ValueType, Is.EqualTo(KVValueType.String)); + Assert.That((string)data["empty.string"], Is.EqualTo(string.Empty)); + }); + } + } +} diff --git a/ValveKeyValue/ValveKeyValue.Test/TextKV3/HeadersTestCase.cs b/ValveKeyValue/ValveKeyValue.Test/TextKV3/HeadersTestCase.cs new file mode 100644 index 00000000..4c715620 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/TextKV3/HeadersTestCase.cs @@ -0,0 +1,74 @@ +using System.Text; + +namespace ValveKeyValue.Test.TextKV3 +{ + class HeadersTestCase + { + [TestCase("")] + [TestCase("")] + [TestCase("\n{}")] + [TestCase("\n{}")] + [TestCase("\n{}")] + [TestCase("\n{}")] + [TestCase("\n{}")] + [TestCase("\n{}")] + [TestCase("\n{}")] + [TestCase("\n{}")] + [TestCase("\n{}")] + [TestCase(""; + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(value)); + var kv = KVSerializer.Create(KVSerializationFormat.KeyValues3Text); + + Assert.That(() => kv.Deserialize(stream), Throws.Exception.TypeOf()); + } + + [Test] + public void IncorrectFormatGenericGuidThrows() + { + var value = ""; + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(value)); + var kv = KVSerializer.Create(KVSerializationFormat.KeyValues3Text); + + Assert.That(() => kv.Deserialize(stream), Throws.Exception.TypeOf()); + } + + [TestCase("")] + [TestCase("")] + [TestCase("")] + [TestCase("}")] + public void InvalidGuidThrows(string value) + { + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(value)); + var kv = KVSerializer.Create(KVSerializationFormat.KeyValues3Text); + + Assert.That(() => kv.Deserialize(stream), Throws.Exception.TypeOf()); + } + + [TestCase("\n{}")] + [TestCase("\n{}")] + [TestCase("\n{}")] + [TestCase("\n{}")] + [TestCase("\n{}")] + public void ValidHeadersAreParsed(string value) + { + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(value)); + var kv = KVSerializer.Create(KVSerializationFormat.KeyValues3Text); + + Assert.That(() => true); + } + } +} diff --git a/ValveKeyValue/ValveKeyValue.Test/TextKV3/Kv1ToKv3TestCase.cs b/ValveKeyValue/ValveKeyValue.Test/TextKV3/Kv1ToKv3TestCase.cs new file mode 100644 index 00000000..5eba97b6 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/TextKV3/Kv1ToKv3TestCase.cs @@ -0,0 +1,55 @@ +namespace ValveKeyValue.Test.TextKV3 +{ + class Kv1ToKv3TestCase + { + [Test] + public void SerializesBasicObjects() + { + using var stream = TestDataHelper.OpenResource("TextKV3.flagged_value_kv1.vdf"); + var expected = TestDataHelper.ReadTextResource("TextKV3.flagged_value_from_kv1.kv3"); + + var kv1 = KVSerializer.Create(KVSerializationFormat.KeyValues1Text); + var kv3 = KVSerializer.Create(KVSerializationFormat.KeyValues3Text); + var data = kv1.Deserialize(stream); + + data.Add(new KVObject("test", "success")); + + string text; + using (var ms = new MemoryStream()) + { + kv3.Serialize(ms, data); + + ms.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(ms); + text = reader.ReadToEnd(); + } + + Assert.That(text, Is.EqualTo(expected)); + } + + [Test] + public void SerializesAndKeepsLinearObjects() // TODO: Perhaps in the future KV1 arrays can use the KVArray type so it can be emitted as an array + { + using var stream = TestDataHelper.OpenResource("TextKV3.array_kv1.vdf"); + var expected = TestDataHelper.ReadTextResource("TextKV3.array_from_kv1.kv3"); + + var kv1 = KVSerializer.Create(KVSerializationFormat.KeyValues1Text); + var kv3 = KVSerializer.Create(KVSerializationFormat.KeyValues3Text); + var data = kv1.Deserialize(stream); + + data.Add(new KVObject("test", "success")); + + string text; + using (var ms = new MemoryStream()) + { + kv3.Serialize(ms, data); + + ms.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(ms); + text = reader.ReadToEnd(); + } + + Assert.That(text, Is.EqualTo(expected)); + } + } +} diff --git a/ValveKeyValue/ValveKeyValue.Test/TextKV3/Kv3ToKv1TestCase.cs b/ValveKeyValue/ValveKeyValue.Test/TextKV3/Kv3ToKv1TestCase.cs new file mode 100644 index 00000000..3ede1317 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/TextKV3/Kv3ToKv1TestCase.cs @@ -0,0 +1,55 @@ +namespace ValveKeyValue.Test.TextKV3 +{ + class Kv3ToKv1TestCase + { + [Test] + public void SerializesAndDropsFlags() + { + using var stream = TestDataHelper.OpenResource("TextKV3.flagged_value.kv3"); + var expected = TestDataHelper.ReadTextResource("TextKV3.flagged_value_kv1.vdf"); + + var kv1 = KVSerializer.Create(KVSerializationFormat.KeyValues1Text); + var kv3 = KVSerializer.Create(KVSerializationFormat.KeyValues3Text); + var data = kv3.Deserialize(stream); + + data.Add(new KVObject("test", "success")); + + string text; + using (var ms = new MemoryStream()) + { + kv1.Serialize(ms, data); + + ms.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(ms); + text = reader.ReadToEnd(); + } + + Assert.That(text, Is.EqualTo(expected)); + } + + [Test] + public void SerializesArraysToObjects() + { + using var stream = TestDataHelper.OpenResource("TextKV3.array.kv3"); + var expected = TestDataHelper.ReadTextResource("TextKV3.array_kv1.vdf"); + + var kv1 = KVSerializer.Create(KVSerializationFormat.KeyValues1Text); + var kv3 = KVSerializer.Create(KVSerializationFormat.KeyValues3Text); + var data = kv3.Deserialize(stream); + + data.Add(new KVObject("test", "success")); + + string text; + using (var ms = new MemoryStream()) + { + kv1.Serialize(ms, data); + + ms.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(ms); + text = reader.ReadToEnd(); + } + + Assert.That(text, Is.EqualTo(expected)); + } + } +} diff --git a/ValveKeyValue/ValveKeyValue.Test/TextKV3/RootTypesTestCase.cs b/ValveKeyValue/ValveKeyValue.Test/TextKV3/RootTypesTestCase.cs new file mode 100644 index 00000000..cf4e6c4f --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/TextKV3/RootTypesTestCase.cs @@ -0,0 +1,145 @@ +namespace ValveKeyValue.Test.TextKV3 +{ + class RootTypesTestCase + { + [Test] + public void DeserializesRootNull() + { + using var stream = TestDataHelper.OpenResource("TextKV3.root_null.kv3"); + var data = KVSerializer.Create(KVSerializationFormat.KeyValues3Text).Deserialize(stream); + + Assert.Multiple(() => + { + Assert.That(data.Name, Is.Null); + Assert.That(data.Value.ValueType, Is.EqualTo(KVValueType.Null)); + }); + } + + [Test] + public void DeserializesRootString() + { + using var stream = TestDataHelper.OpenResource("TextKV3.root_string.kv3"); + var data = KVSerializer.Create(KVSerializationFormat.KeyValues3Text).Deserialize(stream); + + Assert.Multiple(() => + { + Assert.That(data.Name, Is.Null); + Assert.That(data.Value.ValueType, Is.EqualTo(KVValueType.String)); + Assert.That((string)data.Value, Is.EqualTo("cool 123 string")); + }); + } + + [Test] + public void DeserializesRootMultilineString() + { + using var stream = TestDataHelper.OpenResource("TextKV3.root_multiline.kv3"); + var data = KVSerializer.Create(KVSerializationFormat.KeyValues3Text).Deserialize(stream); + + Assert.Multiple(() => + { + Assert.That(data.Name, Is.Null); + Assert.That((string)data.Value, Is.EqualTo("First line of a multi-line string literal.\nSecond line of a multi-line string literal.")); + }); + } + + [Test] + public void DeserializesRootFlaggedString() + { + using var stream = TestDataHelper.OpenResource("TextKV3.root_flagged_string.kv3"); + var data = KVSerializer.Create(KVSerializationFormat.KeyValues3Text).Deserialize(stream); + + Assert.Multiple(() => + { + Assert.That(data.Name, Is.Null); + Assert.That(data.Value.ValueType, Is.EqualTo(KVValueType.String)); + Assert.That(data.Value.Flag, Is.EqualTo(KVFlag.Resource)); + Assert.That((string)data.Value, Is.EqualTo("cool_resource.txt")); + }); + } + + [Test] + public void DeserializesRootFlaggedObject() + { + using var stream = TestDataHelper.OpenResource("TextKV3.root_flagged_object.kv3"); + var data = KVSerializer.Create(KVSerializationFormat.KeyValues3Text).Deserialize(stream); + + Assert.Multiple(() => + { + Assert.That(data.Name, Is.Null); + Assert.That(data.Value.ValueType, Is.EqualTo(KVValueType.Collection)); + Assert.That(data.Value.Flag, Is.EqualTo(KVFlag.Panorama)); + Assert.That(data["foo"].Flag, Is.EqualTo(KVFlag.Resource)); + Assert.That((string)data["foo"], Is.EqualTo("bar")); + }); + } + + [Test] + public void DeserializesRootBinaryBlob() + { + using var stream = TestDataHelper.OpenResource("TextKV3.root_binary_blob.kv3"); + var data = KVSerializer.Create(KVSerializationFormat.KeyValues3Text).Deserialize(stream); + + Assert.Multiple(() => + { + Assert.That(data.Name, Is.Null); + Assert.That(data.Value.ValueType, Is.EqualTo(KVValueType.BinaryBlob)); + Assert.That(((KVBinaryBlob)data.Value).Bytes.ToArray(), Is.EqualTo(new byte[] + { + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xFF + })); + }); + } + + [Test] + public void DeserializesRootNumber() + { + using var stream = TestDataHelper.OpenResource("TextKV3.root_number.kv3"); + var data = KVSerializer.Create(KVSerializationFormat.KeyValues3Text).Deserialize(stream); + + Assert.Multiple(() => + { + Assert.That(data.Name, Is.Null); + Assert.That(data.Value.ValueType, Is.EqualTo(KVValueType.UInt64)); + Assert.That((int)data.Value, Is.EqualTo(1234567890)); + }); + } + + [Test] + public void DeserializesRootNumberNegative() + { + using var stream = TestDataHelper.OpenResource("TextKV3.root_number_negative.kv3"); + var data = KVSerializer.Create(KVSerializationFormat.KeyValues3Text).Deserialize(stream); + + Assert.Multiple(() => + { + Assert.That(data.Name, Is.Null); + Assert.That(data.Value.ValueType, Is.EqualTo(KVValueType.Int64)); + Assert.That((int)data.Value, Is.EqualTo(-1234567890)); + }); + } + + [Test] + public void DeserializesRootFloat() + { + using var stream = TestDataHelper.OpenResource("TextKV3.root_float.kv3"); + var data = KVSerializer.Create(KVSerializationFormat.KeyValues3Text).Deserialize(stream); + + Assert.Multiple(() => + { + Assert.That(data.Name, Is.Null); + Assert.That(data.Value.ValueType, Is.EqualTo(KVValueType.FloatingPoint64)); + Assert.That((float)data.Value, Is.EqualTo(-1337.401f)); + }); + } + + [Test] + public void DeserializesRootArray() + { + using var stream = TestDataHelper.OpenResource("TextKV3.root_array.kv3"); + var data = KVSerializer.Create(KVSerializationFormat.KeyValues3Text).Deserialize(stream); + + Assert.That(data.Value.ValueType, Is.EqualTo(KVValueType.Array)); + } + } +} diff --git a/ValveKeyValue/ValveKeyValue.Test/TextKV3/SerializationTestCase.cs b/ValveKeyValue/ValveKeyValue.Test/TextKV3/SerializationTestCase.cs new file mode 100644 index 00000000..711b0ca1 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/TextKV3/SerializationTestCase.cs @@ -0,0 +1,169 @@ +namespace ValveKeyValue.Test.TextKV3 +{ + class SerializationTestCase + { + [Test] + public void CreatesTextDocument() + { + using var stream = TestDataHelper.OpenResource("TextKV3.types.kv3"); + var expected = TestDataHelper.ReadTextResource("TextKV3.types_serialized.kv3"); + + var kv = KVSerializer.Create(KVSerializationFormat.KeyValues3Text); + var data = kv.Deserialize(stream); + + data.Add(new KVObject("multiLineString", "hello\nworld")); + + string text; + using (var ms = new MemoryStream()) + { + kv.Serialize(ms, data); + + ms.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(ms); + text = reader.ReadToEnd(); + } + + Assert.That(text, Is.EqualTo(expected)); + } + + [Test] + public void SerializesArray() + { + using var stream = TestDataHelper.OpenResource("TextKV3.array.kv3"); + var expected = TestDataHelper.ReadTextResource("TextKV3.array_serialized.kv3"); + + var kv = KVSerializer.Create(KVSerializationFormat.KeyValues3Text); + var data = kv.Deserialize(stream); + + data.Add(new KVObject("test", "success")); + + string text; + using (var ms = new MemoryStream()) + { + kv.Serialize(ms, data); + + ms.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(ms); + text = reader.ReadToEnd(); + } + + Assert.That(text, Is.EqualTo(expected)); + } + + [Test] + public void SerializesNestedArray() + { + var expected = TestDataHelper.ReadTextResource("TextKV3.array_nested.kv3"); + + var kv = KVSerializer.Create(KVSerializationFormat.KeyValues3Text); + var data = kv.Deserialize(expected); + + string text; + using (var ms = new MemoryStream()) + { + kv.Serialize(ms, data); + + ms.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(ms); + text = reader.ReadToEnd(); + } + + Assert.That(text, Is.EqualTo(expected)); + } + + [Test] + public void SerializesEscapeSequencesRoundTrip() + { + using var stream = TestDataHelper.OpenResource("TextKV3.escape_sequences.kv3"); + var kv = KVSerializer.Create(KVSerializationFormat.KeyValues3Text); + var data = kv.Deserialize(stream); + + var data2 = RoundTrip(kv, data); + + Assert.Multiple(() => + { + Assert.That((string)data2["newline"], Is.EqualTo("hello\nworld")); + Assert.That((string)data2["tab"], Is.EqualTo("hello\tworld")); + Assert.That((string)data2["backslash"], Is.EqualTo("hello\\world")); + Assert.That((string)data2["quote"], Is.EqualTo("hello\"world")); + Assert.That((string)data2["combined"], Is.EqualTo("line1\nline2\ttab\\slash\"quote")); + }); + } + + [Test] + public void SerializesEntityNameFlag() + { + using var stream = TestDataHelper.OpenResource("TextKV3.entity_name.kv3"); + var kv = KVSerializer.Create(KVSerializationFormat.KeyValues3Text); + var data = kv.Deserialize(stream); + + var data2 = RoundTrip(kv, data); + + Assert.Multiple(() => + { + Assert.That(data2["name"].Flag, Is.EqualTo(KVFlag.EntityName)); + Assert.That((string)data2["name"], Is.EqualTo("some_entity")); + }); + } + + [Test] + public void SerializesRootValues() + { + var kv = KVSerializer.Create(KVSerializationFormat.KeyValues3Text); + + using var nullStream = TestDataHelper.OpenResource("TextKV3.root_null.kv3"); + var nullData = RoundTrip(kv, kv.Deserialize(nullStream)); + Assert.That(nullData.Value.ValueType, Is.EqualTo(KVValueType.Null)); + + using var stringStream = TestDataHelper.OpenResource("TextKV3.root_string.kv3"); + var stringData = RoundTrip(kv, kv.Deserialize(stringStream)); + Assert.That((string)stringData.Value, Is.EqualTo("cool 123 string")); + + using var numberStream = TestDataHelper.OpenResource("TextKV3.root_number.kv3"); + var numberData = RoundTrip(kv, kv.Deserialize(numberStream)); + Assert.That((int)numberData.Value, Is.EqualTo(1234567890)); + + using var floatStream = TestDataHelper.OpenResource("TextKV3.root_float.kv3"); + var floatData = RoundTrip(kv, kv.Deserialize(floatStream)); + Assert.That((float)floatData.Value, Is.EqualTo(-1337.401f)); + + using var arrayStream = TestDataHelper.OpenResource("TextKV3.root_array.kv3"); + var arrayData = RoundTrip(kv, kv.Deserialize(arrayStream)); + Assert.That(arrayData.Value.ValueType, Is.EqualTo(KVValueType.Array)); + var array = (KVArrayValue)arrayData.Value; + Assert.That(array[0].ToString(), Is.EqualTo("a")); + } + + [Test] + public void SerializesFlags() + { + using var stream = TestDataHelper.OpenResource("TextKV3.flagged_value.kv3"); + var expected = TestDataHelper.ReadTextResource("TextKV3.flagged_value_serialized.kv3"); + + var kv = KVSerializer.Create(KVSerializationFormat.KeyValues3Text); + var data = kv.Deserialize(stream); + + data.Add(new KVObject("test", "success")); + + string text; + using (var ms = new MemoryStream()) + { + kv.Serialize(ms, data); + + ms.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(ms); + text = reader.ReadToEnd(); + } + + Assert.That(text, Is.EqualTo(expected)); + } + + static KVDocument RoundTrip(KVSerializer kv, KVDocument data) + { + using var ms = new MemoryStream(); + kv.Serialize(ms, data); + ms.Seek(0, SeekOrigin.Begin); + return kv.Deserialize(ms); + } + } +} diff --git a/ValveKeyValue/ValveKeyValue.Test/ValveKeyValue.Test.csproj b/ValveKeyValue/ValveKeyValue.Test/ValveKeyValue.Test.csproj index 60caf9ed..f4c478cc 100644 --- a/ValveKeyValue/ValveKeyValue.Test/ValveKeyValue.Test.csproj +++ b/ValveKeyValue/ValveKeyValue.Test/ValveKeyValue.Test.csproj @@ -1,6 +1,6 @@  - net9.0 + net10.0 LatestMajor true Valve KeyValue Library - Unit Tests @@ -11,10 +11,12 @@ + + diff --git a/ValveKeyValue/ValveKeyValue/Abstraction/IVisitationListener.cs b/ValveKeyValue/ValveKeyValue/Abstraction/IVisitationListener.cs index f5109716..8625d1bb 100644 --- a/ValveKeyValue/ValveKeyValue/Abstraction/IVisitationListener.cs +++ b/ValveKeyValue/ValveKeyValue/Abstraction/IVisitationListener.cs @@ -2,10 +2,16 @@ namespace ValveKeyValue.Abstraction { interface IVisitationListener : IDisposable { - void OnObjectStart(string name); + void OnObjectStart(string name, KVFlag flag); void OnObjectEnd(); void OnKeyValuePair(string name, KVValue value); + + void OnArrayStart(string name, KVFlag flag); + + void OnArrayValue(KVValue value); + + void OnArrayEnd(); } } diff --git a/ValveKeyValue/ValveKeyValue/Abstraction/KVObjectVisitor.cs b/ValveKeyValue/ValveKeyValue/Abstraction/KVObjectVisitor.cs index e316a3a1..28670e9b 100644 --- a/ValveKeyValue/ValveKeyValue/Abstraction/KVObjectVisitor.cs +++ b/ValveKeyValue/ValveKeyValue/Abstraction/KVObjectVisitor.cs @@ -13,25 +13,43 @@ public KVObjectVisitor(IVisitationListener listener) public void Visit(KVObject @object) { - VisitObject(@object.Name, @object.Value); + VisitObject(@object.Name, @object.Value, false); } - void VisitObject(string name, KVValue value) + void VisitObject(string name, KVValue value, bool isArray) { switch (value.ValueType) { case KVValueType.Collection: - listener.OnObjectStart(name); + listener.OnObjectStart(name, value.Flag); VisitValue((IEnumerable)value); listener.OnObjectEnd(); break; + case KVValueType.Array: + listener.OnArrayStart(name, value.Flag); + VisitArray((IEnumerable)value); + listener.OnArrayEnd(); + break; + + case KVValueType.BinaryBlob: case KVValueType.FloatingPoint: + case KVValueType.FloatingPoint64: + case KVValueType.Int16: case KVValueType.Int32: + case KVValueType.UInt16: + case KVValueType.UInt32: case KVValueType.Pointer: case KVValueType.String: case KVValueType.UInt64: case KVValueType.Int64: + case KVValueType.Boolean: + case KVValueType.Null: + if (isArray) + { + listener.OnArrayValue(value); + break; + } listener.OnKeyValuePair(name, value); break; @@ -44,7 +62,15 @@ void VisitValue(IEnumerable collection) { foreach (var item in collection) { - VisitObject(item.Name, item.Value); + VisitObject(item.Name, item.Value, false); + } + } + + void VisitArray(IEnumerable collection) + { + foreach (var item in collection) + { + VisitObject(null, item, true); } } } diff --git a/ValveKeyValue/ValveKeyValue/Deserialization/IVisitingReader.cs b/ValveKeyValue/ValveKeyValue/Deserialization/IVisitingReader.cs index 7b6eaa4e..0f2246ee 100644 --- a/ValveKeyValue/ValveKeyValue/Deserialization/IVisitingReader.cs +++ b/ValveKeyValue/ValveKeyValue/Deserialization/IVisitingReader.cs @@ -2,6 +2,6 @@ namespace ValveKeyValue.Deserialization { interface IVisitingReader : IDisposable { - void ReadObject(); + KVHeader ReadHeader(); } } diff --git a/ValveKeyValue/ValveKeyValue/Deserialization/KVObjectBuilder.cs b/ValveKeyValue/ValveKeyValue/Deserialization/KVObjectBuilder.cs index 0a3b782b..578a37f4 100644 --- a/ValveKeyValue/ValveKeyValue/Deserialization/KVObjectBuilder.cs +++ b/ValveKeyValue/ValveKeyValue/Deserialization/KVObjectBuilder.cs @@ -19,7 +19,7 @@ public KVObject GetObject() } var state = stateStack.Peek(); - return MakeObject(state); + return state.IsArray ? MakeArray(state) : MakeObject(state); } readonly Stack stateStack = new(); @@ -43,6 +43,24 @@ public void OnKeyValuePair(string name, KVValue value) } } + public void OnArrayValue(KVValue value) + { + if (StateStack.Count > 0) + { + var state = StateStack.Peek(); + state.Children.Add(value); + } + else + { + var state = new KVPartialState + { + Value = value + }; + + StateStack.Push(state); + } + } + public void OnObjectEnd() { if (StateStack.Count <= 1) @@ -55,7 +73,38 @@ public void OnObjectEnd() var completedObject = MakeObject(state); var parentState = StateStack.Peek(); - parentState.Items.Add(completedObject); + + if (parentState.IsArray) + { + parentState.Children.Add(completedObject.Value); // TODO: Avoid wrapping it into KVObject in the first place? + } + else + { + parentState.Items.Add(completedObject); + } + } + + public void OnArrayEnd() + { + if (StateStack.Count <= 1) + { + return; + } + + var state = StateStack.Pop(); + + var completedObject = MakeArray(state); + + var parentState = StateStack.Peek(); + + if (parentState.IsArray) + { + parentState.Children.Add(completedObject.Value); // TODO: Avoid wrapping it into KVObject in the first place? + } + else + { + parentState.Items.Add(completedObject); + } } public void DiscardCurrentObject() @@ -71,11 +120,23 @@ public void DiscardCurrentObject() } } - public void OnObjectStart(string name) + public void OnObjectStart(string name, KVFlag flag) + { + var state = new KVPartialState + { + Key = name, + Flag = flag, + }; + StateStack.Push(state); + } + + public void OnArrayStart(string name, KVFlag flag) { var state = new KVPartialState { - Key = name + Key = name, + Flag = flag, + IsArray = true, }; StateStack.Push(state); } @@ -115,6 +176,11 @@ static KVObject MakeObject(KVPartialState state) return null; } + if (state.IsArray) + { + throw new InvalidCastException("Tried to make an object ouf of an array."); + } + KVObject @object; if (state.Value != null) @@ -124,6 +190,34 @@ static KVObject MakeObject(KVPartialState state) else { @object = new KVObject(state.Key, state.Items); + @object.Value.Flag = state.Flag; + } + + return @object; + } + + KVObject MakeArray(KVPartialState state) + { + if (state.Discard) + { + return null; + } + + if (!state.IsArray) + { + throw new InvalidCastException("Tried to make an array out of an object."); + } + + KVObject @object; + + if (state.Value != null) + { + @object = new KVObject(state.Key, state.Value); + } + else + { + @object = new KVObject(state.Key, state.Children); + @object.Value.Flag = state.Flag; } return @object; diff --git a/ValveKeyValue/ValveKeyValue/Deserialization/KVPartialState.cs b/ValveKeyValue/ValveKeyValue/Deserialization/KVPartialState.cs index 685d54a5..130ab05a 100644 --- a/ValveKeyValue/ValveKeyValue/Deserialization/KVPartialState.cs +++ b/ValveKeyValue/ValveKeyValue/Deserialization/KVPartialState.cs @@ -4,10 +4,17 @@ class KVPartialState { public string Key { get; set; } + public KVFlag Flag { get; set; } + public KVValue Value { get; set; } public IList Items { get; } = new List(); + // TODO: Somehow merge with Items? + public IList Children { get; } = new List(); + public bool Discard { get; set; } + + public bool IsArray { get; set; } } } diff --git a/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues1/KV1BinaryReader.cs b/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues1/KV1BinaryReader.cs index b8ef232b..51628ca3 100644 --- a/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues1/KV1BinaryReader.cs +++ b/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues1/KV1BinaryReader.cs @@ -32,7 +32,7 @@ public KV1BinaryReader(Stream stream, IVisitationListener listener, StringTable bool disposed; KV1BinaryNodeType endMarker = KV1BinaryNodeType.End; - public void ReadObject() + public KVHeader ReadHeader() { ObjectDisposedException.ThrowIf(disposed, this); @@ -54,6 +54,8 @@ public void ReadObject() { throw new KeyValueException("Error while parsing binary KeyValues.", ex); } + + return new KVHeader(); } public void Dispose() @@ -96,7 +98,7 @@ void ReadValue(KV1BinaryNodeType type) switch (type) { case KV1BinaryNodeType.ChildObject: - listener.OnObjectStart(name); + listener.OnObjectStart(name, KVFlag.None); ReadObjectCore(); listener.OnObjectEnd(); return; diff --git a/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues1/KV1TextReader.cs b/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues1/KV1TextReader.cs index 9fcaacb4..947f1d69 100644 --- a/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues1/KV1TextReader.cs +++ b/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues1/KV1TextReader.cs @@ -27,7 +27,7 @@ public KV1TextReader(TextReader textReader, IParsingVisitationListener listener, readonly KV1TextReaderStateMachine stateMachine; bool disposed; - public void ReadObject() + public KVHeader ReadHeader() { ObjectDisposedException.ThrowIf(disposed, this); @@ -103,6 +103,8 @@ public void ReadObject() throw new ArgumentOutOfRangeException(nameof(token.TokenType), token.TokenType, "Unhandled token type."); } } + + return new KVHeader(); } public void Dispose() @@ -155,7 +157,7 @@ void BeginNewObject() throw new InvalidOperationException($"Attempted to begin new object while in state {stateMachine.Current} at {tokenReader.PreviousTokenPosition}."); } - listener.OnObjectStart(stateMachine.CurrentName); + listener.OnObjectStart(stateMachine.CurrentName, KVFlag.None); stateMachine.PushObject(); stateMachine.Push(KV1TextReaderState.InObjectBeforeKey); @@ -224,7 +226,7 @@ void DoIncludeAndMerge(string filePath) using var stream = OpenFileForInclude(filePath); using var reader = new KV1TextReader(new StreamReader(stream), mergeListener, options); - reader.ReadObject(); + reader.ReadHeader(); } void DoIncludeAndAppend(string filePath) @@ -233,7 +235,7 @@ void DoIncludeAndAppend(string filePath) using var stream = OpenFileForInclude(filePath); using var reader = new KV1TextReader(new StreamReader(stream), appendListener, options); - reader.ReadObject(); + reader.ReadHeader(); } Stream OpenFileForInclude(string filePath) diff --git a/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues3/KV3TextReader.cs b/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues3/KV3TextReader.cs new file mode 100644 index 00000000..43186a95 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues3/KV3TextReader.cs @@ -0,0 +1,344 @@ +using System.Globalization; +using ValveKeyValue.Abstraction; + +namespace ValveKeyValue.Deserialization.KeyValues3 +{ + sealed class KV3TextReader : IVisitingReader + { + public KV3TextReader(TextReader textReader, IParsingVisitationListener listener) + { + ArgumentNullException.ThrowIfNull(textReader); + ArgumentNullException.ThrowIfNull(listener); + + this.listener = listener; + + tokenReader = new KV3TokenReader(textReader); + stateMachine = new KV3TextReaderStateMachine(); + } + + readonly IParsingVisitationListener listener; + + readonly KV3TokenReader tokenReader; + readonly KV3TextReaderStateMachine stateMachine; + bool disposed; + + public KVHeader ReadHeader() + { + ObjectDisposedException.ThrowIf(disposed, this); + + var header = tokenReader.ReadHeader(); + + while (stateMachine.IsInObject) + { + KVToken token; + + try + { + token = tokenReader.ReadNextToken(); + } + catch (InvalidDataException ex) + { + throw new KeyValueException(ex.Message, ex); + } + catch (EndOfStreamException ex) + { + throw new KeyValueException("Found end of file while trying to read token.", ex); + } + + switch (token.TokenType) + { + case KVTokenType.Assignment: + ReadAssignment(); + break; + + case KVTokenType.Comma: + ReadComma(); + break; + + case KVTokenType.Flag: + ReadFlag(token.Value); + break; + + case KVTokenType.Identifier: + case KVTokenType.String: + ReadText(token.Value); + break; + + case KVTokenType.BinaryBlob: + ReadBinaryBlob(token.Value); + break; + + case KVTokenType.ObjectStart: + BeginNewObject(); + break; + + case KVTokenType.ObjectEnd: + FinalizeCurrentObject(@explicit: true); + break; + + case KVTokenType.ArrayStart: + BeginNewArray(); + break; + + case KVTokenType.ArrayEnd: + FinalizeCurrentArray(); + break; + + case KVTokenType.EndOfFile: + try + { + FinalizeDocument(); + } + catch (InvalidOperationException ex) + { + throw new KeyValueException("Found end of file when another token type was expected.", ex); + } + + break; + + case KVTokenType.Comment: + break; + + default: + throw new ArgumentOutOfRangeException(nameof(token.TokenType), token.TokenType, "Unhandled token type."); + } + } + + return header; + } + + public void Dispose() + { + if (!disposed) + { + tokenReader.Dispose(); + disposed = true; + } + } + + void ReadAssignment() + { + if (stateMachine.Current != KV3TextReaderState.InObjectAfterKey) + { + throw new InvalidOperationException($"Attempted to assign while in state {stateMachine.Current}."); + } + } + + void ReadComma() + { + if (stateMachine.Current != KV3TextReaderState.InArray) + { + throw new InvalidOperationException($"Attempted to have a comma character while in state {stateMachine.Current}."); + } + } + + void ReadFlag(string text) + { + if (stateMachine.Current != KV3TextReaderState.InArray && stateMachine.Current != KV3TextReaderState.InObjectAfterKey) + { + throw new InvalidOperationException($"Attempted to read flag while in state {stateMachine.Current}."); + } + + var flag = ParseFlag(text); + + stateMachine.SetFlag(flag); + } + + void ReadText(string text) + { + switch (stateMachine.Current) + { + case KV3TextReaderState.InArray: + { + var value = ParseValue(text); + value.Flag = stateMachine.GetAndResetFlag(); + listener.OnArrayValue(value); + break; + } + + case KV3TextReaderState.InObjectBeforeKey: + SetObjectKey(text); + break; + + case KV3TextReaderState.InObjectAfterKey: + { + var name = stateMachine.CurrentName; + var value = ParseValue(text); + value.Flag = stateMachine.GetAndResetFlag(); + listener.OnKeyValuePair(name, value); + + stateMachine.Push(KV3TextReaderState.InObjectBeforeKey); + break; + } + + default: + throw new InvalidOperationException($"Unhandled text reader state: {stateMachine.Current}."); + } + } + + void ReadBinaryBlob(string text) + { + var bytes = HexStringHelper.ParseHexStringAsByteArray(text); + var value = new KVBinaryBlob(bytes) + { + Flag = stateMachine.GetAndResetFlag() + }; + + switch (stateMachine.Current) + { + case KV3TextReaderState.InArray: + { + listener.OnArrayValue(value); + break; + } + + case KV3TextReaderState.InObjectAfterKey: + { + var name = stateMachine.CurrentName; + listener.OnKeyValuePair(name, value); + + stateMachine.Push(KV3TextReaderState.InObjectBeforeKey); + break; + } + + default: + throw new InvalidOperationException($"Unhandled text reader state: {stateMachine.Current}."); + } + } + + void BeginNewArray() + { + if (stateMachine.Current != KV3TextReaderState.InArray && stateMachine.Current != KV3TextReaderState.InObjectAfterKey) + { + throw new InvalidOperationException($"Attempted to begin new array while in state {stateMachine.Current}."); + } + + listener.OnArrayStart(stateMachine.CurrentName, stateMachine.GetAndResetFlag()); + + stateMachine.PushObject(); + stateMachine.SetArrayCurrent(); + stateMachine.Push(KV3TextReaderState.InArray); + } + + void FinalizeCurrentArray() + { + if (stateMachine.Current != KV3TextReaderState.InArray) + { + throw new InvalidOperationException($"Attempted to finalize array while in state {stateMachine.Current}."); + } + + stateMachine.PopObject(); + + if (stateMachine.IsInObject && !stateMachine.IsInArray) + { + stateMachine.Push(KV3TextReaderState.InObjectBeforeKey); + } + + listener.OnArrayEnd(); + } + + void SetObjectKey(string name) + { + stateMachine.GetAndResetFlag(); + stateMachine.SetName(name); + stateMachine.Push(KV3TextReaderState.InObjectAfterKey); + } + + void BeginNewObject() + { + if (stateMachine.Current != KV3TextReaderState.InArray && stateMachine.Current != KV3TextReaderState.InObjectAfterKey) + { + throw new InvalidOperationException($"Attempted to begin new object while in state {stateMachine.Current}."); + } + + listener.OnObjectStart(stateMachine.CurrentName, stateMachine.GetAndResetFlag()); + + stateMachine.PushObject(); + stateMachine.Push(KV3TextReaderState.InObjectBeforeKey); + } + + void FinalizeCurrentObject(bool @explicit) + { + if (stateMachine.Current != KV3TextReaderState.InObjectBeforeKey) + { + throw new InvalidOperationException($"Attempted to finalize object while in state {stateMachine.Current}."); + } + + stateMachine.PopObject(); + + if (stateMachine.IsInObject && !stateMachine.IsInArray) + { + stateMachine.Push(KV3TextReaderState.InObjectBeforeKey); + } + + if (@explicit) + { + listener.OnObjectEnd(); + } + } + + void FinalizeDocument() + { + FinalizeCurrentObject(@explicit: true); + + if (stateMachine.IsInObject) + { + throw new InvalidOperationException("Inconsistent state - at end of file whilst inside an object."); + } + } + + static KVValue ParseValue(string text) + { + if (text.Equals("false", StringComparison.Ordinal)) + { + return new KVObjectValue(false, KVValueType.Boolean); + } + else if (text.Equals("true", StringComparison.Ordinal)) + { + return new KVObjectValue(true, KVValueType.Boolean); + } + else if (text.Equals("null", StringComparison.Ordinal)) + { + return new KVNullValue(); + } + else if (text.Length > 0 && ((text[0] >= '0' && text[0] <= '9') || text[0] == '-' || text[0] == '+')) + { + // TODO: Due to Valve's string to int/double conversion functions, it is possible to have 0x hex values (as well as prefixed with minus like -0x) + + const NumberStyles IntegerNumberStyles = NumberStyles.AllowLeadingSign; + + if (text[0] == '-' && long.TryParse(text, IntegerNumberStyles, CultureInfo.InvariantCulture, out var intValue)) + { + return new KVObjectValue(intValue, KVValueType.Int64); + } + else if (ulong.TryParse(text, IntegerNumberStyles, CultureInfo.InvariantCulture, out var uintValue)) + { + return new KVObjectValue(uintValue, KVValueType.UInt64); + } + + const NumberStyles FloatingPointNumberStyles = + NumberStyles.AllowDecimalPoint | + NumberStyles.AllowExponent | + NumberStyles.AllowLeadingSign; + + if (double.TryParse(text, FloatingPointNumberStyles, CultureInfo.InvariantCulture, out var floatValue)) + { + return new KVObjectValue(floatValue, KVValueType.FloatingPoint64); + } + } + + return new KVObjectValue(text, KVValueType.String); + } + + static KVFlag ParseFlag(string flag) + { + if (flag.Equals("resource", StringComparison.OrdinalIgnoreCase)) return KVFlag.Resource; + if (flag.Equals("resource_name", StringComparison.OrdinalIgnoreCase)) return KVFlag.ResourceName; + if (flag.Equals("panorama", StringComparison.OrdinalIgnoreCase)) return KVFlag.Panorama; + if (flag.Equals("soundevent", StringComparison.OrdinalIgnoreCase)) return KVFlag.SoundEvent; + if (flag.Equals("subclass", StringComparison.OrdinalIgnoreCase)) return KVFlag.SubClass; + if (flag.Equals("entity_name", StringComparison.OrdinalIgnoreCase)) return KVFlag.EntityName; + throw new InvalidDataException($"Unknown flag '{flag}'"); + } + } +} diff --git a/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues3/KV3TextReaderState.cs b/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues3/KV3TextReaderState.cs new file mode 100644 index 00000000..b402cd31 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues3/KV3TextReaderState.cs @@ -0,0 +1,9 @@ +namespace ValveKeyValue.Deserialization.KeyValues3 +{ + enum KV3TextReaderState + { + InObjectBeforeKey, + InObjectAfterKey, + InArray, + } +} diff --git a/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues3/KV3TextReaderStateMachine.cs b/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues3/KV3TextReaderStateMachine.cs new file mode 100644 index 00000000..eafe8895 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues3/KV3TextReaderStateMachine.cs @@ -0,0 +1,49 @@ +namespace ValveKeyValue.Deserialization.KeyValues3 +{ + class KV3TextReaderStateMachine + { + public KV3TextReaderStateMachine() + { + states = new Stack>(); + + PushObject(); + Push(KV3TextReaderState.InObjectAfterKey); + } + + readonly Stack> states; + + public KV3TextReaderState Current => CurrentObject.States.Peek(); + + public bool IsInObject => states.Count > 0; + + public bool IsInArray => states.Count > 0 && CurrentObject.IsArray; + + public void PushObject() => states.Push(new KVPartialState()); + + public void Push(KV3TextReaderState state) => CurrentObject.States.Push(state); + + public void PopObject() + { + states.Pop(); + } + + public string CurrentName => CurrentObject.Key; + + public void SetName(string name) => CurrentObject.Key = name; + + public void SetFlag(KVFlag flag) => CurrentObject.Flag = flag; + + public KVFlag GetAndResetFlag() + { + var flag = CurrentObject.Flag; + + CurrentObject.Flag = KVFlag.None; + + return flag; + } + + public void SetArrayCurrent() => CurrentObject.IsArray = true; + + KVPartialState CurrentObject => states.Peek(); + } +} diff --git a/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues3/KV3TokenReader.cs b/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues3/KV3TokenReader.cs new file mode 100644 index 00000000..edd39daa --- /dev/null +++ b/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues3/KV3TokenReader.cs @@ -0,0 +1,474 @@ +using System.Linq; +using System.Text; +using ValveKeyValue.KeyValues3; +using Encoding = ValveKeyValue.KeyValues3.Encoding; + +namespace ValveKeyValue.Deserialization.KeyValues3 +{ + class KV3TokenReader : KVTokenReader + { + const char ObjectStart = '{'; + const char ObjectEnd = '}'; + const char BinaryBlobMarker = '#'; + const char ArrayStart = '['; + const char ArrayEnd = ']'; + const char CommentBegin = '/'; + const char Assignment = '='; + const char Comma = ','; + + public KV3TokenReader(TextReader textReader) : base(textReader) + { + // Dota 2 binary from 2017 used "+" as a terminate (for flagged values), but then they changed it to "|" + var terminators = "{}[]=, \t\n\r'\":|;".ToCharArray(); + integerTerminators = new HashSet(terminators.Select(t => (int)t)); + } + + readonly HashSet integerTerminators; + + public KVToken ReadNextToken() + { + ObjectDisposedException.ThrowIf(disposed, this); + SwallowWhitespace(); + + var nextChar = Peek(); + if (IsEndOfFile(nextChar)) + { + return new KVToken(KVTokenType.EndOfFile); + } + + return nextChar switch + { + ObjectStart => ReadObjectStart(), + ObjectEnd => ReadObjectEnd(), + BinaryBlobMarker => ReadBinaryBlob(), + ArrayStart => ReadArrayStart(), + ArrayEnd => ReadArrayEnd(), + CommentBegin => ReadComment(), + Assignment => ReadAssignment(), + Comma => ReadComma(), + _ => ReadStringOrIdentifier(), + }; + } + + KVToken ReadAssignment() + { + ReadChar(Assignment); + return new KVToken(KVTokenType.Assignment); + } + + KVToken ReadComma() + { + ReadChar(Comma); + return new KVToken(KVTokenType.Comma); + } + + KVToken ReadArrayStart() + { + ReadChar(ArrayStart); + return new KVToken(KVTokenType.ArrayStart); + } + + KVToken ReadArrayEnd() + { + ReadChar(ArrayEnd); + return new KVToken(KVTokenType.ArrayEnd); + } + + KVToken ReadObjectStart() + { + ReadChar(ObjectStart); + return new KVToken(KVTokenType.ObjectStart); + } + + KVToken ReadObjectEnd() + { + ReadChar(ObjectEnd); + return new KVToken(KVTokenType.ObjectEnd); + } + + KVToken ReadStringOrIdentifier() + { + SwallowWhitespace(); + + var token = ReadToken(); + var type = KVTokenType.String; + + if (IsIdentifier(token)) + { + type = KVTokenType.Identifier; + + var next = Peek(); + + if (next == ':' || next == '|') + { + Next(); + type = KVTokenType.Flag; + } + } + + return new KVToken(type, token); + } + + KVToken ReadBinaryBlob() + { + ReadChar(BinaryBlobMarker); + ReadChar(ArrayStart); // TODO: Strictly speaking Valve allows bare # without [ to be read as literal value (but what would that be?) + + var sb = new StringBuilder(); + + while (true) + { + var next = Next(); + + if (char.IsWhiteSpace(next)) + { + continue; + } + + if (next == ArrayEnd) + { + break; + } + + sb.Append(next); + } + + return new KVToken(KVTokenType.BinaryBlob, sb.ToString()); + } + + public KVHeader ReadHeader() + { + var str = ReadToken(); + + if (str != "") + { + throw new InvalidDataException($"The header is incorrect, expected '-->' but got '{str}'."); + } + + if (encodingType.Equals("text", StringComparison.OrdinalIgnoreCase) && encoding != Encoding.Text) + { + throw new InvalidDataException($"Unrecognized encoding version, expected '{Encoding.Text}' but got '{encoding}'."); + } + + if (formatType.Equals("generic", StringComparison.OrdinalIgnoreCase) && format != Format.Generic) + { + throw new InvalidDataException($"Unrecognized format version, expected '{Format.Generic}' but got '{format}'."); + } + + return new KVHeader + { + Encoding = new KV3ID(encodingType, encoding), + Format = new KV3ID(formatType, format), + }; + } + + KVToken ReadComment() + { + ReadChar(CommentBegin); + + var next = Next(); + + if (next == '*') + { + while (true) + { + next = Next(); + + if (next == '*' && Peek() == '/') + { + Next(); + break; + } + } + } + else if (next == CommentBegin) + { + while (true) + { + var peek = Peek(); + + if (IsEndOfFile(peek) || peek == '\n') + { + break; + } + + Next(); + } + } + else + { + throw new InvalidDataException($"The syntax is incorrect, expected comment but got '/{next}'."); + } + + return new KVToken(KVTokenType.Comment); + } + + bool IsIdentifier(string text) + { + for (var i = 0; i < text.Length; i++) + { + var c = text[i]; + + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) + { + continue; + } + + if (c >= '0' && c <= '9') + { + continue; + } + + // TODO: Disallow : because it's a token terminator? + if (c == '_' || c == ':' || c == '.') + { + continue; + } + + return false; + } + + return true; + } + + string ReadToken() + { + var next = Peek(); + + if (next == '"' || next == '\'') + { + return ReadQuotedStringRaw((char)next); + } + + var sb = new StringBuilder(); + + while (true) + { + next = Peek(); + + if (next <= ' ' || integerTerminators.Contains(next)) + { + break; + } + + sb.Append(Next()); + } + + return sb.ToString(); + } + + string ReadQuotedStringRaw(char quotationMark) + { + ReadChar(quotationMark); + + var isMultiline = false; + + var sb = new StringBuilder(); + + if (quotationMark == '"' && Peek() == '"') + { + Next(); + + // If the next character is not another quote, it's an empty string + if (Peek() == '"') + { + isMultiline = true; + + Next(); + + if (Peek() == '\r') + { + Next(); + } + + ReadChar('\n'); + } + else + { + return string.Empty; + } + } + + if (isMultiline) + { + while (true) + { + var next = Next(); + + if (next == '"' && !IsEscaped(sb)) + { + // Check if this is the start of """ + if (Peek() == '"') + { + Next(); + + if (Peek() == '"') + { + Next(); + break; + } + + // Only two quotes, append both + sb.Append(next); + sb.Append('"'); + continue; + } + } + + sb.Append(next); + } + + // Strip trailing newline (\n or \r\n) + if (sb.Length > 0 && sb[^1] == '\n') + { + sb.Remove(sb.Length - 1, 1); + + if (sb.Length > 0 && sb[^1] == '\r') + { + sb.Remove(sb.Length - 1, 1); + } + } + } + else + { + while (true) + { + var next = Next(); + + if (next == quotationMark && !IsEscaped(sb)) + { + break; + } + + sb.Append(next); + } + + return UnescapeString(sb); + } + + return sb.ToString(); + } + + static bool IsEscaped(StringBuilder sb) + { + var count = 0; + + for (var i = sb.Length - 1; i >= 0 && sb[i] == '\\'; i--) + { + count++; + } + + return count % 2 == 1; + } + + static string UnescapeString(StringBuilder input) + { + if (input.Length == 0) + { + return string.Empty; + } + + var result = new StringBuilder(input.Length); + var isEscaped = false; + + for (var i = 0; i < input.Length; i++) + { + var c = input[i]; + + if (c == '\\' && !isEscaped) + { + isEscaped = true; + continue; + } + + if (isEscaped) + { + switch (c) + { + case 'n': + result.Append('\n'); + break; + case 't': + result.Append('\t'); + break; + default: + result.Append(c); + break; + } + + isEscaped = false; + } + else + { + result.Append(c); + } + } + + return result.ToString(); + } + } +} diff --git a/ValveKeyValue/ValveKeyValue/HexStringHelper.cs b/ValveKeyValue/ValveKeyValue/HexStringHelper.cs index 2b057845..c316ea4f 100644 --- a/ValveKeyValue/ValveKeyValue/HexStringHelper.cs +++ b/ValveKeyValue/ValveKeyValue/HexStringHelper.cs @@ -12,8 +12,7 @@ public static byte[] ParseHexStringAsByteArray(string hexadecimalRepresentation) var data = new byte[hexadecimalRepresentation.Length / 2]; for (var i = 0; i < data.Length; i++) { - var currentByteText = hexadecimalRepresentation.Substring(i * 2, 2); - data[i] = byte.Parse(currentByteText, NumberStyles.HexNumber, CultureInfo.InvariantCulture); + data[i] = byte.Parse(hexadecimalRepresentation.AsSpan(i * 2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture); } return data; diff --git a/ValveKeyValue/ValveKeyValue/KVCollectionValue.cs b/ValveKeyValue/ValveKeyValue/KVCollectionValue.cs index 7621c094..ab7de1b2 100644 --- a/ValveKeyValue/ValveKeyValue/KVCollectionValue.cs +++ b/ValveKeyValue/ValveKeyValue/KVCollectionValue.cs @@ -3,8 +3,12 @@ namespace ValveKeyValue { - class KVCollectionValue : KVValue, IEnumerable + /// + /// Represents a collection of named KeyValue children. + /// + public class KVCollectionValue : KVValue, IEnumerable { + /// public KVCollectionValue() { children = new List(); @@ -12,28 +16,41 @@ public KVCollectionValue() readonly List children; + /// public override KVValueType ValueType => KVValueType.Collection; + /// + public int Count => children.Count; + + /// public override KVValue this[string key] => Get(key)?.Value; + /// public void Add(KVObject value) { ArgumentNullException.ThrowIfNull(value); children.Add(value); } + /// public void AddRange(IEnumerable values) { ArgumentNullException.ThrowIfNull(values); children.AddRange(values); } + /// + /// Gets a child by name. + /// public KVObject Get(string name) { ArgumentNullException.ThrowIfNull(name); return children.FirstOrDefault(c => c.Name == name); } + /// + /// Sets or replaces a child by name. + /// public void Set(string name, KVValue value) { ArgumentNullException.ThrowIfNull(name); @@ -45,90 +62,108 @@ public void Set(string name, KVValue value) #region IEnumerable + /// public IEnumerator GetEnumerator() => children.GetEnumerator(); #endregion #region IConvertible + /// public override TypeCode GetTypeCode() { throw new NotSupportedException(); } + /// public override bool ToBoolean(IFormatProvider provider) { throw new NotSupportedException(); } + /// public override byte ToByte(IFormatProvider provider) { throw new NotSupportedException(); } + /// public override char ToChar(IFormatProvider provider) { throw new NotSupportedException(); } + /// public override DateTime ToDateTime(IFormatProvider provider) { throw new NotSupportedException(); } + /// public override decimal ToDecimal(IFormatProvider provider) { throw new NotSupportedException(); } + /// public override double ToDouble(IFormatProvider provider) { throw new NotSupportedException(); } + /// public override short ToInt16(IFormatProvider provider) { throw new NotSupportedException(); } + /// public override int ToInt32(IFormatProvider provider) { throw new NotSupportedException(); } + /// public override long ToInt64(IFormatProvider provider) { throw new NotSupportedException(); } + /// public override sbyte ToSByte(IFormatProvider provider) { throw new NotSupportedException(); } + /// public override float ToSingle(IFormatProvider provider) { throw new NotSupportedException(); } + /// public override string ToString(IFormatProvider provider) => ToString(); + /// public override object ToType(Type conversionType, IFormatProvider provider) { throw new NotSupportedException(); } + /// public override ushort ToUInt16(IFormatProvider provider) { throw new NotSupportedException(); } + /// public override uint ToUInt32(IFormatProvider provider) { throw new NotSupportedException(); } + /// public override ulong ToUInt64(IFormatProvider provider) { throw new NotSupportedException(); @@ -138,10 +173,12 @@ public override ulong ToUInt64(IFormatProvider provider) #region IEnumerable + /// IEnumerator IEnumerable.GetEnumerator() => children.GetEnumerator(); #endregion + /// public override string ToString() => "[Collection]"; } } diff --git a/ValveKeyValue/ValveKeyValue/KVDocument.cs b/ValveKeyValue/ValveKeyValue/KVDocument.cs index 660f50c5..de116f50 100644 --- a/ValveKeyValue/ValveKeyValue/KVDocument.cs +++ b/ValveKeyValue/ValveKeyValue/KVDocument.cs @@ -5,14 +5,20 @@ namespace ValveKeyValue /// public class KVDocument : KVObject { + /// + /// Gets the header of this document containing encoding and format identifiers. + /// + public KVHeader Header { get; } + /// /// Initializes a new instance of the class. /// + /// Header of the document. /// Name of the document. /// Root value of the document. - public KVDocument(string name, KVValue value) : base(name, value) + public KVDocument(KVHeader header, string name, KVValue value) : base(name, value) { - // KV3 will require a header field that contains format/encoding here. + Header = header; } } } diff --git a/ValveKeyValue/ValveKeyValue/KVFlag.cs b/ValveKeyValue/ValveKeyValue/KVFlag.cs new file mode 100644 index 00000000..60ee80fb --- /dev/null +++ b/ValveKeyValue/ValveKeyValue/KVFlag.cs @@ -0,0 +1,29 @@ +namespace ValveKeyValue +{ + /// + /// Flags for KeyValue values. + /// + public enum KVFlag + { + /// No flag. + None = 0, + + /// Resource reference. + Resource = 1, + + /// Resource name reference. + ResourceName = 2, + + /// Panorama reference. + Panorama = 3, + + /// Sound event reference. + SoundEvent = 4, + + /// Sub-class reference. + SubClass = 5, + + /// Entity name reference. + EntityName = 6, + } +} diff --git a/ValveKeyValue/ValveKeyValue/KVHeader.cs b/ValveKeyValue/ValveKeyValue/KVHeader.cs new file mode 100644 index 00000000..b60112a5 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue/KVHeader.cs @@ -0,0 +1,20 @@ +using ValveKeyValue.KeyValues3; + +namespace ValveKeyValue +{ + /// + /// Represents the header of a KeyValues3 document containing encoding and format identifiers. + /// + public class KVHeader + { + /// + /// Gets or sets the encoding identifier. + /// + public KV3ID Encoding { get; set; } + + /// + /// Gets or sets the format identifier. + /// + public KV3ID Format { get; set; } + } +} diff --git a/ValveKeyValue/ValveKeyValue/KVNullValue.cs b/ValveKeyValue/ValveKeyValue/KVNullValue.cs new file mode 100644 index 00000000..91a76d93 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue/KVNullValue.cs @@ -0,0 +1,65 @@ +namespace ValveKeyValue +{ + /// + /// Represents a null KeyValue value. + /// + public class KVNullValue : KVValue + { + /// + public override KVValueType ValueType => KVValueType.Null; + + /// + public override TypeCode GetTypeCode() => TypeCode.Empty; + + /// + public override bool ToBoolean(IFormatProvider provider) => throw new NotSupportedException("Cannot convert null to Boolean."); + + /// + public override byte ToByte(IFormatProvider provider) => throw new NotSupportedException("Cannot convert null to Byte."); + + /// + public override char ToChar(IFormatProvider provider) => throw new NotSupportedException("Cannot convert null to Char."); + + /// + public override DateTime ToDateTime(IFormatProvider provider) => throw new NotSupportedException("Cannot convert null to DateTime."); + + /// + public override decimal ToDecimal(IFormatProvider provider) => throw new NotSupportedException("Cannot convert null to Decimal."); + + /// + public override double ToDouble(IFormatProvider provider) => throw new NotSupportedException("Cannot convert null to Double."); + + /// + public override short ToInt16(IFormatProvider provider) => throw new NotSupportedException("Cannot convert null to Int16."); + + /// + public override int ToInt32(IFormatProvider provider) => throw new NotSupportedException("Cannot convert null to Int32."); + + /// + public override long ToInt64(IFormatProvider provider) => throw new NotSupportedException("Cannot convert null to Int64."); + + /// + public override sbyte ToSByte(IFormatProvider provider) => throw new NotSupportedException("Cannot convert null to SByte."); + + /// + public override float ToSingle(IFormatProvider provider) => throw new NotSupportedException("Cannot convert null to Single."); + + /// + public override string ToString(IFormatProvider provider) => string.Empty; + + /// + public override object ToType(Type conversionType, IFormatProvider provider) => throw new NotSupportedException($"Cannot convert null to {conversionType}."); + + /// + public override ushort ToUInt16(IFormatProvider provider) => throw new NotSupportedException("Cannot convert null to UInt16."); + + /// + public override uint ToUInt32(IFormatProvider provider) => throw new NotSupportedException("Cannot convert null to UInt32."); + + /// + public override ulong ToUInt64(IFormatProvider provider) => throw new NotSupportedException("Cannot convert null to UInt64."); + + /// + public override string ToString() => string.Empty; + } +} diff --git a/ValveKeyValue/ValveKeyValue/KVObject.cs b/ValveKeyValue/ValveKeyValue/KVObject.cs index 25b909c2..9335c0e6 100644 --- a/ValveKeyValue/ValveKeyValue/KVObject.cs +++ b/ValveKeyValue/ValveKeyValue/KVObject.cs @@ -9,6 +9,15 @@ namespace ValveKeyValue [DebuggerDisplay("{DebuggerDescription}")] public partial class KVObject { + /// + /// Initializes a new instance of the class with an empty collection value. + /// + /// Name of this object. + public KVObject(string name) + : this(name, Array.Empty()) + { + } + /// /// Initializes a new instance of the class. /// @@ -16,7 +25,7 @@ public partial class KVObject /// Value of this object. public KVObject(string name, KVValue value) { - ArgumentNullException.ThrowIfNull(name); + //ArgumentNullException.ThrowIfNull(name); // Objects in an array will not have a name ArgumentNullException.ThrowIfNull(value); Name = name; @@ -30,7 +39,7 @@ public KVObject(string name, KVValue value) /// Child items of this object. public KVObject(string name, IEnumerable items) { - ArgumentNullException.ThrowIfNull(name); + //ArgumentNullException.ThrowIfNull(name); // Objects in an array will not have a name ArgumentNullException.ThrowIfNull(items); Name = name; @@ -40,6 +49,23 @@ public KVObject(string name, IEnumerable items) Value = value; } + /// + /// Initializes a new instance of the class. + /// + /// Name of this object. + /// Child items of this object. + public KVObject(string name, IEnumerable items) + { + //ArgumentNullException.ThrowIfNull(name); // Objects in an array will not have a name + ArgumentNullException.ThrowIfNull(items); + + Name = name; + var value = new KVArrayValue(); + value.AddRange(items); + + Value = value; + } + /// /// Gets the name of this object. /// @@ -50,6 +76,22 @@ public KVObject(string name, IEnumerable items) /// public KVValue Value { get; } + /// + /// Gets the number of children in this object's collection or array value. + /// Returns 0 if the value is neither a collection nor an array. + /// + public int Count => Value switch + { + KVCollectionValue c => c.Count, + KVArrayValue a => a.Count, + _ => 0, + }; + + /// + /// Gets a value indicating whether this object's value is an array. + /// + public bool IsArray => Value is KVArrayValue; + /// /// Indexer to find a child item by name. /// @@ -59,8 +101,12 @@ public KVValue this[string key] { get { - var children = GetCollectionValue(); - return children[key]; + if (Value is not KVCollectionValue collection) + { + return null; + } + + return collection[key]; } set @@ -70,6 +116,24 @@ public KVValue this[string key] } } + /// + /// Indexer to access an array element by index. + /// + /// The array index. + /// The at the specified index. + public KVValue this[int index] + { + get + { + if (Value is KVArrayValue array) + { + return array[index]; + } + + throw new NotSupportedException($"Integer indexer on a {nameof(KVObject)} can only be used when the value is an array."); + } + } + /// /// Adds a as a child of the current object. /// @@ -79,11 +143,70 @@ public void Add(KVObject value) GetCollectionValue().Add(value); } + /// + /// Adds a value to this object's array. + /// + /// The value to add. + public void Add(KVValue value) + { + if (Value is not KVArrayValue array) + { + throw new InvalidOperationException($"This operation on a {nameof(KVObject)} can only be used when the value is an array."); + } + + array.Add(value); + } + + /// + /// Adds a named value as a child of the current object. + /// + /// Name of the child. + /// Value of the child. + public void AddProperty(string name, KVValue value) + { + Add(new KVObject(name, value)); + } + + /// + /// Gets a child by name. + /// + /// Name of the child to find. + /// The child , or null if not found. + public KVObject GetChild(string name) + { + if (Value is KVCollectionValue collection) + { + return collection.Get(name); + } + + return null; + } + + /// + /// Determines whether this object contains a child with the given name. + /// + /// The name to check for. + /// true if a child with the given name exists; otherwise, false. + public bool ContainsKey(string name) + { + if (Value is KVCollectionValue collection) + { + return collection.Get(name) != null; + } + + return false; + } + /// /// Gets the children of this . /// public IEnumerable Children => (Value as KVCollectionValue) ?? Enumerable.Empty(); + /// + /// Gets the children of this . + /// + public IEnumerable ChildrenValues => (Value as KVArrayValue) ?? Enumerable.Empty(); + KVCollectionValue GetCollectionValue() { if (Value is not KVCollectionValue collection) @@ -94,6 +217,17 @@ KVCollectionValue GetCollectionValue() return collection; } - string DebuggerDescription => $"{Name}: {Value}"; + string DebuggerDescription + { + get + { + if (Value.ValueType == KVValueType.String) + { + return $"{Name}: {Value}"; + } + + return $"{Name}: {Value} ({Value.ValueType})"; + } + } } } diff --git a/ValveKeyValue/ValveKeyValue/KVObjectValue.cs b/ValveKeyValue/ValveKeyValue/KVObjectValue.cs index 0c95dc35..17f05172 100644 --- a/ValveKeyValue/ValveKeyValue/KVObjectValue.cs +++ b/ValveKeyValue/ValveKeyValue/KVObjectValue.cs @@ -22,10 +22,16 @@ public override TypeCode GetTypeCode() { return ValueType switch { + KVValueType.Boolean => TypeCode.Boolean, KVValueType.Collection => TypeCode.Object, KVValueType.FloatingPoint => TypeCode.Single, + KVValueType.FloatingPoint64 => TypeCode.Double, + KVValueType.Int16 => TypeCode.Int16, KVValueType.Int32 or KVValueType.Pointer => TypeCode.Int32, + KVValueType.Int64 => TypeCode.Int64, KVValueType.String => TypeCode.String, + KVValueType.UInt16 => TypeCode.UInt16, + KVValueType.UInt32 => TypeCode.UInt32, KVValueType.UInt64 => TypeCode.UInt64, _ => throw new NotImplementedException($"No known TypeCode for '{ValueType}'."), }; diff --git a/ValveKeyValue/ValveKeyValue/KVSerializationFormat.cs b/ValveKeyValue/ValveKeyValue/KVSerializationFormat.cs index 6a1f7a6d..20640b9c 100644 --- a/ValveKeyValue/ValveKeyValue/KVSerializationFormat.cs +++ b/ValveKeyValue/ValveKeyValue/KVSerializationFormat.cs @@ -13,6 +13,11 @@ public enum KVSerializationFormat /// /// KeyValues 1 binary format. Used occasionally in Steam. /// - KeyValues1Binary + KeyValues1Binary, + + /// + /// KeyValues 3 textual format. Used in the Source 2 engine. + /// + KeyValues3Text, } } diff --git a/ValveKeyValue/ValveKeyValue/KVSerializer.cs b/ValveKeyValue/ValveKeyValue/KVSerializer.cs index 7a9b1e26..b90fc8a1 100644 --- a/ValveKeyValue/ValveKeyValue/KVSerializer.cs +++ b/ValveKeyValue/ValveKeyValue/KVSerializer.cs @@ -2,7 +2,9 @@ using ValveKeyValue.Abstraction; using ValveKeyValue.Deserialization; using ValveKeyValue.Deserialization.KeyValues1; +using ValveKeyValue.Deserialization.KeyValues3; using ValveKeyValue.Serialization.KeyValues1; +using ValveKeyValue.Serialization.KeyValues3; namespace ValveKeyValue { @@ -38,13 +40,12 @@ public KVDocument Deserialize(Stream stream, KVSerializerOptions options = null) var builder = new KVObjectBuilder(); - using (var reader = MakeReader(stream, builder, options ?? KVSerializerOptions.DefaultOptions)) - { - reader.ReadObject(); - } + using var reader = MakeReader(stream, builder, options ?? KVSerializerOptions.DefaultOptions); + var header = reader.ReadHeader(); var root = builder.GetObject(); - return new KVDocument(root.Name, root.Value); + + return new KVDocument(header, root.Name, root.Value); } /// @@ -77,13 +78,17 @@ public void Serialize(Stream stream, KVObject data, KVSerializerOptions options } /// - /// Serializes a KeyValue object into stream. + /// Serializes a KeyValue document into stream, preserving header encoding and format. /// /// The stream to serialize into. /// The data to serialize. /// Options to use that can influence the serialization process. - public void Serialize(Stream stream, KVDocument data, KVSerializerOptions options = null) => - Serialize(stream, (KVObject)data, options); + public void Serialize(Stream stream, KVDocument data, KVSerializerOptions options = null) + { + using var serializer = MakeSerializer(stream, options ?? KVSerializerOptions.DefaultOptions, data.Header); + var visitor = new KVObjectVisitor(serializer); + visitor.Visit(data); + } /// /// Serializes a KeyValue object into stream in plain text. @@ -113,11 +118,12 @@ IVisitingReader MakeReader(Stream stream, IParsingVisitationListener listener, K { KVSerializationFormat.KeyValues1Text => new KV1TextReader(new StreamReader(stream, null, true, -1, leaveOpen: true), listener, options), KVSerializationFormat.KeyValues1Binary => new KV1BinaryReader(stream, listener, options.StringTable), + KVSerializationFormat.KeyValues3Text => new KV3TextReader(new StreamReader(stream, null, true, -1, leaveOpen: true), listener), _ => throw new ArgumentOutOfRangeException(nameof(format), format, "Invalid serialization format."), }; } - IVisitationListener MakeSerializer(Stream stream, KVSerializerOptions options) + IVisitationListener MakeSerializer(Stream stream, KVSerializerOptions options, KVHeader header = null) { ArgumentNullException.ThrowIfNull(stream); ArgumentNullException.ThrowIfNull(options); @@ -126,6 +132,7 @@ IVisitationListener MakeSerializer(Stream stream, KVSerializerOptions options) { KVSerializationFormat.KeyValues1Text => new KV1TextSerializer(stream, options), KVSerializationFormat.KeyValues1Binary => new KV1BinarySerializer(stream, options.StringTable), + KVSerializationFormat.KeyValues3Text => new KV3TextSerializer(stream, header), _ => throw new ArgumentOutOfRangeException(nameof(format), format, "Invalid serialization format."), }; } diff --git a/ValveKeyValue/ValveKeyValue/KVTokenType.cs b/ValveKeyValue/ValveKeyValue/KVTokenType.cs index f11dceed..83e68f74 100644 --- a/ValveKeyValue/ValveKeyValue/KVTokenType.cs +++ b/ValveKeyValue/ValveKeyValue/KVTokenType.cs @@ -9,6 +9,17 @@ enum KVTokenType Comment, Condition, IncludeAndAppend, - IncludeAndMerge + IncludeAndMerge, + + // KeyValues3 + Header, + Identifier, + Flag, + Assignment, + Comma, + CommentBlock, + ArrayStart, + ArrayEnd, + BinaryBlob, } } diff --git a/ValveKeyValue/ValveKeyValue/KVValue.cs b/ValveKeyValue/ValveKeyValue/KVValue.cs index 2fad47ac..0f5ed2e9 100644 --- a/ValveKeyValue/ValveKeyValue/KVValue.cs +++ b/ValveKeyValue/ValveKeyValue/KVValue.cs @@ -10,6 +10,11 @@ public abstract partial class KVValue : IConvertible /// public abstract KVValueType ValueType { get; } + /// + /// Gets or sets the current flags of this . + /// + public KVFlag Flag { get; set; } + /// /// Gets the child with the given key. /// diff --git a/ValveKeyValue/ValveKeyValue/KVValue_operators.cs b/ValveKeyValue/ValveKeyValue/KVValue_operators.cs index e10fc280..ed21d0ea 100644 --- a/ValveKeyValue/ValveKeyValue/KVValue_operators.cs +++ b/ValveKeyValue/ValveKeyValue/KVValue_operators.cs @@ -29,7 +29,7 @@ public static implicit operator KVValue(int value) /// The to cast. public static implicit operator KVValue(bool value) { - return new KVObjectValue(value ? 1 : 0, KVValueType.Int32); + return new KVObjectValue(value, KVValueType.Boolean); } /// @@ -68,6 +68,42 @@ public static implicit operator KVValue(long value) return new KVObjectValue(value, KVValueType.Int64); } + /// + /// Implicit cast operator for to . + /// + /// The to cast. + public static implicit operator KVValue(double value) + { + return new KVObjectValue(value, KVValueType.FloatingPoint64); + } + + /// + /// Implicit cast operator for to . + /// + /// The to cast. + public static implicit operator KVValue(uint value) + { + return new KVObjectValue(value, KVValueType.UInt32); + } + + /// + /// Implicit cast operator for to . + /// + /// The to cast. + public static implicit operator KVValue(short value) + { + return new KVObjectValue(value, KVValueType.Int16); + } + + /// + /// Implicit cast operator for to . + /// + /// The to cast. + public static implicit operator KVValue(ushort value) + { + return new KVObjectValue(value, KVValueType.UInt16); + } + /// /// Converts a to a . /// diff --git a/ValveKeyValue/ValveKeyValue/KeyValues3/Encoding.cs b/ValveKeyValue/ValveKeyValue/KeyValues3/Encoding.cs new file mode 100644 index 00000000..73bb0447 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue/KeyValues3/Encoding.cs @@ -0,0 +1,26 @@ +namespace ValveKeyValue.KeyValues3 +{ + /// + /// Known KeyValues3 encoding identifiers. + /// + public class Encoding + { + /// Automatic binary encoding selection. + public static Guid BinaryAuto { get; } = new(new byte[] { 0xE6, 0x09, 0xB1, 0x6E, 0x85, 0x6B, 0x83, 0x45, 0xA3, 0x12, 0x70, 0x3A, 0x6E, 0x04, 0x06, 0x8C }); + + /// Block-compressed binary encoding. + public static Guid BinaryBlockCompressed { get; } = new(new byte[] { 0x46, 0x1A, 0x79, 0x95, 0xBC, 0x95, 0x6C, 0x4F, 0xA7, 0x0B, 0x05, 0xBC, 0xA1, 0xB7, 0xDF, 0xD2 }); + + /// LZ4-compressed binary encoding. + public static Guid BinaryLZ4 { get; } = new(new byte[] { 0x8A, 0x34, 0x47, 0x68, 0xA1, 0x63, 0x5C, 0x4F, 0xA1, 0x97, 0x53, 0x80, 0x6F, 0xD9, 0xB1, 0x19 }); + + /// Zstandard-compressed binary encoding. + public static Guid BinaryZstd { get; } = new(new byte[] { 0x00, 0x0A, 0x62, 0x6F, 0xF0, 0xFE, 0x05, 0x43, 0xA3, 0x5F, 0x04, 0x23, 0x46, 0xB1, 0xDB, 0x29 }); + + /// Uncompressed binary encoding. + public static Guid Binary { get; } = new(new byte[] { 0x00, 0x05, 0x86, 0x1B, 0xD8, 0xF7, 0xC1, 0x40, 0xAD, 0x82, 0x75, 0xA4, 0x82, 0x67, 0xE7, 0x14 }); + + /// Text encoding. + public static Guid Text { get; } = new(new byte[] { 0x3C, 0x7F, 0x1C, 0xE2, 0x33, 0x8A, 0xC5, 0x41, 0x99, 0x77, 0xA7, 0x6D, 0x3A, 0x32, 0xAA, 0x0D }); + } +} diff --git a/ValveKeyValue/ValveKeyValue/KeyValues3/Format.cs b/ValveKeyValue/ValveKeyValue/KeyValues3/Format.cs new file mode 100644 index 00000000..8f7d1852 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue/KeyValues3/Format.cs @@ -0,0 +1,11 @@ +namespace ValveKeyValue.KeyValues3 +{ + /// + /// Known KeyValues3 format identifiers. + /// + public class Format + { + /// Generic format. + public static Guid Generic { get; } = new(new byte[] { 0x7C, 0x16, 0x12, 0x74, 0xE9, 0x06, 0x98, 0x46, 0xAF, 0xF2, 0xE6, 0x3E, 0xB5, 0x90, 0x37, 0xE7 }); + } +} diff --git a/ValveKeyValue/ValveKeyValue/KeyValues3/KV3ID.cs b/ValveKeyValue/ValveKeyValue/KeyValues3/KV3ID.cs new file mode 100644 index 00000000..551895e9 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue/KeyValues3/KV3ID.cs @@ -0,0 +1,17 @@ +namespace ValveKeyValue.KeyValues3 +{ + /// + /// Represents a KeyValues3 identifier with a name and GUID. + /// + public readonly record struct KV3ID(string Name, Guid Id) + { + /// + /// + /// Returns the in the format "Name:version{Guid}". + /// + public override string ToString() + { + return $"{Name}:version{{{Id}}}"; + } + } +} diff --git a/ValveKeyValue/ValveKeyValue/Serialization/KeyValues1/KV1BinarySerializer.cs b/ValveKeyValue/ValveKeyValue/Serialization/KeyValues1/KV1BinarySerializer.cs index d07ac029..a3c42f79 100644 --- a/ValveKeyValue/ValveKeyValue/Serialization/KeyValues1/KV1BinarySerializer.cs +++ b/ValveKeyValue/ValveKeyValue/Serialization/KeyValues1/KV1BinarySerializer.cs @@ -23,7 +23,7 @@ public void Dispose() writer.Dispose(); } - public void OnObjectStart(string name) + public void OnObjectStart(string name, KVFlag flag) { objectDepth++; Write(KV1BinaryNodeType.ChildObject); @@ -74,6 +74,10 @@ public void OnKeyValuePair(string name, KVValue value) } } + public void OnArrayStart(string name, KVFlag flag) => throw new NotImplementedException(); + public void OnArrayValue(KVValue value) => throw new NotImplementedException(); + public void OnArrayEnd() => throw new NotImplementedException(); + void Write(KV1BinaryNodeType nodeType) { writer.Write((byte)nodeType); diff --git a/ValveKeyValue/ValveKeyValue/Serialization/KeyValues1/KV1TextSerializer.cs b/ValveKeyValue/ValveKeyValue/Serialization/KeyValues1/KV1TextSerializer.cs index dd072339..fbb24108 100644 --- a/ValveKeyValue/ValveKeyValue/Serialization/KeyValues1/KV1TextSerializer.cs +++ b/ValveKeyValue/ValveKeyValue/Serialization/KeyValues1/KV1TextSerializer.cs @@ -21,13 +21,14 @@ public KV1TextSerializer(Stream stream, KVSerializerOptions options) readonly KVSerializerOptions options; readonly TextWriter writer; int indentation = 0; + readonly Stack arrayCount = new(); public void Dispose() { writer.Dispose(); } - public void OnObjectStart(string name) + public void OnObjectStart(string name, KVFlag flag) => WriteStartObject(name); public void OnObjectEnd() @@ -36,6 +37,27 @@ public void OnObjectEnd() public void OnKeyValuePair(string name, KVValue value) => WriteKeyValuePair(name, value); + public void OnArrayStart(string name, KVFlag flag) + { + WriteStartObject(name); + arrayCount.Push(0); + } + + public void OnArrayValue(KVValue value) + { + var count = arrayCount.Pop(); + + WriteKeyValuePair(count.ToString(), value); + + arrayCount.Push(count + 1); + } + + public void OnArrayEnd() + { + WriteEndObject(); + arrayCount.Pop(); + } + public void DiscardCurrentObject() { throw new NotSupportedException("Discard not supported when writing."); @@ -43,6 +65,22 @@ public void DiscardCurrentObject() void WriteStartObject(string name) { + if (name == null) + { + if (arrayCount.Count > 0) + { + var count = arrayCount.Pop(); + + name = count.ToString(); + + arrayCount.Push(count + 1); + } + else + { + name = string.Empty; + } + } + WriteIndentation(); WriteText(name); WriteLine(); @@ -60,12 +98,21 @@ void WriteEndObject() writer.WriteLine(); } - void WriteKeyValuePair(string name, IConvertible value) + void WriteKeyValuePair(string name, KVValue value) { WriteIndentation(); WriteText(name); writer.Write('\t'); - WriteText(value.ToString(CultureInfo.InvariantCulture)); + + if (value.ValueType == KVValueType.Boolean) + { + WriteText(value.ToBoolean(null) ? "1" : "0"); + } + else + { + WriteText(((IConvertible)value).ToString(CultureInfo.InvariantCulture)); + } + WriteLine(); } diff --git a/ValveKeyValue/ValveKeyValue/Serialization/KeyValues3/KV3TextSerializer.cs b/ValveKeyValue/ValveKeyValue/Serialization/KeyValues3/KV3TextSerializer.cs new file mode 100644 index 00000000..47ecaa4a --- /dev/null +++ b/ValveKeyValue/ValveKeyValue/Serialization/KeyValues3/KV3TextSerializer.cs @@ -0,0 +1,393 @@ +using System.Globalization; +using System.Text; +using ValveKeyValue.Abstraction; + +namespace ValveKeyValue.Serialization.KeyValues3 +{ + sealed class KV3TextSerializer : IVisitationListener, IDisposable + { + public KV3TextSerializer(Stream stream, KVHeader header = null) + { + ArgumentNullException.ThrowIfNull(stream); + + writer = new StreamWriter(stream, new UTF8Encoding(), bufferSize: 1024, leaveOpen: true) + { + NewLine = "\n" + }; + + var defaultEncoding = new ValveKeyValue.KeyValues3.KV3ID("text", ValveKeyValue.KeyValues3.Encoding.Text); + var defaultFormat = new ValveKeyValue.KeyValues3.KV3ID("generic", ValveKeyValue.KeyValues3.Format.Generic); + + var encoding = header?.Encoding.Name != null ? header.Encoding : defaultEncoding; + var format = header?.Format.Name != null ? header.Format : defaultFormat; + + writer.WriteLine($""); + } + + readonly TextWriter writer; + int indentation = 0; + readonly Stack inArray = new(); + + bool IsInArray => inArray.Count > 0 && inArray.Peek(); + + public void Dispose() + { + writer.Dispose(); + } + + public void OnObjectStart(string name, KVFlag flag) + { + inArray.Push(false); + + WriteStartObject(name, flag); + } + + public void OnObjectEnd() + { + inArray.Pop(); + + WriteEndObject(); + } + + public void OnKeyValuePair(string name, KVValue value) + => WriteKeyValuePair(name, value); + + public void OnArrayStart(string name, KVFlag flag) + { + inArray.Push(true); + + WriteIndentation(); + + WriteKey(name); + WriteFlag(flag); + + writer.Write('['); + indentation++; + WriteLine(); + } + + public void OnArrayValue(KVValue value) + { + WriteIndentation(); + + WriteValue(value); + + writer.Write(','); + writer.WriteLine(); // TODO: If short, no line? + } + + public void OnArrayEnd() + { + inArray.Pop(); + + indentation--; + WriteIndentation(); + writer.Write(']'); + + if (IsInArray) + { + writer.Write(','); + } + + writer.WriteLine(); + } + + public void DiscardCurrentObject() + { + throw new NotSupportedException("Discard not supported when writing."); + } + + void WriteStartObject(string name, KVFlag flag) + { + WriteIndentation(); + + if (indentation > 0) + { + WriteKey(name); + } + + WriteFlag(flag); + + writer.Write('{'); + indentation++; + WriteLine(); + } + + void WriteEndObject() + { + indentation--; + WriteIndentation(); + writer.Write('}'); + + if (IsInArray) + { + writer.Write(','); + } + + writer.WriteLine(); + } + + void WriteKeyValuePair(string name, KVValue value) + { + WriteIndentation(); + + WriteKey(name); + + WriteValue(value); + + WriteLine(); + } + + void WriteValue(KVValue value) + { + WriteFlag(value.Flag); + + switch (value.ValueType) + { + case KVValueType.BinaryBlob: + WriteBinaryBlob((KVBinaryBlob)value); + break; + case KVValueType.Boolean: + if ((bool)value) + { + writer.Write("true"); + } + else + { + writer.Write("false"); + } + break; + case KVValueType.Null: + writer.Write("null"); + break; + case KVValueType.FloatingPoint: + writer.Write(Convert.ToSingle(value, CultureInfo.InvariantCulture).ToString("#0.000000", CultureInfo.InvariantCulture)); + break; + case KVValueType.FloatingPoint64: + writer.Write(Convert.ToDouble(value, CultureInfo.InvariantCulture).ToString("#0.000000", CultureInfo.InvariantCulture)); + break; + case KVValueType.Int16: + case KVValueType.Int32: + case KVValueType.Int64: + case KVValueType.UInt16: + case KVValueType.UInt32: + case KVValueType.UInt64: + writer.Write(value.ToString(null)); + break; + default: + WriteText(value.ToString(null)); + break; + } + } + + void WriteBinaryBlob(KVBinaryBlob value) + { + var bytes = value.Bytes.Span; + + // TODO: Verify this against Valve + if (bytes.Length > 32) + { + writer.WriteLine(); + WriteIndentation(); + } + + writer.Write('#'); + writer.Write('['); + writer.WriteLine(); + indentation++; + WriteIndentation(); + + var i = 0; + + for (; i < bytes.Length; i++) + { + var b = bytes[i]; + writer.Write(HexStringHelper.HexToCharUpper(b >> 4)); + writer.Write(HexStringHelper.HexToCharUpper(b)); + + if (i > 0 && i % 32 == 0) + { + writer.WriteLine(); + WriteIndentation(); + } + else if (i != bytes.Length - 1) + { + writer.Write(' '); + } + } + + indentation--; + + if (i % 32 != 0) + { + writer.WriteLine(); + WriteIndentation(); + } + + writer.Write(']'); + } + + void WriteIndentation() + { + if (indentation == 0) + { + return; + } + + var text = new string('\t', indentation); + writer.Write(text); + } + + void WriteText(string text) + { + var isMultiline = text.Contains("\n", StringComparison.Ordinal); + + if (isMultiline) + { + text = text.Replace("\r\n", "\n"); + text = text.Replace("\"\"\"", "\\\"\"\""); + + writer.Write("\"\"\"\n"); + writer.Write(text); + writer.Write("\n\"\"\""); + } + else + { + writer.Write('"'); + + foreach (var @char in text) + { + switch (@char) + { + case '\n': + writer.Write("\\n"); + break; + + case '\t': + writer.Write("\\t"); + break; + + case '\\': + writer.Write("\\\\"); + break; + + case '"': + writer.Write("\\\""); + break; + + default: + writer.Write(@char); + break; + } + } + + writer.Write('"'); + } + } + + void WriteKey(string key) + { + if (key == null) + { + return; + } + + var escaped = key.Length == 0; // Quote empty strings + var sb = new StringBuilder(key.Length + 2); + sb.Append('"'); + + if (key.Length > 0 && key[0] >= '0' && key[0] <= '9') + { + // Quote when first character is a digit + escaped = true; + } + + foreach (var @char in key) + { + switch (@char) + { + case '\t': + escaped = true; + sb.Append('\\'); + sb.Append('t'); + break; + + case '\n': + escaped = true; + sb.Append('\\'); + sb.Append('n'); + break; + + case '"': + escaped = true; + sb.Append('\\'); + sb.Append('"'); + break; + + case '\\': + escaped = true; + sb.Append('\\'); + sb.Append('\\'); + break; + + default: + // TODO: Use char.IsAscii* functions from newer .NET + if (@char != '.' && @char != '_' && !((@char >= 'A' && @char <= 'Z') || (@char >= 'a' && @char <= 'z') || (@char >= '0' && @char <= '9'))) + { + escaped = true; + } + + sb.Append(@char); + break; + } + } + + if (escaped) + { + sb.Append('"'); + writer.Write(sb.ToString()); + } + else + { + writer.Write(key); + } + + writer.Write(" = "); + } + + void WriteFlag(KVFlag kvFlag) + { + if (kvFlag == KVFlag.None) + { + return; + } + + var name = SerializeFlagName(kvFlag); + + if (name != null) + { + writer.Write(name); + writer.Write(':'); + } + } + + void WriteLine() + { + writer.WriteLine(); + } + + static string SerializeFlagName(KVFlag flag) + { + return flag switch + { + KVFlag.Resource => "resource", + KVFlag.ResourceName => "resource_name", + KVFlag.Panorama => "panorama", + KVFlag.SoundEvent => "soundevent", + KVFlag.SubClass => "subclass", + KVFlag.EntityName => "entity_name", + _ => null, + }; + } + } +} diff --git a/ValveKeyValue/ValveKeyValue/ValveKeyValue.csproj b/ValveKeyValue/ValveKeyValue/ValveKeyValue.csproj index b7c0f666..81315404 100644 --- a/ValveKeyValue/ValveKeyValue/ValveKeyValue.csproj +++ b/ValveKeyValue/ValveKeyValue/ValveKeyValue.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0 10.0 Valve KeyValue Library Library to parse and write Valve KeyValue formats