From cfd67bae20e04aa04e5003982c5303e7fecb6272 Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Sat, 14 Nov 2026 11:59:01 +0100 Subject: [PATCH] [Go] Reject OTF group entries whose blockLength is below the fixed fields. A group header with numInGroup above zero and a blockLength smaller than the fields present at the acting version now panics, so entries cannot be decoded from zero bytes. Constant fields and composite members newer than the acting version must stay excluded from the minimum. --- .gitignore | 2 +- .../src/otf/otf_group_block_length_test.go | 74 +++++++++++++++++++ .../src/main/golang/otf/otfmessagedecoder.go | 47 ++++++++++++ .../otf-group-block-length-schema.xml | 36 +++++++++ 4 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 gocode/flyweight/src/otf/otf_group_block_length_test.go create mode 100644 sbe-tool/src/test/resources/otf-group-block-length-schema.xml diff --git a/.gitignore b/.gitignore index 02c31be8a8..8fd57d676e 100644 --- a/.gitignore +++ b/.gitignore @@ -79,7 +79,7 @@ gocode/flyweight/src/*/*.go !gocode/flyweight/src/*/*_test.go gocode/flyweight/src/*/*/*.go !gocode/flyweight/src/*/*/*_test.go -gocode/flyweight/src/otf/code-generation-schema.sbeir +gocode/flyweight/src/otf/*.sbeir # csharp csharp/*/bin diff --git a/gocode/flyweight/src/otf/otf_group_block_length_test.go b/gocode/flyweight/src/otf/otf_group_block_length_test.go new file mode 100644 index 0000000000..a95fbbd4e2 --- /dev/null +++ b/gocode/flyweight/src/otf/otf_group_block_length_test.go @@ -0,0 +1,74 @@ +package otf + +import ( + "encoding/binary" + "github.com/real-logic/simple-binary-encoding/otf" + "github.com/real-logic/simple-binary-encoding/otf/test/code_generation_test" + "github.com/stretchr/testify/assert" + "testing" +) + +//go:generate java --add-opens java.base/jdk.internal.misc=ALL-UNNAMED -Dsbe.output.dir=. -Dsbe.target.language=golang -Dsbe.go.generate.generate.flyweights=true -Dsbe.generate.ir=true -Dsbe.keyword.append.token=X -jar ../../../${SBE_JAR} ../../../../sbe-tool/src/test/resources/otf-group-block-length-schema.xml + +const BlockLengthSchemaFilename = "otf-group-block-length-schema.sbeir" + +func TestRejectsGroupBlockLengthBelowFields(t *testing.T) { + assert := assert.New(t) + + ir := otf.NewIrDecoder() + assert.GreaterOrEqual(ir.DecodeFile(SchemaFilename), 0) + + var car code_generation_test.Car + buffer := make([]byte, MsgBufferCapacity) + encodeCar(&car, buffer) + + headerDecoder, _ := otf.NewOtfHeaderDecoder(ir.Header()) + templateId, _ := headerDecoder.TemplateId(buffer) + actingVersion, _ := headerDecoder.SchemaVersion(buffer) + blockLength, _ := headerDecoder.BlockLength(buffer) + headerLength := int(headerDecoder.EncodedLength()) + tokens := ir.MessageByID(int32(templateId)) + + // The fuelFigures group header follows the message block; its blockLength is the first uint16. + binary.LittleEndian.PutUint16(buffer[headerLength+int(blockLength):], 0) + + assert.PanicsWithValue( + "group blockLength=0 is less than the fields present at actingVersion=0 for group=fuelFigures", + func() { + otf.Decode(buffer[headerLength:], actingVersion, blockLength, tokens, &TestListener{}) + }) +} + +func decodeQuotes(t *testing.T, actingVersion uint64, groupBlockLength uint16, numInGroup uint16) (out string) { + ir := otf.NewIrDecoder() + if ir.DecodeFile(BlockLengthSchemaFilename) < 0 { + t.Fatalf("Error decoding %s", BlockLengthSchemaFilename) + } + + buffer := make([]byte, 4+int(numInGroup)*16) + binary.LittleEndian.PutUint16(buffer[0:], groupBlockLength) + binary.LittleEndian.PutUint16(buffer[2:], numInGroup) + + listener := &TestListener{} + otf.Decode(buffer, actingVersion, 0, ir.MessageByID(1), listener) + return listener.out.String() +} + +func TestGroupBlockLengthExcludesMembersNewerThanActingVersion(t *testing.T) { + assert := assert.New(t) + + // A version 0 entry holds only the 8 byte mantissa: side was added to the composite in version 1. + assert.NotPanics(func() { decodeQuotes(t, 0, 8, 2) }) + assert.PanicsWithValue( + "group blockLength=7 is less than the fields present at actingVersion=0 for group=entries", + func() { decodeQuotes(t, 0, 7, 2) }) + + // From version 1 the entry also holds side, and the constant field adds no bytes. + assert.NotPanics(func() { decodeQuotes(t, 1, 9, 2) }) + assert.PanicsWithValue( + "group blockLength=8 is less than the fields present at actingVersion=1 for group=entries", + func() { decodeQuotes(t, 1, 8, 2) }) + + // An empty group consumes nothing, whatever its blockLength. + assert.NotPanics(func() { decodeQuotes(t, 1, 0, 0) }) +} diff --git a/sbe-tool/src/main/golang/otf/otfmessagedecoder.go b/sbe-tool/src/main/golang/otf/otfmessagedecoder.go index 09e070e120..bb5cc881e9 100644 --- a/sbe-tool/src/main/golang/otf/otfmessagedecoder.go +++ b/sbe-tool/src/main/golang/otf/otfmessagedecoder.go @@ -180,6 +180,48 @@ func DecodeData( return bufferIndex } +// typeEnd returns the end of the bytes a type occupies at the acting version. Composite members added in a +// later version do not count, since a block encoded by an older version ends before them. +func typeEnd(tokens []Token, typeIndex int, baseOffset uint64, actingVersion uint64) uint64 { + typeToken := tokens[typeIndex] + if typeToken.IsConstantEncoding() || int64(typeToken.TokenVersion()) > int64(actingVersion) { + return 0 + } + + offset := baseOffset + uint64(typeToken.Offset()) + if SignalBeginComposite != typeToken.Signal() { + return offset + uint64(typeToken.EncodedLength()) + } + + end := uint64(0) + endIndex := typeIndex + int(typeToken.ComponentTokenCount()) - 1 + for i := typeIndex + 1; i < endIndex; i += int(tokens[i].ComponentTokenCount()) { + if memberEnd := typeEnd(tokens, i, offset, actingVersion); memberEnd > end { + end = memberEnd + } + } + return end +} + +// minBlockLength returns the extent of the fixed fields present at the acting version, which is the smallest +// block length from which they can be read. Constant fields occupy no bytes on the wire. +func minBlockLength(tokens []Token, tokenIndex int, numTokens int, actingVersion uint64) uint64 { + minLength := uint64(0) + for tokenIndex < numTokens { + fieldToken := tokens[tokenIndex] + if SignalBeginField != fieldToken.Signal() { + break + } + if int64(fieldToken.TokenVersion()) <= int64(actingVersion) && !fieldToken.IsConstantEncoding() { + if fieldEnd := typeEnd(tokens, tokenIndex+1, 0, actingVersion); fieldEnd > minLength { + minLength = fieldEnd + } + } + tokenIndex += int(fieldToken.ComponentTokenCount()) + } + return minLength +} + func DecodeGroups( buffer []byte, bufferIndex int, @@ -221,6 +263,11 @@ func DecodeGroups( } beginFieldsIndex := tokenIndex + int(dimensionsTypeComposite.ComponentTokenCount()) + 1 + if isPresent && numInGroup > 0 && blockLength < minBlockLength(tokens, beginFieldsIndex, numTokens, actingVersion) { + panic(fmt.Sprintf( + "group blockLength=%d is less than the fields present at actingVersion=%d for group=%s", + blockLength, actingVersion, token.Name())) + } listener.OnGroupHeader(token, numInGroup) for i := uint64(0); i < numInGroup; i++ { listener.OnBeginGroup(token, i, numInGroup) diff --git a/sbe-tool/src/test/resources/otf-group-block-length-schema.xml b/sbe-tool/src/test/resources/otf-group-block-length-schema.xml new file mode 100644 index 0000000000..e90c34d204 --- /dev/null +++ b/sbe-tool/src/test/resources/otf-group-block-length-schema.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + 1 + 2 + + X + + + + + + + + + + + +