Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement support for KV1 binary serialization with string tables #100

Merged
merged 8 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
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
];
}
}
15 changes: 15 additions & 0 deletions ValveKeyValue/ValveKeyValue.Test/Test Data/apisurface.txt
Original file line number Diff line number Diff line change
Expand Up @@ -207,12 +207,14 @@ 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();
}

Expand Down Expand Up @@ -295,3 +297,16 @@ public sealed enum ValveKeyValue.KVValueType
public string ToString(string format, IFormatProvider provider);
}

public sealed class ValveKeyValue.StringTable
{
public bool Equals(object obj);
protected void Finalize();
public string get_Item(int index);
public int GetHashCode();
public Type GetType();
public int IndexOf(string value);
protected object MemberwiseClone();
public void PrepareForSerialization();
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
16 changes: 11 additions & 5 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 @@ -125,11 +125,17 @@ IVisitationListener MakeSerializer(Stream stream, KVSerializerOptions options)
Require.NotNull(stream, nameof(stream));
Require.NotNull(options, nameof(options));

return format switch
switch (format)
{
KVSerializationFormat.KeyValues1Text => new KV1TextSerializer(stream, options),
KVSerializationFormat.KeyValues1Binary => new KV1BinarySerializer(stream),
_ => throw new ArgumentOutOfRangeException(nameof(format), format, "Invalid serialization format."),
case KVSerializationFormat.KeyValues1Text:
return new KV1TextSerializer(stream, options);

case KVSerializationFormat.KeyValues1Binary:
options.StringTable?.PrepareForSerialization();
return new KV1BinarySerializer(stream, options.StringTable);

default:
throw new ArgumentOutOfRangeException(nameof(format), format, "Invalid serialization format.");
};
}
}
Expand Down
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
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Linq;
using System.Text;
using ValveKeyValue.Abstraction;
using ValveKeyValue.KeyValues1;
Expand All @@ -6,14 +7,16 @@ namespace ValveKeyValue.Serialization.KeyValues1
{
sealed class KV1BinarySerializer : IVisitationListener, IDisposable
{
public KV1BinarySerializer(Stream stream)
public KV1BinarySerializer(Stream stream, StringTable stringTable)
{
Require.NotNull(stream, nameof(stream));

writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true);
this.stringTable = stringTable;
}

readonly BinaryWriter writer;
readonly StringTable stringTable;
int objectDepth;

public void Dispose()
Expand All @@ -25,7 +28,7 @@ public void OnObjectStart(string name)
{
objectDepth++;
Write(KV1BinaryNodeType.ChildObject);
WriteNullTerminatedBytes(Encoding.UTF8.GetBytes(name));
WriteKeyForNextValue(name);
}

public void OnObjectEnd()
Expand All @@ -42,7 +45,7 @@ public void OnObjectEnd()
public void OnKeyValuePair(string name, KVValue value)
{
Write(GetNodeType(value.ValueType));
WriteNullTerminatedBytes(Encoding.UTF8.GetBytes(name));
WriteKeyForNextValue(name);

switch (value.ValueType)
{
Expand Down Expand Up @@ -83,6 +86,18 @@ void WriteNullTerminatedBytes(byte[] value)
writer.Write((byte)0);
}

void WriteKeyForNextValue(string name)
{
if (stringTable is not null)
{
writer.Write(stringTable.IndexOf(name));
}
else
{
WriteNullTerminatedBytes(Encoding.UTF8.GetBytes(name));
}
}

static KV1BinaryNodeType GetNodeType(KVValueType type)
{
return type switch
Expand Down
58 changes: 58 additions & 0 deletions ValveKeyValue/ValveKeyValue/StringTable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
namespace ValveKeyValue
{
public sealed class StringTable
{
public StringTable(Memory<string> values)
{
this.lookup = values;
}

readonly Memory<string> lookup;
Dictionary<string, int> reverse;

public string this[int index]
{
get
{
if (index < 0)
{
throw new ArgumentOutOfRangeException(nameof(index), "Index must be non-negative.");
}

if (index >= lookup.Length)
{
throw new ArgumentOutOfRangeException(nameof(index), index, "Index must be less than the number of strings in the table.");
}

return lookup.Span[index];
}
}

public int IndexOf(string value)
yaakov-h marked this conversation as resolved.
Show resolved Hide resolved
{
if (reverse is null)
{
throw new InvalidOperationException("String table has not been prepared for serialization.");
}

return reverse[value];
}

public void PrepareForSerialization()
{
if (reverse is not null)
{
return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this gonna cause issues if you serialize, change the kv and try to serialize again?

Actually not seeing a way for stringtable to be populated by the seializer.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh that's an interesting idea, should we dynamically populate a string table?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to support serialization, we kind of have to.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also need a public accessor to the list if that happens, because you need to store it somehow

}

reverse = new Dictionary<string, int>(capacity: lookup.Length, StringComparer.Ordinal);
var span = lookup.Span;

for (var i = 0; i < span.Length; i++)
{
var value = span[i];
reverse[value] = i;
}
}
}
}