diff --git a/go/fory/buffer_test.go b/go/fory/buffer_test.go index c00ac1dde4..a65d49a7a9 100644 --- a/go/fory/buffer_test.go +++ b/go/fory/buffer_test.go @@ -81,3 +81,33 @@ func checkVarintWrite(t *testing.T, buf *ByteBuffer, value int32) { require.Equal(t, buf.ReaderIndex(), buf.WriterIndex()) require.Equal(t, value, varInt) } + +// TestUnsafePutVarUint32PhysicalWriteWidth verifies that UnsafePutVarUint32 performs +// an 8-byte physical write for 5-byte varints and that Reserve(8) (as required by +// the contract) keeps those 8 bytes within the backing array. +func TestUnsafePutVarUint32PhysicalWriteWidth(t *testing.T) { + const sentinelByte = byte(0xAB) + const totalCap = 16 + backing := make([]byte, totalCap, totalCap) + + // Fill [8, totalCap) with sentinels; [0, 8) is the reserved window. + for i := 8; i < totalCap; i++ { + backing[i] = sentinelByte + } + + // Expose 8 bytes of len, matching Reserve(8) contract. + buf := NewByteBuffer(backing[:8]) + + // Reserve(8) should return immediately as len(data) is already 8. + buf.Reserve(8) + + // Encode value >= 2^28 (5 varint bytes) which triggers 8-byte bulk write. + written := buf.UnsafePutVarUint32(0, 1<<28) + require.Equal(t, 5, written, "expected 5 logical bytes written") + + // Verify bytes [8, totalCap) remain untouched by the 8-byte bulk write. + for i := 8; i < totalCap; i++ { + require.Equal(t, sentinelByte, backing[i], + "byte at index %d is outside the 8-byte reserved window and must not be written", i) + } +} diff --git a/go/fory/struct.go b/go/fory/struct.go index 52e53ad997..6f77aeedb3 100644 --- a/go/fory/struct.go +++ b/go/fory/struct.go @@ -339,7 +339,8 @@ func (s *structSerializer) WriteData(ctx *WriteContext, value reflect.Value) { // - Reserve max size once, track offset locally, update writerIndex once at end // ========================================================================== if s.fieldGroup.MaxVarintSize > 0 { - buf.Reserve(s.fieldGroup.MaxVarintSize) + // +8 padding for UnsafePutVarUint32 bulk write (8 bytes physically written for 5-byte varints) + buf.Reserve(s.fieldGroup.MaxVarintSize + 8) offset := buf.WriterIndex() for _, field := range s.fieldGroup.PrimitiveVarintFields { @@ -1530,7 +1531,8 @@ func (s *structSerializer) ReadData(ctx *ReadContext, value reflect.Value) { // Note: For tagged int64/uint64, we can't use unsafe reads because they need bounds checking if len(s.fieldGroup.PrimitiveVarintFields) > 0 { err := ctx.Err() - if buf.remaining() >= s.fieldGroup.MaxVarintSize { + // +8 padding for readVarUint32Fast bulk load (8 bytes physically read regardless of varint length) + if buf.remaining() >= s.fieldGroup.MaxVarintSize+8 { for _, field := range s.fieldGroup.PrimitiveVarintFields { fieldPtr := unsafe.Add(ptr, field.Offset) optInfo := optionalInfo{} diff --git a/go/fory/struct_test.go b/go/fory/struct_test.go index d4d42c130a..c2d2041b7b 100644 --- a/go/fory/struct_test.go +++ b/go/fory/struct_test.go @@ -617,3 +617,29 @@ func TestFloat16StructField(t *testing.T) { // Specific value check require.Equal(t, float32(1.5), res.F16.Float32()) } + +// TestVarintFastPathTightBuffer exercises the varint fast-path with a single uint32 field. +// Serializing a value requiring 5 varint bytes exercises the 8-byte bulk write. +// Deserializing from a tight buffer (len==cap) exercises the read guard, ensuring +// the 8-byte bulk load does not read past the end of the backing array. +func TestVarintFastPathTightBuffer(t *testing.T) { + type SingleVarintStruct struct { + // compress=true forces the varint fast path. + Value uint32 `fory:"compress=true"` + } + + f := New(WithXlang(false)) + require.NoError(t, f.RegisterStruct(SingleVarintStruct{}, 7001)) + + // 1<<28 requires 5 varint bytes, forcing the 8-byte bulk write path. + obj := SingleVarintStruct{Value: 1 << 28} + + data, err := f.Serialize(&obj) + require.NoError(t, err) + + // Deserialize from a tight buffer where len == cap. This ensures the read guard + // properly handles bulk loads that would otherwise overrun the slice. + var out SingleVarintStruct + require.NoError(t, f.Deserialize(data, &out)) + require.Equal(t, obj.Value, out.Value) +}