Skip to content

Commit 36bd1d2

Browse files
authored
Implement support for KV1 binary serialization with string tables (#100)
* fix typo in existing test case * Bump C# lang version to latest (12 with SDK8) * Implement binary string table support * add constructors to apisurface test
1 parent f7d32f2 commit 36bd1d2

File tree

11 files changed

+306
-8
lines changed

11 files changed

+306
-8
lines changed

Directory.Build.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<AssemblyVersion>$(ProjectBaseVersion)</AssemblyVersion>
88
<FileVersion>$(ProjectVersion)</FileVersion>
99
<Version>$(ProjectVersion)</Version>
10+
<LangVersion>latest</LangVersion>
1011
</PropertyGroup>
1112

1213
<PropertyGroup>

ValveKeyValue/ValveKeyValue.Test/ApiSurfaceTestCase.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,37 @@ static void GenerateTypeApiSurface(StringBuilder sb, Type type)
8383
sb.Append('\n');
8484
}
8585

86+
var constructors = type
87+
.GetConstructors(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
88+
.Where(t => !t.IsPrivate && !t.IsAssembly && !t.IsFamilyAndAssembly)
89+
.OrderBy(t => t.Name, StringComparer.InvariantCulture)
90+
.ThenBy(t => string.Join(", ", t.GetParameters().Select(GetParameterAsString)), StringComparer.InvariantCulture);
91+
92+
foreach (var constructor in constructors)
93+
{
94+
sb.Append(" ");
95+
96+
if (constructor.IsPublic)
97+
{
98+
sb.Append("public");
99+
}
100+
else
101+
{
102+
sb.Append("protected");
103+
}
104+
105+
if (constructor.IsStatic)
106+
{
107+
sb.Append(" static");
108+
}
109+
110+
sb.Append(' ');
111+
sb.Append(constructor.Name);
112+
sb.Append('(');
113+
sb.Append(string.Join(", ", constructor.GetParameters().Select(GetParameterAsString)));
114+
sb.Append(");\n");
115+
}
116+
86117
var methods = type
87118
.GetMethods(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
88119
.Where(t => !t.IsPrivate && !t.IsAssembly && !t.IsFamilyAndAssembly)

ValveKeyValue/ValveKeyValue.Test/Binary/SimpleBinaryTestCase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public void SetUp()
5656
0x06, // pointer: ptr = 0x11223344
5757
0x70, 0x74, 0x72, 0x00,
5858
0x44, 0x33, 0x22, 0x11,
59-
0x07, // uint64: long = 0x1122334455667788
59+
0x07, // uint64: lng = 0x1122334455667788
6060
0x6C, 0x6E, 0x67, 0x00,
6161
0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11,
6262
0x0A, // int64, i64 = 0x0102030405070809
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System.Linq;
2+
3+
namespace ValveKeyValue.Test
4+
{
5+
class StringTableFromScratchTestCase
6+
{
7+
[Test]
8+
public void PopulatesStringTableDuringSerialization()
9+
{
10+
var kv = new KVObject("root",
11+
[
12+
new KVObject("key", "value"),
13+
new KVObject("child", [
14+
new KVObject("key", 123),
15+
]),
16+
]);
17+
18+
var stringTable = new StringTable();
19+
20+
var serializer = KVSerializer.Create(KVSerializationFormat.KeyValues1Binary);
21+
22+
using var ms = new MemoryStream();
23+
serializer.Serialize(ms, kv, new KVSerializerOptions { StringTable = stringTable });
24+
25+
var strings = stringTable.ToArray();
26+
Assert.That(strings, Is.EqualTo(new[] { "root", "key", "child" }));
27+
}
28+
}
29+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using System.Linq;
2+
3+
namespace ValveKeyValue.Test
4+
{
5+
class StringTableTestCase
6+
{
7+
[Test]
8+
public void IsNotNull()
9+
=> Assert.That(obj, Is.Not.Null);
10+
11+
[Test]
12+
public void HasName()
13+
=> Assert.That(obj.Name, Is.EqualTo("TestObject"));
14+
15+
[Test]
16+
public void IsObjectWithChildren()
17+
=> Assert.That(obj.Value.ValueType, Is.EqualTo(KVValueType.Collection));
18+
19+
[TestCase(ExpectedResult = 5)]
20+
public int HasChildren()
21+
=> obj.Children.Count();
22+
23+
[TestCase("key", "value", typeof(string))]
24+
[TestCase("int", 0x01020304, typeof(int))]
25+
[TestCase("flt", 1234.5678f, typeof(float))]
26+
[TestCase("lng", 0x1122334455667788, typeof(ulong))]
27+
[TestCase("i64", 0x0102030405060708, typeof(long))]
28+
public void HasNamedChildWithValue(string name, object value, Type valueType)
29+
{
30+
Assert.That(Convert.ChangeType(obj[name], valueType), Is.EqualTo(value));
31+
}
32+
33+
[Test]
34+
public void SymmetricStringTableSerialization()
35+
{
36+
var serializer = KVSerializer.Create(KVSerializationFormat.KeyValues1Binary);
37+
38+
using var ms = new MemoryStream();
39+
serializer.Serialize(ms, obj, new KVSerializerOptions { StringTable = new(TestStringTable) });
40+
41+
Assert.That(ms.ToArray(), Is.EqualTo(TestData.ToArray()));
42+
}
43+
44+
KVObject obj;
45+
46+
[OneTimeSetUp]
47+
public void SetUp()
48+
{
49+
obj = KVSerializer.Create(KVSerializationFormat.KeyValues1Binary)
50+
.Deserialize(
51+
TestData.ToArray(),
52+
new KVSerializerOptions { StringTable = new(TestStringTable) });
53+
}
54+
55+
static string[] TestStringTable => [
56+
"flt",
57+
"i64",
58+
"int",
59+
"key",
60+
"lng",
61+
"TestObject"
62+
];
63+
64+
static ReadOnlySpan<byte> TestData =>
65+
[
66+
0x00, // object: TestObject
67+
0x05, 0x00, 0x00, 0x00, // stringTable[5] = "TestObject",
68+
0x01, // string: key = value
69+
0x03, 0x00, 0x00, 0x00, // stringTable[3] = "key",
70+
0x76, 0x61, 0x6C, 0x75, 0x65, 0x00,
71+
0x02, // int32: int = 0x01020304
72+
0x02, 0x00, 0x00, 0x00, // stringTable[2] = "int"
73+
0x04, 0x03, 0x02, 0x01,
74+
0x03, // float32: flt = 1234.5678f
75+
0x00, 0x00, 0x00, 0x00, // stringTable[0] = "flt"
76+
0x2B, 0x52, 0x9A, 0x44,
77+
0x07, // uint64: lng = 0x1122334455667788
78+
0x04, 0x00, 0x00, 0x00, // stringTable[4] = "lng"
79+
0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11,
80+
0x0A, // int64, i64 = 0x0102030405070809
81+
0x01, 0x00, 0x00, 0x00, // stringTable[1] = "i64"
82+
0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01,
83+
0x08, // end object
84+
0x08, // end document
85+
];
86+
}
87+
}

ValveKeyValue/ValveKeyValue.Test/Test Data/apisurface.txt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ public interface ValveKeyValue.IIncludedFileLoader
55

66
public class ValveKeyValue.KeyValueException
77
{
8+
public .ctor();
9+
public .ctor(string message);
10+
public .ctor(string message, Exception inner);
811
protected void add_SerializeObjectState(EventHandler`1[[System.Runtime.Serialization.SafeSerializationEventArgs]] value);
912
public bool Equals(object obj);
1013
protected void Finalize();
@@ -31,6 +34,7 @@ public class ValveKeyValue.KeyValueException
3134

3235
public class ValveKeyValue.KVArrayValue
3336
{
37+
public .ctor();
3438
public void Add(ValveKeyValue.KVValue value);
3539
public void AddRange(System.Collections.Generic.IEnumerable`1[[ValveKeyValue.KVValue]] values);
3640
public void Clear();
@@ -74,6 +78,8 @@ public class ValveKeyValue.KVArrayValue
7478

7579
public class ValveKeyValue.KVBinaryBlob
7680
{
81+
public .ctor(byte[] value);
82+
public .ctor(Memory`1[[byte]] value);
7783
public bool Equals(object obj);
7884
protected void Finalize();
7985
public Memory`1[[byte]] get_Bytes();
@@ -104,6 +110,7 @@ public class ValveKeyValue.KVBinaryBlob
104110

105111
public class ValveKeyValue.KVDocument
106112
{
113+
public .ctor(string name, ValveKeyValue.KVValue value);
107114
public void Add(ValveKeyValue.KVObject value);
108115
public bool Equals(object obj);
109116
protected void Finalize();
@@ -121,6 +128,7 @@ public class ValveKeyValue.KVDocument
121128

122129
public sealed class ValveKeyValue.KVIgnoreAttribute
123130
{
131+
public .ctor();
124132
public bool Equals(object obj);
125133
protected void Finalize();
126134
public object get_TypeId();
@@ -134,6 +142,8 @@ public sealed class ValveKeyValue.KVIgnoreAttribute
134142

135143
public class ValveKeyValue.KVObject
136144
{
145+
public .ctor(string name, System.Collections.Generic.IEnumerable`1[[ValveKeyValue.KVObject]] items);
146+
public .ctor(string name, ValveKeyValue.KVValue value);
137147
public void Add(ValveKeyValue.KVObject value);
138148
public bool Equals(object obj);
139149
protected void Finalize();
@@ -151,6 +161,7 @@ public class ValveKeyValue.KVObject
151161

152162
public sealed class ValveKeyValue.KVPropertyAttribute
153163
{
164+
public .ctor(string propertyName);
154165
public bool Equals(object obj);
155166
protected void Finalize();
156167
public string get_PropertyName();
@@ -200,24 +211,28 @@ public class ValveKeyValue.KVSerializer
200211

201212
public sealed class ValveKeyValue.KVSerializerOptions
202213
{
214+
public .ctor();
203215
public bool Equals(object obj);
204216
protected void Finalize();
205217
public System.Collections.Generic.IList`1[[string]] get_Conditions();
206218
public static ValveKeyValue.KVSerializerOptions get_DefaultOptions();
207219
public bool get_EnableValveNullByteBugBehavior();
208220
public ValveKeyValue.IIncludedFileLoader get_FileLoader();
209221
public bool get_HasEscapeSequences();
222+
public ValveKeyValue.StringTable get_StringTable();
210223
public int GetHashCode();
211224
public Type GetType();
212225
protected object MemberwiseClone();
213226
public void set_EnableValveNullByteBugBehavior(bool value);
214227
public void set_FileLoader(ValveKeyValue.IIncludedFileLoader value);
215228
public void set_HasEscapeSequences(bool value);
229+
public void set_StringTable(ValveKeyValue.StringTable value);
216230
public string ToString();
217231
}
218232

219233
public class ValveKeyValue.KVValue
220234
{
235+
protected .ctor();
221236
public bool Equals(object obj);
222237
protected void Finalize();
223238
public ValveKeyValue.KVValue get_Item(string key);
@@ -295,3 +310,20 @@ public sealed enum ValveKeyValue.KVValueType
295310
public string ToString(string format, IFormatProvider provider);
296311
}
297312

313+
public sealed class ValveKeyValue.StringTable
314+
{
315+
public .ctor();
316+
public .ctor(int capacity);
317+
public .ctor(System.Collections.Generic.IList`1[[string]] values);
318+
public void Add(string value);
319+
public bool Equals(object obj);
320+
protected void Finalize();
321+
public string get_Item(int index);
322+
public int GetHashCode();
323+
public int GetOrAdd(string value);
324+
public Type GetType();
325+
protected object MemberwiseClone();
326+
public string[] ToArray();
327+
public string ToString();
328+
}
329+

ValveKeyValue/ValveKeyValue/Deserialization/KeyValues1/KV1BinaryReader.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class KV1BinaryReader : IVisitingReader
88
{
99
public const int BinaryMagicHeader = 0x564B4256; // VBKV
1010

11-
public KV1BinaryReader(Stream stream, IVisitationListener listener)
11+
public KV1BinaryReader(Stream stream, IVisitationListener listener, StringTable stringTable)
1212
{
1313
Require.NotNull(stream, nameof(stream));
1414
Require.NotNull(listener, nameof(listener));
@@ -20,12 +20,14 @@ public KV1BinaryReader(Stream stream, IVisitationListener listener)
2020

2121
this.stream = stream;
2222
this.listener = listener;
23+
this.stringTable = stringTable;
2324
reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true);
2425
}
2526

2627
readonly Stream stream;
2728
readonly BinaryReader reader;
2829
readonly IVisitationListener listener;
30+
readonly StringTable stringTable;
2931
bool disposed;
3032
KV1BinaryNodeType endMarker = KV1BinaryNodeType.End;
3133

@@ -74,9 +76,20 @@ void ReadObjectCore()
7476
}
7577
}
7678

79+
string ReadKeyForNextValue()
80+
{
81+
if (stringTable is not null)
82+
{
83+
var index = reader.ReadInt32();
84+
return stringTable[index];
85+
}
86+
87+
return Encoding.UTF8.GetString(ReadNullTerminatedBytes());
88+
}
89+
7790
void ReadValue(KV1BinaryNodeType type)
7891
{
79-
var name = Encoding.UTF8.GetString(ReadNullTerminatedBytes());
92+
var name = ReadKeyForNextValue();
8093
KVValue value;
8194

8295
switch (type)

ValveKeyValue/ValveKeyValue/KVSerializer.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ IVisitingReader MakeReader(Stream stream, IParsingVisitationListener listener, K
115115
return format switch
116116
{
117117
KVSerializationFormat.KeyValues1Text => new KV1TextReader(new StreamReader(stream), listener, options),
118-
KVSerializationFormat.KeyValues1Binary => new KV1BinaryReader(stream, listener),
118+
KVSerializationFormat.KeyValues1Binary => new KV1BinaryReader(stream, listener, options.StringTable),
119119
_ => throw new ArgumentOutOfRangeException(nameof(format), format, "Invalid serialization format."),
120120
};
121121
}
@@ -128,9 +128,10 @@ IVisitationListener MakeSerializer(Stream stream, KVSerializerOptions options)
128128
return format switch
129129
{
130130
KVSerializationFormat.KeyValues1Text => new KV1TextSerializer(stream, options),
131-
KVSerializationFormat.KeyValues1Binary => new KV1BinarySerializer(stream),
131+
KVSerializationFormat.KeyValues1Binary => new KV1BinarySerializer(stream, options.StringTable),
132132
_ => throw new ArgumentOutOfRangeException(nameof(format), format, "Invalid serialization format."),
133133
};
134+
;
134135
}
135136
}
136137
}

ValveKeyValue/ValveKeyValue/KVSerializerOptions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ public sealed class KVSerializerOptions
2727
/// </summary>
2828
public IIncludedFileLoader FileLoader { get; set; }
2929

30+
31+
/// <summary>
32+
/// Gets or sets the string table used for smaller binary serialization.
33+
/// </summary>
34+
public StringTable StringTable { get; set; }
35+
3036
/// <summary>
3137
/// Gets the default options (used when none are specified).
3238
/// </summary>

0 commit comments

Comments
 (0)