diff --git a/field_map.go b/field_map.go index 4aac64b1..7f343336 100644 --- a/field_map.go +++ b/field_map.go @@ -302,6 +302,10 @@ func (m *FieldMap) clearNoLock() { } // CopyInto overwrites the given FieldMap with this one. +// +// Warning: CopyInto preserves legacy behavior for repeated TagValues under the +// same tag, such as repeating group contents. Only the first TagValue for each +// tag is copied. Use CopyIntoDeep to preserve complete field slices. func (m *FieldMap) CopyInto(to *FieldMap) { m.rwLock.RLock() defer m.rwLock.RUnlock() @@ -317,6 +321,33 @@ func (m *FieldMap) CopyInto(to *FieldMap) { to.compare = m.compare } +// CopyIntoDeep overwrites the given FieldMap with this one, preserving every +// TagValue stored for each tag. +func (m *FieldMap) CopyIntoDeep(to *FieldMap) { + m.rwLock.RLock() + defer m.rwLock.RUnlock() + + to.tagLookup = make(map[Tag]field) + for tag, f := range m.tagLookup { + clone := make(field, len(f)) + for i := range f { + clone[i] = cloneTagValue(f[i]) + } + to.tagLookup[tag] = clone + } + to.tags = make([]Tag, len(m.tags)) + copy(to.tags, m.tags) + to.compare = m.compare +} + +func cloneTagValue(tv TagValue) TagValue { + return TagValue{ + tag: tv.tag, + value: append([]byte(nil), tv.value...), + bytes: append([]byte(nil), tv.bytes...), + } +} + func (m *FieldMap) add(f field) { t := fieldTag(f) if _, ok := m.tagLookup[t]; !ok { diff --git a/field_map_test.go b/field_map_test.go index 0e907873..6efe477d 100644 --- a/field_map_test.go +++ b/field_map_test.go @@ -191,6 +191,41 @@ func TestFieldMap_CopyInto(t *testing.T) { assert.Equal(t, "a", s) } +func TestFieldMap_CopyIntoDeep(t *testing.T) { + group := NewRepeatingGroup(453, GroupTemplate{ + GroupElement(448), + GroupElement(447), + GroupElement(452), + }) + group.Add(). + SetString(448, "party1"). + SetString(447, "D"). + SetString(452, "28") + group.Add(). + SetString(448, "party2"). + SetString(447, "D"). + SetString(452, "28") + + var fMapA FieldMap + fMapA.init() + fMapA.SetGroup(group) + + var fMapB FieldMap + fMapB.init() + fMapA.CopyIntoDeep(&fMapB) + + assert.Equal(t, len(fMapA.tagLookup[453]), len(fMapB.tagLookup[453])) + + var copied bytes.Buffer + writeField(fMapB.tagLookup[453], &copied) + assert.Equal(t, "453=2448=party1447=D452=28448=party2447=D452=28", copied.String()) + + fMapA.tagLookup[453][1].value[0] = 'X' + fMapA.tagLookup[453][1].bytes[4] = 'X' + assert.Equal(t, []byte("party1"), fMapB.tagLookup[453][1].value) + assert.Equal(t, "448=party1", string(fMapB.tagLookup[453][1].bytes)) +} + func TestFieldMap_Remove(t *testing.T) { var fMap FieldMap fMap.init() diff --git a/message.go b/message.go index 35e2ff67..8518366e 100644 --- a/message.go +++ b/message.go @@ -139,8 +139,11 @@ func NewMessage() *Message { return m } -// CopyInto erases the dest messages and copies the currency message content -// into it. +// CopyInto erases the dest message and copies this message content into it. +// +// Warning: CopyInto preserves legacy FieldMap.CopyInto behavior, so repeated +// TagValues under the same tag, such as repeating group contents, are not fully +// copied. Use CopyIntoDeep to preserve complete field slices. func (m *Message) CopyInto(to *Message) { m.Header.CopyInto(&to.Header.FieldMap) m.Body.CopyInto(&to.Body.FieldMap) @@ -155,6 +158,23 @@ func (m *Message) CopyInto(to *Message) { } } +// CopyIntoDeep erases the dest message and copies this message content into it, +// preserving every TagValue stored for each tag. As with CopyInto, rawMessage is +// not copied, so Bytes and String on the destination rebuild from field maps. +func (m *Message) CopyIntoDeep(to *Message) { + m.Header.CopyIntoDeep(&to.Header.FieldMap) + m.Body.CopyIntoDeep(&to.Body.FieldMap) + m.Trailer.CopyIntoDeep(&to.Trailer.FieldMap) + + to.ReceiveTime = m.ReceiveTime + to.bodyBytes = make([]byte, len(m.bodyBytes)) + copy(to.bodyBytes, m.bodyBytes) + to.fields = make([]TagValue, len(m.fields)) + for i := range to.fields { + to.fields[i] = cloneTagValue(m.fields[i]) + } +} + // ParseMessage constructs a Message from a byte slice wrapping a FIX message. func ParseMessage(msg *Message, rawMessage *bytes.Buffer) (err error) { return ParseMessageWithDataDictionary(msg, rawMessage, nil, nil) @@ -574,6 +594,13 @@ func (m *Message) Bytes() []byte { return m.build() } +// Build constructs bytes from the current Header, Body, and Trailer fields. +// It ignores any raw message buffer captured during parsing and recalculates +// BodyLength and CheckSum. +func (m *Message) Build() []byte { + return m.build() +} + func (m *Message) String() string { if m.rawMessage != nil { return m.rawMessage.String() diff --git a/message_test.go b/message_test.go index b02508cd..3b1d0762 100644 --- a/message_test.go +++ b/message_test.go @@ -126,6 +126,40 @@ func (s *MessageSuite) TestBuild() { s.True(bytes.Equal(expectedBytes, result), "Unexpected bytes, got %s", string(result)) } +func (s *MessageSuite) TestBuildIgnoresRawMessage() { + rawMsg := bytes.NewBufferString("8=FIX.4.29=10435=D34=249=TW52=20140515-19:49:56.65956=ISLD11=10021=140=154=155=TSLA60=00010101-00:00:00.00010=039") + + s.Nil(ParseMessage(s.msg, rawMsg)) + s.msg.Body.SetString(11, "upstream-id") + + built := s.msg.Build() + s.Equal(rawMsg.String(), string(s.msg.Bytes())) + s.NotEqual(rawMsg.String(), string(built)) + s.Contains(string(built), "11=upstream-id") + s.NotContains(string(built), "11=100") + + parsedBuilt := NewMessage() + s.Nil(ParseMessage(parsedBuilt, bytes.NewBuffer(built))) + + bodyLength, err := parsedBuilt.Header.GetInt(tagBodyLength) + s.Nil(err) + bodyStart := bytes.Index(built, []byte("35=")) + checkSumStart := bytes.LastIndex(built, []byte("10=")) + s.Equal(checkSumStart-bodyStart, bodyLength) + + checkSum, err := parsedBuilt.Trailer.GetString(tagCheckSum) + s.Nil(err) + s.Equal(formatCheckSum(byteSum(built[:checkSumStart])%256), checkSum) +} + +func byteSum(b []byte) int { + total := 0 + for _, c := range b { + total += int(c) + } + return total +} + func (s *MessageSuite) TestReBuild() { rawMsg := bytes.NewBufferString("8=FIX.4.29=10435=D34=249=TW52=20140515-19:49:56.65956=ISLD11=10021=140=154=155=TSLA60=00010101-00:00:00.00010=039") @@ -505,6 +539,25 @@ func (s *MessageSuite) TestCopyIntoMessage() { s.Equal(string(dest.Bytes()), renderedString) } +func (s *MessageSuite) TestCopyIntoDeepMessage() { + dict, dictErr := datadictionary.Parse("spec/FIX44.xml") + s.Nil(dictErr) + + msgString := "8=FIX.4.49=18735=D34=249=0100150=01001a52=20231231-20:19:4156=TEST" + + "1=acct111=1397621=138=140=244=1254=155=SYMABC59=060=20231231-20:19:41453=2448=4501447=D452=28448=4502447=D452=28" + + "10=044" + msgBuf := bytes.NewBufferString(msgString) + s.Nil(ParseMessageWithDataDictionary(s.msg, msgBuf, dict, dict)) + + dest := NewMessage() + s.msg.CopyIntoDeep(dest) + + s.Equal(msgString, string(dest.Build())) + + msgBuf.Reset() + s.Equal(msgString, string(dest.Build())) +} + func checkFieldInt(s *MessageSuite, fields FieldMap, tag, expected int) { toCheck, _ := fields.GetInt(Tag(tag)) s.Equal(expected, toCheck)