Skip to content

Commit

Permalink
Implement support for KV1 binary serialization with string tables (#100)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
yaakov-h authored Jul 3, 2024
1 parent f7d32f2 commit 36bd1d2
Show file tree
Hide file tree
Showing 11 changed files with 306 additions and 8 deletions.
1 change: 1 addition & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<AssemblyVersion>$(ProjectBaseVersion)</AssemblyVersion>
<FileVersion>$(ProjectVersion)</FileVersion>
<Version>$(ProjectVersion)</Version>
<LangVersion>latest</LangVersion>
</PropertyGroup>

<PropertyGroup>
Expand Down
31 changes: 31 additions & 0 deletions ValveKeyValue/ValveKeyValue.Test/ApiSurfaceTestCase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,37 @@ static void GenerateTypeApiSurface(StringBuilder sb, Type type)
sb.Append('\n');
}

var constructors = type
.GetConstructors(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Where(t => !t.IsPrivate && !t.IsAssembly && !t.IsFamilyAndAssembly)
.OrderBy(t => t.Name, StringComparer.InvariantCulture)
.ThenBy(t => string.Join(", ", t.GetParameters().Select(GetParameterAsString)), StringComparer.InvariantCulture);

foreach (var constructor in constructors)
{
sb.Append(" ");

if (constructor.IsPublic)
{
sb.Append("public");
}
else
{
sb.Append("protected");
}

if (constructor.IsStatic)
{
sb.Append(" static");
}

sb.Append(' ');
sb.Append(constructor.Name);
sb.Append('(');
sb.Append(string.Join(", ", constructor.GetParameters().Select(GetParameterAsString)));
sb.Append(");\n");
}

var methods = type
.GetMethods(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Where(t => !t.IsPrivate && !t.IsAssembly && !t.IsFamilyAndAssembly)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public void SetUp()
0x06, // pointer: ptr = 0x11223344
0x70, 0x74, 0x72, 0x00,
0x44, 0x33, 0x22, 0x11,
0x07, // uint64: long = 0x1122334455667788
0x07, // uint64: lng = 0x1122334455667788
0x6C, 0x6E, 0x67, 0x00,
0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11,
0x0A, // int64, i64 = 0x0102030405070809
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Linq;

namespace ValveKeyValue.Test
{
class StringTableFromScratchTestCase
{
[Test]
public void PopulatesStringTableDuringSerialization()
{
var kv = new KVObject("root",
[
new KVObject("key", "value"),
new KVObject("child", [
new KVObject("key", 123),
]),
]);

var stringTable = new StringTable();

var serializer = KVSerializer.Create(KVSerializationFormat.KeyValues1Binary);

using var ms = new MemoryStream();
serializer.Serialize(ms, kv, new KVSerializerOptions { StringTable = stringTable });

var strings = stringTable.ToArray();
Assert.That(strings, Is.EqualTo(new[] { "root", "key", "child" }));
}
}
}
87 changes: 87 additions & 0 deletions ValveKeyValue/ValveKeyValue.Test/Binary/StringTableTestCase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using System.Linq;

namespace ValveKeyValue.Test
{
class StringTableTestCase
{
[Test]
public void IsNotNull()
=> Assert.That(obj, Is.Not.Null);

[Test]
public void HasName()
=> Assert.That(obj.Name, Is.EqualTo("TestObject"));

[Test]
public void IsObjectWithChildren()
=> Assert.That(obj.Value.ValueType, Is.EqualTo(KVValueType.Collection));

[TestCase(ExpectedResult = 5)]
public int HasChildren()
=> obj.Children.Count();

[TestCase("key", "value", typeof(string))]
[TestCase("int", 0x01020304, typeof(int))]
[TestCase("flt", 1234.5678f, typeof(float))]
[TestCase("lng", 0x1122334455667788, typeof(ulong))]
[TestCase("i64", 0x0102030405060708, typeof(long))]
public void HasNamedChildWithValue(string name, object value, Type valueType)
{
Assert.That(Convert.ChangeType(obj[name], valueType), Is.EqualTo(value));
}

[Test]
public void SymmetricStringTableSerialization()
{
var serializer = KVSerializer.Create(KVSerializationFormat.KeyValues1Binary);

using var ms = new MemoryStream();
serializer.Serialize(ms, obj, new KVSerializerOptions { StringTable = new(TestStringTable) });

Assert.That(ms.ToArray(), Is.EqualTo(TestData.ToArray()));
}

KVObject obj;

[OneTimeSetUp]
public void SetUp()
{
obj = KVSerializer.Create(KVSerializationFormat.KeyValues1Binary)
.Deserialize(
TestData.ToArray(),
new KVSerializerOptions { StringTable = new(TestStringTable) });
}

static string[] TestStringTable => [
"flt",
"i64",
"int",
"key",
"lng",
"TestObject"
];

static ReadOnlySpan<byte> TestData =>
[
0x00, // object: TestObject
0x05, 0x00, 0x00, 0x00, // stringTable[5] = "TestObject",
0x01, // string: key = value
0x03, 0x00, 0x00, 0x00, // stringTable[3] = "key",
0x76, 0x61, 0x6C, 0x75, 0x65, 0x00,
0x02, // int32: int = 0x01020304
0x02, 0x00, 0x00, 0x00, // stringTable[2] = "int"
0x04, 0x03, 0x02, 0x01,
0x03, // float32: flt = 1234.5678f
0x00, 0x00, 0x00, 0x00, // stringTable[0] = "flt"
0x2B, 0x52, 0x9A, 0x44,
0x07, // uint64: lng = 0x1122334455667788
0x04, 0x00, 0x00, 0x00, // stringTable[4] = "lng"
0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11,
0x0A, // int64, i64 = 0x0102030405070809
0x01, 0x00, 0x00, 0x00, // stringTable[1] = "i64"
0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01,
0x08, // end object
0x08, // end document
];
}
}
32 changes: 32 additions & 0 deletions ValveKeyValue/ValveKeyValue.Test/Test Data/apisurface.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ 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();
Expand All @@ -31,6 +34,7 @@ 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();
Expand Down Expand Up @@ -74,6 +78,8 @@ 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();
Expand Down Expand Up @@ -104,6 +110,7 @@ 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();
Expand All @@ -121,6 +128,7 @@ public class ValveKeyValue.KVDocument

public sealed class ValveKeyValue.KVIgnoreAttribute
{
public .ctor();
public bool Equals(object obj);
protected void Finalize();
public object get_TypeId();
Expand All @@ -134,6 +142,8 @@ 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();
Expand All @@ -151,6 +161,7 @@ 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();
Expand Down Expand Up @@ -200,24 +211,28 @@ 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();
public static ValveKeyValue.KVSerializerOptions get_DefaultOptions();
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.KVValue get_Item(string key);
Expand Down Expand Up @@ -295,3 +310,20 @@ public sealed enum ValveKeyValue.KVValueType
public string ToString(string format, IFormatProvider provider);
}

public sealed class ValveKeyValue.StringTable
{
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 int GetHashCode();
public int GetOrAdd(string value);
public Type GetType();
protected object MemberwiseClone();
public string[] ToArray();
public string ToString();
}

Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class KV1BinaryReader : IVisitingReader
{
public const int BinaryMagicHeader = 0x564B4256; // VBKV

public KV1BinaryReader(Stream stream, IVisitationListener listener)
public KV1BinaryReader(Stream stream, IVisitationListener listener, StringTable stringTable)
{
Require.NotNull(stream, nameof(stream));
Require.NotNull(listener, nameof(listener));
Expand All @@ -20,12 +20,14 @@ public KV1BinaryReader(Stream stream, IVisitationListener listener)

this.stream = stream;
this.listener = listener;
this.stringTable = stringTable;
reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true);
}

readonly Stream stream;
readonly BinaryReader reader;
readonly IVisitationListener listener;
readonly StringTable stringTable;
bool disposed;
KV1BinaryNodeType endMarker = KV1BinaryNodeType.End;

Expand Down Expand Up @@ -74,9 +76,20 @@ void ReadObjectCore()
}
}

string ReadKeyForNextValue()
{
if (stringTable is not null)
{
var index = reader.ReadInt32();
return stringTable[index];
}

return Encoding.UTF8.GetString(ReadNullTerminatedBytes());
}

void ReadValue(KV1BinaryNodeType type)
{
var name = Encoding.UTF8.GetString(ReadNullTerminatedBytes());
var name = ReadKeyForNextValue();
KVValue value;

switch (type)
Expand Down
5 changes: 3 additions & 2 deletions ValveKeyValue/ValveKeyValue/KVSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ IVisitingReader MakeReader(Stream stream, IParsingVisitationListener listener, K
return format switch
{
KVSerializationFormat.KeyValues1Text => new KV1TextReader(new StreamReader(stream), listener, options),
KVSerializationFormat.KeyValues1Binary => new KV1BinaryReader(stream, listener),
KVSerializationFormat.KeyValues1Binary => new KV1BinaryReader(stream, listener, options.StringTable),
_ => throw new ArgumentOutOfRangeException(nameof(format), format, "Invalid serialization format."),
};
}
Expand All @@ -128,9 +128,10 @@ IVisitationListener MakeSerializer(Stream stream, KVSerializerOptions options)
return format switch
{
KVSerializationFormat.KeyValues1Text => new KV1TextSerializer(stream, options),
KVSerializationFormat.KeyValues1Binary => new KV1BinarySerializer(stream),
KVSerializationFormat.KeyValues1Binary => new KV1BinarySerializer(stream, options.StringTable),
_ => throw new ArgumentOutOfRangeException(nameof(format), format, "Invalid serialization format."),
};
;
}
}
}
6 changes: 6 additions & 0 deletions ValveKeyValue/ValveKeyValue/KVSerializerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ public sealed class KVSerializerOptions
/// </summary>
public IIncludedFileLoader FileLoader { get; set; }


/// <summary>
/// Gets or sets the string table used for smaller binary serialization.
/// </summary>
public StringTable StringTable { get; set; }

/// <summary>
/// Gets the default options (used when none are specified).
/// </summary>
Expand Down
Loading

0 comments on commit 36bd1d2

Please sign in to comment.