diff --git a/ValveKeyValue/ValveKeyValue.Test/Test Data/apisurface.txt b/ValveKeyValue/ValveKeyValue.Test/Test Data/apisurface.txt index b05adec7..a5367b4f 100644 --- a/ValveKeyValue/ValveKeyValue.Test/Test Data/apisurface.txt +++ b/ValveKeyValue/ValveKeyValue.Test/Test Data/apisurface.txt @@ -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(); } @@ -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(); +} + diff --git a/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues1/KV1BinaryReader.cs b/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues1/KV1BinaryReader.cs index 7c4be940..d987ca9d 100644 --- a/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues1/KV1BinaryReader.cs +++ b/ValveKeyValue/ValveKeyValue/Deserialization/KeyValues1/KV1BinaryReader.cs @@ -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)); @@ -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; @@ -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) diff --git a/ValveKeyValue/ValveKeyValue/KVSerializer.cs b/ValveKeyValue/ValveKeyValue/KVSerializer.cs index 965765d8..3a1998b7 100644 --- a/ValveKeyValue/ValveKeyValue/KVSerializer.cs +++ b/ValveKeyValue/ValveKeyValue/KVSerializer.cs @@ -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."), }; } @@ -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."); }; } } diff --git a/ValveKeyValue/ValveKeyValue/KVSerializerOptions.cs b/ValveKeyValue/ValveKeyValue/KVSerializerOptions.cs index d3b3c8d6..f58367e5 100644 --- a/ValveKeyValue/ValveKeyValue/KVSerializerOptions.cs +++ b/ValveKeyValue/ValveKeyValue/KVSerializerOptions.cs @@ -27,6 +27,12 @@ public sealed class KVSerializerOptions /// public IIncludedFileLoader FileLoader { get; set; } + + /// + /// Gets or sets the string table used for smaller binary serialization. + /// + public StringTable StringTable { get; set; } + /// /// Gets the default options (used when none are specified). /// diff --git a/ValveKeyValue/ValveKeyValue/Serialization/KeyValues1/KV1BinarySerializer.cs b/ValveKeyValue/ValveKeyValue/Serialization/KeyValues1/KV1BinarySerializer.cs index 6c2dc155..0b2b6307 100644 --- a/ValveKeyValue/ValveKeyValue/Serialization/KeyValues1/KV1BinarySerializer.cs +++ b/ValveKeyValue/ValveKeyValue/Serialization/KeyValues1/KV1BinarySerializer.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Text; using ValveKeyValue.Abstraction; using ValveKeyValue.KeyValues1; @@ -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() @@ -25,7 +28,7 @@ public void OnObjectStart(string name) { objectDepth++; Write(KV1BinaryNodeType.ChildObject); - WriteNullTerminatedBytes(Encoding.UTF8.GetBytes(name)); + WriteKeyForNextValue(name); } public void OnObjectEnd() @@ -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) { @@ -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 diff --git a/ValveKeyValue/ValveKeyValue/StringTable.cs b/ValveKeyValue/ValveKeyValue/StringTable.cs new file mode 100644 index 00000000..0c030056 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue/StringTable.cs @@ -0,0 +1,58 @@ +namespace ValveKeyValue +{ + public sealed class StringTable + { + public StringTable(Memory values) + { + this.lookup = values; + } + + readonly Memory lookup; + Dictionary 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) + { + 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; + } + + reverse = new Dictionary(capacity: lookup.Length, StringComparer.Ordinal); + var span = lookup.Span; + + for (var i = 0; i < span.Length; i++) + { + var value = span[i]; + reverse[value] = i; + } + } + } +}