diff --git a/ValveKeyValue/ValveKeyValue.Test/KVValueToStringTestCase.cs b/ValveKeyValue/ValveKeyValue.Test/KVValueToStringTestCase.cs index dbb0f1f4..d10398ba 100644 --- a/ValveKeyValue/ValveKeyValue.Test/KVValueToStringTestCase.cs +++ b/ValveKeyValue/ValveKeyValue.Test/KVValueToStringTestCase.cs @@ -14,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..4e33631b --- /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.7551 + "1" = 20.3763 + "2" = 19.6448 + } + arrayNoSpace = { + "0" = 1.3763 + "1" = 19.6448 + } + arrayMixedTypes = { + "0" = "a" + "1" = "1" + "2" = "True" + "3" = "False" + "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" + 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..942164b6 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/array_kv1.vdf @@ -0,0 +1,44 @@ +"root" +{ + "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" "True" + "3" "False" + "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..e69142e3 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/array_serialized.kv3 @@ -0,0 +1,42 @@ + +{ + 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.42, + ] + 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/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..a23ab7c5 --- /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..2b3b84a0 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/TextKV3/flagged_value_kv1.vdf @@ -0,0 +1,21 @@ +"root" +{ + "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..859d794e --- /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 = 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" + } + 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..e6cd87b3 --- /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..b089f37d --- /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.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 = 123.456 + 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..87654957 100644 --- a/ValveKeyValue/ValveKeyValue.Test/Test Data/apisurface.txt +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/apisurface.txt @@ -5,9 +5,6 @@ public interface ValveKeyValue.IIncludedFileLoader public class ValveKeyValue.KeyValueException { - public .ctor(); - public .ctor(string message); - public .ctor(string message, Exception inner); protected void add_SerializeObjectState(EventHandler`1[[System.Runtime.Serialization.SafeSerializationEventArgs]] value); public bool Equals(object obj); protected void Finalize(); @@ -34,7 +31,6 @@ public class ValveKeyValue.KeyValueException public class ValveKeyValue.KVArrayValue { - public .ctor(); public void Add(ValveKeyValue.KVValue value); public void AddRange(System.Collections.Generic.IEnumerable`1[[ValveKeyValue.KVValue]] values); public void Clear(); @@ -43,6 +39,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 +53,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); @@ -78,17 +76,17 @@ public class ValveKeyValue.KVArrayValue public class ValveKeyValue.KVBinaryBlob { - public .ctor(byte[] value); - public .ctor(Memory`1[[byte]] value); 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); @@ -110,11 +108,12 @@ public class ValveKeyValue.KVBinaryBlob public class ValveKeyValue.KVDocument { - public .ctor(string name, ValveKeyValue.KVValue value); public void Add(ValveKeyValue.KVObject value); 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 ValveKeyValue.KVHeader get_Header(); public ValveKeyValue.KVValue get_Item(string key); public string get_Name(); public ValveKeyValue.KVValue get_Value(); @@ -126,9 +125,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; + + 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 bool Equals(object obj); + protected void Finalize(); + public Guid get_Encoding(); + public Guid get_Format(); + public int GetHashCode(); + public Type GetType(); + protected object MemberwiseClone(); + public void set_Encoding(Guid value); + public void set_Format(Guid value); + public string ToString(); +} + public sealed class ValveKeyValue.KVIgnoreAttribute { - public .ctor(); public bool Equals(object obj); protected void Finalize(); public object get_TypeId(); @@ -142,12 +177,11 @@ public sealed class ValveKeyValue.KVIgnoreAttribute public class ValveKeyValue.KVObject { - public .ctor(string name, System.Collections.Generic.IEnumerable`1[[ValveKeyValue.KVObject]] items); - public .ctor(string name, ValveKeyValue.KVValue value); public void Add(ValveKeyValue.KVObject value); 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 ValveKeyValue.KVValue get_Item(string key); public string get_Name(); public ValveKeyValue.KVValue get_Value(); @@ -161,7 +195,6 @@ public class ValveKeyValue.KVObject public sealed class ValveKeyValue.KVPropertyAttribute { - public .ctor(string propertyName); public bool Equals(object obj); protected void Finalize(); public string get_PropertyName(); @@ -178,6 +211,7 @@ public sealed enum ValveKeyValue.KVSerializationFormat { KeyValues1Text = 0; KeyValues1Binary = 1; + KeyValues3Text = 2; public int CompareTo(object target); public bool Equals(object obj); @@ -211,7 +245,6 @@ public class ValveKeyValue.KVSerializer public sealed class ValveKeyValue.KVSerializerOptions { - public .ctor(); public bool Equals(object obj); protected void Finalize(); public System.Collections.Generic.IList`1[[string]] get_Conditions(); @@ -219,22 +252,20 @@ public sealed class ValveKeyValue.KVSerializerOptions public bool get_EnableValveNullByteBugBehavior(); public ValveKeyValue.IIncludedFileLoader get_FileLoader(); public bool get_HasEscapeSequences(); - public ValveKeyValue.StringTable get_StringTable(); public int GetHashCode(); public Type GetType(); protected object MemberwiseClone(); public void set_EnableValveNullByteBugBehavior(bool value); public void set_FileLoader(ValveKeyValue.IIncludedFileLoader value); public void set_HasEscapeSequences(bool value); - public void set_StringTable(ValveKeyValue.StringTable value); public string ToString(); } 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(); @@ -263,6 +294,7 @@ public class ValveKeyValue.KVValue public static ValveKeyValue.KVValue op_Implicit(long value); public static ValveKeyValue.KVValue op_Implicit(string value); public static ValveKeyValue.KVValue op_Implicit(ulong value); + public void set_Flag(ValveKeyValue.KVFlag value); public bool ToBoolean(IFormatProvider provider); public byte ToByte(IFormatProvider provider); public char ToChar(IFormatProvider provider); @@ -288,17 +320,13 @@ public sealed enum ValveKeyValue.KVValueType Collection = 1; Array = 2; BinaryBlob = 3; - Boolean = 4; - String = 5; - Int16 = 6; - Int32 = 7; - Int64 = 8; - UInt16 = 9; - UInt32 = 10; - UInt64 = 11; - FloatingPoint = 12; - FloatingPoint64 = 13; - Pointer = 14; + String = 4; + Int32 = 5; + UInt64 = 6; + FloatingPoint = 7; + Pointer = 8; + Int64 = 9; + Boolean = 10; public int CompareTo(object target); public bool Equals(object obj); @@ -314,20 +342,28 @@ public sealed enum ValveKeyValue.KVValueType public string ToString(string format, IFormatProvider provider); } -public sealed class ValveKeyValue.StringTable +public class ValveKeyValue.KeyValues3.Encoding +{ + public bool Equals(object obj); + protected void Finalize(); + public static Guid get_BinaryBlockCompressed(); + public static Guid get_BinaryBlockLZ4(); + public static Guid get_BinaryBlockUncompressed(); + 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 .ctor(int capacity); - public .ctor(System.Collections.Generic.IList`1[[string]] values); - public void Add(string value); public bool Equals(object obj); protected void Finalize(); - public string get_Item(int index); + public static Guid get_Generic(); public int GetHashCode(); - public int GetOrAdd(string value); public Type GetType(); protected object MemberwiseClone(); - public string[] ToArray(); 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..8f51a7cd --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/TextKV3/BasicKV3TestCases.cs @@ -0,0 +1,204 @@ +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 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.FloatingPoint)); + 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.FloatingPoint)); + 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.FloatingPoint)); + 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.FloatingPoint)); + Assert.That((double)data["doubleMaxValue"], Is.EqualTo(62147483647.1337)); + + Assert.That(data["doubleNegativeMaxValue"].ValueType, Is.EqualTo(KVValueType.FloatingPoint)); + Assert.That((double)data["doubleNegativeMaxValue"], Is.EqualTo(-62147483647.1337)); + + Assert.That(data["doubleExponent"].ValueType, Is.EqualTo(KVValueType.FloatingPoint)); + 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..b27bdcbb --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/TextKV3/RootTypesTestCase.cs @@ -0,0 +1,146 @@ +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.EqualTo("root")); + Assert.That(data.Value.ValueType, Is.EqualTo(KVValueType.Null)); + Assert.That((string)data.Value, Is.EqualTo("")); // TODO: This should be a null value + }); + } + + [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.EqualTo("root")); + 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.EqualTo("root")); + 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.EqualTo("root")); + 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.EqualTo("root")); + 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.EqualTo("root")); + 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.EqualTo("root")); + 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.EqualTo("root")); + 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.EqualTo("root")); + Assert.That(data.Value.ValueType, Is.EqualTo(KVValueType.FloatingPoint)); + 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..71ac6899 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue.Test/TextKV3/SerializationTestCase.cs @@ -0,0 +1,98 @@ +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 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)); + } + } +} diff --git a/ValveKeyValue/ValveKeyValue.Test/ValveKeyValue.Test.csproj b/ValveKeyValue/ValveKeyValue.Test/ValveKeyValue.Test.csproj index 60caf9ed..6634dcaa 100644 --- a/ValveKeyValue/ValveKeyValue.Test/ValveKeyValue.Test.csproj +++ b/ValveKeyValue/ValveKeyValue.Test/ValveKeyValue.Test.csproj @@ -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 431e3a0d..25858b06 100644 --- a/ValveKeyValue/ValveKeyValue/Abstraction/KVObjectVisitor.cs +++ b/ValveKeyValue/ValveKeyValue/Abstraction/KVObjectVisitor.cs @@ -13,25 +13,39 @@ 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: // TODO: Should binary blobs have their own method? case KVValueType.FloatingPoint: case KVValueType.Int32: 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 +58,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 36c82a2a..49b15553 100644 --- a/ValveKeyValue/ValveKeyValue/Deserialization/KVObjectBuilder.cs +++ b/ValveKeyValue/ValveKeyValue/Deserialization/KVObjectBuilder.cs @@ -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 @@ 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) @@ -126,6 +192,36 @@ KVObject MakeObject(KVPartialState state) @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 7f367afb..c022e8c4 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() { Require.NotDisposed(nameof(KV1TextReader), disposed); @@ -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 14399372..aef2f90f 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() { Require.NotDisposed(nameof(KV1TextReader), disposed); @@ -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..a59a0f23 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues3/KV3TextReader.cs @@ -0,0 +1,363 @@ +using System.Globalization; +using ValveKeyValue.Abstraction; + +namespace ValveKeyValue.Deserialization.KeyValues3 +{ + sealed class KV3TextReader : IVisitingReader + { + public KV3TextReader(TextReader textReader, IParsingVisitationListener listener) + { + Require.NotNull(textReader, nameof(textReader)); + Require.NotNull(listener, nameof(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() + { + Require.NotDisposed(nameof(KV3TextReader), disposed); + + 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: + ReadText(token.Value); + break; + + 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)) + { + // TODO: Null is not a string + // TODO: KVObjectValue does not accept null + //value = new KVObjectValue(null, KVValueType.Null); + return new KVObjectValue(string.Empty, KVValueType.Null); + } + 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; + + // TODO: + if (double.TryParse(text, FloatingPointNumberStyles, CultureInfo.InvariantCulture, out var floatValue)) + { + return new KVObjectValue(floatValue, KVValueType.FloatingPoint); + } + } + + return new KVObjectValue(text, KVValueType.String); + } + + static byte ParseHexCharacter(string hexadecimalRepresentation) + { + if (hexadecimalRepresentation.Length != 2) + { + throw new InvalidDataException("Expected hex byte (eg. 00-FF)"); + } + + return byte.Parse(hexadecimalRepresentation, NumberStyles.HexNumber, CultureInfo.InvariantCulture); + } + + static KVFlag ParseFlag(string flag) + { + return flag.ToLowerInvariant() switch + { + "resource" => KVFlag.Resource, + "resource_name" => KVFlag.ResourceName, + "panorama" => KVFlag.Panorama, + "soundevent" => KVFlag.SoundEvent, + "subclass" => KVFlag.SubClass, + _ => 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..b4a0a685 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues3/KV3TextReaderState.cs @@ -0,0 +1,10 @@ +namespace ValveKeyValue.Deserialization.KeyValues3 +{ + enum KV3TextReaderState + { + InObjectBeforeKey, + InObjectAfterKey, + InObjectAfterValue, + InArray, + } +} diff --git a/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues3/KV3TextReaderStateMachine.cs b/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues3/KV3TextReaderStateMachine.cs new file mode 100644 index 00000000..b233dc88 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues3/KV3TextReaderStateMachine.cs @@ -0,0 +1,52 @@ +namespace ValveKeyValue.Deserialization.KeyValues3 +{ + class KV3TextReaderStateMachine + { + public KV3TextReaderStateMachine() + { + states = new Stack>(); + + // TODO: Get rid of this, kv3 has no root + // Bare values such as 'null' can be root + PushObject(); + SetName("root"); + 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..91625ced --- /dev/null +++ b/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues3/KV3TokenReader.cs @@ -0,0 +1,439 @@ +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() + { + Require.NotDisposed(nameof(KV3TokenReader), disposed); + 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 format specifier, expected '{Encoding.Text}' but got '{encoding}'."); + } + + if (formatType.Equals("generic", StringComparison.OrdinalIgnoreCase) && format != Format.Generic) + { + throw new InvalidDataException($"Unrecognized encoding specifier, expected '{Format.Generic}' but got '{format}'."); + } + + return new KVHeader + { + Encoding = encoding, + Format = format, + }; + } + + KVToken ReadComment() + { + ReadChar(CommentBegin); + + var sb = new StringBuilder(); + var next = Next(); + var isMultiline = false; + + if (next == '*') + { + isMultiline = true; + } + else if (next != CommentBegin) + { + // TODO: Return identifier? + throw new InvalidDataException("The syntax is incorrect, or is it?"); + } + + if (isMultiline) + { + while (true) + { + next = Next(); + + if (next == '*') + { + var nextNext = Peek(); + + if (nextNext == '/') + { + Next(); + break; + } + } + + sb.Append(next); + } + } + else + { + while (true) + { + next = Next(); + + if (next == '\n') + { + break; + } + + sb.Append(next); + } + + if (sb.Length > 0 && sb[^1] == '\r') + { + sb.Remove(sb.Length - 1, 1); + } + } + + var text = sb.ToString(); + + return new KVToken(KVTokenType.Comment, text); + } + + 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(); + + // Is there another quote mark? + // TODO: Peek() for more than one character + 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) + { + var escapeNext = false; + + // Scan until \n""" + while (true) + { + var next = Next(); + + if (next == '\\') + { + // TODO: Is valve keeping the \ character in the string? + escapeNext = true; + } + + if (!escapeNext && next == '\n') + { + var a = Next(); + var b = Next(); + var c = Next(); + + if (a == '"' && b == '"' && c == '"') + { + break; + } + + sb.Append(next); + sb.Append(a); + sb.Append(b); + sb.Append(c); + } + else + { + escapeNext = false; + + sb.Append(next); + } + } + + if (sb.Length > 0 && sb[^1] == '\r') + { + sb.Remove(sb.Length - 1, 1); + } + } + else + { + // TODO: Figure out '\' character escapes, does Valve actually unescape anything? + while (Peek() != quotationMark) + { + var next = Next(); + sb.Append(next); + } + + ReadChar(quotationMark); + } + + return sb.ToString(); + } + } +} diff --git a/ValveKeyValue/ValveKeyValue/KVDocument.cs b/ValveKeyValue/ValveKeyValue/KVDocument.cs index aebd995d..2417ddfa 100644 --- a/ValveKeyValue/ValveKeyValue/KVDocument.cs +++ b/ValveKeyValue/ValveKeyValue/KVDocument.cs @@ -2,9 +2,11 @@ namespace ValveKeyValue { public class KVDocument : KVObject { - public KVDocument(string name, KVValue value) : base(name, value) + public KVHeader Header { get; } + + 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..eea986e0 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue/KVFlag.cs @@ -0,0 +1,12 @@ +namespace ValveKeyValue +{ + public enum KVFlag + { + None = 0, + Resource = 1, + ResourceName = 2, + Panorama = 3, + SoundEvent = 4, + SubClass = 5, + } +} diff --git a/ValveKeyValue/ValveKeyValue/KVHeader.cs b/ValveKeyValue/ValveKeyValue/KVHeader.cs new file mode 100644 index 00000000..17ab54aa --- /dev/null +++ b/ValveKeyValue/ValveKeyValue/KVHeader.cs @@ -0,0 +1,8 @@ +namespace ValveKeyValue +{ + public class KVHeader + { + public Guid Encoding { get; set; } + public Guid Format { get; set; } + } +} diff --git a/ValveKeyValue/ValveKeyValue/KVObject.cs b/ValveKeyValue/ValveKeyValue/KVObject.cs index 1e907cba..7dfbb684 100644 --- a/ValveKeyValue/ValveKeyValue/KVObject.cs +++ b/ValveKeyValue/ValveKeyValue/KVObject.cs @@ -17,7 +17,7 @@ public partial class KVObject /// Value of this object. public KVObject(string name, KVValue value) { - Require.NotNull(name, nameof(name)); + //Require.NotNull(name, nameof(name)); // Objects in an array will not have a name Require.NotNull(value, nameof(value)); Name = name; @@ -31,7 +31,7 @@ public KVObject(string name, KVValue value) /// Child items of this object. public KVObject(string name, IEnumerable items) { - Require.NotNull(name, nameof(name)); + //Require.NotNull(name, nameof(name)); // Objects in an array will not have a name Require.NotNull(items, nameof(items)); Name = name; @@ -41,6 +41,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) + { + //Require.NotNull(name, nameof(name)); // Objects in an array will not have a name + Require.NotNull(items, nameof(items)); + + Name = name; + var value = new KVArrayValue(); + value.AddRange(items); + + Value = value; + } + /// /// Gets the name of this object. /// @@ -90,6 +107,11 @@ public void Add(KVObject value) /// 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) @@ -100,6 +122,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/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 72db7638..77fb4569 100644 --- a/ValveKeyValue/ValveKeyValue/KVSerializer.cs +++ b/ValveKeyValue/ValveKeyValue/KVSerializer.cs @@ -1,7 +1,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 { @@ -36,13 +38,12 @@ public KVDocument Deserialize(Stream stream, KVSerializerOptions options = null) Require.NotNull(stream, nameof(stream)); 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); // TODO } /// @@ -116,6 +117,7 @@ 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."), }; } @@ -129,6 +131,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), _ => 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 ba0a5dc0..18dc7fa9 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/KeyValues3/Encoding.cs b/ValveKeyValue/ValveKeyValue/KeyValues3/Encoding.cs new file mode 100644 index 00000000..d2f6b21b --- /dev/null +++ b/ValveKeyValue/ValveKeyValue/KeyValues3/Encoding.cs @@ -0,0 +1,10 @@ +namespace ValveKeyValue.KeyValues3 +{ + public class 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 }); + public static Guid BinaryBlockCompressed { get; } = new(new byte[] { 0x46, 0x1A, 0x79, 0x95, 0xBC, 0x95, 0x6C, 0x4F, 0xA7, 0x0B, 0x05, 0xBC, 0xA1, 0xB7, 0xDF, 0xD2 }); + public static Guid BinaryBlockUncompressed { get; } = new(new byte[] { 0x00, 0x05, 0x86, 0x1B, 0xD8, 0xF7, 0xC1, 0x40, 0xAD, 0x82, 0x75, 0xA4, 0x82, 0x67, 0xE7, 0x14 }); + public static Guid BinaryBlockLZ4 { get; } = new(new byte[] { 0x8A, 0x34, 0x47, 0x68, 0xA1, 0x63, 0x5C, 0x4F, 0xA1, 0x97, 0x53, 0x80, 0x6F, 0xD9, 0xB1, 0x19 }); + } +} diff --git a/ValveKeyValue/ValveKeyValue/KeyValues3/Format.cs b/ValveKeyValue/ValveKeyValue/KeyValues3/Format.cs new file mode 100644 index 00000000..b2c6ebb2 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue/KeyValues3/Format.cs @@ -0,0 +1,7 @@ +namespace ValveKeyValue.KeyValues3 +{ + public class 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/Serialization/KeyValues1/KV1BinarySerializer.cs b/ValveKeyValue/ValveKeyValue/Serialization/KeyValues1/KV1BinarySerializer.cs index 4a5705d3..46868dde 100644 --- a/ValveKeyValue/ValveKeyValue/Serialization/KeyValues1/KV1BinarySerializer.cs +++ b/ValveKeyValue/ValveKeyValue/Serialization/KeyValues1/KV1BinarySerializer.cs @@ -24,7 +24,7 @@ public void Dispose() writer.Dispose(); } - public void OnObjectStart(string name) + public void OnObjectStart(string name, KVFlag flag) { objectDepth++; Write(KV1BinaryNodeType.ChildObject); @@ -75,6 +75,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 7473dc97..57a1bfe3 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,15 @@ public void DiscardCurrentObject() void WriteStartObject(string name) { + if (name == null) + { + var count = arrayCount.Pop(); + + name = count.ToString(); + + arrayCount.Push(count + 1); + } + WriteIndentation(); WriteText(name); WriteLine(); @@ -62,6 +93,8 @@ void WriteEndObject() void WriteKeyValuePair(string name, IConvertible value) { + // TODO: Handle true, false, null value types + WriteIndentation(); WriteText(name); writer.Write('\t'); diff --git a/ValveKeyValue/ValveKeyValue/Serialization/KeyValues3/KV3TextSerializer.cs b/ValveKeyValue/ValveKeyValue/Serialization/KeyValues3/KV3TextSerializer.cs new file mode 100644 index 00000000..db6359bc --- /dev/null +++ b/ValveKeyValue/ValveKeyValue/Serialization/KeyValues3/KV3TextSerializer.cs @@ -0,0 +1,397 @@ +using System.Text; +using ValveKeyValue.Abstraction; + +namespace ValveKeyValue.Serialization.KeyValues3 +{ + sealed class KV3TextSerializer : IVisitationListener, IDisposable + { + public KV3TextSerializer(Stream stream) + { + Require.NotNull(stream, nameof(stream)); + + writer = new StreamWriter(stream, new UTF8Encoding(), bufferSize: 1024, leaveOpen: true) + { + NewLine = "\n" + }; + + // TODO: Write correct encoding and format + 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(); + + // TODO: Dumb hack, we should not have a root name + if (indentation != 0 && name != "root") + { + 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: + case KVValueType.Int64: + 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) + { + writer.Write("\"\"\"\n"); + + text = text.Replace("\"\"\"", "\\\"\"\""); + + writer.Write(text); + writer.Write("\n\"\"\""); + } + else + { + writer.Write('"'); + + foreach (var @char in text) + { + switch (@char) + { + 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 flags = (int)kvFlag; + var i = 0; + var currentFlag = -1; + var more = false; + + while (i < flags) + { + var flag = (1 << ++currentFlag); + + i += flag; + + if ((flag & flags) == 0) + { + continue; + } + + var serialized = SerializeFlagName((KVFlag)flag); + + if (serialized == null) + { + continue; + } + + if (more) + { + writer.Write('|'); + } + + writer.Write(serialized); + + more = true; + } + + writer.Write(':'); + } + + void WriteLine() + { + writer.WriteLine(); + } + + string SerializeFlagName(KVFlag flag) + { + return flag switch + { + KVFlag.Resource => "resource", + KVFlag.ResourceName => "resource_name", + KVFlag.Panorama => "panorama", + KVFlag.SoundEvent => "soundevent", + KVFlag.SubClass => "subclass", + _ => null, + }; + } + } +}