From 7dfe5b37202266f5f0ec1c57aca7838a85f09b7d Mon Sep 17 00:00:00 2001 From: Egor Seredin <4819888+agmt@users.noreply.github.com> Date: Wed, 22 Apr 2026 02:37:43 +0000 Subject: [PATCH 1/2] Test: body field after nested repeating group --- message_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/message_test.go b/message_test.go index 72f11dcd0..4a1b51a21 100644 --- a/message_test.go +++ b/message_test.go @@ -113,6 +113,48 @@ func (s *MessageSuite) TestParseOutOfOrder() { s.Nil(ParseMessage(s.msg, rawMsg)) } +func (s *MessageSuite) TestParseGroup_BodyFieldAfterNestedGroup() { + dict, dictErr := datadictionary.Parse("spec/FIX44.xml") + s.Nil(dictErr) + + // Wire layout (FIX 4.4 MassQuoteAcknowledgement): + // 117=QID QuoteID (body) + // 296=1 NoQuoteSets count (body group) + // 302=SET1 QuoteSetID (inside NoQuoteSets) + // 295=1 NoQuoteEntries count (nested group) + // 299=E1 QuoteEntryID (inside NoQuoteEntries) + // 132=100 BidPx (inside NoQuoteEntries) + // 133=101 OfferPx (inside NoQuoteEntries) + // 297=0 QuoteStatus (BODY level, AFTER the group) + rawMsg := bytes.NewBufferString( + "8=FIX.4.49=6335=b117=QID" + + "296=1302=SET1" + + "295=1299=E1132=100133=101" + + "297=1" + + "10=002") + + err := ParseMessageWithDataDictionary(s.msg, rawMsg, dict, dict) + s.Nil(err) + + rebuildBytes := s.msg.build() + expectedBytes := rawMsg.Bytes() + s.True(bytes.Equal(expectedBytes, rebuildBytes), "Unexpected bytes,\n +%s\n -%s", rebuildBytes, expectedBytes) + + // NoQuoteSets count (group delimiter) lands in Body as expected. + s.True(s.msg.Body.Has(Tag(296))) + + // ToDo: should be correct — QuoteStatus (297) must be a top-level body + // field per FIX 4.4. Current parseGroup logic silently absorbs 297 into + // the NoQuoteSets group buffer because it only checks whether the + // parent tag is a NumInGroup, not whether 297 is a member of the + // parent group template. + s.False(s.msg.Body.Has(Tag(297)), + "BUG: QuoteStatus (297) is absent from Body.FieldMap — parser "+ + "mis-attributed it to the parent NoQuoteSets group. This "+ + "assertion documents the bug; flip to s.True(...) when "+ + "parseGroup is fixed.") +} + func (s *MessageSuite) TestBuild() { s.msg.Header.SetField(tagBeginString, FIXString(BeginStringFIX44)) s.msg.Header.SetField(tagMsgType, FIXString("A")) From 57fef4cc7f3ce91d63fd61090fb76ac3d78880e9 Mon Sep 17 00:00:00 2001 From: Egor Seredin <4819888+agmt@users.noreply.github.com> Date: Wed, 22 Apr 2026 02:41:27 +0000 Subject: [PATCH 2/2] parseGroup: stop misattributing body fields after nested groups --- message.go | 20 ++++++++++---------- message_test.go | 17 +++++++---------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/message.go b/message.go index 5bea1d375..550f68fb0 100644 --- a/message.go +++ b/message.go @@ -310,6 +310,7 @@ func parseGroup(mp *msgParser, tags []Tag) { dm := mp.msg.fields[mp.fieldIndex : mp.fieldIndex+1] fields := getGroupFields(mp.msg, tags, mp.appDataDictionary) +parseLoop: for { mp.fieldIndex++ mp.parsedFieldBytes = &mp.msg.fields[mp.fieldIndex] @@ -350,16 +351,15 @@ func parseGroup(mp *msgParser, tags []Tag) { fields = getGroupFields(mp.msg, searchTags, mp.appDataDictionary) continue } - if len(tags) > 1 { - searchTags = tags[:len(tags)-1] - } - // Did this tag occur after a nested group and belongs to the parent group. - if isNumInGroupField(mp.msg, searchTags, mp.appDataDictionary) { - // Add the field member to the group. - dm = append(dm, *mp.parsedFieldBytes) - // Continue parsing the parent group. - fields = getGroupFields(mp.msg, searchTags, mp.appDataDictionary) - continue + // The tag isn't a member of the current group. Walk up: if an + // ancestor group includes it, resume there; otherwise it's body-level. + for len(tags) > 1 { + tags = tags[:len(tags)-1] + fields = getGroupFields(mp.msg, tags, mp.appDataDictionary) + if isGroupMember(mp.parsedFieldBytes.tag, fields) { + dm = append(dm, *mp.parsedFieldBytes) + continue parseLoop + } } // Add the repeating group. mp.msg.Body.add(dm) diff --git a/message_test.go b/message_test.go index 4a1b51a21..7bdd3af37 100644 --- a/message_test.go +++ b/message_test.go @@ -143,16 +143,13 @@ func (s *MessageSuite) TestParseGroup_BodyFieldAfterNestedGroup() { // NoQuoteSets count (group delimiter) lands in Body as expected. s.True(s.msg.Body.Has(Tag(296))) - // ToDo: should be correct — QuoteStatus (297) must be a top-level body - // field per FIX 4.4. Current parseGroup logic silently absorbs 297 into - // the NoQuoteSets group buffer because it only checks whether the - // parent tag is a NumInGroup, not whether 297 is a member of the - // parent group template. - s.False(s.msg.Body.Has(Tag(297)), - "BUG: QuoteStatus (297) is absent from Body.FieldMap — parser "+ - "mis-attributed it to the parent NoQuoteSets group. This "+ - "assertion documents the bug; flip to s.True(...) when "+ - "parseGroup is fixed.") + // QuoteStatus (297) must be a top-level body field per FIX 4.4. + s.True(s.msg.Body.Has(Tag(297)), + "QuoteStatus (297) must land at the body level; it is a body field "+ + "in MassQuoteAcknowledgement, not a member of NoQuoteSets.") + val, verr := s.msg.Body.GetInt(Tag(297)) + s.Nil(verr) + s.Equal(1, val) } func (s *MessageSuite) TestBuild() {