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 + + + + + + + + + + + +