Skip to main content

fix_codec_rs/
message.rs

1use std::cell::OnceCell;
2
3use smallvec::SmallVec;
4
5use crate::body_length::parse_body_length;
6use crate::checksum::{compute_checksum, parse_checksum};
7use crate::error::FixError;
8use crate::field::Field;
9use crate::group::{FIX42_GROUPS, FIX44_GROUPS, GroupIter, GroupSpec, parse_count};
10use crate::tag::{self, Tag};
11
12/// Default inline capacity for the sorted index — matches the decoder's field capacity.
13const SORTED_CAPACITY: usize = 32;
14
15/// A decoded FIX message.
16///
17/// Zero-copy: field values are sub-slices of the original input buffer — no
18/// bytes are copied when accessing fields.
19///
20/// The sorted tag index for [`find`] is built lazily on the first call and
21/// cached for the lifetime of the message. This means `decode()` pays no sort
22/// cost when you never call `find()`, and pays it at most once when you do.
23#[derive(Debug)]
24pub struct Message<'a> {
25    /// The raw bytes of the complete FIX message as received (e.g. the network
26    /// buffer passed to `Decoder::decode`). Every field value is a sub-slice of
27    /// this buffer — no bytes are copied when accessing fields.
28    pub(crate) buf: &'a [u8],
29
30    /// Index of parsed fields. Each entry is `(tag, start, end)` where:
31    /// - `tag`   — the numeric FIX tag (e.g. `8`, `35`, `49`).
32    /// - `start` — byte offset in `buf` where the field *value* begins
33    ///   (the byte immediately after `=`).
34    /// - `end`   — byte offset in `buf` where the field value ends
35    ///   (the SOH byte `\x01`, exclusive).
36    ///
37    /// A field value is recovered as `&buf[start as usize..end as usize]`.
38    /// The slice is borrowed from the `Decoder`'s internal `SmallVec`, so it
39    /// lives as long as `'a`.
40    pub(crate) offsets: &'a [(Tag, u32, u32)],
41
42    /// Sorted (tag, offsets_index) pairs for O(log n) binary search in find().
43    ///
44    /// Built lazily on the first call to `find()` and cached for the lifetime
45    /// of the message via `OnceCell`. Never allocated if `find()` is never
46    /// called, and built at most once regardless of how many times `find()` is
47    /// called.
48    sorted: OnceCell<SmallVec<[(Tag, u16); SORTED_CAPACITY]>>,
49}
50
51impl<'a> Message<'a> {
52    /// Create a new message from a buffer and an offset slice.
53    /// The sorted index starts uninitialized and is built lazily on first find().
54    pub(crate) fn new(buf: &'a [u8], offsets: &'a [(Tag, u32, u32)]) -> Self {
55        Self {
56            buf,
57            offsets,
58            sorted: OnceCell::new(),
59        }
60    }
61
62    /// Number of fields in the message.
63    #[inline]
64    pub fn len(&self) -> usize {
65        self.offsets.len()
66    }
67
68    /// Returns true if the message contains no fields.
69    #[inline]
70    pub fn is_empty(&self) -> bool {
71        self.offsets.is_empty()
72    }
73
74    /// Returns the field at `index`, reconstructing it zero-copy from the
75    /// stored byte offsets. Panics if `index >= self.len()`.
76    #[inline]
77    pub fn field(&self, index: usize) -> Field<'a> {
78        let (tag, start, end) = self.offsets[index];
79        Field {
80            tag,
81            value: &self.buf[start as usize..end as usize],
82        }
83    }
84
85    /// Returns an iterator over all fields, reconstructing each `Field<'a>`
86    /// zero-copy on demand.
87    #[inline]
88    pub fn fields(&self) -> impl Iterator<Item = Field<'a>> + '_ {
89        self.offsets.iter().map(move |&(tag, start, end)| Field {
90            tag,
91            value: &self.buf[start as usize..end as usize],
92        })
93    }
94
95    /// Return the value of tag 8 (`BEGIN_STRING`) as a byte slice, or `None`
96    /// if the field is absent.
97    ///
98    /// Common values are `b"FIX.4.2"`, `b"FIX.4.4"`, `b"FIXT.1.1"`, etc.
99    #[inline]
100    pub fn fix_version(&self) -> Option<&'a [u8]> {
101        self.find(tag::BEGIN_STRING).map(|f| f.value)
102    }
103
104    /// Find the first field with the given tag, or `None` if not present.
105    ///
106    /// The sorted index is built lazily on the first call (O(n log n)) and
107    /// cached for subsequent calls (O(log n) binary search). If `find()` is
108    /// never called, the sort never happens.
109    #[inline]
110    pub fn find(&self, tag: Tag) -> Option<Field<'a>> {
111        let sorted = self.sorted.get_or_init(|| {
112            let mut v: SmallVec<[(Tag, u16); SORTED_CAPACITY]> =
113                SmallVec::with_capacity(self.offsets.len());
114            for (i, &(t, _, _)) in self.offsets.iter().enumerate() {
115                v.push((t, i as u16));
116            }
117            v.sort_unstable_by_key(|&(t, _)| t);
118            v
119        });
120
121        let idx = sorted.partition_point(|&(t, _)| t < tag);
122        let &(found_tag, offset_idx) = sorted.get(idx)?;
123        if found_tag != tag {
124            return None;
125        }
126        let (t, start, end) = self.offsets[offset_idx as usize];
127        Some(Field {
128            tag: t,
129            value: &self.buf[start as usize..end as usize],
130        })
131    }
132
133    /// Return an iterator over the instances of the repeating group described
134    /// by `spec`.
135    ///
136    /// The iterator is zero-copy: each `Group` borrows directly into this
137    /// message's offset slice and raw buffer. If the count tag is absent or
138    /// its value is `0`, the iterator yields nothing.
139    ///
140    /// # Example
141    /// ```ignore
142    /// for entry in msg.groups(&group::MD_ENTRIES) {
143    ///     let ty  = entry.find(tag::MD_ENTRY_TYPE);
144    ///     let px  = entry.find(tag::MD_ENTRY_PX);
145    /// }
146    /// ```
147    #[inline]
148    pub fn groups(&self, spec: &GroupSpec) -> GroupIter<'a> {
149        // Find the NO_* count tag position.
150        let pos = self
151            .offsets
152            .iter()
153            .position(|&(t, _, _)| t == spec.count_tag);
154
155        let (count, remaining) = match pos {
156            None => (0, &[][..]),
157            Some(i) => {
158                let (_, start, end) = self.offsets[i];
159                let count = parse_count(&self.buf[start as usize..end as usize]);
160                let after = &self.offsets[i + 1..];
161                (count, after)
162            }
163        };
164
165        GroupIter {
166            buf: self.buf,
167            remaining,
168            delimiter_tag: spec.delimiter_tag,
169            count,
170            emitted: 0,
171        }
172    }
173
174    /// Return an iterator over every repeating group present in this message.
175    ///
176    /// Scans the appropriate group spec array based on the FIX version detected
177    /// from tag 8 (`BEGIN_STRING`): `FIX42_GROUPS` for FIX 4.2 messages, and
178    /// both `FIX42_GROUPS` + `FIX44_GROUPS` for FIX 4.4 messages (which is a
179    /// superset). Yields `(&'static GroupSpec, GroupIter<'a>)` for each spec
180    /// whose count tag is found in the message with a non-zero count. Groups
181    /// whose count tag is absent or zero are skipped.
182    ///
183    /// The order follows the order of the spec arrays, not the order fields
184    /// appear in the message.
185    ///
186    /// # Example
187    /// ```ignore
188    /// for (spec, instances) in msg.all_groups() {
189    ///     for g in instances {
190    ///         // process each group instance
191    ///     }
192    /// }
193    /// ```
194    /// Validate the BodyLength field (tag 9).
195    ///
196    /// A FIX message body spans from the first byte after the `9=…\x01` field
197    /// up to and including the SOH that terminates the last field before `10=`.
198    /// This method computes that byte count from the raw buffer and compares it
199    /// to the value declared in tag 9.
200    ///
201    /// # Errors
202    /// Returns `FixError::InvalidBodyLength` when:
203    /// - The message has fewer than 3 fields (no room for tags 8, 9, and 10).
204    /// - Tag 9 is not at position 1 or its value cannot be parsed as an integer.
205    /// - Tag 10 is not the last field.
206    /// - The computed byte count does not match the declared value.
207    pub fn validate_body_length(&self) -> Result<(), FixError> {
208        let n = self.offsets.len();
209        if n < 3 {
210            return Err(FixError::InvalidBodyLength);
211        }
212
213        // Tag 9 must be the second field.
214        let (tag9, _, body_length_value_end) = self.offsets[1];
215        if tag9 != tag::BODY_LENGTH {
216            return Err(FixError::InvalidBodyLength);
217        }
218
219        // Tag 10 must be the last field.
220        let (tag10, checksum_value_start, _) = self.offsets[n - 1];
221        if tag10 != tag::CHECK_SUM {
222            return Err(FixError::InvalidBodyLength);
223        }
224
225        // Parse the declared body length from the raw buffer.
226        let declared = parse_body_length(
227            &self.buf[self.offsets[1].1 as usize..body_length_value_end as usize],
228        )
229        .ok_or(FixError::InvalidBodyLength)?;
230
231        // Body bytes: from (SOH of tag-9 field + 1) to (start of "10=" tag bytes).
232        // "10=" is 3 bytes, so the tag-10 field starts at checksum_value_start - 3.
233        let body_start = body_length_value_end as usize + 1;
234        let checksum_tag_start = checksum_value_start as usize - 3; // len("10=") == 3
235        let computed = checksum_tag_start.saturating_sub(body_start);
236
237        if computed == declared {
238            Ok(())
239        } else {
240            Err(FixError::InvalidBodyLength)
241        }
242    }
243
244    /// Validate the CheckSum field (tag 10).
245    ///
246    /// The FIX checksum is the sum of every byte from the start of the buffer
247    /// up to (but not including) the `10=` tag bytes, taken mod 256. This
248    /// method computes that value and compares it to the 3-digit decimal string
249    /// stored in tag 10.
250    ///
251    /// # Errors
252    /// Returns `FixError::InvalidCheckSum` when:
253    /// - The message has fewer than 1 field.
254    /// - Tag 10 is not the last field or its value cannot be parsed.
255    /// - The computed checksum does not match the declared value.
256    pub fn validate_checksum(&self) -> Result<(), FixError> {
257        let n = self.offsets.len();
258        if n == 0 {
259            return Err(FixError::InvalidCheckSum);
260        }
261
262        // Tag 10 must be the last field.
263        let (tag10, checksum_value_start, checksum_value_end) = self.offsets[n - 1];
264        if tag10 != tag::CHECK_SUM {
265            return Err(FixError::InvalidCheckSum);
266        }
267
268        // Parse the declared checksum from the raw buffer.
269        let declared =
270            parse_checksum(&self.buf[checksum_value_start as usize..checksum_value_end as usize])
271                .ok_or(FixError::InvalidCheckSum)?;
272
273        // Checksum covers all bytes before the "10=" tag bytes.
274        let checksum_tag_start = checksum_value_start as usize - 3; // len("10=") == 3
275        let computed = compute_checksum(&self.buf[..checksum_tag_start]);
276
277        if computed == declared {
278            Ok(())
279        } else {
280            Err(FixError::InvalidCheckSum)
281        }
282    }
283
284    #[inline]
285    pub fn all_groups(&self) -> impl Iterator<Item = (&'static GroupSpec, GroupIter<'a>)> + '_ {
286        let specs: &[&GroupSpec] = match self.fix_version() {
287            Some(b"FIX.4.4") => FIX44_GROUPS,
288            _ => FIX42_GROUPS,
289        };
290
291        specs.iter().copied().filter_map(|spec| {
292            // Check if the count tag is present with a non-zero count.
293            let found = self.offsets.iter().find(|&&(t, _, _)| t == spec.count_tag);
294            let &(_, start, end) = found?;
295            let count = parse_count(&self.buf[start as usize..end as usize]);
296            if count == 0 {
297                return None;
298            }
299            Some((spec, self.groups(spec)))
300        })
301    }
302}