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];
}
}