From 3f508ac6c98bc3a71966842fe7abda1ca08fab04 Mon Sep 17 00:00:00 2001 From: Lukas Kabrt Date: Sun, 11 Jan 2026 11:18:48 +0100 Subject: [PATCH 1/5] Add methods for easier serialization of nested classes in the LengthPrefixedBlock. --- src/PbfLite.Tests/PbfBlockWriterTests.cs | 24 +++++++++ src/PbfLite/PbfBlockWriter.Collections.cs | 22 ++------- src/PbfLite/PbfBlockWriter.cs | 59 +++++++++++++++++++++++ src/PbfLite/PbfLite.csproj | 2 +- 4 files changed, 87 insertions(+), 20 deletions(-) diff --git a/src/PbfLite.Tests/PbfBlockWriterTests.cs b/src/PbfLite.Tests/PbfBlockWriterTests.cs index cd5f72b..b64358b 100644 --- a/src/PbfLite.Tests/PbfBlockWriterTests.cs +++ b/src/PbfLite.Tests/PbfBlockWriterTests.cs @@ -1,3 +1,4 @@ +using System.Linq; using Xunit; namespace PbfLite.Tests; @@ -62,4 +63,27 @@ public void GetVarIntByteCount_BorderValues(uint value, int expectedBytesCount) Assert.Equal(expectedBytesCount, bytesCount); } + + [Theory] + [InlineData(1)] + [InlineData(128)] + [InlineData(32768)] + public void WritesLengthPrefixedBlock(int estimatedBlockLength) + { + var data = Enumerable.Repeat(0x01, 128).ToArray(); + + var buffer = new byte[256]; + var writer = PbfBlockWriter.Create(buffer); + + var block = writer.StartLengthPrefixedBlock(estimatedBlockLength); + writer.WriteRaw(data); + writer.FinalizeLengthPrefixedBlock(block); + + var expectedData = new byte[2 + data.Length]; + expectedData[0] = 0x80; + expectedData[1] = 0x01; + data.CopyTo(expectedData, 2); + + SpanAssert.Equal(expectedData, writer.Block.Slice(0, expectedData.Length)); + } } \ No newline at end of file diff --git a/src/PbfLite/PbfBlockWriter.Collections.cs b/src/PbfLite/PbfBlockWriter.Collections.cs index b794bc8..6bb0bf8 100644 --- a/src/PbfLite/PbfBlockWriter.Collections.cs +++ b/src/PbfLite/PbfBlockWriter.Collections.cs @@ -5,33 +5,17 @@ namespace PbfLite; public ref partial struct PbfBlockWriter { private delegate void ItemWriterDelegate(ref PbfBlockWriter writer, T item); - + private void WriteScalarCollection(ReadOnlySpan items, ItemWriterDelegate itemWriter) { - var lengthPosition = _position; - - // Placeholder for length that will be overwritten - WriteVarInt32(0); + var block = StartLengthPrefixedBlock(items.Length); foreach (var item in items) { itemWriter(ref this, item); } - var contentLength = _position - lengthPosition - 1; - var contentLengthBytesCount = GetVarIntBytesCount((uint)contentLength); - - if (contentLengthBytesCount > 1) - { - var content = _block.Slice(lengthPosition + 1, contentLength); - var newContentStart = lengthPosition + contentLengthBytesCount; - - content.CopyTo(_block.Slice(newContentStart)); - - _position = newContentStart + contentLength; - } - - WriteVarInt32At(lengthPosition, (uint)contentLength); + FinalizeLengthPrefixedBlock(block); } private void WriteScalarCollection(ReadOnlySpan items, ItemWriterDelegate itemWriter, int contentLengthBytes) diff --git a/src/PbfLite/PbfBlockWriter.cs b/src/PbfLite/PbfBlockWriter.cs index 41e6021..001a21f 100644 --- a/src/PbfLite/PbfBlockWriter.cs +++ b/src/PbfLite/PbfBlockWriter.cs @@ -139,4 +139,63 @@ public void WriteLengthPrefixedBytes(ReadOnlySpan data) data.CopyTo(_block[_position..]); _position += data.Length; } + + /// + /// Writes the specified sequence of bytes directly to the underlying buffer at the current position. + /// + /// The span of bytes to write to the buffer. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteRaw(ReadOnlySpan data) + { + data.CopyTo(_block[_position..]); + _position += data.Length; + } + + /// + /// Starts a length-prefixed block with an estimated block length. + /// + /// The estimated length of the block content. + /// A LengthPrefixedBlock struct representing the block. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public LengthPrefixedBlock StartLengthPrefixedBlock(int estimatedBlockLength) + { + var lengthPosition = _position; + var contentPosition = lengthPosition + GetVarIntBytesCount((uint)estimatedBlockLength); + _position = contentPosition; + + return new LengthPrefixedBlock + { + LengthPosition = lengthPosition, + ContentPosition = contentPosition + }; + } + + /// + /// Finalizes a length-prefixed block by calculating its actual length and writing it at the appropriate position. + /// + /// The LengthPrefixedBlock to finalize. + public void FinalizeLengthPrefixedBlock(LengthPrefixedBlock block) + { + var estimatedLengthBytesCount = block.ContentPosition - block.LengthPosition; + + var contentLength = _position - block.ContentPosition; + var contentLengthBytesCount = GetVarIntBytesCount((uint)contentLength); + + if (contentLengthBytesCount != estimatedLengthBytesCount) + { + var content = _block.Slice(block.ContentPosition, contentLength); + var newContentStart = block.LengthPosition + contentLengthBytesCount; + + content.CopyTo(_block.Slice(newContentStart)); + + _position = newContentStart + contentLength; + } + + WriteVarInt32At(block.LengthPosition, (uint)contentLength); + } +} + +public record struct LengthPrefixedBlock { + public int LengthPosition; + public int ContentPosition; } \ No newline at end of file diff --git a/src/PbfLite/PbfLite.csproj b/src/PbfLite/PbfLite.csproj index b58657d..d709a5d 100644 --- a/src/PbfLite/PbfLite.csproj +++ b/src/PbfLite/PbfLite.csproj @@ -5,7 +5,7 @@ PbfLite - 0.3.0 + 0.4.0 Lukas Kabrt PbfLITE is a low-level .NET Protocol Buffers implementation. It is intended for scenarios where you need fine-grained control over serialization and deserialization without the overhead of reflection, but it requires developers to manually implement the serialization / deserialization logic. MIT From ec86163c975c6b8db240fefc6ad4062e71355463 Mon Sep 17 00:00:00 2001 From: Lukas Kabrt Date: Sun, 11 Jan 2026 11:37:04 +0100 Subject: [PATCH 2/5] Update src/PbfLite/PbfBlockWriter.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/PbfLite/PbfBlockWriter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PbfLite/PbfBlockWriter.cs b/src/PbfLite/PbfBlockWriter.cs index 001a21f..78628ac 100644 --- a/src/PbfLite/PbfBlockWriter.cs +++ b/src/PbfLite/PbfBlockWriter.cs @@ -195,7 +195,7 @@ public void FinalizeLengthPrefixedBlock(LengthPrefixedBlock block) } } -public record struct LengthPrefixedBlock { +public record struct LengthPrefixedBlock { public int LengthPosition; public int ContentPosition; } \ No newline at end of file From b7185505c4f544e3a1a072370e05cd16f21add96 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 11 Jan 2026 11:52:14 +0100 Subject: [PATCH 3/5] Make LengthPrefixedBlock fields readonly (#18) * Initial plan * Make LengthPrefixedBlock fields readonly using primary constructor Co-authored-by: lukaskabrt <2894161+lukaskabrt@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lukaskabrt <2894161+lukaskabrt@users.noreply.github.com> --- src/PbfLite/PbfBlockWriter.cs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/PbfLite/PbfBlockWriter.cs b/src/PbfLite/PbfBlockWriter.cs index 78628ac..4bc70ef 100644 --- a/src/PbfLite/PbfBlockWriter.cs +++ b/src/PbfLite/PbfBlockWriter.cs @@ -163,11 +163,7 @@ public LengthPrefixedBlock StartLengthPrefixedBlock(int estimatedBlockLength) var contentPosition = lengthPosition + GetVarIntBytesCount((uint)estimatedBlockLength); _position = contentPosition; - return new LengthPrefixedBlock - { - LengthPosition = lengthPosition, - ContentPosition = contentPosition - }; + return new LengthPrefixedBlock(lengthPosition, contentPosition); } /// @@ -195,7 +191,4 @@ public void FinalizeLengthPrefixedBlock(LengthPrefixedBlock block) } } -public record struct LengthPrefixedBlock { - public int LengthPosition; - public int ContentPosition; -} \ No newline at end of file +public record struct LengthPrefixedBlock(int LengthPosition, int ContentPosition); \ No newline at end of file From 5fa0be07af184756fde0b62e5e0c0e5bee235db4 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 11 Jan 2026 11:52:57 +0100 Subject: [PATCH 4/5] Add dedicated test coverage for WriteRaw method (#17) * Initial plan * Add dedicated tests for WriteRaw method Co-authored-by: lukaskabrt <2894161+lukaskabrt@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lukaskabrt <2894161+lukaskabrt@users.noreply.github.com> --- .../PbfBlockWriterTests.Primitives.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/PbfLite.Tests/PbfBlockWriterTests.Primitives.cs b/src/PbfLite.Tests/PbfBlockWriterTests.Primitives.cs index 65dfed5..498edc1 100644 --- a/src/PbfLite.Tests/PbfBlockWriterTests.Primitives.cs +++ b/src/PbfLite.Tests/PbfBlockWriterTests.Primitives.cs @@ -97,5 +97,35 @@ public void WriteLengthPrefixedBytes_WritesPrefixAndData() SpanAssert.Equal(expected, writer.Block); } + + [Fact] + public void WriteRaw_WritesDataAndAdvancesPosition() + { + var data = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 }; + var buffer = new byte[10]; + var writer = PbfBlockWriter.Create(buffer); + + writer.WriteRaw(data); + + SpanAssert.Equal(data, writer.Block); + Assert.Equal(5, writer.Position); + } + + [Fact] + public void WriteRaw_WritesMultipleSequences() + { + var data1 = new byte[] { 0xAA, 0xBB }; + var data2 = new byte[] { 0xCC, 0xDD, 0xEE }; + var expected = new byte[] { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE }; + + var buffer = new byte[10]; + var writer = PbfBlockWriter.Create(buffer); + + writer.WriteRaw(data1); + writer.WriteRaw(data2); + + SpanAssert.Equal(expected, writer.Block); + Assert.Equal(5, writer.Position); + } } } \ No newline at end of file From 958f188e41d5494562204517b82d4396f54c360c Mon Sep 17 00:00:00 2001 From: Lukas Kabrt Date: Sun, 11 Jan 2026 15:56:09 +0100 Subject: [PATCH 5/5] PR feedback --- .../PbfBlockWriterTests.Primitives.cs | 35 +++++++++++-------- src/PbfLite.Tests/PbfBlockWriterTests.cs | 23 ------------ 2 files changed, 21 insertions(+), 37 deletions(-) diff --git a/src/PbfLite.Tests/PbfBlockWriterTests.Primitives.cs b/src/PbfLite.Tests/PbfBlockWriterTests.Primitives.cs index 498edc1..7f904a3 100644 --- a/src/PbfLite.Tests/PbfBlockWriterTests.Primitives.cs +++ b/src/PbfLite.Tests/PbfBlockWriterTests.Primitives.cs @@ -1,3 +1,4 @@ +using System.Linq; using Xunit; namespace PbfLite.Tests; @@ -98,33 +99,39 @@ public void WriteLengthPrefixedBytes_WritesPrefixAndData() SpanAssert.Equal(expected, writer.Block); } - [Fact] - public void WriteRaw_WritesDataAndAdvancesPosition() + [Theory] + [InlineData(1)] + [InlineData(128)] + [InlineData(32768)] + public void StartAndFinalizeLengthPrefixedBlock_WritesLengthPrefixedBlock(int estimatedBlockLength) { - var data = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 }; - var buffer = new byte[10]; + var data = Enumerable.Repeat(0x01, 128).ToArray(); + + var buffer = new byte[256]; var writer = PbfBlockWriter.Create(buffer); + var block = writer.StartLengthPrefixedBlock(estimatedBlockLength); writer.WriteRaw(data); + writer.FinalizeLengthPrefixedBlock(block); - SpanAssert.Equal(data, writer.Block); - Assert.Equal(5, writer.Position); + var expectedData = new byte[2 + data.Length]; + expectedData[0] = 0x80; + expectedData[1] = 0x01; + data.CopyTo(expectedData, 2); + + SpanAssert.Equal(expectedData, writer.Block.Slice(0, expectedData.Length)); } [Fact] - public void WriteRaw_WritesMultipleSequences() + public void WriteRaw_WritesDataAndAdvancesPosition() { - var data1 = new byte[] { 0xAA, 0xBB }; - var data2 = new byte[] { 0xCC, 0xDD, 0xEE }; - var expected = new byte[] { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE }; - + var data = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 }; var buffer = new byte[10]; var writer = PbfBlockWriter.Create(buffer); - writer.WriteRaw(data1); - writer.WriteRaw(data2); + writer.WriteRaw(data); - SpanAssert.Equal(expected, writer.Block); + SpanAssert.Equal(data, writer.Block); Assert.Equal(5, writer.Position); } } diff --git a/src/PbfLite.Tests/PbfBlockWriterTests.cs b/src/PbfLite.Tests/PbfBlockWriterTests.cs index b64358b..21134a9 100644 --- a/src/PbfLite.Tests/PbfBlockWriterTests.cs +++ b/src/PbfLite.Tests/PbfBlockWriterTests.cs @@ -63,27 +63,4 @@ public void GetVarIntByteCount_BorderValues(uint value, int expectedBytesCount) Assert.Equal(expectedBytesCount, bytesCount); } - - [Theory] - [InlineData(1)] - [InlineData(128)] - [InlineData(32768)] - public void WritesLengthPrefixedBlock(int estimatedBlockLength) - { - var data = Enumerable.Repeat(0x01, 128).ToArray(); - - var buffer = new byte[256]; - var writer = PbfBlockWriter.Create(buffer); - - var block = writer.StartLengthPrefixedBlock(estimatedBlockLength); - writer.WriteRaw(data); - writer.FinalizeLengthPrefixedBlock(block); - - var expectedData = new byte[2 + data.Length]; - expectedData[0] = 0x80; - expectedData[1] = 0x01; - data.CopyTo(expectedData, 2); - - SpanAssert.Equal(expectedData, writer.Block.Slice(0, expectedData.Length)); - } } \ No newline at end of file