Skip to main content

hotfix_message/
builder.rs

1use crate::Part;
2use crate::error::{MessageIntegrityError, ParserError, ParserResult};
3use crate::field_map::Field;
4use crate::field_types::CheckSum;
5use crate::message::{Config, Message};
6use crate::parsed_message::{GarbledReason, InvalidReason, ParsedMessage};
7use crate::parts::{Body, Header, RepeatingGroup, Trailer};
8use crate::tags::{BEGIN_STRING, BODY_LENGTH, CHECK_SUM, MSG_TYPE};
9use hotfix_dictionary::{Dictionary, LayoutItem, LayoutItemKind, TagU32};
10use std::collections::{HashMap, HashSet};
11
12pub const SOH: u8 = 0x1;
13
14const USER_DEFINED_TAG_MIN: u32 = 5000;
15
16/// Length of the checksum field.
17///
18/// It should always be 7 bytes:
19/// - 2 bytes for the tag (`10`)
20/// - a byte for the separator
21/// - 3 bytes for the value
22/// - a byte for the final delimiter
23///
24/// e.g. `10=643|`
25const CHECKSUM_LENGTH: usize = 7;
26
27pub struct MessageBuilder {
28    dict: Dictionary,
29    header_tags: HashSet<TagU32>,
30    trailer_tags: HashSet<TagU32>,
31    message_specification: HashMap<String, MessageSpecification>,
32    config: Config,
33}
34
35impl MessageBuilder {
36    pub fn new(dict: Dictionary, config: Config) -> ParserResult<Self> {
37        let header_tags = Self::get_tags_for_component(&dict, "StandardHeader")?;
38        let trailer_tags = Self::get_tags_for_component(&dict, "StandardTrailer")?;
39        let message_definitions = build_message_specifications(&dict)?;
40
41        let parser = Self {
42            dict,
43            header_tags,
44            trailer_tags,
45            message_specification: message_definitions,
46            config,
47        };
48
49        Ok(parser)
50    }
51
52    pub fn build(&self, data: &[u8]) -> ParsedMessage {
53        let mut parser = Parser {
54            position: 0,
55            raw_data: data,
56            config: &self.config,
57        };
58        let (mut header, mut trailer) = match self.verify_integrity(&mut parser) {
59            Ok((header, trailer)) => (header, trailer),
60            Err(err) => return err.into(),
61        };
62
63        let next = match self.build_header(&mut header, &mut parser) {
64            Ok(next_field) => next_field,
65            Err(err) => {
66                return parser_error_to_parsed_message(err, header);
67            }
68        };
69
70        #[allow(clippy::expect_used)]
71        let msg_type = header.get::<&str>(MSG_TYPE).expect("we know this is valid at this point as we have already verified the integrity of the header");
72        let (body, next) = match self.build_body(msg_type, &mut parser, next) {
73            Ok((body, field)) => (body, field),
74            Err(err) => {
75                return parser_error_to_parsed_message(err, header);
76            }
77        };
78
79        self.build_trailer(&mut trailer, &mut parser, next);
80
81        let msg = Message {
82            header,
83            body,
84            trailer,
85        };
86
87        ParsedMessage::Valid(msg)
88    }
89
90    fn verify_integrity(
91        &self,
92        parser: &mut Parser,
93    ) -> Result<(Header, Trailer), MessageIntegrityError> {
94        let mut header = Header::default();
95
96        // The first field should always be BeginString
97        let begin_string_field = self.parse_begin_string(parser)?;
98        header.fields.insert(begin_string_field);
99
100        // The second field should always be BodyLength
101        let body_length_field = self.parse_body_length(parser)?;
102        header.fields.insert(body_length_field);
103
104        // The BodyLength is the number of bytes between the end of the BodyLength field and the start of the last field (i.e. the checksum)
105        let body_length = if let Ok(body_length) = header.get::<usize>(BODY_LENGTH) {
106            let expected_length = parser.position + body_length + CHECKSUM_LENGTH;
107            if parser.raw_data.len() != expected_length {
108                return Err(MessageIntegrityError::InvalidBodyLength);
109            }
110            body_length
111        } else {
112            // we failed to parse body length as usize
113            return Err(MessageIntegrityError::InvalidBodyLength);
114        };
115
116        // Parse the checksum (at the end of the message) and verify it matches the computed checksum
117        let mut trailer = Trailer::default();
118        let checksum_field = parser.parse_checksum(parser.position + body_length)?;
119        trailer.fields.insert(checksum_field);
120
121        if let Ok(checksum) = trailer.get::<CheckSum>(CHECK_SUM) {
122            let computed_checksum =
123                CheckSum::compute(&parser.raw_data[0..parser.position + body_length]);
124            if computed_checksum != checksum {
125                return Err(MessageIntegrityError::InvalidCheckSum);
126            }
127        }
128
129        // The third field should be the MsgType
130        let msg_type_field = self.parse_message_type(parser)?;
131        header.fields.insert(msg_type_field);
132
133        Ok((header, trailer))
134    }
135
136    fn parse_begin_string(&self, parser: &mut Parser) -> Result<Field, MessageIntegrityError> {
137        if let Some(begin_string) = parser.next_field()
138            && begin_string.tag.get() == BEGIN_STRING.tag
139        {
140            Ok(begin_string)
141        } else {
142            Err(MessageIntegrityError::InvalidBeginString)
143        }
144    }
145
146    fn parse_body_length(&self, parser: &mut Parser) -> Result<Field, MessageIntegrityError> {
147        if let Some(body_length) = parser.next_field()
148            && body_length.tag.get() == BODY_LENGTH.tag
149        {
150            Ok(body_length)
151        } else {
152            Err(MessageIntegrityError::InvalidBodyLength)
153        }
154    }
155
156    fn parse_message_type(&self, parser: &mut Parser) -> Result<Field, MessageIntegrityError> {
157        if let Some(msg_type) = parser.next_field()
158            && msg_type.tag.get() == MSG_TYPE.tag
159        {
160            Ok(msg_type)
161        } else {
162            Err(MessageIntegrityError::InvalidMsgType)
163        }
164    }
165
166    fn build_header(&self, header: &mut Header, parser: &mut Parser) -> ParserResult<Field> {
167        // we have already added the first 3 mandatory fields, build the rest
168
169        loop {
170            let field = parser.next_field().ok_or(ParserError::Malformed(
171                "message ended within header".to_string(),
172            ))?;
173
174            if self.is_header_tag(field.tag) {
175                header.fields.insert(field);
176            } else {
177                // check the message type once all other header fields have been parsed
178                // we delay it until after parsing so our rejection has access to fields like the sequence number
179                #[allow(clippy::expect_used)]
180                let msg_type = header
181                    .get::<&str>(MSG_TYPE)
182                    .expect("this never fails as we've verified the integrity of the header");
183                if self.dict.message_by_msgtype(msg_type).is_none() {
184                    return Err(ParserError::InvalidMsgType(msg_type.to_string()));
185                }
186
187                return Ok(field);
188            }
189        }
190    }
191
192    fn build_body(
193        &self,
194        msg_type: &str,
195        parser: &mut Parser,
196        next_field: Field,
197    ) -> ParserResult<(Body, Field)> {
198        let message_def = self.get_message_def(msg_type)?;
199        let mut body = Body::default();
200        let mut field = next_field;
201
202        loop {
203            if message_def.contains_tag(field.tag) {
204                let tag = field.tag;
205                body.store_field(field);
206
207                // check if it's the start of a group and parse the group as needed
208                let field_def = self.get_dict_field_by_tag(tag.get())?;
209                match message_def.get_group(tag) {
210                    Some(group_def) => {
211                        let (groups, next) =
212                            self.parse_groups(parser, group_def, field_def.tag())?;
213                        Self::check_required_fields_in_groups(&groups, group_def)?;
214                        body.set_groups(groups).map_err(|err| {
215                            ParserError::Malformed(format!(
216                                "failed to set groups for tag {}: {err}",
217                                tag.get()
218                            ))
219                        })?;
220                        field = next;
221                    }
222                    None => {
223                        field = parser.next_field().ok_or(ParserError::Malformed(
224                            "message ended within the body".to_string(),
225                        ))?;
226                    }
227                }
228            } else if self.should_accept_unknown_user_defined(field.tag) {
229                body.store_field(field);
230                field = parser.next_field().ok_or(ParserError::Malformed(
231                    "message ended within the body".to_string(),
232                ))?;
233            } else {
234                break;
235            }
236        }
237
238        if !self.is_trailer_tag(field.tag) {
239            return Err(ParserError::InvalidField(field.tag.get()));
240        }
241
242        Ok((body, field))
243    }
244
245    fn should_accept_unknown_user_defined(&self, tag: TagU32) -> bool {
246        !self.config.validate_user_defined_fields
247            && Self::is_user_defined(tag)
248            && self.dict.field_by_tag(tag.get()).is_none()
249    }
250
251    fn is_user_defined(tag: TagU32) -> bool {
252        tag.get() >= USER_DEFINED_TAG_MIN
253    }
254
255    fn build_trailer(&self, trailer: &mut Trailer, parser: &mut Parser, next_field: Field) {
256        let mut field = Some(next_field);
257        while let Some(f) = field {
258            if f.tag.get() == CHECK_SUM.tag {
259                break;
260            }
261            trailer.store_field(f);
262            field = parser.next_field();
263        }
264    }
265
266    fn parse_groups(
267        &self,
268        parser: &mut Parser,
269        group_def: &GroupSpecification,
270        start_tag: TagU32,
271    ) -> ParserResult<(Vec<RepeatingGroup>, Field)> {
272        let mut groups = vec![];
273        let delimiter_tag = group_def.delimiter_tag();
274
275        let mut field = parser.next_field().ok_or(ParserError::Malformed(
276            "missing delimiter field".to_string(),
277        ))?;
278
279        if field.tag != delimiter_tag {
280            return Err(ParserError::InvalidGroupFieldOrder {
281                tag: field.tag.get(),
282                group_tag: group_def.number_of_entries_tag().get(),
283            });
284        }
285
286        let mut current_group: Option<RepeatingGroup> = None;
287        let mut next_declared_idx: usize = 0;
288
289        loop {
290            let current_tag = field.tag;
291
292            if current_tag == delimiter_tag {
293                // delimiter starts a new group instance
294                if let Some(g) = current_group.take() {
295                    groups.push(g);
296                }
297                let mut new_group = RepeatingGroup::new_with_tags(start_tag, delimiter_tag);
298                new_group.store_field(field);
299                current_group = Some(new_group);
300                next_declared_idx = 1;
301                field = parser
302                    .next_field()
303                    .ok_or(ParserError::Malformed("incomplete group".to_string()))?;
304            } else if group_def.contains_tag(current_tag) {
305                // tag is declared in this group; enforce declaration order
306                #[allow(clippy::expect_used)]
307                let position = group_def
308                    .fields()
309                    .iter()
310                    .position(|f| f.tag == current_tag)
311                    .expect("contains_tag guarantees the tag is present in fields()");
312                if position < next_declared_idx {
313                    return Err(ParserError::InvalidGroupFieldOrder {
314                        tag: current_tag.get(),
315                        group_tag: group_def.number_of_entries_tag().get(),
316                    });
317                }
318                next_declared_idx = position + 1;
319
320                #[allow(clippy::expect_used)]
321                let group = current_group
322                    .as_mut()
323                    .expect("the delimiter opens a group before any declared field is seen");
324                group.store_field(field);
325
326                field = if let Some(nested_group_def) = group_def.get_nested_group(current_tag) {
327                    let (nested_groups, next) =
328                        self.parse_groups(parser, nested_group_def, current_tag)?;
329                    #[allow(clippy::expect_used)]
330                    group.set_groups(nested_groups).expect(
331                        "parse_groups yields a non-empty run of entries sharing one start and delimiter tag",
332                    );
333                    next
334                } else {
335                    parser
336                        .next_field()
337                        .ok_or(ParserError::Malformed("incomplete group".to_string()))?
338                };
339            } else if self.should_accept_unknown_user_defined(current_tag) {
340                #[allow(clippy::expect_used)]
341                let group = current_group
342                    .as_mut()
343                    .expect("the delimiter opens a group before any user-defined field is seen");
344                group.store_field(field);
345                field = parser
346                    .next_field()
347                    .ok_or(ParserError::Malformed("incomplete group".to_string()))?;
348            } else {
349                if let Some(g) = current_group.take() {
350                    groups.push(g);
351                }
352                return Ok((groups, field));
353            }
354        }
355    }
356
357    fn check_required_fields_in_groups(
358        groups: &[RepeatingGroup],
359        group_def: &GroupSpecification,
360    ) -> ParserResult<()> {
361        let group_tag = group_def.number_of_entries_tag().get();
362        for entry in groups {
363            let field_map = entry.get_fields();
364
365            for field_def in group_def.fields() {
366                if field_def.is_required && !field_map.fields.contains_key(&field_def.tag) {
367                    return Err(ParserError::RequiredFieldMissing {
368                        tag: field_def.tag.get(),
369                        group_tag: Some(group_tag),
370                    });
371                }
372            }
373
374            for (nested_tag, nested_spec) in &group_def.nested_groups {
375                if let Some(nested_entries) = field_map.groups.get(nested_tag) {
376                    Self::check_required_fields_in_groups(nested_entries, nested_spec)?;
377                }
378            }
379        }
380        Ok(())
381    }
382
383    fn get_dict_field_by_tag(&self, tag: u32) -> ParserResult<hotfix_dictionary::Field<'_>> {
384        self.dict
385            .field_by_tag(tag)
386            .ok_or(ParserError::InvalidField(tag))
387    }
388
389    fn is_header_tag(&self, tag: TagU32) -> bool {
390        self.header_tags.contains(&tag)
391    }
392
393    fn is_trailer_tag(&self, tag: TagU32) -> bool {
394        self.trailer_tags.contains(&tag)
395    }
396
397    fn get_message_def(&self, msg_type: &str) -> ParserResult<&MessageSpecification> {
398        match self.message_specification.get(msg_type) {
399            Some(message_def) => Ok(message_def),
400            None => Err(ParserError::InvalidMsgType(msg_type.to_string())),
401        }
402    }
403
404    fn get_tags_for_component(
405        dict: &Dictionary,
406        component_name: &str,
407    ) -> ParserResult<HashSet<TagU32>> {
408        let mut tags = HashSet::new();
409        let component = dict
410            .component_by_name(component_name)
411            .ok_or_else(|| ParserError::InvalidComponent(component_name.to_string()))?;
412        for item in component.items() {
413            if let LayoutItemKind::Field(field) = item.kind() {
414                tags.insert(field.tag());
415            }
416        }
417
418        Ok(tags)
419    }
420}
421
422struct Parser<'a> {
423    position: usize,
424    raw_data: &'a [u8],
425    config: &'a Config,
426}
427
428impl<'a> Parser<'a> {
429    fn next_field(&mut self) -> Option<Field> {
430        let (field, end_position) = self.parse_field_at(self.position)?;
431        self.position = end_position + 1;
432
433        Some(field)
434    }
435
436    fn parse_field_at(&self, position: usize) -> Option<(Field, usize)> {
437        let mut iter = self.raw_data[position..].iter();
438        let equal_sign_position = position + iter.position(|c| *c == b'=')?;
439        let bytes_until_separator = iter.position(|c| *c == self.config.separator)?;
440        let separator_position = equal_sign_position + bytes_until_separator + 1;
441
442        let tag = tag_from_bytes(&self.raw_data[position..equal_sign_position])?;
443        let data = self.raw_data[equal_sign_position + 1..separator_position].to_vec();
444        let field = Field::new(tag, data);
445
446        Some((field, separator_position))
447    }
448
449    fn parse_checksum(&self, checksum_start: usize) -> Result<Field, MessageIntegrityError> {
450        if let Some((checksum, _)) = self.parse_field_at(checksum_start)
451            && checksum.tag.get() == CHECK_SUM.tag
452        {
453            Ok(checksum)
454        } else {
455            Err(MessageIntegrityError::InvalidCheckSum)
456        }
457    }
458}
459
460fn tag_from_bytes(bytes: &[u8]) -> Option<TagU32> {
461    let mut tag = 0u32;
462    for byte in bytes.iter().copied() {
463        tag = tag * 10 + (byte as u32 - b'0' as u32);
464    }
465
466    TagU32::new(tag)
467}
468
469fn parser_error_to_parsed_message(err: ParserError, header: Header) -> ParsedMessage {
470    match err {
471        ParserError::IOError(_) => ParsedMessage::Garbled(GarbledReason::Malformed),
472        ParserError::InvalidField(tag) => ParsedMessage::Invalid {
473            reason: InvalidReason::InvalidField(tag),
474            message: Message::with_header(header),
475        },
476        ParserError::InvalidGroup(tag) => ParsedMessage::Invalid {
477            reason: InvalidReason::InvalidGroup(tag),
478            message: Message::with_header(header),
479        },
480        ParserError::InvalidGroupFieldOrder { tag, group_tag } => ParsedMessage::Invalid {
481            reason: InvalidReason::InvalidOrderInGroup { tag, group_tag },
482            message: Message::with_header(header),
483        },
484        ParserError::InvalidComponent(tag) => ParsedMessage::Invalid {
485            reason: InvalidReason::InvalidComponent(tag),
486            message: Message::with_header(header),
487        },
488        ParserError::InvalidMsgType(msg_type) => ParsedMessage::Invalid {
489            reason: InvalidReason::InvalidMsgType(msg_type),
490            message: Message::with_header(header),
491        },
492        ParserError::RequiredFieldMissing { tag, group_tag } => ParsedMessage::Invalid {
493            reason: InvalidReason::RequiredFieldMissing { tag, group_tag },
494            message: Message::with_header(header),
495        },
496        ParserError::Malformed(_) => ParsedMessage::Garbled(GarbledReason::Malformed),
497    }
498}
499
500struct FieldSpecification {
501    pub(crate) tag: TagU32,
502    pub(crate) is_required: bool,
503}
504
505struct GroupSpecification {
506    number_of_entries_tag: TagU32,
507    fields: Vec<FieldSpecification>,
508    nested_groups: HashMap<TagU32, GroupSpecification>,
509}
510
511impl GroupSpecification {
512    pub fn fields(&self) -> &[FieldSpecification] {
513        self.fields.as_slice()
514    }
515    pub fn number_of_entries_tag(&self) -> TagU32 {
516        self.number_of_entries_tag
517    }
518
519    pub fn delimiter_tag(&self) -> TagU32 {
520        #[allow(clippy::expect_used)]
521        self.fields
522            .first()
523            .expect("groups always have at least one field")
524            .tag
525    }
526
527    pub fn contains_tag(&self, tag: TagU32) -> bool {
528        self.fields.iter().any(|f| f.tag == tag)
529    }
530
531    pub fn get_nested_group(&self, tag: TagU32) -> Option<&GroupSpecification> {
532        self.nested_groups.get(&tag)
533    }
534}
535
536struct MessageSpecification {
537    fields: Vec<FieldSpecification>,
538    groups: HashMap<TagU32, GroupSpecification>,
539}
540
541impl MessageSpecification {
542    pub fn contains_tag(&self, tag: TagU32) -> bool {
543        self.fields.iter().any(|f| f.tag == tag)
544    }
545
546    pub fn get_group(&self, tag: TagU32) -> Option<&GroupSpecification> {
547        self.groups.get(&tag)
548    }
549}
550
551fn build_message_specifications(
552    dict: &Dictionary,
553) -> ParserResult<HashMap<String, MessageSpecification>> {
554    let mut definitions = HashMap::new();
555
556    for message in dict.messages() {
557        let fields = message
558            .layout()
559            .flat_map(|item| extract_fields(dict, item))
560            .flatten()
561            .collect();
562
563        let mut groups = HashMap::new();
564        for item in message.layout() {
565            groups.extend(extract_groups(dict, item)?);
566        }
567
568        let message_def = MessageSpecification { fields, groups };
569        definitions.insert(message.msg_type().to_string(), message_def);
570    }
571
572    Ok(definitions)
573}
574
575fn extract_fields(dict: &Dictionary, item: LayoutItem) -> ParserResult<Vec<FieldSpecification>> {
576    let is_required = item.required();
577    let fields = match item.kind() {
578        LayoutItemKind::Component(c) => {
579            let component = dict
580                .component_by_name(c.name())
581                .ok_or_else(|| ParserError::InvalidComponent(c.name().to_string()))?;
582            component
583                .items()
584                .flat_map(|i| extract_fields(dict, i))
585                .flatten()
586                .collect()
587        }
588        LayoutItemKind::Field(field) => vec![FieldSpecification {
589            tag: field.tag(),
590            is_required,
591        }],
592        LayoutItemKind::Group(field, _) => vec![FieldSpecification {
593            tag: field.tag(),
594            is_required,
595        }],
596    };
597
598    Ok(fields)
599}
600
601fn extract_groups(
602    dict: &Dictionary,
603    item: LayoutItem,
604) -> ParserResult<HashMap<TagU32, GroupSpecification>> {
605    let mut groups = HashMap::new();
606    match item.kind() {
607        LayoutItemKind::Component(c) => {
608            let component = dict
609                .component_by_name(c.name())
610                .ok_or_else(|| ParserError::InvalidComponent(c.name().to_string()))?;
611            for i in component.items() {
612                groups.extend(extract_groups(dict, i)?);
613            }
614        }
615        LayoutItemKind::Group(field, items) => {
616            let mut nested_groups = HashMap::new();
617            for i in items.iter() {
618                nested_groups.extend(extract_groups(dict, i.clone())?);
619            }
620            groups.insert(
621                field.tag(),
622                GroupSpecification {
623                    number_of_entries_tag: field.tag(),
624                    fields: items
625                        .iter()
626                        .flat_map(|i| extract_fields(dict, i.clone()))
627                        .flatten()
628                        .collect(),
629                    nested_groups,
630                },
631            );
632        }
633        _ => {}
634    };
635
636    Ok(groups)
637}
638
639#[cfg(test)]
640mod tests {
641    use crate::builder::MessageBuilder;
642    use crate::field_types::Currency;
643    use crate::message::Config;
644    use crate::parsed_message::{GarbledReason, InvalidReason, ParsedMessage};
645    use crate::{Part, fix44};
646    use hotfix_dictionary::{Dictionary, IsFieldDefinition, TagU32};
647
648    const CONFIG: Config = Config::with_separator(b'|');
649
650    #[test]
651    fn test_specification_top_level_fields() {
652        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
653        let message_def = builder.get_message_def("J").unwrap();
654
655        // check that it contains `Symbol`, a tag from the nested `Instrument` component
656        assert!(message_def.contains_tag(fix44::SYMBOL.tag()));
657
658        // check that it contains `NoOrders`, the starting tag for `OrdAllocGrp`
659        assert!(message_def.contains_tag(fix44::NO_ORDERS.tag()));
660
661        // check that it doesn't contain other tags from the `OrdAllocGroup`
662        assert!(!message_def.contains_tag(fix44::ORDER_QTY.tag()));
663    }
664
665    #[test]
666    fn test_specification_top_level_groups() {
667        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
668        let message_def = builder.get_message_def("J").unwrap();
669
670        // check that it contains the right number of top-level groups
671        // expected 10 groups (7 directly (including `Parties` and `Stipulations`), 2 in `Instrument`, 1 in `InstrumentExtension`,
672        let expected_group_fields = vec![
673            fix44::NO_ORDERS,
674            fix44::NO_ALLOCS,
675            fix44::NO_EXECS,
676            fix44::NO_STIPULATIONS,
677            fix44::NO_PARTY_I_DS,
678            fix44::NO_SECURITY_ALT_ID,
679            fix44::NO_LEGS,
680            fix44::NO_UNDERLYINGS,
681            fix44::NO_EVENTS,
682            fix44::NO_INSTR_ATTRIB,
683        ];
684        assert_eq!(message_def.groups.len(), expected_group_fields.len());
685        for field in expected_group_fields {
686            assert!(
687                message_def
688                    .get_group(TagU32::new(field.tag).unwrap())
689                    .is_some()
690            );
691        }
692
693        // check that nested groups are not included directly
694        assert!(
695            message_def
696                .get_group(fix44::NO_NESTED2_PARTY_I_DS.tag())
697                .is_none()
698        );
699    }
700
701    #[test]
702    fn test_specification_nested_groups() {
703        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
704        let message_def = builder.get_message_def("J").unwrap();
705
706        // Order allocation groups only have one nested group, the parties
707        let order_alloc_group = message_def.get_group(fix44::NO_ORDERS.tag()).unwrap();
708        assert_eq!(order_alloc_group.nested_groups.len(), 1);
709        let nested_parties_2_group = order_alloc_group
710            .get_nested_group(fix44::NO_NESTED2_PARTY_I_DS.tag())
711            .expect("nested parties group to exist");
712
713        // The parties group only has one nested group, the parties subgroup
714        assert_eq!(nested_parties_2_group.nested_groups.len(), 1);
715        let subgroup = nested_parties_2_group
716            .get_nested_group(fix44::NO_NESTED2_PARTY_SUB_I_DS.tag())
717            .expect("parties subgroup to exist");
718        assert!(subgroup.nested_groups.is_empty());
719    }
720
721    #[test]
722    fn test_specification_field_order_in_nested_group() {
723        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
724        let message_def = builder.get_message_def("J").unwrap();
725
726        // get the parties group nested in the order allocation group
727        let order_alloc_group = message_def.get_group(fix44::NO_ORDERS.tag()).unwrap();
728        assert_eq!(order_alloc_group.nested_groups.len(), 1);
729        let nested_parties_2_group = order_alloc_group
730            .get_nested_group(fix44::NO_NESTED2_PARTY_I_DS.tag())
731            .expect("nested parties group to exist");
732
733        let mut fields = nested_parties_2_group.fields.iter();
734        let expected_fields = vec![
735            (fix44::NESTED2_PARTY_ID, false),
736            (fix44::NESTED2_PARTY_ID_SOURCE, false),
737            (fix44::NESTED2_PARTY_ROLE, false),
738            (fix44::NO_NESTED2_PARTY_SUB_I_DS, false),
739        ];
740
741        for (field_definition, is_required) in expected_fields {
742            let next = fields.next().unwrap();
743            assert_eq!(next.tag.get(), field_definition.tag);
744            assert_eq!(next.is_required, is_required);
745        }
746    }
747
748    #[test]
749    fn parse_simple_message() {
750        let raw = b"8=FIX.4.4|9=40|35=D|49=AFUNDMGR|56=ABROKER|15=USD|59=0|10=093|";
751        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
752
753        let message = builder.build(raw).into_message().unwrap();
754
755        let begin: &str = message.header().get(fix44::BEGIN_STRING).unwrap();
756        assert_eq!(begin, "FIX.4.4");
757
758        let body_length: u32 = message.header().get(fix44::BODY_LENGTH).unwrap();
759        assert_eq!(body_length, 40);
760
761        let message_type: &str = message.header().get(fix44::MSG_TYPE).unwrap();
762        assert_eq!(message_type, "D");
763
764        let currency: &Currency = message.get(fix44::CURRENCY).unwrap();
765        assert_eq!(currency, b"USD");
766
767        let time_in_force: &str = message.get(fix44::TIME_IN_FORCE).unwrap();
768        assert_eq!(time_in_force, "0");
769
770        let checksum: &str = message.trailer().get(fix44::CHECK_SUM).unwrap();
771        assert_eq!(checksum, "093");
772    }
773
774    #[test]
775    fn repeating_group_entries() {
776        let raw = b"8=FIX.4.4|9=191|35=8|49=SENDER|56=TARGET|34=123|52=20231103-12:00:00|11=12345|17=ABC123|150=2|39=1|55=XYZ|54=1|38=200|44=10|32=100|31=10|14=100|6=10|151=100|136=2|137=100|138=EUR|139=7|137=160|138=GBP|139=7|10=140|";
777        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
778
779        let message = builder.build(raw).into_message().unwrap();
780
781        let begin: &str = message.header().get(fix44::BEGIN_STRING).unwrap();
782        assert_eq!(begin, "FIX.4.4");
783
784        let fee1 = message.get_group(fix44::NO_MISC_FEES, 0).unwrap();
785        let amt: f64 = fee1.get(fix44::MISC_FEE_AMT).unwrap();
786        assert_eq!(amt, 100.0);
787
788        let fee2 = message.get_group(fix44::NO_MISC_FEES, 1).unwrap();
789        let fee_type: &str = fee2.get(fix44::MISC_FEE_TYPE).unwrap();
790        assert_eq!(fee_type, "7");
791
792        let checksum: &str = message.trailer().get(fix44::CHECK_SUM).unwrap();
793        assert_eq!(checksum, "140");
794    }
795
796    #[test]
797    fn nested_repeating_group_entries() {
798        let raw = b"8=FIX.4.4|9=247|35=8|34=2|49=Broker|52=20231103-09:30:00|56=Client|11=Order12345|17=Exec12345|150=0|39=0|55=APPL|54=1|38=100|32=50|31=150.00|151=50|14=50|6=150.00|453=2|448=PARTYA|447=D|452=1|802=2|523=SUBPARTYA1|803=1|523=SUBPARTYA2|803=2|448=PARTYB|447=D|452=2|10=129|";
799        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
800        let message = builder.build(raw).into_message().unwrap();
801
802        let party_a = message.get_group(fix44::NO_PARTY_I_DS, 0).unwrap();
803        let party_a_0 = party_a
804            .get_group(fix44::NO_PARTY_SUB_I_DS.tag(), 0)
805            .unwrap();
806        let sub_id_0: &str = party_a_0.get(fix44::PARTY_SUB_ID).unwrap();
807        assert_eq!(sub_id_0, "SUBPARTYA1");
808
809        let party_b = message.get_group(fix44::NO_PARTY_I_DS, 1).unwrap();
810        let party_b_id: &str = party_b.get(fix44::PARTY_ID).unwrap();
811        assert_eq!(party_b_id, "PARTYB");
812
813        let party_b_role: u32 = party_b.get(fix44::PARTY_ROLE).unwrap();
814        assert_eq!(party_b_role, 2);
815
816        let checksum: &str = message.trailer().get(fix44::CHECK_SUM).unwrap();
817        assert_eq!(checksum, "129");
818    }
819
820    #[test]
821    fn test_begin_string_not_the_first_tag() {
822        let raw = b"9=40|8=FIX.4.4|35=D|49=AFUNDMGR|56=ABROKER|15=USD|59=0|10=093|";
823        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
824        let parsed_message = builder.build(raw);
825
826        assert!(matches!(
827            parsed_message,
828            ParsedMessage::Garbled(GarbledReason::InvalidBeginString)
829        ));
830    }
831
832    #[test]
833    fn test_body_length_not_the_second_tag() {
834        let raw = b"8=FIX.4.4|49=SENDER|9=191|35=8|56=TARGET|34=123|52=20231103-12:00:00|11=12345|17=ABC123|150=2|39=1|55=XYZ|54=1|38=200|44=10|32=100|31=10|14=100|6=10|151=100|136=2|137=100|138=EUR|139=7|137=160|138=GBP|139=7|10=140|";
835        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
836        let parsed_message = builder.build(raw);
837
838        assert!(matches!(
839            parsed_message,
840            ParsedMessage::Garbled(GarbledReason::InvalidBodyLength)
841        ));
842    }
843
844    #[test]
845    fn test_body_length_is_wrong() {
846        let raw = b"8=FIX.4.4|9=192|35=8|49=SENDER|56=TARGET|34=123|52=20231103-12:00:00|11=12345|17=ABC123|150=2|39=1|55=XYZ|54=1|38=200|44=10|32=100|31=10|14=100|6=10|151=100|136=2|137=100|138=EUR|139=7|137=160|138=GBP|139=7|10=140|";
847        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
848        let parsed_message = builder.build(raw);
849
850        assert!(matches!(
851            parsed_message,
852            ParsedMessage::Garbled(GarbledReason::InvalidBodyLength)
853        ));
854    }
855
856    #[test]
857    fn test_body_length_exceeds_message_length() {
858        let raw = b"8=FIX.4.4|9=500|35=8|49=SENDER|56=TARGET|34=123|52=20231103-12:00:00|11=12345|17=ABC123|150=2|39=1|55=XYZ|54=1|38=200|44=10|32=100|31=10|14=100|6=10|151=100|136=2|137=100|138=EUR|139=7|137=160|138=GBP|139=7|10=140|";
859        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
860        let parsed_message = builder.build(raw);
861
862        assert!(matches!(
863            parsed_message,
864            ParsedMessage::Garbled(GarbledReason::InvalidBodyLength)
865        ));
866    }
867
868    #[test]
869    fn test_msg_type_is_not_the_third_tag() {
870        let raw = b"8=FIX.4.4|9=191|49=SENDER|35=8|56=TARGET|34=123|52=20231103-12:00:00|11=12345|17=ABC123|150=2|39=1|55=XYZ|54=1|38=200|44=10|32=100|31=10|14=100|6=10|151=100|136=2|137=100|138=EUR|139=7|137=160|138=GBP|139=7|10=140|";
871        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
872        let parsed_message = builder.build(raw);
873
874        assert!(matches!(
875            parsed_message,
876            ParsedMessage::Garbled(GarbledReason::InvalidMsgType)
877        ));
878    }
879
880    #[test]
881    fn test_checksum_is_not_the_last_tag() {
882        let raw = b"8=FIX.4.4|9=191|35=8|49=SENDER|56=TARGET|34=123|52=20231103-12:00:00|11=12345|17=ABC123|150=2|39=1|55=XYZ|54=1|38=200|44=10|32=100|31=10|14=100|6=10|151=100|136=2|137=100|138=EUR|139=7|137=160|138=GBP|10=140|139=7|";
883        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
884        let parsed_message = builder.build(raw);
885
886        assert!(matches!(
887            parsed_message,
888            ParsedMessage::Garbled(GarbledReason::InvalidChecksum)
889        ));
890    }
891
892    #[test]
893    fn test_invalid_checksum() {
894        let raw = b"8=FIX.4.4|9=40|35=D|49=AFUNDMGR|56=ABROKER|15=USD|59=0|10=000|";
895        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
896        let parsed_message = builder.build(raw);
897
898        assert!(matches!(
899            parsed_message,
900            ParsedMessage::Garbled(GarbledReason::InvalidChecksum)
901        ));
902    }
903
904    #[test]
905    fn test_invalid_field_in_body() {
906        let raw = b"8=FIX.4.4|9=53|35=D|49=AFUNDMGR|9999=invalid|56=ABROKER|15=USD|59=0|10=229|";
907        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
908        let parsed_message = builder.build(raw);
909
910        assert!(matches!(
911            parsed_message,
912            ParsedMessage::Invalid {
913                reason: InvalidReason::InvalidField(_),
914                ..
915            }
916        ));
917    }
918
919    #[test]
920    fn test_invalid_group_in_body() {
921        // tag=384 is `NoMsgTypes`, which is supposed to have `RefMsgType` (tag=372) and `MsgDirection` (tag=385)
922        // in our message, `RefMsgType` is missing
923        let raw = b"8=FIX.4.4|9=75|35=A|49=SENDER|56=TARGET|34=1|52=20231103-12:00:00|98=0|108=30|384=1|385=R|10=050|";
924        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
925        let parsed_message = builder.build(raw);
926
927        assert!(matches!(
928            parsed_message,
929            ParsedMessage::Invalid {
930                reason: InvalidReason::InvalidOrderInGroup {
931                    tag: 385,
932                    group_tag: 384
933                },
934                ..
935            }
936        ));
937    }
938
939    #[test]
940    fn test_parsing_nested_component_inside_group() {
941        // an `AllocationInstruction` with `CommissionData` nested inside `AllocGrp`
942        let raw_instrument = "55=AAPL|107=Apple Inc|167=CS";
943        let raw_alloc_group =
944            "78=2|79=ACC001|661=1|80=5000|12=100|13=3|79=ACC002|661=1|80=5000|12=75|13=2";
945        let raw = format!(
946            "8=FIX.4.4|9=222|35=J|49=SELLSIDE|56=BUYSIDE|34=100|52=20251023-14:30:00|70=ALLOC001|71=0|626=1|857=0|54=1|{raw_instrument}|53=10000|6=125|75=20251023|{raw_alloc_group}|10=068|"
947        );
948        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
949        let message = builder.build(raw.as_bytes()).into_message().unwrap();
950
951        let alloc_1 = message.get_group(fix44::NO_ALLOCS, 0).unwrap();
952        assert_eq!(alloc_1.get::<&str>(fix44::ALLOC_ACCOUNT).unwrap(), "ACC001");
953        assert_eq!(alloc_1.get::<f64>(fix44::COMMISSION).unwrap(), 100.0);
954        assert_eq!(alloc_1.get::<&str>(fix44::COMM_TYPE).unwrap(), "3");
955
956        let alloc_2 = message.get_group(fix44::NO_ALLOCS, 1).unwrap();
957        assert_eq!(alloc_2.get::<&str>(fix44::ALLOC_ACCOUNT).unwrap(), "ACC002");
958        assert_eq!(alloc_2.get::<f64>(fix44::COMMISSION).unwrap(), 75.0);
959        assert_eq!(alloc_2.get::<&str>(fix44::COMM_TYPE).unwrap(), "2");
960    }
961
962    const CONFIG_NO_USER_VALIDATION: Config =
963        Config::with_separator(b'|').validate_user_defined_fields(false);
964
965    fn build_pipe_separated_message(content: &str) -> Vec<u8> {
966        let body_length = content.len();
967        let prefix = format!("8=FIX.4.4|9={body_length}|{content}");
968        let checksum: u8 = prefix.bytes().fold(0u8, |acc, x| acc.wrapping_add(x));
969        format!("{prefix}10={checksum:03}|").into_bytes()
970    }
971
972    #[test]
973    fn test_unknown_user_defined_tag_at_body_rejected_by_default() {
974        let raw =
975            build_pipe_separated_message("35=D|49=SENDER|56=TARGET|55=AAPL|59=0|20000=custom|");
976        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
977        let parsed = builder.build(&raw);
978
979        assert!(matches!(
980            parsed,
981            ParsedMessage::Invalid {
982                reason: InvalidReason::InvalidField(20000),
983                ..
984            }
985        ));
986    }
987
988    #[test]
989    fn test_unknown_user_defined_tag_at_body_accepted_when_validation_disabled() {
990        let raw =
991            build_pipe_separated_message("35=D|49=SENDER|56=TARGET|55=AAPL|59=0|20000=custom|");
992        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG_NO_USER_VALIDATION).unwrap();
993        let message = builder.build(&raw).into_message().unwrap();
994
995        let custom_tag = TagU32::new(20000).unwrap();
996        let custom_value = message.get_field_map().get_raw(custom_tag).unwrap();
997        assert_eq!(custom_value, b"custom");
998
999        // Known fields should still be reachable.
1000        assert_eq!(message.get::<&str>(fix44::SYMBOL).unwrap(), "AAPL");
1001    }
1002
1003    #[test]
1004    fn test_unknown_user_defined_tag_inside_group_stays_in_group_when_validation_disabled() {
1005        let raw = build_pipe_separated_message(
1006            "35=D|49=SENDER|56=TARGET|453=1|448=PARTYA|447=D|452=1|20000=custom|55=AAPL|",
1007        );
1008        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG_NO_USER_VALIDATION).unwrap();
1009        let message = builder.build(&raw).into_message().unwrap();
1010
1011        // 20000 doesn't belong to the parent message and isn't a
1012        // trailer field, so with user-defined validation disabled it stays
1013        // inside the current group rather than leaking to the body.
1014        let party = message.get_group(fix44::NO_PARTY_I_DS, 0).unwrap();
1015        assert_eq!(party.get::<&str>(fix44::PARTY_ID).unwrap(), "PARTYA");
1016        assert!(message.get_group(fix44::NO_PARTY_I_DS, 1).is_none());
1017
1018        let custom_tag = TagU32::new(20000).unwrap();
1019        let in_group = party.get_field_map().get_raw(custom_tag).unwrap();
1020        assert_eq!(in_group, b"custom");
1021
1022        // The body must NOT have picked up the unknown tag.
1023        assert!(message.get_field_map().get_raw(custom_tag).is_none());
1024
1025        // Tag 55 belongs to NewOrderSingle, so it ends the group and lands on the body.
1026        assert_eq!(message.get::<&str>(fix44::SYMBOL).unwrap(), "AAPL");
1027    }
1028
1029    #[test]
1030    fn test_unknown_user_defined_tag_inside_group_rejected_when_validation_enabled() {
1031        let raw = build_pipe_separated_message(
1032            "35=D|49=SENDER|56=TARGET|453=1|448=PARTYA|447=D|452=1|20000=custom|55=AAPL|",
1033        );
1034        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
1035        let parsed = builder.build(&raw);
1036
1037        assert!(matches!(
1038            parsed,
1039            ParsedMessage::Invalid {
1040                reason: InvalidReason::InvalidField(20000),
1041                ..
1042            }
1043        ));
1044    }
1045
1046    #[test]
1047    fn test_unknown_user_defined_tag_inside_nested_group_stays_in_nested_group() {
1048        // 20000 appears inside a NoPartySubIDs entry. NoPartySubIDs is the
1049        // immediate parent (a Group) and doesn't contain 20000, so the field
1050        // is kept inside the nested entry. The trailer (10) ends both the
1051        // nested group and the outer NoPartyIDs group cleanly.
1052        let raw = build_pipe_separated_message(
1053            "35=D|49=SENDER|56=TARGET|453=1|448=PARTYA|447=D|452=1|802=1|523=SUB1|803=1|20000=custom|",
1054        );
1055        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG_NO_USER_VALIDATION).unwrap();
1056        let message = builder.build(&raw).into_message().unwrap();
1057
1058        let party = message.get_group(fix44::NO_PARTY_I_DS, 0).unwrap();
1059        let sub = party.get_group(fix44::NO_PARTY_SUB_I_DS.tag(), 0).unwrap();
1060        assert_eq!(sub.get::<&str>(fix44::PARTY_SUB_ID).unwrap(), "SUB1");
1061
1062        let custom_tag = TagU32::new(20000).unwrap();
1063        // 20000 lives inside the nested NoPartySubIDs entry...
1064        let in_sub = sub.get_field_map().get_raw(custom_tag).unwrap();
1065        assert_eq!(in_sub, b"custom");
1066        // ...not on the outer NoPartyIDs entry...
1067        assert!(party.get_field_map().get_raw(custom_tag).is_none());
1068        // ...nor on the body.
1069        assert!(message.get_field_map().get_raw(custom_tag).is_none());
1070    }
1071
1072    #[test]
1073    fn test_group_rejects_when_first_tag_is_not_delimiter() {
1074        // 453 (NoPartyIDs) declares the count but the first tag after it is
1075        // 20000 instead of the delimiter 448. This is a
1076        // hard reject regardless of user-defined-field validation.
1077        let raw = build_pipe_separated_message(
1078            "35=D|49=SENDER|56=TARGET|453=1|20000=x|448=PARTYA|447=D|452=1|",
1079        );
1080        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG_NO_USER_VALIDATION).unwrap();
1081        let parsed = builder.build(&raw);
1082
1083        assert!(matches!(
1084            parsed,
1085            ParsedMessage::Invalid {
1086                reason: InvalidReason::InvalidOrderInGroup {
1087                    tag: 20000,
1088                    group_tag: 453,
1089                },
1090                ..
1091            }
1092        ));
1093    }
1094
1095    #[test]
1096    fn test_known_outer_field_after_group_fields_ends_group() {
1097        let raw = build_pipe_separated_message(
1098            "35=D|49=SENDER|56=TARGET|453=1|448=PARTYA|447=D|452=1|55=AAPL|59=0|",
1099        );
1100        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
1101        let message = builder.build(&raw).into_message().unwrap();
1102
1103        let party = message.get_group(fix44::NO_PARTY_I_DS, 0).unwrap();
1104        assert_eq!(party.get::<&str>(fix44::PARTY_ID).unwrap(), "PARTYA");
1105        assert!(message.get_group(fix44::NO_PARTY_I_DS, 1).is_none());
1106
1107        // Symbol must not have been pulled into the group.
1108        assert!(party.get_raw(fix44::SYMBOL).is_none());
1109        assert_eq!(message.get::<&str>(fix44::SYMBOL).unwrap(), "AAPL");
1110        assert_eq!(message.get::<&str>(fix44::TIME_IN_FORCE).unwrap(), "0");
1111    }
1112
1113    #[test]
1114    fn test_nested_and_outer_group_end_on_body_field() {
1115        let raw = build_pipe_separated_message(
1116            "35=D|49=SENDER|56=TARGET|453=1|448=PARTYA|447=D|452=1|802=1|523=SUB1|803=1|55=AAPL|59=0|",
1117        );
1118        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
1119        let message = builder.build(&raw).into_message().unwrap();
1120
1121        let party = message.get_group(fix44::NO_PARTY_I_DS, 0).unwrap();
1122        let sub = party.get_group(fix44::NO_PARTY_SUB_I_DS.tag(), 0).unwrap();
1123        assert_eq!(sub.get::<&str>(fix44::PARTY_SUB_ID).unwrap(), "SUB1");
1124
1125        assert_eq!(message.get::<&str>(fix44::SYMBOL).unwrap(), "AAPL");
1126        assert_eq!(message.get::<&str>(fix44::TIME_IN_FORCE).unwrap(), "0");
1127    }
1128
1129    #[test]
1130    fn test_declared_group_field_out_of_order_is_rejected() {
1131        let raw =
1132            build_pipe_separated_message("35=D|49=SENDER|56=TARGET|453=1|448=PARTYA|452=1|447=D|");
1133        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
1134        let parsed = builder.build(&raw);
1135
1136        assert!(matches!(
1137            parsed,
1138            ParsedMessage::Invalid {
1139                reason: InvalidReason::InvalidOrderInGroup {
1140                    tag: 447,
1141                    group_tag: 453,
1142                },
1143                ..
1144            }
1145        ));
1146    }
1147
1148    #[test]
1149    fn test_multiple_nested_group_instances_are_parsed() {
1150        let raw = build_pipe_separated_message(
1151            "35=D|49=SENDER|56=TARGET|453=1|448=PARTYA|447=D|452=1|802=2|523=SUB1|803=1|523=SUB2|803=2|55=AAPL|",
1152        );
1153        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
1154        let message = builder.build(&raw).into_message().unwrap();
1155
1156        let party = message.get_group(fix44::NO_PARTY_I_DS, 0).unwrap();
1157        let sub0 = party.get_group(fix44::NO_PARTY_SUB_I_DS.tag(), 0).unwrap();
1158        let sub1 = party.get_group(fix44::NO_PARTY_SUB_I_DS.tag(), 1).unwrap();
1159        assert_eq!(sub0.get::<&str>(fix44::PARTY_SUB_ID).unwrap(), "SUB1");
1160        assert_eq!(sub1.get::<&str>(fix44::PARTY_SUB_ID).unwrap(), "SUB2");
1161        assert!(party.get_group(fix44::NO_PARTY_SUB_I_DS.tag(), 2).is_none());
1162
1163        assert_eq!(message.get::<&str>(fix44::SYMBOL).unwrap(), "AAPL");
1164    }
1165
1166    const NESTED_REQUIRED_SPEC: &str = r#"<fix type='FIX' major='4' minor='4' servicepack='0'>
1167 <header>
1168  <field name='BeginString' required='Y'/>
1169  <field name='BodyLength' required='Y'/>
1170  <field name='MsgType' required='Y'/>
1171  <field name='SenderCompID' required='Y'/>
1172  <field name='TargetCompID' required='Y'/>
1173  <field name='MsgSeqNum' required='Y'/>
1174  <field name='SendingTime' required='Y'/>
1175 </header>
1176 <trailer>
1177  <field name='CheckSum' required='Y'/>
1178 </trailer>
1179 <messages>
1180  <message name='NestedReqMsg' msgtype='U1' msgcat='app'>
1181   <group name='NoOuter' required='N'>
1182    <field name='OuterDelim' required='N'/>
1183    <group name='NoInner' required='N'>
1184     <field name='InnerDelim' required='N'/>
1185     <field name='InnerRequired' required='Y'/>
1186    </group>
1187   </group>
1188   <field name='CustomBody' required='N'/>
1189  </message>
1190 </messages>
1191 <components>
1192 </components>
1193 <fields>
1194  <field number='8' name='BeginString' type='STRING'/>
1195  <field number='9' name='BodyLength' type='LENGTH'/>
1196  <field number='35' name='MsgType' type='STRING'/>
1197  <field number='49' name='SenderCompID' type='STRING'/>
1198  <field number='56' name='TargetCompID' type='STRING'/>
1199  <field number='34' name='MsgSeqNum' type='SEQNUM'/>
1200  <field number='52' name='SendingTime' type='UTCTIMESTAMP'/>
1201  <field number='10' name='CheckSum' type='STRING'/>
1202  <field number='6001' name='NoOuter' type='NUMINGROUP'/>
1203  <field number='6002' name='OuterDelim' type='STRING'/>
1204  <field number='6003' name='NoInner' type='NUMINGROUP'/>
1205  <field number='6004' name='InnerDelim' type='STRING'/>
1206  <field number='6005' name='InnerRequired' type='STRING'/>
1207  <field number='5100' name='CustomBody' type='STRING'/>
1208 </fields>
1209</fix>"#;
1210
1211    #[test]
1212    fn test_declared_user_defined_body_field_ends_group_when_validation_disabled() {
1213        let dict = Dictionary::from_quickfix_spec(NESTED_REQUIRED_SPEC).unwrap();
1214        let raw = build_pipe_separated_message(
1215            "35=U1|49=S|56=T|34=1|52=20231103-12:00:00|6001=1|6002=od|6003=1|6004=id|6005=req|5100=v|",
1216        );
1217        let builder = MessageBuilder::new(dict, CONFIG_NO_USER_VALIDATION).unwrap();
1218        let message = builder.build(&raw).into_message().unwrap();
1219
1220        let custom_tag = TagU32::new(5100).unwrap();
1221        let outer_tag = TagU32::new(6001).unwrap();
1222        let inner_tag = TagU32::new(6003).unwrap();
1223
1224        let outer = message.get_field_map().get_group(outer_tag, 0).unwrap();
1225        let inner = outer.get_group(inner_tag, 0).unwrap();
1226        assert!(inner.get_field_map().get_raw(custom_tag).is_none());
1227        assert!(outer.get_field_map().get_raw(custom_tag).is_none());
1228        assert_eq!(message.get_field_map().get_raw(custom_tag).unwrap(), b"v");
1229    }
1230
1231    #[test]
1232    fn test_required_field_missing_in_nested_group() {
1233        let dict = Dictionary::from_quickfix_spec(NESTED_REQUIRED_SPEC).unwrap();
1234        let raw = build_pipe_separated_message(
1235            "35=U1|49=S|56=T|34=1|52=20231103-12:00:00|6001=1|6002=od|6003=1|6004=id|",
1236        );
1237        let builder = MessageBuilder::new(dict, CONFIG).unwrap();
1238        let parsed = builder.build(&raw);
1239
1240        assert!(matches!(
1241            parsed,
1242            ParsedMessage::Invalid {
1243                reason: InvalidReason::RequiredFieldMissing {
1244                    tag: 6005,
1245                    group_tag: Some(6003),
1246                },
1247                ..
1248            }
1249        ));
1250    }
1251
1252    #[test]
1253    fn test_fields_declared_in_group_still_parsed_inside_when_validation_disabled() {
1254        // Same nested-group payload as `nested_repeating_group_entries`, but with
1255        // user-defined-field validation disabled. Tags that *are* declared inside
1256        // the group dictionary (including nested NoPartySubIDs) must still be
1257        // placed inside the group rather than leaking to the body.
1258        let raw = b"8=FIX.4.4|9=247|35=8|34=2|49=Broker|52=20231103-09:30:00|56=Client|11=Order12345|17=Exec12345|150=0|39=0|55=APPL|54=1|38=100|32=50|31=150.00|151=50|14=50|6=150.00|453=2|448=PARTYA|447=D|452=1|802=2|523=SUBPARTYA1|803=1|523=SUBPARTYA2|803=2|448=PARTYB|447=D|452=2|10=129|";
1259        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG_NO_USER_VALIDATION).unwrap();
1260        let message = builder.build(raw).into_message().unwrap();
1261
1262        let party_a = message.get_group(fix44::NO_PARTY_I_DS, 0).unwrap();
1263        let sub = party_a
1264            .get_group(fix44::NO_PARTY_SUB_I_DS.tag(), 0)
1265            .unwrap();
1266        assert_eq!(sub.get::<&str>(fix44::PARTY_SUB_ID).unwrap(), "SUBPARTYA1");
1267
1268        let party_b = message.get_group(fix44::NO_PARTY_I_DS, 1).unwrap();
1269        assert_eq!(party_b.get::<&str>(fix44::PARTY_ID).unwrap(), "PARTYB");
1270    }
1271
1272    #[test]
1273    fn test_group_entry_missing_required_field_is_rejected() {
1274        let dict = Dictionary::fix44();
1275        let builder = MessageBuilder::new(dict, CONFIG).unwrap();
1276
1277        // Find any (msg_type, group, required-non-delim-field) combination so
1278        // we can construct a minimal message that's structurally well-formed
1279        // except for the missing group field.
1280        let candidate = builder
1281            .message_specification
1282            .iter()
1283            .find_map(|(msg_type, message_def)| {
1284                message_def
1285                    .groups
1286                    .iter()
1287                    .find_map(|(group_tag, group_spec)| {
1288                        let required_non_delim = group_spec
1289                            .fields()
1290                            .iter()
1291                            .find(|f| f.is_required && f.tag != group_spec.delimiter_tag())?;
1292                        Some((
1293                            msg_type.clone(),
1294                            group_tag.get(),
1295                            group_spec.delimiter_tag().get(),
1296                            required_non_delim.tag.get(),
1297                        ))
1298                    })
1299            });
1300
1301        let Some((msg_type, group_tag, delimiter_tag, missing_tag)) = candidate else {
1302            // The dictionary has no group with a required non-delimiter field.
1303            return;
1304        };
1305
1306        let body = format!(
1307            "35={msg_type}|49=S|56=T|34=1|52=20231103-12:00:00|{group_tag}=1|{delimiter_tag}=X|"
1308        );
1309        let raw = build_pipe_separated_message(&body);
1310        let parsed = builder.build(&raw);
1311
1312        match parsed {
1313            ParsedMessage::Invalid {
1314                reason:
1315                    InvalidReason::RequiredFieldMissing {
1316                        tag,
1317                        group_tag: Some(g),
1318                    },
1319                ..
1320            } => {
1321                assert_eq!(tag, missing_tag);
1322                assert_eq!(g, group_tag);
1323            }
1324            other => panic!(
1325                "expected RequiredFieldMissing(tag={missing_tag}, group_tag={group_tag}); \
1326                 msg_type={msg_type}, delimiter={delimiter_tag}, got: {}",
1327                match &other {
1328                    ParsedMessage::Valid(_) => "Valid".to_string(),
1329                    ParsedMessage::Invalid { reason, .. } => match reason {
1330                        InvalidReason::InvalidField(t) => format!("InvalidField({t})"),
1331                        InvalidReason::InvalidGroup(t) => format!("InvalidGroup({t})"),
1332                        InvalidReason::InvalidOrderInGroup { tag, group_tag } => {
1333                            format!("InvalidOrderInGroup(tag={tag}, group_tag={group_tag})")
1334                        }
1335                        InvalidReason::InvalidComponent(s) => format!("InvalidComponent({s})"),
1336                        InvalidReason::InvalidMsgType(s) => format!("InvalidMsgType({s})"),
1337                        InvalidReason::RequiredFieldMissing { tag, group_tag } => {
1338                            format!("RequiredFieldMissing(tag={tag}, group_tag={group_tag:?})")
1339                        }
1340                    },
1341                    ParsedMessage::Garbled(_) => "Garbled".to_string(),
1342                    ParsedMessage::UnexpectedError(_) => "UnexpectedError".to_string(),
1343                }
1344            ),
1345        }
1346    }
1347}