Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
74 changes: 74 additions & 0 deletions gocode/flyweight/src/otf/otf_group_block_length_test.go
Original file line number Diff line number Diff line change
@@ -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) })
}
47 changes: 47 additions & 0 deletions sbe-tool/src/main/golang/otf/otfmessagedecoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
36 changes: 36 additions & 0 deletions sbe-tool/src/test/resources/otf-group-block-length-schema.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<sbe:messageSchema xmlns:sbe="http://fixprotocol.io/2016/sbe"
package="otf_block_length"
id="7"
version="1"
semanticVersion="1"
description="Group entries whose fixed fields include versioned and constant members"
byteOrder="littleEndian">
<types>
<composite name="messageHeader" description="Message identifiers and length of message root">
<type name="blockLength" primitiveType="uint16"/>
<type name="templateId" primitiveType="uint16"/>
<type name="schemaId" primitiveType="uint16"/>
<type name="version" primitiveType="uint16"/>
</composite>
<composite name="groupSizeEncoding" description="Repeating group dimensions">
<type name="blockLength" primitiveType="uint16"/>
<type name="numInGroup" primitiveType="uint16"/>
</composite>
<enum name="Side" encodingType="uint8">
<validValue name="BUY">1</validValue>
<validValue name="SELL">2</validValue>
</enum>
<type name="Venue" primitiveType="char" presence="constant">X</type>
<composite name="Price">
<type name="mantissa" primitiveType="int64"/>
<ref name="side" type="Side" sinceVersion="1"/>
</composite>
</types>
<sbe:message name="Quotes" id="1">
<group name="entries" id="1" dimensionType="groupSizeEncoding">
<field name="price" id="1" type="Price"/>
<field name="venue" id="2" type="Venue"/>
</group>
</sbe:message>
</sbe:messageSchema>