From 9df7176f9a1f949b7451a3f1ce46c8348c9213a3 Mon Sep 17 00:00:00 2001 From: Zero Ash Date: Thu, 20 Aug 2026 12:18:54 +0800 Subject: [PATCH] [Proto] fix: negative signed integers corrupt varint encoding (#917) --- Lagrange.Proto.Test/NegativeVarIntTest.cs | 136 ++++++++++++++++++++++ Lagrange.Proto/Primitives/ProtoWriter.cs | 2 +- Lagrange.Proto/Utility/ProtoHelper.cs | 6 +- 3 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 Lagrange.Proto.Test/NegativeVarIntTest.cs diff --git a/Lagrange.Proto.Test/NegativeVarIntTest.cs b/Lagrange.Proto.Test/NegativeVarIntTest.cs new file mode 100644 index 000000000..056e708c0 --- /dev/null +++ b/Lagrange.Proto.Test/NegativeVarIntTest.cs @@ -0,0 +1,136 @@ +using Lagrange.Proto.Serialization; + +namespace Lagrange.Proto.Test; + +/// +/// Regression tests for https://github.com/LagrangeDev/Lagrange.Core/issues/917 +/// Negative values of signed integer types with default (unsigned varint) handling +/// corrupt the serialized payload. +/// +[TestFixture] +public class NegativeVarIntTest +{ + [Test] + public void TestNegativeInt_Roundtrip_Reflection() + { + var obj = new NegativeVarIntMessage + { + IntField = default, + StringField1 = null, + StringField2 = null, + NegativeIntField = -1 + }; + + byte[] bytes = ProtoSerializer.Serialize(obj); + var deserialized = ProtoSerializer.Deserialize(bytes); + + Assert.Multiple(() => + { + Assert.That(deserialized.IntField, Is.EqualTo(obj.IntField)); + Assert.That(deserialized.StringField1, Is.EqualTo(obj.StringField1)); + Assert.That(deserialized.StringField2, Is.EqualTo(obj.StringField2)); + Assert.That(deserialized.NegativeIntField, Is.EqualTo(obj.NegativeIntField)); + }); + } + + [Test] + public void TestNegativeInt_Roundtrip_SourceGenerated() + { + var obj = new NegativeVarIntMessage + { + IntField = default, + StringField1 = null, + StringField2 = null, + NegativeIntField = -1 + }; + + byte[] bytes = ProtoSerializer.SerializeProtoPackable(obj); + var deserialized = ProtoSerializer.DeserializeProtoPackable(bytes); + + Assert.Multiple(() => + { + Assert.That(deserialized.IntField, Is.EqualTo(obj.IntField)); + Assert.That(deserialized.StringField1, Is.EqualTo(obj.StringField1)); + Assert.That(deserialized.StringField2, Is.EqualTo(obj.StringField2)); + Assert.That(deserialized.NegativeIntField, Is.EqualTo(obj.NegativeIntField)); + }); + } + + [Test] + public void TestNegativeInt_NestedObject_Roundtrip() + { + var obj = new NegativeVarIntNested + { + Inner = new NegativeVarIntMessage { IntField = 1, StringField1 = "t", StringField2 = "d", NegativeIntField = -1 } + }; + + byte[] bytes = ProtoSerializer.SerializeProtoPackable(obj); + var deserialized = ProtoSerializer.DeserializeProtoPackable(bytes); + + Assert.Multiple(() => + { + Assert.That(deserialized.Inner, Is.Not.Null); + Assert.That(deserialized.Inner!.IntField, Is.EqualTo(obj.Inner.IntField)); + Assert.That(deserialized.Inner.NegativeIntField, Is.EqualTo(obj.Inner.NegativeIntField)); + }); + } + + [Test] + public void TestNegativeValues_Boundary() + { + var obj = new NegativeVarIntBoundary + { + SByteValue = -1, + ShortValue = -1, + IntValue = -1, + LongValue = -1, + SByteMin = sbyte.MinValue, + ShortMin = short.MinValue, + IntMin = int.MinValue, + LongMin = long.MinValue + }; + + byte[] bytes = ProtoSerializer.SerializeProtoPackable(obj); + var deserialized = ProtoSerializer.DeserializeProtoPackable(bytes); + + Assert.Multiple(() => + { + Assert.That(deserialized.SByteValue, Is.EqualTo(obj.SByteValue)); + Assert.That(deserialized.ShortValue, Is.EqualTo(obj.ShortValue)); + Assert.That(deserialized.IntValue, Is.EqualTo(obj.IntValue)); + Assert.That(deserialized.LongValue, Is.EqualTo(obj.LongValue)); + Assert.That(deserialized.SByteMin, Is.EqualTo(obj.SByteMin)); + Assert.That(deserialized.ShortMin, Is.EqualTo(obj.ShortMin)); + Assert.That(deserialized.IntMin, Is.EqualTo(obj.IntMin)); + Assert.That(deserialized.LongMin, Is.EqualTo(obj.LongMin)); + }); + } +} + +[ProtoPackable] +public partial class NegativeVarIntMessage +{ + [ProtoMember(1)] public int IntField { get; set; } + [ProtoMember(2)] public string? StringField1 { get; set; } + [ProtoMember(3)] public string? StringField2 { get; set; } + [ProtoMember(4)] public int NegativeIntField { get; set; } +} + +[ProtoPackable] +public partial class NegativeVarIntNested +{ + [ProtoMember(1)] public NegativeVarIntMessage? Inner { get; set; } +} + +[ProtoPackable] +public partial class NegativeVarIntBoundary +{ + [ProtoMember(1)] public sbyte SByteValue { get; set; } + [ProtoMember(2)] public short ShortValue { get; set; } + [ProtoMember(3)] public int IntValue { get; set; } + [ProtoMember(4)] public long LongValue { get; set; } + [ProtoMember(5)] public sbyte SByteMin { get; set; } + [ProtoMember(6)] public short ShortMin { get; set; } + [ProtoMember(7)] public int IntMin { get; set; } + [ProtoMember(8)] public long LongMin { get; set; } +} diff --git a/Lagrange.Proto/Primitives/ProtoWriter.cs b/Lagrange.Proto/Primitives/ProtoWriter.cs index de938fdee..27eef16ae 100644 --- a/Lagrange.Proto/Primitives/ProtoWriter.cs +++ b/Lagrange.Proto/Primitives/ProtoWriter.cs @@ -97,7 +97,7 @@ public void EncodeVarInt(T value) where T : unmanaged, INumber { if (_memory.Length - BytesPending >= 10) { - if (value < T.CreateTruncating(0x80)) + if (ulong.CreateTruncating(value) < 0x80) // signed negative values must not take the single-byte path { Unsafe.Add(ref MemoryMarshal.GetReference(_memory.Span), BytesPending++) = byte.CreateTruncating(value); return; diff --git a/Lagrange.Proto/Utility/ProtoHelper.cs b/Lagrange.Proto/Utility/ProtoHelper.cs index 9ca22e99d..e54405bf3 100644 --- a/Lagrange.Proto/Utility/ProtoHelper.cs +++ b/Lagrange.Proto/Utility/ProtoHelper.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using System.Runtime.CompilerServices; using System.Text; using Lagrange.Proto.Primitives; @@ -40,12 +40,12 @@ public static unsafe int GetVarIntLength(T value) where T : unmanaged, INumbe if (sizeof(T) <= 4) { - int leadingZeros = BitOperations.LeadingZeroCount(uint.CreateSaturating(value)); + int leadingZeros = BitOperations.LeadingZeroCount(uint.CreateTruncating(value)); return VarIntLengths32[leadingZeros]; } else { - int leadingZeros = BitOperations.LeadingZeroCount(ulong.CreateSaturating(value)); + int leadingZeros = BitOperations.LeadingZeroCount(ulong.CreateTruncating(value)); return VarIntLengths64[leadingZeros]; } }